From ee1dca77c7b5f64c4c19e54c61567a42bb0f8b0e Mon Sep 17 00:00:00 2001 From: "IQ.Lvbs CI [bot]" Date: Fri, 28 Aug 2026 08:35:52 -0500 Subject: [PATCH] IQ.Pilot Release Commit @ d2ce8a8 --- .gitignore | 10 + artifacts/iqpilot_alc_private/.source_sha | 2 +- artifacts/iqpilot_alc_private/manifest.json | 12 +- .../iqpilot_alc_private/manifest.json.sig | 2 +- .../alc.cpython-312-aarch64-linux-gnu.so | Bin 402072 -> 402144 bytes ...cle_state.cpython-312-aarch64-linux-gnu.so | Bin 69688 -> 69712 bytes artifacts/iqpilot_emac_private/manifest.json | 561 +++++++ .../iqpilot_emac_private/manifest.json.sig | 1 + .../python/_iqclosure/_vendored.json | 84 + .../python/_iqclosure/iqpilot/__init__.py | 0 .../_iqclosure/iqpilot/common/__init__.py | 0 .../_iqclosure/iqpilot/common/api/__init__.py | 26 + .../_iqclosure/iqpilot/common/api/base.py | 84 + .../iqpilot/common/api/comma_connect.py | 11 + .../_iqclosure/iqpilot/common/atlas_alerts.py | 281 ++++ .../_iqclosure/iqpilot/common/basedir.py | 4 + .../_iqclosure/iqpilot/common/constants.py | 23 + .../_iqclosure/iqpilot/common/file_chunker.py | 55 + .../iqpilot/common/filter_simple.py | 71 + .../python/_iqclosure/iqpilot/common/gpio.py | 89 + .../python/_iqclosure/iqpilot/common/i2c.py | 81 + .../_iqclosure/iqpilot/common/iq_perf.py | 187 +++ .../_iqclosure/iqpilot/common/issue_debug.py | 44 + .../iqpilot/common/logging_extra.py | 249 +++ .../_iqclosure/iqpilot/common/realtime.py | 134 ++ .../_iqclosure/iqpilot/common/spinner.py | 52 + .../_iqclosure/iqpilot/common/steer_delay.py | 60 + .../_iqclosure/iqpilot/common/swaglog.py | 165 ++ .../_iqclosure/iqpilot/common/time_helpers.py | 15 + .../python/_iqclosure/iqpilot/common/utils.py | 271 +++ .../_iqclosure/iqpilot/konn3kt/__init__.py | 0 .../iqpilot/konn3kt/cloud_client.py | 17 + .../iqpilot/konn3kt/registration.py | 226 +++ .../_iqclosure/iqpilot/selfdrive/__init__.py | 0 .../iqpilot/selfdrive/controls/__init__.py | 0 .../selfdrive/controls/lib/__init__.py | 0 .../selfdrive/controls/lib/desire_helper.py | 286 ++++ .../selfdrive/controls/lib/drive_helpers.py | 80 + .../controls/lib/helpers/__init__.py | 0 .../controls/lib/helpers/lane_change.py | 293 ++++ .../controls/lib/helpers/lane_turn.py | 157 ++ .../iqpilot/selfdrive/iqmodeld/__init__.py | 0 .../iqpilot/selfdrive/iqmodeld/big_catalog.py | 9 + .../iqpilot/selfdrive/iqmodeld/camera.py | 70 + .../iqpilot/selfdrive/iqmodeld/config.py | 133 ++ .../iqpilot/selfdrive/iqmodeld/daemon.py | 766 +++++++++ .../selfdrive/iqmodeld/driving_action.py | 46 + .../selfdrive/iqmodeld/egpu_helpers.py | 158 ++ .../iqpilot/selfdrive/iqmodeld/egpu_model.py | 9 + .../selfdrive/iqmodeld/emac_model_meta.py | 9 + .../iqpilot/selfdrive/iqmodeld/messaging.py | 269 +++ .../iqpilot/selfdrive/iqmodeld/metadata.py | 96 ++ .../selfdrive/iqmodeld/model_channel.py | 58 + .../iqpilot/selfdrive/iqmodeld/model_warp.py | 80 + .../selfdrive/iqmodeld/models/__init__.py | 3 + .../iqmodeld/models/combined_artifact.py | 100 ++ .../selfdrive/iqmodeld/models/fetcher.py | 11 + .../selfdrive/iqmodeld/models/helpers.py | 313 ++++ .../iqmodeld/models/inference_state.py | 9 + .../iqmodeld/models/runners/__init__.py | 0 .../iqmodeld/models/runners/model_runner.py | 231 +++ .../models/runners/tinygrad/__init__.py | 0 .../runners/tinygrad/combined_split_runner.py | 245 +++ .../models/runners/tinygrad/fused_runner.py | 178 ++ .../models/runners/tinygrad/model_types.py | 60 + .../runners/tinygrad/supercombo_runner.py | 339 ++++ .../runners/tinygrad/tinygrad_runner.py | 190 +++ .../iqmodeld/models/split_model_constants.py | 89 + .../iqpilot/selfdrive/iqmodeld/parser.py | 219 +++ .../selfdrive/iqmodeld/runtime/__init__.py | 0 .../selfdrive/iqmodeld/runtime/tinygrad.py | 23 + .../selfdrive/iqmodeld/temporal_state.py | 131 ++ .../selfdrive/iqmodeld/tools/__init__.py | 0 .../iqmodeld/tools/compile_supercombo.py | 422 +++++ .../selfdrive/iqmodeld/tools/compile_warp.py | 119 ++ .../iqpilot/selfdrive/locationd/__init__.py | 0 .../locationd/calibration_helpers.py | 32 + .../selfdrive/locationd/calibrationd.py | 367 ++++ .../iqpilot/selfdrive/locationd/helpers.py | 188 +++ .../iqpilot/selfdrive/locationd/locationd.py | 336 ++++ .../selfdrive/locationd/models/__init__.py | 0 .../selfdrive/locationd/models/constants.py | 88 + .../selfdrive/locationd/models/pose_kf.py | 67 + .../iqpilot/selfdrive/selfdrived/__init__.py | 0 .../selfdrive/selfdrived/alertmanager.py | 68 + .../iqpilot/selfdrive/selfdrived/events.py | 991 +++++++++++ .../selfdrive/state_estimation/__init__.py | 7 + .../selfdrive/state_estimation/estimator.py | 311 ++++ .../iqpilot/selfdrive/ui/__init__.py | 0 .../iqpilot/selfdrive/ui/feedback/__init__.py | 0 .../selfdrive/ui/feedback/feedbackd.py | 70 + .../_iqclosure/iqpilot/system/__init__.py | 0 .../iqpilot/system/camerad/__init__.py | 0 .../system/camerad/cameras/__init__.py | 0 .../system/camerad/cameras/nv12_info.py | 21 + .../iqpilot/system/hardware/__init__.py | 22 + .../iqpilot/system/hardware/base.py | 228 +++ .../_iqclosure/iqpilot/system/hardware/hw.py | 137 ++ .../iqpilot/system/hardware/pc/__init__.py | 0 .../iqpilot/system/hardware/pc/hardware.py | 12 + .../iqpilot/system/hardware/tici/__init__.py | 0 .../iqpilot/system/hardware/tici/amplifier.py | 159 ++ .../iqpilot/system/hardware/tici/hardware.py | 740 ++++++++ .../iqpilot/system/hardware/tici/iwlist.py | 35 + .../iqpilot/system/hardware/tici/lpa.py | 1482 +++++++++++++++++ .../iqpilot/system/hardware/tici/pins.py | 30 + .../system/hardware/tici/usb_storage.py | 71 + .../_iqclosure/iqpilot/system/hardware/usb.py | 184 ++ .../python/_iqclosure/iqpilot/system/micd.py | 160 ++ .../_iqclosure/iqpilot/system/sentry.py | 171 ++ .../_iqclosure/iqpilot/system/version.py | 200 +++ .../python/iqpilot_private/__init__.py | 0 .../python/iqpilot_private/emac/__init__.py | 3 + ...transport.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 202152 bytes ...ac_client.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 267376 bytes ..._protocol.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 203544 bytes ...ciqmodeld.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 333336 bytes .../manifest.json | 38 +- .../manifest.json.sig | 2 +- .../__init__.cpython-312-aarch64-linux-gnu.so | Bin 67616 -> 67616 bytes ...g_catalog.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 133840 bytes ...gpu_model.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 68112 bytes ...odel_meta.cpython-312-aarch64-linux-gnu.so | Bin 0 -> 68264 bytes .../fetcher.cpython-312-aarch64-linux-gnu.so | Bin 136200 -> 136200 bytes .../git_auth.cpython-312-aarch64-linux-gnu.so | Bin 67864 -> 67864 bytes .../helpers.cpython-312-aarch64-linux-gnu.so | Bin 137944 -> 137944 bytes .../manager.cpython-312-aarch64-linux-gnu.so | Bin 340000 -> 340000 bytes .../iqdbc/iqdbc/car/hyundai/carcontroller.py | 8 +- .../iqdbc/iqdbc/car/hyundai/hyundaicanfd.py | 103 -- .../iqdbc/car/volkswagen/carcontroller.py | 27 +- .../iqdbc/iqdbc/car/volkswagen/mebcan.py | 6 +- .../volkswagen/tests/test_meb_nav_blinker.py | 69 +- .../car/volkswagen/tests/test_volkswagen.py | 1 + .../iqdbc/iqdbc/safety/modes/hyundai_canfd.h | 235 +-- .../iqdbc/safety/tests/test_hyundai_canfd.py | 37 +- .../safety/tests/test_hyundai_controller.py | 23 +- .../panda/board/drivers/can_common.h | 2 +- iqpilot/cereal/custom.capnp | 4 + iqpilot/cereal/log.capnp | 56 + iqpilot/cereal/services.py | 1 + iqpilot/common/params_keys.h | 35 + iqpilot/selfdrive/car/card.py | 3 + iqpilot/selfdrive/car/cruise.py | 35 + .../car/tests/test_speed_limit_set_speed.py | 59 + iqpilot/selfdrive/controls/controlsd.py | 5 +- iqpilot/selfdrive/controls/lib/slc_vcruise.py | 2 + .../controls/lib/speed_limit_controller.py | 46 +- .../lib/tests/test_slc_speed_limit_control.py | 202 +++ .../controls/steering_fault_recovery.py | 18 + .../tests/test_steering_fault_recovery.py | 28 + .../dmonitoringmodeld/dmonitoringmodeld.py | 7 + iqpilot/selfdrive/iqmodeld/big_catalog.py | 9 + iqpilot/selfdrive/iqmodeld/daemon.py | 45 +- iqpilot/selfdrive/iqmodeld/driving_action.py | 46 + iqpilot/selfdrive/iqmodeld/egpu_helpers.py | 158 ++ iqpilot/selfdrive/iqmodeld/egpu_model.py | 9 + iqpilot/selfdrive/iqmodeld/egpu_pipeline.py | 47 + iqpilot/selfdrive/iqmodeld/egpu_telemetry.py | 106 ++ .../selfdrive/iqmodeld/emac_input_state.py | 10 + iqpilot/selfdrive/iqmodeld/emac_model_meta.py | 9 + iqpilot/selfdrive/iqmodeld/iqegpumodeld.py | 377 +++++ iqpilot/selfdrive/iqmodeld/model_channel.py | 58 + iqpilot/selfdrive/iqmodeld/model_warp.py | 80 + iqpilot/selfdrive/iqmodeld/modeld_selector.py | 373 +++++ .../iqmodeld/models/runners/model_runner.py | 19 +- iqpilot/selfdrive/iqmodeld/temporal_state.py | 131 ++ .../iqmodeld/tests/test_egpu_stock_parity.py | 163 ++ .../iqmodeld/tests/test_egpu_worker.py | 509 ++++++ .../iqmodeld/tests/test_emac_input_state.py | 111 ++ .../iqmodeld/tests/test_lat_delay_source.py | 1 + .../tests/test_selector_version_gate.py | 31 + .../iqmodeld/tests/test_split_input_state.py | 131 ++ .../iqmodeld/tools/compile_egpu_model.py | 144 ++ .../iqmodeld/tools/compile_emac_warp.py | 9 + .../selfdrive/iqmodeld/tools/compile_warp.py | 119 ++ .../selfdrive/selfdrived/alerts_offroad.json | 28 + iqpilot/selfdrive/selfdrived/events.py | 9 + iqpilot/selfdrive/selfdrived/selfdrived.py | 25 + .../test/process_replay/migration.py | 4 +- .../test/process_replay/test_migration.py | 19 + iqpilot/system/hardware/egpu_dock/TESTING.md | 47 + iqpilot/system/hardware/egpu_dock/__init__.py | 0 .../system/hardware/egpu_dock/dock_probe.py | 46 + .../hardware/egpu_dock/firmware_wrapped.bin | Bin 0 -> 9207 bytes iqpilot/system/hardware/egpu_dock/flash.py | 617 +++++++ iqpilot/system/hardware/egpu_dock/status.py | 89 + iqpilot/system/hardware/fan_controller.py | 13 +- iqpilot/system/hardware/hardwared.py | 77 +- .../hardware/tests/test_egpu_dock_flash.py | 132 ++ .../hardware/tests/test_fan_controller.py | 76 +- .../system/hardware/tests/test_usb_state.py | 267 +++ .../system/hardware/tici/set_usb_storage.sh | 71 +- iqpilot/system/hardware/tici/usb_storage.py | 54 + iqpilot/system/hardware/usb.py | 184 ++ iqpilot/system/manager/manager.py | 10 + iqpilot/system/manager/process_config.py | 26 + iqpilot/tools/lib/url_file.py | 8 + iqpilot/tools/replay/camera.cc | 3 + iqpilot/tools/replay/camera.h | 1 + iqpilot/tools/replay/framereader.cc | 114 +- iqpilot/tools/replay/framereader.h | 4 +- iqpilot/tools/replay/main.cc | 10 + iqpilot/tools/replay/qcom_decoder.cc | 2 +- iqpilot/ui/layouts/settings/iq_panels.py | 69 +- iqpilot/ui/mici/onroad/emac_source.py | 43 + iqpilot/ui/mici/onroad/hud_renderer.py | 3 +- iqpilot/ui/onroad/big_model_status.py | 71 + iqpilot/ui/onroad/emac_status.py | 44 + iqpilot/ui/onroad/hud_renderer.py | 4 + iqpilot/ui/tests/test_big_model_status.py | 66 + 210 files changed, 19726 insertions(+), 455 deletions(-) create mode 100644 artifacts/iqpilot_emac_private/manifest.json create mode 100644 artifacts/iqpilot_emac_private/manifest.json.sig create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/_vendored.json create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/atlas_alerts.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/basedir.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/constants.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/file_chunker.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/filter_simple.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/gpio.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/i2c.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/iq_perf.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/issue_debug.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/logging_extra.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/steer_delay.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/swaglog.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/time_helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/big_catalog.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/camera.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/config.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/emac_model_meta.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/messaging.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/metadata.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_warp.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/inference_state.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/model_types.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/parser.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/temporal_state.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibration_helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/helpers.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/locationd.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/constants.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/pose_kf.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/estimator.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/nv12_info.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/base.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/hw.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/usb_storage.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/usb.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/micd.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/sentry.py create mode 100644 artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/version.py create mode 100644 artifacts/iqpilot_emac_private/python/iqpilot_private/__init__.py create mode 100644 artifacts/iqpilot_emac_private/python/iqpilot_private/emac/__init__.py create mode 100755 artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so create mode 100755 artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so create mode 100644 iqpilot/selfdrive/controls/steering_fault_recovery.py create mode 100644 iqpilot/selfdrive/controls/tests/test_steering_fault_recovery.py create mode 100644 iqpilot/selfdrive/iqmodeld/big_catalog.py create mode 100644 iqpilot/selfdrive/iqmodeld/driving_action.py create mode 100644 iqpilot/selfdrive/iqmodeld/egpu_helpers.py create mode 100644 iqpilot/selfdrive/iqmodeld/egpu_model.py create mode 100644 iqpilot/selfdrive/iqmodeld/egpu_pipeline.py create mode 100644 iqpilot/selfdrive/iqmodeld/egpu_telemetry.py create mode 100644 iqpilot/selfdrive/iqmodeld/emac_input_state.py create mode 100644 iqpilot/selfdrive/iqmodeld/emac_model_meta.py create mode 100644 iqpilot/selfdrive/iqmodeld/iqegpumodeld.py create mode 100644 iqpilot/selfdrive/iqmodeld/model_channel.py create mode 100644 iqpilot/selfdrive/iqmodeld/model_warp.py create mode 100644 iqpilot/selfdrive/iqmodeld/modeld_selector.py create mode 100644 iqpilot/selfdrive/iqmodeld/temporal_state.py create mode 100644 iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py create mode 100644 iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py create mode 100644 iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py create mode 100644 iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py create mode 100644 iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py create mode 100644 iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py create mode 100644 iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py create mode 100644 iqpilot/selfdrive/iqmodeld/tools/compile_warp.py create mode 100644 iqpilot/selfdrive/test/process_replay/test_migration.py create mode 100644 iqpilot/system/hardware/egpu_dock/TESTING.md create mode 100644 iqpilot/system/hardware/egpu_dock/__init__.py create mode 100644 iqpilot/system/hardware/egpu_dock/dock_probe.py create mode 100644 iqpilot/system/hardware/egpu_dock/firmware_wrapped.bin create mode 100644 iqpilot/system/hardware/egpu_dock/flash.py create mode 100644 iqpilot/system/hardware/egpu_dock/status.py create mode 100644 iqpilot/system/hardware/tests/test_egpu_dock_flash.py create mode 100644 iqpilot/system/hardware/tests/test_usb_state.py create mode 100644 iqpilot/system/hardware/usb.py create mode 100644 iqpilot/ui/mici/onroad/emac_source.py create mode 100644 iqpilot/ui/onroad/big_model_status.py create mode 100644 iqpilot/ui/onroad/emac_status.py create mode 100644 iqpilot/ui/tests/test_big_model_status.py diff --git a/.gitignore b/.gitignore index d1af22dbb..64090c18d 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,7 @@ TASK.md /iqpilot/selfdrive/iqlocd/models/generated/car.cpp /iqpilot/selfdrive/iqlocd/models/generated/car.h /iqpilot/tools/scripts/debug/ +/iqpilot/tools/scripts/plans/ !scripts/iqpilot/iqos_usrlocal_larch64.tar.zst # pretty-build capture logs (model compile / mpc + dbc codegen stdout+stderr) @@ -182,3 +183,12 @@ iqpilot/selfdrive/ui/tests/test_ui/nav_demo_report/ # fetcher: closed-source precompiled component — ship only the obfuscated .so /iqpilot/models_private_src/fetcher.py + +# build output: a signed .app is a 233-file sealed bundle, never track a subset +iqpilot/tools/iqmacd/macos/dist/IQ eMac.app/ +iqpilot/tools/iqmacd/macos/dist/.stage/ +iqpilot/tools/iqmacd/macos/dist/.DS_Store + +iqpilot/tools/iqmacd/macos/dist/ + +iqpilot/tools/scripts/iqemac/clips/README.md diff --git a/artifacts/iqpilot_alc_private/.source_sha b/artifacts/iqpilot_alc_private/.source_sha index 499bd083d..b3535496a 100644 --- a/artifacts/iqpilot_alc_private/.source_sha +++ b/artifacts/iqpilot_alc_private/.source_sha @@ -1 +1 @@ -0a3bb124deb8a9fdc8cea76764c690c7673a597f +076c1d057f97b067222d74180fbb63ba5467254a diff --git a/artifacts/iqpilot_alc_private/manifest.json b/artifacts/iqpilot_alc_private/manifest.json index 26f645a56..c1f1617d3 100644 --- a/artifacts/iqpilot_alc_private/manifest.json +++ b/artifacts/iqpilot_alc_private/manifest.json @@ -21,13 +21,13 @@ }, "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "4d3eb2b63aa962bd764a9ee0086d3936846fb34a255321f7e95594412856f0fd", - "size": 402072 + "sha256": "268267fdfb6ae438f1b3c6f5f2c378a5b878e68357d5560f604f77d5c36a2cea", + "size": 402144 }, "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so": { "mode": 493, - "sha256": "8e6829e7d8828e76258eba191144d70ae5b42dec620608dcf65431db25c0670f", - "size": 69688 + "sha256": "b3d33e86493408e5a02f2df753e1b257f003e5028480b5b040aa5a07eeed98be", + "size": 69712 }, "runtime": { "entries": { @@ -42,7 +42,7 @@ } }, "signatures": { - "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so": "c/cZ53FdLJJLrMc8J40fW/CFkByPO0Dt2AMFIOY1AiT8/09piyULhhWAytgNaIejk8BP0aQhlk5xfaivI3bKCw==", - "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so": "bbcjBEVBBQskXGTqTTQmLAdjcgtoL45mwGhRUwchrEkMNNeMJrvSCQ7ZXf8KpNUgFaiSU7L/5tnqHsrtKvU8Dw==" + "python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so": "v0mM7HrM8Ssbm3E/TxIUefAh5aCkzbZIKDQk5yFN6kr4EgqmVyPMNkqu7kCsn4ah0fTzr7xCrhs/gXjGf+6yBQ==", + "python/iqpilot_private/konn3kt/iqlvbs/vehicle_state.cpython-312-aarch64-linux-gnu.so": "5KQhJ61bt8tNmKuegPVMnMiqEHKcnjhqMtCDv2CphlSS54L/GmsOZjieHQEcgpOIDw7n/7qz2k6iFnwJ6clLCg==" } } diff --git a/artifacts/iqpilot_alc_private/manifest.json.sig b/artifacts/iqpilot_alc_private/manifest.json.sig index f7893e093..d514c4fa7 100644 --- a/artifacts/iqpilot_alc_private/manifest.json.sig +++ b/artifacts/iqpilot_alc_private/manifest.json.sig @@ -1 +1 @@ -M1n62PoNdW9Odr9dhsZgHsKupcmVorFMp8QU14sERQn2XkFWjOGDfjUX018m7IWOdxtbciDzTzHYOIHkBxqVAg== +Xr+Nn9b/ZoT2/S94B/I16aHVQI5dZZE3hGvpS6elgasC7e4JZouZvln/ue3Lh5Hg/NTJCCEStRmpEtNfgcjbAA== diff --git a/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_alc_private/python/iqpilot_private/konn3kt/iqlvbs/alc.cpython-312-aarch64-linux-gnu.so index c69dbf237165580192d302de82276d720528c1b8..6ed51787ee88cb0d773275c2214d9b22aa81d10b 100755 GIT binary patch literal 402144 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 z%0D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
H|_5O!> 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ⅇ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 literal 69688 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 tuple[str, str, str] | tuple[None, None, None]: + return iqpilot.common.api.comma_connect.CommaConnectApi(None).get_key_pair() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py new file mode 100644 index 000000000..8170a19b2 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/base.py @@ -0,0 +1,84 @@ +import jwt +import os +import requests +import unicodedata +from datetime import datetime, timedelta, UTC +from functools import lru_cache +from iqpilot.system.hardware.hw import Paths +from iqpilot.system.version import get_version + +# name: jwt signature algorithm +KEYS = {"id_rsa": "RS256", + "id_ecdsa": "ES256"} + + +@lru_cache(maxsize=4) +def load_signing_key(private_key: str): + # PyJWT re-parses a PEM string on every encode; an RSA parse is ~40ms, so cache the key object + try: + from cryptography.hazmat.primitives.serialization import load_pem_private_key + return load_pem_private_key(private_key.encode(), password=None) + except Exception: + return private_key + + +class BaseApi: + def __init__(self, dongle_id, api_host, user_agent="openpilot-"): + self.dongle_id = dongle_id + self.api_host = api_host + self.user_agent = user_agent + self.jwt_algorithm, self.private_key, _ = self.get_key_pair() + + def get(self, *args, **kwargs): + return self.request('GET', *args, **kwargs) + + def post(self, *args, **kwargs): + return self.request('POST', *args, **kwargs) + + def request(self, method, endpoint, timeout=None, access_token=None, **params): + return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params) + + def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload): + now = datetime.now(UTC).replace(tzinfo=None) + payload = { + 'identity': self.dongle_id, + 'nbf': now, + 'iat': now, + 'exp': now + timedelta(hours=expiry_hours), + **extra_payload + } + if payload_extra is not None: + payload.update(payload_extra) + key = load_signing_key(self.private_key) if self.private_key else self.private_key + token = jwt.encode(payload, key, algorithm=self.jwt_algorithm) + if isinstance(token, bytes): + token = token.decode('utf8') + return token + + def get_token(self, payload_extra=None, expiry_hours=1): + return self._get_token(payload_extra, expiry_hours) + + def remove_non_ascii_chars(self, text): + normalized_text = unicodedata.normalize('NFD', text) + ascii_encoded_text = normalized_text.encode('ascii', 'ignore') + return ascii_encoded_text.decode() + + def api_get(self, endpoint, method='GET', timeout=None, access_token=None, session=None, json=None, **params): + headers = {} + if access_token is not None: + headers['Authorization'] = "JWT " + access_token + + version = self.remove_non_ascii_chars(get_version()) + headers['User-Agent'] = self.user_agent + version + + # TODO: add session to Api + req = requests if session is None else session + return req.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params) + + @staticmethod + def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]: + for key in KEYS: + if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'): + with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public: + return KEYS[key], private.read(), public.read() + return None, None, None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py new file mode 100644 index 000000000..bc16d40eb --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/api/comma_connect.py @@ -0,0 +1,11 @@ +import os + +from iqpilot.common.api.base import BaseApi + +API_HOST = os.getenv('API_HOST', 'https://api-iqlabs.konn3kt.com') + + +class CommaConnectApi(BaseApi): + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "openpilot-" diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/atlas_alerts.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/atlas_alerts.py new file mode 100644 index 000000000..c261db844 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/atlas_alerts.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from bisect import insort +from collections.abc import Callable, Iterable +from dataclasses import dataclass, field +from enum import IntEnum + +import iqpilot.cereal.messaging as messaging +from iqpilot.cereal import car, log +from iqpilot.common.realtime import DT_CTRL +from iqpilot.system.hardware import HARDWARE + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert + + +def _frames_for(seconds: float) -> int: + return int(seconds / DT_CTRL) + + +class Tier(IntEnum): + LOWEST = 0 + LOWER = 1 + LOW = 2 + MID = 3 + HIGH = 4 + HIGHEST = 5 + + +class Tags: + ENABLE = "enable" + PRE_ENABLE = "preEnable" + OVERRIDE_LATERAL = "overrideLateral" + OVERRIDE_LONGITUDINAL = "overrideLongitudinal" + NO_ENTRY = "noEntry" + WARNING = "warning" + USER_DISABLE = "userDisable" + SOFT_DISABLE = "softDisable" + IMMEDIATE_DISABLE = "immediateDisable" + PERMANENT = "permanent" + + +@dataclass(slots=True) +class AlertCard: + alert_text_1: str + alert_text_2: str + alert_status: log.SelfdriveState.AlertStatus + alert_size: log.SelfdriveState.AlertSize + priority: Tier + visual_alert: car.CarControl.HUDControl.VisualAlert + audible_alert: car.CarControl.HUDControl.AudibleAlert + duration: int + creation_delay: float = 0.0 + alert_type: str = field(default="", init=False) + event_type: str | None = field(default=None, init=False) + + def __init__(self, + alert_text_1: str, + alert_text_2: str, + alert_status: log.SelfdriveState.AlertStatus, + alert_size: log.SelfdriveState.AlertSize, + priority: Tier, + visual_alert: car.CarControl.HUDControl.VisualAlert, + audible_alert: car.CarControl.HUDControl.AudibleAlert, + duration: float, + creation_delay: float = 0.0): + self.alert_text_1 = alert_text_1 + self.alert_text_2 = alert_text_2 + self.alert_status = alert_status + self.alert_size = alert_size + self.priority = priority + self.visual_alert = visual_alert + self.audible_alert = audible_alert + self.duration = _frames_for(duration) + self.creation_delay = creation_delay + self.alert_type = "" + self.event_type = None + + def __str__(self) -> str: + return f"{self.alert_text_1}/{self.alert_text_2} {self.priority} {self.visual_alert} {self.audible_alert}" + + +AlertFactory = Callable[[car.CarParams, car.CarState, messaging.SubMaster, bool, int, log.ControlsState], AlertCard] + + +def car_mode_entry_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> AlertCard: + del CS, sm, metric, soft_disable_time, personality + headline = "Enable Adaptive Cruise to Engage" + if CP.brand == "honda": + headline = "Enable Main Switch to Engage" + return NoEntryCard(headline) + + +class EventBook(ABC): + def __init__(self): + self._live_names: list[int] = [] + self._latched_names: list[int] = [] + self.event_counters: dict[int, int] = {} + + @property + def events(self) -> list[int]: + return self._live_names + + @events.setter + def events(self, values: list[int]) -> None: + self._live_names = values + + @property + def static_events(self) -> list[int]: + return self._latched_names + + @static_events.setter + def static_events(self, values: list[int]) -> None: + self._latched_names = values + + @property + def names(self) -> list[int]: + return list(self._live_names) + + def __len__(self) -> int: + return len(self._live_names) + + def add(self, event_name: int, static: bool = False) -> None: + if static: + insort(self._latched_names, event_name) + insort(self._live_names, event_name) + + def clear(self) -> None: + refreshed: dict[int, int] = {} + for event_name, frames_seen in self.event_counters.items(): + refreshed[event_name] = frames_seen + 1 if event_name in self._live_names else 0 + self.event_counters = refreshed + self._live_names = list(self._latched_names) + + def contains(self, event_type: str) -> bool: + board = self.get_events_mapping() + return any(event_type in board.get(event_name, {}) for event_name in self._live_names) + + def has(self, event_name: int) -> bool: + return event_name in self._live_names + + def contains_in_list(self, events_list: list[int]) -> bool: + return any(event_name in self._live_names for event_name in events_list) + + def remove(self, event_name: int, static: bool = False) -> None: + if static and event_name in self._latched_names: + self._latched_names.remove(event_name) + + if event_name in self._live_names: + self.event_counters[event_name] = self.event_counters.get(event_name, 0) + 1 + self._live_names.remove(event_name) + + def add_from_msg(self, events: Iterable) -> None: + for event in events: + insort(self._live_names, event.name.raw) + + def to_msg(self): + board = self.get_events_mapping() + outbound = [] + for event_name in self._live_names: + msg = self.get_event_msg_type().new_message() + msg.name = event_name + for event_kind in board.get(event_name, {}): + setattr(msg, event_kind, True) + outbound.append(msg) + return outbound + + def create_alerts(self, event_types: list[str], callback_args=None): + callback_args = [] if callback_args is None else callback_args + board = self.get_events_mapping() + spawned: list[AlertCard] = [] + for event_name in self._live_names: + variants = board.get(event_name, {}) + for event_type in event_types: + chosen = variants.get(event_type) + if chosen is None: + continue + alert = self._realize(chosen, callback_args) + age_frames = self.event_counters.get(event_name, 0) + 1 + if age_frames * DT_CTRL < alert.creation_delay: + continue + alert.alert_type = f"{self.get_event_name(event_name)}/{event_type}" + alert.event_type = event_type + spawned.append(alert) + return spawned + + @staticmethod + def _realize(candidate: AlertCard | AlertFactory, callback_args: list) -> AlertCard: + return candidate if isinstance(candidate, AlertCard) else candidate(*callback_args) + + @abstractmethod + def get_events_mapping(self) -> dict[int, dict[str, AlertCard | AlertFactory]]: + raise NotImplementedError + + @abstractmethod + def get_event_name(self, event: int) -> str: + raise NotImplementedError + + @abstractmethod + def get_event_msg_type(self): + raise NotImplementedError + + +def _mici_reframe(primary: str, secondary: str) -> tuple[str, str, log.SelfdriveState.AlertSize]: + if HARDWARE.get_device_type() == "mici": + return secondary, primary, AlertSize.small + return primary, secondary, AlertSize.mid + + +class NoEntryCard(AlertCard): + def __init__(self, + alert_text_2: str, + alert_text_1: str = "IQ.Pilot Unavailable", + visual_alert: car.CarControl.HUDControl.VisualAlert = VisualAlert.none, + priority: Tier = Tier.LOW): + primary, secondary, size = _mici_reframe(alert_text_1, alert_text_2) + super().__init__(primary, secondary, AlertStatus.normal, size, priority, visual_alert, AudibleAlert.refuse, 3.0) + + +class GentleDisableCard(AlertCard): + def __init__(self, alert_text_2: str): + super().__init__( + "TAKE CONTROL IMMEDIATELY", + alert_text_2, + AlertStatus.userPrompt, + AlertSize.full, + Tier.MID, + VisualAlert.steerRequired, + AudibleAlert.warningSoft, + 2.0, + ) + + +class PendingDisableCard(GentleDisableCard): + def __init__(self, alert_text_2: str): + super().__init__(alert_text_2) + self.alert_text_1 = "IQ.Pilot will disengage" + + +class HardDisableCard(AlertCard): + def __init__(self, alert_text_2: str): + super().__init__( + "TAKE CONTROL IMMEDIATELY", + alert_text_2, + AlertStatus.critical, + AlertSize.full, + Tier.HIGHEST, + VisualAlert.steerRequired, + AudibleAlert.warningImmediate, + 4.0, + ) + + +class ChimeCard(AlertCard): + def __init__(self, audible_alert: car.CarControl.HUDControl.AudibleAlert): + super().__init__("", "", AlertStatus.normal, AlertSize.none, Tier.MID, VisualAlert.none, audible_alert, 0.2) + + +class BannerCard(AlertCard): + def __init__(self, alert_text_1: str, alert_text_2: str = "", duration: float = 0.2, priority: Tier = Tier.LOWER, creation_delay: float = 0.0): + size = AlertSize.mid if alert_text_2 else AlertSize.small + super().__init__(alert_text_1, alert_text_2, AlertStatus.normal, size, priority, VisualAlert.none, AudibleAlert.none, duration, creation_delay) + + +class BootCard(AlertCard): + def __init__(self, alert_text_1: str, alert_text_2: str = "Always keep hands on wheel and eyes on road", alert_status=AlertStatus.normal): + if HARDWARE.get_device_type() == "mici": + compact_secondary = "" if alert_text_2 == "Always keep hands on wheel and eyes on road" else alert_text_2 + super().__init__(alert_text_1, compact_secondary, alert_status, AlertSize.small, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0) + else: + super().__init__(alert_text_1, alert_text_2, alert_status, AlertSize.mid, Tier.LOWER, VisualAlert.none, AudibleAlert.none, 5.0) + + +class AlertBase(AlertCard): + pass + + +NULL_ALERT = AlertCard("", "", AlertStatus.normal, AlertSize.none, Tier.LOWEST, VisualAlert.none, AudibleAlert.none, 0.0) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/basedir.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/basedir.py new file mode 100644 index 000000000..49b092d93 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/basedir.py @@ -0,0 +1,4 @@ +import os + + +BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../..")) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/constants.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/constants.py new file mode 100644 index 000000000..7ca425c4b --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/constants.py @@ -0,0 +1,23 @@ +import numpy as np + +# conversions +class CV: + # Speed + MPH_TO_KPH = 1.609344 + KPH_TO_MPH = 1. / MPH_TO_KPH + MS_TO_KPH = 3.6 + KPH_TO_MS = 1. / MS_TO_KPH + MS_TO_MPH = MS_TO_KPH * KPH_TO_MPH + MPH_TO_MS = MPH_TO_KPH * KPH_TO_MS + MS_TO_KNOTS = 1.9438 + KNOTS_TO_MS = 1. / MS_TO_KNOTS + + # Angle + DEG_TO_RAD = np.pi / 180. + RAD_TO_DEG = 1. / DEG_TO_RAD + + # Mass + LB_TO_KG = 0.453592 + + +ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/file_chunker.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/file_chunker.py new file mode 100644 index 000000000..57dfc3553 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/file_chunker.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +import sys +import math +import os +from pathlib import Path + +CHUNK_SIZE = 45 * 1024 * 1024 # 45MB, under GitHub's 50MB limit + +def get_chunk_name(name, idx, num_chunks): + return f"{name}.chunk{idx+1:02d}of{num_chunks:02d}" + +def get_manifest_path(name): + return f"{name}.chunkmanifest" + +def _chunk_paths(path, num_chunks): + return [get_manifest_path(path)] + [get_chunk_name(path, i, num_chunks) for i in range(num_chunks)] + +def get_chunk_targets(path, file_size): + num_chunks = math.ceil(file_size / CHUNK_SIZE) + return _chunk_paths(path, num_chunks) + +def chunk_file(path, targets): + manifest_path, *chunk_paths = targets + with open(path, 'rb') as f: + data = f.read() + actual_num_chunks = max(1, math.ceil(len(data) / CHUNK_SIZE)) + assert len(chunk_paths) >= actual_num_chunks, f"Allowed {len(chunk_paths)} chunks but needs at least {actual_num_chunks}, for path {path}" + for i, chunk_path in enumerate(chunk_paths): + with open(chunk_path, 'wb') as f: + f.write(data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]) + Path(manifest_path).write_text(str(len(chunk_paths))) + os.remove(path) + +def get_existing_chunks(path): + if os.path.isfile(path): + return [path] + if os.path.isfile(manifest := get_manifest_path(path)): + num_chunks = int(Path(manifest).read_text().strip()) + return _chunk_paths(path, num_chunks) + raise FileNotFoundError(path) + +def read_file_chunked(path): + manifest_path = get_manifest_path(path) + if os.path.isfile(manifest_path): + num_chunks = int(Path(manifest_path).read_text().strip()) + return b''.join(Path(get_chunk_name(path, i, num_chunks)).read_bytes() for i in range(num_chunks)) + if os.path.isfile(path): + return Path(path).read_bytes() + raise FileNotFoundError(path) + + +if __name__ == "__main__": + path = sys.argv[1] + chunk_paths = get_chunk_targets(path, os.path.getsize(path)) + chunk_file(path, chunk_paths) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/filter_simple.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/filter_simple.py new file mode 100644 index 000000000..30839f66e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/filter_simple.py @@ -0,0 +1,71 @@ +from collections import deque + +import numpy as np + + +class FirstOrderFilter: + def __init__(self, x0, rc, dt, initialized=True): + self.x = x0 + self.dt = dt + self.update_alpha(rc) + self.initialized = initialized + + def update_alpha(self, rc): + self.alpha = self.dt / (rc + self.dt) + + def update(self, x): + if self.initialized: + self.x = (1. - self.alpha) * self.x + self.alpha * x + else: + self.initialized = True + self.x = x + return self.x + + +class BounceFilter(FirstOrderFilter): + def __init__(self, x0, rc, dt, initialized=True, bounce=2): + self.velocity = FirstOrderFilter(0.0, 0.15, dt) + self.bounce = bounce + super().__init__(x0, rc, dt, initialized) + + def update(self, x): + super().update(x) + scale = self.dt / (1.0 / 60.0) # tuned at 60 fps + self.velocity.x += (x - self.x) * self.bounce * scale * self.dt + self.velocity.update(0.0) + if abs(self.velocity.x) < 1e-5: + self.velocity.x = 0.0 + self.x += self.velocity.x + return self.x + + +class MyMovingAverage: + def __init__(self, window_size, value=None): + self.window_size = window_size + if value is not None: + self.values = deque([value] * window_size, maxlen=window_size) + self.sum = value * window_size + self.result = value + else: + self.values = deque(maxlen=window_size) + self.sum = 0 + self.result = 0 + + def set(self, value): + self.values.clear() + self.values.append(value) + self.sum = value + self.result = value + return value + + def set_all(self, value): + self.values = deque([value] * self.window_size, maxlen=self.window_size) + self.sum = value * self.window_size + self.result = value + return value + + def process(self, value, median=False): + self.values.append(value) + self.sum = sum(self.values) + self.result = float(np.median(self.values)) if median else float(self.sum) / len(self.values) + return self.result diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/gpio.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/gpio.py new file mode 100644 index 000000000..8f025a2da --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/gpio.py @@ -0,0 +1,89 @@ +import os +import fcntl +import ctypes +from functools import cache + +def gpio_init(pin: int, output: bool) -> None: + try: + with open(f"/sys/class/gpio/gpio{pin}/direction", 'wb') as f: + f.write(b"out" if output else b"in") + except Exception as e: + print(f"Failed to set gpio {pin} direction: {e}") + +def gpio_set(pin: int, high: bool) -> None: + try: + with open(f"/sys/class/gpio/gpio{pin}/value", 'wb') as f: + f.write(b"1" if high else b"0") + except Exception as e: + print(f"Failed to set gpio {pin} value: {e}") + +def gpio_read(pin: int) -> bool | None: + val = None + try: + with open(f"/sys/class/gpio/gpio{pin}/value", 'rb') as f: + val = bool(int(f.read().strip())) + except Exception as e: + print(f"Failed to set gpio {pin} value: {e}") + + return val + +def gpio_export(pin: int) -> None: + if os.path.isdir(f"/sys/class/gpio/gpio{pin}"): + return + + try: + with open("/sys/class/gpio/export", 'w') as f: + f.write(str(pin)) + except Exception: + print(f"Failed to export gpio {pin}") + +@cache +def get_irq_action(irq: int) -> list[str]: + try: + with open(f"/sys/kernel/irq/{irq}/actions") as f: + actions = f.read().strip().split(',') + return actions + except FileNotFoundError: + return [] + +def get_irqs_for_action(action: str) -> list[str]: + ret = [] + with open("/proc/interrupts") as f: + for l in f.readlines(): + irq = l.split(':')[0].strip() + if irq.isdigit() and action in get_irq_action(irq): + ret.append(irq) + return ret + +# *** gpiochip *** + +class gpioevent_data(ctypes.Structure): + _fields_ = [ + ("timestamp", ctypes.c_uint64), + ("id", ctypes.c_uint32), + ] + +class gpioevent_request(ctypes.Structure): + _fields_ = [ + ("lineoffset", ctypes.c_uint32), + ("handleflags", ctypes.c_uint32), + ("eventflags", ctypes.c_uint32), + ("label", ctypes.c_char * 32), + ("fd", ctypes.c_int) + ] + +def gpiochip_get_ro_value_fd(label: str, gpiochip_id: int, pin: int) -> int: + GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 + GPIOHANDLE_REQUEST_INPUT = 0x1 + GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 + + rq = gpioevent_request() + rq.lineoffset = pin + rq.handleflags = GPIOHANDLE_REQUEST_INPUT + rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES + rq.label = label.encode('utf-8')[:31] + b'\0' + + fd = os.open(f"/dev/gpiochip{gpiochip_id}", os.O_RDONLY) + fcntl.ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, rq) + os.close(fd) + return int(rq.fd) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/i2c.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/i2c.py new file mode 100644 index 000000000..1dfaa659a --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/i2c.py @@ -0,0 +1,81 @@ +import os +import fcntl +import ctypes + +# I2C constants from /usr/include/linux/i2c-dev.h +I2C_SLAVE = 0x0703 +I2C_SLAVE_FORCE = 0x0706 +I2C_SMBUS = 0x0720 + +# SMBus transfer types +I2C_SMBUS_READ = 1 +I2C_SMBUS_WRITE = 0 +I2C_SMBUS_BYTE_DATA = 2 +I2C_SMBUS_I2C_BLOCK_DATA = 8 + +I2C_SMBUS_BLOCK_MAX = 32 + + +class _I2cSmbusData(ctypes.Union): + _fields_ = [ + ("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("block", ctypes.c_uint8 * (I2C_SMBUS_BLOCK_MAX + 2)), + ] + + +class _I2cSmbusIoctlData(ctypes.Structure): + _fields_ = [ + ("read_write", ctypes.c_uint8), + ("command", ctypes.c_uint8), + ("size", ctypes.c_uint32), + ("data", ctypes.POINTER(_I2cSmbusData)), + ] + + +class SMBus: + def __init__(self, bus: int): + self._fd = os.open(f'/dev/i2c-{bus}', os.O_RDWR) + + def __enter__(self) -> 'SMBus': + return self + + def __exit__(self, *args) -> None: + self.close() + + def close(self) -> None: + if hasattr(self, '_fd') and self._fd >= 0: + os.close(self._fd) + self._fd = -1 + + def _set_address(self, addr: int, force: bool = False) -> None: + ioctl_arg = I2C_SLAVE_FORCE if force else I2C_SLAVE + fcntl.ioctl(self._fd, ioctl_arg, addr) + + def _smbus_access(self, read_write: int, command: int, size: int, data: _I2cSmbusData) -> None: + ioctl_data = _I2cSmbusIoctlData(read_write, command, size, ctypes.pointer(data)) + fcntl.ioctl(self._fd, I2C_SMBUS, ioctl_data) + + def read_byte_data(self, addr: int, register: int, force: bool = False) -> int: + self._set_address(addr, force) + data = _I2cSmbusData() + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_BYTE_DATA, data) + return int(data.byte) + + def write_byte_data(self, addr: int, register: int, value: int, force: bool = False) -> None: + self._set_address(addr, force) + data = _I2cSmbusData() + data.byte = value & 0xFF + self._smbus_access(I2C_SMBUS_WRITE, register, I2C_SMBUS_BYTE_DATA, data) + + def read_i2c_block_data(self, addr: int, register: int, length: int, force: bool = False) -> list[int]: + self._set_address(addr, force) + if not (0 <= length <= I2C_SMBUS_BLOCK_MAX): + raise ValueError(f"length must be 0..{I2C_SMBUS_BLOCK_MAX}") + + data = _I2cSmbusData() + data.block[0] = length + self._smbus_access(I2C_SMBUS_READ, register, I2C_SMBUS_I2C_BLOCK_DATA, data) + read_len = int(data.block[0]) or length + read_len = min(read_len, length) + return [int(b) for b in data.block[1 : read_len + 1]] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/iq_perf.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/iq_perf.py new file mode 100644 index 000000000..f36f5edef --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/iq_perf.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import time +from collections import deque +from dataclasses import dataclass +from typing import Any + +import iqpilot.cereal.messaging as messaging +from iqpilot.cereal import custom +from iqpilot.common.swaglog import cloudlog + + +TRACE_SERVICE = "iqPerfTrace" +MAX_TRACE_SAMPLES = 16 +_SHARED_PM: messaging.PubMaster | None = None + + +@dataclass(slots=True) +class PerfSample: + frame_id: int = 0 + loop_dt_us: int = 0 + update_us: int = 0 + state_control_us: int = 0 + publish_us: int = 0 + tail_work_us: int = 0 + rk_remaining_us: int = 0 + stale_carcontrol_us: int = 0 + stale_carcontrol_frames: int = 0 + sendcan_gap_us: int = 0 + model_eval_us: int = 0 + model_dropped_frames: int = 0 + model_backlog: int = 0 + texture_decode_us: int = 0 + texture_upload_us: int = 0 + texture_unload_us: int = 0 + texture_prune_us: int = 0 + texture_consume_us: int = 0 + texture_batch_size: int = 0 + texture_bytes: int = 0 + texture_cache_before: int = 0 + texture_cache_after: int = 0 + texture_unloaded: int = 0 + memory_usage_percent: int = 0 + gpu_usage_percent: int = 0 + cpu_usage_percent: int = 0 + flags: int = 0 + + +class PerfTraceRing: + def __init__(self, size: int = MAX_TRACE_SAMPLES): + self._samples: deque[PerfSample] = deque(maxlen=size) + + def push(self, sample: PerfSample) -> None: + self._samples.append(sample) + + def snapshot(self) -> list[PerfSample]: + return list(self._samples) + + +class PerfTraceEmitter: + _SEVERITY_MAP = { + "info": custom.IQPerfTrace.Severity.info, + "warning": custom.IQPerfTrace.Severity.warning, + "error": custom.IQPerfTrace.Severity.error, + "critical": custom.IQPerfTrace.Severity.critical, + } + + def __init__(self, process_name: str, pubmaster: messaging.PubMaster | None = None): + self.process_name = process_name + self._pm: messaging.PubMaster | None = pubmaster + self._last_emit_mono: dict[str, float] = {} + self._disabled = False + + def _pubmaster(self) -> messaging.PubMaster: + global _SHARED_PM + if self._pm is not None: + return self._pm + if _SHARED_PM is None: + _SHARED_PM = messaging.PubMaster([TRACE_SERVICE]) + self._pm = _SHARED_PM + return self._pm + + @staticmethod + def _clamp_uint(value: int, bits: int) -> int: + return max(0, min(value, (1 << bits) - 1)) + + @staticmethod + def _clamp_int(value: int, bits: int) -> int: + lo = -(1 << (bits - 1)) + hi = (1 << (bits - 1)) - 1 + return max(lo, min(value, hi)) + + def emit(self, event_class: str, *, + severity: str = "warning", + frame_id: int = 0, + total_time_us: int = 0, + rk_remaining_us: int = 0, + batch_size: int = 0, + dropped_frames: int = 0, + backlog: int = 0, + flags: int = 0, + samples: list[PerfSample] | None = None, + missing_services: list[str] | None = None, + top_processes: list[str] | None = None, + detail: str = "", + min_interval_s: float = 0.0, + mirror_cloudlog: bool = True) -> bool: + if self._disabled: + return False + now = time.monotonic() + last_emit = self._last_emit_mono.get(event_class, 0.0) + if min_interval_s > 0.0 and (now - last_emit) < min_interval_s: + return False + self._last_emit_mono[event_class] = now + + msg = messaging.new_message(TRACE_SERVICE) + trace = msg.iqPerfTrace + trace.process = self.process_name + trace.eventClass = event_class + trace.severity = self._SEVERITY_MAP.get(severity, custom.IQPerfTrace.Severity.warning) + trace.frameId = self._clamp_uint(int(frame_id), 32) + trace.totalTimeUs = self._clamp_uint(int(total_time_us), 32) + trace.rkRemainingUs = self._clamp_int(int(rk_remaining_us), 32) + trace.batchSize = self._clamp_uint(int(batch_size), 16) + trace.droppedFrames = self._clamp_uint(int(dropped_frames), 16) + trace.backlog = self._clamp_uint(int(backlog), 16) + trace.flags = self._clamp_uint(int(flags), 32) + trace.missingServices = list(missing_services or []) + trace.topProcesses = list(top_processes or []) + trace.detail = detail + + trace_samples = samples or [] + samples_builder = trace.init("samples", len(trace_samples)) + for i, sample in enumerate(trace_samples): + builder = samples_builder[i] + builder.frameId = self._clamp_uint(int(sample.frame_id), 32) + builder.loopDtUs = self._clamp_uint(int(sample.loop_dt_us), 32) + builder.updateUs = self._clamp_uint(int(sample.update_us), 32) + builder.stateControlUs = self._clamp_uint(int(sample.state_control_us), 32) + builder.publishUs = self._clamp_uint(int(sample.publish_us), 32) + builder.tailWorkUs = self._clamp_uint(int(sample.tail_work_us), 32) + builder.rkRemainingUs = self._clamp_int(int(sample.rk_remaining_us), 32) + builder.staleCarControlUs = self._clamp_uint(int(sample.stale_carcontrol_us), 32) + builder.staleCarControlFrames = self._clamp_uint(int(sample.stale_carcontrol_frames), 16) + builder.sendcanGapUs = self._clamp_uint(int(sample.sendcan_gap_us), 32) + builder.modelEvalUs = self._clamp_uint(int(sample.model_eval_us), 32) + builder.modelDroppedFrames = self._clamp_uint(int(sample.model_dropped_frames), 16) + builder.modelBacklog = self._clamp_uint(int(sample.model_backlog), 16) + builder.textureDecodeUs = self._clamp_uint(int(sample.texture_decode_us), 32) + builder.textureUploadUs = self._clamp_uint(int(sample.texture_upload_us), 32) + builder.textureUnloadUs = self._clamp_uint(int(sample.texture_unload_us), 32) + builder.texturePruneUs = self._clamp_uint(int(sample.texture_prune_us), 32) + builder.textureConsumeUs = self._clamp_uint(int(sample.texture_consume_us), 32) + builder.textureBatchSize = self._clamp_uint(int(sample.texture_batch_size), 16) + builder.textureBytes = self._clamp_uint(int(sample.texture_bytes), 32) + builder.textureCacheBefore = self._clamp_uint(int(sample.texture_cache_before), 16) + builder.textureCacheAfter = self._clamp_uint(int(sample.texture_cache_after), 16) + builder.textureUnloaded = self._clamp_uint(int(sample.texture_unloaded), 16) + builder.memoryUsagePercent = self._clamp_uint(int(sample.memory_usage_percent), 16) + builder.gpuUsagePercent = self._clamp_uint(int(sample.gpu_usage_percent), 16) + builder.cpuUsagePercent = self._clamp_uint(int(sample.cpu_usage_percent), 16) + builder.flags = self._clamp_uint(int(sample.flags), 32) + + try: + self._pubmaster().send(TRACE_SERVICE, msg) + except messaging.MultiplePublishersError: + self._disabled = True + cloudlog.error(f"iq_perf_trace disabled for {self.process_name}: duplicate publisher for {TRACE_SERVICE}") + return False + except Exception: + cloudlog.exception(f"iq_perf_trace publish failed for {self.process_name}") + return False + + if mirror_cloudlog: + cloudlog.event( + "iq_perf_trace", + process=self.process_name, + event_class=event_class, + severity=severity, + frame_id=int(frame_id), + total_time_us=int(total_time_us), + dropped_frames=int(dropped_frames), + flags=int(flags), + detail=detail, + ) + return True diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/issue_debug.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/issue_debug.py new file mode 100644 index 000000000..e8c517a7d --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/issue_debug.py @@ -0,0 +1,44 @@ +import os +import threading +import time +from datetime import datetime +from pathlib import Path + +from iqpilot.system.hardware import PC +from iqpilot.system.hardware.hw import Paths + + +DEBUG_FILENAME = "iqpilot_issue_debug.txt" +DEBUG_PATH = Path(Paths.comma_home()) / "community" / DEBUG_FILENAME if PC else Path("/data/community") / DEBUG_FILENAME + +_lock = threading.Lock() +_last_log_times: dict[str, float] = {} + + +def log_issue(tag: str, message: str) -> None: + try: + DEBUG_PATH.parent.mkdir(parents=True, exist_ok=True) + with _lock: + with open(DEBUG_PATH, "a", encoding="utf-8") as f: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + f.write(f"[{timestamp}] [{tag}] {message}\n") + except OSError: + pass + + +def log_issue_limited(key: str, tag: str, message: str, interval_sec: float = 1.0) -> None: + now = time.monotonic() + with _lock: + last = _last_log_times.get(key, 0.0) + if now - last < interval_sec: + return + _last_log_times[key] = now + + log_issue(tag, message) + + +def clear_issue_debug_log() -> None: + try: + os.remove(DEBUG_PATH) + except OSError: + pass diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/logging_extra.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/logging_extra.py new file mode 100644 index 000000000..ceaf083cd --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/logging_extra.py @@ -0,0 +1,249 @@ +import io +import os +import sys +import copy +import json +import time +import uuid +import socket +import logging +import traceback +import numpy as np +from threading import local +from collections import OrderedDict +from contextlib import contextmanager + +LOG_TIMESTAMPS = "LOG_TIMESTAMPS" in os.environ + +def json_handler(obj): + if isinstance(obj, np.bool_): + return bool(obj) + # if isinstance(obj, (datetime.date, datetime.time)): + # return obj.isoformat() + return repr(obj) + +def json_robust_dumps(obj): + return json.dumps(obj, default=json_handler) + +class NiceOrderedDict(OrderedDict): + def __str__(self): + return json_robust_dumps(self) + +class SwagFormatter(logging.Formatter): + def __init__(self, swaglogger): + logging.Formatter.__init__(self, None, '%a %b %d %H:%M:%S %Z %Y') + + self.swaglogger = swaglogger + self.host = socket.gethostname() + + def format_dict(self, record): + record_dict = NiceOrderedDict() + + if isinstance(record.msg, dict): + record_dict['msg'] = record.msg + else: + try: + record_dict['msg'] = record.getMessage() + except (ValueError, TypeError): + record_dict['msg'] = [record.msg]+record.args + + record_dict['ctx'] = self.swaglogger.get_ctx() + + if record.exc_info: + record_dict['exc_info'] = self.formatException(record.exc_info) + + record_dict['level'] = record.levelname + record_dict['levelnum'] = record.levelno + record_dict['name'] = record.name + record_dict['filename'] = record.filename + record_dict['lineno'] = record.lineno + record_dict['pathname'] = record.pathname + record_dict['module'] = record.module + record_dict['funcName'] = record.funcName + record_dict['host'] = self.host + record_dict['process'] = record.process + record_dict['thread'] = record.thread + record_dict['threadName'] = record.threadName + record_dict['created'] = record.created + + return record_dict + + def format(self, record): + if self.swaglogger is None: + raise Exception("must set swaglogger before calling format()") + return json_robust_dumps(self.format_dict(record)) + +class SwagLogFileFormatter(SwagFormatter): + def fix_kv(self, k, v): + # append type to names to preserve legacy naming in logs + # avoids overlapping key namespaces with different types + # e.g. log.info() creates 'msg' -> 'msg$s' + # log.event() creates 'msg.health.logMonoTime' -> 'msg.health.logMonoTime$i' + # because overlapping namespace 'msg' caused problems + if isinstance(v, (str, bytes)): + k += "$s" + elif isinstance(v, float): + k += "$f" + elif isinstance(v, bool): + k += "$b" + elif isinstance(v, int): + k += "$i" + elif isinstance(v, dict): + nv = {} + for ik, iv in v.items(): + ik, iv = self.fix_kv(ik, iv) + nv[ik] = iv + v = nv + elif isinstance(v, list): + k += "$a" + return k, v + + def format(self, record): + if isinstance(record, str): + v = json.loads(record) + else: + v = self.format_dict(record) + + mk, mv = self.fix_kv('msg', v['msg']) + del v['msg'] + v[mk] = mv + v['id'] = uuid.uuid4().hex + + return json_robust_dumps(v) + +class SwagErrorFilter(logging.Filter): + def filter(self, record): + return record.levelno < logging.ERROR + +def _tmpfunc(): + return 0 + +def _srcfile(): + return os.path.normcase(_tmpfunc.__code__.co_filename) + +class SwagLogger(logging.Logger): + def __init__(self): + logging.Logger.__init__(self, "swaglog") + + self.global_ctx = {} + + self.log_local = local() + self.log_local.ctx = {} + + def local_ctx(self): + try: + return self.log_local.ctx + except AttributeError: + self.log_local.ctx = {} + return self.log_local.ctx + + def get_ctx(self): + return dict(self.local_ctx(), **self.global_ctx) + + @contextmanager + def ctx(self, **kwargs): + old_ctx = self.local_ctx() + self.log_local.ctx = copy.copy(old_ctx) or {} + self.log_local.ctx.update(kwargs) + try: + yield + finally: + self.log_local.ctx = old_ctx + + def bind(self, **kwargs): + self.local_ctx().update(kwargs) + + def bind_global(self, **kwargs): + self.global_ctx.update(kwargs) + + def event(self, event, *args, **kwargs): + evt = NiceOrderedDict() + evt['event'] = event + if args: + evt['args'] = args + evt.update(kwargs) + if 'error' in kwargs: + self.error(evt) + elif 'debug' in kwargs: + self.debug(evt) + else: + self.info(evt) + + def timestamp(self, event_name): + if LOG_TIMESTAMPS: + t = time.monotonic() + tstp = NiceOrderedDict() + tstp['timestamp'] = NiceOrderedDict() + tstp['timestamp']["event"] = event_name + tstp['timestamp']["time"] = t*1e9 + self.debug(tstp) + + def findCaller(self, stack_info=False, stacklevel=1): + """ + Find the stack frame of the caller so that we can note the source + file name, line number and function name. + """ + f = sys._getframe(3) + #On some versions of IronPython, currentframe() returns None if + #IronPython isn't run with -X:Frames. + if f is not None: + f = f.f_back + orig_f = f + while f and stacklevel > 1: + f = f.f_back + stacklevel -= 1 + if not f: + f = orig_f + rv = "(unknown file)", 0, "(unknown function)", None + while hasattr(f, "f_code"): + co = f.f_code + filename = os.path.normcase(co.co_filename) + + if filename == _srcfile: + f = f.f_back + continue + sinfo = None + if stack_info: + sio = io.StringIO() + sio.write('Stack (most recent call last):\n') + traceback.print_stack(f, file=sio) + sinfo = sio.getvalue() + if sinfo[-1] == '\n': + sinfo = sinfo[:-1] + sio.close() + rv = (co.co_filename, f.f_lineno, co.co_name, sinfo) + break + return rv + +if __name__ == "__main__": + log = SwagLogger() + + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setLevel(logging.INFO) + stdout_handler.addFilter(SwagErrorFilter()) + log.addHandler(stdout_handler) + + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setLevel(logging.ERROR) + log.addHandler(stderr_handler) + + log.info("asdasd %s", "a") + log.info({'wut': 1}) + log.warning("warning") + log.error("error") + log.critical("critical") + log.event("test", x="y") + + with log.ctx(): + stdout_handler.setFormatter(SwagFormatter(log)) + stderr_handler.setFormatter(SwagFormatter(log)) + log.bind(user="some user") + log.info("in req") + print("") + log.warning("warning") + print("") + log.error("error") + print("") + log.critical("critical") + print("") + log.event("do_req", a=1, b="c") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py new file mode 100644 index 000000000..9fd2f695d --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/realtime.py @@ -0,0 +1,134 @@ +"""Utilities for reading real time clocks and keeping soft real time constraints.""" +import gc +import os +import sys +import time + +from setproctitle import getproctitle + +from iqpilot.common.utils import MovingAverage +from iqpilot.system.hardware import PC + + +# time step for each process +DT_CTRL = 0.01 # controlsd +DT_MDL = 0.05 # model +DT_HW = 0.5 # hardwared and manager +DT_DMON = 0.05 # driver monitoring + + +class Priority: + # CORE 2 + # - modeld = 55 + # - camerad = 54 + CTRL_LOW = 51 # plannerd & radard + + # CORE 3 + # - pandad = 55 + CTRL_HIGH = 53 + + +def set_core_affinity(cores: list[int]) -> None: + if sys.platform == 'linux' and not PC: + os.sched_setaffinity(0, cores) + + +def config_realtime_process(cores: int | list[int], priority: int) -> None: + gc.disable() + if sys.platform == 'linux' and not PC: + os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(priority)) + c = cores if isinstance(cores, list) else [cores, ] + set_core_affinity(c) + + +def config_background_thread() -> None: + if sys.platform == 'linux' and not PC: + os.sched_setscheduler(0, os.SCHED_OTHER, os.sched_param(0)) + set_core_affinity(list(range(os.cpu_count() or 1))) + + +def lock_memory() -> None: + """mlockall this process so memory reclaim/compaction can't stall it. RT control + procs only (locking ui/modeld would worsen pressure). Best-effort.""" + if sys.platform != 'linux' or PC: + return + try: + import ctypes + import resource + resource.setrlimit(resource.RLIMIT_MEMLOCK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)) + MCL_CURRENT, MCL_FUTURE = 0x1, 0x2 + libc = ctypes.CDLL("libc.so.6", use_errno=True) + if libc.mlockall(MCL_CURRENT | MCL_FUTURE) != 0: + raise OSError(ctypes.get_errno(), os.strerror(ctypes.get_errno())) + except Exception as e: + try: + from iqpilot.common.swaglog import cloudlog + cloudlog.warning(f"lock_memory (mlockall) failed: {e}") + except Exception: + pass + + +class Ratekeeper: + def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None: + """Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative.""" + self._interval = 1. / rate + self._print_delay_threshold = print_delay_threshold + self._frame = 0 + self._remaining = 0.0 + self._process_name = getproctitle() + self._last_monitor_time = -1. + self._next_frame_time = -1. + + self.avg_dt = MovingAverage(100) + self.avg_dt.add_value(self._interval) + + def reset(self) -> None: + self._remaining = 0.0 + self._last_monitor_time = -1. + self._next_frame_time = -1. + self.avg_dt = MovingAverage(100) + self.avg_dt.add_value(self._interval) + + @property + def frame(self) -> int: + return self._frame + + @property + def remaining(self) -> float: + return self._remaining + + @property + def lag(self) -> float: + return max(0., -self._remaining) + + @property + def lagging(self) -> bool: + expected_dt = self._interval * (1 / 0.9) + return self.avg_dt.get_average() > expected_dt + + # Maintain loop rate by calling this at the end of each loop + def keep_time(self) -> bool: + lagged = self.monitor_time() + if self._remaining > 0: + time.sleep(self._remaining) + return lagged + + # Monitors the cumulative lag, but does not enforce a rate + def monitor_time(self) -> bool: + if self._last_monitor_time < 0: + self._next_frame_time = time.monotonic() + self._interval + self._last_monitor_time = time.monotonic() + + prev = self._last_monitor_time + self._last_monitor_time = time.monotonic() + self.avg_dt.add_value(self._last_monitor_time - prev) + + lagged = False + remaining = self._next_frame_time - time.monotonic() + self._next_frame_time += self._interval + if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold: + print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms") + lagged = True + self._frame += 1 + self._remaining = remaining + return lagged diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py new file mode 100644 index 000000000..b823d09a4 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/spinner.py @@ -0,0 +1,52 @@ +import os +import subprocess +from iqpilot.common.basedir import BASEDIR + + +class Spinner: + def __init__(self): + try: + self.spinner_proc = subprocess.Popen(["./spinner.py"], + stdin=subprocess.PIPE, + cwd=os.path.join(BASEDIR, "iqpilot", "system", "ui"), + close_fds=True) + except OSError: + self.spinner_proc = None + + def __enter__(self): + return self + + def update(self, spinner_text: str): + if self.spinner_proc is not None: + self.spinner_proc.stdin.write(spinner_text.encode('utf8') + b"\n") + try: + self.spinner_proc.stdin.flush() + except BrokenPipeError: + pass + + def update_progress(self, cur: float, total: float): + self.update(str(round(100 * cur / total))) + + def close(self): + if self.spinner_proc is not None: + self.spinner_proc.kill() + try: + self.spinner_proc.communicate(timeout=2.) + except subprocess.TimeoutExpired: + print("WARNING: failed to kill spinner") + self.spinner_proc = None + + def __del__(self): + self.close() + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +if __name__ == "__main__": + import time + with Spinner() as s: + s.update("Spinner text") + time.sleep(5.0) + print("gone") + time.sleep(5.0) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/steer_delay.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/steer_delay.py new file mode 100644 index 000000000..349f975d3 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/steer_delay.py @@ -0,0 +1,60 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos + +Chooses which steer-actuator delay the lateral controllers run with: the value the +live estimator learned, or the driver's fixed software delay — gated by the +"IQLiveSteerDelay" param. The pick is mirrored into "IQSteerDelayCache" so consumers that do +not subscribe to lateralDelay can still read the current value. +""" +from iqpilot.cereal import car +from iqpilot.common.params import Params + +_ENABLE_KEY = "IQLiveSteerDelay" +_FIXED_KEY = "IQSoftwareSteerDelay" +_CACHE_KEY = "IQSteerDelayCache" + + +def fixed_steer_delay(params, stock_delay): + """The rack's own delay plus the driver's IQSoftwareSteerDelay offset, as the UI reports it.""" + return stock_delay + float(params.get(_FIXED_KEY, return_default=True)) + + +def resolve_steer_delay(params, stock_delay): + """Learned lateral delay while live-learning is enabled, otherwise the driver's fixed delay.""" + if not params.get_bool(_ENABLE_KEY): + return fixed_steer_delay(params, stock_delay) + return float(params.get(_CACHE_KEY, return_default=True)) + + +def lateral_action_delay(params, car_params, live_delay): + """Delay the lateral path should be planned against. + + Angle cars honour the IQLiveSteerDelay toggle so that with live learning off the + estimate never reaches the path: lagd cross-correlates against localizer lateral + accel, so it reports whole-vehicle response (~0.36 s measured on VW MQB, 0.44 s on + Tesla) where the lookahead wants actuator delay (~0.10 s). Torque cars keep the + live estimate. + """ + if car_params.steerControlType == car.CarParams.SteerControlType.angle: + return resolve_steer_delay(params, car_params.steerActuatorDelay) + return live_delay + + +def cached_steer_delay(): + """Last value SteerDelayPublisher mirrored into the param — usable without a + lateralDelay subscription (e.g. at process startup).""" + return Params().get(_CACHE_KEY, return_default=True) + + +class SteerDelayPublisher: + """Refreshes IQSteerDelayCache every lag message: the learned live delay when the + toggle is on, else the actuator delay plus the driver's fixed software offset.""" + + def __init__(self, car_params): + self._params = Params() + self._actuator_delay = car_params.steerActuatorDelay + + def update(self, lag_msg): + live = self._params.get_bool(_ENABLE_KEY) + value = lag_msg.lateralDelay.lateralDelay if live else fixed_steer_delay(self._params, self._actuator_delay) + self._params.put_nonblocking(_CACHE_KEY, value) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/swaglog.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/swaglog.py new file mode 100644 index 000000000..f8fd9d392 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/swaglog.py @@ -0,0 +1,165 @@ +import logging +import os +import sys +import time +import warnings +from pathlib import Path +from logging.handlers import BaseRotatingHandler + +import zmq + +from iqpilot.common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter +from iqpilot.system.hardware.hw import Paths + + +def get_file_handler(): + Path(Paths.swaglog_root()).mkdir(parents=True, exist_ok=True) + base_filename = os.path.join(Paths.swaglog_root(), "swaglog") + handler = SwaglogRotatingFileHandler(base_filename) + return handler + +class SwaglogRotatingFileHandler(BaseRotatingHandler): + def __init__(self, base_filename, interval=60, max_bytes=1024*256, backup_count=2500, encoding=None): + super().__init__(base_filename, mode="a", encoding=encoding, delay=True) + self.base_filename = base_filename + self.interval = interval # seconds + self.max_bytes = max_bytes + self.backup_count = backup_count + self.log_files = self.get_existing_logfiles() + log_indexes = [f.split(".")[-1] for f in self.log_files] + self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1]) + self.last_rollover = None + self.doRollover() + + def _open(self): + self.last_rollover = time.monotonic() + self.last_file_idx += 1 + next_filename = f"{self.base_filename}.{self.last_file_idx:010}" + stream = open(next_filename, self.mode, encoding=self.encoding) + self.log_files.insert(0, next_filename) + return stream + + def get_existing_logfiles(self): + log_files = list() + base_dir = os.path.dirname(self.base_filename) + for fn in os.listdir(base_dir): + fp = os.path.join(base_dir, fn) + if fp.startswith(self.base_filename) and os.path.isfile(fp): + log_files.append(fp) + return sorted(log_files) + + def shouldRollover(self, record): + size_exceeded = self.max_bytes > 0 and self.stream.tell() >= self.max_bytes + time_exceeded = self.interval > 0 and self.last_rollover + self.interval <= time.monotonic() + return size_exceeded or time_exceeded + + def doRollover(self): + if self.stream: + self.stream.close() + self.stream = self._open() + + if self.backup_count > 0: + while len(self.log_files) > self.backup_count: + to_delete = self.log_files.pop() + if os.path.exists(to_delete): # just being safe, should always exist + os.remove(to_delete) + +class UnixDomainSocketHandler(logging.Handler): + def __init__(self, formatter): + logging.Handler.__init__(self) + self.setFormatter(formatter) + self.pid = None + + self.zctx = None + self.sock = None + + def __del__(self): + self.close() + + def close(self): + if self.sock is not None: + self.sock.close() + if self.zctx is not None: + self.zctx.term() + + def connect(self): + self.zctx = zmq.Context() + self.sock = self.zctx.socket(zmq.PUSH) + self.sock.setsockopt(zmq.LINGER, 10) + self.sock.connect(Paths.swaglog_ipc()) + self.pid = os.getpid() + + def emit(self, record): + if os.getpid() != self.pid: + # TODO suppresses warning about forking proc with zmq socket, fix root cause + warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") + self.connect() + + msg = self.format(record).rstrip('\n') + # print("SEND".format(repr(msg))) + try: + s = chr(record.levelno)+msg + self.sock.send(s.encode('utf8'), zmq.NOBLOCK) + except zmq.error.Again: + # drop :/ + pass + + +class ForwardingHandler(logging.Handler): + def __init__(self, target_logger): + super().__init__() + self.target_logger = target_logger + + def emit(self, record): + self.target_logger.handle(record) + + +def add_file_handler(log): + """ + Function to add the file log handler to swaglog. + This can be used to store logs when logmessaged is not running. + """ + handler = get_file_handler() + handler.setFormatter(SwagLogFileFormatter(log)) + log.addHandler(handler) + + +cloudlog = log = SwagLogger() +log.setLevel(logging.DEBUG) + + +class PrettyConsoleFormatter(logging.Formatter): + # StreamHandler writes to stderr, so tty-gate on that + _COLOR = sys.stderr.isatty() and os.environ.get('NO_COLOR') is None + + def format(self, record): + msg = record.getMessage() + if not self._COLOR: + return f"{record.filename}: {msg}" + lvl = record.levelno + if lvl >= 50: lc, ln = "\033[1;38;5;196m", "CRIT" + elif lvl >= 40: lc, ln = "\033[1;38;5;203m", " ERR" + elif lvl >= 30: lc, ln = "\033[38;5;214m", "WARN" + elif lvl >= 20: lc, ln = "\033[38;5;110m", "info" + else: lc, ln = "\033[38;5;244m", " dbg" + body = f"\033[1;38;5;210m{msg}\033[0m" if lvl >= 40 else msg + src = "" if record.filename == "(unknown file)" else f"\033[2m{record.filename}\033[0m " + return f"{lc}{ln:>4}\033[0m {src}{body}" + + +outhandler = logging.StreamHandler() +outhandler.setFormatter(PrettyConsoleFormatter()) + +print_level = os.environ.get('LOGPRINT', 'warning') +if print_level == 'debug': + outhandler.setLevel(logging.DEBUG) +elif print_level == 'info': + outhandler.setLevel(logging.INFO) +elif print_level == 'warning': + outhandler.setLevel(logging.WARNING) + +ipchandler = UnixDomainSocketHandler(SwagFormatter(log)) + +log.addHandler(outhandler) +# logs are sent through IPC before writing to disk to prevent disk I/O blocking +log.addHandler(ipchandler) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/time_helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/time_helpers.py new file mode 100644 index 000000000..8564e270c --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/time_helpers.py @@ -0,0 +1,15 @@ +import datetime +from pathlib import Path + +MIN_DATE = datetime.datetime(year=2025, month=2, day=21) + +def min_date(): + # on systemd systems, the default time is the systemd build time + systemd_path = Path("/lib/systemd/systemd") + if systemd_path.exists(): + d = datetime.datetime.fromtimestamp(systemd_path.stat().st_mtime) + return max(MIN_DATE, d + datetime.timedelta(days=1)) + return MIN_DATE + +def system_time_valid(): + return datetime.datetime.now() > min_date() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py new file mode 100644 index 000000000..e0aa1605d --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/common/utils.py @@ -0,0 +1,271 @@ +import io +import os +import tempfile +import contextlib +import subprocess +import time +import functools +from subprocess import Popen, PIPE, TimeoutExpired +import zstandard as zstd + +LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change + +class Timer: + """Simple lap timer for profiling sequential operations.""" + + def __init__(self): + self._start = self._lap = time.monotonic() + self._sections = {} + + def lap(self, name): + now = time.monotonic() + self._sections[name] = now - self._lap + self._lap = now + + @property + def total(self): + return time.monotonic() - self._start + + def fmt(self, duration): + parts = ", ".join(f"{k}={v:.2f}s" + (f" ({duration/v:.0f}x)" if k == 'render' and v > 0 else "") for k, v in self._sections.items()) + total = self.total + realtime = f"{duration/total:.1f}x realtime" if total > 0 else "N/A" + return f"{duration}s in {total:.1f}s ({realtime}) | {parts}" + +def sudo_write(val: str, path: str) -> None: + try: + with open(path, 'w') as f: + f.write(str(val)) + except PermissionError: + os.system(f"sudo chmod a+w {path}") + try: + with open(path, 'w') as f: + f.write(str(val)) + except PermissionError: + # fallback for debugfs files + os.system(f"sudo su -c 'echo {val} > {path}'") + + +def sudo_read(path: str) -> str: + try: + return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip() + except Exception: + return "" + + +class MovingAverage: + def __init__(self, window_size: int): + self.window_size: int = window_size + self.buffer: list[float] = [0.0] * window_size + self.index: int = 0 + self.count: int = 0 + self.sum: float = 0.0 + + def add_value(self, new_value: float): + # Update the sum: subtract the value being replaced and add the new value + self.sum -= self.buffer[self.index] + self.buffer[self.index] = new_value + self.sum += new_value + + # Update the index in a circular manner + self.index = (self.index + 1) % self.window_size + + # Track the number of added values (for partial windows) + self.count = min(self.count + 1, self.window_size) + + def get_average(self) -> float: + if self.count == 0: + return float('nan') + return self.sum / self.count + + +class CallbackReader: + """Wraps a file, but overrides the read method to also + call a callback function with the number of bytes read so far.""" + + def __init__(self, f, callback, *args): + self.f = f + self.callback = callback + self.cb_args = args + self.total_read = 0 + + def __getattr__(self, attr): + return getattr(self.f, attr) + + def read(self, *args, **kwargs): + chunk = self.f.read(*args, **kwargs) + self.total_read += len(chunk) + self.callback(*self.cb_args, self.total_read) + return chunk + + +@contextlib.contextmanager +def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None, + overwrite: bool = False): + """Write to a file atomically using a temporary file in the same directory as the destination file.""" + dir_name = os.path.dirname(path) + + if not overwrite and os.path.exists(path): + raise FileExistsError(f"File '{path}' already exists. To overwrite it, set 'overwrite' to True.") + + with tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) as tmp_file: + yield tmp_file + tmp_file_name = tmp_file.name + os.replace(tmp_file_name, path) + + +def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.BufferedIOBase, int]: + if not should_compress: + file_size = os.path.getsize(filepath) + file_stream = open(filepath, "rb") + return file_stream, file_size + + # Compress the file on the fly + compressed_stream = io.BytesIO() + compressor = zstd.ZstdCompressor(level=LOG_COMPRESSION_LEVEL) + + with open(filepath, "rb") as f: + compressor.copy_stream(f, compressed_stream) + compressed_size = compressed_stream.tell() + compressed_stream.seek(0) + return compressed_stream, compressed_size + + +# remove all keys that end in DEPRECATED +def strip_deprecated_keys(d): + for k in list(d.keys()): + if isinstance(k, str): + if k.endswith('DEPRECATED'): + d.pop(k) + elif isinstance(d[k], dict): + strip_deprecated_keys(d[k]) + return d + + +def run_cmd(cmd: list[str], cwd=None, env=None) -> str: + return subprocess.check_output(cmd, encoding='utf8', cwd=cwd, env=env).strip() + + +def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> str: + try: + return run_cmd(cmd, cwd=cwd, env=env) + except subprocess.CalledProcessError: + return default + + +@contextlib.contextmanager +def managed_proc(cmd: list[str], env: dict[str, str]): + proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE) + try: + yield proc + finally: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except TimeoutExpired: + proc.kill() + + +def tabulate(tabular_data, headers=(), tablefmt="simple", floatfmt="g", stralign="left", numalign=None): + rows = [list(row) for row in tabular_data] + + def fmt(val): + if isinstance(val, str): + return val + if isinstance(val, (bool, int)): + return str(val) + try: + return format(val, floatfmt) + except (TypeError, ValueError): + return str(val) + + formatted = [[fmt(c) for c in row] for row in rows] + hdrs = [str(h) for h in headers] if headers else None + + ncols = max((len(r) for r in formatted), default=0) + if hdrs: + ncols = max(ncols, len(hdrs)) + if ncols == 0: + return "" + + for r in formatted: + r.extend([""] * (ncols - len(r))) + if hdrs: + hdrs.extend([""] * (ncols - len(hdrs))) + + widths = [0] * ncols + if hdrs: + for i in range(ncols): + widths[i] = len(hdrs[i]) + for row in formatted: + for i in range(ncols): + widths[i] = max(widths[i], max(len(ln) for ln in row[i].split('\n'))) + + def _align(s, w): + if stralign == "center": + return s.center(w) + return s.ljust(w) + + if tablefmt == "html": + parts = [""] + if hdrs: + parts.append("") + parts.append("" + "".join(f"" for h in hdrs) + "") + parts.append("") + parts.append("") + for row in formatted: + parts.append("" + "".join(f"" for c in row) + "") + parts.append("") + parts.append("
{h}
{c}
") + return "\n".join(parts) + + if tablefmt == "simple_grid": + def _sep(left, mid, right): + return left + mid.join("─" * (w + 2) for w in widths) + right + + top, mid_sep, bot = _sep("┌", "┬", "┐"), _sep("├", "┼", "┤"), _sep("└", "┴", "┘") + + def _fmt_row(cells): + split = [c.split('\n') for c in cells] + nlines = max(len(s) for s in split) + for s in split: + s.extend([""] * (nlines - len(s))) + return ["│" + "│".join(f" {_align(split[i][li], widths[i])} " for i in range(ncols)) + "│" for li in range(nlines)] + + lines = [top] + if hdrs: + lines.extend(_fmt_row(hdrs)) + lines.append(mid_sep) + for ri, row in enumerate(formatted): + lines.extend(_fmt_row(row)) + lines.append(mid_sep if ri < len(formatted) - 1 else bot) + return "\n".join(lines) + + gap = " " + lines = [] + if hdrs: + lines.append(gap.join(h.ljust(w) for h, w in zip(hdrs, widths, strict=True))) + lines.append(gap.join("-" * w for w in widths)) + for row in formatted: + lines.append(gap.join(_align(row[i], widths[i]) for i in range(ncols))) + return "\n".join(lines) + + +def retry(attempts=3, delay=1.0, ignore_failure=False): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for _ in range(attempts): + try: + return func(*args, **kwargs) + except Exception: + print(f"{func.__name__} failed, trying again") + time.sleep(delay) + + if ignore_failure: + print(f"{func.__name__} failed after retry") + else: + raise Exception(f"{func.__name__} failed after retry") + return wrapper + return decorator diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py new file mode 100644 index 000000000..2897609eb --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/cloud_client.py @@ -0,0 +1,17 @@ +""" +Copyright ©️ IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" + + +import os +from iqpilot.common.api.base import BaseApi +API_HOST = os.getenv('KONN3KT_API_HOST', 'https://api-iqlabs.konn3kt.com') + +class Konn3ktApi(BaseApi): + + def __init__(self, dongle_id): + super().__init__(dongle_id, API_HOST) + self.user_agent = "konn3kt-device-" + + def get_token(self, expiry_hours=1): + return super()._get_token(expiry_hours=expiry_hours) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py new file mode 100644 index 000000000..1ef907dd9 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/konn3kt/registration.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +import os +import time +import json +import jwt +import re +import secrets +from typing import cast +from pathlib import Path + +from datetime import datetime, timedelta, UTC +from iqpilot.common.api import api_get, get_key_pair +from iqpilot.common.params import Params +from iqpilot.common.spinner import Spinner +from iqpilot.system.hardware import HARDWARE, PC +from iqpilot.system.hardware.hw import Paths +from iqpilot.common.swaglog import cloudlog + + +UNREGISTERED_DONGLE_ID = "UnregisteredDevice" + +_DONGLE_ID_RE = re.compile(r"^[a-fA-F0-9]{16}$") +IMEI_WAIT_TIMEOUT = 15.0 + + +def _read_persist_dongle_id() -> str | None: + p = Path(Paths.persist_root()) / "comma" / "dongle_id" + try: + if not p.is_file(): + return None + s = p.read_text().strip() + return s or None + except Exception: + cloudlog.exception("failed to read persist dongle_id") + return None + + +def get_cached_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str | None: + ro = _read_persist_dongle_id() + if is_valid_dongle_id(ro): + ro = ro.lower() + if prefer_readonly and ro: + return ro + p = Params() if params is None else params + v = p.get("DongleId") + if v and v != UNREGISTERED_DONGLE_ID: + return v.lower() if is_valid_dongle_id(v) else v + return ro or None +def is_valid_dongle_id(dongle_id: str | None) -> bool: + return bool(dongle_id and _DONGLE_ID_RE.fullmatch(dongle_id)) +def get_or_create_dongle_id(params: Params | None = None, prefer_readonly: bool = True) -> str: + p = Params() if params is None else params + dongle_id = get_cached_dongle_id(p, prefer_readonly=prefer_readonly) + if dongle_id and dongle_id != UNREGISTERED_DONGLE_ID: + return dongle_id + dongle_id = secrets.token_hex(8) + p.put("DongleId", dongle_id) + cloudlog.warning(f"generated new DongleId={dongle_id} (no readonly dongle_id found)") + return dongle_id +def ensure_dev_pairing_identity(params: Params | None = None, force_reset: bool = False) -> dict[str, str]: + p = Params() if params is None else params + + persist_dir = Path(Paths.persist_root()) / "comma" + persist_dir.mkdir(parents=True, exist_ok=True) + + dongle_path = persist_dir / "dongle_id" + priv_path = persist_dir / "id_rsa" + pub_path = persist_dir / "id_rsa.pub" + + if force_reset: + for fp in (dongle_path, priv_path, pub_path): + try: + fp.unlink(missing_ok=True) + except Exception: + cloudlog.exception(f"failed to remove {fp}") + try: + (persist_dir / "konn3kt_prime_type").unlink(missing_ok=True) + except Exception: + pass + try: + p.remove("PrimeType") + except Exception: + pass + forced_dongle = os.getenv("KONN3KT_DEV_DONGLE_ID") + dongle_id = forced_dongle.strip().lower() if forced_dongle else None + if dongle_id and not is_valid_dongle_id(dongle_id): + cloudlog.error("KONN3KT_DEV_DONGLE_ID must be 16 hex chars") + dongle_id = None + if dongle_id is None: + existing = None + try: + existing = dongle_path.read_text().strip().lower() if dongle_path.is_file() else None + except Exception: + cloudlog.exception("failed reading existing dev dongle_id") + dongle_id = existing if is_valid_dongle_id(existing) else secrets.token_hex(8) + try: + dongle_path.write_text(dongle_id) + except Exception: + cloudlog.exception("failed writing dev dongle_id") + p.put("DongleId", dongle_id) + p.put("HardwareSerial", p.get("HardwareSerial") or f"DEV-{dongle_id}") + if force_reset or (not priv_path.is_file()) or (not pub_path.is_file()): + try: + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + priv_bytes = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + pub_bytes = key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + priv_path.write_bytes(priv_bytes) + pub_path.write_bytes(pub_bytes) + except Exception: + cloudlog.exception("failed generating dev RSA keys") + raise + return { + "dongle_id": dongle_id, + "serial": p.get("HardwareSerial") or f"DEV-{dongle_id}", + "persist_dir": str(persist_dir), + } +def is_registered_device() -> bool: + dongle = Params().get("DongleId") + return dongle not in (None, UNREGISTERED_DONGLE_ID) + + +def _normalize_imei(value: str | None) -> str: + return value or "" + + +def get_registration_identifiers(wait_timeout: float = IMEI_WAIT_TIMEOUT, show_spinner: bool = False) -> tuple[str, str, str]: + serial = HARDWARE.get_serial() + spinner = Spinner() if show_spinner else None + start_time = time.monotonic() + imei1: str | None = None + imei2: str | None = None + + while time.monotonic() - start_time < wait_timeout: + try: + imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) + if imei1 or imei2: + break + except RuntimeError as e: + if "no modems" in str(e).lower(): + cloudlog.warning("No cellular modem available, proceeding without IMEI") + break + cloudlog.exception("Error getting imei, trying again...") + except Exception: + cloudlog.exception("Error getting imei, trying again...") + time.sleep(1) + + imei1 = _normalize_imei(imei1) + imei2 = _normalize_imei(imei2) + + if not imei1 and not imei2: + cloudlog.warning(f"proceeding with serial-only registration for serial={serial}") + if spinner is not None: + spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") + spinner.close() + + return serial, imei1, imei2 + + +def register(show_spinner=False) -> str | None: + params = Params() + + dongle_id: str | None = get_cached_dongle_id(params, prefer_readonly=True) + if dongle_id in ("", UNREGISTERED_DONGLE_ID): + dongle_id = None + + jwt_algo, private_key, public_key = get_key_pair() + + if not public_key: + dongle_id = UNREGISTERED_DONGLE_ID + cloudlog.warning("missing public key") + elif dongle_id is None: + if show_spinner: + spinner = Spinner() + spinner.update("registering device") + + serial, imei1, imei2 = get_registration_identifiers(wait_timeout=IMEI_WAIT_TIMEOUT, show_spinner=False) + + backoff = 0 + start_time = time.monotonic() + while True: + try: + register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, + cast(str, private_key), algorithm=jwt_algo) + cloudlog.info("getting pilotauth") + cloudlog.info("getting pilotauth") + resp = api_get("v2/pilotauth/", method='POST', timeout=15, + imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token) + + if resp.status_code in (400, 402, 403): + cloudlog.info(f"Unable to register device, got {resp.status_code}") + dongle_id = UNREGISTERED_DONGLE_ID + else: + dongleauth = json.loads(resp.text) + dongle_id = dongleauth["dongle_id"] + break + except Exception: + cloudlog.exception("failed to authenticate") + backoff = min(backoff + 1, 15) + time.sleep(backoff) + + if time.monotonic() - start_time > 60 and show_spinner: + spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + return UNREGISTERED_DONGLE_ID + + if show_spinner: + spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1 or None}, {imei2 or None})") + spinner.close() + + if dongle_id: + params.put("DongleId", dongle_id) + from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert + set_offroad_alert("Offroad_UnregisteredHardware", False) + return dongle_id + + +if __name__ == "__main__": + print(register()) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py new file mode 100644 index 000000000..e19a2e24f --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/desire_helper.py @@ -0,0 +1,286 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +from iqpilot.cereal import car, custom, log + +from iqpilot.common.constants import CV +from iqpilot.common.params import Params +from iqpilot.common.realtime import DT_MDL +from iqpilot.selfdrive.controls.lib.helpers.lane_change import ( + IQLaneSwapController, + AutoLaneChangeMode, + NavExitLaneChangeController, +) +from iqpilot.selfdrive.controls.lib.helpers.lane_turn import IQNavTurnController + +LaneChangeState = log.LaneChangeState +LaneChangeDirection = log.LaneChangeDirection +TurnDirection = custom.IQTurnSignalDirection +NavManeuverPhase = custom.IQNavState.ManeuverPhase + +LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS +LANE_CHANGE_TIME_MAX = 10.0 +TURN_DESIRE_STOP_HOLD_TIME = 3.4 +TURN_DESIRE_STOP_GAP_TIME = 0.2 +TURN_DESIRE_STOP_CYCLE_TIME = TURN_DESIRE_STOP_HOLD_TIME + TURN_DESIRE_STOP_GAP_TIME +TURN_DESIRE_CYCLE_SPEED_MAX = 5 * CV.MPH_TO_MS +TURN_DESIRE_COMMIT_YAW_RATE = 0.08 + +_LANE_CHANGE_DESIRES = { + (LaneChangeDirection.none, LaneChangeState.off): log.Desire.none, + (LaneChangeDirection.none, LaneChangeState.preLaneChange): log.Desire.none, + (LaneChangeDirection.none, LaneChangeState.laneChangeStarting): log.Desire.none, + (LaneChangeDirection.none, LaneChangeState.laneChangeFinishing): log.Desire.none, + (LaneChangeDirection.left, LaneChangeState.off): log.Desire.none, + (LaneChangeDirection.left, LaneChangeState.preLaneChange): log.Desire.none, + (LaneChangeDirection.left, LaneChangeState.laneChangeStarting): log.Desire.laneChangeLeft, + (LaneChangeDirection.left, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeLeft, + (LaneChangeDirection.right, LaneChangeState.off): log.Desire.none, + (LaneChangeDirection.right, LaneChangeState.preLaneChange): log.Desire.none, + (LaneChangeDirection.right, LaneChangeState.laneChangeStarting): log.Desire.laneChangeRight, + (LaneChangeDirection.right, LaneChangeState.laneChangeFinishing): log.Desire.laneChangeRight, +} + +_TURN_DESIRES = { + TurnDirection.none: log.Desire.none, + TurnDirection.turnLeft: log.Desire.turnLeft, + TurnDirection.turnRight: log.Desire.turnRight, +} + +_STOP_CYCLING_TURN_DESIRES = { + log.Desire.turnLeft, + log.Desire.turnRight, +} + + +def turn_desire(turn_direction) -> log.Desire: + return _TURN_DESIRES[getattr(turn_direction, "raw", turn_direction)] + + +def _direction_from_blinkers(carstate) -> int: + if carstate.leftBlinker: + return LaneChangeDirection.left + if carstate.rightBlinker: + return LaneChangeDirection.right + return LaneChangeDirection.none + + +def _steering_nudge_matches(carstate, direction: int) -> bool: + if not carstate.steeringPressed: + return False + return ( + (direction == LaneChangeDirection.left and carstate.steeringTorque > 0) or + (direction == LaneChangeDirection.right and carstate.steeringTorque < 0) + ) + + +def _blindspot_matches(carstate, direction: int) -> bool: + return ( + (direction == LaneChangeDirection.left and carstate.leftBlindspot) or + (direction == LaneChangeDirection.right and carstate.rightBlindspot) + ) + + +def _read_enable_bsm() -> bool: + try: + with car.CarParams.from_bytes(Params().get("CarParams")) as cp: + return bool(cp.enableBsm) + except Exception: + return False + + +class DesireHelper: + def __init__(self): + self.lane_change_state = LaneChangeState.off + self.lane_change_direction = LaneChangeDirection.none + self.lane_change_timer = 0.0 + self.lane_change_ll_prob = 1.0 + self.prev_one_blinker = False + self.prev_nav_exit_active = False + self.desire = log.Desire.none + + self.alc = IQLaneSwapController(self) + self.lane_turn_controller = IQNavTurnController(self) + self.nav_exit = NavExitLaneChangeController(_read_enable_bsm()) + self.lane_turn_direction = TurnDirection.none + self.nav_turn_direction = TurnDirection.none + self.turn_desire_stop_timer = 0.0 + self.turn_desire_stop_active = False + self.turn_desire_cycle_input = log.Desire.none + self.turn_desire_committed = False + + @staticmethod + def get_lane_change_direction(carstate): + return _direction_from_blinkers(carstate) + + @staticmethod + def _nav_turn_desire(nav_state): + if nav_state is None or not getattr(nav_state, "active", False): + return TurnDirection.none + if getattr(nav_state, "maneuverPhase", NavManeuverPhase.none) != NavManeuverPhase.turnActive: + return TurnDirection.none + if not getattr(nav_state, "shouldSendTurnDesire", False): + return TurnDirection.none + return getattr(nav_state, "turnDesireDirection", TurnDirection.none) + + def _clear_lane_change(self) -> None: + self.lane_change_state = LaneChangeState.off + self.lane_change_direction = LaneChangeDirection.none + + def _refresh_turn_overrides(self, carstate, nav_state) -> bool: + speed_mps = carstate.vEgo + self.lane_turn_controller.update_params() + self.lane_turn_controller.update_lane_turn( + blindspot_left=carstate.leftBlindspot, + blindspot_right=carstate.rightBlindspot, + left_blinker=carstate.leftBlinker, + right_blinker=carstate.rightBlinker, + v_ego=speed_mps, + ) + self.lane_turn_direction = self.lane_turn_controller.get_turn_direction() + self.nav_turn_direction = self._nav_turn_desire(nav_state) + + self.nav_exit.update_params() + self.nav_exit.update(nav_state, carstate) + return bool(self.nav_exit.active) + + def _reset_required(self, lateral_active: bool, nav_exit_active: bool) -> bool: + timed_out = self.lane_change_timer > LANE_CHANGE_TIME_MAX + feature_disabled = self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF and not nav_exit_active + return (not lateral_active) or timed_out or feature_disabled + + def _begin_from_idle(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: + if below_speed: + return + if one_blinker and not self.prev_one_blinker: + self.lane_change_state = LaneChangeState.preLaneChange + self.lane_change_direction = _direction_from_blinkers(self._last_carstate) + self.lane_change_ll_prob = 1.0 + return + if nav_exit_active and not self.prev_nav_exit_active: + self.lane_change_state = LaneChangeState.preLaneChange + self.lane_change_direction = self.nav_exit.direction + self.lane_change_ll_prob = 1.0 + + def _refresh_requested_direction(self, one_blinker: bool, nav_exit_active: bool) -> None: + if one_blinker: + self.lane_change_direction = _direction_from_blinkers(self._last_carstate) + elif nav_exit_active: + self.lane_change_direction = self.nav_exit.direction + + def _step_pre_lane_change(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool) -> None: + self._refresh_requested_direction(one_blinker, nav_exit_active) + blindspot_detected = _blindspot_matches(self._last_carstate, self.lane_change_direction) + steering_ready = _steering_nudge_matches(self._last_carstate, self.lane_change_direction) + nav_auto_start = nav_exit_active and self.nav_exit.auto_allowed + + self.alc.update_lane_change(blindspot_detected=blindspot_detected, brake_pressed=self._last_carstate.brakePressed) + allowed_to_launch = steering_ready or self.alc.auto_lane_change_allowed or nav_auto_start + + if (not (one_blinker or nav_exit_active)) or below_speed: + self._clear_lane_change() + elif allowed_to_launch and not blindspot_detected: + self.lane_change_state = LaneChangeState.laneChangeStarting + + def _step_lane_change_starting(self, lane_change_prob: float) -> None: + self.lane_change_ll_prob = max(self.lane_change_ll_prob - (2.0 * DT_MDL), 0.0) + if lane_change_prob < 0.02 and self.lane_change_ll_prob < 0.01: + self.lane_change_state = LaneChangeState.laneChangeFinishing + + def _step_lane_change_finishing(self, one_blinker: bool) -> None: + self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0) + if self.lane_change_ll_prob <= 0.99: + return + self.lane_change_direction = LaneChangeDirection.none + self.lane_change_state = LaneChangeState.preLaneChange if one_blinker else LaneChangeState.off + + def _advance_lane_change_machine(self, one_blinker: bool, nav_exit_active: bool, below_speed: bool, lane_change_prob: float) -> None: + if self.lane_change_state == LaneChangeState.off: + self._begin_from_idle(one_blinker, nav_exit_active, below_speed) + return + if self.lane_change_state == LaneChangeState.preLaneChange: + self._step_pre_lane_change(one_blinker, nav_exit_active, below_speed) + return + if self.lane_change_state == LaneChangeState.laneChangeStarting: + self._step_lane_change_starting(lane_change_prob) + return + if self.lane_change_state == LaneChangeState.laneChangeFinishing: + self._step_lane_change_finishing(one_blinker) + + def _update_timer(self) -> None: + if self.lane_change_state in (LaneChangeState.off, LaneChangeState.preLaneChange): + self.lane_change_timer = 0.0 + else: + self.lane_change_timer += DT_MDL + + def _clear_turn_desire_stop_cycle(self) -> None: + self.turn_desire_stop_timer = 0.0 + self.turn_desire_stop_active = False + self.turn_desire_cycle_input = log.Desire.none + self.turn_desire_committed = False + + def _cycle_turn_desire_when_stopped(self, desired_output: log.Desire) -> log.Desire: + if desired_output not in _STOP_CYCLING_TURN_DESIRES: + self._clear_turn_desire_stop_cycle() + return desired_output + + if desired_output != self.turn_desire_cycle_input: + self.turn_desire_stop_timer = 0.0 + self.turn_desire_stop_active = False + self.turn_desire_cycle_input = desired_output + self.turn_desire_committed = False + + if abs(getattr(self._last_carstate, "yawRate", 0.0)) >= TURN_DESIRE_COMMIT_YAW_RATE: + self.turn_desire_committed = True + + if self.turn_desire_committed: + self.turn_desire_stop_timer = 0.0 + self.turn_desire_stop_active = False + return desired_output + + if self._last_carstate.vEgo > TURN_DESIRE_CYCLE_SPEED_MAX: + self.turn_desire_stop_timer = 0.0 + self.turn_desire_stop_active = False + return desired_output + + if not self.turn_desire_stop_active: + self.turn_desire_stop_active = True + self.turn_desire_stop_timer = 0.0 + + cycle_phase = self.turn_desire_stop_timer % TURN_DESIRE_STOP_CYCLE_TIME + self.turn_desire_stop_timer += DT_MDL + if cycle_phase >= TURN_DESIRE_STOP_HOLD_TIME: + return log.Desire.none + return desired_output + + def _pick_desire_output(self) -> None: + desired_output = log.Desire.none + if self.nav_turn_direction != TurnDirection.none: + desired_output = turn_desire(self.nav_turn_direction) + elif self.lane_turn_direction != TurnDirection.none: + desired_output = turn_desire(self.lane_turn_direction) + else: + desired_output = _LANE_CHANGE_DESIRES[(self.lane_change_direction, self.lane_change_state)] + + self.desire = self._cycle_turn_desire_when_stopped(desired_output) + + def update(self, carstate, lateral_active, lane_change_prob, nav_state=None, modeldata=None, radar_state=None): + self._last_carstate = carstate + one_blinker = carstate.leftBlinker != carstate.rightBlinker + below_speed = carstate.vEgo < LANE_CHANGE_SPEED_MIN + nav_exit_active = self._refresh_turn_overrides(carstate, nav_state) + + self.alc.update_params() + if self._reset_required(lateral_active, nav_exit_active): + self._clear_lane_change() + else: + self._advance_lane_change_machine(one_blinker, nav_exit_active, below_speed, lane_change_prob) + + self._update_timer() + self.prev_one_blinker = one_blinker and lateral_active + self.prev_nav_exit_active = nav_exit_active + self.alc.update_state() + self._pick_desire_output() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py new file mode 100644 index 000000000..c89027b03 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/drive_helpers.py @@ -0,0 +1,80 @@ +import numpy as np +from iqpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY +from iqpilot.common.realtime import DT_CTRL, DT_MDL + +MIN_SPEED = 1.0 +CONTROL_N = 17 +CAR_ROTATION_RADIUS = 0.0 +# This is a turn radius smaller than most cars can achieve +MAX_CURVATURE = 0.4 +MAX_VEL_ERR = 5.0 # m/s + +MAX_LATERAL_JERK = 5.0 # m/s^3 +MAX_LATERAL_ACCEL_NO_ROLL = 5.0 # m/s^2 +MAX_LATERAL_ACCEL_NO_ROLL_OVERRIDE = 5.0 # m/s^2 +DEFAULT_STOPPING_SPEED = 0.25 # m/s + + +def should_stop(v_ego: float, a_target: float, stopping_speed: float = DEFAULT_STOPPING_SPEED) -> bool: + return bool(v_ego < stopping_speed and a_target < 0.1) + + +def clamp(val, min_val, max_val): + clamped_val = float(np.clip(val, min_val, max_val)) + return clamped_val, clamped_val != val + +def smooth_value(val, prev_val, tau, dt=DT_MDL): + alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1 + return alpha * val + (1 - alpha) * prev_val + +# "Model smoothing": when the policy's own predicted uncertainty (plan_stds) for the +# 1s-ahead lateral position spikes, temporarily lengthen the desiredCurvature smoothing +# time constant so a noisy/uncertain model output doesn't jerk the wheel. +MODEL_SMOOTHING_STD_LOW = 0.15 # m, plan y_std at 1s below which no extra smoothing is added +MODEL_SMOOTHING_STD_HIGH = 0.25 # m, plan y_std at 1s at/above which the full max_extra_seconds is added +MODEL_SMOOTHING_MAX_TOTAL_SEC = 0.60 # hard ceiling on base + dynamic lat smoothing seconds + +def dynamic_lat_smooth_extra_seconds(y_std_1s: float, max_extra_seconds: float) -> float: + if max_extra_seconds <= 0.0: + return 0.0 + return float(np.interp(y_std_1s, [MODEL_SMOOTHING_STD_LOW, MODEL_SMOOTHING_STD_HIGH], [0.0, max_extra_seconds])) + +def clip_curvature(v_ego, prev_curvature, new_curvature, roll, override=False) -> tuple[float, bool]: + # This function respects ISO lateral jerk and acceleration limits + a max curvature + v_ego = max(v_ego, MIN_SPEED) + max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 + new_curvature = np.clip(new_curvature, + prev_curvature - max_curvature_rate * DT_CTRL, + prev_curvature + max_curvature_rate * DT_CTRL) + + max_lat_accel_no_roll = MAX_LATERAL_ACCEL_NO_ROLL_OVERRIDE if override else MAX_LATERAL_ACCEL_NO_ROLL + roll_compensation = roll * ACCELERATION_DUE_TO_GRAVITY + max_lat_accel = max_lat_accel_no_roll + roll_compensation + min_lat_accel = -max_lat_accel_no_roll + roll_compensation + new_curvature, limited_accel = clamp(new_curvature, min_lat_accel / v_ego ** 2, max_lat_accel / v_ego ** 2) + + new_curvature, limited_max_curv = clamp(new_curvature, -MAX_CURVATURE, MAX_CURVATURE) + return float(new_curvature), limited_accel or limited_max_curv + + +def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, stopping_speed=DEFAULT_STOPPING_SPEED): + if len(speeds) == len(t_idxs): + v_now = speeds[0] + a_now = accels[0] + v_target = np.interp(action_t, t_idxs, speeds) + a_target = 2 * (v_target - v_now) / (action_t) - a_now + else: + v_now = 0.0 + v_target = 0.0 + a_target = 0.0 + return a_target, should_stop(v_now, a_target, stopping_speed) + +def curv_from_psis(psi_target, psi_rate, vego, action_t): + vego = np.clip(vego, MIN_SPEED, np.inf) + curv_from_psi = psi_target / (vego * action_t) + return 2*curv_from_psi - psi_rate / vego + +def get_curvature_from_plan(yaws, yaw_rates, t_idxs, vego, action_t): + psi_target = np.interp(action_t, t_idxs, yaws) + psi_rate = yaw_rates[0] + return curv_from_psis(psi_target, psi_rate, vego, action_t) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py new file mode 100644 index 000000000..58d826647 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_change.py @@ -0,0 +1,293 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqpilot.cereal import custom, log + +from iqpilot.common.params import Params +from iqpilot.common.realtime import DT_MDL + +NAV_EXIT_COMMIT_DISTANCE = 500.0 # m before a route exit to begin moving into the exit lane +_ManeuverType = custom.IQNavState.ManeuverType +_NavDirection = custom.NavDirection + + +class LaneSwapPreset: + DISABLED = -1 + STEERING_NUDGE = 0 + DIRECT = 1 + DELAY_HALF = 2 + DELAY_ONE = 3 + DELAY_TWO = 4 + DELAY_THREE = 5 + OFF = DISABLED + NUDGE = STEERING_NUDGE + NUDGELESS = DIRECT + HALF_SECOND = DELAY_HALF + ONE_SECOND = DELAY_ONE + TWO_SECONDS = DELAY_TWO + THREE_SECONDS = DELAY_THREE + + +PRESET_SECONDS = { + LaneSwapPreset.DISABLED: 0.0, + LaneSwapPreset.STEERING_NUDGE: 0.0, + LaneSwapPreset.DIRECT: 0.05, + LaneSwapPreset.DELAY_HALF: 0.5, + LaneSwapPreset.DELAY_ONE: 1.0, + LaneSwapPreset.DELAY_TWO: 2.0, + LaneSwapPreset.DELAY_THREE: 3.0, +} + +LANE_SWAP_SECONDS = dict(PRESET_SECONDS) +BLINDSPOT_WAIT_OFFSET = -1 + + +class LaneSwapEngine: + def __init__(self, desire_hub): + self._hub = desire_hub + self._kv = Params() + self._mem = { + "sec": 0.0, + "tick": 0, + "gate": 0.0, + "preset": self._kv.get("IQLaneChangeTimer", return_default=True), + "bsm_hold": False, + "braked": False, + "ready": False, + "used": False, + } + self.reload_setup() + + def _pull_setup(self) -> None: + self._mem["bsm_hold"] = self._kv.get_bool("IQLaneChangeBsmDelay") + self._mem["preset"] = self._kv.get("IQLaneChangeTimer", return_default=True) + + def _idle_phase(self) -> bool: + return ( + self._hub.lane_change_state == log.LaneChangeState.off and + self._hub.lane_change_direction == log.LaneChangeDirection.none + ) + + def _seconds_for_preset(self) -> float: + picked = self._mem["preset"] + return PRESET_SECONDS.get(picked, PRESET_SECONDS[LaneSwapPreset.STEERING_NUDGE]) + + def _auto_preset_active(self) -> bool: + picked = self._mem["preset"] + return picked not in (LaneSwapPreset.DISABLED, LaneSwapPreset.STEERING_NUDGE) + + def _advance_clock(self, blindspot_now: bool) -> None: + wait_s = self._seconds_for_preset() + self._mem["gate"] = wait_s + self._mem["sec"] += DT_MDL + if self._mem["bsm_hold"] and blindspot_now and wait_s > 0.0: + if wait_s == PRESET_SECONDS[LaneSwapPreset.DIRECT]: + self._mem["sec"] = BLINDSPOT_WAIT_OFFSET + else: + self._mem["sec"] = wait_s + BLINDSPOT_WAIT_OFFSET + + def _ready_to_fire(self) -> bool: + return ( + self._auto_preset_active() and + (not self._mem["braked"]) and + (not self._mem["used"]) and + (self._mem["sec"] > self._mem["gate"]) + ) + + def reload_setup(self) -> None: + self._pull_setup() + + def heartbeat(self) -> None: + if (self._mem["tick"] % 50) == 0: + self._pull_setup() + self._mem["tick"] += 1 + + def sample(self, blindspot_now: bool = False, brake_now: bool = False, **legacy) -> None: + blindspot_now = bool(legacy.get("blindspot_detected", blindspot_now)) + brake_now = bool(legacy.get("brake_pressed", brake_now)) + self._mem["braked"] = self._mem["braked"] or brake_now + self._advance_clock(blindspot_now) + self._mem["ready"] = self._ready_to_fire() + + def finalize(self) -> None: + started = self._hub.lane_change_state == log.LaneChangeState.laneChangeStarting + self._mem["used"] = self._mem["used"] or started + if self._idle_phase(): + self._mem["sec"] = 0.0 + self._mem["braked"] = False + self._mem["used"] = False + + @property + def ready(self): + return self._mem["ready"] + + @property + def delay(self): + return self._mem["gate"] + + @property + def elapsed(self): + return self._mem["sec"] + + @property + def preset(self): + return self._mem["preset"] + + @preset.setter + def preset(self, value): + self._mem["preset"] = value + + @property + def bsm_hold(self): + return self._mem["bsm_hold"] + + @bsm_hold.setter + def bsm_hold(self, value): + self._mem["bsm_hold"] = bool(value) + + @property + def braked(self): + return self._mem["braked"] + + @braked.setter + def braked(self, value): + self._mem["braked"] = bool(value) + + @property + def used(self): + return self._mem["used"] + + @used.setter + def used(self, value): + self._mem["used"] = bool(value) + + +class NavExitLaneChangeController: + def __init__(self, enable_bsm: bool): + self._params = Params() + self._enable_bsm = bool(enable_bsm) + self.enabled = self._read_enabled() + self._tick = 0 + self.active = False + self.direction = log.LaneChangeDirection.none + self.auto_allowed = False + + def _read_enabled(self) -> bool: + try: + return self._params.get_bool("NavExitLaneChange") + except Exception: + return False + + def update_params(self) -> None: + if self._tick % 50 == 0: + self.enabled = self._read_enabled() + self._tick += 1 + + @staticmethod + def _raw(value): + return getattr(value, "raw", value) + + def update(self, nav_state, carstate) -> None: + self.active = False + self.direction = log.LaneChangeDirection.none + self.auto_allowed = False + + if not self.enabled or nav_state is None or not getattr(nav_state, "active", False): + return + if not getattr(nav_state, "nextManeuverValid", False): + return + if self._raw(getattr(nav_state, "nextManeuverType", _ManeuverType.none)) != int(_ManeuverType.exit): + return + distance = float(getattr(nav_state, "nextManeuverDistance", 0.0)) + if not 0.0 < distance <= NAV_EXIT_COMMIT_DISTANCE: + return + + direction = self._raw(getattr(nav_state, "nextManeuverDirection", _NavDirection.none)) + if direction == int(_NavDirection.left): + self.direction = log.LaneChangeDirection.left + elif direction == int(_NavDirection.right): + self.direction = log.LaneChangeDirection.right + else: + return + + self.active = True + blindspot = carstate.leftBlindspot if self.direction == log.LaneChangeDirection.left else carstate.rightBlindspot + self.auto_allowed = (not blindspot) if self._enable_bsm else False + + +AutoLaneChangeMode = LaneSwapPreset +AUTO_LANE_CHANGE_TIMER = LANE_SWAP_SECONDS +ONE_SECOND_DELAY = BLINDSPOT_WAIT_OFFSET + + +class IQLaneSwapController(LaneSwapEngine): + def __init__(self, desire_helper): + super().__init__(desire_helper) + + def reset(self) -> None: + self.finalize() + + def update_params(self) -> None: + self.heartbeat() + + def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None: + self.sample(blindspot_now=blindspot_detected, brake_now=brake_pressed) + + def update_state(self) -> None: + self.finalize() + + @property + def lane_change_wait_timer(self): + return self.elapsed + + @lane_change_wait_timer.setter + def lane_change_wait_timer(self, value): + self._mem["sec"] = float(value) + + @property + def lane_change_delay(self): + return self.delay + + @lane_change_delay.setter + def lane_change_delay(self, value): + self._mem["gate"] = float(value) + + @property + def lane_change_set_timer(self): + return self.preset + + @lane_change_set_timer.setter + def lane_change_set_timer(self, value): + self.preset = value + + @property + def lane_change_bsm_delay(self): + return self.bsm_hold + + @lane_change_bsm_delay.setter + def lane_change_bsm_delay(self, value): + self.bsm_hold = value + + @property + def prev_brake_pressed(self): + return self.braked + + @prev_brake_pressed.setter + def prev_brake_pressed(self, value): + self.braked = value + + @property + def auto_lane_change_allowed(self): + return self.ready + + @auto_lane_change_allowed.setter + def auto_lane_change_allowed(self, value): + self._mem["ready"] = bool(value) + + @property + def prev_lane_change(self): + return self.used + + @prev_lane_change.setter + def prev_lane_change(self, value): + self.used = value diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py new file mode 100644 index 000000000..dd1c0598e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/controls/lib/helpers/lane_turn.py @@ -0,0 +1,157 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from iqpilot.cereal import custom + +from iqpilot.common.constants import CV +from iqpilot.common.params import Params + +TurnDirection = custom.IQTurnSignalDirection + +TURN_TRIGGER_MPS = 20 * CV.MPH_TO_MS +TURN_SPEED_GATE_MPS = TURN_TRIGGER_MPS +LANE_CHANGE_SPEED_MIN = TURN_SPEED_GATE_MPS + + +@dataclass +class _TurnGateState: + active: bool = False + speed_limit_mps: float = TURN_TRIGGER_MPS + outcome: int = TurnDirection.none + refresh_tick: int = 0 + + +def _mph_param_to_mps(raw_value) -> float: + try: + return float(raw_value) * CV.MPH_TO_MS + except (TypeError, ValueError): + return TURN_TRIGGER_MPS + + +def _resolve_signal_choice(speed_mps: float, + speed_limit_mps: float, + left_signal: bool, + right_signal: bool, + left_blocked: bool, + right_blocked: bool) -> int: + if speed_mps >= speed_limit_mps: + return TurnDirection.none + if left_signal and not right_signal and not left_blocked: + return TurnDirection.turnLeft + if right_signal and not left_signal and not right_blocked: + return TurnDirection.turnRight + return TurnDirection.none + + +class TurnSignalPlanner: + _REFRESH_STRIDE = 50 + + def __init__(self, desire_hub): + self._desire_hub = desire_hub + self._params = Params() + self._state = _TurnGateState() + self.reload_setup() + + def _refresh_from_params(self) -> None: + requested_gate = _mph_param_to_mps(self._params.get("IQLaneTurnValue", return_default=True)) + self._state.active = self._params.get_bool("IQLaneTurnDesire") + self._state.speed_limit_mps = min(TURN_TRIGGER_MPS, requested_gate) + + def _consume_legacy_kwargs(self, **legacy) -> tuple[bool, bool, bool, bool, float]: + return ( + bool(legacy.get("blindspot_left", False)), + bool(legacy.get("blindspot_right", False)), + bool(legacy.get("left_blinker", False)), + bool(legacy.get("right_blinker", False)), + float(legacy.get("v_ego", 0.0)), + ) + + def reload_setup(self): + self._refresh_from_params() + + def heartbeat(self) -> None: + if self._state.refresh_tick % self._REFRESH_STRIDE == 0: + self._refresh_from_params() + self._state.refresh_tick += 1 + + def sample(self, + blocked_l: bool = False, + blocked_r: bool = False, + blink_l: bool = False, + blink_r: bool = False, + speed_mps: float = 0.0, + **legacy) -> None: + if legacy: + blocked_l, blocked_r, blink_l, blink_r, speed_mps = self._consume_legacy_kwargs(**legacy) + self._state.outcome = _resolve_signal_choice(speed_mps, + self._state.speed_limit_mps, + blink_l, + blink_r, + blocked_l, + blocked_r) + + def output(self): + return self._state.outcome if self._state.active else TurnDirection.none + + @property + def enabled(self): + return self._state.active + + @enabled.setter + def enabled(self, value): + self._state.active = bool(value) + + @property + def speed_gate(self): + return self._state.speed_limit_mps + + @speed_gate.setter + def speed_gate(self, value): + self._state.speed_limit_mps = float(value) + + @property + def turn_direction(self): + return self._state.outcome + + @turn_direction.setter + def turn_direction(self, value): + self._state.outcome = value + + +class IQNavTurnController(TurnSignalPlanner): + def __init__(self, desire_helper): + super().__init__(desire_helper) + + def read_params(self): + self.reload_setup() + + def update_params(self) -> None: + self.heartbeat() + + def update_lane_turn(self, + blindspot_left: bool, + blindspot_right: bool, + left_blinker: bool, + right_blinker: bool, + v_ego: float) -> None: + self.sample(blocked_l=blindspot_left, + blocked_r=blindspot_right, + blink_l=left_blinker, + blink_r=right_blinker, + speed_mps=v_ego) + + def get_turn_direction(self): + return self.output() + + @property + def lane_turn_value(self): + return self.speed_gate + + @lane_turn_value.setter + def lane_turn_value(self, value): + self.speed_gate = value diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/big_catalog.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/big_catalog.py new file mode 100644 index 000000000..972671971 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/big_catalog.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.big_catalog") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.big_catalog import * diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/camera.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/camera.py new file mode 100644 index 000000000..a19e6ce51 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/camera.py @@ -0,0 +1,70 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import numpy as np + +from iqpilot.common.transformations.camera import DEVICE_CAMERAS + +MAX_CAMERA_OFFSET_METERS = 0.35 + + +class _OffsetSmoother: + def __init__(self, blend: float = 0.1): + self._blend = blend + self._value = 0.0 + + def step(self, target: float) -> float: + self._value = ((1.0 - self._blend) * self._value) + (self._blend * float(target)) + return self._value + + +def _clamped_offset(raw_offset) -> float: + try: + parsed = float(raw_offset) + except (TypeError, ValueError): + parsed = 0.0 + return float(np.clip(parsed, -MAX_CAMERA_OFFSET_METERS, MAX_CAMERA_OFFSET_METERS)) + + +def _camera_profile(sm): + return DEVICE_CAMERAS[(str(sm["deviceState"].deviceType), str(sm["roadCameraState"].sensor))] + + +def _calibration_height(sm) -> float: + from iqpilot.selfdrive.locationd.calibrationd import HEIGHT_SANE_MIN, HEIGHT_SANE_MAX + h = sm["extrinsicsCalibration"].height[0] if sm["extrinsicsCalibration"].height else 1.22 + return h if HEIGHT_SANE_MIN <= h <= HEIGHT_SANE_MAX else 1.22 + + +def _sheared_transform(model_transform, intrinsics, height: float, lateral_offset: float): + optical_center_y = intrinsics[1, 2] + projection_bias = np.eye(3, dtype=np.float32) + projection_bias[0, 1] = lateral_offset / height + projection_bias[0, 2] = -(lateral_offset / height) * optical_center_y + return (projection_bias @ model_transform).astype(np.float32) + + +class CameraOffsetHelper: + def __init__(self): + self.camera_offset = 0.0 + self.actual_camera_offset = 0.0 + self._smoother = _OffsetSmoother() + + @staticmethod + def apply_camera_offset(model_transform, intrinsics, height, offset_param): + return _sheared_transform(model_transform, intrinsics, height, offset_param) + + def set_offset(self, offset): + self.camera_offset = _clamped_offset(offset) + + def update(self, model_transform_main, model_transform_extra, sm, main_wide_camera, extra_uses_wide_camera=True): + self.actual_camera_offset = self._smoother.step(self.camera_offset) + camera_bundle = _camera_profile(sm) + camera_height = _calibration_height(sm) + main_intrinsics = camera_bundle.ecam.intrinsics if main_wide_camera else camera_bundle.fcam.intrinsics + extra_intrinsics = camera_bundle.ecam.intrinsics if extra_uses_wide_camera else camera_bundle.fcam.intrinsics + + return ( + self.apply_camera_offset(model_transform_main, main_intrinsics, camera_height, self.actual_camera_offset), + self.apply_camera_offset(model_transform_extra, extra_intrinsics, camera_height, self.actual_camera_offset), + ) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/config.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/config.py new file mode 100644 index 000000000..5b73c0115 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/config.py @@ -0,0 +1,133 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import numpy as np + + +def index_function(index: int, max_val: float = 192, max_idx: int = 32) -> float: + return max_val * ((index / max_idx) ** 2) + + +def _quadratic_series(limit: float, steps: int) -> list[float]: + return [index_function(index, max_val=limit, max_idx=steps - 1) for index in range(steps)] + + +def _probability_window(*values: float) -> np.ndarray: + return np.asarray(values, dtype=np.float32) + + +def _field_group(start: int, stop: int, stride: int) -> slice: + return slice(start, stop, stride) + + +_IDX_COUNT = 33 +_T_AXIS = _quadratic_series(10.0, _IDX_COUNT) +_X_AXIS = _quadratic_series(192.0, _IDX_COUNT) + + +class ModelConstants: + IDX_N = _IDX_COUNT + T_IDXS = _T_AXIS + X_IDXS = _X_AXIS + LEAD_T_IDXS = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] + LEAD_T_OFFSETS = [0.0, 2.0, 4.0] + META_T_IDXS = [2.0, 4.0, 6.0, 8.0, 10.0] + + MODEL_FREQ = 20 + FEATURE_LEN = 512 + FULL_HISTORY_BUFFER_LEN = 99 + HISTORY_BUFFER_LEN = FULL_HISTORY_BUFFER_LEN + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + NAV_FEATURE_LEN = 256 + NAV_INSTRUCTION_LEN = 150 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + FCW_THRESHOLDS_5MS2 = _probability_window(0.05, 0.05, 0.15, 0.15, 0.15) + FCW_THRESHOLDS_3MS2 = _probability_window(0.7, 0.7) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + SIM_POSE_WIDTH = 6 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + POLY_PATH_DEGREE = 4 + + +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + + +class Meta: + ENGAGED = _field_group(0, 1, 1) + GAS_DISENGAGE = _field_group(1, 31, 6) + BRAKE_DISENGAGE = _field_group(2, 31, 6) + STEER_OVERRIDE = _field_group(3, 31, 6) + HARD_BRAKE_3 = _field_group(4, 31, 6) + HARD_BRAKE_4 = _field_group(5, 31, 6) + HARD_BRAKE_5 = _field_group(6, 31, 6) + GAS_PRESS = _field_group(31, 55, 4) + BRAKE_PRESS = _field_group(32, 55, 4) + LEFT_BLINKER = _field_group(33, 55, 4) + RIGHT_BLINKER = _field_group(34, 55, 4) + + +class MetaTombRaider: + ENGAGED = _field_group(0, 1, 1) + GAS_DISENGAGE = _field_group(1, 41, 8) + BRAKE_DISENGAGE = _field_group(2, 41, 8) + STEER_OVERRIDE = _field_group(3, 41, 8) + HARD_BRAKE_3 = _field_group(4, 41, 8) + HARD_BRAKE_4 = _field_group(5, 41, 8) + HARD_BRAKE_5 = _field_group(6, 41, 8) + GAS_PRESS = _field_group(7, 41, 8) + BRAKE_PRESS = _field_group(8, 41, 8) + LEFT_BLINKER = _field_group(41, 53, 2) + RIGHT_BLINKER = _field_group(42, 53, 2) + + +class MetaSimPose: + ENGAGED = _field_group(0, 1, 1) + GAS_DISENGAGE = _field_group(1, 36, 7) + BRAKE_DISENGAGE = _field_group(2, 36, 7) + STEER_OVERRIDE = _field_group(3, 36, 7) + HARD_BRAKE_3 = _field_group(4, 36, 7) + HARD_BRAKE_4 = _field_group(5, 36, 7) + HARD_BRAKE_5 = _field_group(6, 36, 7) + GAS_PRESS = _field_group(7, 36, 7) + LEFT_BLINKER = _field_group(36, 48, 2) + RIGHT_BLINKER = _field_group(37, 48, 2) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py new file mode 100644 index 000000000..2f1edd563 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/daemon.py @@ -0,0 +1,766 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import sys +import time +from dataclasses import dataclass +from typing import Any + +import iqpilot.cereal.messaging as messaging +import numpy as np + +from iqpilot.cereal import car, custom, log +from iqpilot.cereal.messaging import PubMaster, SubMaster +from iqpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionBuf, VisionIpcClient +from iqdbc.car.car_helpers import get_demo_car_params +from setproctitle import setproctitle + +from iqpilot.common.filter_simple import FirstOrderFilter +from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter, PerfTraceRing +from iqpilot.common.params import Params +from iqpilot.common.realtime import DT_MDL, config_realtime_process +from iqpilot.common.swaglog import cloudlog +from iqpilot.common.transformations.camera import DEVICE_CAMERAS +from iqpilot.common.transformations.model import get_warp_matrix +from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from iqpilot.selfdrive.controls.lib.drive_helpers import ( + MODEL_SMOOTHING_MAX_TOTAL_SEC, + dynamic_lat_smooth_extra_seconds, + get_accel_from_plan, + smooth_value, +) +from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy +from iqpilot.system import sentry + +from iqpilot.common.steer_delay import lateral_action_delay +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle +from iqpilot.selfdrive.iqmodeld.models.inference_state import InferenceStateBase +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import get_model_runner +from iqpilot.selfdrive.iqmodeld.camera import CameraOffsetHelper +from iqpilot.selfdrive.iqmodeld.config import Plan +from iqpilot.selfdrive.iqmodeld.messaging import ( + DrivePacketMemory, + pick_curvature, + populate_drive_messages, + populate_odometry_message, +) +from iqpilot.selfdrive.iqmodeld.metadata import select_meta_layout + +try: + from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector, WarpContext +except ModuleNotFoundError: + class WarpContext: + def __init__(self, *args, **kwargs): + raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built") + + class RoadProjector: + def __init__(self, *args, **kwargs): + raise ModuleNotFoundError("iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx is not built") + + +PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.daemon" +IQP_NAV_MODEL_INFLUENCE_ENABLED = False +TurnDirection = custom.IQTurnSignalDirection +IQMODEL_EVAL_WARN_US = int(DT_MDL * 1_000_000) +IQMODEL_EVAL_ERROR_US = IQMODEL_EVAL_WARN_US * 2 +_FRAME_STARVED_BACKOFF_POLLS = 5 +_FRAME_STARVED_BACKOFF_SECONDS = 0.005 +_FRAME_STARVED_LOG_EVERY = 200 + + +def _plan_y_std_1s(outputs: dict[str, np.ndarray]) -> float: + # plan_stds is (batch, IDX_N, PLAN_WIDTH); index 10 ~= 1s ahead (see ModelConstants.T_IDXS), + # POSITION is an (x, y, z) slice within PLAN_WIDTH so [1] picks the lateral (y) std. + try: + return float(outputs["plan_stds"][0, 10, Plan.POSITION][1]) + except (KeyError, IndexError): + return 0.0 + + +def _model_lat_smooth_max_sec(params: Params) -> float: + if not params.get_bool("ModelSmoothingEnabled"): + return 0.0 + try: + raw = params.get("ModelLatSmoothSec", return_default=True) + raw = 0 if raw is None else int(raw) + except (ValueError, TypeError): + raw = 0 + return min(max(raw, 0), 30) * 0.01 + + +@dataclass +class CaptureStamp: + frame_id: int = 0 + timestamp_sof: int = 0 + timestamp_eof: int = 0 + + @classmethod + def from_vipc(cls, client: VisionIpcClient) -> "CaptureStamp": + return cls(client.frame_id, client.timestamp_sof, client.timestamp_eof) + + +@dataclass(frozen=True) +class StreamLayout: + dual_camera: bool + main_is_wide: bool + primary_stream: VisionStreamType + + +class ReplayLedger: + def __init__(self, tensor_shapes: dict[str, tuple[int, ...]], frame_inputs: list[str]): + self.inputs: dict[str, np.ndarray] = {} + self.archive: dict[str, np.ndarray] = {} + self.selectors: dict[str, np.ndarray] = {} + self._frame_inputs = set(frame_inputs) + self._pulse_name: str | None = None + self._pulse_memory: np.ndarray | None = None + + feature_shape = tensor_shapes.get("features_buffer") + for tensor_name, tensor_shape in tensor_shapes.items(): + if tensor_name in self._frame_inputs: + continue + + self.inputs[tensor_name] = np.zeros(tensor_shape, dtype=np.float32) + if len(tensor_shape) != 3 or tensor_shape[1] <= 1: + continue + + history_len = self._history_length(tensor_shape, feature_shape) + self.archive[tensor_name] = np.zeros((1, history_len, tensor_shape[2]), dtype=np.float32) + export_index = self._export_index(tensor_shape, history_len, feature_shape) + if export_index is not None: + self.selectors[tensor_name] = export_index + + if tensor_name.startswith("desire"): + self._pulse_name = tensor_name + self._pulse_memory = np.zeros(tensor_shape[2], dtype=np.float32) + + @staticmethod + def _history_length(tensor_shape: tuple[int, ...], feature_shape: tuple[int, ...] | None) -> int: + if tensor_shape[1] >= 99: + return tensor_shape[1] + if tensor_shape[1] in (24, 25) and feature_shape is not None and feature_shape[1] == 24: + return (feature_shape[1] + 1) * 4 + return tensor_shape[1] * 4 + + @staticmethod + def _export_index(tensor_shape: tuple[int, ...], history_len: int, + feature_shape: tuple[int, ...] | None) -> np.ndarray | None: + if tensor_shape[1] in (24, 25) and feature_shape is not None and feature_shape[1] == 24: + stride = int(-history_len / tensor_shape[1]) + return np.arange(stride, stride * (tensor_shape[1] + 1), stride)[::-1] + if tensor_shape[1] == 25: + skip = history_len // tensor_shape[1] + return np.arange(history_len)[-1 - (skip * (tensor_shape[1] - 1))::skip] + if tensor_shape[1] >= 99: + return np.arange(tensor_shape[1]) + return None + + @property + def pulse_name(self) -> str: + if self._pulse_name is None: + raise KeyError("No desire-like pulse input present in model inputs") + return self._pulse_name + + def _shift_archive(self, tensor_name: str) -> np.ndarray: + history = self.archive[tensor_name] + history[0, :-1] = history[0, 1:] + return history + + def inject_pulse(self, pulse_values: np.ndarray) -> None: + pulse = pulse_values.copy() + pulse[0] = 0 + assert self._pulse_memory is not None + rising = np.where(pulse - self._pulse_memory > 0.99, pulse, 0) + self._pulse_memory[:] = pulse + + history = self._shift_archive(self.pulse_name) + history[0, -1] = rising + exported_shape = self.inputs[self.pulse_name].shape + if history.shape[1] > exported_shape[1]: + stride = history.shape[1] // exported_shape[1] + self.inputs[self.pulse_name][:] = history[0].reshape( + exported_shape[0], exported_shape[1], stride, -1 + ).max(axis=2) + return + self.inputs[self.pulse_name][:] = history[0, self.selectors[self.pulse_name]] + + def merge_inputs(self, fresh_inputs: dict[str, np.ndarray]) -> None: + pulse_name = self.pulse_name + for tensor_name, tensor_value in fresh_inputs.items(): + if tensor_name in self.inputs and tensor_name != pulse_name: + self.inputs[tensor_name][:] = tensor_value + + def note_hidden_state(self, hidden_state: np.ndarray) -> None: + if "features_buffer" not in self.archive: + return + history = self._shift_archive("features_buffer") + history[0, -1] = hidden_state[0] + self.inputs["features_buffer"][:] = history[0, self.selectors["features_buffer"]] + + def note_feedback(self, tensor_name: str, values: np.ndarray, zero_export: bool = False) -> None: + if tensor_name not in self.archive: + return + history = self._shift_archive(tensor_name) + history[0, -1, :] = values[0] + exported = history[0, self.selectors[tensor_name]] + self.inputs[tensor_name][:] = 0 * exported if zero_export else exported + + +def _planplus_gain(vehicle_speed: float) -> float: + return 0.75 if vehicle_speed >= 25.0 else 1.0 + + +def _merged_plan(runtime_state: "NeuralEngineState", outputs: dict[str, np.ndarray], vehicle_speed: float) -> np.ndarray: + base_plan = outputs["plan"][0] + if "planplus" not in outputs: + return base_plan + return base_plan + (runtime_state.PLANPLUS_CONTROL * _planplus_gain(vehicle_speed)) * outputs["planplus"][0] + + +class NeuralEngineState(InferenceStateBase): + frames: dict[str, RoadProjector] + + def __init__(self, gpu_context: WarpContext): + super().__init__() + runner = get_model_runner() + bundle = get_active_bundle() + + self.model_runner = runner + self.constants = runner.constants + self.generation = bundle.generation if bundle is not None else None + + knob_values = {entry.key: entry.value for entry in bundle.overrides} if bundle is not None else {} + self.LAT_SMOOTH_SECONDS = float(knob_values.get("lat", ".0")) + self.LONG_SMOOTH_SECONDS = float(knob_values.get("long", ".0")) + self.MIN_LAT_CONTROL_SPEED = 0.3 + self.PLANPLUS_CONTROL = 1.0 + self.model_smoothing_max_extra_sec = 0.0 + + context_depth = 5 if runner.is_20hz else 2 + self.frames = { + stream_name: RoadProjector(gpu_context, context_depth) + for stream_name in runner.vision_input_names + } + + self._ledger = ReplayLedger(runner.input_shapes, runner.vision_input_names) + self.numpy_inputs = self._ledger.inputs + self.temporal_buffers = self._ledger.archive + self.temporal_idxs_map = self._ledger.selectors + + @property + def mlsim(self) -> bool: + return bool(self.generation is not None and self.generation >= 11) + + @property + def desire_key(self) -> str: + return self._ledger.pulse_name + + def _warp_frames(self, vision_bufs: dict[str, VisionBuf], + transform_map: dict[str, np.ndarray]) -> dict[str, Any]: + return { + stream_name: self.frames[stream_name].stage(vision_bufs[stream_name], transform_map[stream_name].flatten()) + for stream_name in self.model_runner.vision_input_names + } + + def _run_split_model(self) -> dict[str, np.ndarray]: + if hasattr(self.model_runner, "run_vision"): + vision_packet = self.model_runner.run_vision() + self._ledger.note_hidden_state(vision_packet["hidden_state"]) + self.model_runner.refresh_policy_features(self.numpy_inputs["features_buffer"]) + return {**vision_packet, **self.model_runner.run_policy()} + + result = self.model_runner.run_model() + if "hidden_state" in result: + self._ledger.note_hidden_state(result["hidden_state"]) + return result + + def _write_curvature_memory(self, outputs: dict[str, np.ndarray]) -> None: + if "desired_curvature" not in outputs: + return + + feedback_slot = None + if "prev_desired_curvs" in self.numpy_inputs: + feedback_slot = "prev_desired_curvs" + elif "prev_desired_curv" in self.numpy_inputs: + feedback_slot = "prev_desired_curv" + + if feedback_slot is not None: + self._ledger.note_feedback(feedback_slot, outputs["desired_curvature"], zero_export=self.mlsim) + + def run(self, vision_bufs: dict[str, VisionBuf], transform_map: dict[str, np.ndarray], + fresh_inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: + if not getattr(self.model_runner, "uses_opencl_warp", True): + return self.model_runner.run_fused(vision_bufs, transform_map, fresh_inputs) + + self._ledger.inject_pulse(fresh_inputs[self.desire_key]) + self._ledger.merge_inputs(fresh_inputs) + warped_frames = self._warp_frames(vision_bufs, transform_map) + self.model_runner.prepare_inputs(warped_frames, self.numpy_inputs, self.frames) + + outputs = self._run_split_model() + self._write_curvature_memory(outputs) + return outputs + + def get_action_from_model(self, outputs: dict[str, np.ndarray], previous_action: log.ModelDataV2.Action, + lat_action_t: float, long_action_t: float, vehicle_speed: float, + lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + if lat_smooth_seconds is None: + lat_smooth_seconds = self.LAT_SMOOTH_SECONDS + + if "action" in outputs: + curvature_cmd = outputs["action"][0, 0] / (max(1.0, vehicle_speed)) ** 2 + accel_cmd = outputs["action"][0, 1] + should_stop = bool(vehicle_speed < 0.3 and accel_cmd < 0.1) + + accel_cmd = smooth_value(accel_cmd, previous_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) + if vehicle_speed > self.MIN_LAT_CONTROL_SPEED: + curvature_cmd = smooth_value(curvature_cmd, previous_action.desiredCurvature, lat_smooth_seconds) + else: + curvature_cmd = previous_action.desiredCurvature + + return log.ModelDataV2.Action( + desiredCurvature=float(curvature_cmd), + desiredAcceleration=float(accel_cmd), + shouldStop=should_stop, + ) + + plan_rows = _merged_plan(self, outputs, vehicle_speed) + accel_cmd, should_stop = get_accel_from_plan( + plan_rows[:, Plan.VELOCITY][:, 0], + plan_rows[:, Plan.ACCELERATION][:, 0], + self.constants.T_IDXS, + action_t=long_action_t, + ) + accel_cmd = smooth_value(accel_cmd, previous_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) + + curvature_cmd = pick_curvature(outputs, plan_rows, vehicle_speed, lat_action_t, self.mlsim) + if self.generation is not None and self.generation >= 10: + if vehicle_speed > self.MIN_LAT_CONTROL_SPEED: + curvature_cmd = smooth_value(curvature_cmd, previous_action.desiredCurvature, lat_smooth_seconds) + else: + curvature_cmd = previous_action.desiredCurvature + + return log.ModelDataV2.Action( + desiredCurvature=float(curvature_cmd), + desiredAcceleration=float(accel_cmd), + shouldStop=bool(should_stop), + ) + + +class CameraIngress: + def __init__(self, gpu_context: WarpContext): + self.layout = self._discover_layout() + self._primary = VisionIpcClient("camerad", self.layout.primary_stream, True, gpu_context) + self._secondary = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False, gpu_context) + + while not self._primary.connect(False): + time.sleep(0.1) + while self.layout.dual_camera and not self._secondary.connect(False): + time.sleep(0.1) + + cloudlog.warning( + f"connected main cam with buffer size: {self._primary.buffer_len} ({self._primary.width} x {self._primary.height})" + ) + if self.layout.dual_camera: + cloudlog.warning( + f"connected extra cam with buffer size: {self._secondary.buffer_len} ({self._secondary.width} x {self._secondary.height})" + ) + + @staticmethod + def _discover_layout() -> StreamLayout: + while True: + available = VisionIpcClient.available_streams("camerad", block=False) + if available: + dual_camera = ( + VisionStreamType.VISION_STREAM_WIDE_ROAD in available + and VisionStreamType.VISION_STREAM_ROAD in available + ) + main_is_wide = VisionStreamType.VISION_STREAM_ROAD not in available + primary_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_is_wide else VisionStreamType.VISION_STREAM_ROAD + cloudlog.warning( + f"vision stream set up, main_wide_camera: {main_is_wide}, use_extra_client: {dual_camera}" + ) + return StreamLayout(dual_camera=dual_camera, main_is_wide=main_is_wide, primary_stream=primary_stream) + time.sleep(0.1) + + def pull(self) -> tuple[VisionBuf, VisionBuf, CaptureStamp, CaptureStamp] | None: + main_buf = None + wide_buf = None + main_stamp = CaptureStamp() + wide_stamp = CaptureStamp() + + while main_stamp.timestamp_sof < wide_stamp.timestamp_sof + 25000000: + main_buf = self._primary.recv() + main_stamp = CaptureStamp.from_vipc(self._primary) + if main_buf is None: + return None + + if not self.layout.dual_camera: + return main_buf, main_buf, main_stamp, main_stamp + + while True: + wide_buf = self._secondary.recv() + wide_stamp = CaptureStamp.from_vipc(self._secondary) + if wide_buf is None or main_stamp.timestamp_sof < wide_stamp.timestamp_sof + 25000000: + break + + if wide_buf is None: + return None + + if abs(main_stamp.timestamp_sof - wide_stamp.timestamp_sof) > 10000000: + cloudlog.error( + f"frames out of sync! main: {main_stamp.frame_id} ({main_stamp.timestamp_sof / 1e9:.5f})," + f" extra: {wide_stamp.frame_id} ({wide_stamp.timestamp_sof / 1e9:.5f})" + ) + return main_buf, wide_buf, main_stamp, wide_stamp + + +class CalibrationAtlas: + def __init__(self): + self.main_warp = np.zeros((3, 3), dtype=np.float32) + self.extra_warp = np.zeros((3, 3), dtype=np.float32) + self.ready = False + self._offset_tuner = CameraOffsetHelper() + + def set_offset(self, offset_value: Any) -> None: + self._offset_tuner.set_offset(offset_value) + + def refresh(self, sm: SubMaster, main_is_wide: bool, dual_camera: bool) -> tuple[np.ndarray, np.ndarray, bool]: + if not (sm.seen["extrinsicsCalibration"] and sm.seen["roadCameraState"] and sm.seen["deviceState"]): + return self.main_warp, self.extra_warp, self.ready + + rpy = get_calibrated_rpy(sm["extrinsicsCalibration"]) + if rpy is None: + live_calib = sm["extrinsicsCalibration"] + if len(live_calib.rpyCalib) == 3: + rpy = np.array(live_calib.rpyCalib, dtype=np.float32) + else: + rpy = np.zeros(3, dtype=np.float32) + + device_key = (str(sm["deviceState"].deviceType), str(sm["roadCameraState"].sensor)) + device_camera = DEVICE_CAMERAS[device_key] + main_intrinsics = device_camera.ecam.intrinsics if main_is_wide else device_camera.fcam.intrinsics + extra_uses_wide_camera = dual_camera or main_is_wide + extra_intrinsics = device_camera.ecam.intrinsics if extra_uses_wide_camera else device_camera.fcam.intrinsics + self.main_warp = get_warp_matrix(rpy, main_intrinsics, False).astype(np.float32) + self.extra_warp = get_warp_matrix(rpy, extra_intrinsics, True).astype(np.float32) + self.main_warp, self.extra_warp = self._offset_tuner.update( + self.main_warp, self.extra_warp, sm, main_is_wide, extra_uses_wide_camera + ) + self.ready = True + return self.main_warp, self.extra_warp, self.ready + + +class FrameDropMeter: + def __init__(self, model_freq: float): + self._smoother = FirstOrderFilter(0.0, 10.0, 1.0 / model_freq) + self._warm_frames = 0 + self._last_frame_id = 0 + + def sample(self, frame_id: int) -> tuple[int, float, bool]: + dropped = max(0, frame_id - self._last_frame_id - 1) + smooth = self._smoother.update(min(dropped, 10)) + if self._warm_frames < 10: + self._smoother.x = 0.0 + smooth = 0.0 + self._warm_frames += 1 + return dropped, smooth / (1 + smooth), dropped > 0 + + def commit(self, frame_id: int) -> None: + self._last_frame_id = frame_id + + +class InferenceDaemon: + def __init__(self, demo: bool = False, channel_path: str | None = None): + cloudlog.warning("iqmodeld init") + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process(7, 54) + + cloudlog.warning("setting up CL context") + self._gpu = WarpContext() + cloudlog.warning("CL context ready; loading model") + self._runtime = NeuralEngineState(self._gpu) + self._meta_layout = select_meta_layout() + cloudlog.warning("models loaded, iqmodeld starting") + + self._channel = None + if channel_path is not None: + from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel + self._channel = ModelChannel(channel_path, create=True) + + self._cameras = CameraIngress(self._gpu) + pub_services = ["iqPerfTrace"] if self._channel is not None else [ + "modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData", "iqPerfTrace"] + self._pub = PubMaster(pub_services) + self._sub = SubMaster([ + "deviceState", "carState", "roadCameraState", "extrinsicsCalibration", + "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState", + ]) + self._message_memory = DrivePacketMemory() + self._params = Params() + self._frame_meter = FrameDropMeter(self._runtime.constants.MODEL_FREQ) + self._warps = CalibrationAtlas() + self._perf = PerfTraceEmitter("iqmodeld", pubmaster=self._pub) + self._perf_ring = PerfTraceRing() + + self._car_params = self._load_car_params(demo) + self._long_action_delay = self._car_params.longitudinalActuatorDelay + self._runtime.LONG_SMOOTH_SECONDS + self._previous_action = log.ModelDataV2.Action() + self._desire_logic = DesireHelper() + self._lat_smooth_extra_sec = 0.0 + + def _load_car_params(self, demo: bool): + car_params = get_demo_car_params() if demo else messaging.log_from_bytes( + self._params.get("CarParams", block=True), car.CarParams) + cloudlog.info("iqmodeld got CarParams: %s", car_params.brand) + return car_params + + def _refresh_tunables(self, tick: int) -> None: + if tick % 60 != 0: + return + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + big_enabled = self._params.get_bool("IQEmacEnabled") or egpu_selected(self._params) + if big_enabled != (self._channel is not None): + # publish mode is fixed at startup: staying up would fight the selector for modelV2 + cloudlog.warning("iqmodeld: big backend toggled, restarting to switch publish mode") + sys.exit(0) + self._runtime.lat_delay = lateral_action_delay(self._params, self._car_params, self._sub["lateralDelay"].lateralDelay) + self._runtime.PLANPLUS_CONTROL = self._params.get("PlanplusControl", return_default=True) + self._runtime.model_smoothing_max_extra_sec = _model_lat_smooth_max_sec(self._params) + self._warps.set_offset(self._params.get("CameraOffset", return_default=True)) + + def _traffic_side(self) -> np.ndarray: + traffic = np.zeros(2, dtype=np.float32) + traffic[int(self._sub["driverMonitoringState"].isRHD)] = 1 + return traffic + + def _desire_pulse(self) -> np.ndarray: + pulse = np.zeros(self._runtime.constants.DESIRE_LEN, dtype=np.float32) + desire_idx = self._desire_logic.desire + if 0 <= desire_idx < self._runtime.constants.DESIRE_LEN: + pulse[desire_idx] = 1 + return pulse + + def _compose_inputs(self, vehicle_speed: float, lat_horizon: float, long_horizon: float) -> dict[str, np.ndarray]: + inputs: dict[str, np.ndarray] = { + self._runtime.desire_key: self._desire_pulse(), + "traffic_convention": self._traffic_side(), + } + if "lateral_control_params" in self._runtime.numpy_inputs: + inputs["lateral_control_params"] = np.array([vehicle_speed, lat_horizon], dtype=np.float32) + if "action_t" in self._runtime.numpy_inputs: + inputs["action_t"] = np.array([lat_horizon, long_horizon], dtype=np.float32) + return inputs + + def _publish(self, outputs: dict[str, np.ndarray], main_stamp: CaptureStamp, extra_stamp: CaptureStamp, + road_frame_id: int, frame_drop_ratio: float, dropped_frames: int, + execution_time: float, live_calib_seen: bool, + lat_horizon: float, long_horizon: float, vehicle_speed: float) -> None: + model_msg = messaging.new_message("modelV2") + driving_msg = messaging.new_message("drivingModelData") + pose_msg = messaging.new_message("cameraOdometry") + iq_msg = messaging.new_message("iqDriveModelData") + + self._lat_smooth_extra_sec = dynamic_lat_smooth_extra_seconds( + _plan_y_std_1s(outputs), self._runtime.model_smoothing_max_extra_sec + ) + lat_smooth_total_sec = min(self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec, MODEL_SMOOTHING_MAX_TOTAL_SEC) + action = self._runtime.get_action_from_model( + outputs, self._previous_action, lat_horizon, long_horizon, vehicle_speed, lat_smooth_total_sec + ) + self._previous_action = action + + populate_drive_messages( + driving_msg, + model_msg, + outputs, + action, + self._message_memory, + main_stamp.frame_id, + extra_stamp.frame_id, + road_frame_id, + frame_drop_ratio, + main_stamp.timestamp_eof, + execution_time, + live_calib_seen, + self._meta_layout, + ) + + desire_state = model_msg.modelV2.meta.desireState + lane_change_prob = desire_state[log.Desire.laneChangeLeft] + desire_state[log.Desire.laneChangeRight] + self._desire_logic.update( + self._sub["carState"], + self._sub["carControl"].latActive, + lane_change_prob, + self._sub["iqNavState"], + model_msg.modelV2, + self._sub["radarState"], + ) + model_msg.modelV2.meta.laneChangeState = self._desire_logic.lane_change_state + model_msg.modelV2.meta.laneChangeDirection = self._desire_logic.lane_change_direction + driving_msg.drivingModelData.meta.laneChangeState = self._desire_logic.lane_change_state + driving_msg.drivingModelData.meta.laneChangeDirection = self._desire_logic.lane_change_direction + iq_msg.iqDriveModelData.turnSignalDirection = self._desire_logic.lane_turn_direction + + populate_odometry_message( + pose_msg, + outputs, + main_stamp.frame_id, + dropped_frames, + main_stamp.timestamp_eof, + live_calib_seen, + ) + + if self._channel is not None: + self._channel.write(main_stamp.frame_id, { + "source": "small", + "frame_id": main_stamp.frame_id, + "timestamp_sof": int(main_stamp.timestamp_sof), + "live_calib_seen": bool(live_calib_seen), + "model_execution_time": float(execution_time), + "msgs": { + "modelV2": model_msg.to_bytes(), + "drivingModelData": driving_msg.to_bytes(), + "cameraOdometry": pose_msg.to_bytes(), + "iqDriveModelData": iq_msg.to_bytes(), + }, + }) + return + + self._pub.send("modelV2", model_msg) + self._pub.send("drivingModelData", driving_msg) + self._pub.send("cameraOdometry", pose_msg) + self._pub.send("iqDriveModelData", iq_msg) + + def serve(self) -> None: + tick = 0 + starved_polls = 0 + while True: + frame_pair = self._cameras.pull() + if frame_pair is None: + starved_polls += 1 + if starved_polls >= _FRAME_STARVED_BACKOFF_POLLS: + time.sleep(_FRAME_STARVED_BACKOFF_SECONDS) + if starved_polls % _FRAME_STARVED_LOG_EVERY == 0: + cloudlog.error(f"visionipc delivered no frames for {starved_polls} polls; model is not running") + continue + + if starved_polls: + cloudlog.warning(f"visionipc recovered after {starved_polls} frameless polls") + starved_polls = 0 + + main_buf, extra_buf, main_stamp, extra_stamp = frame_pair + self._sub.update(0) + self._refresh_tunables(tick) + + vehicle_speed = max(self._sub["carState"].vEgo, 0.0) + lat_horizon = self._runtime.lat_delay + self._runtime.LAT_SMOOTH_SECONDS + self._lat_smooth_extra_sec + DT_MDL + long_horizon = self._long_action_delay + DT_MDL + + main_warp, extra_warp, live_calib_seen = self._warps.refresh( + self._sub, self._cameras.layout.main_is_wide, self._cameras.layout.dual_camera + ) + dropped_frames, frame_drop_ratio, prepare_only = self._frame_meter.sample(main_stamp.frame_id) + + vision_bufs = { + stream_name: extra_buf if "big" in stream_name else main_buf + for stream_name in self._runtime.model_runner.vision_input_names + } + warp_map = { + stream_name: extra_warp if "big" in stream_name else main_warp + for stream_name in self._runtime.model_runner.vision_input_names + } + fresh_inputs = self._compose_inputs(vehicle_speed, lat_horizon, long_horizon) + + started_at = time.perf_counter() + outputs = self._runtime.run(vision_bufs, warp_map, fresh_inputs) + execution_time = time.perf_counter() - started_at + execution_us = int(execution_time * 1_000_000) + + sample = PerfSample( + frame_id=main_stamp.frame_id, + model_eval_us=execution_us, + model_dropped_frames=dropped_frames, + model_backlog=max(0, dropped_frames), + ) + self._perf_ring.push(sample) + if dropped_frames > 0 or execution_us >= IQMODEL_EVAL_WARN_US: + severity = "warning" + if dropped_frames > 0 or execution_us >= IQMODEL_EVAL_ERROR_US: + severity = "error" + self._perf.emit( + "iqmodeld_dropped_frames" if dropped_frames > 0 else "iqmodeld_slow_eval", + severity=severity, + frame_id=main_stamp.frame_id, + total_time_us=execution_us, + dropped_frames=dropped_frames, + backlog=max(0, dropped_frames), + samples=self._perf_ring.snapshot(), + detail=( + f"model_eval_us={execution_us} dropped_frames={dropped_frames} prepare_only={int(prepare_only)} " + f"road_frame_id={self._sub['roadCameraState'].frameId}" + ), + min_interval_s=0.25, + ) + + if outputs is not None: + self._publish( + outputs, + main_stamp, + extra_stamp, + self._sub["roadCameraState"].frameId, + frame_drop_ratio, + dropped_frames, + execution_time, + live_calib_seen, + lat_horizon, + long_horizon, + vehicle_speed, + ) + + self._frame_meter.commit(main_stamp.frame_id) + tick += 1 + + +def main(demo: bool = False, channel_path: str | None = "auto"): + if channel_path == "auto": + channel_path = None + params = Params() + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + if params.get_bool("IQEmacEnabled") or egpu_selected(params): + from iqpilot.selfdrive.iqmodeld.model_channel import SMALL_CHANNEL + channel_path = SMALL_CHANNEL + InferenceDaemon(demo=demo, channel_path=channel_path).serve() + + +__all__ = [ + "PROCESS_NAME", + "IQP_NAV_MODEL_INFLUENCE_ENABLED", + "TurnDirection", + "CaptureStamp", + "ReplayLedger", + "NeuralEngineState", + "CameraIngress", + "CalibrationAtlas", + "FrameDropMeter", + "InferenceDaemon", + "main", +] + + +if __name__ == "__main__": + try: + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--demo", action="store_true", help="Run iqmodeld in demo mode.") + args = parser.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") + except Exception: + sentry.capture_exception() + raise diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py new file mode 100644 index 000000000..3e5f9750c --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/driving_action.py @@ -0,0 +1,46 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import numpy as np + +from iqpilot.cereal import log + +from iqpilot.selfdrive.controls.lib.drive_helpers import smooth_value + +LAT_SMOOTH_SECONDS = 0.0 +LONG_SMOOTH_SECONDS = 0.3 +MIN_LAT_CONTROL_SPEED = 0.3 +DESIRE_LEN = 8 + + +def get_action_from_model(outputs: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, + v_ego: float, lat_action_t: float, long_action_t: float, + lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + if "action" in outputs: + desired_accel = float(outputs["action"][0, 1]) + desired_curvature = float(outputs["action"][0, 0]) / (max(1.0, v_ego)) ** 2 + should_stop = bool(v_ego < 0.3 and desired_accel < 0.1) + else: + from iqpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan + from iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan + plan = outputs["plan"][0] + desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], + plan[:, Plan.ACCELERATION][:, 0], + ModelConstants.T_IDXS, + action_t=long_action_t) + desired_curvature = get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], + plan[:, Plan.ORIENTATION_RATE][:, 2], + ModelConstants.T_IDXS, v_ego, lat_action_t) + desired_accel, should_stop = float(desired_accel), bool(should_stop) + desired_curvature = float(desired_curvature) + desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) + if v_ego > MIN_LAT_CONTROL_SPEED: + lat_smooth = LAT_SMOOTH_SECONDS if lat_smooth_seconds is None else lat_smooth_seconds + desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, lat_smooth) + else: + desired_curvature = prev_action.desiredCurvature + return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature), + desiredAcceleration=float(desired_accel), + shouldStop=should_stop) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py new file mode 100644 index 000000000..d09895c86 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_helpers.py @@ -0,0 +1,158 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import hashlib +import json +import os +import urllib.request +from pathlib import Path + +from iqpilot.system.hardware.usb import egpu_dock_ready + +USB_SYSFS_ROOT = "/sys/bus/usb/devices" + +COMMA_LFS_BATCH_URL = "https://gitlab.com/commaai/openpilot-lfs.git/info/lfs/objects/batch" + +DOWNLOAD_CHUNK = 4 * 1024 * 1024 + + +def usbgpu_present(sysfs_root: str = USB_SYSFS_ROOT) -> bool: + return egpu_dock_ready(Path(sysfs_root)) + + +def egpu_present_consented(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool: + try: + if params is not None and params.get_bool("IQEgpuDisabled"): + return False + except Exception: + pass + return usbgpu_present(sysfs_root) + + +def egpu_selected(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool: + try: + if params is not None and params.get_bool("IQEgpuDisabled"): + return False + if params is not None and params.get_bool("IQEgpuEnabled"): + return True + except Exception: + pass + return usbgpu_present(sysfs_root) + + +def resolve_backend(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> str | None: + if egpu_present: + return "egpu" + if emac_enabled: + return "emac" + if egpu_enabled: + return "egpu" + return None + + +def egpu_pkl_path(meta: dict) -> str: + from iqpilot.system.hardware.hw import Paths + return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_tinygrad.pkl") + + +def onnx_cache_path(meta: dict) -> str: + from iqpilot.system.hardware.hw import Paths + return os.path.join(Paths.model_root(), f"{meta['model_name']}_{meta['sha256'][:8]}.onnx") + + +def _sha256_file(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as f: + while chunk := f.read(DOWNLOAD_CHUNK): + digest.update(chunk) + return digest.hexdigest() + + +def quarantine_artifact(path: str, why: str) -> None: + try: + if os.path.isfile(path): + os.replace(path, path + ".unusable") + except OSError: + try: + os.remove(path) + except OSError: + pass + + +def local_onnx(meta: dict) -> str | None: + path = onnx_cache_path(meta) + if not os.path.isfile(path): + return None + size = int(meta.get("download", {}).get("size", 0)) + if size and os.path.getsize(path) != size: + quarantine_artifact(path, "onnx size mismatch") + return None + if _sha256_file(path) != meta["sha256"]: + quarantine_artifact(path, "onnx sha256 mismatch") + return None + return path + + +def resolve_download_url(download_url: str, sha256: str, size: int, timeout: float = 30.0) -> str: + if download_url.startswith("commalfs:"): + oid = download_url.split(":", 1)[1] + body = json.dumps({"operation": "download", "transfers": ["basic"], + "objects": [{"oid": oid, "size": size}]}).encode() + req = urllib.request.Request(COMMA_LFS_BATCH_URL, data=body, headers={ + "Accept": "application/vnd.git-lfs+json", "Content-Type": "application/vnd.git-lfs+json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + return d["objects"][0]["actions"]["download"]["href"] + return download_url + + +def download_onnx(meta: dict, progress_cb=None) -> str: + from iqpilot.selfdrive.iqmodeld.egpu_model import download_descriptor + download_url, size = download_descriptor(meta) + if not download_url: + raise RuntimeError(f"model {meta['key']} has no download source; stage the onnx at {onnx_cache_path(meta)}") + + url = resolve_download_url(download_url, meta["sha256"], size) + path = onnx_cache_path(meta) + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + ".part" + digest = hashlib.sha256() + got = 0 + with urllib.request.urlopen(url, timeout=60) as r, open(tmp, "wb") as f: + while chunk := r.read(DOWNLOAD_CHUNK): + f.write(chunk) + digest.update(chunk) + got += len(chunk) + if progress_cb is not None and size: + progress_cb(got / size) + if size and got != size: + os.remove(tmp) + raise RuntimeError(f"onnx download truncated: {got}/{size} bytes") + if digest.hexdigest() != meta["sha256"]: + os.remove(tmp) + raise RuntimeError(f"onnx sha256 mismatch for {meta['key']}") + os.replace(tmp, path) + return path + + +def patch_tinygrad_fetch_fw() -> None: + import pathlib + + import zstandard + from tinygrad import helpers + if getattr(helpers.fetch_fw, "_iq_patched", False): + return + _orig = helpers.fetch_fw + + def fetch_fw(path, name, sha256): + p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst") + if p.is_file(): + blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read() + if hashlib.sha256(blob).hexdigest() == sha256: + return blob + return _orig(path, name, sha256) + + fetch_fw._iq_patched = True + helpers.fetch_fw = fetch_fw diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py new file mode 100644 index 000000000..b9dc2c77e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/egpu_model.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.egpu_model") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.egpu_model import * diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/emac_model_meta.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/emac_model_meta.py new file mode 100644 index 000000000..75f405d67 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/emac_model_meta.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.emac_model_meta") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.emac_model_meta import * diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/messaging.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/messaging.py new file mode 100644 index 000000000..ca43b2702 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/messaging.py @@ -0,0 +1,269 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass, field + +import capnp +import numpy as np + +from iqpilot.cereal import log +from iqpilot.selfdrive.iqmodeld.models.helpers import plan_x_idxs_helper +from iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan +from iqpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan + +SEND_RAW_PRED = os.getenv("SEND_RAW_PRED") +ConfidenceClass = log.ModelDataV2.ConfidenceClass + + +def pick_curvature(outputs: dict[str, np.ndarray], plan_rows: np.ndarray, vehicle_speed: float, + action_horizon: float, synthetic_lane_logic: bool) -> float: + direct_signal = None if synthetic_lane_logic else outputs.get("desired_curvature") + if direct_signal is not None: + return float(direct_signal[0, 0]) + + yaw_track = plan_rows[:, Plan.T_FROM_CURRENT_EULER][:, 2] + yaw_rate_track = plan_rows[:, Plan.ORIENTATION_RATE][:, 2] + return float(get_curvature_from_plan(yaw_track, yaw_rate_track, ModelConstants.T_IDXS, vehicle_speed, action_horizon)) + + +@dataclass +class DrivePacketMemory: + disengage_rollup: np.ndarray = field(default_factory=lambda: np.zeros( + ModelConstants.CONFIDENCE_BUFFER_LEN * ModelConstants.DISENGAGE_WIDTH, dtype=np.float32)) + brake_watch_5: np.ndarray = field(default_factory=lambda: np.zeros( + ModelConstants.FCW_5MS2_PROBS_WIDTH, dtype=np.float32)) + brake_watch_3: np.ndarray = field(default_factory=lambda: np.zeros( + ModelConstants.FCW_3MS2_PROBS_WIDTH, dtype=np.float32)) + + +def _assign_xyz(builder, t_points, x_track, y_track, z_track, + x_std=None, y_std=None, z_std=None) -> None: + builder.t = t_points + builder.x = x_track.tolist() + builder.y = y_track.tolist() + builder.z = z_track.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if z_std is not None: + builder.zStd = z_std.tolist() + + +def _assign_xyva(builder, t_points, x_track, y_track, v_track, a_track, + x_std=None, y_std=None, v_std=None, a_std=None) -> None: + builder.t = t_points + builder.x = x_track.tolist() + builder.y = y_track.tolist() + builder.v = v_track.tolist() + builder.a = a_track.tolist() + if x_std is not None: + builder.xStd = x_std.tolist() + if y_std is not None: + builder.yStd = y_std.tolist() + if v_std is not None: + builder.vStd = v_std.tolist() + if a_std is not None: + builder.aStd = a_std.tolist() + + +def fill_xyz_poly(builder, degree: int, x_track: np.ndarray, y_track: np.ndarray, z_track: np.ndarray) -> None: + stacked = np.stack([x_track, y_track, z_track], axis=1) + coeffs = np.polynomial.polynomial.polyfit(ModelConstants.T_IDXS, stacked, deg=degree) + builder.xCoefficients = coeffs[:, 0].tolist() + builder.yCoefficients = coeffs[:, 1].tolist() + builder.zCoefficients = coeffs[:, 2].tolist() + + +def fill_lane_line_meta(builder, lane_lines, lane_probs: list[float]) -> None: + builder.leftY = lane_lines[1].y[0] + builder.leftProb = lane_probs[1] + builder.rightY = lane_lines[2].y[0] + builder.rightProb = lane_probs[2] + + +def _roll_brake_watch(outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout) -> bool: + memory.brake_watch_5[:-1] = memory.brake_watch_5[1:] + memory.brake_watch_5[-1] = outputs["meta"][0, meta_layout.HARD_BRAKE_5][0] + memory.brake_watch_3[:-1] = memory.brake_watch_3[1:] + memory.brake_watch_3[-1] = outputs["meta"][0, meta_layout.HARD_BRAKE_3][0] + return bool( + (memory.brake_watch_5 > ModelConstants.FCW_THRESHOLDS_5MS2).all() + and (memory.brake_watch_3 > ModelConstants.FCW_THRESHOLDS_3MS2).all() + ) + + +def _confidence_bucket(outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout, frame_id: int): + width = ModelConstants.DISENGAGE_WIDTH + if frame_id % (2 * ModelConstants.MODEL_FREQ) == 0: + brake_probs = outputs["meta"][0, meta_layout.BRAKE_DISENGAGE] + gas_probs = outputs["meta"][0, meta_layout.GAS_DISENGAGE] + steer_probs = outputs["meta"][0, meta_layout.STEER_OVERRIDE] + takeover_curve = 1 - ((1 - brake_probs) * (1 - gas_probs) * (1 - steer_probs)) + independent = np.r_[takeover_curve[0], np.diff(takeover_curve) / (1 - takeover_curve[:-1])] + memory.disengage_rollup[:-width] = memory.disengage_rollup[width:] + memory.disengage_rollup[-width:] = independent + + score = 0.0 + for idx in range(width): + score += memory.disengage_rollup[idx * width + width - 1 - idx].item() / width + + if score < ModelConstants.RYG_GREEN: + return ConfidenceClass.green + if score < ModelConstants.RYG_YELLOW: + return ConfidenceClass.yellow + return ConfidenceClass.red + + +def _write_plan_family(model_packet, driving_packet, outputs: dict[str, np.ndarray]) -> None: + plan_rows = outputs["plan"][0] + plan_stds = outputs["plan_stds"][0] + _assign_xyz(model_packet.position, ModelConstants.T_IDXS, *plan_rows[:, Plan.POSITION].T, *plan_stds[:, Plan.POSITION].T) + _assign_xyz(model_packet.velocity, ModelConstants.T_IDXS, *plan_rows[:, Plan.VELOCITY].T) + _assign_xyz(model_packet.acceleration, ModelConstants.T_IDXS, *plan_rows[:, Plan.ACCELERATION].T) + _assign_xyz(model_packet.orientation, ModelConstants.T_IDXS, *plan_rows[:, Plan.T_FROM_CURRENT_EULER].T) + _assign_xyz(model_packet.orientationRate, ModelConstants.T_IDXS, *plan_rows[:, Plan.ORIENTATION_RATE].T) + fill_xyz_poly(driving_packet.path, ModelConstants.POLY_PATH_DEGREE, *plan_rows[:, Plan.POSITION].T) + + +def _write_temporal_pose(model_packet, outputs: dict[str, np.ndarray]) -> None: + pose_packet = model_packet.temporalPoseDEPRECATED + if "sim_pose" in outputs: + half_width = ModelConstants.POSE_WIDTH // 2 + pose_packet.trans = outputs["sim_pose"][0, :half_width].tolist() + pose_packet.transStd = outputs["sim_pose_stds"][0, :half_width].tolist() + pose_packet.rot = outputs["sim_pose"][0, half_width:].tolist() + pose_packet.rotStd = outputs["sim_pose_stds"][0, half_width:].tolist() + return + + pose_packet.trans = outputs["plan"][0, 0, Plan.VELOCITY].tolist() + pose_packet.transStd = outputs["plan_stds"][0, 0, Plan.VELOCITY].tolist() + pose_packet.rot = outputs["plan"][0, 0, Plan.ORIENTATION_RATE].tolist() + pose_packet.rotStd = outputs["plan_stds"][0, 0, Plan.ORIENTATION_RATE].tolist() + + +def _write_lane_family(model_packet, driving_packet, outputs: dict[str, np.ndarray]) -> None: + time_axis = plan_x_idxs_helper(ModelConstants, Plan, outputs) + model_packet.init("laneLines", 4) + for lane_idx in range(4): + lane_builder = model_packet.laneLines[lane_idx] + _assign_xyz( + lane_builder, + time_axis, + np.array(ModelConstants.X_IDXS), + outputs["lane_lines"][0, lane_idx, :, 0], + outputs["lane_lines"][0, lane_idx, :, 1], + ) + model_packet.laneLineStds = outputs["lane_lines_stds"][0, :, 0, 0].tolist() + model_packet.laneLineProbs = outputs["lane_lines_prob"][0, 1::2].tolist() + fill_lane_line_meta(driving_packet.laneLineMeta, model_packet.laneLines, model_packet.laneLineProbs) + + model_packet.init("roadEdges", 2) + for edge_idx in range(2): + edge_builder = model_packet.roadEdges[edge_idx] + _assign_xyz( + edge_builder, + time_axis, + np.array(ModelConstants.X_IDXS), + outputs["road_edges"][0, edge_idx, :, 0], + outputs["road_edges"][0, edge_idx, :, 1], + ) + model_packet.roadEdgeStds = outputs["road_edges_stds"][0, :, 0, 0].tolist() + + +def _write_leads(model_packet, outputs: dict[str, np.ndarray]) -> None: + model_packet.init("leadsV3", 3) + for lead_idx in range(3): + lead_builder = model_packet.leadsV3[lead_idx] + _assign_xyva( + lead_builder, + ModelConstants.LEAD_T_IDXS, + *outputs["lead"][0, lead_idx].T, + *outputs["lead_stds"][0, lead_idx].T, + ) + lead_builder.prob = outputs["lead_prob"][0, lead_idx].tolist() + lead_builder.probTime = ModelConstants.LEAD_T_OFFSETS[lead_idx] + + +def _write_meta(model_packet, outputs: dict[str, np.ndarray], memory: DrivePacketMemory, meta_layout, frame_id: int) -> None: + meta = model_packet.meta + meta.desireState = outputs["desire_state"][0].reshape(-1).tolist() + meta.desirePrediction = outputs["desire_pred"][0].reshape(-1).tolist() + meta.engagedProb = outputs["meta"][0, meta_layout.ENGAGED].item() + meta.init("disengagePredictions") + + pred = meta.disengagePredictions + pred.t = ModelConstants.META_T_IDXS + pred.brakeDisengageProbs = outputs["meta"][0, meta_layout.BRAKE_DISENGAGE].tolist() + pred.gasDisengageProbs = outputs["meta"][0, meta_layout.GAS_DISENGAGE].tolist() + pred.steerOverrideProbs = outputs["meta"][0, meta_layout.STEER_OVERRIDE].tolist() + pred.brake3MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_3].tolist() + pred.brake4MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_4].tolist() + pred.brake5MetersPerSecondSquaredProbs = outputs["meta"][0, meta_layout.HARD_BRAKE_5].tolist() + + if hasattr(meta_layout, "GAS_PRESS") and hasattr(meta_layout, "BRAKE_PRESS"): + pred.gasPressProbs = outputs["meta"][0, meta_layout.GAS_PRESS].tolist() + pred.brakePressProbs = outputs["meta"][0, meta_layout.BRAKE_PRESS].tolist() + + meta.hardBrakePredicted = _roll_brake_watch(outputs, memory, meta_layout) + model_packet.confidence = _confidence_bucket(outputs, memory, meta_layout, frame_id) + + +def populate_drive_messages(primary_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, + outputs: dict[str, np.ndarray], action: log.ModelDataV2.Action, + memory: DrivePacketMemory, vipc_frame_id: int, vipc_frame_id_extra: int, + frame_id: int, frame_drop: float, timestamp_eof: int, + model_execution_time: float, valid: bool, meta_layout) -> None: + frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0 + frame_drop_percent = frame_drop * 100 + primary_msg.valid = valid + extended_msg.valid = valid + + driving_packet = primary_msg.drivingModelData + driving_packet.frameId = vipc_frame_id + driving_packet.frameIdExtra = vipc_frame_id_extra + driving_packet.frameDropPerc = frame_drop_percent + driving_packet.modelExecutionTime = model_execution_time + driving_packet.action = action + + model_packet = extended_msg.modelV2 + model_packet.frameId = vipc_frame_id + model_packet.frameIdExtra = vipc_frame_id_extra + model_packet.frameAge = frame_age + model_packet.frameDropPerc = frame_drop_percent + model_packet.timestampEof = timestamp_eof + model_packet.modelExecutionTime = model_execution_time + model_packet.action = action + + _write_plan_family(model_packet, driving_packet, outputs) + _write_temporal_pose(model_packet, outputs) + _write_lane_family(model_packet, driving_packet, outputs) + _write_leads(model_packet, outputs) + _write_meta(model_packet, outputs, memory, meta_layout, vipc_frame_id) + + if SEND_RAW_PRED: + model_packet.rawPredictions = outputs["raw_pred"].tobytes() + + +def populate_odometry_message(msg: capnp._DynamicStructBuilder, outputs: dict[str, np.ndarray], + vipc_frame_id: int, vipc_dropped_frames: int, + timestamp_eof: int, live_calib_seen: bool) -> None: + msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + odo = msg.cameraOdometry + odo.frameId = vipc_frame_id + odo.timestampEof = timestamp_eof + odo.trans = outputs["pose"][0, :3].tolist() + odo.rot = outputs["pose"][0, 3:].tolist() + odo.wideFromDeviceEuler = outputs["wide_from_device_euler"][0, :].tolist() + odo.roadTransformTrans = outputs["road_transform"][0, :3].tolist() + odo.transStd = outputs["pose_stds"][0, :3].tolist() + odo.rotStd = outputs["pose_stds"][0, 3:].tolist() + odo.wideFromDeviceEulerStd = outputs["wide_from_device_euler_stds"][0, :].tolist() + odo.roadTransformTransStd = outputs["road_transform_stds"][0, :3].tolist() + +__all__ = [ + "DrivePacketMemory", + "pick_curvature", + "populate_drive_messages", + "populate_odometry_message", +] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/metadata.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/metadata.py new file mode 100644 index 000000000..c81ebe542 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/metadata.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +import codecs +import pathlib +import pickle +import sys +from collections.abc import Iterable +from typing import Any + +from iqpilot.cereal import custom +from tinygrad.nn.onnx import OnnxPBParser + +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle +from iqpilot.selfdrive.iqmodeld.config import Meta + + +ModelBundle = custom.IQModelManager.ModelBundle + + +def _blank_proto_doc() -> dict[str, Any]: + return {"graph": {"input": [], "output": []}, "metadata_props": []} + + +class TelemetryEnvelopeParser(OnnxPBParser): + def _parse_ModelProto(self) -> dict: + envelope = _blank_proto_doc() + for fid, wire_type in self._parse_message(self.reader.len): + if fid == 7: + envelope["graph"] = self._parse_GraphProto() + elif fid == 14: + envelope["metadata_props"].append(self._parse_StringStringEntryProto()) + else: + self.reader.skip_field(wire_type) + return envelope + + +def _shape_fingerprint(value_info: dict[str, Any]) -> tuple[str, tuple[int, ...]]: + resolved = [] + for axis in value_info["parsed_type"].shape: + resolved.append(int(axis) if isinstance(axis, int) else 0) + return value_info["name"], tuple(resolved) + + +def _lookup_metadata(props: Iterable[dict[str, Any]], wanted_key: str) -> str | Any: + for entry in props: + if entry["key"] == wanted_key: + return entry["value"] + return None + + +class Meta20hz(Meta): + ENGAGED = slice(0, 1) + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) + + +def select_meta_layout(): + active_bundle = get_active_bundle() + return Meta20hz if active_bundle is not None and active_bundle.is20hz else Meta + + +def _decoded_slices(props: Iterable[dict[str, Any]]): + encoded = _lookup_metadata(props, "output_slices") + assert encoded is not None, "output_slices not found in metadata" + return pickle.loads(codecs.decode(encoded.encode(), "base64")) + + +def _graph_shape_table(graph_doc: dict[str, Any], field_name: str) -> dict[str, tuple[int, ...]]: + return dict(_shape_fingerprint(item) for item in graph_doc[field_name]) + + +def build_metadata_record(model_path): + parsed = TelemetryEnvelopeParser(model_path).parse() + props = parsed["metadata_props"] + graph = parsed["graph"] + return { + "model_checkpoint": _lookup_metadata(props, "model_checkpoint"), + "output_slices": _decoded_slices(props), + "input_shapes": _graph_shape_table(graph, "input"), + "output_shapes": _graph_shape_table(graph, "output"), + } + + +if __name__ == "__main__": + model_path = pathlib.Path(sys.argv[1]) + metadata_path = model_path.parent / f"{model_path.stem}_metadata.pkl" + with open(metadata_path, "wb") as handle: + pickle.dump(build_metadata_record(model_path), handle) + print(f"saved metadata to {metadata_path}") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py new file mode 100644 index 000000000..a9fa710bc --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_channel.py @@ -0,0 +1,58 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import mmap +import os +import pickle +import struct + +from iqpilot.common.swaglog import cloudlog + +SMALL_CHANNEL = "/dev/shm/iqpilot_smallmodel" +BIG_CHANNEL = "/dev/shm/iqpilot_bigmodel" +SHM_SIZE = 8 * 1024 * 1024 +HEADER = struct.Struct(" None: + data = pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL) + if HEADER.size + len(data) > SHM_SIZE: + cloudlog.error(f"model payload {len(data)} bytes exceeds shm {SHM_SIZE}, dropping frame {frame_id}") + return + seq = HEADER.unpack(self.mm[:HEADER.size])[0] + HEADER.pack_into(self.mm, 0, seq + 1, frame_id, len(data)) + self.mm[HEADER.size:HEADER.size + len(data)] = data + HEADER.pack_into(self.mm, 0, seq + 2, frame_id, len(data)) + + def peek_frame_id(self) -> int | None: + seq, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) + if seq == 0 or seq % 2 != 0 or length == 0: + return None + return frame_id + + def read(self) -> tuple[int, dict] | None: + seq1, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) + if seq1 == 0 or seq1 % 2 != 0 or length == 0: + return None + data = bytes(self.mm[HEADER.size:HEADER.size + length]) + seq2 = HEADER.unpack(self.mm[:HEADER.size])[0] + if seq1 != seq2: + return None + try: + return frame_id, pickle.loads(data) + except Exception: + return None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_warp.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_warp.py new file mode 100644 index 000000000..50697728a --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/model_warp.py @@ -0,0 +1,80 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import os +import pickle + +import numpy as np + +from iqpilot.common.swaglog import cloudlog +from iqpilot.system.hardware.hw import Paths + + +def _load_bundle(pkl_path: str, cam_w: int, cam_h: int, frame_skip: int) -> dict: + with open(pkl_path, "rb") as f: + bundle = pickle.load(f) + if bundle.get("frame_skip") != frame_skip: + raise RuntimeError(f"frame_skip {bundle.get('frame_skip')} != {frame_skip}") + if (cam_w, cam_h) not in bundle: + raise RuntimeError(f"missing {cam_w}x{cam_h}; has {[k for k in bundle if isinstance(k, tuple)]}") + _verify_selftest(bundle, cam_w, cam_h) + return bundle + + +def _verify_selftest(bundle: dict, cam_w: int, cam_h: int) -> None: + want = bundle.get("selftest") + if not want: + raise RuntimeError("warp artifact predates the self-test; recompiling") + from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info + from iqpilot.selfdrive.iqmodeld.tools.compile_warp import selftest_digest + nv12_size = get_nv12_info(cam_w, cam_h)[3] + got = selftest_digest(bundle[(cam_w, cam_h)], cam_w, cam_h, nv12_size) + if got != want: + raise RuntimeError(f"warp self-test {got[:12]} != {want[:12]}; artifact computes differently here") + + +class FrameWarp: + + def __init__(self, cam_w: int, cam_h: int, frame_skip: int): + from tinygrad.tensor import Tensor + + pkl_path = os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl") + bundle = None + if os.path.isfile(pkl_path): + try: + bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip) + except Exception as e: + cloudlog.warning(f"warp artifact unusable ({e}); discarding and recompiling") + os.remove(pkl_path) + if bundle is None: + cloudlog.warning(f"warp artifact missing; compiling for {cam_w}x{cam_h} (one-time)") + from iqpilot.selfdrive.iqmodeld.tools.compile_warp import compile_warp + compile_warp(cam_w, cam_h, pkl_path, frame_skip=frame_skip) + cloudlog.warning(f"warp compiled -> {pkl_path}") + bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip) + self._jit = bundle[(cam_w, cam_h)] + + self._npy = {"tfm": np.zeros((3, 3), dtype=np.float32), "big_tfm": np.zeros((3, 3), dtype=np.float32)} + self._tensors = {k: Tensor(v, device="NPY").realize() for k, v in self._npy.items()} + self._blob_cache: dict[tuple[str, int], object] = {} + self._Tensor = Tensor + + def _frame_tensor(self, key: str, buf): + from tinygrad.device import Device + arr = np.frombuffer(buf.data, dtype=np.uint8) + ck = (key, arr.ctypes.data) + t = self._blob_cache.get(ck) + if t is None: + t = self._Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype="uint8", device=Device.DEFAULT) + self._blob_cache[ck] = t + return t + + def run(self, main_buf, extra_buf, main_tfm: np.ndarray, extra_tfm: np.ndarray) -> np.ndarray: + self._npy["tfm"][:] = main_tfm + self._npy["big_tfm"][:] = extra_tfm + warped = self._jit(tfm=self._tensors["tfm"], big_tfm=self._tensors["big_tfm"], + frame=self._frame_tensor("img", main_buf), + big_frame=self._frame_tensor("big_img", extra_buf)) + return warped.numpy().astype(np.uint8, copy=False) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py new file mode 100644 index 000000000..5ece1c074 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/__init__.py @@ -0,0 +1,3 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py new file mode 100644 index 000000000..bf17800a7 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/combined_artifact.py @@ -0,0 +1,100 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +from iqpilot.system.hardware.hw import Paths + + +_MODEL_ROOT = Path(Paths.model_root()) +_OVERRIDE_KEYS = ( + "combinedRuntimeArtifact", + "combinedSplitArtifact", + "iqCombinedArtifact", +) +_SPLIT_ROLE_PATTERN = re.compile(r"^driving_(vision|policy|off_policy|on_policy)_(.+)_tinygrad\.pkl$") + + +def _bundle_models(bundle) -> list: + models = getattr(bundle, "models", None) + return list(models) if models is not None else [] + + +def _bundle_override_map(bundle) -> dict[str, str]: + result: dict[str, str] = {} + for override in getattr(bundle, "overrides", None) or []: + key = getattr(override, "key", None) + value = getattr(override, "value", None) + if key and value: + result[str(key)] = str(value) + return result + + +def _artifact_name(model) -> str: + return getattr(getattr(model, "artifact", None), "fileName", "") or "" + + +def _split_suffixes(bundle) -> list[str]: + suffixes: list[str] = [] + for model in _bundle_models(bundle): + match = _SPLIT_ROLE_PATTERN.match(_artifact_name(model)) + if match: + suffixes.append(match.group(2)) + return suffixes + + +def _derived_candidates(bundle) -> list[str]: + seen: set[str] = set() + candidates: list[str] = [] + + for suffix in _split_suffixes(bundle): + for candidate in ( + f"driving_combined_{suffix}.pkl", + f"iqmodeld_combined_{suffix}.pkl", + ): + if candidate not in seen: + seen.add(candidate) + candidates.append(candidate) + + ref = getattr(bundle, "ref", None) + if ref: + short_ref = str(ref)[:8] + for candidate in ( + f"driving_combined_{short_ref}.pkl", + f"iqmodeld_combined_{short_ref}.pkl", + ): + if candidate not in seen: + seen.add(candidate) + candidates.append(candidate) + + return candidates + + +def combined_split_artifact_candidates(bundle) -> list[Path]: + explicit_env = os.getenv("IQMODEL_COMBINED_PKL") + if explicit_env: + explicit_path = Path(explicit_env) + return [explicit_path if explicit_path.is_absolute() else _MODEL_ROOT / explicit_path] + + overrides = _bundle_override_map(bundle) + explicit_names = [overrides[key] for key in _OVERRIDE_KEYS if key in overrides] + if explicit_names: + return [_MODEL_ROOT / name for name in explicit_names] + + return [_MODEL_ROOT / name for name in _derived_candidates(bundle)] + + +def resolve_combined_split_artifact(bundle) -> Path | None: + for candidate in combined_split_artifact_candidates(bundle): + if candidate.is_file(): + return candidate + return None + + +def has_combined_split_artifact(bundle) -> bool: + return resolve_combined_split_artifact(bundle) is not None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py new file mode 100644 index 000000000..d06c7c3dd --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/fetcher.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.fetcher") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.fetcher import * # noqa: F403 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py new file mode 100644 index 000000000..4b9a2f4d4 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/helpers.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +""" +Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +import json +import os +import shutil +from pathlib import Path + +from iqpilot.cereal import custom +from iqpilot.common.params import Params +from iqpilot.common.swaglog import cloudlog +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module +from iqpilot.system.hardware.hw import Paths + +try: + load_private_module(__name__, "iqpilot_private.models.helpers") +except ProprietaryModuleMissing: + try: + from iqpilot.models_private_src.helpers import * # noqa: F403 + except ImportError: + pass + + +ModelBundle = custom.IQModelManager.ModelBundle +Runner = custom.IQModelManager.Runner +_MODEL_ROOT = Path(Paths.model_root()) +_ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle" +_MODELS_CACHE_KEY = "ModelManager_ModelsCache" +_RUNNER_CACHE_KEY = "ModelRunnerTypeCache" +_DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex" +_PENDING_INDEX_KEY = "ModelManager_PendingIndex" +_PENDING_MODEL_RESTORE_FILE = "/data/k3_pending_model_restore" +_STOCK_RUNNER = int(Runner.stock) +_TINYGRAD_RUNNER = int(Runner.tinygrad) +_SNPE_RUNNER = int(Runner.snpe) + +_DEFAULT_MODEL_DIR = Path(__file__).resolve().parents[1] / "default_model" +_DEFAULT_BUNDLE_JSON = _DEFAULT_MODEL_DIR / "bundle.json" +_DEFAULT_BUNDLE_REF = "default" + + +def get_default_model_bundle(_bundles): + return None + + +def _coerce_runner_value(value) -> int | None: + raw = getattr(value, "raw", value) + try: + return int(raw) + except (TypeError, ValueError): + return None + + +def _bundle_models(bundle) -> list: + models = getattr(bundle, "models", None) + return list(models) if models is not None else [] + + +def _bundle_needs_runtime_upgrade(bundle) -> bool: + if bundle is None: + return False + + if _coerce_runner_value(getattr(bundle, "runner", None)) == _SNPE_RUNNER: + return True + + for model in _bundle_models(bundle): + file_name = getattr(getattr(model, "artifact", None), "fileName", "") or "" + if file_name.endswith(".thneed"): + return True + + return False + + +def _load_cached_manifest_bundles(params: Params): + cached = params.get(_MODELS_CACHE_KEY) or {} + bundles = [] + for raw_bundle in cached.get("bundles", []): + try: + min_selector_version = int(raw_bundle.get("minimumSelectorVersion", raw_bundle.get("minimum_selector_version", 0))) + compatibility_view = dict(raw_bundle) + compatibility_view["minimumSelectorVersion"] = min_selector_version + is_compatible = globals().get("is_bundle_version_compatible") + if is_compatible is not None and not is_compatible(compatibility_view): + continue + + if "short_name" in raw_bundle: + from iqpilot.selfdrive.iqmodeld.models.fetcher import ManifestDecoder + bundles.append(ManifestDecoder._decode_bundle(raw_bundle)) + continue + + if "internalName" in raw_bundle: + bundles.append(ModelBundle(**raw_bundle)) + continue + + bundle = ModelBundle() + bundle.index = int(raw_bundle["index"]) + bundle.internalName = raw_bundle.get("short_name") + bundle.displayName = raw_bundle.get("display_name") + bundle.status = 0 + bundle.generation = int(raw_bundle["generation"]) + bundle.environment = raw_bundle["environment"] + bundle.runner = raw_bundle.get("runner", Runner.tinygrad) + bundle.is20hz = raw_bundle.get("is_20hz", False) + bundle.minimumSelectorVersion = int(min_selector_version) + bundle.ref = raw_bundle.get("ref") + bundle.overrides = [] + for key, value in raw_bundle.get("overrides", {}).items(): + override = custom.IQModelManager.Override() + override.key = key + override.value = value + bundle.overrides.append(override) + + bundle.models = [] + for raw_model in raw_bundle.get("models", []): + model = custom.IQModelManager.Model() + model.type = raw_model.get("type") + for attr_name in ("artifact", "metadata"): + raw_artifact = raw_model.get(attr_name) + if not raw_artifact: + continue + artifact = custom.IQModelManager.Artifact() + artifact.fileName = raw_artifact.get("file_name") + download_uri = custom.IQModelManager.DownloadUri() + download_uri.uri = raw_artifact.get("download_uri", {}).get("url") + download_uri.sha256 = raw_artifact.get("download_uri", {}).get("sha256") + artifact.downloadUri = download_uri + setattr(model, attr_name, artifact) + bundle.models.append(model) + + bundles.append(bundle) + except Exception: + continue + return bundles + + +def _bundle_match_key(bundle) -> tuple[str | None, str | None, str | None]: + return ( + getattr(bundle, "ref", None), + getattr(bundle, "internalName", None), + getattr(bundle, "displayName", None), + ) + + +def _find_runtime_upgrade(bundle, params: Params, available_bundles=None): + if not _bundle_needs_runtime_upgrade(bundle): + return bundle + + candidate_bundles = available_bundles if available_bundles is not None else _load_cached_manifest_bundles(params) + ref, internal_name, display_name = _bundle_match_key(bundle) + + for candidate in candidate_bundles: + if getattr(candidate, "ref", None) and getattr(candidate, "ref", None) == ref: + return candidate + + for candidate in candidate_bundles: + if getattr(candidate, "internalName", None) == internal_name: + return candidate + + for candidate in candidate_bundles: + if getattr(candidate, "displayName", None) == display_name: + return candidate + + return None + + +def bundle_files_ready(bundle) -> bool: + if bundle is None: + return False + + for model in _bundle_models(bundle): + artifact = getattr(model, "artifact", None) + metadata = getattr(model, "metadata", None) + for file_name in (getattr(metadata, "fileName", None), getattr(artifact, "fileName", None)): + if file_name and not (_MODEL_ROOT / file_name).is_file(): + return False + return True + + +def persist_active_bundle(params: Params, bundle) -> None: + params.put(_ACTIVE_BUNDLE_KEY, bundle.to_dict()) + params.remove(_RUNNER_CACHE_KEY) + + +def _load_default_bundle_dict() -> dict: + return json.loads(_DEFAULT_BUNDLE_JSON.read_text()) + + +def _default_bundle_filenames(bundle_dict: dict) -> list[str]: + names = [] + for model in bundle_dict.get("models", []): + for artifact in (model.get("metadata"), model.get("artifact")): + file_name = artifact.get("fileName", "") if isinstance(artifact, dict) else "" + if file_name: + names.append(file_name) + return names + + +def is_default_bundle(bundle) -> bool: + return bool(bundle is not None and getattr(bundle, "ref", None) == _DEFAULT_BUNDLE_REF) + + +def ensure_default_model_files(bundle_dict: dict = None) -> None: + bundle_dict = bundle_dict if bundle_dict is not None else _load_default_bundle_dict() + try: + _MODEL_ROOT.mkdir(parents=True, exist_ok=True) + except OSError as e: + cloudlog.exception(f"default_model: cannot create model root: {e}") + return + for file_name in _default_bundle_filenames(bundle_dict): + src = _DEFAULT_MODEL_DIR / file_name + dst = _MODEL_ROOT / file_name + if not src.is_file(): + cloudlog.error(f"default_model: shipped asset missing {src}") + continue + if dst.is_file() and dst.stat().st_size == src.stat().st_size: + continue + try: + shutil.copy2(src, dst) + cloudlog.warning(f"default_model: staged {file_name} into model root") + except OSError as e: + cloudlog.exception(f"default_model: failed staging {file_name}: {e}") + + +def select_default_model(params: Params = None) -> None: + params = Params() if params is None else params + bundle_dict = _load_default_bundle_dict() + ensure_default_model_files(bundle_dict) + params.remove(_DOWNLOAD_INDEX_KEY) + params.remove(_PENDING_INDEX_KEY) + params.put(_ACTIVE_BUNDLE_KEY, bundle_dict) + params.remove(_RUNNER_CACHE_KEY) + params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER) + try: + if os.path.isfile(_PENDING_MODEL_RESTORE_FILE): + os.remove(_PENDING_MODEL_RESTORE_FILE) + except OSError: + pass + + +def seed_default_bundle_if_unset(params: Params = None) -> None: + params = Params() if params is None else params + if params.get(_ACTIVE_BUNDLE_KEY): + return + queued_download = params.get(_DOWNLOAD_INDEX_KEY) + try: + select_default_model(params) + if queued_download is not None: + params.put(_DOWNLOAD_INDEX_KEY, queued_download) + cloudlog.warning("default_model: seeded Default (CD210) as active bundle") + except Exception as e: + cloudlog.exception(f"default_model: failed to seed default bundle: {e}") + + +def get_runtime_bundle_upgrade(bundle, params: Params = None, available_bundles=None): + params = Params() if params is None else params + return _find_runtime_upgrade(bundle, params, available_bundles) + + +def get_active_bundle(params: Params = None): + params = Params() if params is None else params + + try: + active_bundle = params.get(_ACTIVE_BUNDLE_KEY) or {} + if not active_bundle: + return None + is_compatible = globals().get("is_bundle_version_compatible") + if is_compatible is not None and not is_compatible(active_bundle): + return None + bundle = ModelBundle(**active_bundle) + except Exception: + return None + + replacement = _find_runtime_upgrade(bundle, params) + if replacement is not None and replacement is not bundle and bundle_files_ready(replacement): + persist_active_bundle(params, replacement) + return replacement + + return bundle + + +def get_active_model_runner(params: Params = None, force_check=False): + params = Params() if params is None else params + + active_bundle = get_active_bundle(params) + if not active_bundle: + seed_default_bundle_if_unset(params) + active_bundle = get_active_bundle(params) + if not active_bundle: + if params.get(_RUNNER_CACHE_KEY) != str(_TINYGRAD_RUNNER): + params.put(_RUNNER_CACHE_KEY, _TINYGRAD_RUNNER) + return _TINYGRAD_RUNNER + + cached_runner_type = params.get(_RUNNER_CACHE_KEY) + if cached_runner_type and not force_check and isinstance(cached_runner_type, str) and cached_runner_type.isdigit(): + return int(cached_runner_type) + + runner_type = _coerce_runner_value(active_bundle.runner) + if runner_type == _SNPE_RUNNER: + replacement = _find_runtime_upgrade(active_bundle, params) + if replacement is not None and replacement is not active_bundle and bundle_files_ready(replacement): + persist_active_bundle(params, replacement) + runner_type = _coerce_runner_value(replacement.runner) + else: + if replacement is not None and getattr(replacement, "index", None) is not None and params.get(_DOWNLOAD_INDEX_KEY) is None: + params.put(_DOWNLOAD_INDEX_KEY, int(replacement.index)) + cloudlog.warning(f"Queued tinygrad migration for retired bundle {getattr(active_bundle, 'internalName', '')}") + runner_type = _TINYGRAD_RUNNER + + if cached_runner_type != runner_type: + params.put(_RUNNER_CACHE_KEY, int(runner_type)) + + return runner_type diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/inference_state.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/inference_state.py new file mode 100644 index 000000000..1bc1a254a --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/inference_state.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqpilot.common.steer_delay import cached_steer_delay + + +class InferenceStateBase: + def __init__(self): + self.lat_delay = cached_steer_delay() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py new file mode 100644 index 000000000..8ede5f6c4 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py @@ -0,0 +1,231 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +import os +import pickle as _pk +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +import numpy as np +from iqpilot.cereal import custom +from iqpilot.common.swaglog import cloudlog +from iqpilot.system.hardware import TICI +from iqpilot.system.hardware.hw import Paths as _hw_paths +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle as _fetch_bundle +from iqpilot.selfdrive.iqmodeld.models.combined_artifact import has_combined_split_artifact + +# ---- runtime type surface (native OpenCL/frame handles resolve to Any off-device) ---- +if TYPE_CHECKING: + from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot, RoadProjector +else: + def _resolve_native_types() -> tuple[Any, Any]: + try: + from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import GpuMemorySlot as iq_clmem + from iqpilot.selfdrive.iqmodeld.native.iqmodel_pyx import RoadProjector as iq_frame + return iq_clmem, iq_frame + except (ModuleNotFoundError, ImportError): + return Any, Any + + GpuMemorySlot, RoadProjector = _resolve_native_types() + +NumpyDict = dict[str, np.ndarray] +ShapeDict = dict[str, tuple[int, ...]] +SliceDict = dict[str, slice] +CLMemDict = dict[str, GpuMemorySlot] +FrameDict = dict[str, RoadProjector] + +ModelType = custom.IQModelManager.Model.Type +Model = custom.IQModelManager.Model + +SEND_RAW_PRED = os.getenv("SEND_RAW_PRED") +CUSTOM_MODEL_PATH = _hw_paths.model_root() + +_META_FIELDS = ("input_shapes", "output_slices") + +USBGPU = "USBGPU" in os.environ + + +def _configure_accelerator(): + """Point tinygrad at the right backend. Must run before tinygrad is imported, + which is why it fires at module import.""" + backend, extra = ("QCOM" if TICI else "CPU"), {} + if USBGPU: + backend, extra = "AMD", {"AMD_IFACE": "USB"} + elif TICI: + extra = {"QCOM_PRIORITY": "8"} + os.environ["DEV"] = backend + os.environ.update(extra) + + +_configure_accelerator() + + +# real metadata pkls are a few KB; anything bigger is a model artifact wrongly +# referenced as metadata (pre-fix manifests self-referenced the artifact), and +# unpickling it here double-loads the model onto the GPU +_META_MAX_BYTES = 1 << 20 + + +def load_artifact_metadata(metadata_filename): + """Read one artifact's metadata pkl: (input shapes, output slices).""" + try: + path = os.path.join(CUSTOM_MODEL_PATH, metadata_filename) + if os.path.getsize(path) > _META_MAX_BYTES: + cloudlog.error(f"metadata pkl {metadata_filename} is artifact-sized, refusing to unpickle it") + return tuple({} for _ in _META_FIELDS) + with open(path, 'rb') as fh: + blob = _pk.load(fh) + return tuple(blob.get(field, {}) for field in _META_FIELDS) + except Exception: + cloudlog.exception(f"unreadable metadata pkl {metadata_filename}, continuing without it") + return tuple({} for _ in _META_FIELDS) + + +@dataclass +class ArtifactSpec: + """One model of the active bundle plus its unpacked metadata.""" + model: Any + metadata: Any = None + input_shapes: ShapeDict = field(default_factory=dict) + output_slices: SliceDict = field(default_factory=dict) + + def __post_init__(self): + self.metadata = self.model.metadata + if self.metadata: + self.input_shapes, self.output_slices = load_artifact_metadata(self.metadata.fileName) + + +# kept name: some runners annotate against the old alias +ModelData = ArtifactSpec + + +class RunnerRoot: + """Shared root of the runner hierarchy. + + Both ModelRunner and the per-model parser mixins (model_types.py) inherit + this, so the concrete `TinygradRunner(ModelRunner, *Tinygrad)` diamond keeps + one consistent parser registry + slice implementation. + """ + + parser_method_dict: dict + _model_data: "ArtifactSpec | None" + + def _slice_outputs(self, model_outputs): + raise NotImplementedError + + +class ModelRunner(RunnerRoot): + """Base for the tinygrad/ONNX runners. + + Owns the active bundle's ArtifactSpecs and the shared slice/parse plumbing; + subclasses provide input staging (prepare_inputs) and execution (_run_model). + """ + + # False for fused runners, which warp + manage temporal buffers inside the JIT + uses_opencl_warp = True + + def __init__(self): + active = _fetch_bundle() + if not active: + raise ValueError("runner started without an active model bundle") + + self.models = {spec.type.raw: ArtifactSpec(spec) for spec in _qcom_models(active)} + self.is_20hz_3d = False + self.is_20hz = active.is20hz + self.inputs = {} + self.parser_method_dict = {} + self._model_data = None # active spec for the current operation + self._parser = self._constants = None + + def _active_spec(self): + spec = self._model_data + if spec is None: + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + return spec + + # views proxied straight off the active artifact spec; kept out of the class + # body (served via __getattr__) so the read surface stays data-driven + _SPEC_VIEW = frozenset(("input_shapes", "output_slices")) + + def __getattr__(self, name): + if name == "constants": + return self._constants + if name == "vision_input_names": + return list(self._active_spec().input_shapes) + if name in ModelRunner._SPEC_VIEW: + return getattr(self._active_spec(), name) + raise AttributeError(name) + + def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + """Stage image + numpy inputs for inference; implemented per backend.""" + raise NotImplementedError + + def _run_model(self): + """Execute inference over the staged inputs; implemented per backend.""" + raise NotImplementedError + + def run_model(self): + # parsing happens inside each backend's _run_model + return self._run_model() + + def _slice_outputs(self, model_outputs): + """Split the flat output vector into named views per the artifact's slice table.""" + sliced = {} + for tag, span in self._active_spec().output_slices.items(): + sliced[tag] = model_outputs[np.newaxis, span] + if SEND_RAW_PRED: + sliced["raw_pred"] = model_outputs.copy() + return sliced + + +# ---- runner selection (which backend to build for the active bundle) ---------- + +def _qcom_models(bundle) -> list: + # usbeMac artifacts ride along in a bundle for the eGPU host; they are never + # loaded on QCOM and must not affect runner classification + return [m for m in bundle.models if m.type.raw != ModelType.usbeMac] + + +def _single_artifact_prefix(bundle, prefix: str) -> bool: + models = _qcom_models(bundle) + return len(models) == 1 and models[0].artifact.fileName.startswith(prefix) + + +def _is_fused_bundle(bundle) -> bool: + return _single_artifact_prefix(bundle, "driving_fused_") + + +def _is_supercombo_bundle(bundle) -> bool: + return _single_artifact_prefix(bundle, "driving_supercombo_") + + +def _is_split_bundle(bundle) -> bool: + present = {m.type.raw for m in _qcom_models(bundle)} + split_kinds = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy} + return not present.isdisjoint(split_kinds) + + +def get_model_runner() -> "ModelRunner": + """Build the runner backend that fits the active bundle (supercombo / fused / + combined-split / split / single). Concrete runners are imported lazily so one + backend failing to load can't take down the others at import time.""" + from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import (TinygradRunner, + TinygradSplitRunner) + bundle = _fetch_bundle() + # an eMac-only bundle (no QCOM-loadable models) runs the stock default on + # device; the big host serves the bundle's precompiled artifact + if not (bundle and bundle.models and _qcom_models(bundle)): + return TinygradRunner(ModelType.supercombo) + + if _is_supercombo_bundle(bundle): + from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.supercombo_runner import TinygradSupercomboRunner + return TinygradSupercomboRunner() + if _is_fused_bundle(bundle): + from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.fused_runner import TinygradFusedRunner + return TinygradFusedRunner() + if _is_split_bundle(bundle) and has_combined_split_artifact(bundle): + from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.combined_split_runner import TinygradCombinedSplitRunner + return TinygradCombinedSplitRunner() + if _is_split_bundle(bundle): + return TinygradSplitRunner() + return TinygradRunner(_qcom_models(bundle)[0].type.raw) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py new file mode 100644 index 000000000..71a99cd13 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/combined_split_runner.py @@ -0,0 +1,245 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +import os +import pickle +from pathlib import Path +from typing import Any + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.models.combined_artifact import resolve_combined_split_artifact +from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_bundle +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import NumpyDict, ShapeDict, SliceDict +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner +from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants +from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + + +def _tinygrad_imports(): + from tinygrad.device import Device + from tinygrad.tensor import Tensor + return Tensor, Device + + +def _phase_roles(meta_by_role: dict[str, dict]) -> list[str]: + return [name for name in meta_by_role if name != "vision"] + + +def _phase_desire_key(policy_shapes: dict[str, tuple[int, ...]]) -> str: + for key in policy_shapes: + if key.startswith("desire"): + return key + raise KeyError("No desire-like key found in policy inputs") + + +def _phase_image_keys(vision_shapes: dict[str, tuple[int, ...]]) -> tuple[str, str]: + names = sorted(name for name in vision_shapes if "img" in name) + road_key = next((name for name in names if "big" not in name), None) + wide_key = next((name for name in names if "big" in name), None) + if road_key is None or wide_key is None: + raise ValueError(f"Unable to resolve road/wide image keys from {list(vision_shapes)}") + return road_key, wide_key + + +def _base_policy_keys(policy_shapes: dict[str, tuple[int, ...]]) -> set[str]: + desired_key = _phase_desire_key(policy_shapes) + return {desired_key, "features_buffer", "traffic_convention", "action_t"} + + +def _slice_map(raw_blob: np.ndarray, slices: dict[str, slice]) -> NumpyDict: + return {name: raw_blob[np.newaxis, section] for name, section in slices.items() if name != "pad"} + + +class TinygradCombinedSplitRunner(ModelRunner): + uses_opencl_warp: bool = False + + def __init__(self): + super().__init__() + self._constants = SplitModelConstants + self._parser = PhaseParser() + self._bundle = get_active_bundle() + self._artifact_path = resolve_combined_split_artifact(self._bundle) + if self._artifact_path is None: + raise FileNotFoundError("No IQ combined split artifact is available for the active bundle") + + with open(self._artifact_path, "rb") as artifact: + runtime_package: dict[Any, Any] = pickle.load(artifact) + + self._meta_by_role = runtime_package.get("meta_by_role", runtime_package.get("metadata", {})) + self._policy_roles = runtime_package.get("roles", _phase_roles(self._meta_by_role)) + self._camera_programs = { + camera_key: spec + for camera_key, spec in runtime_package.items() + if isinstance(camera_key, tuple) and isinstance(spec, dict) + } + self._execute_bundle = runtime_package.get("execute_bundle", runtime_package.get("run_policy")) + self._frame_stride = int(runtime_package.get("frame_stride", runtime_package.get("frame_skip", 1))) + + if "vision" not in self._meta_by_role: + raise ValueError("Combined split artifact is missing vision metadata") + if not self._policy_roles: + raise ValueError("Combined split artifact is missing policy roles") + if self._execute_bundle is None: + raise ValueError("Combined split artifact is missing execute_bundle") + + self._vision_meta = self._meta_by_role["vision"] + self._primary_policy_meta = self._meta_by_role[self._policy_roles[0]] + self._desired_key = _phase_desire_key(self._primary_policy_meta["input_shapes"]) + self._road_key, self._wide_key = _phase_image_keys(self._vision_meta["input_shapes"]) + self._extra_policy_keys = [ + key for key in self._primary_policy_meta["input_shapes"] + if key not in _base_policy_keys(self._primary_policy_meta["input_shapes"]) + ] + + self._queue_tensors: dict[str, Any] | None = None + self._numpy_state: dict[str, np.ndarray] | None = None + self._camera_shape: tuple[int, int] | None = None + self._blob_cache: dict[tuple[str, int], Any] = {} + self._last_desire = np.zeros(self._primary_policy_meta["input_shapes"][self._desired_key][2], dtype=np.float32) + + @property + def vision_input_names(self) -> list[str]: + return [self._road_key, self._wide_key] + + @property + def input_shapes(self) -> ShapeDict: + merged: ShapeDict = dict(self._vision_meta["input_shapes"]) + for role in self._policy_roles: + merged.update(self._meta_by_role[role]["input_shapes"]) + return merged + + @property + def output_slices(self) -> SliceDict: + merged: SliceDict = dict(self._vision_meta["output_slices"]) + for role in self._policy_roles: + merged.update(self._meta_by_role[role]["output_slices"]) + return merged + + def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + raise RuntimeError("Combined split runner manages its own warp + queue state; use run_fused()") + + def _frame_blob(self, stream_name: str, buf): + Tensor, Device = _tinygrad_imports() + raw_frame = np.frombuffer(buf.data, dtype=np.uint8) + cache_key = (stream_name, raw_frame.ctypes.data) + tensor = self._blob_cache.get(cache_key) + if tensor is None: + tensor = Tensor.from_blob(raw_frame.ctypes.data, (raw_frame.size,), dtype="uint8", device=Device.DEFAULT) + self._blob_cache[cache_key] = tensor + return tensor + + def _allocate_runtime_state(self, camera_width: int, camera_height: int) -> None: + if self._queue_tensors is not None and self._camera_shape == (camera_width, camera_height): + return + if (camera_width, camera_height) not in self._camera_programs: + raise RuntimeError(f"No combined split kernels available for {camera_width}x{camera_height}") + + Tensor, Device = _tinygrad_imports() + vision_shapes = self._vision_meta["input_shapes"] + policy_shapes = self._primary_policy_meta["input_shapes"] + + image_shape = vision_shapes[self._road_key] + frame_history = image_shape[1] // 6 + queue_depth = self._frame_stride * (frame_history - 1) + 1 + frame_queue_shape = (queue_depth, 6, image_shape[2], image_shape[3]) + + feature_shape = policy_shapes["features_buffer"] + desired_shape = policy_shapes[self._desired_key] + traffic_shape = policy_shapes["traffic_convention"] + action_shape = policy_shapes.get("action_t", traffic_shape) + + numpy_state = { + "tfm": np.zeros((3, 3), dtype=np.float32), + "big_tfm": np.zeros((3, 3), dtype=np.float32), + "desire": np.zeros(desired_shape[2], dtype=np.float32), + "traffic_convention": np.zeros(traffic_shape, dtype=np.float32), + "action_t": np.zeros(action_shape, dtype=np.float32), + } + for key in self._extra_policy_keys: + numpy_state[key] = np.zeros(policy_shapes[key], dtype=np.float32) + + queue_tensors = { + "img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize(), + "big_img_q": Tensor(np.zeros(frame_queue_shape, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize(), + "feat_q": Tensor( + np.zeros((self._frame_stride * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), dtype=np.float32), + device=Device.DEFAULT, + ).contiguous().realize(), + "desire_q": Tensor( + np.zeros((self._frame_stride * desired_shape[1], desired_shape[0], desired_shape[2]), dtype=np.float32), + device=Device.DEFAULT, + ).contiguous().realize(), + **{name: Tensor(value, device="NPY").realize() for name, value in numpy_state.items()}, + } + + self._queue_tensors = queue_tensors + self._numpy_state = numpy_state + self._camera_shape = (camera_width, camera_height) + + def _policy_inputs(self) -> dict[str, Any]: + assert self._queue_tensors is not None + tensor_names = ["feat_q", "desire_q", "desire", "traffic_convention", "action_t", *self._extra_policy_keys] + return {name: self._queue_tensors[name] for name in tensor_names if name in self._queue_tensors} + + def _merge_policy_outputs(self, raw_outputs: tuple[Any, ...]) -> NumpyDict: + outputs = self._parser.parse_vision_outputs( + _slice_map(raw_outputs[0].numpy().flatten(), self._vision_meta["output_slices"]) + ) + + has_on_policy = any(role == "on_policy" for role in self._policy_roles) + for role_name, tensor_out in zip(self._policy_roles, raw_outputs[1:], strict=True): + parsed = self._parser.parse_policy_outputs( + _slice_map(tensor_out.numpy().flatten(), self._meta_by_role[role_name]["output_slices"]) + ) + if role_name == "off_policy" and has_on_policy: + parsed.pop("plan", None) + outputs.update(parsed) + + if "planplus" in outputs and "plan" in outputs: + outputs["plan"] = outputs["plan"] + outputs["planplus"] + return outputs + + def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict: + main_buf = bufs[self._road_key] + self._allocate_runtime_state(main_buf.width, main_buf.height) + assert self._queue_tensors is not None and self._numpy_state is not None and self._camera_shape is not None + + self._numpy_state["tfm"][:] = transforms[self._road_key] + self._numpy_state["big_tfm"][:] = transforms[self._wide_key] + + current_desire = numpy_inputs[self._desired_key].copy() + current_desire[0] = 0 + self._numpy_state["desire"][:] = np.where(current_desire - self._last_desire > 0.99, current_desire, 0) + self._last_desire[:] = current_desire + + if "traffic_convention" in numpy_inputs: + self._numpy_state["traffic_convention"][:] = numpy_inputs["traffic_convention"] + if "action_t" in numpy_inputs: + self._numpy_state["action_t"][:] = numpy_inputs["action_t"] + for key in self._extra_policy_keys: + if key in numpy_inputs: + self._numpy_state[key][:] = numpy_inputs[key] + + stage_inputs = self._camera_programs[self._camera_shape].get("stage_inputs", self._camera_programs[self._camera_shape].get("warp_enqueue")) + if stage_inputs is None: + raise RuntimeError("Combined split artifact camera entry is missing stage_inputs") + + staged_main, staged_wide = stage_inputs( + img_q=self._queue_tensors["img_q"], + big_img_q=self._queue_tensors["big_img_q"], + tfm=self._queue_tensors["tfm"], + big_tfm=self._queue_tensors["big_tfm"], + frame=self._frame_blob(self._road_key, bufs[self._road_key]), + big_frame=self._frame_blob(self._wide_key, bufs[self._wide_key]), + ) + raw_outputs = self._execute_bundle(img=staged_main, big_img=staged_wide, **self._policy_inputs()) + if not isinstance(raw_outputs, tuple): + raw_outputs = (raw_outputs,) + return self._merge_policy_outputs(raw_outputs) + + def _run_model(self) -> NumpyDict: + raise RuntimeError("Combined split runner executes through run_fused()") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py new file mode 100644 index 000000000..33beea423 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/fused_runner.py @@ -0,0 +1,178 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import os +import pickle +from typing import Any + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ( + CUSTOM_MODEL_PATH, NumpyDict, ShapeDict, SliceDict, +) +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner +from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants +from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + + +def _tinygrad_imports(): + from tinygrad.tensor import Tensor + from tinygrad.device import Device + return Tensor, Device + + +WARP_DEV = os.getenv('WARP_DEV') + + +class TinygradFusedRunner(ModelRunner): + uses_opencl_warp: bool = False + + def __init__(self): + super().__init__() + + self._constants = SplitModelConstants + self._parser = PhaseParser() + + if len(self.models) != 1: + raise ValueError(f"fused bundle must have exactly one artifact, got {list(self.models)}") + self._model_data = next(iter(self.models.values())) + + pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName) + with open(pkl_path, 'rb') as f: + self._fused: dict[Any, Any] = pickle.load(f) + + self._vision_meta = self._fused['metadata']['vision'] + self._on_meta = self._fused['metadata']['on_policy'] + self._off_meta = self._fused['metadata']['off_policy'] + self._run_policy = self._fused['run_policy'] + self._warp_jits: dict[tuple[int, int], Any] = {k: v for k, v in self._fused.items() if isinstance(k, tuple)} + if not self._warp_jits: + raise ValueError("fused pkl has no warp JITs") + + self._frame_skip: int = int(self._fused.get('frame_skip', 4)) + + self._queues: dict[str, Any] | None = None + self._npy_buffers: dict[str, np.ndarray] | None = None + self._cam_resolution: tuple[int, int] | None = None + self._blob_cache: dict[tuple[str, int], Any] = {} + + def _frame_tensor(self, key, buf): + Tensor, Device = _tinygrad_imports() + arr = np.frombuffer(buf.data, dtype=np.uint8) + ck = (key, arr.ctypes.data) + t = self._blob_cache.get(ck) + if t is None: + t = Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype='uint8', device=Device.DEFAULT) + self._blob_cache[ck] = t + return t + + @property + def vision_input_names(self) -> list[str]: + return ['img', 'big_img'] + + @property + def input_shapes(self) -> ShapeDict: + return {**self._vision_meta['input_shapes'], **self._on_meta['input_shapes']} + + @property + def output_slices(self) -> SliceDict: + merged: SliceDict = {} + for src in (self._vision_meta['output_slices'], self._on_meta['output_slices'], self._off_meta['output_slices']): + merged.update({k: v for k, v in src.items() if k != 'pad'}) + return merged + + def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + raise RuntimeError("fused runner has no OpenCL path; use run_fused()") + + def _ensure_queues(self, cam_w: int, cam_h: int) -> None: + if self._queues is not None and self._cam_resolution == (cam_w, cam_h): + return + if (cam_w, cam_h) not in self._warp_jits: + raise RuntimeError(f"no warp JIT for {cam_w}x{cam_h}; have {sorted(self._warp_jits)}") + + Tensor, Device = _tinygrad_imports() + img_shape = self._vision_meta['input_shapes']['img'] + fb = self._on_meta['input_shapes']['features_buffer'] + dp = self._on_meta['input_shapes']['desire_pulse'] + n_frames = img_shape[1] // 6 + img_buf_shape = (self._frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3]) + + zeros_u8 = lambda shp: Tensor(np.zeros(shp, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize() + zeros_f32 = lambda shp: Tensor(np.zeros(shp, dtype=np.float32), device=Device.DEFAULT).contiguous().realize() + + self._queues = { + 'img_q': zeros_u8(img_buf_shape), + 'big_img_q': zeros_u8(img_buf_shape), + 'feat_q': zeros_f32((self._frame_skip * (fb[1] - 1) + 1, fb[0], fb[2])), + 'desire_q': zeros_f32((self._frame_skip * dp[1], dp[0], dp[2])), + } + on_shapes = self._on_meta['input_shapes'] + captured = self._run_policy.captured + jit_shapes = { + name: tuple(int(s) for s in view.shape) + for name, (view, _vars, _dtype, _device) in zip(captured.expected_names, captured.expected_input_info) + } + + def policy_input_shape(name): + shape = on_shapes.get(name, jit_shapes.get(name)) + if shape is None: + raise ValueError(f"fused pkl declares no shape for policy input {name}") + return shape + + self._npy_buffers = { + 'desire': np.zeros(dp[2], dtype=np.float32), + 'traffic_convention': np.zeros(policy_input_shape('traffic_convention'), dtype=np.float32), + 'tfm': np.zeros((3, 3), dtype=np.float32), + 'big_tfm': np.zeros((3, 3), dtype=np.float32), + } + if 'action_t' in jit_shapes: + self._npy_buffers['action_t'] = np.zeros(policy_input_shape('action_t'), dtype=np.float32) + self._cam_resolution = (cam_w, cam_h) + + def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict: + Tensor, Device = _tinygrad_imports() + + main_buf = bufs['img'] + self._ensure_queues(main_buf.width, main_buf.height) + assert self._queues is not None and self._npy_buffers is not None + + desire_key = next((k for k in numpy_inputs if k.startswith('desire')), None) + if desire_key is not None: + self._npy_buffers['desire'][:] = numpy_inputs[desire_key] + if 'traffic_convention' in numpy_inputs: + self._npy_buffers['traffic_convention'][:] = numpy_inputs['traffic_convention'] + if 'action_t' in numpy_inputs and 'action_t' in self._npy_buffers: + self._npy_buffers['action_t'][:] = numpy_inputs['action_t'] + self._npy_buffers['tfm'][:] = transforms['img'] + self._npy_buffers['big_tfm'][:] = transforms['big_img'] + + npy = lambda key: Tensor(self._npy_buffers[key], device='NPY') + + frame = self._frame_tensor('img', bufs['img']) + big_frame = self._frame_tensor('big_img', bufs['big_img']) + + warp_jit = self._warp_jits[self._cam_resolution] + img, big_img = warp_jit(img_q=self._queues['img_q'], big_img_q=self._queues['big_img_q'], + tfm=npy('tfm'), big_tfm=npy('big_tfm'), frame=frame, big_frame=big_frame) + + policy_inputs = dict( + img=img, big_img=big_img, feat_q=self._queues['feat_q'], desire_q=self._queues['desire_q'], + desire=npy('desire'), traffic_convention=npy('traffic_convention')) + if 'action_t' in self._npy_buffers: + policy_inputs['action_t'] = npy('action_t') + vision_out_t, on_out_t, off_out_t = self._run_policy(**policy_inputs) + + def _slice(tensor_out, meta) -> NumpyDict: + flat = tensor_out.numpy().flatten() + return {k: flat[np.newaxis, sl] for k, sl in meta['output_slices'].items() if k != 'pad'} + + parsed: NumpyDict = {} + parsed.update(self._parser.parse_vision_outputs(_slice(vision_out_t, self._vision_meta))) + parsed.update(self._parser.parse_policy_outputs(_slice(off_out_t, self._off_meta))) + parsed.update(self._parser.parse_policy_outputs(_slice(on_out_t, self._on_meta))) + return parsed + + def _run_model(self) -> NumpyDict: + raise RuntimeError("fused path goes through run_fused(), not _run_model()") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/model_types.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/model_types.py new file mode 100644 index 000000000..a2f5ec5b6 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/model_types.py @@ -0,0 +1,60 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +from abc import ABC +from collections.abc import Callable + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelType, NumpyDict +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import RunnerRoot +from iqpilot.selfdrive.iqmodeld.parser import ArchiveParser, PhaseParser + + +class _ParserRole(RunnerRoot, ABC): + def _bind_parser_role(self, + selector: int, + parser_builder: Callable[[], object], + projector: Callable[[object, NumpyDict], NumpyDict]) -> None: + parser = parser_builder() + self.parser_method_dict[selector] = lambda model_blob: projector(parser, self._slice_outputs(model_blob)) + + +def _phase_policy(parser: PhaseParser, sliced_outputs: NumpyDict) -> NumpyDict: + return parser.parse_policy_outputs(sliced_outputs) + + +def _phase_vision(parser: PhaseParser, sliced_outputs: NumpyDict) -> NumpyDict: + return parser.parse_vision_outputs(sliced_outputs) + + +def _archive_combined(parser: ArchiveParser, sliced_outputs: NumpyDict) -> NumpyDict: + return parser.parse_outputs(sliced_outputs) + + +class OffPolicyTinygrad(_ParserRole, ABC): + def __init__(self): + self._bind_parser_role(ModelType.offPolicy, PhaseParser, _phase_policy) + + +class OnPolicyTinygrad(_ParserRole, ABC): + def __init__(self): + self._bind_parser_role(ModelType.onPolicy, PhaseParser, _phase_policy) + + +class PolicyTinygrad(_ParserRole, ABC): + def __init__(self): + self._bind_parser_role(ModelType.policy, PhaseParser, _phase_policy) + + +class VisionTinygrad(_ParserRole, ABC): + def __init__(self): + self._bind_parser_role(ModelType.vision, PhaseParser, _phase_vision) + + +class SupercomboTinygrad(_ParserRole, ABC): + def __init__(self): + self._bind_parser_role(ModelType.supercombo, ArchiveParser, _archive_combined) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py new file mode 100644 index 000000000..ccd4a5b1e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/supercombo_runner.py @@ -0,0 +1,339 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import hashlib +import math +import os +import pickle +import re +from typing import Any + +import numpy as np + +from iqpilot.common.params import Params +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import CUSTOM_MODEL_PATH, NumpyDict, ShapeDict, SliceDict +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner +from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants +from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + + +def _tinygrad_imports(): + from tinygrad.tensor import Tensor + from tinygrad.device import Device + return Tensor, Device + + +def _captured_queue_depth(warp_jit: Any) -> int | None: + captured = getattr(warp_jit, "captured", None) + infos = getattr(captured, "expected_input_info", None) + if not infos or len(infos) < 2: + return None + + view_repr = repr(infos[1][0]) + dims = [int(val) for val in re.findall(r"arg=(\d+)", view_repr)] + return dims[0] if len(dims) >= 4 else None + + +def _captured_devices(warp_jit: Any) -> set[str]: + captured = getattr(warp_jit, "captured", None) + infos = getattr(captured, "expected_input_info", None) + if not infos: + return set() + + devices: set[str] = set() + for info in infos: + if isinstance(info, tuple) and len(info) >= 4 and isinstance(info[3], str): + devices.add(info[3]) + return devices + + +def _captured_expected_names(jit_obj: Any) -> list[str]: + captured = getattr(jit_obj, "captured", None) + names = getattr(captured, "expected_names", None) + return list(names) if names else [] + + +def _file_sha256(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _is_jit_arg_mismatch(err: BaseException) -> bool: + return "args mismatch in JIT" in str(err) + + +class TinygradSupercomboRunner(ModelRunner): + uses_opencl_warp: bool = False + + def __init__(self): + super().__init__() + self._constants = SplitModelConstants + self._parser = PhaseParser() + + if len(self.models) != 1: + raise ValueError(f"supercombo bundle must have exactly one artifact, got {list(self.models)}") + self._model_data = next(iter(self.models.values())) + + pkl_path = os.path.join(CUSTOM_MODEL_PATH, self._model_data.model.artifact.fileName) + self._pkl_path = pkl_path + self._expected_sha256 = getattr(getattr(self._model_data.model.artifact, "downloadUri", None), "sha256", "") or "" + self._verify_artifact_file() + with open(pkl_path, 'rb') as f: + self._m: dict[Any, Any] = pickle.load(f) + + self._meta = self._m['metadata'] + self._ish = self._meta['input_shapes'] + self._slices = {k: v for k, v in self._meta['output_slices'].items() if k != 'pad'} + self._hidden_slice = self._meta['output_slices']['hidden_state'] + self._run_policy = self._m['run_policy'] + self._warp_jits: dict[tuple[int, int], Any] = {k: v for k, v in self._m.items() if isinstance(k, tuple)} + if not self._warp_jits: + raise ValueError("supercombo pkl has no warp JITs") + self._frame_skip = int(self._m.get('frame_skip', 4)) + self._validate_warp_jits(pkl_path) + self._validate_jit_names() + + self._queues: dict[str, Any] | None = None + self._npy: dict[str, np.ndarray] | None = None + self._cam: tuple[int, int] | None = None + self._prev_desire = np.zeros(self._ish['desire_pulse'][2], dtype=np.float32) + self._blob_cache: dict[tuple[str, int], Any] = {} + + def _verify_artifact_file(self) -> None: + if not self._expected_sha256: + return + + actual_sha256 = _file_sha256(self._pkl_path) + if actual_sha256 == self._expected_sha256: + return + + try: + os.remove(self._pkl_path) + except OSError: + pass + redownload_msg = self._schedule_active_bundle_redownload() + + raise RuntimeError( + "supercombo artifact SHA mismatch: " + f"expected {self._expected_sha256}, got {actual_sha256} for {self._pkl_path}. " + f"Deleted the stale cached file{redownload_msg}." + ) + + def _validate_warp_jits(self, pkl_path: str) -> None: + img = self._ish['img'] + n_frames = img[1] // 6 + expected_depth = self._frame_skip * (n_frames - 1) + 1 + expected_device = os.getenv('DEV') + + mismatches: list[str] = [] + for cam, warp_jit in sorted(self._warp_jits.items()): + captured_depth = _captured_queue_depth(warp_jit) + captured_devices = _captured_devices(warp_jit) + if captured_depth is not None and captured_depth != expected_depth: + mismatches.append( + f"{cam[0]}x{cam[1]} queue-depth captured={captured_depth} expected={expected_depth}" + ) + if expected_device and captured_devices and expected_device not in captured_devices: + mismatches.append( + f"{cam[0]}x{cam[1]} device captured={sorted(captured_devices)} expected={expected_device}" + ) + + if mismatches: + details = "; ".join(mismatches) + raise RuntimeError( + "supercombo warp JIT compatibility mismatch: " + f"{details}. Bundle {pkl_path} was compiled with the wrong backend, frame_skip, or queue shape; " + "re-download or rebuild this model artifact." + ) + + def _validate_jit_names(self) -> None: + expected_warp_names = ['big_frame', 'big_tfm', 'frame', 'tfm'] + expected_policy_names = ['big_img_q', 'desire_q', 'feat_q', 'img_q', 'packed_npy_inputs', 'warped'] + + mismatches: list[str] = [] + + policy_names = sorted(_captured_expected_names(self._run_policy)) + if policy_names and policy_names != expected_policy_names: + mismatches.append(f"run_policy captured={policy_names} expected={expected_policy_names}") + + for cam, warp_jit in sorted(self._warp_jits.items()): + warp_names = sorted(_captured_expected_names(warp_jit)) + if warp_names and warp_names != expected_warp_names: + mismatches.append(f"{cam[0]}x{cam[1]} warp captured={warp_names} expected={expected_warp_names}") + + if mismatches: + details = "; ".join(mismatches) + actual_sha = None + try: + actual_sha = _file_sha256(self._pkl_path) + except OSError: + pass + + if actual_sha and self._expected_sha256 and actual_sha != self._expected_sha256: + try: + os.remove(self._pkl_path) + except OSError: + pass + redownload_msg = self._schedule_active_bundle_redownload() + raise RuntimeError( + "supercombo artifact contract mismatch with stale cached SHA: " + f"{details}. Expected SHA {self._expected_sha256}, got {actual_sha}. " + f"Deleted the stale cached file{redownload_msg}." + ) + + raise RuntimeError( + "supercombo artifact JIT argument mismatch: " + f"{details}. This model file does not match the current IQPilot runtime contract. " + "Re-download or rebuild this model artifact." + ) + + def _handle_runtime_jit_mismatch(self, err: BaseException) -> None: + if not _is_jit_arg_mismatch(err): + raise err + + actual_sha = None + try: + actual_sha = _file_sha256(self._pkl_path) + except OSError: + pass + + if actual_sha and self._expected_sha256 and actual_sha != self._expected_sha256: + try: + os.remove(self._pkl_path) + except OSError: + pass + redownload_msg = self._schedule_active_bundle_redownload() + raise RuntimeError( + "supercombo artifact runtime JIT mismatch with stale cached SHA: " + f"expected {self._expected_sha256}, got {actual_sha} for {self._pkl_path}. " + f"Deleted the stale cached file{redownload_msg}." + ) from err + + raise RuntimeError( + "supercombo artifact runtime JIT mismatch: " + f"{err}. This model file does not match the current IQPilot runtime contract. " + "Re-download or rebuild this model artifact." + ) from err + + def _schedule_active_bundle_redownload(self) -> str: + try: + params = Params() + active_bundle = params.get("ModelManager_ActiveBundle") or {} + index = active_bundle.get("index") if isinstance(active_bundle, dict) else None + if isinstance(index, str) and index.isdigit(): + index = int(index) + if isinstance(index, int) and index >= 0: + params.put("ModelManager_DownloadIndex", str(index)) + params.remove("ModelRunnerTypeCache") + return "; scheduled automatic re-download of the active model" + except Exception: + pass + + return "; unable to schedule automatic re-download" + + def _frame_tensor(self, key: str, buf): + Tensor, Device = _tinygrad_imports() + arr = np.frombuffer(buf.data, dtype=np.uint8) + ck = (key, arr.ctypes.data) + t = self._blob_cache.get(ck) + if t is None: + t = Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype='uint8', device=Device.DEFAULT) + self._blob_cache[ck] = t + return t + + @property + def vision_input_names(self) -> list[str]: + return ['img', 'big_img'] + + @property + def input_shapes(self) -> ShapeDict: + return dict(self._ish) + + @property + def output_slices(self) -> SliceDict: + return dict(self._slices) + + def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + raise RuntimeError("supercombo runner has no OpenCL path; use run_fused()") + + def _ensure_queues(self, cam_w: int, cam_h: int) -> None: + if self._queues is not None and self._cam == (cam_w, cam_h): + return + if (cam_w, cam_h) not in self._warp_jits: + raise RuntimeError(f"no warp JIT for {cam_w}x{cam_h}; have {sorted(self._warp_jits)}") + + Tensor, Device = _tinygrad_imports() + fs = self._frame_skip + img = self._ish['img'] + n_frames = img[1] // 6 + img_buf = (fs * (n_frames - 1) + 1, 6, img[2], img[3]) + fb = self._ish['features_buffer'] + dp = self._ish['desire_pulse'] + tc = self._ish['traffic_convention'] + at = self._ish['action_t'] + + zeros_u8 = lambda s: Tensor(np.zeros(s, dtype=np.uint8), device=Device.DEFAULT).contiguous().realize() + zeros_f32 = lambda s: Tensor(np.zeros(s, dtype=np.float32), device=Device.DEFAULT).contiguous().realize() + + shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} + sizes = [math.prod(s) for s in shapes.values()] + packed = np.zeros(sum(sizes), dtype=np.float32) + views = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed, np.cumsum(sizes[:-1])), strict=True)} + + self._npy = {'tfm': np.zeros((3, 3), dtype=np.float32), 'big_tfm': np.zeros((3, 3), dtype=np.float32), **views} + self._queues = { + 'img_q': zeros_u8(img_buf), + 'big_img_q': zeros_u8(img_buf), + 'feat_q': zeros_f32((fs * fb[1], fb[0], fb[2])), + 'desire_q': zeros_f32((fs * dp[1], dp[0], dp[2])), + 'tfm': Tensor(self._npy['tfm'], device='NPY'), + 'big_tfm': Tensor(self._npy['big_tfm'], device='NPY'), + 'packed_npy_inputs': Tensor(packed, device='NPY'), + } + self._cam = (cam_w, cam_h) + + def run_fused(self, bufs: dict, transforms: dict[str, np.ndarray], numpy_inputs: NumpyDict) -> NumpyDict: + Tensor, Device = _tinygrad_imports() + main_buf = bufs['img'] + self._ensure_queues(main_buf.width, main_buf.height) + assert self._queues is not None and self._npy is not None + + self._npy['tfm'][:] = transforms['img'] + self._npy['big_tfm'][:] = transforms['big_img'] + + desire_key = next((k for k in numpy_inputs if k.startswith('desire')), None) + cur = numpy_inputs[desire_key].copy() if desire_key is not None else np.zeros_like(self._prev_desire) + cur[0] = 0 + self._npy['desire'][:] = np.where(cur - self._prev_desire > .99, cur, 0) + self._prev_desire[:] = cur + if 'traffic_convention' in numpy_inputs: + self._npy['traffic_convention'][:] = numpy_inputs['traffic_convention'] + if 'action_t' in numpy_inputs: + self._npy['action_t'][:] = numpy_inputs['action_t'] + + frame = self._frame_tensor('img', bufs['img']) + big_frame = self._frame_tensor('big_img', bufs['big_img']) + + warp = self._warp_jits[self._cam] + try: + warped = warp(tfm=self._queues['tfm'], big_tfm=self._queues['big_tfm'], frame=frame, big_frame=big_frame) + out, = self._run_policy(warped=warped, img_q=self._queues['img_q'], big_img_q=self._queues['big_img_q'], + feat_q=self._queues['feat_q'], desire_q=self._queues['desire_q'], + packed_npy_inputs=self._queues['packed_npy_inputs']) + except Exception as err: + self._handle_runtime_jit_mismatch(err) + raise + flat = out.numpy().flatten() + + self._npy['prev_feat'][:] = flat[self._hidden_slice].reshape(self._npy['prev_feat'].shape) + + sliced = {k: flat[np.newaxis, sl] for k, sl in self._slices.items()} + return self._parser.parse_vision_outputs(sliced) + + def _run_model(self) -> NumpyDict: + raise RuntimeError("supercombo path goes through run_fused(), not _run_model()") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py new file mode 100644 index 000000000..2d91c3df1 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/runners/tinygrad/tinygrad_runner.py @@ -0,0 +1,190 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +import pickle +from dataclasses import dataclass + +import numpy as np +from tinygrad.dtype import dtypes +from tinygrad.tensor import Tensor + +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ( + CLMemDict, + CUSTOM_MODEL_PATH, + FrameDict, + ModelType, + NumpyDict, + ShapeDict, + SliceDict, +) +from iqpilot.selfdrive.iqmodeld.models.runners.model_runner import ModelRunner +from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.model_types import ( + OffPolicyTinygrad, + OnPolicyTinygrad, + PolicyTinygrad, + SupercomboTinygrad, + VisionTinygrad, +) +from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants +from iqpilot.selfdrive.iqmodeld.config import ModelConstants +from iqpilot.selfdrive.iqmodeld.runtime.tinygrad import qcom_tensor_from_opencl_address +from iqpilot.system.hardware import TICI + + +@dataclass(frozen=True) +class _TensorShapePlan: + dtype: object + device: str + + +def _artifact_path(filename: str) -> str: + return f"{CUSTOM_MODEL_PATH}/{filename}" + + +def _load_program_blob(filename: str): + with open(_artifact_path(filename), "rb") as artifact: + try: + return pickle.load(artifact) + except FileNotFoundError as exc: + assert "/dev/kgsl-3d0" not in str(exc), "Model was built on C3 or C3X, but is being loaded on PC" + raise + + +def _compile_input_plan(captured) -> dict[str, _TensorShapePlan]: + plan: dict[str, _TensorShapePlan] = {} + for name, info in zip(captured.expected_names, captured.expected_input_info, strict=True): + plan[name] = _TensorShapePlan(dtype=info[2], device=info[3]) + return plan + + +def _merge_step_outputs(output_groups: list[NumpyDict]) -> NumpyDict: + stitched: NumpyDict = {} + for payload in output_groups: + stitched.update(payload) + if "planplus" in stitched and "plan" in stitched: + stitched["plan"] = stitched["plan"] + stitched["planplus"] + return stitched + + +class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad): + def __init__(self, model_type: int = ModelType.supercombo): + ModelRunner.__init__(self) + for initializer in (SupercomboTinygrad, PolicyTinygrad, VisionTinygrad, OffPolicyTinygrad, OnPolicyTinygrad): + initializer.__init__(self) + + self._constants = ModelConstants + self._model_data = self.models.get(model_type) + if self._model_data is None or self._model_data.model is None: + raise ValueError(f"Model data for type {model_type} not available.") + + asset_name = self._model_data.model.artifact.fileName + assert asset_name.endswith("_tinygrad.pkl"), f"Invalid model file {asset_name} for TinygradRunner" + + self.model_run = _load_program_blob(asset_name) + self._input_plan = _compile_input_plan(self.model_run.captured) + for name, spec in self._input_plan.items(): + if "img" in name and spec.dtype is not dtypes.uint8: + raise ValueError(f"{asset_name}: image input {name} expects {spec.dtype}, incompatible with uint8 warp buffer") + self.input_to_dtype = {name: spec.dtype for name, spec in self._input_plan.items()} + self.input_to_device = {name: spec.device for name, spec in self._input_plan.items()} + + @property + def vision_input_names(self) -> list[str]: + return [stream_name for stream_name in self.input_shapes if "img" in stream_name] + + def _attach_vision_tensor(self, stream_name: str, frame_buffers: CLMemDict, frame_views: FrameDict) -> None: + spec = self._input_plan[stream_name] + frame_buffer = frame_buffers[stream_name] + if TICI: + self.inputs[stream_name] = qcom_tensor_from_opencl_address(frame_buffer.mem_address, + self.input_shapes[stream_name], + dtype=spec.dtype) + return + + mirrored = frame_views[stream_name].as_numpy(frame_buffer).reshape(self.input_shapes[stream_name]) + self.inputs[stream_name] = Tensor(mirrored, device=spec.device, dtype=spec.dtype).realize() + + def _attach_state_tensor(self, tensor_name: str, tensor_value: np.ndarray) -> None: + spec = self._input_plan[tensor_name] + self.inputs[tensor_name] = Tensor(tensor_value, device=spec.device, dtype=spec.dtype).realize() + + def prepare_vision_inputs(self, imgs_cl: CLMemDict, frames: FrameDict): + for stream_name in imgs_cl: + if stream_name not in self.inputs or not TICI: + self._attach_vision_tensor(stream_name, imgs_cl, frames) + + def prepare_policy_inputs(self, numpy_inputs: NumpyDict): + for tensor_name, tensor_value in numpy_inputs.items(): + self._attach_state_tensor(tensor_name, tensor_value) + + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + self.prepare_vision_inputs(imgs_cl, frames) + self.prepare_policy_inputs(numpy_inputs) + return self.inputs + + def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: + if self._model_data is None: + raise ValueError("Model data is not available. Ensure the model is loaded correctly.") + return self.parser_method_dict[self._model_data.model.type.raw](model_outputs) + + def _run_model(self) -> NumpyDict: + raw_output = self.model_run(**self.inputs).numpy().reshape(-1) + return self._parse_outputs(raw_output) + + +class TinygradSplitRunner(ModelRunner): + def __init__(self): + super().__init__() + self.is_20hz_3d = True + self._constants = SplitModelConstants + self.vision_runner = TinygradRunner(ModelType.vision) + self.policy_runner = TinygradRunner(ModelType.policy) if self.models.get(ModelType.policy) else None + self.off_policy_runner = TinygradRunner(ModelType.offPolicy) if self.models.get(ModelType.offPolicy) else None + self.on_policy_runner = TinygradRunner(ModelType.onPolicy) if self.models.get(ModelType.onPolicy) else None + + def _policy_units(self) -> list[TinygradRunner]: + return [runner for runner in (self.policy_runner, self.off_policy_runner, self.on_policy_runner) if runner is not None] + + def run_vision(self) -> NumpyDict: + return self.vision_runner.run_model() + + def run_policy(self) -> NumpyDict: + return _merge_step_outputs([runner.run_model() for runner in self._policy_units()]) + + def refresh_policy_features(self, features_buffer: np.ndarray) -> None: + for runner in self._policy_units(): + if "features_buffer" in runner._input_plan: + runner._attach_state_tensor("features_buffer", features_buffer) + + def _run_model(self) -> NumpyDict: + return _merge_step_outputs([self.run_vision(), self.run_policy()]) + + @property + def vision_input_names(self) -> list[str]: + return list(self.vision_runner.vision_input_names) + + @property + def input_shapes(self) -> ShapeDict: + composite: ShapeDict = dict(self.vision_runner.input_shapes) + for runner in self._policy_units(): + composite.update(runner.input_shapes) + return composite + + @property + def output_slices(self) -> SliceDict: + composite: SliceDict = dict(self.vision_runner.output_slices) + for runner in self._policy_units(): + composite.update(runner.output_slices) + return composite + + def prepare_inputs(self, imgs_cl: CLMemDict, numpy_inputs: NumpyDict, frames: FrameDict) -> dict: + self.vision_runner.prepare_vision_inputs(imgs_cl, frames) + assembled_inputs = dict(self.vision_runner.inputs) + for runner in self._policy_units(): + runner.prepare_policy_inputs(numpy_inputs) + assembled_inputs.update(runner.inputs) + self.inputs = assembled_inputs + return assembled_inputs diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py new file mode 100644 index 000000000..3f83ea806 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/models/split_model_constants.py @@ -0,0 +1,89 @@ +import numpy as np + + +def index_function(idx, max_val=192, max_idx=32): + return max_val * ((idx/max_idx)**2) + + +class SplitModelConstants: + IDX_N = 33 + T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] + X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] + LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] + LEAD_T_OFFSETS = [0., 2., 4.] + META_T_IDXS = [2., 4., 6., 8., 10.] + + MODEL_FREQ = 20 + HISTORY_FREQ = 5 + HISTORY_LEN_SECONDS = 5 + TEMPORAL_SKIP = MODEL_FREQ // HISTORY_FREQ + FULL_HISTORY_BUFFER_LEN = MODEL_FREQ * HISTORY_LEN_SECONDS + INPUT_HISTORY_BUFFER_LEN = HISTORY_FREQ * HISTORY_LEN_SECONDS + + FEATURE_LEN = 512 + + DESIRE_LEN = 8 + TRAFFIC_CONVENTION_LEN = 2 + LAT_PLANNER_STATE_LEN = 4 + LATERAL_CONTROL_PARAMS_LEN = 2 + PREV_DESIRED_CURV_LEN = 1 + + FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) + FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) + FCW_5MS2_PROBS_WIDTH = 5 + FCW_3MS2_PROBS_WIDTH = 2 + + DISENGAGE_WIDTH = 5 + POSE_WIDTH = 6 + WIDE_FROM_DEVICE_WIDTH = 3 + LEAD_WIDTH = 4 + LANE_LINES_WIDTH = 2 + ROAD_EDGES_WIDTH = 2 + PLAN_WIDTH = 15 + DESIRE_PRED_WIDTH = 8 + LAT_PLANNER_SOLUTION_WIDTH = 4 + DESIRED_CURV_WIDTH = 1 + ACTION_WIDTH = 2 + + NUM_LANE_LINES = 4 + NUM_ROAD_EDGES = 2 + + LEAD_TRAJ_LEN = 6 + DESIRE_PRED_LEN = 4 + + PLAN_MHP_N = 5 + LEAD_MHP_N = 2 + PLAN_MHP_SELECTION = 1 + LEAD_MHP_SELECTION = 3 + + FCW_THRESHOLD_5MS2_HIGH = 0.15 + FCW_THRESHOLD_5MS2_LOW = 0.05 + FCW_THRESHOLD_3MS2 = 0.7 + + CONFIDENCE_BUFFER_LEN = 5 + RYG_GREEN = 0.01165 + RYG_YELLOW = 0.06157 + + POLY_PATH_DEGREE = 4 + + +class Plan: + POSITION = slice(0, 3) + VELOCITY = slice(3, 6) + ACCELERATION = slice(6, 9) + T_FROM_CURRENT_EULER = slice(9, 12) + ORIENTATION_RATE = slice(12, 15) + + +class Meta: + ENGAGED = slice(0, 1) + GAS_DISENGAGE = slice(1, 31, 6) + BRAKE_DISENGAGE = slice(2, 31, 6) + STEER_OVERRIDE = slice(3, 31, 6) + HARD_BRAKE_3 = slice(4, 31, 6) + HARD_BRAKE_4 = slice(5, 31, 6) + HARD_BRAKE_5 = slice(6, 31, 6) + GAS_PRESS = slice(31, 55, 4) + BRAKE_PRESS = slice(32, 55, 4) + LEFT_BLINKER = slice(33, 55, 4) + RIGHT_BLINKER = slice(34, 55, 4) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/parser.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/parser.py new file mode 100644 index 000000000..1b382b57f --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/parser.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.models.split_model_constants import SplitModelConstants +from iqpilot.selfdrive.iqmodeld.config import ModelConstants + + +def safe_exp(values, out=None): + return np.exp(np.clip(values, -np.inf, 11), out=out) + + +def sigmoid(values): + return 1.0 / (1.0 + safe_exp(-values)) + + +def _softmax_last(values, axis=-1): + values -= np.max(values, axis=axis, keepdims=True) + if values.dtype in (np.float32, np.float64): + safe_exp(values, out=values) + else: + values = safe_exp(values) + values /= np.sum(values, axis=axis, keepdims=True) + return values + + +@dataclass(frozen=True) +class _MixtureRecipe: + input_heads: int + output_heads: int + final_shape: tuple[int, ...] + + +class _TensorKitchen: + def __init__(self, ignore_missing: bool = False): + self.ignore_missing = ignore_missing + + def _grab(self, outputs: dict[str, np.ndarray], tensor_name: str) -> np.ndarray | None: + if tensor_name not in outputs: + if not self.ignore_missing: + raise ValueError(f"Missing output {tensor_name}") + return + return outputs[tensor_name] + + def categorical(self, outputs: dict[str, np.ndarray], tensor_name: str, shape=None) -> None: + raw = self._grab(outputs, tensor_name) + if raw is None: + return + if shape is not None: + raw = raw.reshape((raw.shape[0],) + shape) + outputs[tensor_name] = _softmax_last(raw, axis=-1) + + def binary(self, outputs: dict[str, np.ndarray], tensor_name: str) -> None: + raw = self._grab(outputs, tensor_name) + if raw is None: + return + outputs[tensor_name] = sigmoid(raw) + + def mixture(self, outputs: dict[str, np.ndarray], tensor_name: str, recipe: _MixtureRecipe) -> None: + raw = self._grab(outputs, tensor_name) + if raw is None: + return + + reshaped = raw.reshape((raw.shape[0], max(recipe.input_heads, 1), -1)) + value_count = (reshaped.shape[2] - recipe.output_heads) // 2 + means = reshaped[:, :, :value_count] + stds = safe_exp(reshaped[:, :, value_count:2 * value_count]) + + if recipe.input_heads > 1: + weights = np.zeros((reshaped.shape[0], recipe.input_heads, recipe.output_heads), dtype=reshaped.dtype) + for output_idx in range(recipe.output_heads): + weights[:, :, output_idx - recipe.output_heads] = _softmax_last( + reshaped[:, :, output_idx - recipe.output_heads], axis=-1 + ) + + if recipe.output_heads == 1: + for batch_idx in range(weights.shape[0]): + order = np.argsort(weights[batch_idx][:, 0])[::-1] + weights[batch_idx] = weights[batch_idx][order] + means[batch_idx] = means[batch_idx][order] + stds[batch_idx] = stds[batch_idx][order] + + hypothesis_shape = (reshaped.shape[0], recipe.input_heads, *recipe.final_shape) + outputs[f"{tensor_name}_weights"] = weights + outputs[f"{tensor_name}_hypotheses"] = means.reshape(hypothesis_shape) + outputs[f"{tensor_name}_stds_hypotheses"] = stds.reshape(hypothesis_shape) + + picked_means = np.zeros((reshaped.shape[0], recipe.output_heads, value_count), dtype=reshaped.dtype) + picked_stds = np.zeros((reshaped.shape[0], recipe.output_heads, value_count), dtype=reshaped.dtype) + for batch_idx in range(weights.shape[0]): + for output_idx in range(recipe.output_heads): + order = np.argsort(weights[batch_idx, :, output_idx])[::-1] + picked_means[batch_idx, output_idx] = means[batch_idx, order[0]] + picked_stds[batch_idx, output_idx] = stds[batch_idx, order[0]] + else: + picked_means = means + picked_stds = stds + + final_shape = ((reshaped.shape[0], recipe.output_heads, *recipe.final_shape) + if recipe.output_heads > 1 else (reshaped.shape[0], *recipe.final_shape)) + outputs[tensor_name] = picked_means.reshape(final_shape) + outputs[f"{tensor_name}_stds"] = picked_stds.reshape(final_shape) + + +class ArchiveParser(_TensorKitchen): + def __init__(self, ignore_missing: bool = False): + super().__init__(ignore_missing=ignore_missing) + self._c = ModelConstants + + def _recipes(self) -> list[tuple[str, _MixtureRecipe]]: + c = self._c + return [ + ("plan", _MixtureRecipe(c.PLAN_MHP_N, c.PLAN_MHP_SELECTION, (c.IDX_N, c.PLAN_WIDTH))), + ("lane_lines", _MixtureRecipe(0, 0, (c.NUM_LANE_LINES, c.IDX_N, c.LANE_LINES_WIDTH))), + ("road_edges", _MixtureRecipe(0, 0, (c.NUM_ROAD_EDGES, c.IDX_N, c.LANE_LINES_WIDTH))), + ("pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))), + ("road_transform", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))), + ("wide_from_device_euler", _MixtureRecipe(0, 0, (c.WIDE_FROM_DEVICE_WIDTH,))), + ("lead", _MixtureRecipe(c.LEAD_MHP_N, c.LEAD_MHP_SELECTION, (c.LEAD_TRAJ_LEN, c.LEAD_WIDTH))), + ] + + def parse_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + c = self._c + for tensor_name, recipe in self._recipes(): + self.mixture(outputs, tensor_name, recipe) + if "sim_pose" in outputs: + self.mixture(outputs, "sim_pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))) + if "lat_planner_solution" in outputs: + self.mixture(outputs, "lat_planner_solution", _MixtureRecipe(0, 0, (c.IDX_N, c.LAT_PLANNER_SOLUTION_WIDTH))) + if "desired_curvature" in outputs: + self.mixture(outputs, "desired_curvature", _MixtureRecipe(0, 0, (c.DESIRED_CURV_WIDTH,))) + for name in ("lead_prob", "lane_lines_prob", "meta"): + self.binary(outputs, name) + self.categorical(outputs, "desire_state", shape=(c.DESIRE_PRED_WIDTH,)) + self.categorical(outputs, "desire_pred", shape=(c.DESIRE_PRED_LEN, c.DESIRE_PRED_WIDTH)) + return outputs + + +class PhaseParser(_TensorKitchen): + def __init__(self, ignore_missing: bool = False): + super().__init__(ignore_missing=ignore_missing) + self._c = SplitModelConstants + + def _has_mixture_heads(self, outputs: dict[str, np.ndarray], tensor_name: str, flat_width: int) -> bool: + raw = self._grab(outputs, tensor_name) + if raw is None: + return False + return raw.shape[1] != 2 * flat_width + + def _decode_dynamic_family(self, outputs: dict[str, np.ndarray]) -> None: + c = self._c + if "lead" in outputs: + uses_heads = self._has_mixture_heads(outputs, "lead", c.LEAD_MHP_SELECTION * c.LEAD_TRAJ_LEN * c.LEAD_WIDTH) + self.mixture(outputs, "lead", _MixtureRecipe( + c.LEAD_MHP_N if uses_heads else 0, + c.LEAD_MHP_SELECTION if uses_heads else 0, + (c.LEAD_TRAJ_LEN, c.LEAD_WIDTH) if uses_heads else (c.LEAD_MHP_SELECTION, c.LEAD_TRAJ_LEN, c.LEAD_WIDTH), + )) + + if "plan" in outputs: + uses_heads = self._has_mixture_heads(outputs, "plan", c.IDX_N * c.PLAN_WIDTH) + self.mixture(outputs, "plan", _MixtureRecipe( + c.PLAN_MHP_N if uses_heads else 0, + c.PLAN_MHP_SELECTION if uses_heads else 0, + (c.IDX_N, c.PLAN_WIDTH), + )) + + if "planplus" in outputs: + self.mixture(outputs, "planplus", _MixtureRecipe(0, 0, (c.IDX_N, c.PLAN_WIDTH))) + + def _decode_policy_family(self, outputs: dict[str, np.ndarray]) -> None: + c = self._c + if "action" in outputs: + self.mixture(outputs, "action", _MixtureRecipe(0, 0, (c.ACTION_WIDTH,))) + if "desired_curvature" in outputs: + self.mixture(outputs, "desired_curvature", _MixtureRecipe(0, 0, (c.DESIRED_CURV_WIDTH,))) + if "desire_pred" in outputs: + self.categorical(outputs, "desire_pred", shape=(c.DESIRE_PRED_LEN, c.DESIRE_PRED_WIDTH)) + if "desire_state" in outputs: + self.categorical(outputs, "desire_state", shape=(c.DESIRE_PRED_WIDTH,)) + if "lane_lines" in outputs: + self.mixture(outputs, "lane_lines", _MixtureRecipe(0, 0, (c.NUM_LANE_LINES, c.IDX_N, c.LANE_LINES_WIDTH))) + if "lane_lines_prob" in outputs: + self.binary(outputs, "lane_lines_prob") + if "lead_prob" in outputs: + self.binary(outputs, "lead_prob") + if "lat_planner_solution" in outputs: + self.mixture(outputs, "lat_planner_solution", _MixtureRecipe(0, 0, (c.IDX_N, c.LAT_PLANNER_SOLUTION_WIDTH))) + if "meta" in outputs: + self.binary(outputs, "meta") + if "road_edges" in outputs: + self.mixture(outputs, "road_edges", _MixtureRecipe(0, 0, (c.NUM_ROAD_EDGES, c.IDX_N, c.LANE_LINES_WIDTH))) + if "sim_pose" in outputs: + self.mixture(outputs, "sim_pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))) + + def parse_vision_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + c = self._c + self.mixture(outputs, "pose", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))) + self.mixture(outputs, "wide_from_device_euler", _MixtureRecipe(0, 0, (c.WIDE_FROM_DEVICE_WIDTH,))) + self.mixture(outputs, "road_transform", _MixtureRecipe(0, 0, (c.POSE_WIDTH,))) + self._decode_dynamic_family(outputs) + self._decode_policy_family(outputs) + return outputs + + def parse_policy_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + self._decode_dynamic_family(outputs) + self._decode_policy_family(outputs) + return outputs + + def parse_outputs(self, outputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + return self.parse_policy_outputs(self.parse_vision_outputs(outputs)) + + +__all__ = [ + "ArchiveParser", + "PhaseParser", +] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py new file mode 100644 index 000000000..75c7e350e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/runtime/tinygrad.py @@ -0,0 +1,23 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from tinygrad.tensor import Tensor +from tinygrad.helpers import to_mv + +_PTR_STRIDE = 8 +_RAW_GPU_PTR_SLOT = 20 +_RAW_GPU_PTR_VIEW_BYTES = 0x100 + + +def _descriptor_pointer(opencl_address: int) -> int: + return to_mv(opencl_address, _PTR_STRIDE).cast("Q")[0] + + +def _raw_gpu_pointer(descriptor_pointer: int) -> int: + return to_mv(descriptor_pointer, _RAW_GPU_PTR_VIEW_BYTES).cast("Q")[_RAW_GPU_PTR_SLOT] + + +def qcom_tensor_from_opencl_address(opencl_address, shape, dtype): + descriptor_pointer = _descriptor_pointer(opencl_address) + device_pointer = _raw_gpu_pointer(descriptor_pointer) + return Tensor.from_blob(device_pointer, shape, dtype=dtype, device="QCOM") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/temporal_state.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/temporal_state.py new file mode 100644 index 000000000..70cac9f64 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/temporal_state.py @@ -0,0 +1,131 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +import math + +import numpy as np + +DEFAULT_FRAME_SKIP = 4 + +MODEL_INPUT_SPEC: dict[str, tuple[tuple[int, ...], str]] = { + "img": ((1, 12, 128, 256), "uint8"), + "big_img": ((1, 12, 128, 256), "uint8"), + "desire_pulse": ((1, 25, 8), "float32"), + "traffic_convention": ((1, 2), "float32"), + "features_buffer": ((1, 24, 512), "float32"), + "action_t": ((1, 2), "float32"), +} + + +def spec_from_meta(meta: dict) -> dict[str, tuple[tuple[int, ...], str]] | None: + shapes = meta.get("input_shapes") + if not shapes: + return None + return {name: (tuple(shape), "uint8" if name in ("img", "big_img") else "float32") + for name, shape in shapes.items()} + + +class TemporalInputState: + def __init__(self, frame_skip: int, spec: dict[str, tuple[tuple[int, ...], str]] = MODEL_INPUT_SPEC): + self.frame_skip = frame_skip + img = spec["img"][0] + fb = spec["features_buffer"][0] + dp = spec["desire_pulse"][0] + + self.n_frames = img[1] // 6 + img_q_shape = (frame_skip * (self.n_frames - 1) + 1, 6, img[2], img[3]) + self._img_shape = img + self._fb_shape = fb + self._dp_shape = dp + feat_dim = math.prod(fb[2:]) + + self.img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.feat_q = np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32) + self.desire_q = np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32) + self.prev_desire = np.zeros(dp[2], dtype=np.float32) + self.prev_feat = np.zeros((fb[0], feat_dim), dtype=np.float32) + + @staticmethod + def _shift_append(q: np.ndarray, new_val: np.ndarray) -> None: + q[:-1] = q[1:] + q[-1] = new_val + + def push_and_materialize(self, warped: np.ndarray, desire_pulse: np.ndarray, + traffic_convention: np.ndarray, action_t: np.ndarray, + ) -> dict[str, np.ndarray]: + fs = self.frame_skip + + cur = desire_pulse.astype(np.float32).copy() + cur[0] = 0 + pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32) + self.prev_desire[:] = cur + + self._shift_append(self.img_q, warped[0]) + self._shift_append(self.big_img_q, warped[1]) + self._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2])) + self._shift_append(self.feat_q, self.prev_feat) + + dp = self._dp_shape + return { + "img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape), + "big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape), + "features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape), + "desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp), + "traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1), + "action_t": action_t.astype(np.float32).reshape(1, -1), + } + + def note_hidden_state(self, model_output: np.ndarray, hidden_slice: slice) -> None: + self.prev_feat[:] = model_output[hidden_slice].reshape(self.prev_feat.shape) + + +class SplitTemporalState: + + def __init__(self, frame_skip: int, img_shape: tuple[int, ...], + feature_shape: tuple[int, ...], desire_shape: tuple[int, ...]): + self.frame_skip = frame_skip + self._img_shape = tuple(img_shape) + self._fb_shape = tuple(feature_shape) + self._dp_shape = tuple(desire_shape) + + n_frames = img_shape[1] // 6 + img_q_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3]) + self.img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.feat_q = np.zeros((frame_skip * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), + dtype=np.float32) + self.desire_q = np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32) + self.prev_desire = np.zeros(desire_shape[2], dtype=np.float32) + + def materialize_vision(self, warped: np.ndarray, desire: np.ndarray) -> dict[str, np.ndarray]: + fs = self.frame_skip + cur = desire.astype(np.float32).copy() + cur[0] = 0 + pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32) + self.prev_desire[:] = cur + + TemporalInputState._shift_append(self.img_q, warped[0]) + TemporalInputState._shift_append(self.big_img_q, warped[1]) + TemporalInputState._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2])) + return { + "img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape), + "big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape), + } + + def materialize_policy(self, vision_feature: np.ndarray, traffic_convention: np.ndarray, + action_t: np.ndarray | None = None) -> dict[str, np.ndarray]: + fs = self.frame_skip + TemporalInputState._shift_append(self.feat_q, vision_feature.reshape(self._fb_shape[0], self._fb_shape[2])) + dp = self._dp_shape + out = { + "features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape), + "desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp), + "traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1), + } + if action_t is not None: + out["action_t"] = action_t.astype(np.float32).reshape(1, -1) + return out diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py new file mode 100644 index 000000000..56ce00b2e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_supercombo.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +import argparse +import atexit +import math +import os +import pickle +import re +import tempfile +import time +from functools import partial +from collections import namedtuple + +import numpy as np + +def _patch_tinygrad_fetch_fw(): + import hashlib + import pathlib + import zstandard + from tinygrad import helpers + _orig = helpers.fetch_fw + def fetch_fw(path, name, sha256): + p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst") + if p.is_file(): + blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read() + if hashlib.sha256(blob).hexdigest() == sha256: + return blob + return _orig(path, name, sha256) + helpers.fetch_fw = fetch_fw +_patch_tinygrad_fetch_fw() + +from tinygrad.tensor import Tensor +from tinygrad.helpers import Context +from tinygrad.device import Device +from tinygrad.engine.jit import TinyJit + + +NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) +WARP_INPUTS = ['tfm', 'big_tfm'] +POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] + +UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) +UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) + +WARP_DEV = os.getenv('WARP_DEV') + + +def make_random_images(keys, shape, device=None): + return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys} + + +def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None): + w_dst, h_dst = dst_shape + h_src, w_src = src_shape + + x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) + y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) + + src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] + src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2] + src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2] + + src_x = src_x / src_w + src_y = src_y / src_w + + x_round = Tensor.round(src_x) + y_round = Tensor.round(src_y) + x_nn_clipped = x_round.clip(0, w_src - 1).cast('int') + y_nn_clipped = y_round.clip(0, h_src - 1).cast('int') + idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped + sampled = src_flat[idx] + + if border_fill_val is None: + return sampled + + in_bounds = ((x_round >= 0) & (x_round <= w_src - 1) & + (y_round >= 0) & (y_round <= h_src - 1)).cast(sampled.dtype) + return sampled * in_bounds + Tensor(border_fill_val, dtype=sampled.dtype) * (1 - in_bounds) + + +def frames_to_tensor(frames): + H = (frames.shape[0] * 2) // 3 + W = frames.shape[1] + in_img1 = Tensor.cat(frames[0:H:2, 0::2], + frames[1:H:2, 0::2], + frames[0:H:2, 1::2], + frames[1:H:2, 1::2], + frames[H:H+H//4].reshape((H//2, W//2)), + frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2)) + return in_img1 + + +def make_frame_prepare(nv12: NV12Frame, model_w, model_h): + cam_w, cam_h, stride, y_height, uv_height, _ = nv12 + uv_offset = stride * y_height + stride_pad = stride - cam_w + + def frame_prepare_tinygrad(input_frame, M_inv): + M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV) + uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) + with Context(SPLIT_REDUCEOP=0): + y = warp_perspective_tinygrad(input_frame[:cam_h*stride], + M_inv, (model_w, model_h), + (cam_h, cam_w), stride_pad).realize() + u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(), + M_inv_uv, (model_w//2, model_h//2), + (cam_h//2, cam_w//2), 0).realize() + v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(), + M_inv_uv, (model_w//2, model_h//2), + (cam_h//2, cam_w//2), 0).realize() + yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w)) + tensor = frames_to_tensor(yuv) + return tensor + return frame_prepare_tinygrad + + +def make_warp_input_queues(vision_input_shapes, frame_skip, device): + img = vision_input_shapes['img'] # (1, 12, 128, 256) + n_frames = img[1] // 6 + img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) + + npy = { + 'tfm': np.zeros((3, 3), dtype=np.float32), + 'big_tfm': np.zeros((3, 3), dtype=np.float32), + } + input_queues = { + 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + **{k: Tensor(v, device='NPY').realize() for k, v in npy.items()}, + } + return input_queues, npy + + +def get_policy_npy_shapes(input_shapes): + dp = input_shapes['desire_pulse'] # (1, 25, 8) + tc = input_shapes['traffic_convention'] # (1, 2) + at = input_shapes['action_t'] # (1, 2) + fb = input_shapes['features_buffer'] # (1, 24, 512) + shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} + return shapes, [math.prod(s) for s in shapes.values()] + + +def make_input_queues(input_shapes, frame_skip, device): + input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) + + fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature + dp = input_shapes['desire_pulse'] # (1, 25, 8) + + shapes, sizes = get_policy_npy_shapes(input_shapes) + packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) + npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) + input_queues.update({ + 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), + 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), + 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), + }) + return input_queues, npy + + +def shift_and_sample(buf, new_val, sample_fn): + buf.assign(buf[1:].cat(new_val, dim=0).contiguous()) + return sample_fn(buf) + + +def sample_skip(buf, frame_skip): + return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0) + + +def sample_desire(buf, frame_skip): + return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0) + + +def make_warp(nv12, model_w, model_h, frame_skip): + frame_prepare = make_frame_prepare(nv12, model_w, model_h) + + def warp(tfm, big_tfm, frame, big_frame): + tfm = tfm.to(WARP_DEV) + big_tfm = big_tfm.to(WARP_DEV) + Tensor.realize(tfm, big_tfm) + + warped_frame = frame_prepare(frame, tfm).unsqueeze(0) + warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) + return Tensor.cat(warped_frame, warped_big_frame) + + return warp + + +def make_run_policy(model_runner, model_metadata, frame_skip): + sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) + sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) + npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) + + def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): + packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT) + warped = warped.to(Device.DEFAULT) + Tensor.realize(packed_npy_inputs, warped) + + img = shift_and_sample(img_q, warped[0:1], sample_skip_fn) + big_img = shift_and_sample(big_img_q, warped[1:2], sample_skip_fn) + + desire, traffic_convention, action_t, prev_feat = (t.reshape(s) for t, s in zip(packed_npy_inputs.split(npy_sizes), npy_shapes.values(), strict=True)) + desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn) + feat_buf = shift_and_sample(feat_q, prev_feat.reshape(1, 1, -1), sample_skip_fn) + + inputs = { + 'img': img, + 'big_img': big_img, + 'features_buffer': feat_buf, + 'desire_pulse': desire_buf, + 'traffic_convention': traffic_convention, + 'action_t': action_t, + } + out = next(iter(model_runner(inputs).values())).cast('float32') + return out, + return run_policy + + +def compile_jit(jit, make_random_inputs, input_keys, make_queues): + SEED = 42 + def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): + input_queues, npy = make_queues(Device.DEFAULT) + np.random.seed(seed) + Tensor.manual_seed(seed) + + testing = test_val is not None or test_buffers is not None + n_runs = 1 if testing else 3 + + for i in range(n_runs): + for v in npy.values(): + v[:] = np.random.randn(*v.shape).astype(v.dtype) + Device.default.synchronize() + random_inputs = make_random_inputs() + st = time.perf_counter() + outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs) + mt = time.perf_counter() + Device.default.synchronize() + et = time.perf_counter() + print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") + + if i == 0: + val = [np.copy(v.numpy()) for v in outs] + buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()] + + if test_val is not None: + match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True)) + assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})" + if test_buffers is not None: + match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True)) + assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})" + return val, buffers + + print('capture + replay') + test_val, test_buffers = random_inputs_run(jit, SEED) + print('pickle round trip') + jit = pickle.loads(pickle.dumps(jit)) + random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True) + random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False) + return jit + + +def _captured_devices(jit) -> set[str]: + captured = getattr(jit, 'captured', None) + infos = getattr(captured, 'expected_input_info', None) + if not infos: + return set() + + devices: set[str] = set() + for info in infos: + if isinstance(info, tuple) and len(info) >= 4 and isinstance(info[3], str): + devices.add(info[3]) + return devices + + +def _slice_outputs(model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: + return {name: model_outputs[np.newaxis, tensor_slice] for name, tensor_slice in output_slices.items() if name != 'pad'} + + +def _validate_pose_outputs(parsed_outputs: dict[str, np.ndarray]) -> None: + from iqpilot.selfdrive.locationd.locationd import MIN_STD_SANITY_CHECK, ROTATION_SANITY_CHECK, TRANS_SANITY_CHECK + + required = ( + 'pose', 'pose_stds', 'wide_from_device_euler', 'wide_from_device_euler_stds', + 'road_transform', 'road_transform_stds', + ) + missing = [name for name in required if name not in parsed_outputs] + if missing: + raise AssertionError(f"parsed supercombo outputs missing required odometry tensors: {missing}") + + for name in required: + values = parsed_outputs[name] + if not np.isfinite(values).all(): + raise AssertionError(f"parsed supercombo output {name} contains non-finite values") + + pose = parsed_outputs['pose'][0] + pose_stds = parsed_outputs['pose_stds'][0] + road_transform_stds = parsed_outputs['road_transform_stds'][0] + wide_stds = parsed_outputs['wide_from_device_euler_stds'][0] + + if pose_stds.min() <= MIN_STD_SANITY_CHECK: + raise AssertionError(f"pose_stds min {pose_stds.min()} <= {MIN_STD_SANITY_CHECK}") + if road_transform_stds.min() <= MIN_STD_SANITY_CHECK: + raise AssertionError(f"road_transform_stds min {road_transform_stds.min()} <= {MIN_STD_SANITY_CHECK}") + if wide_stds.min() <= MIN_STD_SANITY_CHECK: + raise AssertionError(f"wide_from_device_euler_stds min {wide_stds.min()} <= {MIN_STD_SANITY_CHECK}") + + if np.linalg.norm(pose[:3]) > TRANS_SANITY_CHECK: + raise AssertionError(f"pose translation norm {np.linalg.norm(pose[:3])} exceeds {TRANS_SANITY_CHECK}") + if np.linalg.norm(pose[3:]) > ROTATION_SANITY_CHECK: + raise AssertionError(f"pose rotation norm {np.linalg.norm(pose[3:])} exceeds {ROTATION_SANITY_CHECK}") + if np.linalg.norm(pose_stds[:3]) > 10 * TRANS_SANITY_CHECK: + raise AssertionError( + f"pose translation std norm {np.linalg.norm(pose_stds[:3])} exceeds {10 * TRANS_SANITY_CHECK}" + ) + if np.linalg.norm(pose_stds[3:]) > 10 * ROTATION_SANITY_CHECK: + raise AssertionError( + f"pose rotation std norm {np.linalg.norm(pose_stds[3:])} exceeds {10 * ROTATION_SANITY_CHECK}" + ) + + +def validate_supercombo_release(run_policy_jit, model_runner, model_metadata, frame_skip, expected_device: str) -> None: + from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + + direct_fn = make_run_policy(model_runner, model_metadata, frame_skip) + parser = PhaseParser() + queue_factory = partial(make_input_queues, model_metadata['input_shapes'], frame_skip) + image_shape = model_metadata['input_shapes']['img'] + + jit_queues, jit_npy = queue_factory(Device.DEFAULT) + direct_queues, direct_npy = queue_factory(Device.DEFAULT) + + for payload in (jit_npy, direct_npy): + for name, value in payload.items(): + value[:] = 0 if value.dtype.kind in ('i', 'u') else 0.0 + + zero_inputs = { + 'warped': Tensor(np.zeros((2, 6, *image_shape[2:]), dtype=np.uint8), device=Device.DEFAULT).realize(), + } + + direct_outs, = direct_fn(**{k: direct_queues[k] for k in POLICY_INPUTS}, **zero_inputs) + jit_outs, = run_policy_jit(**{k: jit_queues[k] for k in POLICY_INPUTS}, **zero_inputs) + + direct_flat = direct_outs.numpy().astype(np.float32).reshape(-1) + jit_flat = jit_outs.numpy().astype(np.float32).reshape(-1) + + if not np.allclose(direct_flat, jit_flat, atol=1e-4, rtol=1e-4): + max_delta = float(np.max(np.abs(direct_flat - jit_flat))) + raise AssertionError(f"JIT supercombo output diverges from direct ONNX execution; max abs delta {max_delta}") + + parsed = parser.parse_vision_outputs(_slice_outputs(jit_flat, model_metadata['output_slices'])) + _validate_pose_outputs(parsed) + + captured_devices = _captured_devices(run_policy_jit) + if expected_device and captured_devices and expected_device not in captured_devices: + raise AssertionError( + f"compiled run_policy backend mismatch: captured {sorted(captured_devices)} expected {expected_device}" + ) + + +def _parse_size(s): + w, h = s.lower().split('x') + return int(w), int(h) + + +def read_file_chunked_to_shm(path): + from iqpilot.common.file_chunker import read_file_chunked + from iqpilot.system.hardware.hw import Paths + with tempfile.NamedTemporaryFile(prefix='compile_modeld_', dir=Paths.shm_path(), delete=False) as f: + f.write(read_file_chunked(path)) + tmp_path = f.name + atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path)) + return tmp_path + + +if __name__ == "__main__": + from tinygrad.nn.onnx import OnnxRunner + from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info + from iqpilot.selfdrive.iqmodeld.metadata import build_metadata_record + p = argparse.ArgumentParser() + p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH') + p.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True, + help='camera resolutions WxH (one or more)') + p.add_argument('--onnx', required=True) + p.add_argument('--output', required=True) + p.add_argument('--frame-skip', type=int, required=True) + p.add_argument('--expected-device', default='QCOM', help='expected tinygrad backend baked into the JIT') + args = p.parse_args() + + model_path = read_file_chunked_to_shm(args.onnx) + model_w, model_h = args.model_size + + model_runner = OnnxRunner(model_path) + out = { + 'metadata': build_metadata_record(model_path), + 'frame_skip': args.frame_skip, + } + + run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) + + make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip) + make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:])) + out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, + make_policy_queues) + validate_supercombo_release(out['run_policy'], model_runner, out['metadata'], args.frame_skip, args.expected_device) + + for cam_w, cam_h in args.camera_resolutions: + nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) + warp_enqueue = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) + make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip) + out[(cam_w,cam_h)] = compile_jit(warp_enqueue, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + captured_devices = _captured_devices(out[(cam_w,cam_h)]) + if args.expected_device and captured_devices and args.expected_device not in captured_devices: + raise AssertionError( + f"compiled warp backend mismatch for {cam_w}x{cam_h}: captured {sorted(captured_devices)} expected {args.expected_device}" + ) + + with open(args.output, "wb") as f: + pickle.dump(out, f) + print(f"Saved JITs to {args.output} ({os.path.getsize(args.output) / 1e6:.2f} MB)") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py new file mode 100644 index 000000000..07e4886eb --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +Compile the backend-neutral warp-only artifact: NV12 camera frames + 3x3 +transforms -> (2, 6, model_h/2, model_w/2) uint8 warped tensor, on the device +GPU (QCOM). maciqmodeld runs this locally +and feed the output to their backend, so the big model's image pipeline is +bit-identical to comma's fused pkl warp stage. + +Run ON the device (needs the QCOM backend): + cd /data/openpilot && DEV=QCOM WARP_DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 \ + python3 iqpilot/selfdrive/iqmodeld/tools/compile_warp.py \ + --camera-resolutions 1928x1208 --output /data/models/emac_warp.pkl +The artifact is then split per-resolution into Paths.model_root(). +""" +from __future__ import annotations + +import argparse +import hashlib +import os +import pickle +from functools import partial + +import numpy as np + +SELFTEST_SEED = 20260817 + +from iqpilot.selfdrive.iqmodeld.temporal_state import DEFAULT_FRAME_SKIP, MODEL_INPUT_SPEC +from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import ( + NV12Frame, WARP_INPUTS, compile_jit, make_random_images, make_warp, make_warp_input_queues, +) + +MODEL_SIZE = (MODEL_INPUT_SPEC["img"][0][3] * 2, MODEL_INPUT_SPEC["img"][0][2] * 2) # (512, 256) + + +def _parse_size(s: str) -> tuple[int, int]: + w, h = s.lower().split("x") + return int(w), int(h) + + +def compile_warp(cam_w: int, cam_h: int, out_path: str | None = None, + frame_skip: int = DEFAULT_FRAME_SKIP) -> str: + """Compile the warp-only QCOM JIT for one camera resolution and write the pkl. + Returns the artifact path. Callable from the workers so a fresh device + self-provisions the warp instead of erroring — needs the QCOM backend.""" + # the QCOM warp env must be set before tinygrad is imported here + os.environ.setdefault("DEV", "QCOM") + os.environ.setdefault("WARP_DEV", "QCOM") + os.environ.setdefault("IMAGE", "1") + os.environ.setdefault("FLOAT16", "1") + os.environ.setdefault("NOLOCALS", "1") + os.environ.setdefault("JIT_BATCH_SIZE", "0") + from tinygrad.engine.jit import TinyJit + from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info + from iqpilot.system.hardware.hw import Paths + + model_w, model_h = MODEL_SIZE + input_shapes = {name: shape for name, (shape, _) in MODEL_INPUT_SPEC.items()} + nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + make_random_warp_inputs = partial(make_random_images, keys=["frame", "big_frame"], + shape=nv12.size, device=os.getenv("WARP_DEV")) + warp_jit = TinyJit(make_warp(nv12, model_w, model_h, frame_skip), prune=True) + make_warp_queues = partial(make_warp_input_queues, input_shapes, frame_skip) + compiled = compile_jit(warp_jit, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + + # historical artifact name: already-provisioned devices keep their warp + out_path = out_path or os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl") + os.makedirs(os.path.dirname(out_path), exist_ok=True) + tmp = out_path + ".part" + bundle = {(cam_w, cam_h): compiled, "frame_skip": frame_skip, "model_size": MODEL_SIZE} + bundle["selftest"] = selftest_digest(compiled, cam_w, cam_h, nv12.size) + with open(tmp, "wb") as f: + pickle.dump(bundle, f) + os.replace(tmp, out_path) # atomic: a reader never sees a half-written pkl + return out_path + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("--camera-resolutions", type=_parse_size, nargs="+", default=[(1928, 1208)]) + p.add_argument("--output", default=None) + p.add_argument("--frame-skip", type=int, default=DEFAULT_FRAME_SKIP) + args = p.parse_args() + for cam_w, cam_h in args.camera_resolutions: + out = compile_warp(cam_w, cam_h, args.output, frame_skip=args.frame_skip) + print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)") + + +if __name__ == "__main__": + main() + + +def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int): + """A fixed synthetic frame pair and pair of matrices. Deterministic so the + digest is reproducible on the device that compiled the artifact.""" + rng = np.random.default_rng(SELFTEST_SEED) + frame = rng.integers(0, 256, nv12_size, dtype=np.uint8) + big_frame = rng.integers(0, 256, nv12_size, dtype=np.uint8) + tfm = np.array([[0.7, 0.02, 300.0], [0.01, 0.7, 240.0], [0.0, 0.0, 1.0]], dtype=np.float32) + big_tfm = np.array([[0.5, 0.01, 380.0], [0.02, 0.5, 300.0], [0.0, 0.0, 1.0]], dtype=np.float32) + return frame, big_frame, tfm, big_tfm + + +def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str: + """Hash the warp's output for a fixed input. + + A warp artifact pinned to one tinygrad can still unpickle under another and + then compute silently wrong, which reaches the model as a garbage image and + looks like a bad model rather than a stale artifact. A version string cannot + see that; running it can.""" + from tinygrad.tensor import Tensor + frame, big_frame, tfm, big_tfm = selftest_inputs(cam_w, cam_h, nv12_size) + dev = os.getenv("WARP_DEV") or "QCOM" + out = compiled(tfm=Tensor(tfm, device="NPY").realize(), + big_tfm=Tensor(big_tfm, device="NPY").realize(), + frame=Tensor(frame, device=dev).realize(), + big_frame=Tensor(big_frame, device=dev).realize()) + return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibration_helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibration_helpers.py new file mode 100644 index 000000000..11e39af3e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibration_helpers.py @@ -0,0 +1,32 @@ +import math + +import numpy as np +from iqpilot.cereal import log + +from iqpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT, HEIGHT_SANE_MIN, HEIGHT_SANE_MAX + + +def get_calibrated_rpy(live_calib: log.ExtrinsicsCalibration) -> np.ndarray | None: + if live_calib.calStatus != log.ExtrinsicsCalibration.Status.calibrated: + return None + + if len(live_calib.rpyCalib) != 3: + return None + + calib_rpy = np.asarray(live_calib.rpyCalib, dtype=np.float32) + return calib_rpy if np.isfinite(calib_rpy).all() else None + + +def get_render_path_height(live_calib: log.ExtrinsicsCalibration) -> float: + if live_calib.calStatus != log.ExtrinsicsCalibration.Status.calibrated: + return float(HEIGHT_INIT[0]) + + if len(live_calib.height) != 1: + return float(HEIGHT_INIT[0]) + + height = float(live_calib.height[0]) + if not math.isfinite(height): + return float(HEIGHT_INIT[0]) + if not (HEIGHT_SANE_MIN <= height <= HEIGHT_SANE_MAX): + return float(HEIGHT_INIT[0]) + return height diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py new file mode 100644 index 000000000..3e3ec6d45 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/calibrationd.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +''' +This process finds calibration values. More info on what these calibration values +are can be found here https://github.com/commaai/openpilot/tree/master/common/transformations +While the roll calibration is a real value that can be estimated, here we assume it's zero, +and the image input into the neural network is not corrected for roll. +''' + +import os +import capnp +import numpy as np +from typing import NoReturn + +from iqpilot.cereal import log, car +import iqpilot.cereal.messaging as messaging +from iqpilot.common.constants import CV +from iqpilot.common.params import Params +from iqpilot.common.issue_debug import log_issue_limited +from iqpilot.common.realtime import config_realtime_process +from iqpilot.common.transformations.orientation import rot_from_euler, euler_from_rot +from iqpilot.common.swaglog import cloudlog +from iqpilot.system.hardware import HARDWARE + +MIN_SPEED_FILTER = 15 * CV.MPH_TO_MS +MAX_VEL_ANGLE_STD = np.radians(0.25) +MAX_YAW_RATE_FILTER = np.radians(2) # per second + +MAX_HEIGHT_STD = np.exp(-3.5) + +# This is at model frequency, blocks needed for efficiency +SMOOTH_CYCLES = 10 +BLOCK_SIZE = 100 +INPUTS_NEEDED = 5 # Minimum blocks needed for valid calibration +INPUTS_WANTED = 50 # We want a little bit more than we need for stability +MAX_ALLOWED_YAW_SPREAD = np.radians(2) +MAX_ALLOWED_PITCH_SPREAD = np.radians(4) +TICI_FAMILY_PITCH_SPREAD_RESET = np.radians(3) +RPY_INIT = np.array([0.0,0.0,0.0]) +WIDE_FROM_DEVICE_EULER_INIT = np.array([0.0, 0.0, 0.0]) +HEIGHT_INIT = np.array([1.22]) +HEIGHT_SANE_MIN, HEIGHT_SANE_MAX = 0.9, 2.0 +DEVICE_IS_TICI_FAMILY = HARDWARE.get_device_type() in ("tici", "tizi") + +# These values are needed to accommodate the model frame in the narrow cam +if HARDWARE.get_device_type() == 'mici': + PITCH_LIMITS = np.array([-0.143101, 0.22235988]) +else: + PITCH_LIMITS = np.array([-0.09074112085129739, 0.17]) +YAW_LIMITS = np.array([-0.06912048084718224, 0.06912048084718235]) +DEBUG = os.getenv("DEBUG") is not None + +def is_calibration_valid(rpy: np.ndarray) -> bool: + return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1]) + + +def sanity_clip(rpy: np.ndarray) -> np.ndarray: + if np.isnan(rpy).any(): + rpy = RPY_INIT + return np.array([rpy[0], + np.clip(rpy[1], PITCH_LIMITS[0] - .005, PITCH_LIMITS[1] + .005), + np.clip(rpy[2], YAW_LIMITS[0] - .005, YAW_LIMITS[1] + .005)]) + +def moving_avg_with_linear_decay(prev_mean: np.ndarray, new_val: np.ndarray, idx: int, block_size: float) -> np.ndarray: + return (idx*prev_mean + (block_size - idx) * new_val) / block_size + +class Calibrator: + def __init__(self, param_put: bool = False): + self.param_put = param_put + + self.not_car = False + self.stable_rpy = RPY_INIT.copy() + self.stable_wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT.copy() + self.stable_height = HEIGHT_INIT.copy() + self.has_stable_snapshot = False + + # Read saved calibration + self.params = Params() + calibration_params = self.params.get("CalibrationParams") + rpy_init = RPY_INIT + wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT + height = HEIGHT_INIT + valid_blocks = 0 + self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated + + if param_put and calibration_params: + try: + with log.Event.from_bytes(calibration_params) as msg: + rpy_init = np.array(msg.extrinsicsCalibration.rpyCalib) + valid_blocks = msg.extrinsicsCalibration.validBlocks + wide_from_device_euler = np.array(msg.extrinsicsCalibration.wideFromDeviceEuler) + height = np.array(msg.extrinsicsCalibration.height) + except Exception: + cloudlog.exception("Error reading cached CalibrationParams") + + self.reset(rpy_init, valid_blocks, wide_from_device_euler, height) + self.update_status() + + # If saved calibration is immediately invalid (e.g. bad params from a previous + # bootstrap bug or device remount), auto-clear it so we recalibrate from scratch + # instead of getting permanently stuck in the "Calibration Invalid" state. + if self.cal_status == log.ExtrinsicsCalibration.Status.invalid: + cloudlog.warning("calibrationd: saved CalibrationParams are invalid, clearing and starting fresh") + if param_put: + self.params.remove("CalibrationParams") + self.reset() + self.update_status() + + def _remember_stable_solution(self) -> None: + self.stable_rpy = self.rpy.copy() + self.stable_wide_from_device_euler = self.wide_from_device_euler.copy() + self.stable_height = self.height.copy() + self.has_stable_snapshot = True + + def reset(self, rpy_init: np.ndarray = RPY_INIT, + valid_blocks: int = 0, + wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT, + height_init: np.ndarray = HEIGHT_INIT, + smooth_from: np.ndarray | None = None) -> None: + if not np.isfinite(rpy_init).all(): + self.rpy = RPY_INIT.copy() + else: + self.rpy = rpy_init.copy() + + if not np.isfinite(height_init).all() or len(height_init) != 1: + self.height = HEIGHT_INIT.copy() + else: + self.height = height_init.copy() + + if not np.isfinite(wide_from_device_euler_init).all() or len(wide_from_device_euler_init) != 3: + self.wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT.copy() + else: + self.wide_from_device_euler = wide_from_device_euler_init.copy() + + if not np.isfinite(valid_blocks) or valid_blocks < 0: + self.valid_blocks = 0 + else: + self.valid_blocks = valid_blocks + + self.rpys = np.tile(self.rpy, (INPUTS_WANTED, 1)) + self.wide_from_device_eulers = np.tile(self.wide_from_device_euler, (INPUTS_WANTED, 1)) + self.heights = np.tile(self.height, (INPUTS_WANTED, 1)) + + self.idx = 0 + self.block_idx = 0 + self.v_ego = 0.0 + + if smooth_from is None: + self.old_rpy = RPY_INIT + self.old_rpy_weight = 0.0 + else: + self.old_rpy = smooth_from + self.old_rpy_weight = 1.0 + + def get_valid_idxs(self) -> list[int]: + # exclude current block_idx from validity window + before_current = list(range(self.block_idx)) + after_current = list(range(min(self.valid_blocks, self.block_idx + 1), self.valid_blocks)) + return before_current + after_current + + def update_status(self) -> None: + valid_idxs = self.get_valid_idxs() + if valid_idxs: + self.wide_from_device_euler = np.mean(self.wide_from_device_eulers[valid_idxs], axis=0) + self.height = np.mean(self.heights[valid_idxs], axis=0) + rpys = self.rpys[valid_idxs] + self.rpy = np.mean(rpys, axis=0) + max_rpy_calib = np.array(np.max(rpys, axis=0)) + min_rpy_calib = np.array(np.min(rpys, axis=0)) + self.calib_spread = np.abs(max_rpy_calib - min_rpy_calib) + else: + self.calib_spread = np.zeros(3) + + if self.valid_blocks < INPUTS_NEEDED: + if self.cal_status == log.ExtrinsicsCalibration.Status.recalibrating: + self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating + else: + self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated + elif is_calibration_valid(self.rpy): + self.cal_status = log.ExtrinsicsCalibration.Status.calibrated + else: + self.cal_status = log.ExtrinsicsCalibration.Status.invalid + + # If spread is too high, assume mounting was changed and reset to last block. + # Make the transition smooth. Abrupt transitions are not good for feedback loop through supercombo model. + # TODO: add height spread check with smooth transition too + pitch_spread_limit = TICI_FAMILY_PITCH_SPREAD_RESET if DEVICE_IS_TICI_FAMILY else MAX_ALLOWED_PITCH_SPREAD + spread_too_high = self.calib_spread[1] > pitch_spread_limit or self.calib_spread[2] > MAX_ALLOWED_YAW_SPREAD + if self.cal_status == log.ExtrinsicsCalibration.Status.calibrated and not spread_too_high: + self._remember_stable_solution() + + if spread_too_high and self.cal_status == log.ExtrinsicsCalibration.Status.calibrated: + use_stable_snapshot = DEVICE_IS_TICI_FAMILY and self.has_stable_snapshot + if use_stable_snapshot: + reset_rpy = self.stable_rpy + reset_wide = self.stable_wide_from_device_euler + reset_height = self.stable_height + else: + reset_rpy = self.rpys[self.block_idx - 1] + reset_wide = self.wide_from_device_eulers[self.block_idx - 1] + reset_height = self.heights[self.block_idx - 1] + + log_issue_limited( + "calibrationd_reset_spread", + "calibration", + f"calibrationd reset unstable solution pitchSpread={self.calib_spread[1]:.6f} " + f"yawSpread={self.calib_spread[2]:.6f} pitchLimit={pitch_spread_limit:.6f} " + f"use_stable_snapshot={use_stable_snapshot} rpy={self.rpy.tolist()}", + interval_sec=0.5, + ) + self.reset(reset_rpy, valid_blocks=1, wide_from_device_euler_init=reset_wide, + height_init=reset_height, smooth_from=self.stable_rpy if use_stable_snapshot else self.rpy) + self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating + + write_this_cycle = (self.idx == 0) and (self.block_idx % (INPUTS_WANTED//5) == 5) + if self.param_put and write_this_cycle: + self.params.put_nonblocking("CalibrationParams", self.get_msg(True).to_bytes()) + + def handle_v_ego(self, v_ego: float) -> None: + self.v_ego = v_ego + + def get_smooth_rpy(self) -> np.ndarray: + if self.old_rpy_weight > 0: + return self.old_rpy_weight * self.old_rpy + (1.0 - self.old_rpy_weight) * self.rpy + else: + return self.rpy + + def handle_cam_odom(self, trans: list[float], + rot: list[float], + wide_from_device_euler: list[float], + trans_std: list[float], + road_transform_trans: list[float], + road_transform_trans_std: list[float]) -> np.ndarray | None: + self.old_rpy_weight = max(0.0, self.old_rpy_weight - 1/SMOOTH_CYCLES) + + fast_enough = self.v_ego > MIN_SPEED_FILTER + motion_speed = max(float(self.v_ego), float(trans[0])) + cam_fast_enough = motion_speed > MIN_SPEED_FILTER + yaw_ok = abs(rot[2]) < MAX_YAW_RATE_FILTER + straight_and_fast = fast_enough and cam_fast_enough and yaw_ok + angle_std_threshold = MAX_VEL_ANGLE_STD + height_std_threshold = MAX_HEIGHT_STD + rpy_certain = np.arctan2(trans_std[1], motion_speed) < angle_std_threshold + if len(road_transform_trans_std) == 3: + height_certain = road_transform_trans_std[2] < height_std_threshold + else: + height_certain = True + + certain_if_calib = rpy_certain + if not (straight_and_fast and certain_if_calib): + log_issue_limited( + "calibrationd_rejected_sample", + "calibration", + f"calibrationd rejected sample vEgo={self.v_ego:.2f} trans0={trans[0]:.2f} yawRate={rot[2]:.4f} " + f"fast_enough={fast_enough} cam_fast_enough={cam_fast_enough} motion_speed={motion_speed:.2f} yaw_ok={yaw_ok} " + f"rpy_certain={rpy_certain} height_certain={height_certain} valid_blocks={self.valid_blocks} idx={self.idx}", + interval_sec=1.0, + ) + return None + + observed_rpy = np.array([0, + -np.arctan2(trans[2], trans[0]), + np.arctan2(trans[1], trans[0])]) + new_rpy = euler_from_rot(rot_from_euler(self.get_smooth_rpy()).dot(rot_from_euler(observed_rpy))) + new_rpy = sanity_clip(new_rpy) + + if len(wide_from_device_euler) == 3: + new_wide_from_device_euler = np.array(wide_from_device_euler) + else: + new_wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT + + if len(road_transform_trans) == 3 and HEIGHT_SANE_MIN <= road_transform_trans[2] <= HEIGHT_SANE_MAX: + new_height = np.array([road_transform_trans[2]]) + else: + new_height = HEIGHT_INIT + + self.rpys[self.block_idx] = moving_avg_with_linear_decay(self.rpys[self.block_idx], new_rpy, self.idx, float(BLOCK_SIZE)) + self.wide_from_device_eulers[self.block_idx] = moving_avg_with_linear_decay(self.wide_from_device_eulers[self.block_idx], + new_wide_from_device_euler, self.idx, float(BLOCK_SIZE)) + self.heights[self.block_idx] = moving_avg_with_linear_decay(self.heights[self.block_idx], new_height, self.idx, float(BLOCK_SIZE)) + + self.idx = (self.idx + 1) % BLOCK_SIZE + if self.idx == 0: + self.block_idx += 1 + self.valid_blocks = max(self.block_idx, self.valid_blocks) + self.block_idx = self.block_idx % INPUTS_WANTED + + self.update_status() + + if self.idx == 0: + log_issue_limited( + "calibrationd_progress_block", + "calibration", + f"calibrationd progress status={int(self.cal_status)} valid_blocks={self.valid_blocks} " + f"calPerc={min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100)} " + f"rpy={self.rpy.tolist()} spread={self.calib_spread.tolist()}", + interval_sec=0.5, + ) + + return new_rpy + + def get_msg(self, valid: bool) -> capnp.lib.capnp._DynamicStructBuilder: + smooth_rpy = self.get_smooth_rpy() + + msg = messaging.new_message('extrinsicsCalibration') + msg.valid = valid + + extrinsicsCalibration = msg.extrinsicsCalibration + extrinsicsCalibration.validBlocks = self.valid_blocks + extrinsicsCalibration.calStatus = self.cal_status + extrinsicsCalibration.calPerc = min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100) + extrinsicsCalibration.rpyCalib = smooth_rpy.tolist() + extrinsicsCalibration.rpyCalibSpread = self.calib_spread.tolist() + extrinsicsCalibration.wideFromDeviceEuler = self.wide_from_device_euler.tolist() + extrinsicsCalibration.height = self.height.tolist() + + return msg + + def send_data(self, pm: messaging.PubMaster, valid: bool) -> None: + pm.send('extrinsicsCalibration', self.get_msg(valid)) + + +def main() -> NoReturn: + config_realtime_process([0, 1, 2, 3], 5) + + pm = messaging.PubMaster(['extrinsicsCalibration']) + sm = messaging.SubMaster(['cameraOdometry', 'carState'], poll='cameraOdometry') + + params_reader = Params() + CP = messaging.log_from_bytes(params_reader.get("CarParams", block=True), car.CarParams) + + calibrator = Calibrator(param_put=True) + calibrator.not_car = CP.notCar + + while 1: + timeout = 0 if sm.frame == -1 else 100 + sm.update(timeout) + + if sm.updated['cameraOdometry']: + calibrator.handle_v_ego(sm['carState'].vEgo) + new_rpy = calibrator.handle_cam_odom(sm['cameraOdometry'].trans, + sm['cameraOdometry'].rot, + sm['cameraOdometry'].wideFromDeviceEuler, + sm['cameraOdometry'].transStd, + sm['cameraOdometry'].roadTransformTrans, + sm['cameraOdometry'].roadTransformTransStd) + + if DEBUG and new_rpy is not None: + print('got new rpy', new_rpy) + + # 4Hz driven by cameraOdometry + if sm.frame % 5 == 0: + checks_ok = sm.all_checks() + if not checks_ok: + ft = sm.freq_tracker + recv_hz = {s: (round(1.0 / ft[s].avg_dt.get_average(), 2) if ft[s].avg_dt.count else None) for s in sm.services} + log_issue_limited( + "calibrationd_checks_failed", + "calibration", + f"calibrationd all_checks failed alive={sm.alive} freq_ok={sm.freq_ok} valid={sm.valid} " + f"seen={sm.seen} recv_hz={recv_hz}", + interval_sec=5.0, + ) + calibrator.send_data(pm, checks_ok) + + +if __name__ == "__main__": + main() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/helpers.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/helpers.py new file mode 100644 index 000000000..56a1c9039 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/helpers.py @@ -0,0 +1,188 @@ +import numpy as np +from typing import Any +from functools import cache + +from iqpilot.cereal import log +from iqpilot.common.transformations.orientation import rot_from_euler, euler_from_rot +from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy + + +@cache +def fft_next_good_size(n: int) -> int: + """ + smallest composite of 2, 3, 5, 7, 11 that is >= n + inspired by pocketfft + """ + if n <= 6: + return n + best, f2 = 2 * n, 1 + while f2 < best: + f23 = f2 + while f23 < best: + f235 = f23 + while f235 < best: + f2357 = f235 + while f2357 < best: + f235711 = f2357 + while f235711 < best: + best = f235711 if f235711 >= n else best + f235711 *= 11 + f2357 *= 7 + f235 *= 5 + f23 *= 3 + f2 *= 2 + return best + + +def parabolic_peak_interp(R, max_index): + if max_index == 0 or max_index == len(R) - 1: + return max_index + + y_m1, y_0, y_p1 = R[max_index - 1], R[max_index], R[max_index + 1] + offset = 0.5 * (y_p1 - y_m1) / (2 * y_0 - y_p1 - y_m1) + + return max_index + offset + + +def rotate_cov(rot_matrix, cov_in): + return rot_matrix @ cov_in @ rot_matrix.T + + +def rotate_std(rot_matrix, std_in): + return np.sqrt(np.diag(rotate_cov(rot_matrix, np.diag(std_in**2)))) + + +class NPQueue: + def __init__(self, maxlen: int, rowsize: int) -> None: + self.maxlen = maxlen + self.arr = np.empty((0, rowsize)) + + def __len__(self) -> int: + return len(self.arr) + + def append(self, pt: list[float]) -> None: + if len(self.arr) < self.maxlen: + self.arr = np.append(self.arr, [pt], axis=0) + else: + self.arr[:-1] = self.arr[1:] + self.arr[-1] = pt + + +class PointBuckets: + def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + self.x_bounds = x_bounds + self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds} + self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True)) + self.min_points_total = min_points_total + + def __len__(self) -> int: + return sum([len(v) for v in self.buckets.values()]) + + def is_valid(self) -> bool: + individual_buckets_valid = all(len(v) >= min_pts for v, min_pts in zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)) + total_points_valid = self.__len__() >= self.min_points_total + return individual_buckets_valid and total_points_valid + + def get_valid_percent(self) -> int: + total_points_perc = min(self.__len__() / self.min_points_total * 100, 100) + individual_buckets_perc = min(min(len(v) / min_pts * 100 for v, min_pts in + zip(self.buckets.values(), self.buckets_min_points.values(), strict=True)), 100) + return int((total_points_perc + individual_buckets_perc) / 2) + + def is_calculable(self) -> bool: + return all(len(v) > 0 for v in self.buckets.values()) + + def add_point(self, x: float, y: float) -> None: + raise NotImplementedError + + def get_points(self, num_points: int | None = None) -> Any: + points = np.vstack([x.arr for x in self.buckets.values()]) + if num_points is None: + return points + return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] + + def load_points(self, points: list[list[float]]) -> None: + for point in points: + self.add_point(*point) + + +class ParameterEstimator: + """ Base class for parameter estimators """ + def reset(self) -> None: + raise NotImplementedError + + def handle_log(self, t: int, which: str, msg: log.Event) -> None: + raise NotImplementedError + + def get_msg(self, valid: bool, with_points: bool) -> log.Event: + raise NotImplementedError + + +class Measurement: + x, y, z = (property(lambda self: self.xyz[0]), property(lambda self: self.xyz[1]), property(lambda self: self.xyz[2])) + x_std, y_std, z_std = (property(lambda self: self.xyz_std[0]), property(lambda self: self.xyz_std[1]), property(lambda self: self.xyz_std[2])) + roll, pitch, yaw = x, y, z + roll_std, pitch_std, yaw_std = x_std, y_std, z_std + + def __init__(self, xyz: np.ndarray, xyz_std: np.ndarray): + self.xyz: np.ndarray = xyz + self.xyz_std: np.ndarray = xyz_std + + @classmethod + def from_measurement_xyz(cls, measurement: log.DeviceMotion.XYZMeasurement) -> 'Measurement': + return cls( + xyz=np.array([measurement.x, measurement.y, measurement.z]), + xyz_std=np.array([measurement.xStd, measurement.yStd, measurement.zStd]) + ) + + +class Pose: + def __init__(self, orientation: Measurement, velocity: Measurement, acceleration: Measurement, angular_velocity: Measurement): + self.orientation = orientation + self.velocity = velocity + self.acceleration = acceleration + self.angular_velocity = angular_velocity + + @classmethod + def from_live_pose(cls, live_pose: log.DeviceMotion) -> 'Pose': + return Pose( + orientation=Measurement.from_measurement_xyz(live_pose.orientationNED), + velocity=Measurement.from_measurement_xyz(live_pose.velocityDevice), + acceleration=Measurement.from_measurement_xyz(live_pose.accelerationDevice), + angular_velocity=Measurement.from_measurement_xyz(live_pose.angularVelocityDevice) + ) + + +class PoseCalibrator: + def __init__(self): + self.calib_valid = False + self.calib_from_device = np.eye(3) + + def _transform_calib_from_device(self, meas: Measurement): + new_xyz = self.calib_from_device @ meas.xyz + new_xyz_std = rotate_std(self.calib_from_device, meas.xyz_std) + return Measurement(new_xyz, new_xyz_std) + + def _ned_from_calib(self, orientation: Measurement): + ned_from_device = rot_from_euler(orientation.xyz) + ned_from_calib = ned_from_device @ self.calib_from_device.T + ned_from_calib_euler_meas = Measurement(euler_from_rot(ned_from_calib), np.full(3, np.nan)) + return ned_from_calib_euler_meas + + def build_calibrated_pose(self, pose: Pose) -> Pose: + ned_from_calib_euler = self._ned_from_calib(pose.orientation) + angular_velocity_calib = self._transform_calib_from_device(pose.angular_velocity) + acceleration_calib = self._transform_calib_from_device(pose.acceleration) + velocity_calib = self._transform_calib_from_device(pose.velocity) + + return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib) + + def feed_live_calib(self, live_calib: log.ExtrinsicsCalibration): + calib_rpy = get_calibrated_rpy(live_calib) + if calib_rpy is not None: + self.calib_from_device = rot_from_euler(calib_rpy).T + self.calib_valid = True + else: + if not self.calib_valid: + self.calib_from_device = np.eye(3) + self.calib_valid = False diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/locationd.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/locationd.py new file mode 100644 index 000000000..bd7864b82 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/locationd.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +import os +import time +import capnp +import numpy as np +from enum import Enum +from collections import defaultdict + +from iqpilot.cereal import log, messaging +from iqpilot.cereal.services import SERVICE_LIST +from iqpilot.common.transformations.orientation import rot_from_euler +from iqpilot.common.realtime import config_realtime_process +from iqpilot.common.params import Params +from iqpilot.common.swaglog import cloudlog +from iqpilot.selfdrive.locationd.calibration_helpers import get_calibrated_rpy +from iqpilot.selfdrive.locationd.helpers import rotate_std +from iqpilot.selfdrive.locationd.models.pose_kf import PoseKalman, States +from iqpilot.selfdrive.locationd.models.constants import ObservationKind + +ACCEL_SANITY_CHECK = 100.0 # m/s^2 +ROTATION_SANITY_CHECK = 10.0 # rad/s +TRANS_SANITY_CHECK = 200.0 # m/s +CALIB_RPY_SANITY_CHECK = 0.5 # rad (+- 30 deg) +MIN_STD_SANITY_CHECK = 1e-5 # m or rad +MAX_FILTER_REWIND_TIME = 0.8 # s +MAX_SENSOR_TIME_DIFF = 0.1 # s +YAWRATE_CROSS_ERR_CHECK_FACTOR = 30 +INPUT_INVALID_LIMIT = 2.0 # 1 (camodo) / 9 (sensor) bad input[s] ignored +INPUT_INVALID_RECOVERY = 10.0 # ~10 secs to resume after exceeding allowed bad inputs by one +POSENET_STD_INITIAL_VALUE = 10.0 +POSENET_STD_HIST_HALF = 20 + + +def calculate_invalid_input_decay(invalid_limit, recovery_time, frequency): + return (1 - 1 / (2 * invalid_limit)) ** (1 / (recovery_time * frequency)) + + +def init_xyz_measurement(measurement: capnp._DynamicStructBuilder, values: np.ndarray, stds: np.ndarray, valid: bool): + assert len(values) == len(stds) == 3 + measurement.x, measurement.y, measurement.z = map(float, values) + measurement.xStd, measurement.yStd, measurement.zStd = map(float, stds) + measurement.valid = valid + + +class HandleLogResult(Enum): + SUCCESS = 0 + TIMING_INVALID = 1 + INPUT_INVALID = 2 + SENSOR_SOURCE_INVALID = 3 + + +class LocationEstimator: + def __init__(self, debug: bool): + self.kf = PoseKalman(MAX_FILTER_REWIND_TIME) + + self.debug = debug + + self.posenet_stds = np.array([POSENET_STD_INITIAL_VALUE] * (POSENET_STD_HIST_HALF * 2)) + self.car_speed = 0.0 + self.camodo_yawrate_distribution = np.array([0.0, 10.0]) # mean, std + self.device_from_calib = np.eye(3) + + obs_kinds = [ObservationKind.PHONE_ACCEL, ObservationKind.PHONE_GYRO, ObservationKind.CAMERA_ODO_ROTATION, ObservationKind.CAMERA_ODO_TRANSLATION] + self.observations = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} + self.observation_errors = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} + + def reset(self, t: float, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P): + self.kf.init_state(x_initial, covs=P_initial, filter_time=t) + + def _validate_sensor_source(self, source: log.SensorEventData.SensorSource): + # some segments have two IMUs, ignore the second one + return source != log.SensorEventData.SensorSource.bmx055 + + def _validate_sensor_time(self, sensor_time: float, t: float): + # ignore empty readings + if sensor_time == 0: + return False + + # sensor time and log time should be close + sensor_time_invalid = abs(sensor_time - t) > MAX_SENSOR_TIME_DIFF + if sensor_time_invalid: + cloudlog.warning("Sensor reading ignored, sensor timestamp more than 100ms off from log time") + return not sensor_time_invalid + + def _validate_timestamp(self, t: float): + kf_t = self.kf.t + invalid = not np.isnan(kf_t) and (kf_t - t) > MAX_FILTER_REWIND_TIME + if invalid: + cloudlog.warning("Observation timestamp is older than the max rewind threshold of the filter") + return not invalid + + def _finite_check(self, t: float, new_x: np.ndarray, new_P: np.ndarray): + all_finite = np.isfinite(new_x).all() and np.isfinite(new_P).all() + if not all_finite: + cloudlog.error("Non-finite values detected, kalman reset") + self.reset(t) + + def handle_log(self, t: float, which: str, msg: capnp._DynamicStructReader) -> HandleLogResult: + new_x, new_P = None, None + if which == "accelerometer" and msg.which() == "acceleration": + sensor_time = msg.timestamp * 1e-9 + + if not self._validate_sensor_time(sensor_time, t) or not self._validate_timestamp(sensor_time): + return HandleLogResult.TIMING_INVALID + + if not self._validate_sensor_source(msg.source): + return HandleLogResult.SENSOR_SOURCE_INVALID + + v = msg.acceleration.v + meas = np.array([-v[2], -v[1], -v[0]]) + if np.linalg.norm(meas) >= ACCEL_SANITY_CHECK: + return HandleLogResult.INPUT_INVALID + + acc_res = self.kf.predict_and_observe(sensor_time, ObservationKind.PHONE_ACCEL, meas) + if acc_res is not None: + _, new_x, _, new_P, _, _, (acc_err,), _, _ = acc_res + self.observation_errors[ObservationKind.PHONE_ACCEL] = np.array(acc_err) + self.observations[ObservationKind.PHONE_ACCEL] = meas + + elif which == "gyroscope" and msg.which() == "gyroUncalibrated": + sensor_time = msg.timestamp * 1e-9 + + if not self._validate_sensor_time(sensor_time, t) or not self._validate_timestamp(sensor_time): + return HandleLogResult.TIMING_INVALID + + if not self._validate_sensor_source(msg.source): + return HandleLogResult.SENSOR_SOURCE_INVALID + + v = msg.gyroUncalibrated.v + meas = np.array([-v[2], -v[1], -v[0]]) + + gyro_bias = self.kf.x[States.GYRO_BIAS] + gyro_camodo_yawrate_err = np.abs((meas[2] - gyro_bias[2]) - self.camodo_yawrate_distribution[0]) + gyro_camodo_yawrate_err_threshold = YAWRATE_CROSS_ERR_CHECK_FACTOR * self.camodo_yawrate_distribution[1] + gyro_valid = gyro_camodo_yawrate_err < gyro_camodo_yawrate_err_threshold + + if np.linalg.norm(meas) >= ROTATION_SANITY_CHECK or not gyro_valid: + return HandleLogResult.INPUT_INVALID + + gyro_res = self.kf.predict_and_observe(sensor_time, ObservationKind.PHONE_GYRO, meas) + if gyro_res is not None: + _, new_x, _, new_P, _, _, (gyro_err,), _, _ = gyro_res + self.observation_errors[ObservationKind.PHONE_GYRO] = np.array(gyro_err) + self.observations[ObservationKind.PHONE_GYRO] = meas + + elif which == "carState": + self.car_speed = abs(msg.vEgo) + + elif which == "extrinsicsCalibration": + # Note that we use this message during calibration + calib = get_calibrated_rpy(msg) + if calib is None and len(msg.rpyCalib) > 0: + calib = np.array(msg.rpyCalib) + + if calib is not None: + if calib.min() < -CALIB_RPY_SANITY_CHECK or calib.max() > CALIB_RPY_SANITY_CHECK: + return HandleLogResult.INPUT_INVALID + + self.device_from_calib = rot_from_euler(calib) + + elif which == "cameraOdometry": + if not self._validate_timestamp(t): + return HandleLogResult.TIMING_INVALID + + rot_device = np.matmul(self.device_from_calib, np.array(msg.rot)) + trans_device = np.matmul(self.device_from_calib, np.array(msg.trans)) + + if np.linalg.norm(rot_device) > ROTATION_SANITY_CHECK or np.linalg.norm(trans_device) > TRANS_SANITY_CHECK: + return HandleLogResult.INPUT_INVALID + + rot_calib_std = np.array(msg.rotStd) + trans_calib_std = np.array(msg.transStd) + + if rot_calib_std.min() <= MIN_STD_SANITY_CHECK or trans_calib_std.min() <= MIN_STD_SANITY_CHECK: + return HandleLogResult.INPUT_INVALID + + if np.linalg.norm(rot_calib_std) > 10 * ROTATION_SANITY_CHECK or np.linalg.norm(trans_calib_std) > 10 * TRANS_SANITY_CHECK: + return HandleLogResult.INPUT_INVALID + + self.posenet_stds = np.roll(self.posenet_stds, -1) + self.posenet_stds[-1] = trans_calib_std[0] + + # Multiply by N to avoid to high certainty in kalman filter because of temporally correlated noise + rot_calib_std *= 10 + trans_calib_std *= 2 + + rot_device_std = rotate_std(self.device_from_calib, rot_calib_std) + trans_device_std = rotate_std(self.device_from_calib, trans_calib_std) + rot_device_noise = rot_device_std ** 2 + trans_device_noise = trans_device_std ** 2 + + cam_odo_rot_res = self.kf.predict_and_observe(t, ObservationKind.CAMERA_ODO_ROTATION, rot_device, np.array([np.diag(rot_device_noise)])) + cam_odo_trans_res = self.kf.predict_and_observe(t, ObservationKind.CAMERA_ODO_TRANSLATION, trans_device, np.array([np.diag(trans_device_noise)])) + self.camodo_yawrate_distribution = np.array([rot_device[2], rot_device_std[2]]) + if cam_odo_rot_res is not None: + _, new_x, _, new_P, _, _, (cam_odo_rot_err,), _, _ = cam_odo_rot_res + self.observation_errors[ObservationKind.CAMERA_ODO_ROTATION] = np.array(cam_odo_rot_err) + self.observations[ObservationKind.CAMERA_ODO_ROTATION] = rot_device + if cam_odo_trans_res is not None: + _, new_x, _, new_P, _, _, (cam_odo_trans_err,), _, _ = cam_odo_trans_res + self.observation_errors[ObservationKind.CAMERA_ODO_TRANSLATION] = np.array(cam_odo_trans_err) + self.observations[ObservationKind.CAMERA_ODO_TRANSLATION] = trans_device + + if new_x is not None and new_P is not None: + self._finite_check(t, new_x, new_P) + return HandleLogResult.SUCCESS + + def get_msg(self, sensors_valid: bool, inputs_valid: bool, filter_valid: bool): + state, cov = self.kf.x, self.kf.P + std = np.sqrt(np.diag(cov)) + + orientation_ned, orientation_ned_std = state[States.NED_ORIENTATION], std[States.NED_ORIENTATION] + velocity_device, velocity_device_std = state[States.DEVICE_VELOCITY], std[States.DEVICE_VELOCITY] + angular_velocity_device, angular_velocity_device_std = state[States.ANGULAR_VELOCITY], std[States.ANGULAR_VELOCITY] + acceleration_device, acceleration_device_std = state[States.ACCELERATION], std[States.ACCELERATION] + + msg = messaging.new_message("deviceMotion") + msg.valid = filter_valid + + deviceMotion = msg.deviceMotion + init_xyz_measurement(deviceMotion.orientationNED, orientation_ned, orientation_ned_std, filter_valid) + init_xyz_measurement(deviceMotion.velocityDevice, velocity_device, velocity_device_std, filter_valid) + init_xyz_measurement(deviceMotion.angularVelocityDevice, angular_velocity_device, angular_velocity_device_std, filter_valid) + init_xyz_measurement(deviceMotion.accelerationDevice, acceleration_device, acceleration_device_std, filter_valid) + if self.debug: + deviceMotion.debugFilterState.value = state.tolist() + deviceMotion.debugFilterState.std = std.tolist() + deviceMotion.debugFilterState.valid = filter_valid + deviceMotion.debugFilterState.observations = [ + {'kind': k, 'value': self.observations[k].tolist(), 'error': self.observation_errors[k].tolist()} + for k in self.observations.keys() + ] + + old_mean = np.mean(self.posenet_stds[:POSENET_STD_HIST_HALF]) + new_mean = np.mean(self.posenet_stds[POSENET_STD_HIST_HALF:]) + std_spike = (new_mean / old_mean) > 4.0 and new_mean > 7.0 + + deviceMotion.inputsOK = inputs_valid + deviceMotion.posenetOK = not std_spike or self.car_speed <= 5.0 + deviceMotion.sensorsOK = sensors_valid + + return msg + + +def sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, simulation): + cur_time = time.monotonic() + for which, msgs in [("accelerometer", acc_msgs), ("gyroscope", gyro_msgs)]: + if len(msgs) > 0: + sensor_valid[which] = msgs[-1].valid + sensor_recv_time[which] = cur_time + + if not simulation: + sensor_alive[which] = (cur_time - sensor_recv_time[which]) < 0.1 + else: + sensor_alive[which] = len(msgs) > 0 + + return all(sensor_alive.values()) and all(sensor_valid.values()) + + +def main(): + config_realtime_process([0, 1, 2, 3], 5) + + DEBUG = bool(int(os.getenv("DEBUG", "0"))) + SIMULATION = bool(int(os.getenv("SIMULATION", "0"))) + + pm = messaging.PubMaster(['deviceMotion']) + sm = messaging.SubMaster(['carState', 'extrinsicsCalibration', 'cameraOdometry'], poll='cameraOdometry') + # separate sensor sockets for efficiency + sensor_sockets = [messaging.sub_sock(which, timeout=20) for which in ['accelerometer', 'gyroscope']] + sensor_alive, sensor_valid, sensor_recv_time = defaultdict(bool), defaultdict(bool), defaultdict(float) + + params = Params() + + estimator = LocationEstimator(DEBUG) + + filter_initialized = False + critcal_services = ["accelerometer", "gyroscope", "cameraOdometry"] + observation_input_invalid = defaultdict(int) + + input_invalid_limit = {s: round(INPUT_INVALID_LIMIT * (SERVICE_LIST[s].frequency / 20.)) for s in critcal_services} + input_invalid_threshold = {s: input_invalid_limit[s] - 0.5 for s in critcal_services} + input_invalid_decay = {s: calculate_invalid_input_decay(input_invalid_limit[s], INPUT_INVALID_RECOVERY, SERVICE_LIST[s].frequency) for s in critcal_services} + + initial_pose_data = params.get("LocationFilterInitialState") + if initial_pose_data is not None: + with log.Event.from_bytes(initial_pose_data) as lp_msg: + filter_state = lp_msg.deviceMotion.debugFilterState + x_initial = np.array(filter_state.value, dtype=np.float64) if len(filter_state.value) != 0 else PoseKalman.initial_x + P_initial = np.diag(np.array(filter_state.std, dtype=np.float64)) if len(filter_state.std) != 0 else PoseKalman.initial_P + estimator.reset(None, x_initial, P_initial) + + while True: + sm.update() + + acc_msgs, gyro_msgs = (messaging.drain_sock(sock) for sock in sensor_sockets) + + if filter_initialized: + msgs = [] + for msg in acc_msgs + gyro_msgs: + t, valid, which, data = msg.logMonoTime, msg.valid, msg.which(), getattr(msg, msg.which()) + msgs.append((t, valid, which, data)) + for which, updated in sm.updated.items(): + if not updated: + continue + t, valid, data = sm.logMonoTime[which], sm.valid[which], sm[which] + msgs.append((t, valid, which, data)) + + for log_mono_time, valid, which, msg in sorted(msgs, key=lambda x: x[0]): + if valid: + t = log_mono_time * 1e-9 + res = estimator.handle_log(t, which, msg) + if which not in critcal_services: + continue + + if res == HandleLogResult.TIMING_INVALID: + cloudlog.warning(f"Observation {which} ignored due to failed timing check") + observation_input_invalid[which] += 1 + elif res == HandleLogResult.INPUT_INVALID: + cloudlog.warning(f"Observation {which} ignored due to failed sanity check") + observation_input_invalid[which] += 1 + elif res == HandleLogResult.SUCCESS: + observation_input_invalid[which] *= input_invalid_decay[which] + else: + filter_initialized = sm.all_checks() and sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION) + + if sm.updated["cameraOdometry"]: + critical_service_inputs_valid = all(observation_input_invalid[s] < input_invalid_threshold[s] for s in critcal_services) + inputs_valid = sm.all_valid() and critical_service_inputs_valid + sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION) + + msg = estimator.get_msg(sensors_valid, inputs_valid, filter_initialized) + pm.send("deviceMotion", msg) + + +if __name__ == "__main__": + main() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/constants.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/constants.py new file mode 100644 index 000000000..8e95ff82e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/constants.py @@ -0,0 +1,88 @@ +class ObservationKind: + UNKNOWN = 0 + NO_OBSERVATION = 1 + GPS_NED = 2 + ODOMETRIC_SPEED = 3 + PHONE_GYRO = 4 + GPS_VEL = 5 + PSEUDORANGE_GPS = 6 + PSEUDORANGE_RATE_GPS = 7 + SPEED = 8 + NO_ROT = 9 + PHONE_ACCEL = 10 + ORB_POINT = 11 + ECEF_POS = 12 + CAMERA_ODO_TRANSLATION = 13 + CAMERA_ODO_ROTATION = 14 + ORB_FEATURES = 15 + MSCKF_TEST = 16 + FEATURE_TRACK_TEST = 17 + LANE_PT = 18 + IMU_FRAME = 19 + PSEUDORANGE_GLONASS = 20 + PSEUDORANGE_RATE_GLONASS = 21 + PSEUDORANGE = 22 + PSEUDORANGE_RATE = 23 + ECEF_VEL = 35 + ECEF_ORIENTATION_FROM_GPS = 32 + NO_ACCEL = 33 + ORB_FEATURES_WIDE = 34 + + ROAD_FRAME_XY_SPEED = 24 # (x, y) [m/s] + ROAD_FRAME_YAW_RATE = 25 # [rad/s] + STEER_ANGLE = 26 # [rad] + ANGLE_OFFSET_FAST = 27 # [rad] + STIFFNESS = 28 # [-] + STEER_RATIO = 29 # [-] + ROAD_FRAME_X_SPEED = 30 # (x) [m/s] + ROAD_ROLL = 31 # [rad] + + names = [ + 'Unknown', + 'No observation', + 'GPS NED', + 'Odometric speed', + 'Phone gyro', + 'GPS velocity', + 'GPS pseudorange', + 'GPS pseudorange rate', + 'Speed', + 'No rotation', + 'Phone acceleration', + 'ORB point', + 'ECEF pos', + 'camera odometric translation', + 'camera odometric rotation', + 'ORB features', + 'MSCKF test', + 'Feature track test', + 'Lane ecef point', + 'imu frame eulers', + 'GLONASS pseudorange', + 'GLONASS pseudorange rate', + 'pseudorange', + 'pseudorange rate', + + 'Road Frame x,y speed', + 'Road Frame yaw rate', + 'Steer Angle', + 'Fast Angle Offset', + 'Stiffness', + 'Steer Ratio', + 'Road Frame x speed', + 'Road Roll', + 'ECEF orientation from GPS', + 'NO accel', + 'ORB features wide camera', + 'ECEF_VEL', + ] + + @classmethod + def to_string(cls, kind): + return cls.names[kind] + + +SAT_OBS = [ObservationKind.PSEUDORANGE_GPS, + ObservationKind.PSEUDORANGE_RATE_GPS, + ObservationKind.PSEUDORANGE_GLONASS, + ObservationKind.PSEUDORANGE_RATE_GLONASS] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/pose_kf.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/pose_kf.py new file mode 100644 index 000000000..6dbf0f091 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/locationd/models/pose_kf.py @@ -0,0 +1,67 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" + +import numpy as np + +from iqpilot.common.transformations.orientation import euler_from_rot, rot_from_euler +from iqpilot.selfdrive.locationd.models.constants import ObservationKind +from iqpilot.selfdrive.state_estimation import EstimatorModel, ModelDefinition, StateEstimator +try: + from iqpilot.selfdrive.state_estimation.native_binding_pyx import pose_predict, pose_update +except ModuleNotFoundError: + pose_predict = None + pose_update = None + + +EARTH_G = 9.81 + + +class States: + NED_ORIENTATION = slice(0, 3) + DEVICE_VELOCITY = slice(3, 6) + ANGULAR_VELOCITY = slice(6, 9) + GYRO_BIAS = slice(9, 12) + ACCELERATION = slice(12, 15) + ACCEL_BIAS = slice(15, 18) + + +def _transition(state: np.ndarray, dt: float, _: dict[str, float]) -> np.ndarray: + result = state.copy() + result[States.DEVICE_VELOCITY] += dt * state[States.ACCELERATION] + rotation = rot_from_euler(state[States.NED_ORIENTATION]) @ rot_from_euler(dt * state[States.ANGULAR_VELOCITY]) + result[States.NED_ORIENTATION] = euler_from_rot(rotation) + return result + + +def _phone_acceleration(state: np.ndarray, _: dict[str, float]) -> np.ndarray: + device_from_ned = rot_from_euler(state[States.NED_ORIENTATION]).T + centripetal = np.cross(state[States.ANGULAR_VELOCITY], state[States.DEVICE_VELOCITY]) + return device_from_ned @ np.array([0.0, 0.0, -EARTH_G]) + state[States.ACCELERATION] + centripetal + state[States.ACCEL_BIAS] + + +class PoseKalman(EstimatorModel): + name = "pose" + initial_x = np.zeros(18) + initial_P = np.diag([0.01**2] * 3 + [10**2] * 3 + [1**2] * 6 + [100**2] * 3 + [0.01**2] * 3) + Q = np.diag([0.001**2] * 3 + [0.01**2] * 3 + [0.1**2] * 3 + [(0.005 / 100)**2] * 3 + [3**2] * 3 + [0.005**2] * 3) + obs_noise = { + ObservationKind.PHONE_GYRO: np.diag([0.025**2] * 3), + ObservationKind.PHONE_ACCEL: np.diag([0.5**2] * 3), + ObservationKind.CAMERA_ODO_TRANSLATION: np.diag([0.5**2] * 3), + ObservationKind.CAMERA_ODO_ROTATION: np.diag([0.05**2] * 3), + } + + def __init__(self, max_rewind_age: float): + measurements = { + ObservationKind.PHONE_GYRO: lambda state, _: state[States.ANGULAR_VELOCITY] + state[States.GYRO_BIAS], + ObservationKind.PHONE_ACCEL: _phone_acceleration, + ObservationKind.CAMERA_ODO_TRANSLATION: lambda state, _: state[States.DEVICE_VELOCITY], + ObservationKind.CAMERA_ODO_ROTATION: lambda state, _: state[States.ANGULAR_VELOCITY], + } + def native_predict(state, covariance, dt, process_noise, _): + pose_predict(state, covariance, process_noise, dt) + + model = ModelDefinition(18, 18, _transition, measurements, self.Q, self.obs_noise, + native_predict=native_predict if pose_predict is not None else None, native_update=pose_update) + super().__init__(StateEstimator(model, self.initial_x, self.initial_P, max_rewind_age=max_rewind_age)) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py new file mode 100644 index 000000000..0ffe29431 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/alertmanager.py @@ -0,0 +1,68 @@ +import copy +import os +import json +from collections import defaultdict +from dataclasses import dataclass + +from iqpilot.common.basedir import BASEDIR +from iqpilot.common.params import Params +from iqpilot.selfdrive.selfdrived.events import Alert +from iqpilot.common.atlas_alerts import NULL_ALERT as EmptyAlert + + +with open(os.path.join(BASEDIR, "iqpilot/selfdrive/selfdrived/alerts_offroad.json")) as f: + OFFROAD_ALERTS = json.load(f) + + +def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None: + if show_alert: + a = copy.copy(OFFROAD_ALERTS[alert]) + a['extra'] = extra_text or '' + Params().put(alert, a) + else: + Params().remove(alert) + + +@dataclass +class AlertEntry: + alert: Alert | None = None + start_frame: int = -1 + end_frame: int = -1 + added_frame: int = -1 + + def active(self, frame: int) -> bool: + return frame <= self.end_frame + + def just_added(self, frame: int) -> bool: + return self.active(frame) and frame == (self.added_frame + 1) + +class AlertManager: + def __init__(self): + self.alerts: dict[str, AlertEntry] = defaultdict(AlertEntry) + self.current_alert = EmptyAlert + + def add_many(self, frame: int, alerts: list[Alert]) -> None: + for alert in alerts: + entry = self.alerts[alert.alert_type] + entry.alert = alert + if not entry.just_added(frame): + entry.start_frame = frame + min_end_frame = entry.start_frame + alert.duration + entry.end_frame = max(frame + 1, min_end_frame) + entry.added_frame = frame + + def process_alerts(self, frame: int, clear_event_types: set): + ae = AlertEntry() + for v in self.alerts.values(): + if not v.alert: + continue + + if v.alert.event_type in clear_event_types: + v.end_frame = -1 + + # sort by priority first and then by start_frame + greater = ae.alert is None or (v.alert.priority, v.start_frame) > (ae.alert.priority, ae.start_frame) + if v.active(frame) and greater: + ae = v + + self.current_alert = ae.alert if ae.alert is not None else EmptyAlert diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py new file mode 100644 index 000000000..60a37668e --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/selfdrived/events.py @@ -0,0 +1,991 @@ +#!/usr/bin/env python3 +import math + +from iqpilot.cereal import log, car +import iqpilot.cereal.messaging as messaging +from iqpilot.common.constants import CV +from iqpilot.common.realtime import DT_CTRL +from iqpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from iqpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER +from iqpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION +from iqpilot.system.hardware import HARDWARE + +from iqpilot.common.atlas_alerts import EventBook as EventsBase, Tier as Priority, Tags as ET, AlertCard as Alert, \ + NoEntryCard as NoEntryAlert, GentleDisableCard as SoftDisableAlert, PendingDisableCard as UserSoftDisableAlert, \ + HardDisableCard as ImmediateDisableAlert, ChimeCard as EngagementAlert, BannerCard as NormalPermanentAlert, \ + BootCard as StartupAlert, AlertFactory as AlertCallbackType, car_mode_entry_alert as wrong_car_mode_alert + + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus +VisualAlert = car.CarControl.HUDControl.VisualAlert +AudibleAlert = car.CarControl.HUDControl.AudibleAlert +EventName = log.OnroadEvent.EventName + + +# get event name from enum +EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()} + + +class Events(EventsBase): + def __init__(self): + super().__init__() + self.event_counters = dict.fromkeys(EVENTS.keys(), 0) + + def get_events_mapping(self) -> dict[int, dict[str, Alert | AlertCallbackType]]: + return EVENTS + + def get_event_name(self, event: int): + return EVENT_NAME[event] + + def get_event_msg_type(self): + return log.OnroadEvent + + + +# ********** helper functions ********** +def get_display_speed(speed_ms: float, metric: bool) -> str: + speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH))) + unit = 'km/h' if metric else 'mph' + return f"{speed} {unit}" + + +# ********** alert callback functions ********** + + +def soft_disable_alert(alert_text_2: str) -> AlertCallbackType: + def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + if soft_disable_time < int(0.5 / DT_CTRL): + return ImmediateDisableAlert(alert_text_2) + return SoftDisableAlert(alert_text_2) + return func + +def user_soft_disable_alert(alert_text_2: str) -> AlertCallbackType: + def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + if soft_disable_time < int(0.5 / DT_CTRL): + return ImmediateDisableAlert(alert_text_2) + return UserSoftDisableAlert(alert_text_2) + return func + +def below_engage_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + return NoEntryAlert(f"Drive above {get_display_speed(CP.minEnableSpeed, metric)} to engage") + + +def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + return Alert( + f"Steer Assist Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) + + +def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + first_word = 'Recalibrating' if sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.recalibrating else 'Calibrating' + return Alert( + f"{first_word}: {sm['extrinsicsCalibration'].calPerc:.0f}%", + f"Drive Above {get_display_speed(MIN_SPEED_FILTER, metric)}", + AlertStatus.normal, AlertSize.mid, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) + + +def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) + return NormalPermanentAlert( + "Recording Audio Feedback", + f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", + priority=Priority.LOW) + + +# *** debug alerts *** + +def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + full_perc = round(100. - sm['deviceState'].freeSpacePercent) + return NormalPermanentAlert("Out of Storage", f"{full_perc}% full") + + +def posenet_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + if sm.frame * DT_CTRL < 10.: + return NoEntryAlert("IQModel is starting up", alert_text_1="Please Wait") + mdl = sm['modelV2'].velocity.x[0] if len(sm['modelV2'].velocity.x) else math.nan + err = CS.vEgo - mdl + msg = f"Speed Error: {err:.1f} m/s" + return NoEntryAlert(msg, alert_text_1="Posenet Speed Invalid") + + +def process_not_running_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + not_running = [p.name for p in sm['managerState'].processes if not p.running and p.shouldBeRunning] + msg = ', '.join(not_running) + return NoEntryAlert(msg, alert_text_1="Process Not Running") + + +def comm_issue_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + bs = [s for s in sm.data.keys() if not sm.all_checks([s, ])] + msg = ', '.join(bs[:4]) # can't fit too many on one line + return NoEntryAlert(msg, alert_text_1="Communication Issue Between Processes") + + +def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + all_cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') + bad_cams = [s.replace('State', '') for s in all_cams if s in sm.data.keys() and not sm.all_checks([s, ])] + return NormalPermanentAlert("Camera Malfunction", ', '.join(bad_cams)) + + +def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + rpy = sm['extrinsicsCalibration'].rpyCalib + yaw = math.degrees(rpy[2] if len(rpy) == 3 else math.nan) + pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan) + angles = f"Remount Device (Pitch: {pitch:.1f}°, Yaw: {yaw:.1f}°)" + return NormalPermanentAlert("Calibration Invalid", angles) + + +def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + if not sm['vehicleParameters'].angleOffsetValid: + angle_offset_deg = sm['vehicleParameters'].angleOffsetDeg + title = "Steering misalignment detected" + text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)" + elif not sm['vehicleParameters'].steerRatioValid: + steer_ratio = sm['vehicleParameters'].steerRatio + title = "Steer ratio mismatch" + text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})" + elif not sm['vehicleParameters'].stiffnessFactorValid: + stiffness_factor = sm['vehicleParameters'].stiffnessFactor + title = "Abnormal tire stiffness" + text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})" + else: + return NoEntryAlert("paramsd Temporary Error") + + return NoEntryAlert(alert_text_1=title, alert_text_2=text) + +def overheat_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + cpu = max(sm['deviceState'].cpuTempC, default=0.) + gpu = max(sm['deviceState'].gpuTempC, default=0.) + temp = max((cpu, gpu, sm['deviceState'].memoryTempC)) + return NormalPermanentAlert("System Overheated", f"{temp:.0f} °C") + + +def low_memory_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + return NormalPermanentAlert("Low Memory", f"{sm['deviceState'].memoryUsagePercent}% used") + + +def high_cpu_usage_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + x = max(sm['deviceState'].cpuUsagePercent, default=0.) + return NormalPermanentAlert("High CPU Usage", f"{x}% used") + + +def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + return NormalPermanentAlert("Driving Model Lagging", f"{sm['modelV2'].frameDropPerc:.1f}% frames dropped") + + +def _joystick_axes(sm: messaging.SubMaster) -> tuple[float, float] | None: + if 'testJoystick' not in sm.data or sm.recv_frame['testJoystick'] == 0: + return None + + axes = list(sm['testJoystick'].axes) + if len(axes) < 2: + return None + + return float(axes[0]), float(axes[1]) + + +def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + gb = sm['carControl'].actuators.accel / 4. + if CP.steerControlType in (car.CarParams.SteerControlType.angle, car.CarParams.SteerControlType.curvatureDEPRECATED): + steer = sm['carControl'].actuators.steeringAngleDeg + vals = f"Gas: {round(gb * 100.)}%, Angle: {round(steer, 1)}°" + else: + steer = sm['carControl'].actuators.torque + vals = f"Gas: {round(gb * 100.)}%, Steer: {round(steer * 100.)}%" + return NormalPermanentAlert("Joystick Mode", vals) + + +def longitudinal_maneuver_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + ad = sm['alertDebug'] + audible_alert = AudibleAlert.prompt if 'Active' in ad.alertText1 else AudibleAlert.none + alert_status = AlertStatus.userPrompt if 'Active' in ad.alertText1 else AlertStatus.normal + alert_size = AlertSize.mid if ad.alertText2 else AlertSize.small + return Alert(ad.alertText1, ad.alertText2, + alert_status, alert_size, + Priority.LOW, VisualAlert.none, audible_alert, 0.2) + + +def personality_changed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + personality = str(personality).title() + return NormalPermanentAlert(f"Driving Personality: {personality}", duration=1.5) + + +def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + title = "Invalid LKAS setting" + text = "Toggle stock LKAS on or off to engage" + if CP.brand == "tesla": + title = "Dashcam Mode" + text = "FSD / Autosteer is active" + elif CP.brand == "mazda": + text = "Enable your car's LKAS to engage" + elif CP.brand == "nissan": + text = "Disable your car's stock LKAS to engage" + return NormalPermanentAlert(title, text) + + +def invalid_lkas_setting_no_entry_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, + metric: bool, soft_disable_time: int, personality) -> Alert: + if CP.brand == "tesla": + return NoEntryAlert("FSD / Autosteer is active", alert_text_1="Dashcam Mode") + return NoEntryAlert("Invalid LKAS setting") + + + +EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { + # ********** events with no alerts ********** + + EventName.stockFcw: {}, + EventName.actuatorsApiUnavailable: {}, + + # ********** events only containing alerts displayed in all states ********** + + EventName.joystickDebug: { + ET.WARNING: joystick_alert, + ET.PERMANENT: NormalPermanentAlert("Joystick Mode"), + }, + + EventName.longitudinalManeuver: { + ET.WARNING: longitudinal_maneuver_alert, + ET.PERMANENT: NormalPermanentAlert("Longitudinal Maneuver Mode", + "Ensure road ahead is clear"), + }, + + EventName.bigModelLoading: { + ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), + }, + + EventName.bigModelFailed: { + ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"), + ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.), + }, + + EventName.lateralManeuver: { + ET.WARNING: longitudinal_maneuver_alert, + ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"), + }, + + EventName.selfdriveInitializing: { + ET.NO_ENTRY: NoEntryAlert("IQ.Pilot Initializing"), + }, + + EventName.startup: { + ET.PERMANENT: StartupAlert("Welcome to IQ.Pilot!") + }, + + EventName.startupMaster: { + ET.PERMANENT: StartupAlert("Welcome to IQ.Pilot!"), + }, + + EventName.startupNoControl: { + ET.PERMANENT: StartupAlert("Dashcam mode"), + ET.NO_ENTRY: NoEntryAlert("Dashcam mode"), + }, + + EventName.startupNoCar: { + ET.PERMANENT: StartupAlert("IQ.Pilot Dashcam mode: car unrecognized"), + }, + + EventName.startupNoSecOcKey: { + ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", + "TSK Security Key Not Available", + priority=Priority.HIGH), + }, + + EventName.dashcamMode: { + ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", + priority=Priority.LOWEST), + }, + + EventName.invalidLkasSetting: { + ET.PERMANENT: invalid_lkas_setting_alert, + ET.NO_ENTRY: invalid_lkas_setting_no_entry_alert, + }, + + EventName.cruiseMismatch: { + #ET.PERMANENT: ImmediateDisableAlert("openpilot failed to cancel cruise"), + }, + + # openpilot doesn't recognize the car. This switches openpilot into a + # read-only mode. This can be solved by adding your fingerprint. + # See https://github.com/commaai/openpilot/wiki/Fingerprinting for more information + EventName.carUnrecognized: { + ET.PERMANENT: NormalPermanentAlert("Dashcam Mode", + "Car Unrecognized", + priority=Priority.LOWEST), + }, + + EventName.aeb: { + ET.PERMANENT: Alert( + "BRAKE!", + "Emergency Braking: Risk of Collision", + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), + ET.NO_ENTRY: NoEntryAlert("AEB: Risk of Collision"), + }, + + EventName.stockAeb: { + ET.PERMANENT: Alert( + "BRAKE!", + "Stock AEB: Risk of Collision", + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.none, 2.), + ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"), + }, + + EventName.stockLkas: { + ET.PERMANENT: Alert( + "Stock LKAS: Lane Departure Detected", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), + ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"), + }, + + EventName.fcw: { + ET.PERMANENT: Alert( + "BRAKE!", + "Risk of Collision", + AlertStatus.critical, AlertSize.full, + Priority.HIGHEST, VisualAlert.fcw, AudibleAlert.warningSoft, 2.), + }, + + EventName.ldw: { + ET.PERMANENT: Alert( + "Lane Departure Detected", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.ldw, AudibleAlert.prompt, 3.), + }, + + # ********** events only containing alerts that display while engaged ********** + + EventName.steerTempUnavailableSilent: { + ET.WARNING: Alert( + "Steering Assist Temporarily Unavailable", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8), + }, + + EventName.preDriverDistracted: { + ET.PERMANENT: Alert( + "Pay Attention", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.promptDriverDistracted: { + ET.PERMANENT: Alert( + "Pay Attention", + "Driver Distracted", + AlertStatus.userPrompt, AlertSize.mid, + Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), + }, + + EventName.driverDistracted: { + ET.PERMANENT: Alert( + "DISENGAGE IMMEDIATELY", + "Driver Distracted", + AlertStatus.critical, AlertSize.full, + Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), + }, + + EventName.preDriverUnresponsive: { + ET.PERMANENT: Alert( + "Touch Steering Wheel: No Face Detected", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.none, .1), + }, + + EventName.promptDriverUnresponsive: { + ET.PERMANENT: Alert( + "Touch Steering Wheel", + "Driver Unresponsive", + AlertStatus.userPrompt, AlertSize.mid, + Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, .1), + }, + + EventName.driverUnresponsive: { + ET.PERMANENT: Alert( + "DISENGAGE IMMEDIATELY", + "Driver Unresponsive", + AlertStatus.critical, AlertSize.full, + Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.warningImmediate, .1), + }, + + EventName.manualRestart: { + ET.WARNING: Alert( + "TAKE CONTROL", + "Resume Driving Manually", + AlertStatus.userPrompt, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), + }, + + EventName.resumeRequired: { + ET.WARNING: Alert( + "Press Resume to Exit Standstill", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), + }, + + EventName.belowSteerSpeed: { + ET.WARNING: below_steer_speed_alert, + }, + + EventName.preLaneChangeLeft: { + ET.WARNING: Alert( + "Steer Left to Start Lane Change Once Safe", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.preLaneChangeRight: { + ET.WARNING: Alert( + "Steer Right to Start Lane Change Once Safe", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.laneChangeBlocked: { + ET.WARNING: Alert( + "Car Detected in Blindspot", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), + }, + + EventName.laneChange: { + ET.WARNING: Alert( + "Changing Lanes", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.steerSaturated: { + ET.WARNING: Alert( + "Take Control", + "Turn Exceeds Steering Limit", + AlertStatus.userPrompt, AlertSize.mid, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), + }, + + # Thrown when the fan is driven at >50% but is not rotating + EventName.fanMalfunction: { + ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Likely Hardware Issue"), + }, + + # Camera is not outputting frames + EventName.cameraMalfunction: { + ET.PERMANENT: camera_malfunction_alert, + ET.SOFT_DISABLE: soft_disable_alert("Camera Malfunction"), + ET.NO_ENTRY: NoEntryAlert("Camera Malfunction: Reboot Your Device"), + }, + # Camera framerate too low + EventName.cameraFrameRate: { + ET.PERMANENT: NormalPermanentAlert("Camera Frame Rate Low", "Reboot your Device"), + ET.SOFT_DISABLE: soft_disable_alert("Camera Frame Rate Low"), + ET.NO_ENTRY: NoEntryAlert("Camera Frame Rate Low: Reboot Your Device"), + }, + + # Unused + + EventName.locationdTemporaryError: { + ET.NO_ENTRY: NoEntryAlert("locationd Temporary Error"), + ET.SOFT_DISABLE: soft_disable_alert("locationd Temporary Error"), + }, + + EventName.locationdPermanentError: { + ET.NO_ENTRY: NoEntryAlert("locationd Permanent Error"), + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("locationd Permanent Error"), + ET.PERMANENT: NormalPermanentAlert("locationd Permanent Error"), + }, + + # openpilot tries to learn certain parameters about your car by observing + # how the car behaves to steering inputs from both human and openpilot driving. + # This includes: + # - steer ratio: gear ratio of the steering rack. Steering angle divided by tire angle + # - tire stiffness: how much grip your tires have + # - angle offset: most steering angle sensors are offset and measure a non zero angle when driving straight + # This alert is thrown when any of these values exceed a sanity check. This can be caused by + # bad alignment or bad sensor data. If this happens consistently consider creating an issue on GitHub + EventName.paramsdTemporaryError: { + ET.NO_ENTRY: paramsd_invalid_alert, + ET.SOFT_DISABLE: soft_disable_alert("paramsd Temporary Error"), + }, + + EventName.paramsdPermanentError: { + ET.NO_ENTRY: NoEntryAlert("paramsd Permanent Error"), + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("paramsd Permanent Error"), + ET.PERMANENT: NormalPermanentAlert("paramsd Permanent Error"), + }, + + # ********** events that affect controls state transitions ********** + + EventName.pcmEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventName.buttonEnable: { + ET.ENABLE: EngagementAlert(AudibleAlert.engage), + }, + + EventName.pcmDisable: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + }, + + EventName.buttonCancel: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"), + }, + + EventName.brakeHold: { + ET.WARNING: Alert( + "Press Resume to Exit Brake Hold", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), + }, + + EventName.parkBrake: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Parking Brake Engaged"), + }, + + EventName.pedalPressed: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Pedal Pressed", + visual_alert=VisualAlert.brakePressed), + }, + + EventName.steerDisengage: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Steering Pressed"), + }, + + EventName.preEnableStandstill: { + ET.PRE_ENABLE: Alert( + "Release Brake to Engage", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1, creation_delay=1.), + }, + + EventName.gasPressedOverride: { + ET.OVERRIDE_LONGITUDINAL: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.steerOverride: { + ET.OVERRIDE_LATERAL: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .1), + }, + + EventName.wrongCarMode: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: wrong_car_mode_alert, + }, + + EventName.resumeBlocked: { + ET.NO_ENTRY: NoEntryAlert("Press Set to Engage"), + }, + + EventName.wrongCruiseMode: { + ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), + ET.NO_ENTRY: NoEntryAlert("Adaptive Cruise Disabled"), + }, + + EventName.steerTempUnavailable: { + ET.SOFT_DISABLE: soft_disable_alert("Steering Assist Temporarily Unavailable"), + ET.NO_ENTRY: NoEntryAlert("Steering Temporarily Unavailable"), + }, + + EventName.steerTimeLimit: { + ET.SOFT_DISABLE: soft_disable_alert("Vehicle Steering Time Limit"), + ET.NO_ENTRY: NoEntryAlert("Vehicle Steering Time Limit"), + }, + + EventName.outOfSpace: { + ET.PERMANENT: out_of_space_alert, + ET.NO_ENTRY: NoEntryAlert("Out of Storage"), + }, + + EventName.belowEngageSpeed: { + ET.NO_ENTRY: below_engage_speed_alert, + }, + + EventName.sensorDataInvalid: { + ET.PERMANENT: Alert( + "Sensor Data Invalid", + "Possible Hardware Issue", + AlertStatus.normal, AlertSize.mid, + Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.), + ET.NO_ENTRY: NoEntryAlert("Sensor Data Invalid"), + ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"), + }, + + EventName.noGps: { + }, + + EventName.tooDistracted: { + ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), + }, + + EventName.excessiveActuation: { + ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"), + ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"), + }, + + EventName.overheat: { + ET.PERMANENT: overheat_alert, + ET.SOFT_DISABLE: soft_disable_alert("System Overheated"), + ET.NO_ENTRY: NoEntryAlert("System Overheated"), + }, + + EventName.wrongGear: { + ET.SOFT_DISABLE: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + ET.NO_ENTRY: Alert( + "", + "", + AlertStatus.normal, AlertSize.none, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, 0.), + }, + + # This alert is thrown when the calibration angles are outside of the acceptable range. + # For example if the device is pointed too much to the left or the right. + # Usually this can only be solved by removing the mount from the windshield completely, + # and attaching while making sure the device is pointed straight forward and is level. + # See https://comma.ai/setup for more information + EventName.calibrationInvalid: { + ET.PERMANENT: calibration_invalid_alert, + ET.SOFT_DISABLE: soft_disable_alert("Calibration Invalid: Remount Device & Recalibrate"), + ET.NO_ENTRY: NoEntryAlert("Calibration Invalid: Remount Device & Recalibrate"), + }, + + EventName.calibrationIncomplete: { + ET.PERMANENT: calibration_incomplete_alert, + ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"), + ET.NO_ENTRY: NoEntryAlert("Calibration in Progress"), + }, + + EventName.calibrationRecalibrating: { + ET.PERMANENT: calibration_incomplete_alert, + ET.SOFT_DISABLE: soft_disable_alert("Device Remount Detected: Recalibrating"), + ET.NO_ENTRY: NoEntryAlert("Remount Detected: Recalibrating"), + }, + + EventName.doorOpen: { + ET.SOFT_DISABLE: user_soft_disable_alert("Door Open"), + ET.NO_ENTRY: NoEntryAlert("Door Open"), + }, + + EventName.seatbeltNotLatched: { + ET.SOFT_DISABLE: user_soft_disable_alert("Seatbelt Unlatched"), + ET.NO_ENTRY: NoEntryAlert("Seatbelt Unlatched"), + }, + + EventName.espDisabled: { + ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Disabled"), + ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Disabled"), + }, + + EventName.lowBattery: { + ET.SOFT_DISABLE: soft_disable_alert("Low Battery"), + ET.NO_ENTRY: NoEntryAlert("Low Battery"), + }, + + # Different openpilot services communicate between each other at a certain + # interval. If communication does not follow the regular schedule this alert + # is thrown. This can mean a service crashed, did not broadcast a message for + # ten times the regular interval, or the average interval is more than 10% too high. + # Soft warnings — no disable, no entry block. UI shows a silent yellow triangle instead. + EventName.commIssue: { + }, + EventName.commIssueAvgFreq: { + }, + + EventName.selfdrivedLagging: { + }, + + # Thrown when manager detects a service exited unexpectedly while driving + EventName.processNotRunning: { + ET.NO_ENTRY: process_not_running_alert, + ET.SOFT_DISABLE: soft_disable_alert("Process Not Running"), + }, + + EventName.radarFault: { + ET.SOFT_DISABLE: soft_disable_alert("Radar Error: Restart the Car"), + ET.NO_ENTRY: NoEntryAlert("Radar Error: Restart the Car"), + }, + + EventName.radarTempUnavailable: { + ET.SOFT_DISABLE: soft_disable_alert("Radar Temporarily Unavailable"), + ET.NO_ENTRY: NoEntryAlert("Radar Temporarily Unavailable"), + }, + + # Every frame from the camera should be processed by the model. If modeld + # is not processing frames fast enough they have to be dropped. This alert is + # thrown when over 20% of frames are dropped. + EventName.modeldLagging: { + ET.SOFT_DISABLE: soft_disable_alert("Driving Model Lagging"), + ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"), + ET.PERMANENT: modeld_lagging_alert, + }, + + # Besides predicting the path, lane lines and lead car data the model also + # predicts the current velocity and rotation speed of the car. If the model is + # very uncertain about the current velocity while the car is moving, this + # usually means the model has trouble understanding the scene. This is used + # as a heuristic to warn the driver. + EventName.posenetInvalid: { + ET.SOFT_DISABLE: soft_disable_alert("Posenet Speed Invalid"), + ET.NO_ENTRY: posenet_invalid_alert, + }, + + # When the localizer detects an acceleration of more than 40 m/s^2 (~4G) we + # alert the driver the device might have fallen from the windshield. + EventName.deviceFalling: { + ET.SOFT_DISABLE: soft_disable_alert("Device Fell Off Mount"), + ET.NO_ENTRY: NoEntryAlert("Device Fell Off Mount"), + }, + + EventName.lowMemory: { + ET.SOFT_DISABLE: soft_disable_alert("Low Memory: Reboot Your Device"), + ET.PERMANENT: low_memory_alert, + ET.NO_ENTRY: NoEntryAlert("Low Memory: Reboot Your Device"), + }, + + EventName.accFaulted: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Fault: Restart the Car"), + ET.PERMANENT: NormalPermanentAlert("Cruise Fault: Restart the car to engage"), + ET.NO_ENTRY: NoEntryAlert("Cruise Fault: Restart the Car"), + }, + + EventName.cruiseFaultLateralAllowed: { + ET.PERMANENT: NormalPermanentAlert("Cruise Faulted", "Lane Assist will continue to work", priority=Priority.LOWEST), + }, + + EventName.espActive: { + ET.SOFT_DISABLE: soft_disable_alert("Electronic Stability Control Active"), + ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Active"), + }, + + EventName.controlsMismatch: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Controls Mismatch"), + ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), + }, + + # Sometimes the USB stack on the device can get into a bad state + # causing the connection to the panda to be lost + EventName.usbError: { + ET.SOFT_DISABLE: soft_disable_alert("USB Error: Reboot Your Device"), + ET.PERMANENT: NormalPermanentAlert("USB Error: Reboot Your Device"), + ET.NO_ENTRY: NoEntryAlert("USB Error: Reboot Your Device"), + }, + + # This alert can be thrown for the following reasons: + # - No CAN data received at all + # - CAN data is received, but some message are not received at the right frequency + # If you're not writing a new car port, this is usually cause by faulty wiring + # Minor canError: low-priority silent HUD badge only — no disable, no entry block, no audible/visual alert + EventName.canError: { + ET.PERMANENT: Alert( + "CAN Error", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), + }, + + EventName.canBusMissing: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Bus Disconnected"), + ET.PERMANENT: Alert( + "CAN Bus Disconnected: Likely Faulty Cable", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.), + ET.NO_ENTRY: NoEntryAlert("CAN Bus Disconnected: Check Connections"), + }, + + EventName.steerUnavailable: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("LKAS Fault: Restart the Car"), + ET.PERMANENT: NormalPermanentAlert("LKAS Fault: Restart the car to engage"), + ET.NO_ENTRY: NoEntryAlert("LKAS Fault: Restart the Car"), + }, + + EventName.reverseGear: { + ET.PERMANENT: Alert( + "Reverse\nGear", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.USER_DISABLE: ImmediateDisableAlert("Reverse Gear"), + ET.NO_ENTRY: NoEntryAlert("Reverse Gear"), + }, + + # On cars that use stock ACC the car can decide to cancel ACC for various reasons. + # When this happens we can no long control the car so the user needs to be warned immediately. + EventName.cruiseDisabled: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Cruise Is Off"), + }, + + # When the relay in the harness box opens the CAN bus between the LKAS camera + # and the rest of the car is separated. When messages from the LKAS camera + # are received on the car side this usually means the relay hasn't opened correctly + # and this alert is thrown. + EventName.relayMalfunction: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Harness Relay Malfunction"), + ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Check Hardware"), + ET.NO_ENTRY: NoEntryAlert("Harness Relay Malfunction"), + }, + + EventName.speedTooLow: { + ET.IMMEDIATE_DISABLE: Alert( + "IQ.Pilot Canceled", + "Speed too low", + AlertStatus.normal, AlertSize.mid, + Priority.HIGH, VisualAlert.none, AudibleAlert.disengage, 3.), + }, + + # When the car is driving faster than most cars in the training data, the model outputs can be unpredictable. + EventName.speedTooHigh: { + ET.WARNING: Alert( + "Speed Too High", + "Model uncertain at this speed", + AlertStatus.userPrompt, AlertSize.mid, + Priority.HIGH, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 4.), + ET.NO_ENTRY: NoEntryAlert("Slow down to engage"), + }, + + EventName.vehicleSensorsInvalid: { + ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Vehicle Sensors Invalid"), + ET.PERMANENT: NormalPermanentAlert("Vehicle Sensors Calibrating", "Drive to Calibrate"), + ET.NO_ENTRY: NoEntryAlert("Vehicle Sensors Calibrating"), + }, + + EventName.personalityChanged: { + ET.WARNING: personality_changed_alert, + }, + + EventName.userBookmark: { + ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), + }, + + EventName.audioFeedback: { + ET.PERMANENT: audio_feedback_alert, + }, +} + + +if HARDWARE.get_device_type() == 'mici': + EVENTS.update({ + EventName.preDriverDistracted: { + ET.PERMANENT: Alert( + "Pay Attention", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 2), + }, + EventName.promptDriverDistracted: { + ET.PERMANENT: Alert( + "Pay Attention", + "Driver Distracted", + AlertStatus.userPrompt, AlertSize.mid, + Priority.MID, VisualAlert.steerRequired, AudibleAlert.promptDistracted, 1), + }, + EventName.resumeRequired: { + ET.WARNING: Alert( + "Press Resume", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .2), + }, + EventName.preLaneChangeLeft: { + ET.WARNING: Alert( + "Steer Left", + "Confirm Lane Change", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + EventName.preLaneChangeRight: { + ET.WARNING: Alert( + "Steer Right", + "Confirm Lane Change", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, .1), + }, + EventName.laneChangeBlocked: { + ET.WARNING: Alert( + "Car in Blindspot", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), + }, + EventName.steerSaturated: { + ET.WARNING: Alert( + "take control", + "turn exceeds limit", + AlertStatus.userPrompt, AlertSize.mid, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), + }, + EventName.calibrationIncomplete: { + ET.PERMANENT: calibration_incomplete_alert, + ET.SOFT_DISABLE: soft_disable_alert("Calibration Incomplete"), + ET.NO_ENTRY: NoEntryAlert("Calibrating"), + }, + EventName.reverseGear: { + ET.PERMANENT: Alert( + "Reverse", + "", + AlertStatus.normal, AlertSize.full, + Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2, creation_delay=0.5), + ET.USER_DISABLE: ImmediateDisableAlert("Reverse"), + ET.NO_ENTRY: NoEntryAlert("Reverse"), + }, + }) + + +if __name__ == '__main__': + # print all alerts by type and priority + from iqpilot.cereal.services import SERVICE_LIST + from collections import defaultdict + + event_names = {v: k for k, v in EventName.schema.enumerants.items()} + alerts_by_type: dict[str, dict[Priority, list[str]]] = defaultdict(lambda: defaultdict(list)) + + CP = car.CarParams.new_message() + CS = car.CarState.new_message() + sm = messaging.SubMaster(list(SERVICE_LIST.keys())) + + for i, alerts in EVENTS.items(): + for et, alert in alerts.items(): + if callable(alert): + alert = alert(CP, CS, sm, False, 1, log.LongitudinalPersonality.standard) + alerts_by_type[et][alert.priority].append(event_names[i]) + + all_alerts: dict[str, list[tuple[Priority, list[str]]]] = {} + for et, priority_alerts in alerts_by_type.items(): + all_alerts[et] = sorted(priority_alerts.items(), key=lambda x: x[0], reverse=True) + + for status, evs in sorted(all_alerts.items(), key=lambda x: x[0]): + print(f"**** {status} ****") + for p, alert_list in evs: + print(f" {repr(p)}:") + print(" ", ', '.join(alert_list), "\n") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/__init__.py new file mode 100644 index 000000000..958a822d4 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/__init__.py @@ -0,0 +1,7 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" + +from iqpilot.selfdrive.state_estimation.estimator import EstimatorModel, ModelDefinition, Observation, StateEstimator + +__all__ = ["EstimatorModel", "ModelDefinition", "Observation", "StateEstimator"] diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/estimator.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/estimator.py new file mode 100644 index 000000000..696bdd737 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/state_estimation/estimator.py @@ -0,0 +1,311 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" + +from collections.abc import Callable +from dataclasses import dataclass +import math + +import numpy as np + + +Array = np.ndarray +Prediction = Callable[[Array, float, dict[str, float]], Array] +Measurement = Callable[[Array, dict[str, float]], Array] +Injection = Callable[[Array, Array], Array] +NativePrediction = Callable[[Array, Array, float, Array, dict[str, float]], None] +NativeUpdate = Callable[[Array, Array, int, Array, Array], Array] + + +@dataclass(frozen=True) +class Observation: + kind: int + values: Array + noise: Array + + +@dataclass(frozen=True) +class ModelDefinition: + state_size: int + error_size: int + transition: Prediction + measurements: dict[int, Measurement] + process_noise: Array + observation_noise: dict[int, Array] + inject_error: Injection | None = None + error_projection: Callable[[Array], Array] | None = None + normalize: Callable[[Array], Array] | None = None + native_predict: NativePrediction | None = None + native_update: NativeUpdate | None = None + + +@dataclass +class _Snapshot: + time: float + state: Array + covariance: Array + + +@dataclass +class _Event: + time: float + observation: Observation + order: int + + +class StateEstimator: + def __init__(self, model: ModelDefinition, initial_state: Array, initial_covariance: Array, + max_rewind_age: float = 0.0): + self.model = model + self.parameters: dict[str, float] = {} + self.max_rewind_age = max_rewind_age + self._order = 0 + self.init_state(initial_state, initial_covariance, None) + + @property + def x(self) -> Array: + return self._state.copy() + + @property + def P(self) -> Array: + return self._covariance.copy() + + @property + def t(self) -> float: + return self._time + + def set_global(self, name: str, value: float) -> None: + self.parameters[name] = float(value) + + def init_state(self, state: Array, covs: Array, filter_time: float | None) -> None: + state = np.asarray(state, dtype=np.float64).reshape(-1) + covariance = np.asarray(covs, dtype=np.float64) + self._validate_state(state, covariance) + self._state = self._normalize(state.copy()) + self._covariance = self._stabilize(covariance.copy()) + self._time = math.nan if filter_time is None else float(filter_time) + self._events: list[_Event] = [] + self._snapshots = [_Snapshot(self._time, self._state.copy(), self._covariance.copy())] + + def set_filter_time(self, filter_time: float | None) -> None: + self._time = math.nan if filter_time is None else float(filter_time) + + def reset_rewind(self) -> None: + self._events.clear() + self._snapshots = [_Snapshot(self._time, self._state.copy(), self._covariance.copy())] + + def predict(self, time: float) -> None: + time = float(time) + if math.isnan(self._time): + self._time = time + return + if time < self._time: + raise ValueError("prediction time precedes estimator time") + dt = time - self._time + if dt == 0.0: + return + if self.model.native_predict is not None: + self.model.native_predict(self._state, self._covariance, dt, self.model.process_noise, self.parameters) + self._time = time + return + previous = self._state.copy() + transition_jacobian = self._jacobian(lambda value: self.model.transition(value, dt, self.parameters), previous) + predicted = self.model.transition(previous, dt, self.parameters) + projection = self._error_projection(previous) + if self.model.error_size == self.model.state_size: + error_transition = transition_jacobian + else: + error_transition = np.linalg.pinv(self._error_projection(predicted)) @ transition_jacobian @ projection + self._state = self._normalize(predicted) + self._covariance = self._stabilize(error_transition @ self._covariance @ error_transition.T + dt * self.model.process_noise) + self._time = time + + def predict_and_observe(self, time: float, kind: int, measurements: Array, noise: Array | None = None): + values = self._measurement_batch(kind, measurements) + noises = self._noise_batch(kind, len(values), noise) + event = _Event(float(time), Observation(kind, values, noises), self._order) + self._order += 1 + if not math.isnan(self._time) and event.time < self._time: + if self.max_rewind_age <= 0.0 or self._time - event.time > self.max_rewind_age: + return None + return self._rewind(event) + result = self._apply_event(event) + self._events.append(event) + self._snapshots.append(_Snapshot(self._time, self._state.copy(), self._covariance.copy())) + self._trim_history() + return result + + def _apply_event(self, event: _Event): + self.predict(event.time) + prior_state = self._state.copy() + prior_covariance = self._covariance.copy() + innovations = [] + for measurement, noise in zip(event.observation.values, event.observation.noise, strict=True): + innovations.append(self._update(event.observation.kind, measurement, noise)) + return (event.time, self.x, prior_state, self.P, prior_covariance, event.observation.kind, + tuple(innovations), event.observation.values.copy(), event.observation.noise.copy()) + + def _update(self, kind: int, measurement: Array, noise: Array) -> Array: + measurement_function = self.model.measurements.get(kind) + if measurement_function is None: + raise KeyError(f"unknown observation kind {kind}") + measurement = np.asarray(measurement, dtype=np.float64).reshape(-1) + if noise.shape != (measurement.size, measurement.size): + raise ValueError("observation noise dimension mismatch") + if self.model.native_update is not None: + innovation = self.model.native_update(self._state, self._covariance, kind, measurement, noise) + return innovation + predicted = np.asarray(measurement_function(self._state, self.parameters), dtype=np.float64).reshape(-1) + if predicted.shape != measurement.shape: + raise ValueError("measurement dimension mismatch") + innovation = measurement - predicted + state_jacobian = self._jacobian(lambda value: measurement_function(value, self.parameters), self._state) + observation_jacobian = state_jacobian @ self._error_projection(self._state) + innovation_covariance = observation_jacobian @ self._covariance @ observation_jacobian.T + noise + gain = np.linalg.solve(innovation_covariance, observation_jacobian @ self._covariance).T + delta = gain @ innovation + self._state = self._normalize(self._inject(self._state, delta)) + identity = np.eye(self.model.error_size) + residual = identity - gain @ observation_jacobian + self._covariance = self._stabilize(residual @ self._covariance @ residual.T + gain @ noise @ gain.T) + self._require_finite() + return innovation + + def _rewind(self, new_event: _Event): + events = sorted(self._events + [new_event], key=lambda event: (event.time, event.order)) + base_index = max(i for i, snapshot in enumerate(self._snapshots) if math.isnan(snapshot.time) or snapshot.time <= new_event.time) + base = self._snapshots[base_index] + retained = self._events[:base_index] + retained_orders = {event.order for event in retained} + replay = [event for event in events if event.order not in retained_orders] + self._state = base.state.copy() + self._covariance = base.covariance.copy() + self._time = base.time + self._events = retained.copy() + self._snapshots = self._snapshots[:base_index + 1] + result = None + for event in replay: + current = self._apply_event(event) + self._events.append(event) + self._snapshots.append(_Snapshot(self._time, self._state.copy(), self._covariance.copy())) + if event is new_event: + result = current + self._trim_history() + return result + + def _trim_history(self) -> None: + if self.max_rewind_age <= 0.0 or math.isnan(self._time): + return + cutoff = self._time - self.max_rewind_age + remove = 0 + while remove < len(self._events) and self._events[remove].time < cutoff: + remove += 1 + if remove: + self._events = self._events[remove:] + self._snapshots = self._snapshots[remove:] + + def _measurement_batch(self, kind: int, measurements: Array) -> Array: + measurement_function = self.model.measurements.get(kind) + if measurement_function is None: + raise KeyError(f"unknown observation kind {kind}") + if self.model.native_update is not None and kind in self.model.observation_noise: + expected = self.model.observation_noise[kind].shape[0] + else: + expected = np.asarray(measurement_function(self._state, self.parameters)).size + values = np.asarray(measurements, dtype=np.float64) + if values.ndim == 1: + values = values.reshape(1, -1) + elif values.ndim != 2: + raise ValueError("measurements must be one or two dimensional") + if values.shape[1] != expected: + raise ValueError("measurement dimension mismatch") + return values + + def _noise_batch(self, kind: int, count: int, noise: Array | None) -> Array: + if noise is None: + base = self.model.observation_noise.get(kind) + if base is None: + raise KeyError(f"missing observation noise for kind {kind}") + return np.repeat(np.asarray(base, dtype=np.float64)[None, :, :], count, axis=0) + noises = np.asarray(noise, dtype=np.float64) + if noises.ndim == 2: + noises = noises[None, :, :] + if noises.shape[0] == 1 and count > 1: + noises = np.repeat(noises, count, axis=0) + if noises.shape[0] != count: + raise ValueError("observation noise batch mismatch") + return noises + + def _jacobian(self, function: Callable[[Array], Array], value: Array) -> Array: + output = np.asarray(function(value), dtype=np.float64).reshape(-1) + result = np.empty((output.size, value.size), dtype=np.float64) + for index in range(value.size): + step = np.cbrt(np.finfo(np.float64).eps) * max(1.0, abs(value[index])) + upper = value.copy() + lower = value.copy() + upper[index] += step + lower[index] -= step + result[:, index] = (np.asarray(function(upper)).reshape(-1) - np.asarray(function(lower)).reshape(-1)) / (2.0 * step) + return result + + def _inject(self, state: Array, delta: Array) -> Array: + if self.model.inject_error is None: + return state + delta + return self.model.inject_error(state, delta) + + def _error_projection(self, state: Array) -> Array: + if self.model.error_projection is None: + return np.eye(self.model.state_size, self.model.error_size) + return np.asarray(self.model.error_projection(state), dtype=np.float64) + + def _normalize(self, state: Array) -> Array: + if self.model.normalize is None: + return np.asarray(state, dtype=np.float64).reshape(-1) + return np.asarray(self.model.normalize(state), dtype=np.float64).reshape(-1) + + def _stabilize(self, covariance: Array) -> Array: + covariance = (covariance + covariance.T) * 0.5 + eigenvalues, eigenvectors = np.linalg.eigh(covariance) + if eigenvalues[0] < -1e-10: + raise FloatingPointError("covariance is not positive semidefinite") + return (eigenvectors * np.maximum(eigenvalues, 0.0)) @ eigenvectors.T + + def _validate_state(self, state: Array, covariance: Array) -> None: + if state.shape != (self.model.state_size,): + raise ValueError("state dimension mismatch") + if covariance.shape != (self.model.error_size, self.model.error_size): + raise ValueError("covariance dimension mismatch") + if self.model.process_noise.shape != covariance.shape: + raise ValueError("process noise dimension mismatch") + if not np.isfinite(state).all() or not np.isfinite(covariance).all(): + raise ValueError("state and covariance must be finite") + + def _require_finite(self) -> None: + if not np.isfinite(self._state).all() or not np.isfinite(self._covariance).all(): + raise FloatingPointError("estimator produced non-finite values") + + +class EstimatorModel: + def __init__(self, estimator: StateEstimator): + self.filter = estimator + + @property + def x(self) -> Array: + return self.filter.x + + @property + def P(self) -> Array: + return self.filter.P + + @property + def t(self) -> float: + return self.filter.t + + def init_state(self, state: Array, covs: Array, filter_time: float | None) -> None: + self.filter.init_state(state, covs, filter_time) + + def predict(self, time: float) -> None: + self.filter.predict(time) + + def predict_and_observe(self, time: float, kind: int, measurements: Array, noise: Array | None = None): + return self.filter.predict_and_observe(time, kind, measurements, noise) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py new file mode 100644 index 000000000..0412edd32 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/selfdrive/ui/feedback/feedbackd.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import iqpilot.cereal.messaging as messaging +from iqpilot.common.params import Params +from iqpilot.common.swaglog import cloudlog +from iqpilot.cereal import car +from iqpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER + +FEEDBACK_MAX_DURATION = 10.0 +ButtonType = car.CarState.ButtonEvent.Type + + +def main(): + params = Params() + pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) + sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton']) + should_record_audio = False + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + + while True: + sm.update() + should_send_bookmark = False + + if False and sm.updated['carState'] and sm['carState'].canValid and not sm['iqState'].aol.available: + for be in sm['carState'].buttonEvents: + if be.type == ButtonType.lkas: + if be.pressed: + if not should_record_audio: + if params.get_bool("RecordAudioFeedback"): + should_record_audio = True + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + cloudlog.info("LKAS button pressed - starting 10-second audio feedback") + else: + should_send_bookmark = True + cloudlog.info("LKAS button pressed - bookmarking") + elif should_record_audio and not waiting_for_release: + waiting_for_release = True + elif waiting_for_release: + waiting_for_release = False + early_stop_triggered = True + cloudlog.info("LKAS button released - ending recording early") + + if should_record_audio and sm.updated['rawAudioData']: + raw_audio = sm['rawAudioData'] + msg = messaging.new_message('audioFeedback', valid=True) + msg.audioFeedback.audio.data = raw_audio.data + msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate + msg.audioFeedback.blockNum = block_num + block_num += 1 + if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: + should_send_bookmark = True + should_record_audio = False + early_stop_triggered = False + cloudlog.info("10-second recording completed or second button press - stopping audio feedback") + pm.send('audioFeedback', msg) + + if sm.updated['bookmarkButton']: + cloudlog.info("Bookmark button pressed!") + should_send_bookmark = True + + if should_send_bookmark: + msg = messaging.new_message('userBookmark', valid=True) + pm.send('userBookmark', msg) + + +if __name__ == '__main__': + main() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/nv12_info.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/nv12_info.py new file mode 100644 index 000000000..bcb6312d2 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/camerad/cameras/nv12_info.py @@ -0,0 +1,21 @@ +# Python version of system/camerad/cameras/nv12_info.h +# Calculations from third_party/linux/include/msm_media_info.h (VENUS_BUFFER_SIZE) + +def align(val: int, alignment: int) -> int: + return ((val + alignment - 1) // alignment) * alignment + +def get_nv12_info(width: int, height: int) -> tuple[int, int, int, int]: + """Returns (stride, y_height, uv_height, buffer_size) for NV12 frame dimensions.""" + stride = align(width, 128) + y_height = align(height, 32) + uv_height = align(height // 2, 16) + + # VENUS_BUFFER_SIZE for NV12 + y_plane = stride * y_height + uv_plane = stride * uv_height + 4096 + size = y_plane + uv_plane + max(16 * 1024, 8 * stride) + size = align(size, 4096) + size += align(width, 512) * 512 # kernel padding for non-aligned frames + size = align(size, 4096) + + return stride, y_height, uv_height, size diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/__init__.py new file mode 100644 index 000000000..1338aa2cf --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/__init__.py @@ -0,0 +1,22 @@ +import os +from typing import cast + +from iqpilot.system.hardware.base import HardwareBase +from iqpilot.system.hardware.tici.hardware import Tici +from iqpilot.system.hardware.pc.hardware import Pc + +TICI = os.path.isfile('/TICI') +AGNOS = os.path.isfile('/AGNOS') +PC = not TICI + + +if TICI: + HARDWARE = cast(HardwareBase, Tici()) +else: + HARDWARE = cast(HardwareBase, Pc()) + +# Only comma 3/3X expose the DMA-BUF EGL extensions used by the zero-copy +# camera renderer and the direct EGL frame-pacing calls. /TICI is also present +# on comma 4, so it identifies the AGNOS hardware family rather than this GPU +# capability. +EGL_DMA_BUF_SUPPORTED = TICI and HARDWARE.get_device_type() in ("tici", "tizi") diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/base.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/base.py new file mode 100644 index 000000000..217696e09 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/base.py @@ -0,0 +1,228 @@ +import os +from abc import abstractmethod, ABC +from dataclasses import dataclass, fields + +from iqpilot.cereal import log + +NetworkType = log.DeviceState.NetworkType +NetworkStrength = log.DeviceState.NetworkStrength + +class LPAError(RuntimeError): + pass + +class LPAProfileNotFoundError(LPAError): + pass + +@dataclass +class Profile: + iccid: str + nickname: str + enabled: bool + provider: str + +@dataclass +class ThermalZone: + # a zone from /sys/class/thermal/thermal_zone* + name: str # a.k.a type + scale: float = 1000. # scale to get degrees in C + zone_number = -1 + + def read(self) -> float: + if self.zone_number < 0: + for n in os.listdir("/sys/devices/virtual/thermal"): + if not n.startswith("thermal_zone"): + continue + with open(os.path.join("/sys/devices/virtual/thermal", n, "type")) as f: + if f.read().strip() == self.name: + self.zone_number = int(n.removeprefix("thermal_zone")) + break + + try: + with open(f"/sys/devices/virtual/thermal/thermal_zone{self.zone_number}/temp") as f: + return int(f.read()) / self.scale + except FileNotFoundError: + return 0 + +@dataclass +class ThermalConfig: + cpu: list[ThermalZone] | None = None + gpu: list[ThermalZone] | None = None + dsp: ThermalZone | None = None + pmic: list[ThermalZone] | None = None + memory: ThermalZone | None = None + intake: ThermalZone | None = None + exhaust: ThermalZone | None = None + case: ThermalZone | None = None + + def get_msg(self): + ret = {} + for f in fields(ThermalConfig): + v = getattr(self, f.name) + if v is not None: + if isinstance(v, list): + ret[f.name + "TempC"] = [x.read() for x in v] + else: + ret[f.name + "TempC"] = v.read() + return ret + +class LPABase(ABC): + @abstractmethod + def list_profiles(self) -> list[Profile]: + pass + + @abstractmethod + def get_active_profile(self) -> Profile | None: + pass + + @abstractmethod + def delete_profile(self, iccid: str) -> None: + pass + + @abstractmethod + def bootstrap(self) -> None: + pass + + @abstractmethod + def download_profile(self, qr: str, nickname: str | None = None) -> None: + pass + + @abstractmethod + def nickname_profile(self, iccid: str, nickname: str) -> None: + pass + + @abstractmethod + def switch_profile(self, iccid: str) -> None: + pass + + def is_comma_profile(self, iccid: str) -> bool: + return any(iccid.startswith(prefix) for prefix in ('8985235',)) + +class HardwareBase(ABC): + @staticmethod + def get_cmdline() -> dict[str, str]: + with open('/proc/cmdline') as f: + cmdline = f.read() + return {kv[0]: kv[1] for kv in [s.split('=') for s in cmdline.split(' ')] if len(kv) == 2} + + @staticmethod + def read_param_file(path, parser, default=0): + try: + with open(path) as f: + return parser(f.read()) + except Exception: + return default + + def booted(self) -> bool: + return True + + def reboot(self, reason=None): + print("REBOOT!") + + def uninstall(self): + print("uninstall") + + def get_os_version(self): + return None + + @abstractmethod + def get_device_type(self): + pass + + def get_imei(self, slot) -> str: + return "" + + def get_serial(self): + return "" + + def get_network_info(self): + return None + + def get_network_type(self): + return NetworkType.none + + def get_sim_info(self): + return { + 'sim_id': '', + 'mcc_mnc': None, + 'network_type': ["Unknown"], + 'sim_state': ["ABSENT"], + 'data_connected': False + } + + def get_sim_lpa(self) -> LPABase: + raise NotImplementedError("SIM LPA not available") + + def get_network_strength(self, network_type): + return NetworkStrength.unknown + + def get_network_metered(self, network_type) -> bool: + return network_type not in (NetworkType.none, NetworkType.wifi, NetworkType.ethernet) + + def get_current_power_draw(self): + return 0 + + def get_som_power_draw(self): + return 0 + + def shutdown(self): + print("SHUTDOWN!") + + def get_thermal_config(self): + return ThermalConfig() + + def set_display_power(self, on: bool): + pass + + def set_screen_brightness(self, percentage): + pass + + def get_screen_brightness(self): + return 0 + + def set_power_save(self, powersave_enabled): + pass + + def get_gpu_usage_percent(self): + return 0 + + def get_modem_version(self): + return None + + def get_modem_temperatures(self): + return [] + + def initialize_hardware(self): + pass + + def configure_modem(self): + pass + + def reboot_modem(self): + pass + + def recover_sim_detection(self) -> bool: + return False + + def get_networks(self): + return None + + def has_internal_panda(self) -> bool: + return False + + def reset_internal_panda(self): + pass + + def recover_internal_panda(self): + pass + + def get_modem_data_usage(self): + return -1, -1 + + def get_voltage(self) -> float: + return 0. + + def get_current(self) -> float: + return 0. + + def set_ir_power(self, percent: int): + pass diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/hw.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/hw.py new file mode 100644 index 000000000..7263ddbfc --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/hw.py @@ -0,0 +1,137 @@ +import os +import platform +from pathlib import Path + +from iqpilot.system.hardware import PC + +DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" + +class Paths: + _persist_root_cache: str | None = None + + @staticmethod + def _is_writable_persist_root(path: str) -> bool: + try: + os.makedirs(path, exist_ok=True) + comma_dir = os.path.join(path, "comma") + os.makedirs(comma_dir, exist_ok=True) + + probe_path = os.path.join(comma_dir, ".rw_probe") + with open(probe_path, "w") as f: + f.write("1") + os.remove(probe_path) + return True + except OSError: + return False + + @staticmethod + def comma_home() -> str: + return os.path.join(str(Path.home()), ".comma" + os.environ.get("OPENPILOT_PREFIX", "")) + + @staticmethod + def params() -> str: + if os.environ.get("PARAMS_ROOT"): + return os.environ["PARAMS_ROOT"] + return os.path.join(Paths.comma_home(), "params") if PC else "/data/params" + + @staticmethod + def log_root() -> str: + if os.environ.get('LOG_ROOT', False): + return os.environ['LOG_ROOT'] + elif PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "realdata") + else: + return '/data/media/0/realdata/' + + @staticmethod + def log_root_external() -> str: + return '/mnt/external_realdata/' + + @staticmethod + def swaglog_root() -> str: + if PC: + return os.path.join(Paths.comma_home(), "log") + else: + return "/data/log/" + + @staticmethod + def swaglog_ipc() -> str: + return "ipc:///tmp/logmessage" + os.environ.get("OPENPILOT_PREFIX", "") + + @staticmethod + def download_cache_root() -> str: + if os.environ.get('COMMA_CACHE', False): + return os.environ['COMMA_CACHE'] + "/" + return DEFAULT_DOWNLOAD_CACHE_ROOT + os.environ.get("OPENPILOT_PREFIX", "") + "/" + + @staticmethod + def persist_root() -> str: + if PC: + return os.path.join(Paths.comma_home(), "persist") + + if Paths._persist_root_cache is not None: + return Paths._persist_root_cache + + for candidate in ("/persist", "/data/persist"): + if Paths._is_writable_persist_root(candidate): + Paths._persist_root_cache = candidate + return candidate + + # Keep previous behavior as a last resort. + Paths._persist_root_cache = "/persist" + return Paths._persist_root_cache + + @staticmethod + def stats_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "stats") + else: + return "/data/stats/" + + @staticmethod + def stats_iq_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "stats") + else: + return "/data/stats_iq/" + + @staticmethod + def config_root() -> str: + if PC: + return Paths.comma_home() + else: + return "/tmp/.comma" + + @staticmethod + def shm_path() -> str: + if PC and platform.system() == "Darwin": + return "/tmp" # This is not really shared memory on macOS, but it's the closest we can get + return "/dev/shm" + + @staticmethod + def model_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "models") + else: + return "/data/media/0/models" + + @staticmethod + def crash_log_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "community" / "crashes") + else: + return "/data/community/crashes" + + @staticmethod + def mapd_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "osm") + else: + return "/data/media/0/osm" + + @staticmethod + def screen_recordings_root() -> str: + if PC: + return str(Path(Paths.comma_home()) / "media" / "0" / "screen_recordings") + else: + return "/data/media/0/screen_recordings" diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py new file mode 100644 index 000000000..72ef2f235 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/pc/hardware.py @@ -0,0 +1,12 @@ +from iqpilot.cereal import log +from iqpilot.system.hardware.base import HardwareBase + +NetworkType = log.DeviceState.NetworkType + + +class Pc(HardwareBase): + def get_device_type(self): + return "pc" + + def get_network_type(self): + return NetworkType.wifi diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py new file mode 100644 index 000000000..48a34ea48 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/amplifier.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +import time +from collections import namedtuple + +from iqpilot.common.i2c import SMBus + +# https://datasheets.maximintegrated.com/en/ds/MAX98089.pdf + +AmpConfig = namedtuple('AmpConfig', ['name', 'value', 'register', 'offset', 'mask']) +EQParams = namedtuple('EQParams', ['K', 'k1', 'k2', 'c1', 'c2']) + + +def configs_from_eq_params(base, eq_params): + return [ + AmpConfig("K (high)", (eq_params.K >> 8), base, 0, 0xFF), + AmpConfig("K (low)", (eq_params.K & 0xFF), base + 1, 0, 0xFF), + AmpConfig("k1 (high)", (eq_params.k1 >> 8), base + 2, 0, 0xFF), + AmpConfig("k1 (low)", (eq_params.k1 & 0xFF), base + 3, 0, 0xFF), + AmpConfig("k2 (high)", (eq_params.k2 >> 8), base + 4, 0, 0xFF), + AmpConfig("k2 (low)", (eq_params.k2 & 0xFF), base + 5, 0, 0xFF), + AmpConfig("c1 (high)", (eq_params.c1 >> 8), base + 6, 0, 0xFF), + AmpConfig("c1 (low)", (eq_params.c1 & 0xFF), base + 7, 0, 0xFF), + AmpConfig("c2 (high)", (eq_params.c2 >> 8), base + 8, 0, 0xFF), + AmpConfig("c2 (low)", (eq_params.c2 & 0xFF), base + 9, 0, 0xFF), + ] + + +BASE_CONFIG = [ + AmpConfig("MCLK prescaler", 0b01, 0x10, 4, 0b00110000), + AmpConfig("PM: enable speakers", 0b11, 0x4D, 4, 0b00110000), + AmpConfig("PM: enable DACs", 0b11, 0x4D, 0, 0b00000011), + AmpConfig("Enable PLL1", 0b1, 0x12, 7, 0b10000000), + AmpConfig("Enable PLL2", 0b1, 0x1A, 7, 0b10000000), + AmpConfig("DAI1: I2S mode", 0b00100, 0x14, 2, 0b01111100), + AmpConfig("DAI2: I2S mode", 0b00100, 0x1C, 2, 0b01111100), + AmpConfig("DAI1 Passband filtering: music mode", 0b1, 0x18, 7, 0b10000000), + AmpConfig("DAI1 voice mode gain (DV1G)", 0b00, 0x2F, 4, 0b00110000), + AmpConfig("DAI1 attenuation (DV1)", 0x0, 0x2F, 0, 0b00001111), + AmpConfig("DAI2 attenuation (DV2)", 0x0, 0x31, 0, 0b00001111), + AmpConfig("DAI2: DC blocking", 0b1, 0x20, 0, 0b00000001), + AmpConfig("DAI2: High sample rate", 0b0, 0x20, 3, 0b00001000), + AmpConfig("ALC enable", 0b1, 0x43, 7, 0b10000000), + AmpConfig("ALC/excursion limiter release time", 0b101, 0x43, 4, 0b01110000), + AmpConfig("ALC multiband enable", 0b1, 0x43, 3, 0b00001000), + AmpConfig("DAI1 EQ enable", 0b0, 0x49, 0, 0b00000001), + AmpConfig("DAI2 EQ clip detection disabled", 0b1, 0x32, 4, 0b00010000), + AmpConfig("DAI2 EQ attenuation", 0x5, 0x32, 0, 0b00001111), + AmpConfig("Excursion limiter upper corner freq", 0b100, 0x41, 4, 0b01110000), + AmpConfig("Excursion limiter lower corner freq", 0b00, 0x41, 0, 0b00000011), + AmpConfig("Excursion limiter threshold", 0b000, 0x42, 0, 0b00001111), + AmpConfig("Distortion limit (THDCLP)", 0x6, 0x46, 4, 0b11110000), + AmpConfig("Distortion limiter release time constant", 0b0, 0x46, 0, 0b00000001), + AmpConfig("Right DAC input mixer: DAI1 left", 0b0, 0x22, 3, 0b00001000), + AmpConfig("Right DAC input mixer: DAI1 right", 0b0, 0x22, 2, 0b00000100), + AmpConfig("Right DAC input mixer: DAI2 left", 0b1, 0x22, 1, 0b00000010), + AmpConfig("Right DAC input mixer: DAI2 right", 0b0, 0x22, 0, 0b00000001), + AmpConfig("DAI1 audio port selector", 0b10, 0x16, 6, 0b11000000), + AmpConfig("DAI2 audio port selector", 0b01, 0x1E, 6, 0b11000000), + AmpConfig("Enable left digital microphone", 0b1, 0x48, 5, 0b00100000), + AmpConfig("Enable right digital microphone", 0b1, 0x48, 4, 0b00010000), + AmpConfig("Enhanced volume smoothing disabled", 0b0, 0x49, 7, 0b10000000), + AmpConfig("Volume adjustment smoothing disabled", 0b0, 0x49, 6, 0b01000000), + AmpConfig("Zero-crossing detection disabled", 0b0, 0x49, 5, 0b00100000), +] + +CONFIGS = { + "tici": [ + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111), + AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010), + *configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)), + *configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)), + *configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)), + *configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)), + *configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)), + ], + "tizi": [ + AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111), + AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), + AmpConfig("Left Speaker Mixer Gain", 0b00, 0x2D, 0, 0b00000011), + AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), + AmpConfig("Left speaker output volume", 0x17, 0x3D, 0, 0b00011111), + AmpConfig("Right speaker output volume", 0x17, 0x3E, 0, 0b00011111), + AmpConfig("DAI2 EQ enable", 0b0, 0x49, 1, 0b00000010), + AmpConfig("DAI2: DC blocking", 0b0, 0x20, 0, 0b00000001), + AmpConfig("ALC enable", 0b0, 0x43, 7, 0b10000000), + AmpConfig("DAI2 EQ attenuation", 0x2, 0x32, 0, 0b00001111), + AmpConfig("Excursion limiter upper corner freq", 0b001, 0x41, 4, 0b01110000), + AmpConfig("Excursion limiter threshold", 0b100, 0x42, 0, 0b00001111), + AmpConfig("Distortion limit (THDCLP)", 0x0, 0x46, 4, 0b11110000), + AmpConfig("Distortion limiter release time constant", 0b1, 0x46, 0, 0b00000001), + AmpConfig("Left DAC input mixer: DAI1 left", 0b0, 0x22, 7, 0b10000000), + AmpConfig("Left DAC input mixer: DAI1 right", 0b0, 0x22, 6, 0b01000000), + AmpConfig("Left DAC input mixer: DAI2 left", 0b1, 0x22, 5, 0b00100000), + AmpConfig("Left DAC input mixer: DAI2 right", 0b0, 0x22, 4, 0b00010000), + AmpConfig("Right DAC input mixer: DAI2 left", 0b0, 0x22, 1, 0b00000010), + AmpConfig("Right DAC input mixer: DAI2 right", 0b1, 0x22, 0, 0b00000001), + AmpConfig("Volume adjustment smoothing disabled", 0b1, 0x49, 6, 0b01000000), + ], +} + + +class Amplifier: + AMP_I2C_BUS = 0 + AMP_ADDRESS = 0x10 + + def __init__(self, debug=False): + self.debug = debug + + def _get_shutdown_config(self, amp_disabled: bool) -> AmpConfig: + return AmpConfig("Global shutdown", 0b0 if amp_disabled else 0b1, 0x51, 7, 0b10000000) + + def _set_configs(self, configs: list[AmpConfig]) -> None: + with SMBus(self.AMP_I2C_BUS) as bus: + for config in configs: + if self.debug: + print(f"Setting \"{config.name}\" to {config.value}:") + + old_value = bus.read_byte_data(self.AMP_ADDRESS, config.register, force=True) + new_value = (old_value & (~config.mask)) | ((config.value << config.offset) & config.mask) + bus.write_byte_data(self.AMP_ADDRESS, config.register, new_value, force=True) + + if self.debug: + print(f" Changed {hex(config.register)}: {hex(old_value)} -> {hex(new_value)}") + + def set_configs(self, configs: list[AmpConfig]) -> bool: + tries = 15 + backoff = 0. + for i in range(tries): + try: + self._set_configs(configs) + return True + except OSError: + backoff += 0.1 + time.sleep(backoff) + print(f"Failed to set amp config, {tries - i - 1} retries left") + return False + + def set_global_shutdown(self, amp_disabled: bool) -> bool: + return self.set_configs([self._get_shutdown_config(amp_disabled), ]) + + def initialize_configuration(self, model: str) -> bool: + cfgs = [ + self._get_shutdown_config(True), + *BASE_CONFIG, + *CONFIGS[model], + self._get_shutdown_config(False), + ] + return self.set_configs(cfgs) + + +if __name__ == "__main__": + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().strip('\x00') + model = model.split('comma ')[-1] + + amp = Amplifier() + amp.initialize_configuration(model) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py new file mode 100644 index 000000000..b9fa7fbff --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/hardware.py @@ -0,0 +1,740 @@ +import math +import os +import sys +import subprocess +import time +import tempfile +from enum import IntEnum +from functools import cached_property, lru_cache +from pathlib import Path + +from iqpilot.cereal import log +from iqpilot.common.utils import sudo_read, sudo_write +from iqpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action +from iqpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone +from iqpilot.system.hardware.tici import iwlist +from iqpilot.system.hardware.tici.lpa import TiciLPA +from iqpilot.system.hardware.tici.pins import GPIO +from iqpilot.system.hardware.tici.amplifier import Amplifier + +NM = 'org.freedesktop.NetworkManager' +NM_CON_ACT = NM + '.Connection.Active' +NM_DEV = NM + '.Device' +NM_DEV_WL = NM + '.Device.Wireless' +NM_DEV_STATS = NM + '.Device.Statistics' +NM_AP = NM + '.AccessPoint' +DBUS_PROPS = 'org.freedesktop.DBus.Properties' + +MM = 'org.freedesktop.ModemManager1' +MM_MODEM = MM + ".Modem" +MM_MODEM_SIMPLE = MM + ".Modem.Simple" +MM_SIM = MM + ".Sim" + +class MM_MODEM_STATE(IntEnum): + FAILED = -1 + UNKNOWN = 0 + INITIALIZING = 1 + LOCKED = 2 + DISABLED = 3 + DISABLING = 4 + ENABLING = 5 + ENABLED = 6 + SEARCHING = 7 + REGISTERED = 8 + DISCONNECTING = 9 + CONNECTING = 10 + CONNECTED = 11 + +class NMActiveConnectionState(IntEnum): + UNKNOWN = 0 + ACTIVATING = 1 + ACTIVATED = 2 + DEACTIVATING = 3 + DEACTIVATED = 4 + +class NMMetered(IntEnum): + NM_METERED_UNKNOWN = 0 + NM_METERED_YES = 1 + NM_METERED_NO = 2 + NM_METERED_GUESS_YES = 3 + NM_METERED_GUESS_NO = 4 + +TIMEOUT = 0.1 +REFRESH_RATE_MS = 1000 + +NetworkType = log.DeviceState.NetworkType +NetworkStrength = log.DeviceState.NetworkStrength + +# https://developer.gnome.org/ModemManager/unstable/ModemManager-Flags-and-Enumerations.html#MMModemAccessTechnology +MM_MODEM_ACCESS_TECHNOLOGY_UMTS = 1 << 5 +MM_MODEM_ACCESS_TECHNOLOGY_LTE = 1 << 14 + +# MMModemStateFailedReason +MM_MODEM_STATE_FAILED_REASON_SIM_MISSING = 2 + + +def affine_irq(val, action): + irqs = get_irqs_for_action(action) + if len(irqs) == 0: + return + + for i in irqs: + sudo_write(str(val), f"/proc/irq/{i}/smp_affinity_list") + +@lru_cache +def get_device_type(): + # lru_cache and cache can cause memory leaks when used in classes + try: + with open("/sys/firmware/devicetree/base/model") as f: + model = f.read().strip('\x00') + except FileNotFoundError: + # off-device (e.g. the prebuilt build container fakes /TICI but has no + # devicetree); import must not crash. Not a real device type. + return "unknown" + return model.split('comma ')[-1] + +class Tici(HardwareBase): + @staticmethod + def _ensure_system_python_path() -> None: + system_site = "/usr/lib/python3/dist-packages" + if system_site not in sys.path and os.path.isdir(system_site): + sys.path.append(system_site) + + @staticmethod + def _run_direct_modem_command(command: str) -> None: + import serial + + last_error: Exception | None = None + for device in ("/dev/ttyUSB2", "/dev/ttyUSB3"): + if not os.path.exists(device): + continue + + try: + with serial.Serial(device, baudrate=9600, timeout=2) as modem: + modem.reset_input_buffer() + modem.write((command + "\r").encode("ascii")) + + deadline = time.monotonic() + 3.0 + while time.monotonic() < deadline: + line = modem.readline().decode(errors="ignore").strip() + if not line: + continue + if line == "OK": + return + if line == "ERROR" or "ERROR" in line: + raise RuntimeError(f"{device}: {line}") + raise TimeoutError(f"{device}: timed out waiting for modem response") + except Exception as e: + last_error = e + + if last_error is not None: + raise last_error + raise RuntimeError("No modem AT port available") + + @cached_property + def bus(self): + try: + import dbus + except ModuleNotFoundError: + self._ensure_system_python_path() + import dbus + return dbus.SystemBus() + + @cached_property + def nm(self): + return self.bus.get_object(NM, '/org/freedesktop/NetworkManager') + + @property # this should not be cached, in case the modemmanager restarts + def mm(self): + return self.bus.get_object(MM, '/org/freedesktop/ModemManager1') + + @cached_property + def amplifier(self): + if self.get_device_type() == "mici": + return None + if os.path.exists('/tmp/lite_hw') or os.environ.get('LITE') == '1': + return None + return Amplifier() + + def get_os_version(self): + with open("/VERSION") as f: + return f.read().strip() + + def get_device_type(self): + return get_device_type() + + def reboot(self, reason=None): + subprocess.check_output(["sudo", "reboot"]) + + def uninstall(self): + Path("/data/__system_reset__").touch() + os.sync() + self.reboot() + + def get_serial(self): + return self.get_cmdline()['androidboot.serialno'] + + def get_voltage(self): + with open("/sys/class/hwmon/hwmon1/in1_input") as f: + return int(f.read()) + + def get_current(self): + with open("/sys/class/hwmon/hwmon1/curr1_input") as f: + return int(f.read()) + + def set_ir_power(self, percent: int): + if self.get_device_type() in ("tici", "tizi"): + return + + value = int((percent / 100) * 300) + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write("0\n") + with open("/sys/class/leds/led:torch_2/brightness", "w") as f: + f.write(f"{value}\n") + with open("/sys/class/leds/led:switch_2/brightness", "w") as f: + f.write(f"{value}\n") + + def get_network_type(self): + try: + primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + primary_connection = self.bus.get_object(NM, primary_connection) + primary_type = primary_connection.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if primary_type == '802-3-ethernet': + return NetworkType.ethernet + elif primary_type == '802-11-wireless': + return NetworkType.wifi + else: + active_connections = self.nm.Get(NM, 'ActiveConnections', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + for conn in active_connections: + c = self.bus.get_object(NM, conn) + tp = c.Get(NM_CON_ACT, 'Type', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if tp == 'gsm': + modem = self.get_modem() + modem_state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if modem_state < MM_MODEM_STATE.REGISTERED: + return NetworkType.none + access_t = modem.Get(MM_MODEM, 'AccessTechnologies', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if access_t >= MM_MODEM_ACCESS_TECHNOLOGY_LTE: + return NetworkType.cell4G + elif access_t >= MM_MODEM_ACCESS_TECHNOLOGY_UMTS: + return NetworkType.cell3G + else: + return NetworkType.cell2G + except Exception: + pass + + return NetworkType.none + + def get_modem(self): + objects = self.mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=TIMEOUT) + if not objects: + raise RuntimeError("ModemManager returned no modems") + modem_path = next(iter(objects)) + return self.bus.get_object(MM, modem_path) + + def get_wlan(self): + wlan_path = self.nm.GetDeviceByIpIface('wlan0', dbus_interface=NM, timeout=TIMEOUT) + return self.bus.get_object(NM, wlan_path) + + def get_wwan(self): + wwan_path = self.nm.GetDeviceByIpIface('wwan0', dbus_interface=NM, timeout=TIMEOUT) + return self.bus.get_object(NM, wwan_path) + + def get_sim_info(self): + modem = self.get_modem() + sim_path = modem.Get(MM_MODEM, 'Sim', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if sim_path == "/": + return { + 'sim_id': '', + 'mcc_mnc': None, + 'network_type': ["Unknown"], + 'sim_state': ["ABSENT"], + 'data_connected': False + } + else: + sim = self.bus.get_object(MM, sim_path) + return { + 'sim_id': str(sim.Get(MM_SIM, 'SimIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)), + 'mcc_mnc': str(sim.Get(MM_SIM, 'OperatorIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)), + 'network_type': ["Unknown"], + 'sim_state': ["READY"], + 'data_connected': modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) == MM_MODEM_STATE.CONNECTED, + } + + def get_sim_lpa(self) -> LPABase: + return TiciLPA() + + def get_imei(self, slot): + if slot != 0: + return "" + + return str(self.get_modem().Get(MM_MODEM, 'EquipmentIdentifier', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + + def get_network_info(self): + if self.get_device_type() == "mici": + return None + try: + modem = self.get_modem() + info = modem.Command("AT+QNWINFO", math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + extra = modem.Command('AT+QENG="servingcell"', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + except Exception: + return None + + if info and info.startswith('+QNWINFO: '): + info = info.replace('+QNWINFO: ', '').replace('"', '').split(',') + extra = "" if extra is None else extra.replace('+QENG: "servingcell",', '').replace('"', '') + state = "" if state is None else MM_MODEM_STATE(state).name + + if len(info) != 4: + return None + + technology, operator, band, channel = info + + return({ + 'technology': technology, + 'operator': operator, + 'band': band, + 'channel': int(channel), + 'extra': extra, + 'state': state, + }) + else: + return None + + def parse_strength(self, percentage): + if percentage < 25: + return NetworkStrength.poor + elif percentage < 50: + return NetworkStrength.moderate + elif percentage < 75: + return NetworkStrength.good + else: + return NetworkStrength.great + + def get_network_strength(self, network_type): + network_strength = NetworkStrength.unknown + + try: + if network_type == NetworkType.none: + pass + elif network_type == NetworkType.wifi: + wlan = self.get_wlan() + active_ap_path = wlan.Get(NM_DEV_WL, 'ActiveAccessPoint', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if active_ap_path != "/": + active_ap = self.bus.get_object(NM, active_ap_path) + strength = int(active_ap.Get(NM_AP, 'Strength', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + network_strength = self.parse_strength(strength) + else: # Cellular + modem = self.get_modem() + strength = int(modem.Get(MM_MODEM, 'SignalQuality', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)[0]) + network_strength = self.parse_strength(strength) + except Exception: + pass + + return network_strength + + def get_network_metered(self, network_type) -> bool: + try: + primary_connection = self.nm.Get(NM, 'PrimaryConnection', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + primary_connection = self.bus.get_object(NM, primary_connection) + primary_devices = primary_connection.Get(NM_CON_ACT, 'Devices', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + for dev in primary_devices: + dev_obj = self.bus.get_object(NM, str(dev)) + metered_prop = dev_obj.Get(NM_DEV, 'Metered', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + if network_type == NetworkType.wifi: + if metered_prop in [NMMetered.NM_METERED_YES, NMMetered.NM_METERED_GUESS_YES]: + return True + elif network_type in [NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G]: + if metered_prop == NMMetered.NM_METERED_NO: + return False + except Exception: + pass + + return super().get_network_metered(network_type) + + def get_modem_version(self): + try: + modem = self.get_modem() + return modem.Get(MM_MODEM, 'Revision', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + except Exception: + return None + + def get_modem_temperatures(self): + timeout = 0.2 # Default timeout is too short + try: + modem = self.get_modem() + temps = modem.Command("AT+QTEMP", math.ceil(timeout), dbus_interface=MM_MODEM, timeout=timeout) + return list(filter(lambda t: t != 255, map(int, temps.split(' ')[1].split(',')))) + except Exception: + return [] + + + 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): + os.system("sudo poweroff") + + def get_thermal_config(self): + intake, exhaust, case = None, None, None + if self.get_device_type() == "mici": + case = ThermalZone("case") + intake = ThermalZone("intake") + exhaust = ThermalZone("exhaust") + return ThermalConfig(cpu=[ThermalZone(f"cpu{i}-silver-usr") for i in range(4)] + + [ThermalZone(f"cpu{i}-gold-usr") for i in range(4)], + gpu=[ThermalZone("gpu0-usr"), ThermalZone("gpu1-usr")], + dsp=ThermalZone("compute-hvx-usr"), + memory=ThermalZone("ddr-usr"), + pmic=[ThermalZone("pm8998_tz"), ThermalZone("pm8005_tz")], + intake=intake, + exhaust=exhaust, + case=case) + + def set_display_power(self, on): + try: + with open("/sys/class/backlight/panel0-backlight/bl_power", "w") as f: + f.write("0" if on else "4") + except Exception: + pass + + def set_screen_brightness(self, percentage): + try: + with open("/sys/class/backlight/panel0-backlight/max_brightness") as f: + max_brightness = float(f.read().strip()) + + val = int(percentage * (max_brightness / 100.)) + with open("/sys/class/backlight/panel0-backlight/brightness", "w") as f: + f.write(str(val)) + except Exception: + pass + + def get_screen_brightness(self): + try: + with open("/sys/class/backlight/panel0-backlight/max_brightness") as f: + max_brightness = float(f.read().strip()) + + with open("/sys/class/backlight/panel0-backlight/brightness") as f: + return int(float(f.read()) / (max_brightness / 100.)) + except Exception: + return 0 + + def set_power_save(self, powersave_enabled): + # amplifier, 100mW at idle + if self.amplifier is not None: + self.amplifier.set_global_shutdown(amp_disabled=powersave_enabled) + if not powersave_enabled: + self.amplifier.initialize_configuration(self.get_device_type()) + + # *** CPU config *** + + # offline big cluster + for i in range(4, 8): + val = '0' if powersave_enabled else '1' + sudo_write(val, f'/sys/devices/system/cpu/cpu{i}/online') + + for n in ('0', '4'): + if powersave_enabled and n == '4': + continue + gov = 'ondemand' if powersave_enabled else 'performance' + sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor') + + # *** IRQ config *** + + # GPU, modeld core + affine_irq(7, "kgsl-3d0") + + # camerad core + camera_irqs = ("a5", "cci", "cpas_camnoc", "cpas-cdm", "csid", "ife", "csid-lite", "ife-lite") + for n in camera_irqs: + affine_irq(6, n) + + def get_gpu_usage_percent(self): + try: + with open('/sys/class/kgsl/kgsl-3d0/gpubusy') as f: + used, total = f.read().strip().split() + return 100.0 * int(used) / int(total) + except Exception: + return 0 + + def initialize_hardware(self): + if self.amplifier is not None: + self.amplifier.initialize_configuration(self.get_device_type()) + + # Allow hardwared to write engagement status to kmsg + os.system("sudo chmod a+w /dev/kmsg") + + # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL + gpio_init(GPIO.SOM_ST_IO, True) + gpio_set(GPIO.SOM_ST_IO, 1) + + # *** IRQ config *** + + # mask off big cluster from default affinity + sudo_write("f", "/proc/irq/default_smp_affinity") + + # move these off the default core + affine_irq(1, "msm_vidc") # encoders + affine_irq(1, "i2c_geni") # sensors + + # *** GPU config *** + # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 + affine_irq(5, "fts_ts") # touch + affine_irq(5, "msm_drm") # display + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on") + sudo_write("1000", "/sys/class/kgsl/kgsl-3d0/idle_timer") + sudo_write("performance", "/sys/class/kgsl/kgsl-3d0/devfreq/governor") + sudo_write("710", "/sys/class/kgsl/kgsl-3d0/max_clock_mhz") + + # setup governors + sudo_write("performance", "/sys/class/devfreq/soc:qcom,cpubw/governor") + sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu0/governor") + sudo_write("performance", "/sys/class/devfreq/soc:qcom,memlat-cpu4/governor") + + # *** VIDC (encoder) config *** + sudo_write("N", "/sys/kernel/debug/msm_vidc/clock_scaling") + sudo_write("Y", "/sys/kernel/debug/msm_vidc/disable_thermal_mitigation") + + # pandad core + affine_irq(3, "spi_geni") # SPI + if "tici" in self.get_device_type(): + affine_irq(3, "xhci-hcd:usb3") + affine_irq(3, "xhci-hcd:usb1") + try: + pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() + subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) + subprocess.call(["sudo", "taskset", "-pc", "3", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessException as e: + print(str(e)) + + def configure_modem(self): + from iqpilot.common.params import Params + + sim_info = self.get_sim_info() + sim_id = sim_info.get('sim_id', '') + params = Params() + manual_apn = params.get("GsmApn", encoding="utf-8") or "" + metered_enabled = params.get_bool("GsmMetered") + + modem = self.get_modem() + try: + manufacturer = str(modem.Get(MM_MODEM, 'Manufacturer', dbus_interface=DBUS_PROPS, timeout=TIMEOUT)) + except Exception: + manufacturer = None + + cmds = [] + is_comma_profile = self.get_sim_lpa().is_comma_profile(sim_id) + roaming_enabled = params.get_bool("GsmRoaming") + initial_eps_apn = "" if is_comma_profile else manual_apn + + if not is_comma_profile and params.get("GsmRoaming") is None: + params.put_bool("GsmRoaming", True) + roaming_enabled = True + + subprocess.call([ + "nmcli", "connection", "modify", "lte", + "gsm.auto-config", "no" if manual_apn else "yes", + "gsm.apn", manual_apn, + "gsm.home-only", "no" if roaming_enabled else "yes", + "gsm.network-id", "", + "gsm.initial-eps-bearer-configure", "yes" if initial_eps_apn else "no", + "gsm.initial-eps-bearer-apn", initial_eps_apn, + "connection.metered", "unknown" if metered_enabled else "no", + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + if self.get_device_type() in ("tici", "tizi"): + if initial_eps_apn: + subprocess.call(["mmcli", "-m", "any", f'--3gpp-set-initial-eps-bearer-settings=apn={initial_eps_apn}']) + else: + subprocess.call(["mmcli", "-m", "any", '--3gpp-set-initial-eps-bearer-settings=apn=']) + + cmds += [ + # configure modem as data-centric + 'AT+QNVW=5280,0,"0102000000000000"', + 'AT+QNVFW="/nv/item_files/ims/IMS_enable",00', + 'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01', + ] + if self.get_device_type() == "tizi": + cmds += [ + 'AT+QSIMDET=1,0', + 'AT+QSIMSTAT=1', + ] + elif manufacturer == 'Cavli Inc.': + cmds += [ + 'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM + 'AT$QCSIMSLEEP=0', # disable SIM sleep + 'AT$QCSIMCFG=SimPowerSave,0', # more sleep disable + + # ethernet config + 'AT$QCPCFG=usbNet,0', + 'AT$QCNETDEVCTL=3,1', + ] + else: + # this modem gets upset with too many AT commands + if sim_id is None or len(sim_id) == 0: + cmds += [ + # SIM sleep disable + 'AT$QCSIMSLEEP=0', + 'AT$QCSIMCFG=SimPowerSave,0', + + # ethernet config + 'AT$QCPCFG=usbNet,1', + ] + + for cmd in cmds: + try: + modem.Command(cmd, math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + except Exception: + pass + + # eSIM prime + dest = "/etc/NetworkManager/system-connections/esim.nmconnection" + if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest): + with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf: + dat = f.read() + dat = dat.replace("sim-id=", f"sim-id={sim_id}") + tf.write(dat) + tf.flush() + + # needs to be root + os.system(f"sudo cp {tf.name} {dest}") + os.system(f"sudo nmcli con load {dest}") + + def recover_sim_detection(self) -> bool: + # A worn SIM-tray presence switch can read "removed" while the SIM pads still make + # contact; with hot-swap detect armed (AT+QSIMDET=1) the modem never powers the SIM + # and lands in failed/sim-missing. Disabling detect and rebooting the modem makes it + # probe the SIM electrically. Safe to retry on failure: firing disarms the QSIMDET + # gate, so a genuinely SIM-less device gets at most one extra modem reboot per boot. + if self.get_device_type() not in ("tici", "tizi"): + return False + + try: + modem = self.get_modem() + state = modem.Get(MM_MODEM, 'State', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if state != MM_MODEM_STATE.FAILED: + return False + reason = modem.Get(MM_MODEM, 'StateFailedReason', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if reason != MM_MODEM_STATE_FAILED_REASON_SIM_MISSING: + return False + detect = str(modem.Command('AT+QSIMDET?', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT)).strip() + if not detect.startswith('+QSIMDET: 1'): + return False + modem.Command('AT+QSIMDET=0,0', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + modem.Command('AT+CFUN=1,1', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + return True + except Exception: + return False + + def reboot_modem(self): + modem = None + try: + modem = self.get_modem() + except Exception: + pass + + if modem is not None: + for state in (0, 1): + try: + modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + except Exception: + pass + return + + for state in (0, 1): + try: + self._run_direct_modem_command(f"AT+CFUN={state}") + except Exception: + pass + + def get_networks(self): + r = {} + + wlan = iwlist.scan() + if wlan is not None: + r['wlan'] = wlan + + lte_info = self.get_network_info() + if lte_info is not None: + extra = lte_info['extra'] + + # ,"LTE",,,,,,,, + # ,,,,,,, + if 'LTE' in extra: + extra = extra.split(',') + try: + r['lte'] = [{ + "mcc": int(extra[3]), + "mnc": int(extra[4]), + "cid": int(extra[5], 16), + "nmr": [{"pci": int(extra[6]), "earfcn": int(extra[7])}], + }] + except (ValueError, IndexError): + pass + + return r + + def get_modem_data_usage(self): + try: + wwan = self.get_wwan() + + # Ensure refresh rate is set so values don't go stale + refresh_rate = wwan.Get(NM_DEV_STATS, 'RefreshRateMs', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + if refresh_rate != REFRESH_RATE_MS: + u = type(refresh_rate) + wwan.Set(NM_DEV_STATS, 'RefreshRateMs', u(REFRESH_RATE_MS), dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + + tx = wwan.Get(NM_DEV_STATS, 'TxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + rx = wwan.Get(NM_DEV_STATS, 'RxBytes', dbus_interface=DBUS_PROPS, timeout=TIMEOUT) + return int(tx), int(rx) + except Exception: + return -1, -1 + + def has_internal_panda(self): + return True + + def reset_internal_panda(self): + gpio_init(GPIO.STM_RST_N, True) + gpio_init(GPIO.STM_BOOT0, True) + + gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_BOOT0, 0) + time.sleep(1) + gpio_set(GPIO.STM_RST_N, 0) + + def recover_internal_panda(self): + gpio_init(GPIO.STM_RST_N, True) + gpio_init(GPIO.STM_BOOT0, True) + + gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_BOOT0, 1) + time.sleep(0.5) + gpio_set(GPIO.STM_RST_N, 0) + time.sleep(0.5) + gpio_set(GPIO.STM_BOOT0, 0) + + def booted(self): + # this normally boots within 8s, but on rare occasions takes 30+s + encoder_state = sudo_read("/sys/kernel/debug/msm_vidc/core0/info") + if "Core state: 0" in encoder_state and (time.monotonic() < 60*2): + return False + return True + +if __name__ == "__main__": + t = Tici() + t.configure_modem() + t.initialize_hardware() + t.set_power_save(False) + print(t.get_sim_info()) diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py new file mode 100644 index 000000000..1e7c428b4 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/iwlist.py @@ -0,0 +1,35 @@ +import subprocess + + +def scan(interface="wlan0"): + result = [] + try: + r = subprocess.check_output(["iwlist", interface, "scan"], encoding='utf8') + + mac = None + for line in r.split('\n'): + if "Address" in line: + # Based on the adapter eithere a percentage or dBm is returned + # Add previous network in case no dBm signal level was seen + if mac is not None: + result.append({"mac": mac}) + mac = None + + mac = line.split(' ')[-1] + elif "dBm" in line: + try: + level = line.split('Signal level=')[1] + rss = int(level.split(' ')[0]) + result.append({"mac": mac, "rss": rss}) + mac = None + except ValueError: + continue + + # Add last network if no dBm was found + if mac is not None: + result.append({"mac": mac}) + + return result + + except Exception: + return None diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py new file mode 100644 index 000000000..a636742d5 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/lpa.py @@ -0,0 +1,1482 @@ +#!/usr/bin/env python3 +# SGP.22 v2.3: https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2021/07/SGP.22-v2.3.pdf + +import argparse +import atexit +import base64 +import hashlib +import json +import math +import os +import re +import requests +import serial +import subprocess +import sys +import time + +from collections.abc import Generator +from contextlib import contextmanager +from pathlib import Path +from typing import Literal +from iqpilot.common.time_helpers import system_time_valid +from iqpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile + +GSMA_CI_BUNDLE = str(Path(__file__).parent / 'gsma_ci_bundle.pem') + + +DEFAULT_DEVICE = "/dev/modem_at0" +DEFAULT_BAUD = 9600 +DEFAULT_TIMEOUT = 5.0 +# https://euicc-manual.osmocom.org/docs/lpa/applet-id/ +ISDR_AID = "A0000005591010FFFFFFFF8900000100" +ES10X_MSS = 120 +MM = "org.freedesktop.ModemManager1" +MM_MODEM = MM + ".Modem" + +# TLV Tags +TAG_ICCID = 0x5A +TAG_STATUS = 0x80 +TAG_EUICC_INFO = 0xBF20 +TAG_PREPARE_DOWNLOAD = 0xBF21 +TAG_PROFILE_INFO_LIST = 0xBF2D +TAG_EUICC_CHALLENGE = 0xBF2E +TAG_SET_NICKNAME = 0xBF29 +TAG_LIST_NOTIFICATION = 0xBF28 +TAG_RETRIEVE_NOTIFICATION = 0xBF2B +TAG_NOTIFICATION_METADATA = 0xBF2F +TAG_NOTIFICATION_SENT = 0xBF30 +TAG_ENABLE_PROFILE = 0xBF31 +TAG_DISABLE_PROFILE = 0xBF32 +TAG_DELETE_PROFILE = 0xBF33 +TAG_BPP = 0xBF36 +TAG_PROFILE_INSTALL_RESULT = 0xBF37 +TAG_AUTH_SERVER = 0xBF38 +TAG_CANCEL_SESSION = 0xBF41 + +STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"} +ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"} +CLASS_LABELS = {0: "test", 1: "provisioning", 2: "operational", 255: "unknown"} +PROFILE_ERROR_CODES = { + 0x01: "iccidOrAidNotFound", 0x02: "profileNotInDisabledState", + 0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling", + 0x05: "catBusy", 0x06: "undefinedError", +} +AUTH_SERVER_ERROR_CODES = { + 0x01: "eUICCVerificationFailed", 0x02: "eUICCCertificateExpired", + 0x03: "eUICCCertificateRevoked", 0x05: "invalidServerSignature", + 0x06: "euiccCiPKUnknown", 0x0A: "matchingIdRefused", + 0x10: "insufficientMemory", +} +BPP_COMMAND_NAMES = { + 0: "initialiseSecureChannel", 1: "configureISDP", 2: "storeMetadata", + 3: "storeMetadata2", 4: "replaceSessionKeys", 5: "loadProfileElements", +} +BPP_ERROR_REASONS = { + 1: "incorrectInputValues", 2: "invalidSignature", 3: "invalidTransactionId", + 4: "unsupportedCrtValues", 5: "unsupportedRemoteOperationType", + 6: "unsupportedProfileClass", 7: "scp03tStructureError", 8: "scp03tSecurityError", + 9: "iccidAlreadyExistsOnEuicc", 10: "insufficientMemoryForProfile", + 11: "installInterrupted", 12: "peProcessingError", 13: "dataMismatch", + 14: "invalidNAA", +} +CANCEL_SESSION_REASON = { + 0: "endUserRejection", 1: "postponed", 2: "timeout", + 3: "pprNotAllowed", 127: "undefinedReason", +} + +ESIM_TLS_BUNDLE_ENV = "IQPILOT_ESIM_CA_BUNDLE" +ESIM_QESIM_INIT_ENV = "IQPILOT_ESIM_QESIM_INIT" +ESIM_EXCLUSIVE_MODEM_ENV = "IQPILOT_ESIM_EXCLUSIVE_MODEM" + + +def _ensure_dbus_import(): + try: + import dbus # noqa: F401 + return + except ModuleNotFoundError: + dist_packages = "/usr/lib/python3/dist-packages" + if dist_packages not in sys.path and os.path.isdir(dist_packages): + sys.path.append(dist_packages) + import dbus # noqa: F401 + + +def _tls_verify_option() -> bool | str: + # Default to the GSMA CI bundle used by the reference eSIM branch. + # Allow explicit override for local testing or forced system trust behavior. + override = os.getenv(ESIM_TLS_BUNDLE_ENV, "").strip() + if not override: + return GSMA_CI_BUNDLE + if override.lower() in ("0", "false", "off"): + return False + if override.lower() in ("1", "true", "on", "system"): + return True + return override + + +def b64e(data: bytes) -> str: + return base64.b64encode(data).decode("ascii") + + +def b64d(s: str) -> bytes: + return base64.b64decode(base64_trim(s)) + + +def _decode_apdu_response_blob(raw: str, label: str) -> bytes: + filtered = re.sub(r"[^0-9A-Fa-f]", "", raw) + if len(filtered) < 2 or len(filtered) % 2: + raise RuntimeError(f"Malformed {label} response payload") + return bytes.fromhex(filtered) + + +class AtClient: + def __init__(self, device: str, baud: int, timeout: float, verbose: bool) -> None: + self._device = device + self._baud = baud + self._timeout = timeout + self.verbose = verbose + self.channel: str | None = None + self._serial: serial.Serial | None = None + self._use_csim = False + self._has_qesim = False + self._qesim_lpa_enabled = False + try: + self._serial = serial.Serial(device, baudrate=baud, timeout=timeout) + self._disable_echo() + except (serial.SerialException, PermissionError, OSError): + self._serial = None + if self.verbose: + print(f"!! using ModemManager DBus AT transport for {device}", file=sys.stderr) + + def close(self) -> None: + try: + if self.channel: + try: + self.query(f"AT+CCHC={self.channel}") + except Exception as e: + if self.verbose: + print(f"!! ignoring channel close failure: {e}", file=sys.stderr) + finally: + self.channel = None + finally: + if self._serial is not None: + self._serial.close() + + @staticmethod + def _is_busy_open_error(error: Exception) -> bool: + text = str(error).lower() + return "errno 16" in text or "device or resource busy" in text + + def _disable_echo(self) -> None: + if self._serial is None: + return + self._serial.reset_input_buffer() + self._serial.write(b"ATE0\r") + time.sleep(0.1) + self._serial.reset_input_buffer() + + def _send(self, cmd: str) -> None: + if self.verbose: + print(f">> {cmd}", file=sys.stderr) + assert self._serial is not None + self._serial.write((cmd + "\r").encode("ascii")) + + def _expect(self) -> list[str]: + lines: list[str] = [] + while True: + assert self._serial is not None + raw = self._serial.readline() + if not raw: + raise TimeoutError("AT command timed out") + line = raw.decode(errors="ignore").strip() + if not line: + continue + if self.verbose: + print(f"<< {line}", file=sys.stderr) + if line == "OK": + return lines + if line == "ERROR": + raise RuntimeError("AT command failed") + if "ERROR" in line and line.startswith("+"): + # Catch +CME ERROR / +CMS ERROR and similar modem-specific failures. + raise RuntimeError(f"AT command failed: {line}") + lines.append(line) + + def _get_modem(self): + _ensure_dbus_import() + import dbus + + bus = dbus.SystemBus() + mm = bus.get_object(MM, '/org/freedesktop/ModemManager1') + objects = mm.GetManagedObjects(dbus_interface="org.freedesktop.DBus.ObjectManager", timeout=self._timeout) + modem_paths = [path for path in objects.keys() if "/Modem/" in str(path)] + if not modem_paths: + raise RuntimeError("ModemManager returned no modems") + return bus.get_object(MM, modem_paths[0]) + + def _dbus_query(self, cmd: str) -> list[str]: + if self.verbose: + print(f"DBUS >> {cmd}", file=sys.stderr) + try: + result = str(self._get_modem().Command(cmd, math.ceil(self._timeout), dbus_interface=MM_MODEM, timeout=self._timeout)) + except Exception as e: + raise RuntimeError(f"AT command failed: {e}") from e + lines = [line.strip() for line in result.splitlines() if line.strip()] + if self.verbose: + for line in lines: + print(f"DBUS << {line}", file=sys.stderr) + return lines + + def _reconnect_serial(self) -> None: + self.channel = None + try: + if self._serial is not None: + self._serial.close() + except Exception: + pass + self._serial = serial.Serial(self._device, baudrate=self._baud, timeout=self._timeout) + self._disable_echo() + + def query(self, cmd: str) -> list[str]: + if self._serial is None: + return self._dbus_query(cmd) + try: + self._send(cmd) + return self._expect() + except (OSError, serial.SerialException) as e: + try: + self._reconnect_serial() + self._send(cmd) + return self._expect() + except Exception: + raise RuntimeError(f"AT transport error ({cmd}): {e}") from e + except TimeoutError as e: + raise TimeoutError(f"AT command timed out ({cmd})") from e + except RuntimeError as e: + raise RuntimeError(f"AT command failed ({cmd}): {e}") from e + + def _supports_qesim(self) -> bool: + try: + lines = self.query("AT+QESIM=?") + except Exception: + return False + return any(line.startswith("+QESIM:") for line in lines) + + def _set_quectel_lpa_mode(self) -> None: + self._has_qesim = self._supports_qesim() + self._qesim_lpa_enabled = False + if not self._has_qesim: + return + + # Some Quectel firmwares require enabling internal LPA mode before + # ISO7816 channel open commands (CCHO/CGLA) will succeed. + lpa_enable_cmds = ( + 'AT+QESIM="lpa_enable",1', + 'AT+QESIM="lpa_enable"', + ) + for cmd in lpa_enable_cmds: + try: + self.query(cmd) + self._qesim_lpa_enabled = True + break + except Exception: + continue + + # Keep BIP auth enabled when supported; this is needed by some firmware + # revisions for profile download/auth paths. + try: + lines = self.query('AT+QCFG="bip/auth"?') + needs_enable = any('"bip/auth",0' in line for line in lines) + if needs_enable: + self.query('AT+QCFG="bip/auth",1') + except Exception: + pass + + def ensure_capabilities(self) -> None: + if self._serial is None: + return + + # Some modem firmwares reject command test forms (e.g. AT+CCHO=?) + # while still supporting real execution. Keep these probes non-fatal. + self.query("AT") + # Keep IQ-specific QESIM initialization opt-in for debugging only. + # Default behavior should match upstream greatgitsby/esim transport flow. + if os.getenv(ESIM_QESIM_INIT_ENV, "").strip() == "1": + try: + self.query("AT+CMEE=2") + except Exception: + pass + self._set_quectel_lpa_mode() + for command in ("AT+CCHO", "AT+CCHC", "AT+CGLA", "AT+CSIM"): + try: + self.query(f"{command}=?") + except Exception as e: + if self.verbose: + print(f"!! capability probe failed for {command}: {e}", file=sys.stderr) + + def open_isdr(self) -> None: + try: + self.query(f'AT+CCHC=1') + except Exception: + pass + ccho_candidates = [ + f'AT+CCHO="{ISDR_AID}"', + f"AT+CCHO={ISDR_AID}", + ] + for cmd in ccho_candidates: + try: + lines = self.query(cmd) + except Exception: + continue + for line in lines: + if line.startswith("+CCHO:") and (ch := line.split(":", 1)[1].strip()): + self.channel = ch + return + + # Fallback for modems that reject CCHO but allow APDUs over basic channel. + self.channel = "0" + aid_len = len(ISDR_AID) // 2 + select_isdr = bytes.fromhex(f"00A40400{aid_len:02X}{ISDR_AID}") + try: + _, sw1, sw2 = self.send_apdu(select_isdr) + if (sw1, sw2) == (0x90, 0x00): + return + raise RuntimeError(f"ISD-R SELECT failed on basic channel SW={sw1:02X}{sw2:02X}") + except Exception: + self.channel = None + # Final fallback for modems that only support APDU via CSIM. + try: + _, sw1, sw2 = self._send_csim_apdu(select_isdr) + if (sw1, sw2) == (0x90, 0x00): + self._use_csim = True + self.channel = "csim" + if self.verbose: + print("!! using CSIM APDU transport fallback", file=sys.stderr) + return + raise RuntimeError(f"ISD-R SELECT failed on CSIM SW={sw1:02X}{sw2:02X}") + except Exception: + self.channel = None + if self._has_qesim and not self._qesim_lpa_enabled and os.getenv(ESIM_QESIM_INIT_ENV, "").strip() == "1": + raise RuntimeError( + "Modem exposes AT+QESIM but rejects LPA enable and ISD-R access; " + "this firmware/SKU likely does not provide usable eSIM functionality" + ) + raise RuntimeError("Failed to access eUICC ISD-R (CCHO/CGLA/CSIM all failed)") + + def send_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + for attempt in range(3): + try: + if not self.channel: + self.open_isdr() + if self._use_csim: + return self._send_csim_apdu(apdu) + hex_payload = apdu.hex().upper() + cmd = f'AT+CGLA={self.channel},{len(hex_payload)},"{hex_payload}"' + for query_attempt in range(2): + for line in self.query(cmd): + if line.startswith("+CGLA:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = _decode_apdu_response_blob(parts[1].strip().strip('"'), "+CGLA") + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + if query_attempt == 0: + time.sleep(0.2) + raise RuntimeError("Missing +CGLA response") + except RuntimeError: + self.channel = None + self._use_csim = False + if attempt == 2: + raise + time.sleep(1 + attempt) + raise RuntimeError("send_apdu failed") + + def _send_csim_apdu(self, apdu: bytes) -> tuple[bytes, int, int]: + hex_payload = apdu.hex().upper() + cmd = f'AT+CSIM={len(hex_payload)},"{hex_payload}"' + for attempt in range(2): + for line in self.query(cmd): + if line.startswith("+CSIM:"): + parts = line.split(":", 1)[1].split(",", 1) + if len(parts) == 2: + data = _decode_apdu_response_blob(parts[1].strip().strip('"'), "+CSIM") + if len(data) >= 2: + return data[:-2], data[-2], data[-1] + if attempt == 0: + time.sleep(0.2) + raise RuntimeError("Missing +CSIM response") + + +# --- TLV utilities --- + +def iter_tlv(data: bytes, with_positions: bool = False) -> Generator: + idx, length = 0, len(data) + while idx < length: + start_pos = idx + tag = data[idx] + idx += 1 + if tag & 0x1F == 0x1F: # Multi-byte tag + tag_value = tag + while idx < length: + next_byte = data[idx] + idx += 1 + tag_value = (tag_value << 8) | next_byte + if not (next_byte & 0x80): + break + else: + tag_value = tag + if idx >= length: + break + size = data[idx] + idx += 1 + if size & 0x80: # Multi-byte length + num_bytes = size & 0x7F + if idx + num_bytes > length: + break + size = int.from_bytes(data[idx : idx + num_bytes], "big") + idx += num_bytes + if idx + size > length: + break + value = data[idx : idx + size] + idx += size + yield (tag_value, value, start_pos, idx) if with_positions else (tag_value, value) + + +def find_tag(data: bytes, target: int) -> bytes | None: + return next((v for t, v in iter_tlv(data) if t == target), None) + + +def encode_tlv(tag: int, value: bytes) -> bytes: + tag_bytes = bytes([(tag >> 8) & 0xFF, tag & 0xFF]) if tag > 255 else bytes([tag]) + vlen = len(value) + if vlen <= 127: + return tag_bytes + bytes([vlen]) + value + length_bytes = vlen.to_bytes((vlen.bit_length() + 7) // 8, "big") + return tag_bytes + bytes([0x80 | len(length_bytes)]) + length_bytes + value + + +def tbcd_to_string(raw: bytes) -> str: + return "".join(str(n) for b in raw for n in (b & 0x0F, b >> 4) if n <= 9) + + +def string_to_tbcd(s: str) -> bytes: + digits = [int(c) for c in s if c.isdigit()] + return bytes(digits[i] | ((digits[i + 1] if i + 1 < len(digits) else 0xF) << 4) for i in range(0, len(digits), 2)) + + +def base64_trim(s: str) -> str: + return "".join(c for c in s if c not in "\n\r \t") + + +# --- Shared helpers --- + +def _int_bytes(n: int) -> bytes: + """Encode a positive integer as minimal big-endian bytes (at least 1 byte).""" + return n.to_bytes((n.bit_length() + 7) // 8 or 1, "big") + + +def _extract_status(response: bytes, tag: int, name: str) -> int: + """Extract the status byte from a tagged ES10x response.""" + root = find_tag(response, tag) + if root is None: + raise RuntimeError(f"Missing {name}Response") + status = find_tag(root, TAG_STATUS) + if status is None: + raise RuntimeError(f"Missing status in {name}Response") + return status[0] + + +# Profile field decoders: TLV tag -> (field_name, decoder) +_PROFILE_FIELDS = { + TAG_ICCID: ("iccid", tbcd_to_string), + 0x4F: ("isdpAid", lambda v: v.hex().upper()), + 0x9F70: ("profileState", lambda v: STATE_LABELS.get(v[0], "unknown")), + 0x90: ("profileNickname", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x91: ("serviceProviderName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x92: ("profileName", lambda v: v.decode("utf-8", errors="ignore") or None), + 0x93: ("iconType", lambda v: ICON_LABELS.get(v[0], "unknown")), + 0x94: ("icon", b64e), + 0x95: ("profileClass", lambda v: CLASS_LABELS.get(v[0], "unknown")), +} + + +def _decode_profile_fields(data: bytes) -> dict: + """Parse known profile metadata TLV fields into a dict.""" + result = {} + for tag, value in iter_tlv(data): + if (field := _PROFILE_FIELDS.get(tag)): + result[field[0]] = field[1](value) + return result + + +# Notification field decoders: TLV tag -> (field_name, decoder) +_NOTIF_FIELDS = { + TAG_STATUS: ("seqNumber", lambda v: int.from_bytes(v, "big")), + 0x81: ("profileManagementOperation", lambda v: next((m for m in [0x80, 0x40, 0x20, 0x10] if len(v) >= 2 and v[1] & m), 0xFF)), + 0x0C: ("notificationAddress", lambda v: v.decode("utf-8", errors="ignore")), + TAG_ICCID: ("iccid", tbcd_to_string), +} + + +# --- ES10x command transport --- + +def es10x_command(client: AtClient, data: bytes) -> bytes: + response = bytearray() + sequence = 0 + offset = 0 + while offset < len(data): + chunk = data[offset : offset + ES10X_MSS] + offset += len(chunk) + is_last = offset == len(data) + apdu = bytes([0x80, 0xE2, 0x91 if is_last else 0x11, sequence & 0xFF, len(chunk)]) + chunk + segment, sw1, sw2 = client.send_apdu(apdu) + response.extend(segment) + while True: + if sw1 == 0x61: # More data available + segment, sw1, sw2 = client.send_apdu(bytes([0x80, 0xC0, 0x00, 0x00, sw2 or 0])) + response.extend(segment) + continue + if (sw1 & 0xF0) == 0x90: + break + raise RuntimeError(f"APDU failed with SW={sw1:02X}{sw2:02X}") + sequence += 1 + return bytes(response) + + +# --- ES9P HTTP --- + +def es9p_request(smdp_address: str, endpoint: str, payload: dict, error_prefix: str = "Request") -> dict: + if not system_time_valid(): + raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock") + url = f"https://{smdp_address}/gsma/rsp2/es9plus/{endpoint}" + headers = {"User-Agent": "gsma-rsp-lpad", "X-Admin-Protocol": "gsma/rsp/v2.3.0", "Content-Type": "application/json"} + resp = requests.post(url, json=payload, headers=headers, timeout=30, verify=_tls_verify_option()) + resp.raise_for_status() + if not resp.content: + return {} + data = resp.json() + if "header" in data and "functionExecutionStatus" in data["header"]: + status = data["header"]["functionExecutionStatus"] + if status.get("status") == "Failed": + sd = status.get("statusCodeData", {}) + raise RuntimeError(f"{error_prefix} failed: {sd.get('reasonCode', 'unknown')}/{sd.get('subjectCode', 'unknown')} - {sd.get('message', 'unknown')}") + return data + + +# --- Profile operations --- + +def decode_profiles(blob: bytes) -> list[dict]: + root = find_tag(blob, TAG_PROFILE_INFO_LIST) + if root is None: + raise RuntimeError("Missing ProfileInfoList") + list_ok = find_tag(root, 0xA0) + if list_ok is None: + return [] + defaults = {name: None for name, _ in _PROFILE_FIELDS.values()} + return [{**defaults, **_decode_profile_fields(value)} for tag, value in iter_tlv(list_ok) if tag == 0xE3] + + +def list_profiles(client: AtClient) -> list[dict]: + return decode_profiles(es10x_command(client, bytes.fromhex("BF2D00"))) + + +def _profile_op(client: AtClient, tag: int, iccid: str, refresh: bool, action: str) -> None: + inner = encode_tlv(TAG_ICCID, string_to_tbcd(iccid)) + if tag != TAG_DELETE_PROFILE: + if not refresh: + inner += encode_tlv(0x81, b'\x00') + inner = encode_tlv(0xA0, inner) + code = _extract_status(es10x_command(client, encode_tlv(tag, inner)), tag, f"{action.capitalize()}Profile") + if code == 0x00: + return + if code == 0x02 and tag != TAG_DELETE_PROFILE: + print(f"profile {iccid} already {action}d") + return + raise RuntimeError(f"{action.capitalize()}Profile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})") + + +def enable_profile(client: AtClient, iccid: str, refresh: bool = True) -> None: + _profile_op(client, TAG_ENABLE_PROFILE, iccid, refresh, "enable") + + +def disable_profile(client: AtClient, iccid: str, refresh: bool = True) -> None: + _profile_op(client, TAG_DISABLE_PROFILE, iccid, refresh, "disable") + + +def delete_profile(client: AtClient, iccid: str) -> None: + _profile_op(client, TAG_DELETE_PROFILE, iccid, True, "delete") + + +def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None: + nickname_bytes = nickname.encode("utf-8") + if len(nickname_bytes) > 64: + raise ValueError("Profile nickname must be 64 bytes or less") + content = encode_tlv(TAG_ICCID, string_to_tbcd(iccid)) + encode_tlv(0x90, nickname_bytes) + code = _extract_status(es10x_command(client, encode_tlv(TAG_SET_NICKNAME, content)), TAG_SET_NICKNAME, "SetNickname") + if code == 0x01: + raise RuntimeError(f"profile {iccid} not found") + if code != 0x00: + raise RuntimeError(f"SetNickname failed with status 0x{code:02X}") + + +# --- Notifications --- + +def list_notifications(client: AtClient) -> list[dict]: + response = es10x_command(client, encode_tlv(TAG_LIST_NOTIFICATION, b"")) + root = find_tag(response, TAG_LIST_NOTIFICATION) + if root is None: + raise RuntimeError("Missing ListNotificationResponse") + metadata_list = find_tag(root, 0xA0) + if metadata_list is None: + return [] + notifications: list[dict] = [] + for tag, value in iter_tlv(metadata_list): + if tag != TAG_NOTIFICATION_METADATA: + continue + notification = {"seqNumber": None, "profileManagementOperation": None, "notificationAddress": None, "iccid": None} + for t, v in iter_tlv(value): + if (field := _NOTIF_FIELDS.get(t)): + notification[field[0]] = field[1](v) + if notification["seqNumber"] is not None and notification["profileManagementOperation"] is not None and notification["notificationAddress"]: + notifications.append(notification) + return notifications + + +def retrieve_notification(client: AtClient, seq_number: int) -> dict: + request = encode_tlv(TAG_RETRIEVE_NOTIFICATION, encode_tlv(0xA0, encode_tlv(TAG_STATUS, _int_bytes(seq_number)))) + response = es10x_command(client, request) + root = find_tag(response, TAG_RETRIEVE_NOTIFICATION) + if root is None: + raise RuntimeError("Invalid RetrieveNotificationsListResponse") + a0_content = find_tag(root, 0xA0) + if a0_content is None: + raise RuntimeError("Invalid RetrieveNotificationsListResponse") + pending_notif, pending_tag = None, None + for tag, value in iter_tlv(a0_content): + if tag in (TAG_PROFILE_INSTALL_RESULT, 0x30): + pending_notif, pending_tag = value, tag + break + if pending_notif is None: + raise RuntimeError("Missing PendingNotification") + if pending_tag == TAG_PROFILE_INSTALL_RESULT: + result_data = find_tag(pending_notif, 0xBF27) + notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA) if result_data else None + else: + notif_meta = find_tag(pending_notif, TAG_NOTIFICATION_METADATA) + if notif_meta is None: + raise RuntimeError("Missing NotificationMetadata") + addr = find_tag(notif_meta, 0x0C) + if addr is None: + raise RuntimeError("Missing notificationAddress") + return {"notificationAddress": addr.decode("utf-8", errors="ignore"), "b64_PendingNotification": b64e(pending_notif)} + + +def remove_notification(client: AtClient, seq_number: int) -> None: + response = es10x_command(client, encode_tlv(TAG_NOTIFICATION_SENT, encode_tlv(TAG_STATUS, _int_bytes(seq_number)))) + root = find_tag(response, TAG_NOTIFICATION_SENT) + if root is None: + raise RuntimeError("Invalid NotificationSentResponse") + status = find_tag(root, TAG_STATUS) + if status is None or int.from_bytes(status, "big") != 0: + raise RuntimeError("RemoveNotificationFromList failed") + + +def process_notifications(client: AtClient) -> None: + for notification in list_notifications(client): + seq_number, smdp_address = notification["seqNumber"], notification["notificationAddress"] + if not seq_number or not smdp_address: + continue + try: + notif_data = retrieve_notification(client, seq_number) + es9p_request(smdp_address, "handleNotification", {"pendingNotification": notif_data["b64_PendingNotification"]}, "HandleNotification") + remove_notification(client, seq_number) + except Exception: + pass + + +# --- Authentication & Download --- + +def get_challenge_and_info(client: AtClient) -> tuple[bytes, bytes]: + challenge_resp = es10x_command(client, encode_tlv(TAG_EUICC_CHALLENGE, b"")) + root = find_tag(challenge_resp, TAG_EUICC_CHALLENGE) + if root is None: + raise RuntimeError("Missing GetEuiccDataResponse") + challenge = find_tag(root, TAG_STATUS) + if challenge is None: + raise RuntimeError("Missing challenge in response") + info_resp = es10x_command(client, encode_tlv(TAG_EUICC_INFO, b"")) + if not info_resp.startswith(bytes([0xBF, 0x20])): + raise RuntimeError("Missing GetEuiccInfo1Response") + return challenge, info_resp + + +def authenticate_server(client: AtClient, b64_signed1: str, b64_sig1: str, b64_pk_id: str, b64_cert: str, matching_id: str | None = None) -> str: + # Build request + tac = bytes([0x35, 0x29, 0x06, 0x11]) + device_info = encode_tlv(TAG_STATUS, tac) + encode_tlv(0xA1, b"") + ctx_inner = b"" + if matching_id: + ctx_inner += encode_tlv(TAG_STATUS, matching_id.encode("utf-8")) + ctx_inner += encode_tlv(0xA1, device_info) + content = base64.b64decode(b64_signed1) + base64.b64decode(b64_sig1) + base64.b64decode(b64_pk_id) + base64.b64decode(b64_cert) + encode_tlv(0xA0, ctx_inner) + request = encode_tlv(TAG_AUTH_SERVER, content) + + response = es10x_command(client, request) + if not response.startswith(bytes([0xBF, 0x38])): + raise RuntimeError("Invalid AuthenticateServerResponse") + + # Check for eUICC-side errors + root = find_tag(response, TAG_AUTH_SERVER) + if root is not None: + error_tag = find_tag(root, 0xA1) + if error_tag is not None: + code = int.from_bytes(error_tag, "big") if error_tag else 0 + desc = AUTH_SERVER_ERROR_CODES.get(code, "unknown") + raise RuntimeError(f"AuthenticateServer rejected by eUICC: {desc} (0x{code:02X})") + + return b64e(response) + + +def prepare_download(client: AtClient, b64_signed2: str, b64_sig2: str, b64_cert: str, cc: str | None = None) -> str: + smdp_signed2 = base64.b64decode(b64_signed2) + smdp_signature2 = base64.b64decode(b64_sig2) + smdp_certificate = base64.b64decode(b64_cert) + smdp_signed2_root = find_tag(smdp_signed2, 0x30) + if smdp_signed2_root is None: + raise RuntimeError("Invalid smdpSigned2") + transaction_id = find_tag(smdp_signed2_root, TAG_STATUS) + cc_required_flag = find_tag(smdp_signed2_root, 0x01) + if transaction_id is None or cc_required_flag is None: + raise RuntimeError("Invalid smdpSigned2") + content = smdp_signed2 + smdp_signature2 + if int.from_bytes(cc_required_flag, "big") != 0: + if not cc: + raise RuntimeError("Confirmation code required but not provided") + content += encode_tlv(0x04, hashlib.sha256(hashlib.sha256(cc.encode("utf-8")).digest() + transaction_id).digest()) + content += smdp_certificate + response = es10x_command(client, encode_tlv(TAG_PREPARE_DOWNLOAD, content)) + if not response.startswith(bytes([0xBF, 0x21])): + raise RuntimeError("Invalid PrepareDownloadResponse") + return b64e(response) + + +def _parse_tlv_header_len(data: bytes) -> int: + """Return the combined tag + length header size for a TLV element.""" + tag_len = 2 if data[0] & 0x1F == 0x1F else 1 + length_byte = data[tag_len] + return tag_len + (1 + (length_byte & 0x7F) if length_byte & 0x80 else 1) + + +def load_bpp(client: AtClient, b64_bpp: str) -> dict: + bpp = b64d(b64_bpp) + if not bpp.startswith(bytes([0xBF, 0x36])): + raise RuntimeError("Invalid BoundProfilePackage") + + bpp_root_value = None + for tag, value, start, end in iter_tlv(bpp, with_positions=True): + if tag == TAG_BPP: + bpp_root_value = value + bpp_value_start = start + _parse_tlv_header_len(bpp[start:end]) + break + if bpp_root_value is None: + raise RuntimeError("Invalid BoundProfilePackage") + + chunks = [] + for tag, value, start, end in iter_tlv(bpp_root_value, with_positions=True): + if tag == 0xBF23: + chunks.append(bpp[0 : bpp_value_start + end]) + elif tag == 0xA0: + chunks.append(bpp[bpp_value_start + start : bpp_value_start + end]) + elif tag in (0xA1, 0xA3): + hdr_len = _parse_tlv_header_len(bpp_root_value[start:end]) + chunks.append(bpp[bpp_value_start + start : bpp_value_start + start + hdr_len]) + for _, _, child_start, child_end in iter_tlv(value, with_positions=True): + chunks.append(value[child_start:child_end]) + elif tag == 0xA2: + chunks.append(bpp[bpp_value_start + start : bpp_value_start + end]) + + result = {"seqNumber": 0, "success": False, "bppCommandId": None, "errorReason": None} + for chunk in chunks: + response = es10x_command(client, chunk) + if not response: + continue + root = find_tag(response, TAG_PROFILE_INSTALL_RESULT) + if not root: + continue + result_data = find_tag(root, 0xBF27) + if not result_data: + break + notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA) + if notif_meta: + seq_num = find_tag(notif_meta, TAG_STATUS) + if seq_num: + result["seqNumber"] = int.from_bytes(seq_num, "big") + final_result = find_tag(result_data, 0xA2) + if final_result: + for tag, value in iter_tlv(final_result): + if tag == 0xA0: + result["success"] = True + elif tag == 0xA1: + bpp_cmd = find_tag(value, TAG_STATUS) + if bpp_cmd: + result["bppCommandId"] = int.from_bytes(bpp_cmd, "big") + err = find_tag(value, 0x81) + if err: + result["errorReason"] = int.from_bytes(err, "big") + break # ProfileInstallResult received — eUICC session is finalized + if not result["success"] and result["errorReason"] is not None: + cmd_name = BPP_COMMAND_NAMES.get(result["bppCommandId"], f"unknown({result['bppCommandId']})") + err_name = BPP_ERROR_REASONS.get(result["errorReason"], f"unknown({result['errorReason']})") + raise RuntimeError(f"Profile installation failed at {cmd_name}: {err_name} (bppCommandId={result['bppCommandId']}, errorReason={result['errorReason']})") + return result + + +def parse_metadata(b64_metadata: str) -> dict: + root = find_tag(b64d(b64_metadata), 0xBF25) + if root is None: + raise RuntimeError("Invalid profileMetadata") + defaults = {"iccid": None, "serviceProviderName": None, "profileName": None, "iconType": None, "icon": None, "profileClass": None} + return {**defaults, **_decode_profile_fields(root)} + + +def cancel_session(client: AtClient, transaction_id: bytes, reason: int = 127) -> str: + content = encode_tlv(0x80, transaction_id) + encode_tlv(0x81, bytes([reason])) + response = es10x_command(client, encode_tlv(TAG_CANCEL_SESSION, content)) + return b64e(response) + + +def parse_lpa_activation_code(activation_code: str) -> tuple[str, str, str]: + if not activation_code.startswith("LPA:"): + raise ValueError("Invalid activation code format") + + parts = activation_code[4:].split("$") + if len(parts) != 3: + raise ValueError("Invalid activation code format") + + version, smdp, matching_id = [p.strip() for p in parts] + if version != "1": + raise ValueError("Unsupported activation code version") + if len(smdp) == 0 or "." not in smdp: + raise ValueError("Invalid SM-DP+ address in activation code") + if len(matching_id) == 0: + raise ValueError("Missing matching ID in activation code") + return version, smdp, matching_id + + +def download_profile(client: AtClient, activation_code: str) -> str: + _, smdp, matching_id = parse_lpa_activation_code(activation_code) + + challenge, euicc_info = get_challenge_and_info(client) + + payload = {"smdpAddress": smdp, "euiccChallenge": b64e(challenge), "euiccInfo1": b64e(euicc_info)} + if matching_id: + payload["matchingId"] = matching_id + auth = es9p_request(smdp, "initiateAuthentication", payload, "Authentication") + tx_id = base64_trim(auth.get("transactionId", "")) + tx_id_bytes = base64.b64decode(tx_id) if tx_id else b"" + + try: + b64_auth_resp = authenticate_server( + client, base64_trim(auth.get("serverSigned1", "")), base64_trim(auth.get("serverSignature1", "")), + base64_trim(auth.get("euiccCiPKIdToBeUsed", "")), base64_trim(auth.get("serverCertificate", "")), + matching_id=matching_id) + + cli = es9p_request(smdp, "authenticateClient", {"transactionId": tx_id, "authenticateServerResponse": b64_auth_resp}, "Authentication") + metadata = parse_metadata(base64_trim(cli.get("profileMetadata", ""))) + iccid = str(metadata.get("iccid") or "") + print(f'Downloading profile: {iccid} - {metadata["serviceProviderName"]} - {metadata["profileName"]}') + + b64_prep = prepare_download( + client, base64_trim(cli.get("smdpSigned2", "")), base64_trim(cli.get("smdpSignature2", "")), base64_trim(cli.get("smdpCertificate", ""))) + + bpp = es9p_request(smdp, "getBoundProfilePackage", {"transactionId": tx_id, "prepareDownloadResponse": b64_prep}, "GetBoundProfilePackage") + + result = load_bpp(client, base64_trim(bpp.get("boundProfilePackage", ""))) + if result["success"]: + print(f"Profile installed successfully (seqNumber: {result['seqNumber']})") + return iccid + else: + raise RuntimeError(f"Profile installation failed: {result}") + except Exception: + if tx_id_bytes: + b64_cancel_resp = "" + try: + b64_cancel_resp = cancel_session(client, tx_id_bytes) + except Exception: + pass + try: + es9p_request(smdp, "cancelSession", { + "transactionId": tx_id, + "cancelSessionResponse": b64_cancel_resp, + }, "CancelSession") + except Exception: + pass + raise + + +class TiciLPA(LPABase): + handles_modem_restart = True + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, interface: Literal["qmi", "at"] = "at", device: str = DEFAULT_DEVICE, baud: int = DEFAULT_BAUD, + timeout: float = DEFAULT_TIMEOUT, verbose: bool = False): + if getattr(self, "_initialized", False): + self.verbose = verbose or self.verbose + return + # This backend is AT-only. Keep interface arg for compatibility with existing CLI/call sites. + self.interface = interface + self.device = device + self.baud = baud + self.timeout = timeout + self.verbose = verbose + self._last_good_device = device + self._euicc_present_cache: bool | None = None + from iqpilot.system.hardware.tici.hardware import get_device_type + self._is_eg25 = get_device_type() in ("tizi",) + self._client: AtClient | None = None + self._initialized = True + # _client is opened lazily via _ensure_client() on first actual eSIM operation. + # Do NOT call _open_ready_client() here: on pSIM devices (no eUICC) it retries + # for 10–30 s against the AT port, blocking hw_state_thread and starving the + # network-type polling loop that drives the WiFi/LTE sidebar indicator. + atexit.register(self.close) + + def close(self) -> None: + if self._client is not None: + try: + self._client.close() + finally: + self._client = None + + @staticmethod + def _set_modemmanager_running(running: bool) -> None: + action = "start" if running else "stop" + subprocess.check_call(["sudo", "systemctl", action, "ModemManager"]) + + @staticmethod + def _is_modemmanager_running() -> bool: + res = subprocess.run(["systemctl", "is-active", "ModemManager"], + capture_output=True, text=True, check=False) + return res.stdout.strip() == "active" + + def _probe_euicc_present(self) -> bool | None: + if self._euicc_present_cache is not None: + return self._euicc_present_cache + + if self._is_modemmanager_running(): + return None + + restarted_modemmanager = False + try: + res = subprocess.run( + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-get-slot-status"], + capture_output=True, text=True, check=False, timeout=12, + ) + output = f"{res.stdout}\n{res.stderr}" + if "Is eUICC: yes" in output: + self._euicc_present_cache = True + elif "Is eUICC: no" in output: + self._euicc_present_cache = False + else: + self._euicc_present_cache = None + except Exception: + self._euicc_present_cache = None + finally: + if restarted_modemmanager: + self._set_modemmanager_running(True) + time.sleep(2.0) + + return self._euicc_present_cache + + @staticmethod + def _ttyusb2_mm_ignored() -> bool: + # AGNOS PR 521 marks ttyUSB2 with ID_MM_PORT_IGNORE=1 for EG25. + # If missing, ModemManager may contend with direct AT eSIM operations. + if not os.path.exists("/dev/ttyUSB2"): + return False + try: + res = subprocess.run( + ["udevadm", "info", "-q", "property", "-n", "/dev/ttyUSB2"], + capture_output=True, text=True, check=False, + ) + if res.returncode != 0: + return False + return "ID_MM_PORT_IGNORE=1" in res.stdout + except Exception: + return False + + def _ensure_client(self) -> AtClient: + if self._client is None: + self._client = self._open_ready_client() + return self._client + + @contextmanager + def _open_client(self): + yield self._ensure_client() + + def _open_ready_client(self) -> AtClient: + euicc_present = self._probe_euicc_present() + if euicc_present is False: + raise LPAError("Modem reports no eUICC support on this unit (QMI: Is eUICC: no)") + + last_error: Exception | None = None + recovery_attempted = False + for attempt in range(1, 6): + for device in self._candidate_devices(): + client: AtClient | None = None + try: + if self.verbose: + print(f"!! trying modem AT device: {device} (attempt {attempt})", file=sys.stderr) + client = AtClient(device, self.baud, self.timeout, self.verbose) + client.ensure_capabilities() + client.open_isdr() + self._last_good_device = device + if self.verbose: + print(f"!! selected modem AT device: {device}", file=sys.stderr) + return client + except Exception as e: + if self.verbose: + print(f"!! modem AT device failed: {device}: {e}", file=sys.stderr) + last_error = e + if client is not None: + try: + client.close() + except Exception: + pass + if not recovery_attempted and last_error is not None and self._should_recover_isdr(last_error): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + # Give modem a moment to finish startup / channel release. + time.sleep(min(0.4 * attempt, 1.5)) + assert last_error is not None + raise last_error + + def _candidate_devices(self) -> list[str]: + if self.device and not os.path.exists(self.device): + return [self.device] + + candidates = [ + self._last_good_device, + self.device, + "/dev/modem_at0", + "/dev/ttyUSB3", + "/dev/ttyUSB2", + "/dev/ttyUSB1", + "/dev/ttyUSB0", + ] + ordered: list[str] = [] + for dev in candidates: + should_include = dev in (self.device, "/dev/modem_at0") or os.path.exists(dev) + if dev and dev not in ordered and should_include: + ordered.append(dev) + return ordered if ordered else [self.device] + + @staticmethod + def _to_profile(data: dict) -> Profile: + return Profile( + iccid=str(data.get("iccid") or ""), + nickname=str(data.get("profileNickname") or ""), + enabled=str(data.get("profileState") or "") == "enabled", + provider=str(data.get("serviceProviderName") or ""), + ) + + def _clear_cat_busy(self) -> None: + time.sleep(3) + try: + self._ensure_client().query('AT+QSTKRSP=254') + except Exception: + pass + time.sleep(1) + try: + self._ensure_client().query('AT+CFUN=4') + except Exception: + pass + time.sleep(2) + try: + self._ensure_client().query('AT+CFUN=1') + except Exception: + pass + self._ensure_client().channel = None + time.sleep(5) + + def _wait_for_modem(self, timeout: float = 45.0) -> None: + start = time.monotonic() + last_error: Exception | None = None + while time.monotonic() - start < timeout: + try: + self.close() + self._client = self._open_ready_client() + return + except Exception as e: + last_error = e + time.sleep(2) + if last_error is not None: + raise RuntimeError("Modem did not recover after reboot") from last_error + raise RuntimeError("Modem did not recover after reboot") + + def _restart_modem(self) -> None: + client = self._ensure_client() + for cmd in ('AT+CFUN=0', 'AT+CFUN=1,1'): + try: + client.query(cmd) + except Exception: + pass + client.channel = None + client._use_csim = False + self._wait_for_modem() + + @staticmethod + def _should_recover_isdr(error: Exception) -> bool: + message = str(error).lower() + markers = ( + "failed to open isd-r", + "failed to access euicc isd-r", + "simfilenotfound", + "at+ccho", + "at+cgla", + ) + return any(marker in message for marker in markers) + + def _recover_isdr_access(self) -> bool: + was_modemmanager_running = self._is_modemmanager_running() + self.close() + try: + if was_modemmanager_running: + self._set_modemmanager_running(False) + time.sleep(2.0) + + for cmd in ( + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-reset"], + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-sim-power-off=1"], + ["sudo", "qmicli", "-p", "-d", "/dev/cdc-wdm0", "--uim-sim-power-on=1"], + ): + subprocess.check_call(cmd) + time.sleep(2.0) + + self._euicc_present_cache = None + return True + except Exception as e: + if self.verbose: + print(f"!! ISD-R recovery failed: {e}", file=sys.stderr) + return False + finally: + if was_modemmanager_running: + self._set_modemmanager_running(True) + time.sleep(2.0) + + def _with_lpa_error(self, fn): + last_error: Exception | None = None + recovery_attempted = False + for attempt in range(3): + try: + return fn() + except LPAError: + raise + except RuntimeError as e: + last_error = e + if "does not provide usable esim functionality" in str(e).lower(): + raise LPAError(str(e)) from e + if not recovery_attempted and self._should_recover_isdr(e): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + if self._is_transient_transport_error(str(e)) and attempt == 0: + self._wait_for_modem() + continue + raise LPAError(str(e)) from e + except (OSError, serial.SerialException) as e: + last_error = e + if not recovery_attempted and self._should_recover_isdr(e): + recovery_attempted = self._recover_isdr_access() + if recovery_attempted: + continue + if self._is_transient_transport_error(str(e)) and attempt == 0: + self._wait_for_modem() + continue + raise LPAError(f"AT transport error: {e}") from e + except TimeoutError as e: + raise LPAError(f"AT modem timeout: {e}") from e + except ValueError as e: + raise LPAError(str(e)) from e + assert last_error is not None + raise LPAError(str(last_error)) + + @staticmethod + def _is_transient_transport_error(message: str) -> bool: + lower = message.lower() + transient_markers = ( + "broken pipe", + "resource busy", + "input/output error", + "device disconnected", + "device reports readiness to read but returned no data", + "transport error", + "missing +cgla response", + "missing +csim response", + "malformed +cgla response payload", + "malformed +csim response payload", + "non-hexadecimal number found in fromhex", + ) + return any(m in lower for m in transient_markers) + + def list_profiles(self) -> list[Profile]: + def _run() -> list[Profile]: + return [self._to_profile(p) for p in list_profiles(self._ensure_client())] + return self._with_lpa_error(_run) + + def get_active_profile(self) -> Profile | None: + return next((p for p in self.list_profiles() if p.enabled), None) + + def _validate_profile_exists(self, iccid: str) -> None: + if not any(p.iccid == iccid for p in self.list_profiles()): + raise LPAProfileNotFoundError(f"profile {iccid} does not exist") + + def _is_bootstrapped(self) -> bool: + return not any(self.is_comma_profile(p.iccid) for p in self.list_profiles()) + + def _ensure_switchable_profile(self, iccid: str) -> None: + if self.is_comma_profile(iccid): + return + + if not self._is_bootstrapped(): + raise LPAError( + "Delete the existing Comma pSIM profile before activating a user eSIM profile" + ) + + def _process_notifications_after_delete(self, client: AtClient, iccid: str) -> None: + try: + process_notifications(client) + return + except Exception as error: + notification_error = error + + self._restart_modem() + + try: + deleted = not any(p.iccid == iccid for p in self.list_profiles()) + except Exception: + raise RuntimeError( + "Profile delete may have succeeded, but modem notification cleanup failed; " + "refresh profiles and retry if needed" + ) from notification_error + + if deleted: + return + + raise RuntimeError( + "Profile delete did not finish cleanly; refresh profiles and retry" + ) from notification_error + + def _process_notifications_after_state_change( + self, + validator, + recovery_message: str, + failure_message: str, + ) -> None: + try: + process_notifications(self._ensure_client()) + return + except Exception as notification_error: + pass + + self._wait_for_modem() + + try: + if validator(): + return + except Exception: + raise RuntimeError(recovery_message) from notification_error + + raise RuntimeError(failure_message) from notification_error + + def delete_profile(self, iccid: str) -> None: + self._validate_profile_exists(iccid) + active = self.get_active_profile() + if active is not None and active.iccid == iccid: + raise LPAError("cannot delete active profile, switch to another profile first") + + def _run() -> None: + client = self._ensure_client() + try: + delete_profile(client, iccid) + except RuntimeError as error: + if "catbusy" in str(error).lower(): + self._clear_cat_busy() + delete_profile(client, iccid) + else: + raise + self._process_notifications_after_delete(client, iccid) + + self._with_lpa_error(_run) + + def bootstrap(self) -> None: + comma_profiles = [p for p in self.list_profiles() if self.is_comma_profile(p.iccid)] + if not comma_profiles: + return + + def _run() -> None: + client = self._ensure_client() + for profile in comma_profiles: + if profile.enabled: + disable_profile(client, profile.iccid, refresh=True) + process_notifications(client) + delete_profile(client, profile.iccid) + process_notifications(client) + self._restart_modem() + + self._with_lpa_error(_run) + + def download_profile(self, qr: str, nickname: str | None = None) -> None: + # Strict upfront validation to provide immediate user feedback. + parse_lpa_activation_code(qr) + before = {p.iccid for p in self.list_profiles()} + + def _run() -> None: + client = self._ensure_client() + iccid = download_profile(client, qr) + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid for profile in self.list_profiles()), + "Profile add may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile add did not finish cleanly; refresh profiles and retry", + ) + + self._with_lpa_error(_run) + + if nickname: + after = self.list_profiles() + new_profile = next((p for p in after if p.iccid not in before), None) + if new_profile is not None and new_profile.iccid: + self.nickname_profile(new_profile.iccid, nickname) + + def nickname_profile(self, iccid: str, nickname: str) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + set_profile_nickname(self._ensure_client(), iccid, nickname) + self._with_lpa_error(_run) + + def switch_profile(self, iccid: str) -> None: + self._validate_profile_exists(iccid) + self._ensure_switchable_profile(iccid) + active = self.get_active_profile() + if active is not None and active.iccid == iccid: + return + + def _run() -> None: + client = self._ensure_client() + use_refresh = self._is_eg25 + try: + enable_profile(client, iccid, refresh=use_refresh) + except RuntimeError as error: + if "catbusy" in str(error).lower(): + self._clear_cat_busy() + enable_profile(client, iccid, refresh=use_refresh) + else: + raise + client.channel = None + client._use_csim = False + if use_refresh: + self._wait_for_modem() + else: + self._restart_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and profile.enabled for profile in self.list_profiles()), + "Profile switch may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile switch did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + # Compatibility methods retained for old tests/scripts. + def process_notifications(self) -> None: + def _run() -> None: + process_notifications(self._ensure_client()) + self._with_lpa_error(_run) + + def enable_profile(self, iccid: str, refresh: bool = True) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + client = self._ensure_client() + enable_profile(client, iccid, refresh=refresh) + client.channel = None + client._use_csim = False + self._wait_for_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and profile.enabled for profile in self.list_profiles()), + "Profile enable may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile enable did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + def disable_profile(self, iccid: str, refresh: bool = True) -> None: + self._validate_profile_exists(iccid) + + def _run() -> None: + client = self._ensure_client() + disable_profile(client, iccid, refresh=refresh) + client.channel = None + client._use_csim = False + self._wait_for_modem() + self._process_notifications_after_state_change( + lambda: any(profile.iccid == iccid and not profile.enabled for profile in self.list_profiles()), + "Profile disable may have succeeded, but modem notification cleanup failed; refresh profiles", + "Profile disable did not finish cleanly; refresh profiles and retry", + ) + self._with_lpa_error(_run) + + +# --- CLI --- + +def main() -> None: + parser = argparse.ArgumentParser(description="Minimal AT-only LPA implementation") + parser.add_argument("--device", default=DEFAULT_DEVICE) + parser.add_argument("--baud", type=int, default=DEFAULT_BAUD) + parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT) + parser.add_argument("--verbose", action="store_true") + parser.add_argument("--enable", type=str) + parser.add_argument("--disable", type=str) + parser.add_argument("--delete", type=str, metavar="ICCID", help="Delete a disabled profile") + parser.add_argument("--no-refresh", action="store_true", help="Skip REFRESH after enable/disable") + parser.add_argument("--set-nickname", nargs=2, metavar=("ICCID", "NICKNAME")) + parser.add_argument("--list-notifications", action="store_true") + parser.add_argument("--process-notifications", action="store_true") + parser.add_argument("--download", type=str, metavar="CODE") + args = parser.parse_args() + + lpa = TiciLPA(device=args.device, baud=args.baud, timeout=args.timeout, verbose=args.verbose) + + if args.enable: + lpa.enable_profile(args.enable, refresh=not args.no_refresh) + elif args.disable: + lpa.disable_profile(args.disable, refresh=not args.no_refresh) + elif args.delete: + lpa.delete_profile(args.delete) + elif args.set_nickname: + lpa.nickname_profile(args.set_nickname[0], args.set_nickname[1]) + elif args.list_notifications: + with lpa._open_client() as client: + print(json.dumps(list_notifications(client), indent=2)) + return + elif args.process_notifications: + lpa.process_notifications() + return + elif args.download: + lpa.download_profile(args.download) + + profiles_out = [{ + "iccid": p.iccid, + "profileNickname": p.nickname, + "profileState": "enabled" if p.enabled else "disabled", + "serviceProviderName": p.provider, + } for p in lpa.list_profiles()] + print(json.dumps(profiles_out, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py new file mode 100644 index 000000000..d53183f7a --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/pins.py @@ -0,0 +1,30 @@ +# GPIO pin definitions +class GPIO: + # both GPIO_STM_RST_N and GPIO_LTE_RST_N are misnamed, they are high to reset + HUB_RST_N = 30 + UBLOX_RST_N = 32 + UBLOX_SAFEBOOT_N = 33 + GNSS_PWR_EN = 34 # SCHEMATIC LABEL: GPIO_UBLOX_PWR_EN + + STM_RST_N = 124 + STM_BOOT0 = 134 + STM_PWR_EN_N = 41 # because STM32H7 RST doesn't generate a full power-on-reset + + SIREN = 42 + SOM_ST_IO = 49 + + LTE_RST_N = 50 + LTE_PWRKEY = 116 + LTE_BOOT = 52 + + # GPIO_CAM0_DVDD_EN = /sys/kernel/debug/regulator/camera_rear_ldo + CAM0_AVDD_EN = 8 + CAM0_RSTN = 9 + CAM1_RSTN = 7 + CAM2_RSTN = 12 + + # Sensor interrupts + BMX055_ACCEL_INT = 21 + BMX055_GYRO_INT = 23 + BMX055_MAGN_INT = 87 + LSM_INT = 84 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/usb_storage.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/usb_storage.py new file mode 100644 index 000000000..7dcb52214 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/tici/usb_storage.py @@ -0,0 +1,71 @@ +import os +import subprocess + +from iqpilot.common.params import Params + +SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "set_usb_storage.sh") + + +def apply_usb_storage_state(state: bool): + Params().put_bool("UsbStorageEnabled", state) + try: + args = ["sudo", SCRIPT_PATH] + if state: + args.append("--rebuild") + subprocess.Popen(args) + except OSError: + pass + + +NCM_TRIED_MARKER = "/tmp/.iqemac_ncm_provisioned" +MAX_NCM_ATTEMPTS = 3 + + +def _ncm_attempts() -> int: + try: + with open(NCM_TRIED_MARKER) as f: + return int(f.read().strip() or 0) + except (OSError, ValueError): + return 0 + + +def ensure_ncm_gadget() -> bool: + # configfs is RAM backed, so the gadget must be rebuilt every boot; binding it + # enumerates on the host, which must not happen mid-drive + if os.path.isdir("/sys/class/net/usb0"): + return True + params = Params() + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + if not (params.get_bool("IQEmacEnabled") or egpu_selected(params)): + return False + attempts = _ncm_attempts() + if attempts >= MAX_NCM_ATTEMPTS: + return False + try: + # stamped before the run: the gadget build can wedge configfs in + # uninterruptible D state, and retrying that forever helps nobody + with open(NCM_TRIED_MARKER, "w") as f: + f.write(str(attempts + 1)) + subprocess.run(["sudo", "-n", SCRIPT_PATH], timeout=120, check=False) + except (OSError, subprocess.SubprocessError): + return False + return os.path.isdir("/sys/class/net/usb0") + + +INPUT_SUSPEND = "/sys/class/power_supply/battery/input_suspend" + + +def suspend_usb_input(suspend: bool = True) -> bool: + # a host on the data port makes the PMIC sink USB-PD while OBD-C feeds the same + # rail; the SOM browns out. This closes the charge path only, data is untouched + if not os.path.exists(INPUT_SUSPEND): + return False + try: + with open(INPUT_SUSPEND) as f: + if f.read().strip() == ("1" if suspend else "0"): + return True + except OSError: + pass + rc = subprocess.run(["sudo", "-n", "sh", "-c", f"echo {int(suspend)} > {INPUT_SUSPEND}"], + check=False, capture_output=True) + return rc.returncode == 0 diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/usb.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/usb.py new file mode 100644 index 000000000..15be6f295 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/hardware/usb.py @@ -0,0 +1,184 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +USB bus snapshot for deviceState: every enumerated device with its negotiated +speed and its controller's link-error count. Landing this in every rlog makes +cable/hub/link regressions diagnosable from a recorded route instead of only +live. + +Link errors come from `portli` on the ssusb controller (IQ.OS 4.9.1+); on older +builds the file is absent and the counts read 0. + +The USB eGPU dock is identified by VID/PID only. comma's internal codename for +it is deliberately not used here: IQ.Pilot runs these models on several +backends (eGPU dock, eMac), so the naming stays about the role, not the vendor. +""" +from pathlib import Path + +# comma's USB eGPU dock, both shipped USB IDs. The ROM ids are the same board +# sitting in its bootloader (ASMedia) before vendor firmware is flashed — it +# enumerates but cannot serve a GPU in that state. +EGPU_DOCK_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) +EGPU_DOCK_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) +# must equal image_product() of the bundled firmware; test_egpu_dock_flash pins them together +EGPU_DOCK_FW_PRODUCT = "custom ed4e39b7-CLEAN" + + +def is_egpu_usb_device(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool: + ids = EGPU_DOCK_USB_IDS + EGPU_DOCK_ROM_USB_IDS if include_bootloader else EGPU_DOCK_USB_IDS + return (vendor_id, product_id) in ids +USB_DEVICES_PATH = Path("/sys/bus/usb/devices") +UDC_PATH = Path("/sys/class/udc") +TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") +USB3_LANES = {1: "a", 2: "b"} # 0 = unattached +SOC_PLATFORM_PATH = Path("/sys/devices/platform/soc") +CONTROLLER_SUFFIX = ".ssusb" +LINK_ERRORS_FILE = "portli" + + +def read(path: Path) -> str | None: + # a controller in peripheral mode fails portli's show(); that surfaces as TypeError, not OSError + try: + return path.read_text().strip() + except Exception: + return None + + +def read_int(path: Path, base: int = 10) -> int: + try: + return int(path.read_text(), base) + except Exception: + return 0 + + +def read_hex_counter(path: Path) -> int: + """sysfs counter printed as '0x0000002a' (portli), tolerating a bare hex value.""" + raw = read(path) + if raw is None: + return 0 + try: + return int(raw, 0) if raw.lower().startswith("0x") else int(raw, 16) + except ValueError: + return 0 + + +def get_usb_topology(root: Path = USB_DEVICES_PATH) -> set[str]: + """Names of everything on the bus; a cheap way to detect hotplug without + re-reading every attribute.""" + try: + return {p.name for p in root.iterdir()} + except Exception: + return set() + + +def usb_devices(root: Path = USB_DEVICES_PATH) -> list[Path]: + try: + return sorted((d for d in root.glob("*") if (d / "idVendor").exists()), key=lambda p: p.name) + except Exception: + return [] + + +def controller(device: Path) -> Path | None: + """The SuperSpeed controller a device hangs off (…/a800000.ssusb).""" + try: + return next((p for p in device.resolve().parents if p.name.endswith(CONTROLLER_SUFFIX)), None) + except Exception: + return None + + +def usb_controllers(soc: Path = SOC_PLATFORM_PATH) -> list[Path]: + try: + return sorted(soc.glob(f"*{CONTROLLER_SUFFIX}")) + except Exception: + return [] + + +def link_controller(udc_root: Path = UDC_PATH) -> str: + """Name of the Type-C port's controller, derived from the UDC rather than + hardcoded: the gadget exposes `.dwc3`, whose address prefix is the + `.ssusb` controller behind the same connector. comma pins the 3X value + directly, which would be wrong on any other board.""" + try: + udc = next(iter(sorted(p.name for p in udc_root.iterdir())), "") + except Exception: + return "" + return f"{udc.split('.')[0]}{CONTROLLER_SUFFIX}" if udc else "" + + +def usb3_lane(orientation: int | None = None) -> str: + """Which SuperSpeed lane the Type-C connector landed on. Unattached reads 0, + which is 'unknown' rather than a lane.""" + if orientation is None: + orientation = read_int(TYPEC_CC_ORIENTATION_PATH) + return USB3_LANES.get(orientation, "unknown") + + +def link_errors(ctrl: Path | None) -> int: + return read_hex_counter(ctrl / LINK_ERRORS_FILE) if ctrl is not None else 0 + + +def get_link_error_count(soc: Path = SOC_PLATFORM_PATH) -> int: + """Cumulative SS port link errors, read off the controller rather than a + device: in peripheral mode (eMac gadget link) the peer never enumerates on + our side, so there is no device row to carry the count.""" + return sum(link_errors(c) for c in usb_controllers(soc)) + + +def egpu_dock_present(root: Path = USB_DEVICES_PATH) -> bool: + """A dock in ROM/bootloader state is deliberately NOT counted as present: it + enumerates but cannot serve a GPU until vendor firmware is flashed.""" + return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS + for d in usb_devices(root)) + + +def egpu_dock_ready(root: Path = USB_DEVICES_PATH) -> bool: + """Present AND running the exact firmware we ship. A dock on any other + firmware enumerates fine but has not been validated with this stack, so the + runtime refuses it; the flasher still sees it via egpu_dock_present.""" + return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS + and (read(d / "product") or "").strip() == EGPU_DOCK_FW_PRODUCT + for d in usb_devices(root)) + + +def get_usb_state(root: Path = USB_DEVICES_PATH, udc_root: Path = UDC_PATH) -> list[dict]: + devices = [] + lane, link_ctrl = usb3_lane(), link_controller(udc_root) + for device in usb_devices(root): + ctrl = controller(device) + devices.append({ + "usb3Lane": lane if ctrl is not None and ctrl.name == link_ctrl else "unknown", + "busnum": read_int(device / "busnum"), + "devnum": read_int(device / "devnum"), + "vendorId": read_int(device / "idVendor", 16), + "productId": read_int(device / "idProduct", 16), + "speedMbps": read_int(device / "speed"), + "manufacturer": read(device / "manufacturer") or "", + "product": read(device / "product") or "", + # 16-bit field upstream, so mask rather than let a wrapped counter overflow it + "linkErrorCount": link_errors(ctrl) & 0xFFFF, + }) + return devices + + +def set_usb_state(device_state, devices: list[dict], link_error_count: int = 0, + lane: str | None = None) -> None: + entries = device_state.usbState.init('devices', len(devices)) + + dock_present = False + for entry, device in zip(entries, devices, strict=True): + entry.busnum = device["busnum"] + entry.devnum = device["devnum"] + entry.vendorId = device["vendorId"] + entry.productId = device["productId"] + entry.speedMbps = device["speedMbps"] + entry.manufacturer = device["manufacturer"] + entry.product = device["product"] + entry.linkErrorCount = device.get("linkErrorCount", 0) & 0xFFFF + entry.usb3Lane = device.get("usb3Lane", "unknown") + + if (entry.vendorId, entry.productId) in EGPU_DOCK_USB_IDS: + dock_present = True + + device_state.usbState.linkErrorCount = link_error_count + device_state.usbState.usb3Lane = lane if lane is not None else usb3_lane() + device_state.egpuDockPresent = dock_present diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/micd.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/micd.py new file mode 100644 index 000000000..5c8d05594 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/micd.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +import numpy as np +import os +import time +from functools import cache +import threading + +from iqpilot.cereal import messaging +from iqpilot.common.params import Params +from iqpilot.common.realtime import Ratekeeper +from iqpilot.common.utils import retry +from iqpilot.common.swaglog import cloudlog + +RATE = 10 +FFT_SAMPLES = 1600 # 100ms +REFERENCE_SPL = 2e-5 # newtons/m^2 +SAMPLE_RATE = 16000 +SAMPLE_BUFFER = 800 # 50ms + + +@cache +def get_a_weighting_filter(): + # Calculate the A-weighting filter + # https://en.wikipedia.org/wiki/A-weighting + freqs = np.fft.fftfreq(FFT_SAMPLES, d=1 / SAMPLE_RATE) + A = 12194 ** 2 * freqs ** 4 / ((freqs ** 2 + 20.6 ** 2) * (freqs ** 2 + 12194 ** 2) * np.sqrt((freqs ** 2 + 107.7 ** 2) * (freqs ** 2 + 737.9 ** 2))) + return A / np.max(A) + + +def calculate_spl(measurements): + # https://www.engineeringtoolbox.com/sound-pressure-d_711.html + sound_pressure = np.sqrt(np.mean(measurements ** 2)) # RMS of amplitudes + if sound_pressure > 0: + sound_pressure_level = 20 * np.log10(sound_pressure / REFERENCE_SPL) # dB + else: + sound_pressure_level = 0 + return sound_pressure, sound_pressure_level + + +def apply_a_weighting(measurements: np.ndarray) -> np.ndarray: + # Generate a Hanning window of the same length as the audio measurements + measurements_windowed = measurements * np.hanning(len(measurements)) + + # Apply the A-weighting filter to the signal + return np.abs(np.fft.ifft(np.fft.fft(measurements_windowed) * get_a_weighting_filter())) + + +class Mic: + def __init__(self): + self.rk = Ratekeeper(RATE) + self.pm = messaging.PubMaster(['soundPressure', 'rawAudioData']) + self.params = Params() + + self.measurements = np.empty(0) + + self.sound_pressure = 0 + self.sound_pressure_weighted = 0 + self.sound_pressure_level_weighted = 0 + + self.lock = threading.Lock() + self.callback_count = 0 + self.last_audio_rms = 0.0 + self.last_audio_peak = 0.0 + self.last_device = None + self.last_status = None + + def update(self): + with self.lock: + sound_pressure = self.sound_pressure + sound_pressure_weighted = self.sound_pressure_weighted + sound_pressure_level_weighted = self.sound_pressure_level_weighted + callback_count = self.callback_count + audio_rms = self.last_audio_rms + audio_peak = self.last_audio_peak + device_name = self.last_device + status = self.last_status + + msg = messaging.new_message('soundPressure', valid=True) + msg.soundPressure.soundPressure = float(sound_pressure) + msg.soundPressure.soundPressureWeighted = float(sound_pressure_weighted) + msg.soundPressure.soundPressureWeightedDb = float(sound_pressure_level_weighted) + + self.pm.send('soundPressure', msg) + if callback_count % RATE == 0: + cloudlog.info( + f"micd health: callbacks={callback_count} rms={audio_rms:.6f} peak={audio_peak:.6f} " + f"device={device_name} status={status!r} livestream={self.params.get_bool('IsLiveStreaming')}" + ) + self.rk.keep_time() + + def callback(self, indata, frames, time, status): + """ + Using amplitude measurements, calculate an uncalibrated sound pressure and sound pressure level. + Then apply A-weighting to the raw amplitudes and run the same calculations again. + + Logged A-weighted equivalents are rough approximations of the human-perceived loudness. + """ + msg = messaging.new_message('rawAudioData', valid=True) + audio_data_int_16 = (indata[:, 0] * 32767).astype(np.int16) + msg.rawAudioData.data = audio_data_int_16.tobytes() + msg.rawAudioData.sampleRate = SAMPLE_RATE + self.pm.send('rawAudioData', msg) + + with self.lock: + self.callback_count += 1 + self.last_audio_rms = float(np.sqrt(np.mean(np.square(indata[:, 0])))) + self.last_audio_peak = float(np.max(np.abs(indata[:, 0]))) + self.last_status = str(status) if status else None + self.measurements = np.concatenate((self.measurements, indata[:, 0])) + + while self.measurements.size >= FFT_SAMPLES: + measurements = self.measurements[:FFT_SAMPLES] + + self.sound_pressure, _ = calculate_spl(measurements) + measurements_weighted = apply_a_weighting(measurements) + self.sound_pressure_weighted, self.sound_pressure_level_weighted = calculate_spl(measurements_weighted) + + self.measurements = self.measurements[FFT_SAMPLES:] + + @retry(attempts=10, delay=3) + def get_stream(self, sd): + # reload sounddevice to reinitialize portaudio + sd._terminate() + sd._initialize() + requested_device = os.environ.get("MICD_DEVICE") + device = int(requested_device) if requested_device is not None else None + return sd.InputStream(channels=1, samplerate=SAMPLE_RATE, callback=self.callback, blocksize=SAMPLE_BUFFER, device=device) + + def micd_thread(self): + # sounddevice must be imported after forking processes + import sounddevice as sd + + device = sd.default.device + if os.environ.get("MICD_DEVICE") is not None: + device = int(os.environ["MICD_DEVICE"]) + sd.default.device = (device, device) + self.last_device = f"{device}: {sd.query_devices(device)['name']}" if isinstance(device, int) else str(device) + cloudlog.info(f"micd selecting input device {self.last_device}") + + while True: + try: + with self.get_stream(sd) as stream: + cloudlog.info(f"micd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}") + while True: + self.update() + except Exception: + # Some A1s wedge the audio DSP (ALSA EINVAL / ADSP_EFAILED until reboot). Dying here + # crash-loops the process and selfdrived raises a takeover alert mid-drive over a + # microphone - stay alive and keep retrying instead; recovers if the DSP comes back. + cloudlog.exception("micd: audio stream unavailable, retrying") + time.sleep(10) + + +def main(): + mic = Mic() + mic.micd_thread() + + +if __name__ == "__main__": + main() diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/sentry.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/sentry.py new file mode 100644 index 000000000..0205903d5 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/sentry.py @@ -0,0 +1,171 @@ +"""Install exception handler for process crash.""" +import os +import shutil +import traceback +from datetime import datetime +import sentry_sdk +from enum import Enum +from sentry_sdk.integrations.threading import ThreadingIntegration + +from iqpilot.common.params import Params +from iqpilot.konn3kt.registration import UNREGISTERED_DONGLE_ID +from iqpilot.system.hardware import HARDWARE +from iqpilot.system.hardware.hw import Paths +from iqpilot.common.swaglog import cloudlog +from iqpilot.system.version import get_build_metadata, get_version + +CRASHES_DIR = Paths.crash_log_root() +CRASH_UPLOADS_DIR = os.path.join(Paths.log_root(), "crash") + + +class SentryProject(Enum): + # python project + SELFDRIVE = "https://186a6736b7927e5ae9b92c869ba81b6b@o1138119.ingest.us.sentry.io/4508660076052480" + # native project + SELFDRIVE_NATIVE = SELFDRIVE + + +def _sentry_enabled() -> bool: + return os.getenv("IQPILOT_ENABLE_SENTRY", "0") == "1" + + +def _ensure_dir(path: str) -> None: + os.makedirs(path, exist_ok=True) + + +def _queue_crash_upload(src: str, name: str | None = None) -> None: + try: + _ensure_dir(CRASH_UPLOADS_DIR) + dest_name = name or os.path.basename(src) + shutil.copyfile(src, os.path.join(CRASH_UPLOADS_DIR, dest_name)) + except Exception: + cloudlog.exception("error when attempting to queue crash upload") + + +def report_tombstone(fn: str, message: str, contents: str) -> None: + cloudlog.error({'tombstone': message}) + + if not _sentry_enabled(): + return + + with sentry_sdk.configure_scope() as scope: + set_user() + scope.set_extra("tombstone_fn", fn) + scope.set_extra("tombstone", contents) + sentry_sdk.capture_message(message=message) + sentry_sdk.flush() + + +def capture_exception(*args, **kwargs) -> None: + cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1)) + + try: + save_exception(traceback.format_exc()) + + if not _sentry_enabled(): + return + + set_user() + sentry_sdk.capture_exception(*args, **kwargs) + sentry_sdk.flush() # https://github.com/getsentry/sentry-python/issues/291 + except Exception: + cloudlog.exception("sentry exception") + + +def save_exception(content: str) -> None: + try: + _ensure_dir(CRASHES_DIR) + commit = (get_build_metadata().openpilot.git_commit or "nocommit")[:8] + + dated_fn = os.path.join(CRASHES_DIR, datetime.now().strftime("%Y-%m-%d--%H-%M-%S.log")) + files = [ + dated_fn, + os.path.join(CRASHES_DIR, "error.log") + ] + + for fn in files: + with open(fn, 'w') as f: + if os.path.basename(fn) == "error.log": + lines = content.splitlines()[-3:] + f.write("\n".join(lines)) + else: + f.write(content) + + upload_name = f"{os.path.splitext(os.path.basename(dated_fn))[0]}_{commit}_python.log" + _queue_crash_upload(dated_fn, upload_name) + cloudlog.error(f"logged crash to {files}") + except Exception: + cloudlog.exception("error when attempting to save exception") + + +def capture_fingerprint_mock() -> None: + try: + set_user() + message = "car doesn't match any fingerprints" + sentry_sdk.capture_message(message=message, level="error") + sentry_sdk.flush() + except Exception as e: + cloudlog.exception(f"sentry fingerprint MOCK exception: {e}") + + +def capture_fingerprint(candidate: str, car_name: str) -> None: + try: + set_user() + sentry_sdk.set_tag("carFingerprint", candidate) + sentry_sdk.set_tag("carName", car_name) + + message = f"Fingerprinted {candidate}" + sentry_sdk.capture_message(message=message, level="info") + sentry_sdk.flush() + except Exception as e: + cloudlog.exception(f"sentry fingerprint exception: {e}") + + +def set_tag(key: str, value: str) -> None: + sentry_sdk.set_tag(key, value) + + +def set_user() -> None: + dongle_id, git_username = get_properties() + sentry_sdk.set_user({"id": dongle_id, "name": git_username}) + + +def get_properties() -> tuple[str, str]: + params = Params() + hardware_serial: str = params.get("HardwareSerial") or "" + git_username: str = params.get("GithubUsername") or "" + dongle_id: str = params.get("DongleId") or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" + + return dongle_id, git_username + + +def init(project: SentryProject) -> bool: + if not _sentry_enabled(): + cloudlog.info("Sentry disabled, using local crash logging + konn3kt uploader") + return False + + build_metadata = get_build_metadata() + + env = build_metadata.channel_type + dongle_id, git_username = get_properties() + + integrations = [] + if project == SentryProject.SELFDRIVE: + integrations.append(ThreadingIntegration(propagate_hub=True)) + + sentry_sdk.init(project.value, + default_integrations=False, + release=get_version(), + integrations=integrations, + traces_sample_rate=1.0, + max_value_length=8192, + environment=env) + + sentry_sdk.set_user({"id": dongle_id, "name": git_username}) + sentry_sdk.set_tag("dirty", build_metadata.openpilot.is_dirty) + sentry_sdk.set_tag("origin", build_metadata.openpilot.git_origin) + sentry_sdk.set_tag("branch", build_metadata.channel) + sentry_sdk.set_tag("commit", build_metadata.openpilot.git_commit) + sentry_sdk.set_tag("device", HARDWARE.get_device_type()) + + return True diff --git a/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/version.py b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/version.py new file mode 100644 index 000000000..cb731d009 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/_iqclosure/iqpilot/system/version.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +from dataclasses import dataclass +from functools import cache +import json +import os +import pathlib +import subprocess + +from iqpilot.common.basedir import BASEDIR +from iqpilot.common.swaglog import cloudlog +from iqpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date + +RELEASE_IQ_BRANCHES = ['release', 'release-tici', 'release-new'] +TESTED_BRANCHES = RELEASE_IQ_BRANCHES +IQ_BRANCH_MIGRATIONS: dict[tuple[str, str], str] = {} + +BUILD_METADATA_FILENAME = "build.json" + +training_version: str = "0.2.0" +terms_version: str = "2" + + +UNKNOWN_VERSION = "0.0.0" + + +@cache +def get_version(path: str = BASEDIR) -> str: + try: + with open(os.path.join(path, "iqpilot", "common", "version.h")) as _versionf: + return _versionf.read().split('"')[1] + except (OSError, IndexError): + return UNKNOWN_VERSION + + +def get_release_notes(path: str = BASEDIR) -> str: + for rel in (("iqpilot", "docs", "CHANGELOG.md"), ("docs", "CHANGELOG.md")): + try: + with open(os.path.join(path, *rel)) as f: + return f.read().split('\n\n', 1)[0] + except OSError: + continue + return "" + + +@cache +def is_prebuilt(path: str = BASEDIR) -> bool: + return os.path.exists(os.path.join(path, 'prebuilt')) + + +@cache +def is_dirty(cwd: str = BASEDIR) -> bool: + if not get_origin() or not get_short_branch(): + return True + + dirty = False + try: + # Actually check dirty files + if not is_prebuilt(cwd): + # This is needed otherwise touched files might show up as modified + try: + subprocess.check_call(["git", "update-index", "--refresh"], cwd=cwd) + except subprocess.CalledProcessError: + pass + + branch = get_branch() + if not branch: + return True + dirty = (subprocess.call(["git", "diff-index", "--quiet", branch, "--"], cwd=cwd)) != 0 + except subprocess.CalledProcessError: + cloudlog.exception("git subprocess failed while checking dirty") + dirty = True + + return dirty + + +@dataclass +class OpenpilotMetadata: + version: str + release_notes: str + git_commit: str + git_origin: str + git_commit_date: str + build_style: str + is_dirty: bool # whether there are local changes + + @property + def short_version(self) -> str: + return self.version.split('-')[0] + + @property + def comma_remote(self) -> bool: + # note to fork maintainers, this is used for release metrics. please do not + # touch this to get rid of the orange startup alert. there's better ways to do that + return self.git_normalized_origin == "github.com/commaai/openpilot" + + @property + def iqpilot_remote(self) -> bool: + return self.git_normalized_origin in ("github.com/iqpilot/iqpilot", + "github.com/iqpilot/openpilot") + + @property + def git_normalized_origin(self) -> str: + return self.git_origin \ + .replace("git@", "", 1) \ + .replace(".git", "", 1) \ + .replace("https://", "", 1) \ + .replace(":", "/", 1) + + +@dataclass +class BuildMetadata: + channel: str + openpilot: OpenpilotMetadata + + @property + def tested_channel(self) -> bool: + return self.channel in TESTED_BRANCHES + + @property + def release_channel(self) -> bool: + return self.channel in RELEASE_IQ_BRANCHES + + @property + def canonical(self) -> str: + return f"{self.openpilot.version}-{self.openpilot.git_commit}-{self.openpilot.build_style}" + + @property + def ui_description(self) -> str: + return f"{self.openpilot.version} / {self.openpilot.git_commit[:6]} / {self.channel}" + + @property + def master_channel(self) -> bool: + return self.channel in RELEASE_IQ_BRANCHES + + @property + def development_channel(self) -> bool: + return self.channel == "dev" or self.channel.startswith("dev-") or self.channel.endswith("-prebuilt") + + @property + def channel_type(self) -> str: + if "-tici" in self.channel or self.channel in ("release-new" or "master-mici"): + return "tici" + elif self.development_channel: + return "development" + elif self.tested_channel: + return "staging" + elif self.master_channel: + return "master" + elif self.release_channel: + return "release" + else: + return "feature" + + +def build_metadata_from_dict(build_metadata: dict) -> BuildMetadata: + channel = build_metadata.get("channel", "unknown") + openpilot_metadata = build_metadata.get("openpilot", {}) + version = openpilot_metadata.get("version", "unknown") + release_notes = openpilot_metadata.get("release_notes", "unknown") + git_commit = openpilot_metadata.get("git_commit", "unknown") + git_origin = openpilot_metadata.get("git_origin", "unknown") + git_commit_date = openpilot_metadata.get("git_commit_date", "unknown") + build_style = openpilot_metadata.get("build_style", "unknown") + return BuildMetadata(channel, + OpenpilotMetadata( + version=version, + release_notes=release_notes, + git_commit=git_commit, + git_origin=git_origin, + git_commit_date=git_commit_date, + build_style=build_style, + is_dirty=False)) + + +def get_build_metadata(path: str = BASEDIR) -> BuildMetadata: + build_metadata_path = pathlib.Path(path) / BUILD_METADATA_FILENAME + + if build_metadata_path.exists(): + build_metadata = json.loads(build_metadata_path.read_text()) + return build_metadata_from_dict(build_metadata) + + git_folder = pathlib.Path(path) / ".git" + + if git_folder.exists(): + return BuildMetadata(get_short_branch(path), + OpenpilotMetadata( + version=get_version(path), + release_notes=get_release_notes(path), + git_commit=get_commit(path), + git_origin=get_origin(path), + git_commit_date=get_commit_date(path), + build_style="unknown", + is_dirty=is_dirty(path))) + + cloudlog.exception("unable to get build metadata") + raise Exception("invalid build metadata") + + +if __name__ == "__main__": + print(get_build_metadata()) diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/__init__.py b/artifacts/iqpilot_emac_private/python/iqpilot_private/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/__init__.py b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/__init__.py new file mode 100644 index 000000000..5ece1c074 --- /dev/null +++ b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/__init__.py @@ -0,0 +1,3 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/bulk_transport.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..5f5fc962ef3a1ae0ed58f93f78db729dabb444be GIT binary patch literal 202152 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�PdHYMQ+V2o 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 literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_client.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..2c3698a90f94144b8206fc48c96e2445c17ce7d0 GIT binary patch literal 267376 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%0DhJ4U!I%paLqzu3w5 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

W@e`v}j>k(0B0- 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 F`x_KGy1oDa literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/mac_protocol.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..90bb87e162c60a576d516d03a72f0269feaa812b GIT binary patch literal 203544 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�(#@3Ef`&;;Z;`RpH0K!u7_%ku

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 G%KrebvF(5W literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_emac_private/python/iqpilot_private/emac/maciqmodeld.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..1b142e0695e1bc1838a099c5843e69267b8ff9cb GIT binary patch literal 333336 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 diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/big_catalog.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..cd3fa6b4366e60ca514a0c818db1d85b1e616608 GIT binary patch literal 133840 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 z

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 AUjP6A literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/egpu_model.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..ee7176c4443ab685c5ea8afc6a3d9215c084d2f6 GIT binary patch literal 68112 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 literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/emac_model_meta.cpython-312-aarch64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..422e0d5a1e74145463cc38517a3ea50e2bc1c2c0 GIT binary patch literal 68264 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

#aO8+x+y`;zC2&SK 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 Y@ki}a`TDkL9a1X4q<$ql$GF4%8+C{T+yDRo literal 0 HcmV?d00001 diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/fetcher.cpython-312-aarch64-linux-gnu.so index 208c82d5c6d4b93e72acdec72664408246bd52ab..90de4ba7abd81442bd2bf45e81ee2b15fce9dbb5 100755 GIT binary patch delta 562 zcmXv~y>68;3~iYD0I0enVq%PAJ5CnFz`z?YwoV)iq#}MFpktqakEwG9Hg0=`#K_FZ z%*10LELF{7Md$eZ`11Pr^7{B40W~Esbb}fkYzjBr;8ISz30|k%=^qG z+vEay`?^n$cLY?MYOy5(ynKz33z=+x_vzWrr2w&b2uh>gY7jY3+MeE|7lHO%y_HJS zL}+H#YRGfDx=Fh;a+#!+8WEPZ@-(nqs~jDjzIkiwJm%@^_wPxl_tf2@^$? zSi9w+SbY0>2#?nWR&uVO5!8T+WaK2>x4(z*Z1tM5v0;z7H-bna5lrg#^ftU$g{?}A z5tGf!Q4^0u0CBs%4eL1))MTS(a`fuWxHWaI%d&s-*89br``7Q^?MLd8&00<$~vf+_h7p!saw3BBU%ju`G*so25=J zO2X`-W2Qk6viTiQEuM6Sp_8b^8lz7tvo2Zgx_v%|C+h&}HLLVdgyZDqeK-#~{yv63 rVKwNjJDIeS!D@2g)LrcO>w9Pq9?oC@c|elM8>P*9czpgN{Cs&27Y(Nf diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/git_auth.cpython-312-aarch64-linux-gnu.so index 342b9ae9f3dec1306cdbcc583c6721990d51b801..622d8bb7ca841b9b62404ec08c710e77247a4035 100755 GIT binary patch delta 133 zcmXxcyA1*{3;@tWx|*6X;y53BfDGR#(q7&-d{y+D1iU~ delta 133 zcmXxbu?@m75Cza6rKV<#v+pk79w1@`DlDJlljvZTR2c&)9UYr63ZkHXdfPf~>-ap* zm#C7+S@IN?HbE080x1kbDYwVHh@yd2d=l%HDk*cVLH~~Gsk;XyQ07eD?H6SC`ExiS VlZm%bJ1rIwm7QU(`}O>Ke*wVaCguPD diff --git a/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so b/artifacts/iqpilot_model_selector_private/python/iqpilot_private/models/helpers.cpython-312-aarch64-linux-gnu.so index c3a061ecdd0884cd2e53caacec465821dbc73419..89887192c69cc1edea742df7d61727b770eb989f 100755 GIT binary patch delta 838 zcmYk4Ka1B#5XNtdi0|OOrPqj6n3?@I0|^9duGc2TdF;;2f{LK8!58suf^DjRjrnzw zP807AQ^m^CGNrX6a^SGtFgx>np6A1>n;%}?e2Hi2dn0V@L2f@ZK5cCJN5O}rl+ou5B^_Vj#p`&x_2 zw3dx=uwgadAG}Sc+rk(ypIs^-Ri$B23hVy*ZNlR z?hh{09VRX;V$8WUZ-|fqt=4+2clKogtYt0+Muo6BYc2ch%d~AHdq7xa;3x{bn0c;Q zb^rhC<_L#s_y|NMR&yUg1-0$BuhOmK);Uw1Bn88uz@kpA=zeyUPT0A(?&8cH$I@Ih z4}itjXPJyZMFlE{8y=w}fL84A=T+kHxKN)qXV}@<;uQ`V&W9WC(_d)|V|t6&DtN%D z@psrsx(*-zO^<)L7t^7$Q^Y#tVxF3hqMrQtEts^fv$i delta 838 zcmYk4v5uEF5X8>~h&Q;ega%PXUhl4VEfFaa&GpcvI8L_LDGoBC6Z+v)u<1aW(=}9>~l+*O!*KHPN5n}=&*U?+A z=DFwn+3WPvrntt`g;SO(_LY?w#oGSmb;{btqCwVZOjz0baz|F~M@MI8zdU|))>+u| zqa z*|I#dE_M{k;Mfd}Q2+41Q_==Xad5-zU|PngtJE_7=c3vwOkbH@ivb&p6XO2tUAnVr z;faogxVX~b&S+jb>>uBylVfiS1jAK$)>5gek>$Ssc%FVfj=Y^s108Hm?<0_NYx|$) z>DJa{R3T>POl!gF9U!x|{nL54X5-eQADOjc8;-zU&DMVVBHi48L>Mrai6-&1woxpH z?Zb<7M>UgX6)b&dr&8U^6rA`iZ$yP?k(_`+9eV`;YQDd`NZTg87wKyvX>qAmMs79Y z?VqolD`#FjxXPFmv<+BI2>08U>E^LStxkbFjCvhjse~@=rK3GyhijQ zIf~W+^F) zgulSbu>XOjB{da7*yUS063>H0pRd`sxij~kIp^H-Zr?j```(EkZXCOD{I}_k`wQuX zW2iaT5gjL=st?t;%=hn4n~Pi{^9Zc5sPeEFT(uI*eCE&T=^{}ZKKkNNQE{t2G+1mt z_h34`2)_3mSjfn;6m!F@d-d~JRb#cpRHe0;LUEzHF`BhL-&$4ssLG-lmwiO@QB!Ud zY`*(ol122wPI3_roV{m{=_y2=&pces40oNc~@>L(-lY?czK7q3)& za)WY68ym|jS9iNzySBW%tG12*ms?+5S-O(_kia1fDeS>h_Q^`!|NY^#d2G>qO%e=^ zhO!Ib$hQR4^Sh5Gx3G}Z)MxK3;h>|?Qu1Es3y-GLdm_ZWhEZFH?5Gh=B<;Mhp}W_2 zws)&;9DUwE0ar4_62(!V8Z~h_*5p^vZAZ6uzF2PUsD1Yq`lu@bJb3d$zNDbI9ZE-i zHQ?-O!qJU41EOawU0u%9jbmbK1hwn@!NK&=$s~BJ)>63}wO$4#k{0I+2h(EFiw|7d zC{~@c1jm8*cX`P-+{FN*?Y68OPpflGqq(LiXQPd$Sj0fQP~ zh-AlP+y`@^BJ+5L7iqvkD4N^Q!B%0*jtzJ7ooCbLUhY|Q?1^!!LJTv8G=9Z&`({VC zZ8^5K?HUaGhI!!kOd`rA2#tz-h|zWT-}xi29-YrD9Tun(8aPYPb97c;aelitn?HX( zon3GZ*3EHN(gH7Id8Hy4=3CDpn0G0AaIO|}1YR&F-8(nmeLih|){`f!j3Q)wF?{<+ffIxRr)OJSAjB3P?3t zuak5c5eegC2j#z?-;j}AJ@sMY6pF1iSQgijSk;%N z)wNcU2mgD3kBptH4I=rdf=mf(54d<`nH!eebLbt*g`b6CG}Z?s6EEfs=I#`FV4|{K b$66!)F&$J;1BqDbA8)>z_HVv==cn)wp&BxI delta 1915 zcmYk7z0X}o5yoHVAn2$fa%rN_M0R8C$L!8dgItovWkf@+W_EVL(wp}+F%T-8A`%T! zC?aiVQ~!4hit(@V+|f>bE-CTn2pk!dz41bH8(NWS%XOO&Yexo*iG-VzSxSyI*6khl+e9B%M~Gad*qbEUym`{Y9=` z1q-ZACl{k=QBxQvC&!x~o%eC;*73u0AE)?#yz$$c$6U$sa3W5bCe)xZX>z#tkFU4O zmkw1MQ=i>UVyQ7xB1!A|H`_b61wu#RnkiEEnv|5I zNj^F8^N-I?&(%*}eqNL^`{af`1Qa5}n9wX9E$yX`Grw{6>*I|xAAfvRx(BwYEs4wU z-XUfPCcPoPC%Cd_c)8<60%CJ%DW<}){UkuOm!HK4JV! zyWDZzd4#fJt)t-dkX770)j+DH`ntbIQA&5o4Io7uW0Zy)g>C)#TQDeIw1Fjvg)@OB zbrChb_rbT@wM&QC%j}M637D!Ror2U-s;;}=;fX6K;!Ign1i_}Gh;zv6(RbTV4^%9- z(Sk%r#xg5rZ^+d3_T%l^!8K+EVh|x&PL-2r44&5?9&dj=VA=xj%H~$7H2?r{rHu9V z6X2*CYaX$cP+Fnr0+l=Ff6XWRROOVCj9{wlOys4u2~>-Lz57R zZ~@pkN)E|e^Sz+|4SL8t^Q0koerM(0g9J(SRXleIgGY=JCfTvGL=>8Az5fi-#-cqG zEpUhi&jaF)2PR&3o}p=$BOFYWlMQVGx6Q0R^7_v+)D37;vj~)81x5<{?eK!Su0BV! zNwqpM#yB>eTpHjZlu>Ou|OCzID#lzs0up7{aS6?U;!9yxBfsoPjwB3C! z#;bNJz#f<2twB&2?R&jH;A~e{IE#`n?{AI^@!QA503{gwx=sb1hHhL~a7+RuIm0S; z!21m3FtgVQYE|$c-NLX@ic^U&)7SCTY0#Vif#JI_l`I$bzkl<{sHlz+XmBAxt1FOvWO diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py index 83ae1d661..25fb87a10 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/carcontroller.py @@ -454,10 +454,10 @@ class CarController(CarControllerBase): hda2 = self.CP.flags & HyundaiFlags.CANFD_HDA2 hda2_long = hda2 and self.CP.openpilotLongitudinalControl # steering control - if camera_scc: - can_sends.extend(hyundaicanfd.create_steering_messages_camera_scc(self.frame, self.packer, self.CP, self.CAN, CC, apply_steer_req, apply_torque, CS, apply_angle, self.lkas_max_torque, angle_control)) - else: - can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque, apply_angle, self.lkas_max_torque, angle_control)) + can_sends.extend(hyundaicanfd.create_steering_messages( + self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque, + apply_angle, self.lkas_max_torque, angle_control, + )) # prevent LFA from activating on HDA2 by sending "no lane lines detected" to ADAS ECU if self.frame % 5 == 0 and hda2 and not camera_scc: diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py index b9c5a5186..003a56e91 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/hyundai/hyundaicanfd.py @@ -12,20 +12,6 @@ LaneChangeDirection = log.LaneChangeDirection TurnDirection = log.Desire -def hyundai_crc8(data: bytes) -> int: - poly = 0x2F - crc = 0xFF - - for byte in data: - crc ^= byte - for _ in range(8): - if crc & 0x80: - crc = ((crc << 1) ^ poly) & 0xFF - else: - crc = (crc << 1) & 0xFF - - return crc ^ 0xFF - class CanBus(CanBusBase): def __init__(self, CP, fingerprint=None, lka_steering=None) -> None: super().__init__(CP, fingerprint) @@ -84,95 +70,6 @@ class CanBus(CanBusBase): # LKAS # 201 - 2a0 - - -def create_steering_messages_camera_scc(frame, packer, CP, CAN, CC, lat_active, apply_steer, CS, apply_angle, max_torque, angle_control): - - emergency_steering = False - if CS.adrv_0x161 is not None: - values = CS.adrv_0x161 - emergency_steering = values["ALERTS_1"] in [11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 26] - - - ret = [] - if CS.mdps is not None: - values = copy.copy(CS.mdps) - #rx_counter = values.pop("COUNTER", None) - if angle_control: - if CS.lfa_alt is not None: - values["LFA2_ACTIVE"] = CS.lfa_alt["LKAS_ANGLE_ACTIVE"] - else: - if CS.lfa is not None: - values["LKA_ACTIVE"] = 1 if CS.lfa["STEER_REQ"] == 1 else 0 - - if frame % 1000 < 40: - values["STEERING_COL_TORQUE"] += 220 - #ret.append(packer.make_can_msg("MDPS", CAN.CAM, values, rx_counter = rx_counter)) - ret.append(packer.make_can_msg("MDPS", CAN.CAM, values)) - - if frame % 10 == 0: - if CS.steer_touch_2af is not None: - values = copy.copy(CS.steer_touch_2af) - if frame % 1000 < 40: - values["TOUCH_DETECT"] = 3 - values["TOUCH1"] = 50 - values["TOUCH2"] = 50 - values["CHECKSUM_"] = 0 - dat = packer.make_can_msg("STEER_TOUCH_2AF", 0, values)[1] - values["CHECKSUM_"] = hyundai_crc8(dat[1:8]) - - ret.append(packer.make_can_msg("STEER_TOUCH_2AF", CAN.CAM, values)) - - if angle_control: - if CS.lfa_alt is not None: - values = copy.copy(CS.lfa_alt) - rx_counter = values.pop("COUNTER", None) - if emergency_steering: - pass - else: - #values = {} #CS.lfa_alt - values["LKAS_ANGLE_ACTIVE"] = 2 if CC.latActive else 1 - values["LKAS_ANGLE_CMD"] = -apply_angle - values["LKAS_ANGLE_MAX_TORQUE"] = max_torque if CC.latActive else 0 - ret.append(packer.make_can_msg("LFA_ALT", CAN.ECAN, values, rx_counter = rx_counter)) - - if CS.lfa is not None: - values = copy.copy(CS.lfa) - rx_counter = values.pop("COUNTER", None) - if not emergency_steering: - values["LKA_MODE"] = 0 - values["LKA_ICON"] = 2 if CC.latActive else 1 - values["TORQUE_REQUEST"] = -1024 # apply_steer, - values["VALUE63"] = 0 # LKA_ASSIST - values["STEER_REQ"] = 0 # 1 if lat_active else 0, - values["HAS_LANE_SAFETY"] = 0 # hide LKAS settings - values["LKA_ACTIVE"] = 3 if CC.latActive else 0 # this changes sometimes, 3 seems to indicate engaged - values["VALUE64"] = 0 #STEER_MODE, NEW_SIGNAL_2 - values["LKAS_ANGLE_CMD"] = -25.6 #-apply_angle, - values["LKAS_ANGLE_ACTIVE"] = 0 #2 if lat_active else 1, - values["LKAS_ANGLE_MAX_TORQUE"] = 0 #max_torque if lat_active else 0, - values["NEW_SIGNAL_1"] = 10 - ret.append(packer.make_can_msg("LFA", CAN.ECAN, values, rx_counter = rx_counter)) - - elif CS.lfa is not None: - values = {} - values["LKA_MODE"] = 2 - values["LKA_ICON"] = 2 if lat_active else 1 - values["TORQUE_REQUEST"] = apply_steer - values["STEER_REQ"] = 1 if lat_active else 0 - values["VALUE64"] = 0 # STEER_MODE, NEW_SIGNAL_2 - values["HAS_LANE_SAFETY"] = 0 - values["LKA_ACTIVE"] = 0 # NEW_SIGNAL_1 - - values["DampingGain"] = 0 if lat_active else 100 - #values["VALUE63"] = 0 - - #values["VALUE82_SET256"] = 0 - - ret.append(packer.make_can_msg("LFA", CAN.ECAN, values)) - - return ret - def create_steering_messages(packer, CP, CAN, enabled, lat_active, apply_steer, apply_angle, max_torque, angle_control): ret = [] diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py index 6f87a30e6..dbdd6bd36 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/carcontroller.py @@ -193,6 +193,22 @@ def accel_during_driver_override(accel: float, gas_pressed: bool, keep_long_acti return 0.0 if gas_pressed and keep_long_active else accel +def ea_send_ready(stock_values, last_counter): + return bool(stock_values) and stock_values["COUNTER"] != last_counter + + +EA_BLINKER_STEP = 2 + + +def next_ea_counter(tx_counter, stock_counter): + return ((stock_counter if tx_counter is None else tx_counter) + 1) % 16 + + +def ea_blinker_command(left_request, right_request, left_active, right_active): + blinker_active = left_active or right_active + return left_request and not blinker_active, right_request and not blinker_active + + class CarController(CarControllerBase): def __init__(self, dbc_names, CP, CP_IQ): from iqpilot.system.proprietary_runtime._verified_import import import_verified_module @@ -241,6 +257,8 @@ class CarController(CarControllerBase): self.sng_handoff_active = False self.acc_counter_seeded = False self.klr_counter_last = None + self.ea_counter_last = None + self.ea_tx_counter = None self.eps_timer_soft_disable_alert = False self.hca_frame_timer_running = 0 self.hca_frame_same_torque = 0 @@ -462,9 +480,14 @@ class CarController(CarControllerBase): self.klr_counter_last = CS.klr_stock_values["COUNTER"] if self.CP.flags & (VolkswagenFlags.MEB | VolkswagenFlags.MQB_EVO): - if self.frame % 2 == 0: + if CS.ea_hud_stock_values and self.frame % EA_BLINKER_STEP == 0: + self.ea_tx_counter = next_ea_counter(self.ea_tx_counter, CS.ea_hud_stock_values["COUNTER"]) + left_blinker, right_blinker = ea_blinker_command( + CC.leftBlinker, CC.rightBlinker, CS.left_blinker_active, CS.right_blinker_active, + ) can_sends.append(self.CCS.create_blinker_control(self.packer_pt, self.CAN.pt, CS.ea_hud_stock_values, CS.ea_control_stock_values, - CC.leftBlinker, CC.rightBlinker, self.hide_ea_error)) + left_blinker, right_blinker, self.hide_ea_error, self.ea_tx_counter)) + self.ea_counter_last = CS.ea_hud_stock_values["COUNTER"] if self.CP.openpilotLongitudinalControl and self.CCS in (mqbcan, mlbcan) and not self.acc_counter_seeded and CS.acc_stock_counters: seed_msgs = ("ACC_01", "ACC_02") if self.CCS is mlbcan else ("ACC_02", "ACC_06", "ACC_07", "ACC_10") diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mebcan.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mebcan.py index 6d2a031ee..05de04a8a 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mebcan.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/mebcan.py @@ -55,8 +55,9 @@ def create_eps_update(packer, bus, eps_stock_values, ea_simulated_torque): return packer.make_can_msg("LH_EPS_03", bus, values) -def create_blinker_control(packer, bus, ea_hud_stock_values, ea_control_stock_values, left_blinker, right_blinker, hide_error): +def create_blinker_control(packer, bus, ea_hud_stock_values, ea_control_stock_values, left_blinker, right_blinker, hide_error, counter=None): values = {s: ea_hud_stock_values[s] for s in [ + "COUNTER", "EA_Texte", "ACF_Lampe_Hands_Off", "EA_Infotainment_Anf", @@ -69,6 +70,9 @@ def create_blinker_control(packer, bus, ea_hud_stock_values, ea_control_stock_va "EA_Unknown", ]} + if counter is not None: + values["COUNTER"] = counter + if ea_hud_stock_values["EA_Blinken"] == 0: values.update({ "EA_Blinken": 1 if left_blinker else (2 if right_blinker else ea_hud_stock_values["EA_Blinken"]), diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_meb_nav_blinker.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_meb_nav_blinker.py index 9c126d7a6..ff567fa38 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_meb_nav_blinker.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_meb_nav_blinker.py @@ -2,9 +2,11 @@ from iqdbc.can import CANPacker, CANParser from iqdbc.car.volkswagen import mebcan +from iqdbc.car.volkswagen.carcontroller import ea_blinker_command, ea_send_ready, next_ea_counter EA_HUD_VALUES = { + "COUNTER": 0, "EA_Texte": 0, "ACF_Lampe_Hands_Off": 0, "EA_Infotainment_Anf": 0, @@ -23,21 +25,74 @@ def blinker_values(dbc, requests): packer = CANPacker(dbc) parser = CANParser(dbc, [("EA_02", 50)], 0) values = [] - for left, right in requests: + for counter, stock_blinker, left, right in requests: + ea_hud_values = {**EA_HUD_VALUES, "COUNTER": counter, "EA_Blinken": stock_blinker} msg = mebcan.create_blinker_control( - packer, 0, EA_HUD_VALUES, EA_CONTROL_VALUES, left, right, False, + packer, 0, ea_hud_values, EA_CONTROL_VALUES, left, right, False, ) parser.update([0, [msg]]) - values.append(int(parser.vl["EA_02"]["EA_Blinken"])) + values.append((int(parser.vl["EA_02"]["COUNTER"]), int(parser.vl["EA_02"]["EA_Blinken"]))) return values +def blinker_value(dbc, counter, stock_blinker, left, right, override_counter=None): + packer = CANPacker(dbc) + parser = CANParser(dbc, [("EA_02", 50)], 0) + ea_hud_values = {**EA_HUD_VALUES, "COUNTER": counter, "EA_Blinken": stock_blinker} + msg = mebcan.create_blinker_control( + packer, 0, ea_hud_values, EA_CONTROL_VALUES, left, right, False, override_counter, + ) + parser.update([0, [msg]]) + return int(parser.vl["EA_02"]["COUNTER"]), int(parser.vl["EA_02"]["EA_Blinken"]) + + def test_nav_blinker_request_remains_asserted_until_released(): - requests = [(True, False)] * 100 + [(False, False)] - assert blinker_values("vw_meb", requests) == [1] * 100 + [0] + requests = [(counter % 16, 0, True, False) for counter in range(100)] + [(4, 0, False, False)] + assert blinker_values("vw_meb", requests) == [(counter % 16, 1) for counter in range(100)] + [(4, 0)] def test_nav_blinker_directions_on_meb_variants(): - requests = [(True, False), (False, True), (False, False)] + requests = [(7, 0, True, False), (8, 0, False, True), (9, 0, False, False)] for dbc in ("vw_meb", "vw_meb_2024", "vw_mqbevo"): - assert blinker_values(dbc, requests) == [1, 2, 0] + assert blinker_values(dbc, requests) == [(7, 1), (8, 2), (9, 0)] + + +def test_stock_blinker_has_priority_over_nav_request(): + requests = [(10, 1, False, True), (11, 2, True, False), (12, 3, True, False)] + assert blinker_values("vw_meb", requests) == [(10, 1), (11, 2), (12, 3)] + + +def test_ea_frame_is_sent_once_per_stock_counter(): + last_counter = None + sends = [] + for counter in (5, 5, 5, 6, 6, 7): + stock_values = {"COUNTER": counter} + if ea_send_ready(stock_values, last_counter): + sends.append(counter) + last_counter = counter + assert sends == [5, 6, 7] + + +def test_blinker_counter_can_continue_between_stock_frames(): + assert blinker_value("vw_meb", 8, 0, True, False, 9) == (9, 1) + assert blinker_value("vw_meb", 8, 0, True, False, 10) == (10, 1) + + +def test_stock_blinker_priority_survives_counter_override(): + assert blinker_value("vw_meb", 8, 2, True, False, 9) == (9, 2) + + +def test_nav_blinker_retriggers_only_between_lamp_cycles(): + assert ea_blinker_command(True, False, False, False) == (True, False) + assert ea_blinker_command(True, False, True, False) == (False, False) + assert ea_blinker_command(True, False, False, False) == (True, False) + assert ea_blinker_command(False, True, False, True) == (False, False) + + +def test_blinker_counter_runs_at_20_ms(): + tx_counter = None + counters = [] + for _ in range(10): + tx_counter = next_ea_counter(tx_counter, 14) + counters.append(tx_counter) + assert counters == [15, 0, 1, 2, 3, 4, 5, 6, 7, 8] diff --git a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py index b791b8fb3..96e44387e 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py +++ b/artifacts/package_sources/iqdbc/iqdbc/car/volkswagen/tests/test_volkswagen.py @@ -210,6 +210,7 @@ def _a4_mk4_frames(packer, reverse=False, eps_torque=None): packer.make_can_msg("Kombi_02", 1, {"KBI_Inhalt_Tank": 40, "KBI_Kilometerstand": 100000}), packer.make_can_msg("Airbag_02", 1, {"AB_Gurtschloss_FA": 3}), packer.make_can_msg("Gateway_05", 1, {"BCM1_Rueckfahrlicht_Schalter": int(reverse)}), + packer.make_can_msg("LH_EPS_01", 1, {}), ] if eps_torque is not None: msgs.append(packer.make_can_msg("LH_EPS_03", 1, {"EPS_Lenkmoment": abs(eps_torque), diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/hyundai_canfd.h b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/hyundai_canfd.h index 3a7d8c8df..34937a977 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/modes/hyundai_canfd.h +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/modes/hyundai_canfd.h @@ -7,7 +7,7 @@ #pragma GCC diagnostic ignored "-Wunused-function" const TorqueSteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = { - .max_torque = 512, //270, + .max_torque = 270, .max_rt_delta = 112, .max_rate_up = 2, @@ -85,34 +85,34 @@ const CanMsg HYUNDAI_CANFD_HDA2_LONG_TX_MSGS[] = { {203, 0, 24}, // CB {373, 2, 24}, // TCS(0x175) {506, 2, 32}, // CLUSTER_SPEED_LIMIT - {234, 2, 24}, // MDPS - {687, 2, 8}, // STEER_TOUCH_2AF - {0x4BE, 2, 8}, // NEW_MSG_4BE (may be corner radar enabler x) {0x4B9, 2, 8}, // NEW_MSG_4B9 (may be corner radar enabler) }; +#define HYUNDAI_CANFD_HDA1_TX_MSGS_COMMON(longitudinal) \ + {0x12A, 0, 16, .check_relay = true}, \ + {0x1A0, 0, 32, .check_relay = (longitudinal)}, \ + {0x1CF, 0, 8, .check_relay = false}, \ + {0x1CF, 2, 8, .check_relay = (longitudinal)}, \ + {0x1E0, 0, 16, .check_relay = true}, \ + {0x160, 0, 16, .check_relay = (longitudinal)}, \ + {0x7D0, 0, 8, .check_relay = false}, \ + {0x1AA, 2, 16, .check_relay = (longitudinal)}, \ + {0x0CB, 0, 24, .check_relay = (longitudinal)}, \ + {0x175, 2, 24, .check_relay = (longitudinal)}, \ + {0x161, 0, 32, .check_relay = (longitudinal)}, \ + {0x162, 0, 32, .check_relay = (longitudinal)}, \ + {0x1EA, 0, 32, .check_relay = (longitudinal)}, \ + {0x200, 0, 8, .check_relay = (longitudinal)}, \ + {0x4A3, 2, 8, .check_relay = false}, \ + {0x4B4, 2, 8, .check_relay = false}, \ + const CanMsg HYUNDAI_CANFD_HDA1_TX_MSGS[] = { - {0x12A, 0, 16}, // LFA - {0x1A0, 0, 32}, // CRUISE_INFO - {0x1CF, 0, 8}, // CRUISE_BUTTON - {0x1CF, 2, 8}, // CRUISE_BUTTON - {0x1E0, 0, 16}, // LFAHDA_CLUSTER - {0x160, 0, 16}, // ADRV_0x160 - {0x7D0, 0, 8}, // tester present for radar ECU disable - {0x1AA, 2, 16}, // CRUISE_ALT_BUTTONS , carrot - {203, 0, 24}, // CB - {373, 2, 24}, // TCS(0x175) - - {353, 0, 32}, // ADRV_353 - {354, 0, 32}, // CORNER_RADAR_HIGHWAY - {512, 0, 8}, // ADRV_0x200 - {1187, 2, 8}, // 4A3 - {1204, 2, 8}, // 4B4 - {373, 2, 24}, // TCS(0x175) - {234, 2, 24}, // MDPS - {687, 2, 8}, // STEER_TOUCH_2AF + HYUNDAI_CANFD_HDA1_TX_MSGS_COMMON(false) +}; +const CanMsg HYUNDAI_CANFD_HDA1_LONG_TX_MSGS[] = { + HYUNDAI_CANFD_HDA1_TX_MSGS_COMMON(true) }; @@ -221,7 +221,6 @@ const int HYUNDAI_PARAM_CANFD_ALT_BUTTONS = 32; const int HYUNDAI_PARAM_CANFD_HDA2_ALT_STEERING = 128; bool hyundai_canfd_alt_buttons = false; bool hyundai_canfd_hda2_alt_steering = false; -bool hyundai_canfd_buffered_fwd = false; int hyundai_canfd_hda2_get_lkas_addr(void) { return hyundai_canfd_hda2_alt_steering ? 0x110 : 0x50; @@ -275,8 +274,6 @@ CanfdTxState canfd_tx_states[] = { {0x1AA, 2, 50, 0U, 0U}, // 426: CRUISE_ALT_BUTTONS {0x1CF, 2, 50, 0U, 0U}, // 463: CRUISE_BUTTON {0x1FA, 2, 10, 0U, 0U}, // 506: CLUSTER_SPEED_LIMIT - {0x0EA, 2, 100, 0U, 0U}, // 234: MDPS - {0x2AF, 2, 10, 0U, 0U}, // 687: STEER_TOUCH_2AF {0x4A3, 2, 5, 0U, 0U}, // 1187: HDA_INFO_4A3 {0x4B4, 2, 10, 0U, 0U}, // 1204: NEW_MSG_4B4 {0x4BE, 2, 10, 0U, 0U}, // 1214: NEW_MSG_4BE @@ -295,30 +292,6 @@ static CanfdTxState* find_canfd_tx_state(int bus, int addr) { } -static void hyundai_canfd_set_counter(CANPacket_t* to_push, uint8_t counter) { - if (GET_LEN(to_push) == 8U) { - to_push->data[1] = (to_push->data[1] & 0x0FU) | ((counter & 0x0FU) << 4); - } - else { - to_push->data[2] = counter; - } -} - -static void hyundai_canfd_set_checksum(CANPacket_t* to_push, uint16_t checksum) { - to_push->data[0] = (uint8_t)(checksum & 0xFFU); - to_push->data[1] = (uint8_t)((checksum >> 8U) & 0xFFU); -} - -static void hyundai_canfd_update_checksum(CANPacket_t* to_push) { - to_push->data[0] = 0U; - to_push->data[1] = 0U; - uint32_t checksum = hyundai_common_canfd_compute_checksum(to_push); - hyundai_canfd_set_checksum(to_push, (uint16_t)checksum); -} -static void canfd_apply_counter_and_update_checksum(CANPacket_t* dst, uint8_t counter) { - hyundai_canfd_set_counter(dst, counter); - hyundai_canfd_update_checksum(dst); -} static void canfd_record_tx_time(int bus, int addr, bool tx) { CanfdTxState* st = find_canfd_tx_state(bus, addr); if (st != NULL) { @@ -334,126 +307,6 @@ static bool canfd_should_block_fwd(int tx_bus, int addr, uint32_t now) { return (now - st->last_tx_us) < st->timeout_us; } -#define CANFD_BFWD_MAX_QUEUE 2 -#define CANFD_BFWD_REUSE_MAX 2 - -typedef struct { - int addr; - int dst_bus; - bool enabled; - - bool started; - uint8_t head; - uint8_t tail; - uint8_t count; - - uint8_t reuse_left; - bool has_last_pkt; - CANPacket_t last_pkt; - - CANPacket_t q[CANFD_BFWD_MAX_QUEUE]; -} CanfdBufferedFwd; - -CanfdBufferedFwd canfd_bfwd[] = { - {.addr = 0x1A0, .dst_bus = 0, .enabled = true }, // SCC_CONTROL - {.addr = 0x12A, .dst_bus = 0, .enabled = true }, // LFA - {.addr = 0x0CB, .dst_bus = 0, .enabled = true }, // LFA_ALT - {.addr = 0x0EA, .dst_bus = 2, .enabled = true }, // MDPS - {.addr = 0x1AA, .dst_bus = 2, .enabled = true }, // CRUISE_ALT_BUTTONS - // {.addr = 0x1CF, .dst_bus = 2, .enabled = true }, // CRUISE_BUTTON - {.addr = 0x175, .dst_bus = 2, .enabled = true }, // TCS - { 0 }, -}; - -static void canfd_copy_packet(CANPacket_t* dst, const CANPacket_t* src) { - dst->fd = src->fd; - dst->returned = 0U; - dst->rejected = 0U; - dst->extended = src->extended; - dst->addr = src->addr; - dst->bus = src->bus; - dst->data_len_code = src->data_len_code; - for (uint8_t i = 0U; i < GET_LEN(src); i++) { - dst->data[i] = src->data[i]; - } -} -static CanfdBufferedFwd* canfd_bfwd_find(int addr, int dst_bus) { - for (int i = 0; canfd_bfwd[i].addr > 0; i++) { - if (canfd_bfwd[i].enabled && - (canfd_bfwd[i].addr == addr) && - (canfd_bfwd[i].dst_bus == dst_bus)) { - return &canfd_bfwd[i]; - } - } - return NULL; -} - -static void canfd_bfwd_reset(CanfdBufferedFwd* st) { - st->started = false; - st->head = 0U; - st->tail = 0U; - st->count = 0U; - st->reuse_left = 0U; - st->has_last_pkt = false; - st->last_pkt = (CANPacket_t){0}; -} -static void canfd_bfwd_push(CanfdBufferedFwd* st, const CANPacket_t* pkt) { - if ((st == NULL) || !st->enabled) return; - if (GET_BUS(pkt) != st->dst_bus) return; - - // queue�� �̹� 2���� �̹� �� packet�� ���� - if (st->count >= CANFD_BFWD_MAX_QUEUE) { - return; - } - - canfd_copy_packet(&st->q[st->tail], pkt); - st->tail = (st->tail + 1U) % CANFD_BFWD_MAX_QUEUE; - st->count++; - st->started = true; -} - -static bool canfd_bfwd_pop(CanfdBufferedFwd* st, CANPacket_t* pkt) { - if ((st == NULL) || !st->enabled) { - return false; - } - - if (!st->started || (st->count == 0U)) { - return false; - } - - canfd_copy_packet(pkt, &st->q[st->head]); - st->head = (st->head + 1U) % CANFD_BFWD_MAX_QUEUE; - st->count--; - - // ������ ���� packet ���� - canfd_copy_packet(&st->last_pkt, pkt); - st->has_last_pkt = true; - st->reuse_left = CANFD_BFWD_REUSE_MAX; - - if (st->count == 0U) { - st->started = false; - } - - return true; -} -static bool canfd_bfwd_reuse_last(CanfdBufferedFwd* st, CANPacket_t* pkt) { - if ((st == NULL) || !st->enabled) { - return false; - } - - if (!st->has_last_pkt || (st->reuse_left == 0U)) { - return false; - } - - canfd_copy_packet(pkt, &st->last_pkt); - st->reuse_left--; - - return true; -} - - - - static void hyundai_canfd_rx_hook(const CANPacket_t *to_push) { int bus = GET_BUS(to_push); int addr = GET_ADDR(to_push); @@ -538,26 +391,7 @@ static void hyundai_canfd_rx_hook(const CANPacket_t *to_push) { } -static bool hyundai_canfd_tx_hook(const CANPacket_t *to_send_const) { - CANPacket_t* to_send = (CANPacket_t*)to_send_const; - - const TorqueSteeringLimits HYUNDAI_CANFD_STEERING_LIMITS = { - .max_torque = 512, - .max_rt_delta = 112, - - .max_rate_up = 10, - .max_rate_down = 10, - .driver_torque_allowance = 250, - .driver_torque_multiplier = 2, - .type = TorqueDriverLimited, - - // the EPS faults when the steering angle is above a certain threshold for too long. to prevent this, - // we allow setting torque actuation bit to 0 while maintaining the requested torque value for two consecutive frames - .min_valid_request_frames = 89, - .max_invalid_request_frames = 2, - .min_valid_request_rt_interval = 810000, // 810ms; a ~10% buffer on cutting every 90 frames - .has_steer_req_tolerance = true, - }; +static bool hyundai_canfd_tx_hook(const CANPacket_t *to_send) { bool tx = true; int addr = GET_ADDR(to_send); @@ -618,14 +452,6 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *to_send_const) { if (violation) { tx = false; } - else if (hyundai_canfd_buffered_fwd) { - CanfdBufferedFwd* bfwd = canfd_bfwd_find(addr, GET_BUS(to_send)); - if (bfwd != NULL) { - canfd_bfwd_push(bfwd, to_send); - //tx = false; - return true; - } - } canfd_record_tx_time(GET_BUS(to_send), addr, tx); @@ -633,6 +459,10 @@ static bool hyundai_canfd_tx_hook(const CANPacket_t *to_send_const) { } static bool hyundai_canfd_fwd_hook(int bus_num, int addr) { + if (!hyundai_canfd_hda2) { + return (bus_num == 0) && (addr == 0x4B9); + } + uint32_t now = microsecond_timer_get(); if (bus_num == 0) { if (canfd_should_block_fwd(2, addr, now)) { @@ -653,16 +483,11 @@ static safety_config hyundai_canfd_init(uint16_t param) { canfd_tx_states[i].last_tx_us = 0U; } - for (int i = 0; canfd_bfwd[i].addr > 0; i++) { - canfd_bfwd_reset(&canfd_bfwd[i]); - } - hyundai_common_init(param); gen_crc_lookup_table_16(0x1021, hyundai_canfd_crc_lut); hyundai_canfd_alt_buttons = GET_FLAG(param, HYUNDAI_PARAM_CANFD_ALT_BUTTONS); hyundai_canfd_hda2_alt_steering = GET_FLAG(param, HYUNDAI_PARAM_CANFD_HDA2_ALT_STEERING); - hyundai_canfd_buffered_fwd = hyundai_camera_scc; // no long for radar-SCC HDA1 yet //if (!hyundai_canfd_hda2 && !hyundai_camera_scc) { @@ -680,8 +505,8 @@ static safety_config hyundai_canfd_init(uint16_t param) { else ret = BUILD_SAFETY_CFG(hyundai_canfd_hda2_long_rx_checks, HYUNDAI_CANFD_HDA2_LONG_TX_MSGS); } } else { - ret = hyundai_canfd_alt_buttons ? BUILD_SAFETY_CFG(hyundai_canfd_long_alt_buttons_rx_checks, HYUNDAI_CANFD_HDA1_TX_MSGS) : \ - BUILD_SAFETY_CFG(hyundai_canfd_long_rx_checks, HYUNDAI_CANFD_HDA1_TX_MSGS); + ret = hyundai_canfd_alt_buttons ? BUILD_SAFETY_CFG(hyundai_canfd_long_alt_buttons_rx_checks, HYUNDAI_CANFD_HDA1_LONG_TX_MSGS) : \ + BUILD_SAFETY_CFG(hyundai_canfd_long_rx_checks, HYUNDAI_CANFD_HDA1_LONG_TX_MSGS); } } else { if (hyundai_canfd_hda2 && hyundai_camera_scc) { diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_canfd.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_canfd.py index 8b84f8169..a30c2e0f6 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_canfd.py +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_canfd.py @@ -44,12 +44,12 @@ def test_canfd_steering_limits(safety, param, addr, length): assert not safety.safety_tx_hook(canfd_steer(addr, length, 1)) safety.set_controls_allowed(True) - assert safety.safety_tx_hook(canfd_steer(addr, length, 10)) - safety.set_desired_torque_last(512) - safety.set_rt_torque_last(512) - assert safety.safety_tx_hook(canfd_steer(addr, length, 512)) - assert not safety.safety_tx_hook(canfd_steer(addr, length, 513)) - assert not safety.safety_tx_hook(canfd_steer(addr, length, -513)) + assert safety.safety_tx_hook(canfd_steer(addr, length, 2)) + safety.set_desired_torque_last(270) + safety.set_rt_torque_last(270) + assert safety.safety_tx_hook(canfd_steer(addr, length, 270)) + assert not safety.safety_tx_hook(canfd_steer(addr, length, 271)) + assert not safety.safety_tx_hook(canfd_steer(addr, length, -271)) def test_canfd_tx_whitelist_and_buttons(safety): @@ -90,6 +90,31 @@ def test_canfd_camera_scc_button_passthrough(safety): assert safety.safety_tx_hook(packet(0x1CF, 2, 8, {2: 4})) +def test_canfd_camera_scc_single_owner_forwarding(safety): + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, HyundaiSafetyFlags.CAMERA_SCC) + safety.init_tests() + safety.set_timer(1_000_000) + + assert safety.safety_fwd_hook(2, 0x12A) == -1 + assert safety.safety_fwd_hook(2, 0x1E0) == -1 + assert safety.safety_fwd_hook(2, 0x1A0) == 0 + assert safety.safety_fwd_hook(0, 0xEA) == 2 + assert not safety.safety_tx_hook(packet(0xEA, 2, 24)) + assert not safety.safety_tx_hook(packet(0x2AF, 2, 8)) + + +def test_canfd_camera_scc_longitudinal_single_owner_forwarding(safety): + param = HyundaiSafetyFlags.CAMERA_SCC | HyundaiSafetyFlags.LONG + safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, param) + safety.init_tests() + safety.set_timer(1_000_000) + + assert safety.safety_fwd_hook(2, 0x12A) == -1 + assert safety.safety_fwd_hook(2, 0x1A0) == -1 + assert safety.safety_fwd_hook(0, 0x175) == -1 + assert safety.safety_fwd_hook(0, 0xEA) == 2 + + def test_canfd_stock_longitudinal_only_allows_cancel(safety): safety.set_safety_hooks(CarParams.SafetyModel.hyundaiCanfd, 0) safety.init_tests() diff --git a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_controller.py b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_controller.py index 71c14b5b1..810665a48 100644 --- a/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_controller.py +++ b/artifacts/package_sources/iqdbc/iqdbc/safety/tests/test_hyundai_controller.py @@ -2,7 +2,7 @@ import pytest from iqdbc.car import gen_empty_fingerprint, structs from iqdbc.car.hyundai.interface import CarInterface -from iqdbc.car.hyundai.values import CAR +from iqdbc.car.hyundai.values import CAR, HyundaiFlags from iqdbc.safety.tests.libsafety import libsafety_py @@ -37,3 +37,24 @@ def test_controller_frames_match_configured_safety(candidate, alpha_long, monkey packet = libsafety_py.make_CANPacket(address, bus, data) rejection = f"{candidate.value} frame {frame}: safety rejected address={address:#x} bus={bus} data={data.hex()}" assert safety.safety_tx_hook(packet), rejection + + +def test_ev6_camera_scc_has_one_lfa_sender(monkeypatch, tmp_path): + monkeypatch.setenv("PARAMS_ROOT", str(tmp_path)) + fingerprint = gen_empty_fingerprint() + cp = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], False, False, False) + cp.flags &= ~HyundaiFlags.CANFD_HDA2.value + cp.flags |= HyundaiFlags.CANFD_CAMERA_SCC.value + cp_iq = CarInterface.get_params_iq(cp, CAR.KIA_EV6, fingerprint, [], False, False, False) + interface = CarInterface(cp, cp_iq) + interface.update([]) + + control = structs.CarControl.new_message() + control.enabled = True + control.latActive = True + control.actuators.torque = 0.01 + + for _ in range(20): + _, can_sends = interface.apply(control.as_reader(), structs.IQCarControl()) + assert [(address, bus) for address, _, bus in can_sends if address == 0x12A] == [(0x12A, 0)] + assert all(address not in (0xEA, 0x2AF) for address, _, _ in can_sends) diff --git a/artifacts/package_sources/panda/board/drivers/can_common.h b/artifacts/package_sources/panda/board/drivers/can_common.h index 7d4b57575..6d0385a53 100644 --- a/artifacts/package_sources/panda/board/drivers/can_common.h +++ b/artifacts/package_sources/panda/board/drivers/can_common.h @@ -307,7 +307,7 @@ void ignition_can_hook(CANPacket_t *msg) { vw_meb_gateway_out_of_p = (fahrstufe >= 6) && (fahrstufe <= 14); } - if ((msg->addr == 0xC0U) && (len == 32)) { + if (((msg->addr == 0xC0U) && (len == 32)) || ((msg->addr == 0x20AU) && (len == 64))) { vw_meb_seen = true; } diff --git a/iqpilot/cereal/custom.capnp b/iqpilot/cereal/custom.capnp index 1e813ec0c..cc561146a 100644 --- a/iqpilot/cereal/custom.capnp +++ b/iqpilot/cereal/custom.capnp @@ -131,6 +131,7 @@ struct IQModelManager @0xe91d6987759290bb { policy @3; offPolicy @4; onPolicy @5; + usbeMac @6; } } @@ -440,6 +441,9 @@ struct IQCarState @0xb1c39318bb6bc2b3 { # VW PQ stock ACC radar feedback for the IQ.Dynamics radar_manager (Blend feature) accRadarStaAdr @4 :UInt8; # ACC_System.ACS_Sta_ADR (0 not-active, 1 active, 2 passive, 3 irrev_Fehler) accRadarFehler @5 :Bool; # ACC_System.ACS_Fehler (stored fault -> radar dead for the drive) + slcSetSpeedRequestId @6 :UInt32; + slcSetSpeedGestureId @7 :UInt32; + slcSetSpeedRequestKph @8 :Float32; } struct IQLiveData @0xf2e2b608e51f4b0e { diff --git a/iqpilot/cereal/log.capnp b/iqpilot/cereal/log.capnp index 549a05f45..fb080df1c 100644 --- a/iqpilot/cereal/log.capnp +++ b/iqpilot/cereal/log.capnp @@ -135,6 +135,8 @@ struct OnroadEvent @0xc4fa6047f024e718 { userBookmark @95; excessiveActuation @96; audioFeedback @97; + bigModelLoading @101; + bigModelFailed @102; soundsUnavailableDEPRECATED @47; } @@ -491,6 +493,13 @@ struct DeviceState @0xa4d8b5af2aa492eb { started @11 :Bool; startedMonoTime @13 :UInt64; + # ordinals track commaai/openpilot exactly (@50 bottomSocTempC, @51 dock + # presence, @52 usbState) so upstream tooling stays able to read our logs; + # only the field NAME differs, we do not use comma's internal dock codename. + bottomSocTempC @50 :Float32; + egpuDockPresent @51 :Bool; + usbState @52 :UsbState; + # system utilization freeSpacePercent @7 :Float32; memoryUsagePercent @19 :Int8; @@ -586,6 +595,37 @@ struct DeviceState @0xa4d8b5af2aa492eb { nvmeTempCDEPRECATED @35 :List(Float32); } +struct UsbState { + devices @0 :List(Device); + # IQ extension (no upstream equivalent): controller-level link errors, needed + # because portli is a CONTROLLER counter and in peripheral mode (eMac gadget + # link) the peer never enumerates, so no Device row can carry it. + linkErrorCount @1 :UInt32; + # IQ extension, same reason: CC orientation describes the PORT, so it is + # readable while the link is in peripheral mode. The per-device field below + # matches upstream but only populates when something enumerates on that + # controller (host mode, e.g. the eGPU dock) — never during an eMac session. + usb3Lane @2 :Device.Usb3Lane; + + struct Device { + busnum @0 :UInt8; + devnum @1 :UInt8; + vendorId @2 :UInt16; + productId @3 :UInt16; + speedMbps @4 :UInt16; + manufacturer @6 :Text; + product @5 :Text; + linkErrorCount @7 :UInt16; + usb3Lane @8 :Usb3Lane; + + enum Usb3Lane { + unknown @0; + a @1; + b @2; + } + } +} + struct PandaState @0xa7649e2575e4591e { ignitionLine @2 :Bool; rxBufferOverflow @7 :UInt32; @@ -745,6 +785,20 @@ struct PeripheralState { } } +struct EgpuDockState { + tempC @0 :Float32; + memoryTempC @1 :Float32; + powerDrawW @2 :Float32; + powerLimitW @3 :Float32; + gpuUsagePercent @4 :UInt8; + gpuClockMhz @5 :UInt16; + fanSpeedRpm @6 :UInt16; + pcieLtssm @7 :UInt8; + supplyVoltage @8 :UInt16; # mV + supplyCurrent @9 :Int16; # mA + supplyFault @10 :Bool; +} + struct RadarState @0x9a185389d6fdd05f { mdMonoTime @6 :UInt64; carStateMonoTime @11 :UInt64; @@ -1078,6 +1132,7 @@ struct ModelDataV2 { timestampEof @3 :UInt64; modelExecutionTime @15 :Float32; rawPredictions @16 :Data; + big @27 :Bool; # predicted future position, orientation, etc.. position @4 :XYZTData; @@ -2668,6 +2723,7 @@ struct Event { iqState @153 :Custom.IQState; iqModelManager @154 :Custom.IQModelManager; iqPlan @155 :Custom.IQPlan; + egpuDockState @166 :EgpuDockState; iqOnroadEvents @156 :Custom.IQOnroadEvent; iqCarParams @157 :Custom.IQCarParams; iqCarControl @158 :Custom.IQCarControl; diff --git a/iqpilot/cereal/services.py b/iqpilot/cereal/services.py index 1f6fa9c50..4bca8a3e4 100755 --- a/iqpilot/cereal/services.py +++ b/iqpilot/cereal/services.py @@ -108,6 +108,7 @@ _services: dict[str, tuple] = { "iqRoadIncidentFeed": (True, 0.2, 1), "iqNavRenderState": (True, 5., 10, QueueSize.MEDIUM), "iqState": (True, 100., 10), + "egpuDockState": (True, 10., 10), "iqPlan": (True, 20., 10), "iqOnroadEvents": (True, 1., 1), "iqDriveModelData": (True, 20., None, QueueSize.MEDIUM), diff --git a/iqpilot/common/params_keys.h b/iqpilot/common/params_keys.h index f3b8a2de8..8dfab24e2 100644 --- a/iqpilot/common/params_keys.h +++ b/iqpilot/common/params_keys.h @@ -105,6 +105,13 @@ inline static std::unordered_map keys = { {"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_EgpuNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_EgpuFansObstructed", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_EgpuOverheated", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_EgpuPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_EgpuUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_EgpuUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_EgpuUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, @@ -224,6 +231,34 @@ inline static std::unordered_map keys = { {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT, JSON}}, + {"IQEmacEnabled", {PERSISTENT, BOOL, "0"}}, + {"IQEmacHost", {PERSISTENT, STRING}}, + {"IQEmacModel", {PERSISTENT, STRING}}, + {"IQEmacCatalogCache", {PERSISTENT, STRING}}, + {"MacModelDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "1.0"}}, + {"MacModelStatus", {CLEAR_ON_MANAGER_START, STRING}}, + {"MacModelPresent", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MacModelReachable", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MacModelReady", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MacModelActive", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MacModelFailed", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MacModelLastError", {CLEAR_ON_MANAGER_START, STRING}}, + {"MacModelLatencyMs", {CLEAR_ON_MANAGER_START, FLOAT, "0.0"}}, + + // comma USB eGPU big-model backend. Mutually exclusive with eMac at + // runtime (eMac wins). UsbGpu* naming/flags mirror comma's handover branch + {"IQEgpuEnabled", {PERSISTENT, BOOL, "0"}}, + {"IQEgpuDisabled", {PERSISTENT, BOOL, "0"}}, + {"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"UsbGpuFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"UsbGpuLastError", {CLEAR_ON_MANAGER_START, STRING}}, + {"UsbGpuLatencyMs", {CLEAR_ON_MANAGER_START, FLOAT, "0.0"}}, + {"UsbGpuStatus", {CLEAR_ON_MANAGER_START, STRING}}, + {"UsbGpuSetupProgress", {CLEAR_ON_MANAGER_START, STRING, "1.0"}}, + // Neural Network Feed Forward {"NeuralNetworkFeedForward", {PERSISTENT, BOOL, "0"}}, diff --git a/iqpilot/selfdrive/car/card.py b/iqpilot/selfdrive/car/card.py index 96f12c692..786be2ffb 100755 --- a/iqpilot/selfdrive/car/card.py +++ b/iqpilot/selfdrive/car/card.py @@ -253,6 +253,9 @@ class Car: # TODO: mirror the carState.cruiseState struct? CS.vCruise = float(self.v_cruise_helper.v_cruise_kph) CS.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph) + CS_IQ.slcSetSpeedRequestId = self.v_cruise_helper.slc_set_speed_request_id + CS_IQ.slcSetSpeedGestureId = self.v_cruise_helper.slc_set_speed_gesture_id + CS_IQ.slcSetSpeedRequestKph = self.v_cruise_helper.slc_set_speed_request_kph return CS, CS_IQ, RD diff --git a/iqpilot/selfdrive/car/cruise.py b/iqpilot/selfdrive/car/cruise.py index f54864f75..79881caee 100644 --- a/iqpilot/selfdrive/car/cruise.py +++ b/iqpilot/selfdrive/car/cruise.py @@ -6,6 +6,7 @@ from iqpilot.common.constants import CV from iqpilot.cereal import car, custom from iqdbc.car import structs from iqpilot.common.params import Params +from iqpilot.common.realtime import DT_CTRL from iqpilot.selfdrive.car.long_increments import LongIncrementConfig, read_long_increment_config, resolve_button_step @@ -237,6 +238,35 @@ class VCruiseHelper(VCruiseHelperIQ): self.v_cruise_kph_last = 0 self.button_timers = {ButtonType.decelCruise: 0, ButtonType.accelCruise: 0} self.button_change_states = {btn: {"standstill": False, "enabled": False} for btn in self.button_timers} + self.slc_set_speed_request_id = 0 + self.slc_set_speed_gesture_id = 0 + self.slc_set_speed_request_kph = 0.0 + self._slc_accel_held = False + self._slc_accel_release_frames = 0 + self._slc_pcm_speed_last = None + + def _update_slc_accel_gesture(self, CS): + self._slc_accel_release_frames = max(0, self._slc_accel_release_frames - 1) + for button in CS.buttonEvents: + if button.type in (ButtonType.accelCruise, ButtonType.resumeCruise): + if button.pressed: + self.slc_set_speed_gesture_id = (self.slc_set_speed_gesture_id + 1) % (1 << 32) + self._slc_accel_release_frames = 0 + else: + self._slc_accel_release_frames = int(0.5 / DT_CTRL) + self._slc_accel_held = button.pressed + elif button.pressed: + self._slc_accel_held = False + self._slc_accel_release_frames = 0 + if not CS.cruiseState.available: + self._slc_accel_held = False + self._slc_accel_release_frames = 0 + + def _record_slc_set_speed_increase(self, previous_kph, enabled): + if enabled and (self._slc_accel_held or self._slc_accel_release_frames > 0) and \ + previous_kph is not None and 0 < previous_kph < self.v_cruise_kph <= V_CRUISE_MAX: + self.slc_set_speed_request_id = (self.slc_set_speed_request_id + 1) % (1 << 32) + self.slc_set_speed_request_kph = float(self.v_cruise_kph) @property def v_cruise_initialized(self): @@ -248,6 +278,7 @@ class VCruiseHelper(VCruiseHelperIQ): def update_v_cruise(self, CS, enabled, is_metric): self.v_cruise_kph_last = self.v_cruise_kph + self._update_slc_accel_gesture(CS) self.get_minimum_set_speed(is_metric) @@ -256,12 +287,15 @@ class VCruiseHelper(VCruiseHelperIQ): if not self.CP.pcmCruise or (not self.CP_IQ.pcmCruiseSpeed and _enabled): # if stock cruise is completely disabled, then we can use our own set speed logic self._update_v_cruise_non_pcm(CS, _enabled, is_metric, self.volkswagen_standby_set_speed) + self._record_slc_set_speed_increase(self.v_cruise_kph_last, _enabled) self.update_speed_limit_assist_v_cruise_non_pcm() self.v_cruise_cluster_kph = self.v_cruise_kph self.update_button_timers(CS, enabled) else: self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH + self._record_slc_set_speed_increase(self._slc_pcm_speed_last, _enabled) + self._slc_pcm_speed_last = self.v_cruise_kph if CS.cruiseState.speed == 0: self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET @@ -273,6 +307,7 @@ class VCruiseHelper(VCruiseHelperIQ): else: self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET + self._slc_pcm_speed_last = None def _update_v_cruise_non_pcm(self, CS, enabled, is_metric, allow_standby_adjustment=False): # handle button presses. TODO: this should be in state_control, but a decelCruise press diff --git a/iqpilot/selfdrive/car/tests/test_speed_limit_set_speed.py b/iqpilot/selfdrive/car/tests/test_speed_limit_set_speed.py index a872cbae1..ea1a7d3c3 100644 --- a/iqpilot/selfdrive/car/tests/test_speed_limit_set_speed.py +++ b/iqpilot/selfdrive/car/tests/test_speed_limit_set_speed.py @@ -73,6 +73,65 @@ class TestSpeedLimitSetSpeedMirror: assert self.v_cruise_helper.v_cruise_cluster_kph == pytest.approx(13.41 * CV.MS_TO_KPH, abs=0.1) +@pytest.mark.parametrize("pcm_cruise", [False, True]) +@pytest.mark.parametrize("is_metric", [False, True]) +def test_driver_increase_marker_survives_button_release(pcm_cruise, is_metric): + helper = VCruiseHelper(car.CarParams(pcmCruise=pcm_cruise), custom.IQCarParams(pcmCruiseSpeed=True)) + helper.set_speed_to_limit = False + helper.v_cruise_kph = helper.v_cruise_cluster_kph = 80.0 + state = car.CarState(cruiseState={"available": True, "speed": 80 * CV.KPH_TO_MS, "speedCluster": 80 * CV.KPH_TO_MS}) + helper.update_v_cruise(state, True, is_metric) + state.buttonEvents = [car.CarState.ButtonEvent(type="accelCruise", pressed=True)] + helper.update_v_cruise(state, True, is_metric) + assert helper.slc_set_speed_request_id == 0 + state.buttonEvents = [car.CarState.ButtonEvent(type="accelCruise", pressed=False)] + if pcm_cruise: + state.cruiseState.speed = state.cruiseState.speedCluster = 81 * CV.KPH_TO_MS + helper.update_v_cruise(state, True, is_metric) + request = custom.IQCarState.new_message( + slcSetSpeedRequestId=helper.slc_set_speed_request_id, + slcSetSpeedGestureId=helper.slc_set_speed_gesture_id, + slcSetSpeedRequestKph=helper.slc_set_speed_request_kph, + ) + assert request.slcSetSpeedRequestId == 1 + assert request.slcSetSpeedRequestKph == pytest.approx(helper.v_cruise_kph) + state.buttonEvents = [] + for _ in range(100): + helper.update_v_cruise(state, True, is_metric) + assert helper.slc_set_speed_request_id == request.slcSetSpeedRequestId + assert helper.slc_set_speed_gesture_id == request.slcSetSpeedGestureId + + +def test_stock_cruise_sync_is_not_a_driver_increase(): + helper = VCruiseHelper(car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True), + custom.IQCarParams(pcmCruiseSpeed=True)) + helper.set_speed_to_limit = True + helper.update_speed_limit_assist(True, TestSpeedLimitSetSpeedMirror._iq_plan( + 50 * CV.KPH_TO_MS, custom.IQPlan.SpeedLimit.AssistState.active)) + state = car.CarState(cruiseState={"available": True, "speed": 80 * CV.KPH_TO_MS, "speedCluster": 80 * CV.KPH_TO_MS}) + for _ in range(10): + helper.update_v_cruise(state, True, True) + assert helper.slc_set_speed_request_id == 0 + state.cruiseState.speed = state.cruiseState.speedCluster = 90 * CV.KPH_TO_MS + helper.update_v_cruise(state, True, True) + assert helper.slc_set_speed_request_id == 0 + + +def test_held_increase_uses_one_gesture_and_multiple_requests(): + helper = VCruiseHelper(car.CarParams(pcmCruise=False), custom.IQCarParams(pcmCruiseSpeed=True)) + helper.set_speed_to_limit = False + helper.v_cruise_kph = 80.0 + state = car.CarState(cruiseState={"available": True}) + state.buttonEvents = [car.CarState.ButtonEvent(type="accelCruise", pressed=True)] + helper.update_v_cruise(state, True, True) + state.buttonEvents = [] + for _ in range(110): + helper.update_v_cruise(state, True, True) + assert helper.slc_set_speed_gesture_id == 1 + assert helper.slc_set_speed_request_id >= 2 + assert helper.slc_set_speed_request_kph == pytest.approx(helper.v_cruise_kph) + + def test_set_speed_does_not_follow_limit_when_feature_off(): # Default off: set speed must stay the driver's value (limiter-only via planner min-blend). CP = car.CarParams(pcmCruise=True, openpilotLongitudinalControl=True) diff --git a/iqpilot/selfdrive/controls/controlsd.py b/iqpilot/selfdrive/controls/controlsd.py index b067ecfdf..9a6f43ff4 100755 --- a/iqpilot/selfdrive/controls/controlsd.py +++ b/iqpilot/selfdrive/controls/controlsd.py @@ -24,6 +24,7 @@ from iqpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from iqpilot.selfdrive.controls.lib.latcontrol_torque_pq import LatControlTorquePQ from iqpilot.selfdrive.controls.lib.latcontrol_torque_v0 import LatControlTorqueV0, is_vw_mqb_torque from iqpilot.selfdrive.controls.lib.longcontrol import LongControl +from iqpilot.selfdrive.controls.steering_fault_recovery import SteeringFaultRecovery from iqpilot.system.proprietary_runtime._verified_import import import_verified_module from iqpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose @@ -73,6 +74,7 @@ class Controls(IQControlsLayer): self.pm = messaging.PubMaster(['carControl', 'controlsState', 'iqPerfTrace'] + self.iq_pub_services) self.steer_limited_by_safety = False + self.steering_fault_recovery = SteeringFaultRecovery() self.curvature = 0.0 self.desired_curvature = 0.0 self.roll_compensation = 0.0 @@ -208,7 +210,8 @@ class Controls(IQControlsLayer): # Get which state to use for active lateral control _lat_active = self.iq_lateral_allowed(self.sm) - CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \ + steering_fault_recovered = self.steering_fault_recovery.update(CS.steerFaultTemporary, CS.steerFaultPermanent) + CC.latActive = _lat_active and steering_fault_recovered and \ (not standstill or self.CP.steerAtStandstill) # long control may stay active through a gas override on platforms that opt in override_longitudinal = any(e.overrideLongitudinal for e in self.sm['onroadEvents']) diff --git a/iqpilot/selfdrive/controls/lib/slc_vcruise.py b/iqpilot/selfdrive/controls/lib/slc_vcruise.py index 0af9facff..7482f6237 100644 --- a/iqpilot/selfdrive/controls/lib/slc_vcruise.py +++ b/iqpilot/selfdrive/controls/lib/slc_vcruise.py @@ -189,6 +189,8 @@ class SLCVCruise: self._user_max_speed = v_cruise_cluster else: self._user_max_speed = 0.0 + if not slc_params["speed_limit_controller"]: + self.slc.reset_override(sm) if slc_params["speed_limit_controller"]: self.slc.update_limits(dashboard_speed_limit, now, time_validated, v_cruise, v_ego, sm, slc_params) self.pending_events = list(getattr(self.slc, 'pending_events', [])) diff --git a/iqpilot/selfdrive/controls/lib/speed_limit_controller.py b/iqpilot/selfdrive/controls/lib/speed_limit_controller.py index 0fd6bdeea..3cdf72ecf 100644 --- a/iqpilot/selfdrive/controls/lib/speed_limit_controller.py +++ b/iqpilot/selfdrive/controls/lib/speed_limit_controller.py @@ -261,10 +261,11 @@ class IQSpeedLimitAssist: for btn in sm["carState"].buttonEvents: if btn.pressed: continue - if is_lower and btn.type in CONFIRM_LOWER_BUTTONS: + button_type = getattr(btn.type, "raw", btn.type) + if is_lower and button_type in CONFIRM_LOWER_BUTTONS: confirmed = True break - elif not is_lower and btn.type in CONFIRM_HIGHER_BUTTONS: + elif not is_lower and button_type in CONFIRM_HIGHER_BUTTONS: confirmed = True break except (AttributeError, TypeError): @@ -314,6 +315,10 @@ class SpeedLimitController: self.override_slc = False self.overridden_speed = 0.0 + self._last_override_request_id = 0 + self._blocked_override_gesture = 0 + self._override_limit = None + self._override_set_speed = False self._resolved_limit = 0.0 self._resolved_source = "None" @@ -807,9 +812,44 @@ class SpeedLimitController: self.pending_events.append(EventNameIQ.constructionZoneDetected) self._czone_was_limiting = czone_limiting + def reset_override(self, sm): + self.override_slc = False + self.overridden_speed = 0.0 + self._last_override_request_id = int(getattr(sm["iqCarState"], "slcSetSpeedRequestId", 0)) + self._blocked_override_gesture = int(getattr(sm["iqCarState"], "slcSetSpeedGestureId", 0)) + self._override_limit = None + def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm, slc_params, is_metric): offset = self.get_offset(is_metric) target = self._assist.target + set_speed_override = slc_params.get("speed_limit_controller_override_set_speed", False) + mode_changed = set_speed_override != self._override_set_speed + self._override_set_speed = set_speed_override + + if set_speed_override: + request_id = int(getattr(sm["iqCarState"], "slcSetSpeedRequestId", 0)) + gesture_id = int(getattr(sm["iqCarState"], "slcSetSpeedGestureId", 0)) + request_speed = float(getattr(sm["iqCarState"], "slcSetSpeedRequestKph", 0.0)) * CV.KPH_TO_MS + new_request = request_id != self._last_override_request_id + limit = (target, self._assist.source) + reset = (mode_changed or limit != self._override_limit or self._assist.just_confirmed or + self._assist.state == SpeedLimitAssistState.preActive or + not bool(getattr(sm["selfdriveState"], "enabled", False)) or target <= 0 or self._resolved_source == "Construction") + cruise_speed = v_cruise + v_cruise_diff + above_limit = cruise_speed > target + offset + 1e-3 + if reset or (self.override_slc and not above_limit): + self.reset_override(sm) + elif above_limit: + driver_increase = new_request and gesture_id != self._blocked_override_gesture and request_speed > target + offset + 1e-3 + gas_override = sm["carState"].gasPressed and v_ego > target + offset + self.override_slc = self.override_slc or driver_increase or gas_override + self.overridden_speed = cruise_speed if self.override_slc else 0.0 + self._last_override_request_id = request_id + self._override_limit = limit + return + + if mode_changed: + self.reset_override(sm) self.override_slc = self.overridden_speed > target + offset > 0 self.override_slc |= sm["carState"].gasPressed and v_ego > target + offset > 0 @@ -820,7 +860,5 @@ class SpeedLimitController: if sm["carState"].gasPressed: self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed) self.overridden_speed = float(np.clip(self.overridden_speed, target + offset, v_cruise + v_cruise_diff)) - elif slc_params.get("speed_limit_controller_override_set_speed", False): - self.overridden_speed = v_cruise + v_cruise_diff else: self.overridden_speed = 0.0 diff --git a/iqpilot/selfdrive/controls/lib/tests/test_slc_speed_limit_control.py b/iqpilot/selfdrive/controls/lib/tests/test_slc_speed_limit_control.py index 2fd7a2b63..2e2acf9c4 100644 --- a/iqpilot/selfdrive/controls/lib/tests/test_slc_speed_limit_control.py +++ b/iqpilot/selfdrive/controls/lib/tests/test_slc_speed_limit_control.py @@ -5,8 +5,13 @@ Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed from datetime import datetime from types import SimpleNamespace +import pytest + +from iqpilot.cereal import car, custom from iqpilot.common.constants import CV from iqpilot.common.slc_variables import OFFSET_MAP_IMPERIAL +from iqpilot.selfdrive.car.cruise import VCruiseHelper +from iqpilot.selfdrive.controls.lib.iq_longitudinal_planner import LongitudinalPlannerIQ from iqpilot.selfdrive.controls.lib.slc_vcruise import SLCVCruise, CRUISING_SPEED from iqpilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitController, POLICY_MAP_DATA_PRIORITY, POLICY_COMBINED @@ -61,6 +66,9 @@ class _FakeSLC: def update_override(self, *_args, **_kwargs): self.update_override_calls += 1 + def reset_override(self, _sm): + self.overridden_speed = 0.0 + def get_offset(self, _is_metric): return self._offset @@ -261,6 +269,200 @@ def test_slc_vcruise_does_not_auto_raise_when_higher_confirmation_enabled(): assert out == v_cruise +@pytest.fixture(params=[True, False], ids=["metric", "imperial"]) +def set_speed_slc(request, monkeypatch): + monkeypatch.setattr("iqpilot.selfdrive.controls.lib.slc_vcruise.Params", FakeParams) + slc = SLCVCruise() + slc._maybe_log_debug = lambda *_args: None + slc.slc.update_gps = lambda _sm: None + slc.slc._resolver.update_map_data = lambda *_args: None + params = _base_slc_params_controller() | { + "speed_limit_controller": True, + "speed_limit_mode": 3, + "show_speed_limits": False, + "is_metric": request.param, + "slc_online_filler": False, + "slc_fallback_experimental_mode": False, + "speed_limit_controller_override_manual": False, + "speed_limit_controller_override_set_speed": True, + } + slc._get_slc_params = lambda: params + unit = CV.KPH_TO_MS if request.param else CV.MPH_TO_MS + slc.slc._resolver.map_speed_limit = 50 * unit + sm = _FakeSM(_build_sm(v_ego_cluster=50 * unit)) + sm["iqCarState"].slcSetSpeedRequestId = 0 + sm["iqCarState"].slcSetSpeedGestureId = 0 + sm["iqCarState"].slcSetSpeedRequestKph = 0.0 + + def step(speed, increase=False, new_gesture=False): + sm["carState"].vCruiseCluster = speed * unit * CV.MS_TO_KPH + if new_gesture: + sm["iqCarState"].slcSetSpeedGestureId += 1 + if increase: + sm["iqCarState"].slcSetSpeedRequestId += 1 + sm["iqCarState"].slcSetSpeedRequestKph = sm["carState"].vCruiseCluster + target = slc.update(sm["selfdriveState"].enabled, None, True, speed * unit, 50 * unit, sm) + return min(speed * unit, target) / unit + + step(50) + return SimpleNamespace(slc=slc, params=params, sm=sm, unit=unit, step=step) + + +@pytest.mark.parametrize("confirm_higher", [False, True]) +def test_set_speed_override_tracks_driver_adjustments(set_speed_slc, confirm_higher): + system = set_speed_slc + system.params["speed_limit_confirmation_higher"] = confirm_higher + assert system.step(50, new_gesture=True) == pytest.approx(50) + assert system.step(55, increase=True) == pytest.approx(55) + assert system.step(60, increase=True) == pytest.approx(60) + assert system.step(60) == pytest.approx(60) + assert system.step(55) == pytest.approx(55) + assert system.step(50) == pytest.approx(50) + assert not system.slc.slc.override_slc + assert system.step(45) == pytest.approx(45) + assert system.step(60) == pytest.approx(50) + assert system.step(61, increase=True, new_gesture=True) == pytest.approx(61) + + +def test_set_speed_override_ignores_automatic_speed_changes(set_speed_slc): + system = set_speed_slc + assert system.step(80) == pytest.approx(50) + system.slc.slc._resolver.map_speed_limit = 60 * system.unit + assert system.step(60) == pytest.approx(60) + assert system.step(80) == pytest.approx(60) + assert system.slc.slc.overridden_speed == 0 + + +@pytest.mark.parametrize("limit", [40, 55]) +def test_set_speed_override_resets_on_accepted_limit(set_speed_slc, limit): + system = set_speed_slc + assert system.step(60, increase=True, new_gesture=True) == pytest.approx(60) + system.slc.slc._resolver.map_speed_limit = limit * system.unit + assert system.step(65, increase=True) == pytest.approx(limit) + assert system.step(70, increase=True) == pytest.approx(limit) + assert system.step(71, increase=True, new_gesture=True) == pytest.approx(71) + + +@pytest.mark.parametrize("limit,button", [(40, "decelCruise"), (55, "accelCruise")]) +def test_set_speed_override_does_not_reuse_confirmation_gesture(set_speed_slc, limit, button): + system = set_speed_slc + system.params["speed_limit_confirmation_higher"] = True + system.params["speed_limit_confirmation_lower"] = True + system.slc.slc._resolver.map_speed_limit = limit * system.unit + system.step(50, new_gesture=True) + assert system.slc.assist_state == custom.IQPlan.SpeedLimit.AssistState.preActive + assert system.step(60, increase=True) == pytest.approx(50) + system.sm["carState"].buttonEvents = [car.CarState.ButtonEvent(type=button, pressed=False)] + assert system.step(61, increase=True) == pytest.approx(limit) + system.sm["carState"].buttonEvents = [] + assert system.step(65, increase=True) == pytest.approx(limit) + assert system.step(66, increase=True, new_gesture=True) == pytest.approx(66) + + +@pytest.mark.parametrize("reset", ["disengage", "information", "off", "missing_limit"]) +def test_set_speed_override_cannot_survive_reset(set_speed_slc, reset): + system = set_speed_slc + assert system.step(60, increase=True, new_gesture=True) == pytest.approx(60) + if reset == "disengage": + system.sm["selfdriveState"].enabled = False + elif reset in ("information", "off"): + system.params["speed_limit_controller"] = False + system.params["show_speed_limits"] = reset == "information" + else: + system.slc.slc._resolver.map_speed_limit = 0 + system.step(60) + assert system.slc.slc.overridden_speed == 0 + system.sm["selfdriveState"].enabled = True + system.params["speed_limit_controller"] = True + system.slc.slc._resolver.map_speed_limit = 50 * system.unit + assert system.step(60) == pytest.approx(50) + assert system.step(65, increase=True) == pytest.approx(50) + assert system.step(66, increase=True, new_gesture=True) == pytest.approx(66) + + +def test_set_speed_override_respects_offset(set_speed_slc): + system = set_speed_slc + system.slc.slc.params.put("speed_limit_offset1", 10) + system.slc.slc.params.put("speed_limit_offset2", 10) + system.slc.slc.params.put("speed_limit_offset3", 10) + system.slc.slc._offset_cache.clear() + system.step(50, new_gesture=True) + assert system.step(52, increase=True) == pytest.approx(52) + assert not system.slc.slc.override_slc + assert system.step(56, increase=True) == pytest.approx(56) + assert system.step(55) == pytest.approx(55) + assert not system.slc.slc.override_slc + + +def test_manual_override_still_requires_accelerator(set_speed_slc): + system = set_speed_slc + system.params["speed_limit_controller_override_set_speed"] = False + system.params["speed_limit_controller_override_manual"] = True + assert system.step(60, increase=True, new_gesture=True) == pytest.approx(50) + system.sm["carState"].gasPressed = True + system.sm["carState"].vEgoCluster = 55 * system.unit + system.slc.slc.update_override(60 * system.unit, 0, 55 * system.unit, 0, system.sm, system.params, system.params["is_metric"]) + assert system.slc.slc.overridden_speed == pytest.approx(55 * system.unit) + system.sm["carState"].gasPressed = False + system.slc.slc.update_override(60 * system.unit, 0, 55 * system.unit, 0, system.sm, system.params, system.params["is_metric"]) + assert system.slc.slc.overridden_speed == pytest.approx(55 * system.unit) + + +@pytest.mark.parametrize("pcm_cruise", [False, True]) +def test_driver_increase_reaches_slc_without_transient_button_events(set_speed_slc, pcm_cruise): + system = set_speed_slc + helper = VCruiseHelper(car.CarParams(pcmCruise=pcm_cruise), custom.IQCarParams(pcmCruiseSpeed=True)) + helper.set_speed_to_limit = False + helper.v_cruise_kph = helper.v_cruise_cluster_kph = 50 * system.unit * CV.MS_TO_KPH + state = car.CarState(cruiseState={"available": True, "speed": 50 * system.unit, "speedCluster": 50 * system.unit}) + helper.update_v_cruise(state, True, system.params["is_metric"]) + state.buttonEvents = [car.CarState.ButtonEvent(type="accelCruise", pressed=True)] + helper.update_v_cruise(state, True, system.params["is_metric"]) + state.buttonEvents = [car.CarState.ButtonEvent(type="accelCruise", pressed=False)] + if pcm_cruise: + state.cruiseState.speed = state.cruiseState.speedCluster = 51 * system.unit + helper.update_v_cruise(state, True, system.params["is_metric"]) + state.buttonEvents = [] + for _ in range(5): + helper.update_v_cruise(state, True, system.params["is_metric"]) + system.sm["iqCarState"] = custom.IQCarState.new_message( + slcSetSpeedRequestId=helper.slc_set_speed_request_id, + slcSetSpeedGestureId=helper.slc_set_speed_gesture_id, + slcSetSpeedRequestKph=helper.slc_set_speed_request_kph, + ) + requested = helper.v_cruise_kph * CV.KPH_TO_MS / system.unit + assert requested > 50 + assert system.step(requested) == pytest.approx(requested) + + +def test_set_speed_override_keeps_navigation_constraint(set_speed_slc): + system = set_speed_slc + assert system.step(60, increase=True, new_gesture=True) == pytest.approx(60) + planner = LongitudinalPlannerIQ.__new__(LongitudinalPlannerIQ) + planner.slimit = system.slc + planner.iq_dynamic = SimpleNamespace( + set_slc_experimental_mode=lambda _mode: None, update=lambda _sm: None, force_stop_requested=lambda: False) + planner.force_stop_timer = 0.0 + planner.override_force_stop_timer = 0.0 + planner.override_force_stop = False + system.sm["iqNavState"] = SimpleNamespace(longitudinalEngaged=False, valid=False) + assert planner.update_targets(system.sm, 50 * system.unit, 60 * system.unit) == pytest.approx(60 * system.unit) + system.sm["iqNavState"] = SimpleNamespace(longitudinalEngaged=True, valid=True, speedTarget=40 * system.unit) + assert planner.update_targets(system.sm, 50 * system.unit, 60 * system.unit) == pytest.approx(40 * system.unit) + + +def test_set_speed_override_cannot_bypass_construction_zone(set_speed_slc): + system = set_speed_slc + assert system.step(60, increase=True, new_gesture=True) == pytest.approx(60) + system.params["construction_zone_assist"] = True + system.params["construction_zone_speed"] = 40 + system.sm["iqConstructionZone"] = SimpleNamespace(active=True) + system.sm.alive["iqConstructionZone"] = True + assert system.step(60) == pytest.approx(40) + assert system.step(65, increase=True, new_gesture=True) == pytest.approx(40) + assert not system.slc.slc.override_slc + + class _FakeSM(dict): def __init__(self, services, alive=None): super().__init__(services) diff --git a/iqpilot/selfdrive/controls/steering_fault_recovery.py b/iqpilot/selfdrive/controls/steering_fault_recovery.py new file mode 100644 index 000000000..191161e3e --- /dev/null +++ b/iqpilot/selfdrive/controls/steering_fault_recovery.py @@ -0,0 +1,18 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqpilot.common.realtime import DT_CTRL + +STEER_FAULT_RECOVERY_FRAMES = int(1.0 / DT_CTRL) + + +class SteeringFaultRecovery: + def __init__(self) -> None: + self.clear_frames = STEER_FAULT_RECOVERY_FRAMES + + def update(self, temporary: bool, permanent: bool) -> bool: + if temporary or permanent: + self.clear_frames = 0 + else: + self.clear_frames = min(self.clear_frames + 1, STEER_FAULT_RECOVERY_FRAMES) + return self.clear_frames == STEER_FAULT_RECOVERY_FRAMES diff --git a/iqpilot/selfdrive/controls/tests/test_steering_fault_recovery.py b/iqpilot/selfdrive/controls/tests/test_steering_fault_recovery.py new file mode 100644 index 000000000..d6cf76ce3 --- /dev/null +++ b/iqpilot/selfdrive/controls/tests/test_steering_fault_recovery.py @@ -0,0 +1,28 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqpilot.selfdrive.controls.steering_fault_recovery import STEER_FAULT_RECOVERY_FRAMES, SteeringFaultRecovery + + +def test_steering_fault_recovery_starts_ready(): + recovery = SteeringFaultRecovery() + assert recovery.update(False, False) + + +def test_temporary_fault_requires_continuous_clear_interval(): + recovery = SteeringFaultRecovery() + assert not recovery.update(True, False) + for _ in range(STEER_FAULT_RECOVERY_FRAMES - 1): + assert not recovery.update(False, False) + assert recovery.update(False, False) + + +def test_repeated_fault_restarts_recovery_interval(): + recovery = SteeringFaultRecovery() + assert not recovery.update(False, True) + for _ in range(STEER_FAULT_RECOVERY_FRAMES - 1): + assert not recovery.update(False, False) + assert not recovery.update(True, False) + for _ in range(STEER_FAULT_RECOVERY_FRAMES - 1): + assert not recovery.update(False, False) + assert recovery.update(False, False) diff --git a/iqpilot/selfdrive/dmonitoringmodeld/dmonitoringmodeld.py b/iqpilot/selfdrive/dmonitoringmodeld/dmonitoringmodeld.py index d17aa62ca..bcaa13712 100755 --- a/iqpilot/selfdrive/dmonitoringmodeld/dmonitoringmodeld.py +++ b/iqpilot/selfdrive/dmonitoringmodeld/dmonitoringmodeld.py @@ -109,6 +109,13 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t def main(): config_realtime_process(7, 5) + # Set in the child, not at import: manager preimports every process module in the parent, + # so an import-time write lands in one shared env that all children inherit (and setdefault + # in a child is then a guaranteed no-op). tinygrad reads this lazily at QCOMDevice init. + # KGSL: lower value = higher priority. DM has no 50ms deadline; at the driving contexts' + # default 8 its kernels interleave with the warp and blow its submit tail 16ms -> 72ms p90. + os.environ['QCOM_PRIORITY'] = os.getenv('DMON_QCOM_PRIORITY', '12') + cl_context = CLContext() model = ModelState(cl_context) cloudlog.warning("models loaded, dmonitoringmodeld starting") diff --git a/iqpilot/selfdrive/iqmodeld/big_catalog.py b/iqpilot/selfdrive/iqmodeld/big_catalog.py new file mode 100644 index 000000000..972671971 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/big_catalog.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.big_catalog") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.big_catalog import * diff --git a/iqpilot/selfdrive/iqmodeld/daemon.py b/iqpilot/selfdrive/iqmodeld/daemon.py index bf3e0001f..2f1edd563 100755 --- a/iqpilot/selfdrive/iqmodeld/daemon.py +++ b/iqpilot/selfdrive/iqmodeld/daemon.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import sys import time from dataclasses import dataclass from typing import Any @@ -471,7 +472,7 @@ class FrameDropMeter: class InferenceDaemon: - def __init__(self, demo: bool = False): + def __init__(self, demo: bool = False, channel_path: str | None = None): cloudlog.warning("iqmodeld init") sentry.set_tag("daemon", PROCESS_NAME) cloudlog.bind(daemon=PROCESS_NAME) @@ -485,8 +486,15 @@ class InferenceDaemon: self._meta_layout = select_meta_layout() cloudlog.warning("models loaded, iqmodeld starting") + self._channel = None + if channel_path is not None: + from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel + self._channel = ModelChannel(channel_path, create=True) + self._cameras = CameraIngress(self._gpu) - self._pub = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData", "iqPerfTrace"]) + pub_services = ["iqPerfTrace"] if self._channel is not None else [ + "modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData", "iqPerfTrace"] + self._pub = PubMaster(pub_services) self._sub = SubMaster([ "deviceState", "carState", "roadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState", @@ -513,6 +521,12 @@ class InferenceDaemon: def _refresh_tunables(self, tick: int) -> None: if tick % 60 != 0: return + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + big_enabled = self._params.get_bool("IQEmacEnabled") or egpu_selected(self._params) + if big_enabled != (self._channel is not None): + # publish mode is fixed at startup: staying up would fight the selector for modelV2 + cloudlog.warning("iqmodeld: big backend toggled, restarting to switch publish mode") + sys.exit(0) self._runtime.lat_delay = lateral_action_delay(self._params, self._car_params, self._sub["lateralDelay"].lateralDelay) self._runtime.PLANPLUS_CONTROL = self._params.get("PlanplusControl", return_default=True) self._runtime.model_smoothing_max_extra_sec = _model_lat_smooth_max_sec(self._params) @@ -600,6 +614,22 @@ class InferenceDaemon: live_calib_seen, ) + if self._channel is not None: + self._channel.write(main_stamp.frame_id, { + "source": "small", + "frame_id": main_stamp.frame_id, + "timestamp_sof": int(main_stamp.timestamp_sof), + "live_calib_seen": bool(live_calib_seen), + "model_execution_time": float(execution_time), + "msgs": { + "modelV2": model_msg.to_bytes(), + "drivingModelData": driving_msg.to_bytes(), + "cameraOdometry": pose_msg.to_bytes(), + "iqDriveModelData": iq_msg.to_bytes(), + }, + }) + return + self._pub.send("modelV2", model_msg) self._pub.send("drivingModelData", driving_msg) self._pub.send("cameraOdometry", pose_msg) @@ -695,8 +725,15 @@ class InferenceDaemon: tick += 1 -def main(demo: bool = False): - InferenceDaemon(demo=demo).serve() +def main(demo: bool = False, channel_path: str | None = "auto"): + if channel_path == "auto": + channel_path = None + params = Params() + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + if params.get_bool("IQEmacEnabled") or egpu_selected(params): + from iqpilot.selfdrive.iqmodeld.model_channel import SMALL_CHANNEL + channel_path = SMALL_CHANNEL + InferenceDaemon(demo=demo, channel_path=channel_path).serve() __all__ = [ diff --git a/iqpilot/selfdrive/iqmodeld/driving_action.py b/iqpilot/selfdrive/iqmodeld/driving_action.py new file mode 100644 index 000000000..3e5f9750c --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/driving_action.py @@ -0,0 +1,46 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import numpy as np + +from iqpilot.cereal import log + +from iqpilot.selfdrive.controls.lib.drive_helpers import smooth_value + +LAT_SMOOTH_SECONDS = 0.0 +LONG_SMOOTH_SECONDS = 0.3 +MIN_LAT_CONTROL_SPEED = 0.3 +DESIRE_LEN = 8 + + +def get_action_from_model(outputs: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, + v_ego: float, lat_action_t: float, long_action_t: float, + lat_smooth_seconds: float | None = None) -> log.ModelDataV2.Action: + if "action" in outputs: + desired_accel = float(outputs["action"][0, 1]) + desired_curvature = float(outputs["action"][0, 0]) / (max(1.0, v_ego)) ** 2 + should_stop = bool(v_ego < 0.3 and desired_accel < 0.1) + else: + from iqpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, get_curvature_from_plan + from iqpilot.selfdrive.iqmodeld.config import ModelConstants, Plan + plan = outputs["plan"][0] + desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], + plan[:, Plan.ACCELERATION][:, 0], + ModelConstants.T_IDXS, + action_t=long_action_t) + desired_curvature = get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], + plan[:, Plan.ORIENTATION_RATE][:, 2], + ModelConstants.T_IDXS, v_ego, lat_action_t) + desired_accel, should_stop = float(desired_accel), bool(should_stop) + desired_curvature = float(desired_curvature) + desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) + if v_ego > MIN_LAT_CONTROL_SPEED: + lat_smooth = LAT_SMOOTH_SECONDS if lat_smooth_seconds is None else lat_smooth_seconds + desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, lat_smooth) + else: + desired_curvature = prev_action.desiredCurvature + return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature), + desiredAcceleration=float(desired_accel), + shouldStop=should_stop) diff --git a/iqpilot/selfdrive/iqmodeld/egpu_helpers.py b/iqpilot/selfdrive/iqmodeld/egpu_helpers.py new file mode 100644 index 000000000..d09895c86 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/egpu_helpers.py @@ -0,0 +1,158 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import hashlib +import json +import os +import urllib.request +from pathlib import Path + +from iqpilot.system.hardware.usb import egpu_dock_ready + +USB_SYSFS_ROOT = "/sys/bus/usb/devices" + +COMMA_LFS_BATCH_URL = "https://gitlab.com/commaai/openpilot-lfs.git/info/lfs/objects/batch" + +DOWNLOAD_CHUNK = 4 * 1024 * 1024 + + +def usbgpu_present(sysfs_root: str = USB_SYSFS_ROOT) -> bool: + return egpu_dock_ready(Path(sysfs_root)) + + +def egpu_present_consented(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool: + try: + if params is not None and params.get_bool("IQEgpuDisabled"): + return False + except Exception: + pass + return usbgpu_present(sysfs_root) + + +def egpu_selected(params, sysfs_root: str = USB_SYSFS_ROOT) -> bool: + try: + if params is not None and params.get_bool("IQEgpuDisabled"): + return False + if params is not None and params.get_bool("IQEgpuEnabled"): + return True + except Exception: + pass + return usbgpu_present(sysfs_root) + + +def resolve_backend(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> str | None: + if egpu_present: + return "egpu" + if emac_enabled: + return "emac" + if egpu_enabled: + return "egpu" + return None + + +def egpu_pkl_path(meta: dict) -> str: + from iqpilot.system.hardware.hw import Paths + return os.path.join(Paths.model_root(), f"egpu_{meta['key']}_{meta['sha256'][:8]}_amd_tinygrad.pkl") + + +def onnx_cache_path(meta: dict) -> str: + from iqpilot.system.hardware.hw import Paths + return os.path.join(Paths.model_root(), f"{meta['model_name']}_{meta['sha256'][:8]}.onnx") + + +def _sha256_file(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as f: + while chunk := f.read(DOWNLOAD_CHUNK): + digest.update(chunk) + return digest.hexdigest() + + +def quarantine_artifact(path: str, why: str) -> None: + try: + if os.path.isfile(path): + os.replace(path, path + ".unusable") + except OSError: + try: + os.remove(path) + except OSError: + pass + + +def local_onnx(meta: dict) -> str | None: + path = onnx_cache_path(meta) + if not os.path.isfile(path): + return None + size = int(meta.get("download", {}).get("size", 0)) + if size and os.path.getsize(path) != size: + quarantine_artifact(path, "onnx size mismatch") + return None + if _sha256_file(path) != meta["sha256"]: + quarantine_artifact(path, "onnx sha256 mismatch") + return None + return path + + +def resolve_download_url(download_url: str, sha256: str, size: int, timeout: float = 30.0) -> str: + if download_url.startswith("commalfs:"): + oid = download_url.split(":", 1)[1] + body = json.dumps({"operation": "download", "transfers": ["basic"], + "objects": [{"oid": oid, "size": size}]}).encode() + req = urllib.request.Request(COMMA_LFS_BATCH_URL, data=body, headers={ + "Accept": "application/vnd.git-lfs+json", "Content-Type": "application/vnd.git-lfs+json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + d = json.load(r) + return d["objects"][0]["actions"]["download"]["href"] + return download_url + + +def download_onnx(meta: dict, progress_cb=None) -> str: + from iqpilot.selfdrive.iqmodeld.egpu_model import download_descriptor + download_url, size = download_descriptor(meta) + if not download_url: + raise RuntimeError(f"model {meta['key']} has no download source; stage the onnx at {onnx_cache_path(meta)}") + + url = resolve_download_url(download_url, meta["sha256"], size) + path = onnx_cache_path(meta) + os.makedirs(os.path.dirname(path), exist_ok=True) + tmp = path + ".part" + digest = hashlib.sha256() + got = 0 + with urllib.request.urlopen(url, timeout=60) as r, open(tmp, "wb") as f: + while chunk := r.read(DOWNLOAD_CHUNK): + f.write(chunk) + digest.update(chunk) + got += len(chunk) + if progress_cb is not None and size: + progress_cb(got / size) + if size and got != size: + os.remove(tmp) + raise RuntimeError(f"onnx download truncated: {got}/{size} bytes") + if digest.hexdigest() != meta["sha256"]: + os.remove(tmp) + raise RuntimeError(f"onnx sha256 mismatch for {meta['key']}") + os.replace(tmp, path) + return path + + +def patch_tinygrad_fetch_fw() -> None: + import pathlib + + import zstandard + from tinygrad import helpers + if getattr(helpers.fetch_fw, "_iq_patched", False): + return + _orig = helpers.fetch_fw + + def fetch_fw(path, name, sha256): + p = pathlib.Path(f"/lib/firmware/{path}/{name}.zst") + if p.is_file(): + blob = zstandard.ZstdDecompressor().stream_reader(p.read_bytes()).read() + if hashlib.sha256(blob).hexdigest() == sha256: + return blob + return _orig(path, name, sha256) + + fetch_fw._iq_patched = True + helpers.fetch_fw = fetch_fw diff --git a/iqpilot/selfdrive/iqmodeld/egpu_model.py b/iqpilot/selfdrive/iqmodeld/egpu_model.py new file mode 100644 index 000000000..b9dc2c77e --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/egpu_model.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.egpu_model") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.egpu_model import * diff --git a/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py b/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py new file mode 100644 index 000000000..e0cd2f20b --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/egpu_pipeline.py @@ -0,0 +1,47 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC, TemporalInputState, spec_from_meta + + +class EgpuPipelineError(RuntimeError): + pass + + +class EgpuPipeline: + + def __init__(self, meta: dict, infer_fn): + if meta.get("split"): + raise EgpuPipelineError(f"model {meta['key']} is a split model; eGPU v1 runs fused models only") + self.meta = meta + self.infer_fn = infer_fn + self.state = TemporalInputState(meta["frame_skip"], spec_from_meta(meta) or MODEL_INPUT_SPEC) + self.hidden_slice = meta["output_slices"]["hidden_state"] + self.output_len = int(meta["output_len"]) + + def run(self, warped: np.ndarray, desire_vec: np.ndarray, traffic_convention: np.ndarray, + action_t: np.ndarray) -> np.ndarray: + inputs = self.state.push_and_materialize(warped, desire_vec, traffic_convention, action_t) + out = np.asarray(self.infer_fn(inputs), dtype=np.float32).reshape(-1) + if out.shape[0] != self.output_len: + raise EgpuPipelineError(f"eGPU output length {out.shape[0]} != {self.output_len}") + if not np.isfinite(out).all(): + raise EgpuPipelineError("eGPU output contains non-finite values") + self.state.note_hidden_state(out, self.hidden_slice) + return out + + +def make_big_channel_payload(frame_id: int, live_calib_seen: bool, execution_time: float, + egpu_exec_ms: float, msgs: dict[str, bytes]) -> dict: + return { + "source": "egpu_big", + "frame_id": int(frame_id), + "live_calib_seen": bool(live_calib_seen), + "model_execution_time": float(execution_time), + "egpu_exec_ms": float(egpu_exec_ms), + "msgs": msgs, + } diff --git a/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py b/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py new file mode 100644 index 000000000..13936312f --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/egpu_telemetry.py @@ -0,0 +1,106 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import struct + +from iqpilot.cereal import messaging +from iqpilot.common.swaglog import cloudlog + +METRICS_REFRESH_EVERY = 100 + + +class EgpuDockTelemetry: + + def __init__(self, pm, big: bool): + self.pm = pm + self.big = big + self.valid = True + self.sends = 0 + self.metrics: dict[str, float] = {} + self._power_limit: int | None = None + self._asm_usb = None + + def _device(self): + from tinygrad.device import Device + return Device + + def _open_asm_usb(self): + import usb1 + from iqpilot.system.hardware.usb import EGPU_DOCK_USB_IDS + context = usb1.USBContext() + for vendor_id, product_id in EGPU_DOCK_USB_IDS: + handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True) + if handle is not None: + return handle + context.close() + return None + + def _read_ina(self): + Device = self._device() + if "AMD" in Device._opened_devices and self._asm_usb is None: + try: + raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) + return struct.unpack(" int: + if self._power_limit is None: + self._power_limit = smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) + return self._power_limit + + def send(self) -> None: + Device = self._device() + msg = messaging.new_message("egpuDockState") + state = msg.egpuDockState + self.sends += 1 + + if self.big and "AMD" in Device._opened_devices and self.sends % METRICS_REFRESH_EVERY == 1: + try: + smu = Device["AMD"].iface.dev_impl.smu + smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) + metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics + self.metrics = {"tempC": metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], + "memoryTempC": metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], + "powerDrawW": metrics.AverageSocketPower, + "powerLimitW": self.power_limit(smu), + "gpuUsagePercent": metrics.AverageGfxActivity, + "gpuClockMhz": metrics.AverageGfxclkFrequencyPostDs, + "fanSpeedRpm": metrics.AvgFanRpm} + self.valid = True + except Exception: + if self.valid: + cloudlog.exception("egpu dock state read failed") + self.valid = False + self.metrics.clear() + + if self.big: + for k, v in self.metrics.items(): + setattr(state, k, v) + + asm_valid = False + try: + state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() + asm_valid = True + except Exception: + pass + if "AMD" in Device._opened_devices: + try: + state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0] + except Exception: + pass + + msg.valid = asm_valid and (not self.big or self.valid) + self.pm.send("egpuDockState", msg) diff --git a/iqpilot/selfdrive/iqmodeld/emac_input_state.py b/iqpilot/selfdrive/iqmodeld/emac_input_state.py new file mode 100644 index 000000000..698e4f639 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/emac_input_state.py @@ -0,0 +1,10 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from iqpilot.selfdrive.iqmodeld.temporal_state import ( + SplitTemporalState as SplitInputState, + TemporalInputState as EmacInputState, +) + +__all__ = ["EmacInputState", "SplitInputState"] diff --git a/iqpilot/selfdrive/iqmodeld/emac_model_meta.py b/iqpilot/selfdrive/iqmodeld/emac_model_meta.py new file mode 100644 index 000000000..75f405d67 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/emac_model_meta.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot._proprietary_loader import ProprietaryModuleMissing, load_private_module + +try: + load_private_module(__name__, "iqpilot_private.models.emac_model_meta") +except ProprietaryModuleMissing: + from iqpilot.models_private_src.emac_model_meta import * diff --git a/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py new file mode 100644 index 000000000..0a48111c0 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/iqegpumodeld.py @@ -0,0 +1,377 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import os +import pickle +import subprocess +import sys +import time + +from iqpilot.system.hardware import TICI + +os.environ.setdefault("GMMU", "0") +if TICI: + os.environ.setdefault("DEV", "QCOM") +else: + os.environ.setdefault("DEV", "CPU") + +import numpy as np +from setproctitle import setproctitle + +import iqpilot.cereal.messaging as messaging +from iqpilot.cereal import car, log +from iqpilot.cereal.messaging import SubMaster +from iqpilot.cereal.services import SERVICE_LIST +from iqdbc.car.car_helpers import get_demo_car_params + +from iqpilot.common.params import Params +from iqpilot.common.realtime import DT_MDL +from iqpilot.common.swaglog import cloudlog +from iqpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from iqpilot.system import sentry + +from iqpilot.common.steer_delay import lateral_action_delay +from iqpilot.selfdrive.iqmodeld.daemon import CalibrationAtlas, CameraIngress, FrameDropMeter +from iqpilot.selfdrive.iqmodeld.driving_action import ( + DESIRE_LEN, LAT_SMOOTH_SECONDS, LONG_SMOOTH_SECONDS, get_action_from_model, +) +from iqpilot.selfdrive.iqmodeld.egpu_helpers import ( + download_onnx, egpu_pkl_path, egpu_present_consented, egpu_selected, local_onnx, patch_tinygrad_fetch_fw, + quarantine_artifact, resolve_backend, usbgpu_present, +) +from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model +from iqpilot.selfdrive.iqmodeld.egpu_pipeline import EgpuPipeline, EgpuPipelineError, make_big_channel_payload +from iqpilot.selfdrive.iqmodeld.egpu_telemetry import EgpuDockTelemetry +from iqpilot.selfdrive.iqmodeld.messaging import DrivePacketMemory, populate_drive_messages, populate_odometry_message +from iqpilot.selfdrive.iqmodeld.metadata import Meta20hz +from iqpilot.selfdrive.iqmodeld.model_channel import BIG_CHANNEL, ModelChannel +from iqpilot.selfdrive.iqmodeld.model_warp import FrameWarp +from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + +PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.iqegpumodeld" + +PRESENCE_POLL_S = 5.0 +COMPILE_TIMEOUT_S = 3600 +LINK_UP_TIMEOUT_S = 10.0 +SETUP_RETRY_BASE_S = 3.0 +SETUP_RETRY_MAX_S = 30.0 + + +def park(reason: str) -> None: + cloudlog.warning(f"iqegpumodeld parked: {reason}") + params = Params() + params.put_bool("UsbGpuFailed", True) + params.put("UsbGpuLastError", reason[:512]) + while True: + time.sleep(1) + + +def _wait_for_egpu(params: Params) -> None: + while not usbgpu_present(): + params.put_bool("UsbGpuPresent", False) + time.sleep(PRESENCE_POLL_S) + params.put_bool("UsbGpuPresent", True) + try: + from iqpilot.system.hardware.egpu_dock.flash import link_up + except Exception: + return + deadline = time.monotonic() + LINK_UP_TIMEOUT_S + while time.monotonic() < deadline: + try: + if link_up(): + return + except Exception: + return + time.sleep(0.5) + + +def _compile_in_subprocess(meta: dict, onnx_path: str, pkl_path: str) -> None: + cmd = [sys.executable, "-m", "iqpilot.selfdrive.iqmodeld.tools.compile_egpu_model", + "--model", meta["key"], "--onnx", onnx_path, "--output", pkl_path] + compile_env = {**os.environ, "DEV": "USB+AMD:LLVM", "FLOAT16": "1", + "JIT_BATCH_SIZE": "0", "GMMU": "0"} + proc = subprocess.run(cmd, timeout=COMPILE_TIMEOUT_S, capture_output=True, text=True, + env=compile_env, preexec_fn=lambda: os.nice(20)) + if proc.returncode != 0: + tail = (proc.stderr or proc.stdout or "").strip()[-800:] + raise RuntimeError(f"eGPU model compile failed (rc={proc.returncode}): {tail}") + + +def _ensure_artifact(params: Params, meta: dict) -> str: + pkl_path = egpu_pkl_path(meta) + if os.path.isfile(pkl_path): + return pkl_path + + params.put_bool("UsbGpuCompiled", False) + onnx_path = local_onnx(meta) + if onnx_path is None: + params.put("UsbGpuSetupProgress", "0.0") + cloudlog.warning(f"iqegpumodeld downloading {meta['key']} onnx ({meta.get('download', {}).get('size', 0) / 1e6:.0f}MB)") + last = [-1.0] + + def _prog(p: float) -> None: + if p - last[0] >= 0.02 or p >= 1.0: + last[0] = p + params.put("UsbGpuSetupProgress", f"{p:.3f}") + + onnx_path = download_onnx(meta, progress_cb=_prog) + + cloudlog.warning(f"iqegpumodeld compiling {meta['key']} for USB-AMD (one-time, can take minutes)") + _compile_in_subprocess(meta, onnx_path, pkl_path) + cloudlog.warning(f"iqegpumodeld compiled -> {pkl_path}") + return pkl_path + + +def _load_infer_fn(pkl_path: str, meta: dict): + patch_tinygrad_fetch_fw() + from tinygrad.tensor import Tensor + + with open(pkl_path, "rb") as f: + bundle = pickle.load(f) + if bundle.get("model_sha256") != meta["sha256"]: + quarantine_artifact(pkl_path, "pkl model sha mismatch") + raise RuntimeError(f"artifact model sha {bundle.get('model_sha256')} != {meta['sha256']}") + if int(bundle.get("output_len", -1)) != int(meta["output_len"]): + quarantine_artifact(pkl_path, "pkl output_len mismatch") + raise RuntimeError(f"artifact output_len {bundle.get('output_len')} != {meta['output_len']}") + jit = bundle["run_model"] + input_dev = bundle.get("input_device", "AMD") + input_spec = bundle["input_spec"] + + def infer(inputs: dict[str, np.ndarray]) -> np.ndarray: + tensors = {name: Tensor(np.ascontiguousarray(inputs[name]), device=input_dev).realize() + for name in input_spec} + out, = jit(**tensors) + return out.numpy().reshape(-1) + + return infer, input_spec + + +def _warmup(infer_fn, input_spec: dict, output_len: int) -> float: + zeros = {name: np.zeros(shape, dtype=dtype) for name, (shape, dtype) in input_spec.items()} + t0 = time.perf_counter() + out = infer_fn(zeros) + dt = time.perf_counter() - t0 + if out.shape[0] != output_len or not np.isfinite(out).all(): + raise RuntimeError(f"warmup produced invalid output (len={out.shape[0]})") + return dt + + +def main(demo: bool = False) -> None: + cloudlog.warning("iqegpumodeld init") + sentry.set_tag("daemon", PROCESS_NAME) + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + try: + os.sched_setaffinity(0, {4, 5, 6}) + os.nice(-10) + except OSError as e: + cloudlog.warning(f"iqegpumodeld affinity/nice failed ({e}); continuing at defaults") + + params = Params() + backend = resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), egpu_present_consented(params)) + if backend != "egpu": + park(f"backend resolution is {backend!r}, not egpu; refusing to own the big channel") + + channel = ModelChannel(BIG_CHANNEL, create=True) + + cloudlog.warning("iqegpumodeld waiting for camerad") + cameras = CameraIngress(None) + layout = cameras.layout + + _wait_for_egpu(params) + params.put_bool("UsbGpuLoading", True) + attempt = 0 + while True: + try: + meta = resolve_egpu_model(params) + if meta is None: + raise RuntimeError("selected big model is not in the catalog; check connectivity or pick another model") + if meta.get("split"): + params.put_bool("UsbGpuLoading", False) + park(f"model {meta['key']} needs the Mac backend; the eGPU runs fused models only") + warp = FrameWarp(cameras._primary.width, cameras._primary.height, meta["frame_skip"]) + pkl_path = _ensure_artifact(params, meta) + infer_fn, input_spec = _load_infer_fn(pkl_path, meta) + warm_s = _warmup(infer_fn, input_spec, meta["output_len"]) + break + except Exception as e: + attempt += 1 + params.put("UsbGpuLastError", str(e)[:512]) + cloudlog.warning(f"iqegpumodeld setup attempt {attempt} failed: {e}; retrying") + if not usbgpu_present(): + _wait_for_egpu(params) + time.sleep(min(SETUP_RETRY_MAX_S, SETUP_RETRY_BASE_S * attempt)) + + params.put_bool("UsbGpuLoading", False) + params.put_bool("UsbGpuCompiled", True) + params.put("UsbGpuSetupProgress", "1.0") + cloudlog.warning(f"iqegpumodeld model: {meta['key']} ({meta['model_name']})") + cloudlog.warning(f"iqegpumodeld model up (warmup {warm_s * 1e3:.0f}ms)") + + pipeline = EgpuPipeline(meta, infer_fn) + telemetry_pm = messaging.PubMaster(["egpuDockState"]) + telemetry = EgpuDockTelemetry(telemetry_pm, big=True) + telemetry_every = max(1, round((1.0 / DT_MDL) / SERVICE_LIST["egpuDockState"].frequency)) + + sub = SubMaster(["deviceState", "carState", "roadCameraState", "extrinsicsCalibration", + "driverMonitoringState", "carControl", "lateralDelay", "iqNavState", "radarState"]) + if demo: + CP = get_demo_car_params() + else: + CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS + + parser = PhaseParser() + memory = DrivePacketMemory() + desire_logic = DesireHelper() + frame_meter = FrameDropMeter(20.0) + warps = CalibrationAtlas() + prev_action = log.ModelDataV2.Action() + slices = {k: v for k, v in meta["output_slices"].items() if k != "pad"} + + produced = 0 + stats: dict[str, list[float]] = {k: [] for k in ("pull", "warp", "infer", "publish", "loop")} + iter_count = 0 + skip_count = 0 + last_pulled_fid = -1 + last_frame_mono = time.monotonic() + t_loop = time.perf_counter() + cloudlog.warning("iqegpumodeld starting") + + while True: + frame_pair = cameras.pull() + t_pull = time.perf_counter() + if frame_pair is None: + if time.monotonic() - last_frame_mono > 2.0: + cloudlog.warning("iqegpumodeld camera stream silent >2s; reconnecting VisionIPC") + cameras = CameraIngress(None) + last_frame_mono = time.monotonic() + continue + last_frame_mono = time.monotonic() + main_buf, extra_buf, main_stamp, extra_stamp = frame_pair + + stats["pull"].append(t_pull - t_loop) + stats["loop"].append(time.perf_counter() - t_loop) + t_loop = time.perf_counter() + if last_pulled_fid >= 0 and main_stamp.frame_id > last_pulled_fid + 1: + skip_count += main_stamp.frame_id - last_pulled_fid - 1 + last_pulled_fid = main_stamp.frame_id + iter_count += 1 + if iter_count % 200 == 0: + pcts = {k: {"p50": round(sorted(v)[len(v) // 2] * 1e3, 1), + "p90": round(sorted(v)[int(len(v) * 0.9)] * 1e3, 1)} + for k, v in stats.items() if v} + cloudlog.event("iqegpu_stats", **pcts, cam_skips=skip_count, window=iter_count) + msg = " ".join(f"{k}=p50:{v['p50']:.0f}/p90:{v['p90']:.0f}ms" for k, v in pcts.items()) + cloudlog.warning(f"iqegpumodeld stages: {msg} cam_skips={skip_count} over {iter_count}") + for v in stats.values(): + v.clear() + skip_count = 0 + + sub.update(0) + + v_ego = max(sub["carState"].vEgo, 0.0) + lat_delay = lateral_action_delay(params, CP, sub["lateralDelay"].lateralDelay) + LAT_SMOOTH_SECONDS + main_tfm, extra_tfm, live_calib_seen = warps.refresh(sub, layout.main_is_wide, layout.dual_camera) + dropped_frames, frame_drop_ratio, _ = frame_meter.sample(main_stamp.frame_id) + + traffic = np.zeros(2, dtype=np.float32) + traffic[int(sub["driverMonitoringState"].isRHD)] = 1 + desire_vec = np.zeros(DESIRE_LEN, dtype=np.float32) + if 0 <= desire_logic.desire < DESIRE_LEN: + desire_vec[desire_logic.desire] = 1 + + frame_delay = DT_MDL + action_delay = DT_MDL / 2 + lat_action_t = lat_delay + frame_delay + action_delay + long_action_t = long_delay + frame_delay + action_delay + action_t = np.array([lat_action_t, long_action_t], dtype=np.float32) + + started_at = time.perf_counter() + try: + warped = warp.run(main_buf, extra_buf, main_tfm, extra_tfm) + except Exception as e: + park(f"warp run failed: {e}") + t_warp = time.perf_counter() + stats["warp"].append(t_warp - started_at) + + try: + output = pipeline.run(warped, desire_vec, traffic, action_t) + except EgpuPipelineError as e: + park(str(e)) + except Exception as e: + park(f"eGPU inference failed: {e}") + t_infer = time.perf_counter() + stats["infer"].append(t_infer - t_warp) + + execution_time = time.perf_counter() - started_at + sliced = {k: output[np.newaxis, sl] for k, sl in slices.items()} + outputs = parser.parse_vision_outputs(sliced) + + action = get_action_from_model(outputs, prev_action, v_ego, float(lat_action_t), float(long_action_t), + lat_smooth_seconds=meta.get("lat_smooth_seconds")) + prev_action = action + + model_msg = messaging.new_message("modelV2") + driving_msg = messaging.new_message("drivingModelData") + pose_msg = messaging.new_message("cameraOdometry") + iq_msg = messaging.new_message("iqDriveModelData") + + populate_drive_messages( + driving_msg, model_msg, outputs, action, memory, + main_stamp.frame_id, extra_stamp.frame_id, sub["roadCameraState"].frameId, + frame_drop_ratio, main_stamp.timestamp_eof, execution_time, + live_calib_seen, Meta20hz, + ) + + model_msg.modelV2.big = True + + desire_state = model_msg.modelV2.meta.desireState + lane_change_prob = desire_state[log.Desire.laneChangeLeft] + desire_state[log.Desire.laneChangeRight] + desire_logic.update(sub["carState"], sub["carControl"].latActive, lane_change_prob, + sub["iqNavState"], model_msg.modelV2, sub["radarState"]) + model_msg.modelV2.meta.laneChangeState = desire_logic.lane_change_state + model_msg.modelV2.meta.laneChangeDirection = desire_logic.lane_change_direction + driving_msg.drivingModelData.meta.laneChangeState = desire_logic.lane_change_state + driving_msg.drivingModelData.meta.laneChangeDirection = desire_logic.lane_change_direction + iq_msg.iqDriveModelData.turnSignalDirection = desire_logic.lane_turn_direction + + populate_odometry_message(pose_msg, outputs, main_stamp.frame_id, dropped_frames, + main_stamp.timestamp_eof, live_calib_seen) + + channel.write(main_stamp.frame_id, make_big_channel_payload( + main_stamp.frame_id, live_calib_seen, execution_time, (t_infer - t_warp) * 1e3, { + "modelV2": model_msg.to_bytes(), + "drivingModelData": driving_msg.to_bytes(), + "cameraOdometry": pose_msg.to_bytes(), + "iqDriveModelData": iq_msg.to_bytes(), + })) + stats["publish"].append(time.perf_counter() - t_infer) + produced += 1 + if produced == 1 or produced % 100 == 0: + infer_ms = (t_infer - t_warp) * 1e3 + cloudlog.warning(f"iqegpumodeld producing: frame={main_stamp.frame_id} total={execution_time * 1e3:.0f}ms infer={infer_ms:.0f}ms count={produced}") + + if produced % telemetry_every == 0: + telemetry.send() + + frame_meter.commit(main_stamp.frame_id) + + +if __name__ == "__main__": + try: + import argparse + ap = argparse.ArgumentParser() + ap.add_argument("--demo", action="store_true") + args = ap.parse_args() + main(demo=args.demo) + except KeyboardInterrupt: + cloudlog.warning("iqegpumodeld got SIGINT") + except Exception: + import traceback + sentry.capture_exception() + cloudlog.exception("iqegpumodeld crashed, parking") + park(f"crashed: {traceback.format_exc(limit=8)}") diff --git a/iqpilot/selfdrive/iqmodeld/model_channel.py b/iqpilot/selfdrive/iqmodeld/model_channel.py new file mode 100644 index 000000000..a9fa710bc --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/model_channel.py @@ -0,0 +1,58 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import mmap +import os +import pickle +import struct + +from iqpilot.common.swaglog import cloudlog + +SMALL_CHANNEL = "/dev/shm/iqpilot_smallmodel" +BIG_CHANNEL = "/dev/shm/iqpilot_bigmodel" +SHM_SIZE = 8 * 1024 * 1024 +HEADER = struct.Struct(" None: + data = pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL) + if HEADER.size + len(data) > SHM_SIZE: + cloudlog.error(f"model payload {len(data)} bytes exceeds shm {SHM_SIZE}, dropping frame {frame_id}") + return + seq = HEADER.unpack(self.mm[:HEADER.size])[0] + HEADER.pack_into(self.mm, 0, seq + 1, frame_id, len(data)) + self.mm[HEADER.size:HEADER.size + len(data)] = data + HEADER.pack_into(self.mm, 0, seq + 2, frame_id, len(data)) + + def peek_frame_id(self) -> int | None: + seq, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) + if seq == 0 or seq % 2 != 0 or length == 0: + return None + return frame_id + + def read(self) -> tuple[int, dict] | None: + seq1, frame_id, length = HEADER.unpack(self.mm[:HEADER.size]) + if seq1 == 0 or seq1 % 2 != 0 or length == 0: + return None + data = bytes(self.mm[HEADER.size:HEADER.size + length]) + seq2 = HEADER.unpack(self.mm[:HEADER.size])[0] + if seq1 != seq2: + return None + try: + return frame_id, pickle.loads(data) + except Exception: + return None diff --git a/iqpilot/selfdrive/iqmodeld/model_warp.py b/iqpilot/selfdrive/iqmodeld/model_warp.py new file mode 100644 index 000000000..50697728a --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/model_warp.py @@ -0,0 +1,80 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import os +import pickle + +import numpy as np + +from iqpilot.common.swaglog import cloudlog +from iqpilot.system.hardware.hw import Paths + + +def _load_bundle(pkl_path: str, cam_w: int, cam_h: int, frame_skip: int) -> dict: + with open(pkl_path, "rb") as f: + bundle = pickle.load(f) + if bundle.get("frame_skip") != frame_skip: + raise RuntimeError(f"frame_skip {bundle.get('frame_skip')} != {frame_skip}") + if (cam_w, cam_h) not in bundle: + raise RuntimeError(f"missing {cam_w}x{cam_h}; has {[k for k in bundle if isinstance(k, tuple)]}") + _verify_selftest(bundle, cam_w, cam_h) + return bundle + + +def _verify_selftest(bundle: dict, cam_w: int, cam_h: int) -> None: + want = bundle.get("selftest") + if not want: + raise RuntimeError("warp artifact predates the self-test; recompiling") + from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info + from iqpilot.selfdrive.iqmodeld.tools.compile_warp import selftest_digest + nv12_size = get_nv12_info(cam_w, cam_h)[3] + got = selftest_digest(bundle[(cam_w, cam_h)], cam_w, cam_h, nv12_size) + if got != want: + raise RuntimeError(f"warp self-test {got[:12]} != {want[:12]}; artifact computes differently here") + + +class FrameWarp: + + def __init__(self, cam_w: int, cam_h: int, frame_skip: int): + from tinygrad.tensor import Tensor + + pkl_path = os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl") + bundle = None + if os.path.isfile(pkl_path): + try: + bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip) + except Exception as e: + cloudlog.warning(f"warp artifact unusable ({e}); discarding and recompiling") + os.remove(pkl_path) + if bundle is None: + cloudlog.warning(f"warp artifact missing; compiling for {cam_w}x{cam_h} (one-time)") + from iqpilot.selfdrive.iqmodeld.tools.compile_warp import compile_warp + compile_warp(cam_w, cam_h, pkl_path, frame_skip=frame_skip) + cloudlog.warning(f"warp compiled -> {pkl_path}") + bundle = _load_bundle(pkl_path, cam_w, cam_h, frame_skip) + self._jit = bundle[(cam_w, cam_h)] + + self._npy = {"tfm": np.zeros((3, 3), dtype=np.float32), "big_tfm": np.zeros((3, 3), dtype=np.float32)} + self._tensors = {k: Tensor(v, device="NPY").realize() for k, v in self._npy.items()} + self._blob_cache: dict[tuple[str, int], object] = {} + self._Tensor = Tensor + + def _frame_tensor(self, key: str, buf): + from tinygrad.device import Device + arr = np.frombuffer(buf.data, dtype=np.uint8) + ck = (key, arr.ctypes.data) + t = self._blob_cache.get(ck) + if t is None: + t = self._Tensor.from_blob(arr.ctypes.data, (arr.size,), dtype="uint8", device=Device.DEFAULT) + self._blob_cache[ck] = t + return t + + def run(self, main_buf, extra_buf, main_tfm: np.ndarray, extra_tfm: np.ndarray) -> np.ndarray: + self._npy["tfm"][:] = main_tfm + self._npy["big_tfm"][:] = extra_tfm + warped = self._jit(tfm=self._tensors["tfm"], big_tfm=self._tensors["big_tfm"], + frame=self._frame_tensor("img", main_buf), + big_frame=self._frame_tensor("big_img", extra_buf)) + return warped.numpy().astype(np.uint8, copy=False) diff --git a/iqpilot/selfdrive/iqmodeld/modeld_selector.py b/iqpilot/selfdrive/iqmodeld/modeld_selector.py new file mode 100644 index 000000000..5d0c37f5d --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/modeld_selector.py @@ -0,0 +1,373 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import json +import os +import threading +import time +from collections import deque + +from iqpilot.cereal.messaging import PubMaster, log_from_bytes +from setproctitle import setproctitle + +from iqpilot.common.filter_simple import FirstOrderFilter +from iqpilot.common.params import Params +from iqpilot.common.realtime import config_realtime_process +from iqpilot.common.swaglog import cloudlog +from iqpilot.selfdrive.iqmodeld.model_channel import BIG_CHANNEL, SMALL_CHANNEL, ModelChannel + +PROCESS_NAME = "iqpilot.selfdrive.iqmodeld.modeld_selector" + +BIG_MODEL_DEADLINE = float(os.getenv("IQEMAC_BIG_DEADLINE_MS", "45")) / 1000.0 +BIG_MAX_LAG_FRAMES = int(os.getenv("IQEMAC_MAX_BIG_LAG_FRAMES", "6")) +BIG_FUTURE_ACCEPT = int(os.getenv("IQEMAC_BIG_FUTURE_ACCEPT", "2")) +BIG_ANCHOR_MS = float(os.getenv("IQEMAC_BIG_ANCHOR_MS", "90")) +BIG_WAIT_FLOOR_S = 0.002 +BIG_WAIT_CEIL_S = float(os.getenv("IQEMAC_BIG_WAIT_CEIL_MS", "58")) / 1000.0 +BIG_MISS_LIMIT = int(os.getenv("IQEMAC_BIG_MISS_LIMIT", "80")) +ACTIVATE_WINDOW = int(os.getenv("IQEMAC_ACTIVATE_WINDOW", "50")) +ACTIVATE_FRAC = float(os.getenv("IQEMAC_ACTIVATE_FRAC", "0.7")) +REARM_LIMIT = int(os.getenv("IQEMAC_REARM_LIMIT", "2")) +MODEL_FREQ = 20.0 +WARMUP_FRAMES = 40 +STATUS_WINDOW = int(os.getenv("IQEMAC_STATUS_EVERY", "20")) + +SELECTOR_SERVICES = ["modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"] + +EMAC_STATUS_KEYS = { + "active": "MacModelActive", "failed": "MacModelFailed", "last_error": "MacModelLastError", + "latency_ms": "MacModelLatencyMs", "status": "MacModelStatus", + "reachable": "MacModelReachable", "progress": "MacModelDownloadProgress", +} +EGPU_STATUS_KEYS = { + "active": "UsbGpuActive", "failed": "UsbGpuFailed", "last_error": "UsbGpuLastError", + "latency_ms": "UsbGpuLatencyMs", "status": "UsbGpuStatus", + "reachable": "UsbGpuPresent", "progress": "UsbGpuSetupProgress", +} + + +def backend_status_keys(emac_enabled: bool, egpu_enabled: bool, egpu_present: bool = False) -> dict[str, str]: + from iqpilot.selfdrive.iqmodeld.egpu_helpers import resolve_backend + return EGPU_STATUS_KEYS if resolve_backend(emac_enabled, egpu_enabled, egpu_present) == "egpu" else EMAC_STATUS_KEYS + + +def resolve_status_keys(params): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_present_consented, egpu_selected + return backend_status_keys(params.get_bool("IQEmacEnabled"), egpu_selected(params), egpu_present_consented(params)) + + +def resolve_model_name(params, keys) -> str: + if keys is EGPU_STATUS_KEYS: + from iqpilot.selfdrive.iqmodeld.egpu_model import DEFAULT_EGPU_MODEL, resolve_egpu_model + resolved = resolve_egpu_model(params, allow_refresh=False) + return resolved["key"] if resolved else DEFAULT_EGPU_MODEL + name = params.get("IQEmacModel") or b"lebrowski" + return name.decode() if isinstance(name, bytes) else name + + +class AsyncParamWriter: + + def __init__(self, params: Params): + self._params = params + self._pending: dict[str, object] = {} + self._lock = threading.Lock() + self._event = threading.Event() + threading.Thread(target=self._drain, daemon=True).start() + + def put(self, key: str, value) -> None: + with self._lock: + self._pending[key] = value + self._event.set() + + def put_bool(self, key: str, value: bool) -> None: + self.put(key, bool(value)) + + def _drain(self) -> None: + while True: + self._event.wait() + self._event.clear() + with self._lock: + batch, self._pending = self._pending, {} + for key, value in batch.items(): + try: + if isinstance(value, bool): + self._params.put_bool(key, value) + else: + self._params.put(key, value) + except Exception: + cloudlog.exception(f"async param write failed: {key}") + + +def wait_for_big(big_channel, target: int, deadline: float, min_frame: int = -1, + max_lag_frames: int = BIG_MAX_LAG_FRAMES) -> tuple[dict | None, int | None]: + big_peek = None + grab_at = deadline - 0.004 + while time.perf_counter() < deadline: + bfid = big_channel.peek_frame_id() + big_peek = bfid + if bfid == target - 1 and time.perf_counter() < grab_at: + time.sleep(0.0005) + continue + if bfid is not None and min_frame < bfid <= target + BIG_FUTURE_ACCEPT and target - bfid <= max_lag_frames: + got = big_channel.read() + if got is not None and got[0] == bfid: + return got[1], big_peek + break + if bfid is None or bfid <= min_frame or bfid > target + BIG_FUTURE_ACCEPT or target - bfid > max_lag_frames: + break + time.sleep(0.0005) + return None, big_peek + + +class BigLatch: + + def __init__(self, miss_limit: int = BIG_MISS_LIMIT, activate_window: int = ACTIVATE_WINDOW, + activate_frac: float = ACTIVATE_FRAC, rearm_limit: int = REARM_LIMIT): + self.miss_limit = miss_limit + self.activate_window = activate_window + self.activate_need = int(round(activate_window * activate_frac)) + self.rearm_limit = rearm_limit + self.active = False + self.done = False + self._miss = 0 + self._window: deque[bool] = deque(maxlen=activate_window) + self._retires = 0 + + def update(self, used_big: bool) -> tuple[bool, bool]: + if self.done: + return False, False + if not self.active: + self._window.append(used_big) + if len(self._window) >= self.activate_window and sum(self._window) >= self.activate_need: + self.active = True + self._miss = 0 + self._window.clear() + return True, False + if used_big: + self._miss = 0 + elif self.active: + self._miss += 1 + if self._miss >= self.miss_limit: + self.active = False + self._miss = 0 + self._window.clear() + self._retires += 1 + self.done = self._retires > self.rearm_limit + return False, True + return False, False + + +def _patch_and_send(pm: PubMaster, payload: dict, frame_drop_perc: float, selector_dropped: int, + target: int, source_lag: int, mismatch: bool | None = None) -> None: + msgs = payload["msgs"] + if mismatch is None: + mismatch = source_lag > 0 + + model_msg = log_from_bytes(msgs["modelV2"]).as_builder() + if mismatch: + model_msg.modelV2.frameId = target + model_msg.modelV2.frameAge = max(model_msg.modelV2.frameAge, source_lag) + model_msg.modelV2.frameDropPerc = frame_drop_perc + pm.send("modelV2", model_msg) + + driving_msg = log_from_bytes(msgs["drivingModelData"]).as_builder() + if mismatch: + driving_msg.drivingModelData.frameId = target + driving_msg.drivingModelData.frameDropPerc = frame_drop_perc + pm.send("drivingModelData", driving_msg) + + pose_msg = log_from_bytes(msgs["cameraOdometry"]).as_builder() + if mismatch: + pose_msg.cameraOdometry.frameId = target + pose_msg.valid = bool(payload["live_calib_seen"]) and selector_dropped < 1 and not mismatch + pm.send("cameraOdometry", pose_msg) + + pm.send("iqDriveModelData", msgs["iqDriveModelData"]) + + +def _read_float(params, key: str, default: float) -> float: + v = params.get(key) + try: + return float(v) if v is not None else default + except (TypeError, ValueError): + return default + + +def main() -> None: + cloudlog.warning("modeld_selector init") + cloudlog.bind(daemon=PROCESS_NAME) + setproctitle(PROCESS_NAME) + config_realtime_process([0, 1, 2, 3], 54) + + params = Params() + keys = resolve_status_keys(params) + pwriter = AsyncParamWriter(params) + pwriter.put_bool(keys["active"], False) + pwriter.put_bool(keys["failed"], False) + pm = PubMaster(SELECTOR_SERVICES) + + small_channel: ModelChannel | None = None + big_channel: ModelChannel | None = None + latch = BigLatch() + big_used_count = 0 + run_count = 0 + last_published = -1 + last_big_published = -1 + frame_dropped_filter = FirstOrderFilter(0.0, 10.0, 1.0 / MODEL_FREQ) + recent_big = deque(maxlen=STATUS_WINDOW) + model_name = resolve_model_name(params, keys) + last_backend_check = 0.0 + last_latency_ms = 0.0 + last_source_lag = 0 + miss_reasons = {"no_head": 0, "already_used": 0, "far_future": 0, + "too_stale": 0, "head_prev_timeout": 0, "read_race": 0} + + cloudlog.warning(f"modeld_selector starting (max_big_lag_frames={BIG_MAX_LAG_FRAMES})") + while True: + if small_channel is None: + try: + small_channel = ModelChannel(SMALL_CHANNEL, create=False) + except OSError: + time.sleep(0.05) + continue + if big_channel is None: + try: + big_channel = ModelChannel(BIG_CHANNEL, create=False) + except OSError: + big_channel = None + + fid = small_channel.peek_frame_id() + if fid is None or fid == last_published: + time.sleep(0.0005) + continue + if last_published >= 0 and fid < last_published - 1: + cloudlog.warning(f"modeld_selector frame reset {last_published} -> {fid}; re-arming") + last_published = -1 + last_big_published = -1 + big_used_count = 0 + run_count = 0 + latch = BigLatch() + pwriter.put_bool(keys["active"], False) + pwriter.put_bool(keys["failed"], False) + + now_mono = time.monotonic() + if now_mono - last_backend_check > 1.0: + last_backend_check = now_mono + new_keys = resolve_status_keys(params) + if new_keys is not keys: + cloudlog.warning(f"modeld_selector backend changed {keys['active']} -> {new_keys['active']}; re-arming") + pwriter.put_bool(keys["active"], False) + pwriter.put_bool(keys["failed"], False) + keys = new_keys + model_name = resolve_model_name(params, keys) + recent_big.clear() + last_big_published = -1 + big_used_count = 0 + run_count = 0 + latch = BigLatch() + pwriter.put_bool(keys["active"], False) + pwriter.put_bool(keys["failed"], False) + target = fid + t_start = time.perf_counter() + + small_payload = None + got = small_channel.read() + if got is not None and got[0] == target: + small_payload = got[1] + + payload = None + used_big = False + big_peek = None + if big_channel is not None and not latch.done: + deadline = t_start + BIG_MODEL_DEADLINE + sof_ns = (small_payload or {}).get("timestamp_sof") + if sof_ns: + remaining = (BIG_ANCHOR_MS / 1000.0) - (time.clock_gettime(time.CLOCK_BOOTTIME) - sof_ns / 1e9) + deadline = t_start + min(max(remaining, BIG_WAIT_FLOOR_S), BIG_WAIT_CEIL_S) + payload, big_peek = wait_for_big(big_channel, target, deadline, + last_big_published, BIG_MAX_LAG_FRAMES) + used_big = payload is not None + if not used_big: + if big_peek is None: + miss_reasons["no_head"] += 1 + elif big_peek <= last_big_published: + miss_reasons["already_used"] += 1 + elif big_peek > target + BIG_FUTURE_ACCEPT: + miss_reasons["far_future"] += 1 + elif target - big_peek > BIG_MAX_LAG_FRAMES: + miss_reasons["too_stale"] += 1 + elif big_peek == target - 1: + miss_reasons["head_prev_timeout"] += 1 + else: + miss_reasons["read_race"] += 1 + + if payload is None: + payload = small_payload + if payload is None: + got = small_channel.read() + if got is not None and got[0] == target: + payload = got[1] + + activated_now, failed_now = latch.update(used_big) + if activated_now: + pwriter.put_bool(keys["active"], True) + pwriter.put_bool(keys["failed"], False) + cloudlog.warning(f"modeld_selector switched to BIG model at frame {target}") + elif failed_now: + pwriter.put_bool(keys["active"], False) + pwriter.put_bool(keys["failed"], latch.done) + pwriter.put(keys["last_error"], "big model stalled onroad; local fallback latched" + if latch.done else "big model stalled onroad; small active, big may re-arm") + if latch.done: + cloudlog.warning(f"modeld_selector big stalled, staying on small until next ignition (frame {target})") + else: + cloudlog.warning(f"modeld_selector big stalled, small active; big may re-arm after a clean streak (frame {target})") + + if payload is not None: + selector_dropped = max(0, target - last_published - 1) if last_published >= 0 else 0 + frames_dropped = frame_dropped_filter.update(min(selector_dropped, 10)) + if run_count < WARMUP_FRAMES: + frame_dropped_filter.x = 0.0 + frames_dropped = 0.0 + run_count += 1 + recent_big.append(used_big) + if used_big: + big_used_count += 1 + big_fid = int(payload.get("frame_id", big_peek if big_peek is not None else target)) + last_big_published = min(big_fid, target) + last_latency_ms = float(payload.get("model_execution_time", 0.0)) * 1e3 + source_lag = max(0, target - int(payload.get("frame_id", target))) + frame_mismatch = int(payload.get("frame_id", target)) != target + last_source_lag = source_lag + if run_count % STATUS_WINDOW == 0: + hit_rate = (sum(recent_big) / len(recent_big)) if recent_big else 0.0 + pwriter.put(keys["latency_ms"], last_latency_ms) + pwriter.put(keys["status"], json.dumps({ + "active": latch.active, + "failed": latch.done, + "hit_rate": round(hit_rate, 3), + "latency_ms": round(last_latency_ms, 1), + "source_lag_frames": last_source_lag, + "model": model_name, + "reachable": params.get_bool(keys["reachable"]), + "download_progress": _read_float(params, keys["progress"], 1.0), + "ts_mono": round(time.monotonic(), 1), + })) + if run_count % 100 == 0: + cloudlog.warning(f"modeld_selector misses: {miss_reasons}") + cloudlog.warning(f"modeld_selector: big_used={big_used_count}/{run_count} " + f"last_big_peek={big_peek} target={target} active={latch.active} " + f"max_big_lag={BIG_MAX_LAG_FRAMES}") + + frame_drop_perc = 100.0 * frames_dropped / (1.0 + frames_dropped) + _patch_and_send(pm, payload, frame_drop_perc, selector_dropped, target, source_lag, frame_mismatch) + last_published = target + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + cloudlog.warning("modeld_selector got SIGINT") diff --git a/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py b/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py index 56f7883bd..8ede5f6c4 100644 --- a/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py +++ b/iqpilot/selfdrive/iqmodeld/models/runners/model_runner.py @@ -129,7 +129,7 @@ class ModelRunner(RunnerRoot): if not active: raise ValueError("runner started without an active model bundle") - self.models = {spec.type.raw: ArtifactSpec(spec) for spec in active.models} + self.models = {spec.type.raw: ArtifactSpec(spec) for spec in _qcom_models(active)} self.is_20hz_3d = False self.is_20hz = active.is20hz self.inputs = {} @@ -180,8 +180,15 @@ class ModelRunner(RunnerRoot): # ---- runner selection (which backend to build for the active bundle) ---------- +def _qcom_models(bundle) -> list: + # usbeMac artifacts ride along in a bundle for the eGPU host; they are never + # loaded on QCOM and must not affect runner classification + return [m for m in bundle.models if m.type.raw != ModelType.usbeMac] + + def _single_artifact_prefix(bundle, prefix: str) -> bool: - return len(bundle.models) == 1 and bundle.models[0].artifact.fileName.startswith(prefix) + models = _qcom_models(bundle) + return len(models) == 1 and models[0].artifact.fileName.startswith(prefix) def _is_fused_bundle(bundle) -> bool: @@ -193,7 +200,7 @@ def _is_supercombo_bundle(bundle) -> bool: def _is_split_bundle(bundle) -> bool: - present = {m.type.raw for m in bundle.models} + present = {m.type.raw for m in _qcom_models(bundle)} split_kinds = {ModelType.vision, ModelType.policy, ModelType.offPolicy, ModelType.onPolicy} return not present.isdisjoint(split_kinds) @@ -205,7 +212,9 @@ def get_model_runner() -> "ModelRunner": from iqpilot.selfdrive.iqmodeld.models.runners.tinygrad.tinygrad_runner import (TinygradRunner, TinygradSplitRunner) bundle = _fetch_bundle() - if not (bundle and bundle.models): + # an eMac-only bundle (no QCOM-loadable models) runs the stock default on + # device; the big host serves the bundle's precompiled artifact + if not (bundle and bundle.models and _qcom_models(bundle)): return TinygradRunner(ModelType.supercombo) if _is_supercombo_bundle(bundle): @@ -219,4 +228,4 @@ def get_model_runner() -> "ModelRunner": return TinygradCombinedSplitRunner() if _is_split_bundle(bundle): return TinygradSplitRunner() - return TinygradRunner(bundle.models[0].type.raw) + return TinygradRunner(_qcom_models(bundle)[0].type.raw) diff --git a/iqpilot/selfdrive/iqmodeld/temporal_state.py b/iqpilot/selfdrive/iqmodeld/temporal_state.py new file mode 100644 index 000000000..70cac9f64 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/temporal_state.py @@ -0,0 +1,131 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +from __future__ import annotations + +import math + +import numpy as np + +DEFAULT_FRAME_SKIP = 4 + +MODEL_INPUT_SPEC: dict[str, tuple[tuple[int, ...], str]] = { + "img": ((1, 12, 128, 256), "uint8"), + "big_img": ((1, 12, 128, 256), "uint8"), + "desire_pulse": ((1, 25, 8), "float32"), + "traffic_convention": ((1, 2), "float32"), + "features_buffer": ((1, 24, 512), "float32"), + "action_t": ((1, 2), "float32"), +} + + +def spec_from_meta(meta: dict) -> dict[str, tuple[tuple[int, ...], str]] | None: + shapes = meta.get("input_shapes") + if not shapes: + return None + return {name: (tuple(shape), "uint8" if name in ("img", "big_img") else "float32") + for name, shape in shapes.items()} + + +class TemporalInputState: + def __init__(self, frame_skip: int, spec: dict[str, tuple[tuple[int, ...], str]] = MODEL_INPUT_SPEC): + self.frame_skip = frame_skip + img = spec["img"][0] + fb = spec["features_buffer"][0] + dp = spec["desire_pulse"][0] + + self.n_frames = img[1] // 6 + img_q_shape = (frame_skip * (self.n_frames - 1) + 1, 6, img[2], img[3]) + self._img_shape = img + self._fb_shape = fb + self._dp_shape = dp + feat_dim = math.prod(fb[2:]) + + self.img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.feat_q = np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32) + self.desire_q = np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32) + self.prev_desire = np.zeros(dp[2], dtype=np.float32) + self.prev_feat = np.zeros((fb[0], feat_dim), dtype=np.float32) + + @staticmethod + def _shift_append(q: np.ndarray, new_val: np.ndarray) -> None: + q[:-1] = q[1:] + q[-1] = new_val + + def push_and_materialize(self, warped: np.ndarray, desire_pulse: np.ndarray, + traffic_convention: np.ndarray, action_t: np.ndarray, + ) -> dict[str, np.ndarray]: + fs = self.frame_skip + + cur = desire_pulse.astype(np.float32).copy() + cur[0] = 0 + pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32) + self.prev_desire[:] = cur + + self._shift_append(self.img_q, warped[0]) + self._shift_append(self.big_img_q, warped[1]) + self._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2])) + self._shift_append(self.feat_q, self.prev_feat) + + dp = self._dp_shape + return { + "img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape), + "big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape), + "features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape), + "desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp), + "traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1), + "action_t": action_t.astype(np.float32).reshape(1, -1), + } + + def note_hidden_state(self, model_output: np.ndarray, hidden_slice: slice) -> None: + self.prev_feat[:] = model_output[hidden_slice].reshape(self.prev_feat.shape) + + +class SplitTemporalState: + + def __init__(self, frame_skip: int, img_shape: tuple[int, ...], + feature_shape: tuple[int, ...], desire_shape: tuple[int, ...]): + self.frame_skip = frame_skip + self._img_shape = tuple(img_shape) + self._fb_shape = tuple(feature_shape) + self._dp_shape = tuple(desire_shape) + + n_frames = img_shape[1] // 6 + img_q_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3]) + self.img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.big_img_q = np.zeros(img_q_shape, dtype=np.uint8) + self.feat_q = np.zeros((frame_skip * (feature_shape[1] - 1) + 1, feature_shape[0], feature_shape[2]), + dtype=np.float32) + self.desire_q = np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), dtype=np.float32) + self.prev_desire = np.zeros(desire_shape[2], dtype=np.float32) + + def materialize_vision(self, warped: np.ndarray, desire: np.ndarray) -> dict[str, np.ndarray]: + fs = self.frame_skip + cur = desire.astype(np.float32).copy() + cur[0] = 0 + pulse = np.where(cur - self.prev_desire > 0.99, cur, 0).astype(np.float32) + self.prev_desire[:] = cur + + TemporalInputState._shift_append(self.img_q, warped[0]) + TemporalInputState._shift_append(self.big_img_q, warped[1]) + TemporalInputState._shift_append(self.desire_q, pulse.reshape(self._dp_shape[0], self._dp_shape[2])) + return { + "img": np.ascontiguousarray(self.img_q[::fs]).reshape(self._img_shape), + "big_img": np.ascontiguousarray(self.big_img_q[::fs]).reshape(self._img_shape), + } + + def materialize_policy(self, vision_feature: np.ndarray, traffic_convention: np.ndarray, + action_t: np.ndarray | None = None) -> dict[str, np.ndarray]: + fs = self.frame_skip + TemporalInputState._shift_append(self.feat_q, vision_feature.reshape(self._fb_shape[0], self._fb_shape[2])) + dp = self._dp_shape + out = { + "features_buffer": np.ascontiguousarray(self.feat_q[::fs]).reshape(self._fb_shape), + "desire_pulse": self.desire_q.reshape(dp[1], fs, dp[0], dp[2]).max(axis=1).reshape(dp), + "traffic_convention": traffic_convention.astype(np.float32).reshape(1, -1), + } + if action_t is not None: + out["action_t"] = action_t.astype(np.float32).reshape(1, -1) + return out diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py new file mode 100644 index 000000000..3481018e1 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_stock_parity.py @@ -0,0 +1,163 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import types + +import pytest + +from iqpilot.cereal import log, messaging +from iqpilot.cereal.services import SERVICE_LIST + + +class TestTelemetryContract: + def test_service_is_published_at_stock_cadence(self): + assert "egpuDockState" in SERVICE_LIST + assert SERVICE_LIST["egpuDockState"].frequency == 10. + + def test_message_carries_every_stock_field(self): + msg = messaging.new_message("egpuDockState") + state = msg.egpuDockState + for field in ("tempC", "memoryTempC", "powerDrawW", "powerLimitW", "gpuUsagePercent", + "gpuClockMhz", "fanSpeedRpm", "pcieLtssm", "supplyVoltage", "supplyCurrent"): + setattr(state, field, 1) + assert getattr(state, field) == 1 + + def test_metrics_refresh_matches_stock(self): + from iqpilot.selfdrive.iqmodeld.egpu_telemetry import METRICS_REFRESH_EVERY + assert METRICS_REFRESH_EVERY == 100 + + def test_send_without_a_gpu_publishes_an_invalid_message(self): + from iqpilot.selfdrive.iqmodeld import egpu_telemetry + sent = [] + telemetry = egpu_telemetry.EgpuDockTelemetry(types.SimpleNamespace(send=lambda n, m: sent.append((n, m))), big=True) + telemetry._device = lambda: types.SimpleNamespace(_opened_devices=set()) + telemetry.send() + assert sent and sent[0][0] == "egpuDockState" + assert sent[0][1].valid is False + + +class TestBigFrameFlag: + def test_model_message_carries_the_big_flag(self): + msg = messaging.new_message("modelV2") + msg.modelV2.big = True + assert msg.modelV2.big + + +class TestStatusParams: + def test_loading_param_exists_and_is_cleared_like_stock(self): + from pathlib import Path + root = Path(__file__).resolve().parents[3] + keys = (root / "common" / "params_keys.h").read_text() + assert '{"UsbGpuLoading"' in keys + line = next(ln for ln in keys.splitlines() if '"UsbGpuLoading"' in ln) + for flag in ("CLEAR_ON_MANAGER_START", "CLEAR_ON_OFFROAD_TRANSITION", "CLEAR_ON_IGNITION_ON"): + assert flag in line + + +class TestAlerts: + def test_both_stock_big_model_events_exist(self): + assert hasattr(log.OnroadEvent.EventName, "bigModelLoading") + assert hasattr(log.OnroadEvent.EventName, "bigModelFailed") + + def test_alerts_are_wired_with_stock_severities(self): + from iqpilot.selfdrive.selfdrived.events import EVENTS, ET + EventName = log.OnroadEvent.EventName + loading = EVENTS[EventName.bigModelLoading] + failed = EVENTS[EventName.bigModelFailed] + assert ET.NO_ENTRY in loading + assert ET.SOFT_DISABLE in failed and ET.PERMANENT in failed + + +class TestFirmwareGate: + def test_runtime_refuses_a_dock_on_other_firmware(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import usbgpu_present + from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS + vid, pid = EGPU_DOCK_USB_IDS[0] + d = tmp_path / "1-1" + d.mkdir() + (d / "idVendor").write_text(f"{vid:04x}\n") + (d / "idProduct").write_text(f"{pid:04x}\n") + (d / "product").write_text("custom deadbeef-CLEAN\n") + assert not usbgpu_present(str(tmp_path)) + (d / "product").write_text(EGPU_DOCK_FW_PRODUCT + "\n") + assert usbgpu_present(str(tmp_path)) + + +class TestAutoFlash: + def test_hardwared_drives_the_flasher_offroad_only(self): + from iqpilot.system.hardware.hardwared import EgpuDockFlasher + f = EgpuDockFlasher() + calls = [] + f.flash = lambda: calls.append(1) + stale = [{"vendorId": 0xADD1, "productId": 0x0001, "product": "custom deadbeef-CLEAN"}] + f.update(False, stale) + assert f.attempts == 0, "must not flash onroad" + f.update(True, stale) + assert f.attempts == 1 + if f.thread is not None: + f.thread.join(timeout=5) + + def test_matching_firmware_is_never_flashed(self): + from iqpilot.system.hardware.egpu_dock.flash import bundled_version + from iqpilot.system.hardware.hardwared import EgpuDockFlasher + f = EgpuDockFlasher() + f.flash = lambda: pytest.fail("flashed a dock that already matches") + f.update(True, [{"vendorId": 0xADD1, "productId": 0x0001, "product": bundled_version()}]) + assert f.attempts == 0 + + def test_attempts_are_bounded_like_stock(self): + from iqpilot.system.hardware.hardwared import EgpuDockFlasher + assert EgpuDockFlasher.MAX_ATTEMPTS == 3 + assert EgpuDockFlasher.RETRY_INTERVAL == 20. + + +class TestDockIsItsOwnConsent: + + def _params(self, **flags): + class P: + def get_bool(self, k): + return bool(flags.get(k, False)) + def get(self, k, *a, **kw): + return None + return P() + + def _sysfs_with_dock(self, tmp_path, product=None): + from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_USB_IDS + vid, pid = EGPU_DOCK_USB_IDS[0] + d = tmp_path / "1-1" + d.mkdir() + (d / "idVendor").write_text(f"{vid:04x}\n") + (d / "idProduct").write_text(f"{pid:04x}\n") + (d / "product").write_text((product or EGPU_DOCK_FW_PRODUCT) + "\n") + return str(tmp_path) + + def test_a_plugged_in_dock_selects_itself(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + assert egpu_selected(self._params(), self._sysfs_with_dock(tmp_path)) + + def test_nothing_plugged_in_selects_nothing(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + assert not egpu_selected(self._params(), str(tmp_path)) + + def test_a_dock_on_foreign_firmware_does_not_select_itself(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + assert not egpu_selected(self._params(), self._sysfs_with_dock(tmp_path, "custom deadbeef-CLEAN")) + + def test_the_user_can_force_it_off(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + root = self._sysfs_with_dock(tmp_path) + assert not egpu_selected(self._params(IQEgpuDisabled=True), root) + + def test_the_param_can_force_it_on_without_hardware(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + assert egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path)) + + def test_present_dock_wins_even_with_emac_enabled(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present + root = self._sysfs_with_dock(tmp_path) + assert resolve_backend(True, egpu_selected(self._params(), root), usbgpu_present(root)) == "egpu" + + def test_force_param_without_hardware_yields_to_emac(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present + assert resolve_backend(True, egpu_selected(self._params(IQEgpuEnabled=True), str(tmp_path)), + usbgpu_present(str(tmp_path))) == "emac" diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py new file mode 100644 index 000000000..96fae1a8b --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_egpu_worker.py @@ -0,0 +1,509 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import io +import json +import time +import urllib.request + +import numpy as np +import pytest + +from iqpilot.selfdrive.iqmodeld.egpu_helpers import ( + resolve_backend, resolve_download_url, usbgpu_present, +) +from iqpilot.selfdrive.iqmodeld.egpu_pipeline import ( + EgpuPipeline, EgpuPipelineError, make_big_channel_payload, +) +from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model +from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC + + +class FakeParams: + def __init__(self, **flags): + self._flags = {k: bool(v) for k, v in flags.items()} + + def get_bool(self, key: str) -> bool: + return self._flags.get(key, False) + + +def _fake_usb_device(root, vid: str, pid: str, name: str = "1-1", product: str | None = None): + from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT + d = root / name + d.mkdir() + (d / "idVendor").write_text(vid + "\n") + (d / "idProduct").write_text(pid + "\n") + (d / "product").write_text((product if product is not None else EGPU_DOCK_FW_PRODUCT) + "\n") + + +class TestPresence: + def test_present(self, tmp_path): + _fake_usb_device(tmp_path, "add1", "0001") + assert usbgpu_present(str(tmp_path)) + + def test_foreign_firmware_absent(self, tmp_path): + _fake_usb_device(tmp_path, "add1", "0001", product="custom deadbeef-CLEAN") + assert not usbgpu_present(str(tmp_path)) + + def test_wrong_ids_absent(self, tmp_path): + _fake_usb_device(tmp_path, "05ac", "12a8") + assert not usbgpu_present(str(tmp_path)) + + def test_empty_bus_absent(self, tmp_path): + assert not usbgpu_present(str(tmp_path)) + + def test_unreadable_entries_skipped(self, tmp_path): + (tmp_path / "usb1").mkdir() + _fake_usb_device(tmp_path, "add1", "0001", name="1-2") + assert usbgpu_present(str(tmp_path)) + + +class TestBackendResolution: + def test_none(self): + assert resolve_backend(False, False) is None + + def test_emac_only(self): + assert resolve_backend(True, False) == "emac" + + def test_egpu_only(self): + assert resolve_backend(False, True) == "egpu" + + def test_force_param_yields_to_emac_without_hardware(self): + assert resolve_backend(True, True) == "emac" + + def test_present_dock_wins_over_emac(self): + assert resolve_backend(True, True, True) == "egpu" + + +class TestManagerGating: + @pytest.fixture + def pc(self): + return pytest.importorskip("iqpilot.system.manager.process_config") + + def test_egpu_needs_presence(self, pc, monkeypatch): + monkeypatch.setattr(pc, "usbgpu_present", lambda: True) + assert pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None) + assert pc.egpu_enabled(True, FakeParams(), None) + monkeypatch.setattr(pc, "usbgpu_present", lambda: False) + assert not pc.egpu_enabled(True, FakeParams(IQEgpuEnabled=True), None) + assert not pc.egpu_enabled(True, FakeParams(), None) + + def test_present_dock_wins_over_left_on_emac(self, pc, monkeypatch): + monkeypatch.setattr(pc, "usbgpu_present", lambda: True) + both = FakeParams(IQEmacEnabled=True, IQEgpuEnabled=True) + assert not pc.emac_enabled(True, both, None) + assert pc.egpu_enabled(True, both, None) + + def test_emac_runs_when_no_dock(self, pc, monkeypatch): + monkeypatch.setattr(pc, "usbgpu_present", lambda: False) + assert pc.emac_enabled(True, FakeParams(IQEmacEnabled=True), None) + + def test_disabled_dock_yields_to_emac(self, pc, monkeypatch): + monkeypatch.setattr(pc, "usbgpu_present", lambda: True) + both = FakeParams(IQEmacEnabled=True, IQEgpuDisabled=True) + assert pc.emac_enabled(True, both, None) + assert not pc.egpu_enabled(True, both, None) + + def test_disabled_dock_runs_no_backend_when_no_emac(self, pc, monkeypatch): + monkeypatch.setattr(pc, "usbgpu_present", lambda: True) + off = FakeParams(IQEgpuDisabled=True) + assert not pc.egpu_enabled(True, off, None) + assert not pc.emac_enabled(True, off, None) + + def test_selector_runs_for_either_backend(self, pc): + assert pc.big_model_enabled(True, FakeParams(IQEmacEnabled=True), None) + assert pc.big_model_enabled(True, FakeParams(IQEgpuEnabled=True), None) + assert not pc.big_model_enabled(True, FakeParams(), None) + + def test_iqegpumodeld_registered(self, pc): + assert "iqegpumodeld" in pc.managed_processes + assert "maciqmodeld" in pc.managed_processes + + +class TestDownloadResolve: + def test_direct_url_passthrough(self): + assert resolve_download_url("https://x/y.onnx", "0" * 64, 5) == "https://x/y.onnx" + + def test_commalfs_batch(self, monkeypatch): + seen = {} + + def fake_urlopen(req, timeout=0): + seen["url"] = req.full_url + seen["body"] = json.loads(req.data) + return io.BytesIO(json.dumps( + {"objects": [{"actions": {"download": {"href": "https://signed/url"}}}]}).encode()) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + sha = "a5" * 32 + url = resolve_download_url(f"commalfs:{sha}", sha, 1234) + assert url == "https://signed/url" + assert seen["body"]["objects"] == [{"oid": sha, "size": 1234}] + assert seen["url"].endswith("/info/lfs/objects/batch") + + +def _zero_infer(output_len: int, fill=None): + calls = [] + + def infer(inputs): + for name, (shape, dtype) in INPUT_SPEC.items(): + assert tuple(inputs[name].shape) == shape, name + assert inputs[name].dtype == np.dtype(dtype), name + calls.append({k: v.copy() for k, v in inputs.items()}) + out = np.zeros(output_len, dtype=np.float32) + if fill is not None: + out[:] = fill + return out + + infer.calls = calls + return infer + + +def _frame_inputs(seed=0): + rng = np.random.default_rng(seed) + warped = rng.integers(0, 256, (2, 6, 128, 256)).astype(np.uint8) + desire = np.zeros(8, dtype=np.float32) + traffic = np.array([1.0, 0.0], dtype=np.float32) + action_t = np.array([0.25, 0.55], dtype=np.float32) + return warped, desire, traffic, action_t + + +class TestEgpuPipeline: + def setup_method(self): + self.meta = get_egpu_model() + + def test_split_model_rejected(self): + split_meta = {**get_egpu_model(), "key": "some_split", "split": True} + with pytest.raises(EgpuPipelineError, match="split"): + EgpuPipeline(split_meta, _zero_infer(split_meta["output_len"])) + + def test_registry_is_fused_only(self): + assert not any(m.get("split") for m in EGPU_MODELS.values()) + + def test_run_shapes_and_output(self): + infer = _zero_infer(self.meta["output_len"]) + pipe = EgpuPipeline(self.meta, infer) + out = pipe.run(*_frame_inputs()) + assert out.shape == (self.meta["output_len"],) + assert len(infer.calls) == 1 + + def test_hidden_state_feeds_next_features_buffer(self): + output_len = self.meta["output_len"] + hidden = self.meta["output_slices"]["hidden_state"] + + def infer(inputs): + out = np.zeros(output_len, dtype=np.float32) + out[hidden] = np.arange(hidden.stop - hidden.start, dtype=np.float32) + return out + + pipe = EgpuPipeline(self.meta, infer) + pipe.run(*_frame_inputs(1)) + np.testing.assert_array_equal( + pipe.state.prev_feat.reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32)) + pipe.run(*_frame_inputs(2)) + np.testing.assert_array_equal( + pipe.state.feat_q[-1].reshape(-1), np.arange(hidden.stop - hidden.start, dtype=np.float32)) + + def test_desire_rising_edge_pulse(self): + infer = _zero_infer(self.meta["output_len"]) + pipe = EgpuPipeline(self.meta, infer) + warped, _, traffic, action_t = _frame_inputs() + desire_on = np.zeros(8, dtype=np.float32) + desire_on[3] = 1.0 + pipe.run(warped, desire_on, traffic, action_t) + assert infer.calls[-1]["desire_pulse"][0, -1, 3] == 1.0 + for _ in range(5): + pipe.run(warped, desire_on, traffic, action_t) + assert infer.calls[-1]["desire_pulse"][0, :, 3].sum() == 1.0 + + def test_wrong_output_len_raises(self): + pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"] - 1)) + with pytest.raises(EgpuPipelineError, match="length"): + pipe.run(*_frame_inputs()) + + def test_non_finite_output_raises(self): + pipe = EgpuPipeline(self.meta, _zero_infer(self.meta["output_len"], fill=np.nan)) + with pytest.raises(EgpuPipelineError, match="finite"): + pipe.run(*_frame_inputs()) + + +class TestChannelContract: + def _real_msgs(self): + import iqpilot.cereal.messaging as messaging + msgs = {} + for svc in ("modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"): + m = messaging.new_message(svc) + msgs[svc] = m.to_bytes() + return msgs + + def test_payload_keys_match_selector_contract(self): + payload = make_big_channel_payload(7, True, 0.031, 24.0, {"modelV2": b"x"}) + assert payload["source"] == "egpu_big" + for key in ("frame_id", "live_calib_seen", "model_execution_time", "msgs"): + assert key in payload + + def test_selector_consumes_egpu_payload(self, tmp_path): + from iqpilot.selfdrive.iqmodeld.model_channel import ModelChannel + from iqpilot.selfdrive.iqmodeld.modeld_selector import wait_for_big + + chan = ModelChannel(str(tmp_path / "big"), create=True) + payload = make_big_channel_payload(100, True, 0.03, 25.0, self._real_msgs()) + chan.write(100, payload) + + got, peek = wait_for_big(chan, 100, time.perf_counter() + 0.01) + assert peek == 100 + assert got is not None + assert got["source"] == "egpu_big" + assert got["frame_id"] == 100 + + def test_selector_patch_and_send_parses_egpu_msgs(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send + + sent = {} + + class PM: + def send(self, service, msg): + sent[service] = msg + + payload = make_big_channel_payload(42, True, 0.03, 25.0, self._real_msgs()) + _patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=0) + assert set(sent) == {"modelV2", "drivingModelData", "cameraOdometry", "iqDriveModelData"} + assert sent["modelV2"].modelV2.frameDropPerc == 0.0 + assert sent["cameraOdometry"].valid + + def test_selector_lag_patches_frame_id(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import _patch_and_send + + sent = {} + + class PM: + def send(self, service, msg): + sent[service] = msg + + payload = make_big_channel_payload(40, True, 0.03, 25.0, self._real_msgs()) + _patch_and_send(PM(), payload, frame_drop_perc=0.0, selector_dropped=0, target=42, source_lag=2) + assert sent["modelV2"].modelV2.frameId == 42 + assert not sent["cameraOdometry"].valid + + +def _import_worker(): + try: + import iqpilot.selfdrive.iqmodeld.iqegpumodeld as w + return w + except ImportError as e: + if any(tag in str(e) for tag in ("pyx", "visionipc", "proprietary_runtime")): + pytest.skip(f"device-only import chain unavailable on this host: {e}") + raise + + +class TestWorkerModule: + def test_module_imports_off_device(self): + w = _import_worker() + assert w.PROCESS_NAME.endswith("iqegpumodeld") + assert callable(w.main) + + def test_warmup_validates_output(self): + w = _import_worker() + spec = {name: (shape, dtype) for name, (shape, dtype) in INPUT_SPEC.items()} + def good(inputs): + return np.zeros(10, dtype=np.float32) + assert w._warmup(good, spec, 10) >= 0.0 + with pytest.raises(RuntimeError, match="invalid"): + w._warmup(good, spec, 11) + + +class TestSelectorBackendKeys: + def test_emac_default(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys + assert backend_status_keys(False, False) is EMAC_STATUS_KEYS + assert backend_status_keys(True, False) is EMAC_STATUS_KEYS + + def test_egpu_selected(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, backend_status_keys + assert backend_status_keys(False, True) is EGPU_STATUS_KEYS + assert backend_status_keys(False, True)["active"] == "UsbGpuActive" + assert backend_status_keys(False, True)["failed"] == "UsbGpuFailed" + + def test_emac_wins_when_both(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import EMAC_STATUS_KEYS, backend_status_keys + assert backend_status_keys(True, True) is EMAC_STATUS_KEYS + + def test_key_maps_cover_same_roles(self): + from iqpilot.selfdrive.iqmodeld.modeld_selector import EGPU_STATUS_KEYS, EMAC_STATUS_KEYS + assert set(EGPU_STATUS_KEYS) == set(EMAC_STATUS_KEYS) + + +class TestBackendSeparation: + EGPU_SOURCES = ( + "egpu_helpers.py", "egpu_pipeline.py", "egpu_model.py", "iqegpumodeld.py", + "big_catalog.py", "tools/compile_egpu_model.py", + ) + BANNED_IMPORTS = ("emac_input_state", "emac_model_meta", "maciqmodeld", "mac_protocol", "mac_client") + + def _sources(self): + import pathlib + root = pathlib.Path(__file__).resolve().parents[1] + return {name: (root / name).read_text() for name in self.EGPU_SOURCES} + + def test_no_emac_module_imports(self): + for name, src in self._sources().items(): + for banned in self.BANNED_IMPORTS: + assert f"import {banned}" not in src and f"iqmodeld.{banned}" not in src, f"{name} imports {banned}" + + def test_no_macmodel_params(self): + for name, src in self._sources().items(): + assert "MacModel" not in src, f"{name} references MacModel* params" + + def test_emac_shim_reexports_temporal_state(self): + from iqpilot.selfdrive.iqmodeld import emac_input_state, temporal_state + assert emac_input_state.EmacInputState is temporal_state.TemporalInputState + assert emac_input_state.SplitInputState is temporal_state.SplitTemporalState + + def test_emac_modules_are_not_in_the_public_tree(self): + import pathlib + root = pathlib.Path(__file__).resolve().parents[1] + for gone in ("mac_protocol.py", "mac_client.py", "maciqmodeld.py", "bulk_transport.py"): + assert not (root / gone).exists(), f"{gone} must live only in konn3kt_private" + + +class TestMetaDrivenInputSpec: + + def _run_one(self, meta): + seen = {} + def infer(inputs): + seen.update({k: v.shape for k, v in inputs.items()}) + return np.zeros(meta["output_len"], dtype=np.float32) + pipe = EgpuPipeline(meta, infer) + pipe.run(np.zeros((2, 6, 128, 256), np.uint8), np.zeros(8, np.float32), + np.array([1, 0], np.float32), np.zeros(2, np.float32)) + return seen + + def test_default_contract_unchanged(self): + meta = get_egpu_model() + seen = self._run_one(meta) + assert seen["features_buffer"] == (1, 24, 512) + assert seen["desire_pulse"] == (1, 25, 8) + + def test_registry_shapes_drive_the_state(self): + meta = dict(get_egpu_model()) + meta["output_len"] = 18452 + meta["output_slices"] = dict(meta["output_slices"], hidden_state=slice(2066, 18450)) + meta["input_shapes"] = { + "img": (1, 12, 128, 256), "big_img": (1, 12, 128, 256), + "desire_pulse": (1, 33, 8), "traffic_convention": (1, 2), + "action_t": (1, 2), "features_buffer": (1, 32, 32, 512), + } + seen = self._run_one(meta) + assert seen["features_buffer"] == (1, 32, 32, 512) + assert seen["desire_pulse"] == (1, 33, 8) + + +class TestCatalogResolution: + + def _params(self, model, doc=None): + class P: + def get(self, k): + if k == "IQEmacModel": + return model + if k == "IQEmacCatalogCache": + return json.dumps(doc) if doc else None + return None + return P() + + def _doc(self): + return {"schema": 1, "bundles": [{ + "short_name": "ttx", "display_name": "TTx", "index": 1, + "model_name": "big_driving_supercombo", + "wire": {"output_len": 2580, "frame_skip": 4, "pipeline": True, + "output_slices": {"plan": [917, 1907], "hidden_state": [2066, 2578], "pad": [-2, None]}, + "input_shapes": {"img": [1, 12, 128, 256], "big_img": [1, 12, 128, 256], + "desire_pulse": [1, 33, 8], "traffic_convention": [1, 2], + "action_t": [1, 2], "features_buffer": [1, 32, 512]}, + "lat_smooth_seconds": 0.1}, + "source": {"kind": "comma_lfs", "sha256": "c" * 64, "size": 1}, + }]} + + def test_unset_selection_is_the_builtin_default(self): + from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model + m = resolve_egpu_model(self._params(None)) + assert m["key"] == "lebrowski" and m["sha256"].startswith("a501760a") + + def test_catalog_selection_resolves_with_shapes_and_smoothing(self): + from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model + m = resolve_egpu_model(self._params("ttx", self._doc())) + assert m["key"] == "ttx" + assert m["input_shapes"]["features_buffer"] == (1, 32, 512) + assert m["input_shapes"]["desire_pulse"] == (1, 33, 8) + assert m["lat_smooth_seconds"] == 0.1 + assert m["output_slices"]["pad"] == slice(-2, None) + + def test_unknown_selection_is_a_park_not_a_silent_default(self): + from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model + assert resolve_egpu_model(self._params("ghost", self._doc()), allow_refresh=False) is None + + def test_bench_model_is_not_selectable(self): + from iqpilot.selfdrive.iqmodeld.egpu_model import resolve_egpu_model + assert resolve_egpu_model(self._params("comma_small", self._doc()), allow_refresh=False) is None + + def test_registry_carries_no_model_list(self): + from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS + assert set(EGPU_MODELS) == {"lebrowski", "comma_small"} + + +class TestConsentAndIntegrity: + def test_disabled_param_denies_present_dock(self, monkeypatch): + from iqpilot.selfdrive.iqmodeld import egpu_helpers + monkeypatch.setattr(egpu_helpers, "usbgpu_present", lambda sysfs_root=egpu_helpers.USB_SYSFS_ROOT: True) + assert egpu_helpers.egpu_present_consented(FakeParams()) is True + assert egpu_helpers.egpu_present_consented(FakeParams(IQEgpuDisabled=True)) is False + + def test_local_onnx_quarantines_bad_content(self, tmp_path, monkeypatch): + import hashlib + from iqpilot.selfdrive.iqmodeld import egpu_helpers + onnx = tmp_path / "m.onnx" + onnx.write_bytes(b"good") + meta = {"sha256": hashlib.sha256(b"good").hexdigest(), "download": {"size": 4}} + monkeypatch.setattr(egpu_helpers, "onnx_cache_path", lambda m: str(onnx)) + assert egpu_helpers.local_onnx(meta) == str(onnx) + onnx.write_bytes(b"bad!") + assert egpu_helpers.local_onnx(meta) is None + assert not onnx.exists() + assert (tmp_path / "m.onnx.unusable").exists() + + +class TestEgpuDockStatus: + def _run(self, seq): + from iqpilot.system.hardware.egpu_dock.status import EgpuDockStatus + st = EgpuDockStatus() + fired = {} + def set_alert(name, cond, extra=None): + fired[name] = (bool(cond), extra) + for args in seq: + st.update(*args, set_alert) + return {k: v for k, v in fired.items() if v[0]} + + def _dock(self, speed=10000, product="custom ed4e39b7-CLEAN"): + return [{"vendorId": 0xADD1, "productId": 0x0001, "product": product, "speedMbps": speed}] + + def test_no_dock_no_alerts(self): + assert self._run([(True, [], False, False, None, True, None)]) == {} + + def test_usb2_dock_warns_slow(self): + fired = self._run([(True, self._dock(speed=480), False, False, None, True, None)]) + assert fired.get("Offroad_EgpuUsbSlow") == (True, "480 Mbps") + + def test_power_fault_reports_pcie_unavailable(self): + class St: + supplyFault = True + supplyVoltage = 0 + pcieLtssm = 0x78 + tempC = memoryTempC = 40.0 + fanSpeedRpm = 1500 + d = self._dock() + fired = self._run([ + (True, d, False, False, None, True, None), + (False, d, False, True, None, True, None), + (False, d, False, False, b"1", True, St()), + ]) + assert "Offroad_EgpuPcieUnavailable" in fired diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py b/iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py new file mode 100644 index 000000000..ac661b607 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_emac_input_state.py @@ -0,0 +1,111 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import os + +import numpy as np +import pytest + +os.environ.setdefault("DEV", "CPU") + +from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState +from iqpilot.selfdrive.iqmodeld.emac_model_meta import FRAME_SKIP, OUTPUT_LEN, OUTPUT_SLICES +from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC as INPUT_SPEC + +N_FRAMES_TEST = 30 +IMG_SHAPE = INPUT_SPEC["img"][0] +DESIRE_LEN = INPUT_SPEC["desire_pulse"][0][2] + + +class _CaptureRunner: + + def __init__(self): + self.captured: dict[str, np.ndarray] | None = None + + def __call__(self, inputs): + from tinygrad import Tensor + self.captured = {k: v.numpy().copy() for k, v in inputs.items()} + return {"outputs": Tensor(np.zeros((1, OUTPUT_LEN), dtype=np.float32))} + + +@pytest.fixture(scope="module") +def reference(): + from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import ( + POLICY_INPUTS, make_input_queues, make_run_policy, + ) + + input_shapes = {name: shape for name, (shape, _) in INPUT_SPEC.items()} + metadata = {"input_shapes": input_shapes} + capture = _CaptureRunner() + run_policy = make_run_policy(capture, metadata, FRAME_SKIP) + queues, npy = make_input_queues(input_shapes, FRAME_SKIP, device="CPU") + return run_policy, queues, npy, capture, POLICY_INPUTS + + +def _rising_edge(raw_desire: np.ndarray, prev: np.ndarray) -> np.ndarray: + cur = raw_desire.astype(np.float32).copy() + cur[0] = 0 + pulse = np.where(cur - prev > 0.99, cur, 0).astype(np.float32) + prev[:] = cur + return pulse + + +def test_materialized_inputs_match_tinygrad_reference(reference): + from tinygrad import Tensor + + run_policy, queues, npy, capture, policy_inputs = reference + rng = np.random.default_rng(1234) + state = EmacInputState(FRAME_SKIP) + ref_prev_desire = np.zeros(DESIRE_LEN, dtype=np.float32) + + hidden = np.zeros((1, 512), dtype=np.float32) + for frame in range(N_FRAMES_TEST): + warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8) + raw_desire = np.zeros(DESIRE_LEN, dtype=np.float32) + if frame % 3: + raw_desire[int(rng.integers(0, DESIRE_LEN))] = 1.0 + traffic = rng.standard_normal(2).astype(np.float32) + action_t = rng.standard_normal(2).astype(np.float32) + + npy["desire"][:] = _rising_edge(raw_desire, ref_prev_desire) + npy["traffic_convention"][:] = traffic + npy["action_t"][:] = action_t + npy["prev_feat"][:] = hidden + run_policy(warped=Tensor(warped), **{k: queues[k] for k in policy_inputs}) + ref_inputs = capture.captured + + state.prev_feat[:] = hidden + mat = state.push_and_materialize(warped, raw_desire, traffic, action_t) + + for name in INPUT_SPEC: + assert ref_inputs[name].shape == tuple(INPUT_SPEC[name][0]), name + np.testing.assert_array_equal( + mat[name].astype(ref_inputs[name].dtype), ref_inputs[name], + err_msg=f"frame {frame}: materialized {name} diverges from tinygrad reference") + + fake_output = rng.standard_normal(OUTPUT_LEN).astype(np.float32) + state.note_hidden_state(fake_output, OUTPUT_SLICES["hidden_state"]) + hidden = fake_output[OUTPUT_SLICES["hidden_state"]].reshape(1, 512).copy() + + +def test_note_hidden_state_slice(): + state = EmacInputState(FRAME_SKIP) + out = np.arange(OUTPUT_LEN, dtype=np.float32) + state.note_hidden_state(out, OUTPUT_SLICES["hidden_state"]) + np.testing.assert_array_equal(state.prev_feat.reshape(-1), out[OUTPUT_SLICES["hidden_state"]]) + + +def test_desire_pulse_rising_edge_only_once(): + state = EmacInputState(FRAME_SKIP) + held = np.zeros(DESIRE_LEN, dtype=np.float32) + held[3] = 1.0 + warped = np.zeros((2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.uint8) + zeros2 = np.zeros(2, dtype=np.float32) + + first = state.push_and_materialize(warped, held, zeros2, zeros2) + assert first["desire_pulse"][0, -1, 3] == 1.0 + second = state.push_and_materialize(warped, held, zeros2, zeros2) + assert state.desire_q[-1].max() == 0.0 + assert second["desire_pulse"][0, -1, 3] == 1.0 diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py b/iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py index bd4fcba79..de48f580c 100644 --- a/iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py +++ b/iqpilot/selfdrive/iqmodeld/tests/test_lat_delay_source.py @@ -34,6 +34,7 @@ def _daemon(params, steer_control_type): return SimpleNamespace( _params=params, _car_params=car_params, + _channel=None, _sub={"lateralDelay": SimpleNamespace(lateralDelay=LIVE_DELAY)}, _runtime=SimpleNamespace(lat_delay=None, PLANPLUS_CONTROL=None, model_smoothing_max_extra_sec=None), _warps=SimpleNamespace(set_offset=lambda _: None), diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py b/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py new file mode 100644 index 000000000..73a9fe23b --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_selector_version_gate.py @@ -0,0 +1,31 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +The eMac bundles ship `minimum_selector_version = 17`, and the version +gate lives in the COMPILED private selector bundle, not in this repo. If that +bundle is rebuilt from stale source the gate still reads 16, every eMac bundle +is silently dropped as "too new", and the selector simply shows no eMac models +— with no error anywhere. Assert the effective gate instead, so a stale +private bundle fails here rather than on a device. +""" +from iqpilot.selfdrive.iqmodeld.emac_model_meta import EMAC_BUNDLE_MIN_SELECTOR_VERSION +from iqpilot.selfdrive.iqmodeld.models.helpers import is_bundle_version_compatible + + +def test_gate_accepts_the_version_our_emac_bundles_ship(): + assert is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION}), ( + f"the effective selector gate rejects minimumSelectorVersion=" + f"{EMAC_BUNDLE_MIN_SELECTOR_VERSION}; the private selector bundle is stale. " + f"Rebuild it from BOTH iqpilot/models_private_src/helpers.py " + f"(CURRENT_SELECTOR_VERSION) and fetcher.py (MANIFEST_VERSION)." + ) + + +def test_gate_still_accepts_older_bundles(): + # the window is a range, not a floor: bumping it must not orphan the existing catalogue + assert is_bundle_version_compatible({"minimumSelectorVersion": 12}) + assert is_bundle_version_compatible({"minimumSelectorVersion": 16}) + + +def test_gate_rejects_a_bundle_from_the_future(): + assert not is_bundle_version_compatible({"minimumSelectorVersion": EMAC_BUNDLE_MIN_SELECTOR_VERSION + 5}) diff --git a/iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py b/iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py new file mode 100644 index 000000000..e2d54c017 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tests/test_split_input_state.py @@ -0,0 +1,131 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +eMac split-model "prepared input equivalence": SplitInputState must reproduce, +byte-exact, the queue semantics of compile_split_runtime's execute_bundle — +the real tinygrad reference graph run on CPU with stub vision/policy runners, +over a multi-frame random sequence with desire rising edges. +""" +from __future__ import annotations + +import os + +import numpy as np +import pytest + +os.environ.setdefault("DEV", "CPU") + +from iqpilot.selfdrive.iqmodeld.emac_input_state import EmacInputState, SplitInputState + +N_FRAMES_TEST = 30 +FRAME_SKIP = 4 +IMG_SHAPE = (1, 12, 16, 32) # small spatial dims: queue math is shape-generic +FB_SHAPE = (1, 25, 512) +DP_SHAPE = (1, 25, 8) +VISION_OUT_LEN = 1576 +HIDDEN_SLICE = slice(1064, 1576) + +VISION_SHAPES = {"img": IMG_SHAPE, "big_img": IMG_SHAPE} +POLICY_SHAPES = {"desire_pulse": DP_SHAPE, "traffic_convention": (1, 2), "features_buffer": FB_SHAPE} + + +class _StubRunner: + """Stands in for OnnxRunner inside execute_bundle: returns a preset output + and records the materialized inputs it was fed.""" + + def __init__(self, out_len: int): + self.out_len = out_len + self.next_output: np.ndarray | None = None + self.captured: dict[str, np.ndarray] | None = None + + def __call__(self, inputs): + from tinygrad import Tensor + self.captured = {k: v.numpy().copy() for k, v in inputs.items()} + out = self.next_output if self.next_output is not None else np.zeros((1, self.out_len), dtype=np.float32) + return {"outputs": Tensor(out.astype(np.float32))} + + +@pytest.fixture(scope="module") +def reference(): + from tinygrad import Tensor + from iqpilot.selfdrive.iqmodeld.tools.compile_split_runtime import _role_executor + + meta_by_role = { + "vision": {"input_shapes": dict(VISION_SHAPES), "output_slices": {"hidden_state": HIDDEN_SLICE}}, + "policy": {"input_shapes": dict(POLICY_SHAPES), "output_slices": {}}, + } + vision, policy = _StubRunner(VISION_OUT_LEN), _StubRunner(1000) + execute_bundle = _role_executor({"vision": vision, "policy": policy}, meta_by_role, FRAME_SKIP) + + feat_q = Tensor(np.zeros((FRAME_SKIP * (FB_SHAPE[1] - 1) + 1, FB_SHAPE[0], FB_SHAPE[2]), dtype=np.float32), + device="CPU").contiguous().realize() + desire_q = Tensor(np.zeros((FRAME_SKIP * DP_SHAPE[1], DP_SHAPE[0], DP_SHAPE[2]), dtype=np.float32), + device="CPU").contiguous().realize() + return execute_bundle, feat_q, desire_q, vision, policy + + +def test_split_inputs_match_tinygrad_reference(reference): + from tinygrad import Tensor + + execute_bundle, feat_q, desire_q, vision_stub, policy_stub = reference + rng = np.random.default_rng(4321) + state = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE) + ref_prev_desire = np.zeros(DP_SHAPE[2], dtype=np.float32) + + for frame in range(N_FRAMES_TEST): + warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8) + raw_desire = np.zeros(DP_SHAPE[2], dtype=np.float32) + if frame % 3: + raw_desire[int(rng.integers(0, DP_SHAPE[2]))] = 1.0 + traffic = rng.standard_normal((1, 2)).astype(np.float32) + vision_out = rng.standard_normal((1, VISION_OUT_LEN)).astype(np.float32) + vision_stub.next_output = vision_out + + # --- ours --- + vis_inputs = state.materialize_vision(warped, raw_desire) + pol_inputs = state.materialize_policy(vision_out[0, HIDDEN_SLICE], traffic[0]) + + # --- reference graph: rising edge happens outside execute_bundle (run_fused) --- + cur = raw_desire.copy() + cur[0] = 0 + ref_pulse = np.where(cur - ref_prev_desire > 0.99, cur, 0).astype(np.float32) + ref_prev_desire[:] = cur + + execute_bundle( + img=Tensor(vis_inputs["img"], device="CPU").realize(), + big_img=Tensor(vis_inputs["big_img"], device="CPU").realize(), + feat_q=feat_q, desire_q=desire_q, + desire=Tensor(ref_pulse, device="CPU").realize(), + traffic_convention=Tensor(traffic, device="CPU").realize(), + action_t=Tensor(np.zeros((1, 2), dtype=np.float32), device="CPU").realize(), + ) + ref = policy_stub.captured + assert ref is not None + + assert ref["features_buffer"].tobytes() == pol_inputs["features_buffer"].tobytes(), f"features frame {frame}" + assert ref["desire_pulse"].tobytes() == pol_inputs["desire_pulse"].tobytes(), f"desire frame {frame}" + assert ref["traffic_convention"].tobytes() == pol_inputs["traffic_convention"].tobytes() + # vision saw exactly what our img queues materialized + vref = vision_stub.captured + assert vref["img"].tobytes() == vis_inputs["img"].tobytes(), f"img frame {frame}" + assert vref["big_img"].tobytes() == vis_inputs["big_img"].tobytes(), f"big_img frame {frame}" + + +def test_split_img_queue_matches_fused_state(): + # img/desire mechanics are shared with the fused mirror: same warps must + # materialize identical img/big_img in both states + rng = np.random.default_rng(7) + fused_spec = { + "img": (IMG_SHAPE, "uint8"), "big_img": (IMG_SHAPE, "uint8"), + "desire_pulse": (DP_SHAPE, "float32"), "traffic_convention": ((1, 2), "float32"), + "features_buffer": ((1, 24, 512), "float32"), "action_t": ((1, 2), "float32"), + } + fused = EmacInputState(FRAME_SKIP, fused_spec) + split = SplitInputState(FRAME_SKIP, IMG_SHAPE, FB_SHAPE, DP_SHAPE) + for _ in range(12): + warped = rng.integers(0, 256, (2, 6, IMG_SHAPE[2], IMG_SHAPE[3]), dtype=np.int64).astype(np.uint8) + desire = np.zeros(DP_SHAPE[2], dtype=np.float32) + f = fused.push_and_materialize(warped, desire, np.zeros(2, dtype=np.float32), np.zeros(2, dtype=np.float32)) + s = split.materialize_vision(warped, desire) + assert f["img"].tobytes() == s["img"].tobytes() + assert f["big_img"].tobytes() == s["big_img"].tobytes() diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py b/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py new file mode 100644 index 000000000..7402a36a9 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_egpu_model.py @@ -0,0 +1,144 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from __future__ import annotations + +import argparse +import os +import pickle +import time + +os.environ.setdefault("DEV", "USB+AMD:LLVM") +os.environ.setdefault("FLOAT16", "1") +os.environ.setdefault("JIT_BATCH_SIZE", "0") +os.environ.setdefault("GMMU", "0") + +import numpy as np + +from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_pkl_path, local_onnx, patch_tinygrad_fetch_fw +from iqpilot.selfdrive.iqmodeld.egpu_model import EGPU_MODELS, get_egpu_model, resolve_egpu_model +from iqpilot.selfdrive.iqmodeld.temporal_state import MODEL_INPUT_SPEC, spec_from_meta + +INPUT_SPEC = dict(MODEL_INPUT_SPEC) + +patch_tinygrad_fetch_fw() + +SEED = 42 + + +def set_input_spec(meta: dict) -> None: + spec = spec_from_meta(meta) + if spec is not None: + INPUT_SPEC.clear() + INPUT_SPEC.update(spec) + + +def make_run_model(model_runner): + def run_model(**inputs): + out = next(iter(model_runner({k: inputs[k] for k in INPUT_SPEC}).values())).cast("float32") + return out.reshape(-1), + return run_model + + +def _random_inputs(seed: int): + from tinygrad.device import Device + from tinygrad.tensor import Tensor + rng = np.random.default_rng(seed) + out = {} + for name, (shape, dtype) in INPUT_SPEC.items(): + if dtype == "uint8": + arr = rng.integers(0, 256, shape).astype(np.uint8) + else: + arr = rng.standard_normal(shape).astype(np.float32) + out[name] = Tensor(arr, device=Device.DEFAULT).realize() + return out + + +def _run(fn, seed: int) -> np.ndarray: + from tinygrad.device import Device + st = time.perf_counter() + outs = fn(**_random_inputs(seed)) + Device.default.synchronize() + print(f" run(seed={seed}) {(time.perf_counter() - st) * 1e3:6.1f} ms") + return outs[0].numpy().reshape(-1) + + +def compile_model(meta: dict, onnx_path: str, out_path: str) -> str: + from tinygrad.device import Device + from tinygrad.engine.jit import TinyJit + from tinygrad.nn.onnx import OnnxRunner + + if meta.get("split"): + raise RuntimeError(f"model {meta['key']} is a split model; eGPU v1 compiles fused models only") + + jit = TinyJit(make_run_model(OnnxRunner(onnx_path)), prune=True) + + print("capture + replay") + for _ in range(2): + baseline = _run(jit, SEED) + if baseline.shape[0] != meta["output_len"]: + raise RuntimeError(f"model output length {baseline.shape[0]} != registry {meta['output_len']}") + if not np.isfinite(baseline).all(): + raise RuntimeError("compiled model produced non-finite outputs") + + print("pickle round trip") + jit = pickle.loads(pickle.dumps(jit)) + if not np.array_equal(_run(jit, SEED), baseline): + raise RuntimeError("outputs differ from baseline after pickle round trip") + if np.array_equal(_run(jit, SEED + 1), baseline): + raise RuntimeError("outputs insensitive to inputs after pickle round trip") + + from tinygrad.tensor import Tensor + zeros = {name: Tensor(np.zeros(shape, dtype=dtype), device=Device.DEFAULT).realize() + for name, (shape, dtype) in INPUT_SPEC.items()} + flat = jit(**zeros)[0].numpy().reshape(-1) + from iqpilot.selfdrive.iqmodeld.parser import PhaseParser + from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import _slice_outputs, _validate_pose_outputs + _validate_pose_outputs(PhaseParser().parse_vision_outputs(_slice_outputs(flat, meta["output_slices"]))) + + bundle = { + "run_model": jit, + "model_key": meta["key"], + "model_sha256": meta["sha256"], + "output_len": int(meta["output_len"]), + "frame_skip": int(meta["frame_skip"]), + "input_spec": {name: (tuple(shape), dtype) for name, (shape, dtype) in INPUT_SPEC.items()}, + "input_device": Device.DEFAULT, + } + os.makedirs(os.path.dirname(out_path), exist_ok=True) + tmp = out_path + ".part" + with open(tmp, "wb") as f: + pickle.dump(bundle, f, protocol=pickle.HIGHEST_PROTOCOL) + os.replace(tmp, out_path) + return out_path + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("--model", default=None, help=f"registry key, one of {sorted(EGPU_MODELS)}") + p.add_argument("--onnx", default=None) + p.add_argument("--output", default=None) + args = p.parse_args() + + if args.model is not None: + if args.model in EGPU_MODELS: + meta = get_egpu_model(args.model) + else: + from iqpilot.common.params import Params + meta = resolve_egpu_model(Params(), args.model) + if meta is None: + raise SystemExit(f"unknown model {args.model!r}: not a built-in ({sorted(EGPU_MODELS)}) and not in the synced catalog") + else: + meta = get_egpu_model() + set_input_spec(meta) + + onnx_path = args.onnx or local_onnx(meta) + if onnx_path is None or not os.path.isfile(onnx_path): + raise SystemExit(f"onnx not found for {meta['key']}; pass --onnx or let iqegpumodeld download it first") + + out = compile_model(meta, onnx_path, args.output or egpu_pkl_path(meta)) + print(f"saved eGPU jit to {out} ({os.path.getsize(out) / 1e6:.2f} MB)") + + +if __name__ == "__main__": + main() diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py b/iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py new file mode 100644 index 000000000..4da3425e1 --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_emac_warp.py @@ -0,0 +1,9 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +from iqpilot.selfdrive.iqmodeld.tools.compile_warp import MODEL_SIZE, compile_warp, main + +__all__ = ["MODEL_SIZE", "compile_warp", "main"] + +if __name__ == "__main__": + main() diff --git a/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py b/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py new file mode 100644 index 000000000..07e4886eb --- /dev/null +++ b/iqpilot/selfdrive/iqmodeld/tools/compile_warp.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +Compile the backend-neutral warp-only artifact: NV12 camera frames + 3x3 +transforms -> (2, 6, model_h/2, model_w/2) uint8 warped tensor, on the device +GPU (QCOM). maciqmodeld runs this locally +and feed the output to their backend, so the big model's image pipeline is +bit-identical to comma's fused pkl warp stage. + +Run ON the device (needs the QCOM backend): + cd /data/openpilot && DEV=QCOM WARP_DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 \ + python3 iqpilot/selfdrive/iqmodeld/tools/compile_warp.py \ + --camera-resolutions 1928x1208 --output /data/models/emac_warp.pkl +The artifact is then split per-resolution into Paths.model_root(). +""" +from __future__ import annotations + +import argparse +import hashlib +import os +import pickle +from functools import partial + +import numpy as np + +SELFTEST_SEED = 20260817 + +from iqpilot.selfdrive.iqmodeld.temporal_state import DEFAULT_FRAME_SKIP, MODEL_INPUT_SPEC +from iqpilot.selfdrive.iqmodeld.tools.compile_supercombo import ( + NV12Frame, WARP_INPUTS, compile_jit, make_random_images, make_warp, make_warp_input_queues, +) + +MODEL_SIZE = (MODEL_INPUT_SPEC["img"][0][3] * 2, MODEL_INPUT_SPEC["img"][0][2] * 2) # (512, 256) + + +def _parse_size(s: str) -> tuple[int, int]: + w, h = s.lower().split("x") + return int(w), int(h) + + +def compile_warp(cam_w: int, cam_h: int, out_path: str | None = None, + frame_skip: int = DEFAULT_FRAME_SKIP) -> str: + """Compile the warp-only QCOM JIT for one camera resolution and write the pkl. + Returns the artifact path. Callable from the workers so a fresh device + self-provisions the warp instead of erroring — needs the QCOM backend.""" + # the QCOM warp env must be set before tinygrad is imported here + os.environ.setdefault("DEV", "QCOM") + os.environ.setdefault("WARP_DEV", "QCOM") + os.environ.setdefault("IMAGE", "1") + os.environ.setdefault("FLOAT16", "1") + os.environ.setdefault("NOLOCALS", "1") + os.environ.setdefault("JIT_BATCH_SIZE", "0") + from tinygrad.engine.jit import TinyJit + from iqpilot.system.camerad.cameras.nv12_info import get_nv12_info + from iqpilot.system.hardware.hw import Paths + + model_w, model_h = MODEL_SIZE + input_shapes = {name: shape for name, (shape, _) in MODEL_INPUT_SPEC.items()} + nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + make_random_warp_inputs = partial(make_random_images, keys=["frame", "big_frame"], + shape=nv12.size, device=os.getenv("WARP_DEV")) + warp_jit = TinyJit(make_warp(nv12, model_w, model_h, frame_skip), prune=True) + make_warp_queues = partial(make_warp_input_queues, input_shapes, frame_skip) + compiled = compile_jit(warp_jit, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + + # historical artifact name: already-provisioned devices keep their warp + out_path = out_path or os.path.join(Paths.model_root(), f"emac_warp_{cam_w}x{cam_h}_tinygrad.pkl") + os.makedirs(os.path.dirname(out_path), exist_ok=True) + tmp = out_path + ".part" + bundle = {(cam_w, cam_h): compiled, "frame_skip": frame_skip, "model_size": MODEL_SIZE} + bundle["selftest"] = selftest_digest(compiled, cam_w, cam_h, nv12.size) + with open(tmp, "wb") as f: + pickle.dump(bundle, f) + os.replace(tmp, out_path) # atomic: a reader never sees a half-written pkl + return out_path + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("--camera-resolutions", type=_parse_size, nargs="+", default=[(1928, 1208)]) + p.add_argument("--output", default=None) + p.add_argument("--frame-skip", type=int, default=DEFAULT_FRAME_SKIP) + args = p.parse_args() + for cam_w, cam_h in args.camera_resolutions: + out = compile_warp(cam_w, cam_h, args.output, frame_skip=args.frame_skip) + print(f"saved warp JIT to {out} ({os.path.getsize(out) / 1e6:.2f} MB)") + + +if __name__ == "__main__": + main() + + +def selftest_inputs(cam_w: int, cam_h: int, nv12_size: int): + """A fixed synthetic frame pair and pair of matrices. Deterministic so the + digest is reproducible on the device that compiled the artifact.""" + rng = np.random.default_rng(SELFTEST_SEED) + frame = rng.integers(0, 256, nv12_size, dtype=np.uint8) + big_frame = rng.integers(0, 256, nv12_size, dtype=np.uint8) + tfm = np.array([[0.7, 0.02, 300.0], [0.01, 0.7, 240.0], [0.0, 0.0, 1.0]], dtype=np.float32) + big_tfm = np.array([[0.5, 0.01, 380.0], [0.02, 0.5, 300.0], [0.0, 0.0, 1.0]], dtype=np.float32) + return frame, big_frame, tfm, big_tfm + + +def selftest_digest(compiled, cam_w: int, cam_h: int, nv12_size: int) -> str: + """Hash the warp's output for a fixed input. + + A warp artifact pinned to one tinygrad can still unpickle under another and + then compute silently wrong, which reaches the model as a garbage image and + looks like a bad model rather than a stale artifact. A version string cannot + see that; running it can.""" + from tinygrad.tensor import Tensor + frame, big_frame, tfm, big_tfm = selftest_inputs(cam_w, cam_h, nv12_size) + dev = os.getenv("WARP_DEV") or "QCOM" + out = compiled(tfm=Tensor(tfm, device="NPY").realize(), + big_tfm=Tensor(big_tfm, device="NPY").realize(), + frame=Tensor(frame, device=dev).realize(), + big_frame=Tensor(big_frame, device=dev).realize()) + return hashlib.sha256(out.numpy().astype(np.uint8).tobytes()).hexdigest() diff --git a/iqpilot/selfdrive/selfdrived/alerts_offroad.json b/iqpilot/selfdrive/selfdrived/alerts_offroad.json index 65a8577eb..6fcc7fe6f 100644 --- a/iqpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/iqpilot/selfdrive/selfdrived/alerts_offroad.json @@ -54,5 +54,33 @@ "text": "Unsupported branch! - The current version of %1 is not marked as compatible with the Comma Three (3|tici). Please go to [Device > Software] and install a supported branch such as release or beta for the comma three.", "severity": 1, "_comment": "Set extra field to the current branch name." + }, + "Offroad_EgpuNotDetected": { + "text": "eGPU dock not detected. Check USB and 12V connections.", + "severity": 0 + }, + "Offroad_EgpuFansObstructed": { + "text": "eGPU dock fans obstructed. Check the fans.", + "severity": 0 + }, + "Offroad_EgpuOverheated": { + "text": "eGPU dock overheated. Allow it to cool.", + "severity": 0 + }, + "Offroad_EgpuPcieUnavailable": { + "text": "eGPU dock PCIe unavailable. %1", + "severity": 0 + }, + "Offroad_EgpuUncompiled": { + "text": "eGPU big model not compiled. Keep ignition on and reboot the device.", + "severity": 0 + }, + "Offroad_EgpuUpdateFailed": { + "text": "eGPU dock update failed. Check the USB cable.", + "severity": 0 + }, + "Offroad_EgpuUsbSlow": { + "text": "eGPU dock USB link is slow. Check the USB cable. The current speed is %1.", + "severity": 0 } } diff --git a/iqpilot/selfdrive/selfdrived/events.py b/iqpilot/selfdrive/selfdrived/events.py index 2e36bfe34..60a37668e 100755 --- a/iqpilot/selfdrive/selfdrived/events.py +++ b/iqpilot/selfdrive/selfdrived/events.py @@ -253,6 +253,15 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { "Ensure road ahead is clear"), }, + EventName.bigModelLoading: { + ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), + }, + + EventName.bigModelFailed: { + ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"), + ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.), + }, + EventName.lateralManeuver: { ET.WARNING: longitudinal_maneuver_alert, ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"), diff --git a/iqpilot/selfdrive/selfdrived/selfdrived.py b/iqpilot/selfdrive/selfdrived/selfdrived.py index 10176b9ac..6d861d37f 100755 --- a/iqpilot/selfdrive/selfdrived/selfdrived.py +++ b/iqpilot/selfdrive/selfdrived/selfdrived.py @@ -143,6 +143,9 @@ class SelfdriveD(GapButtonActions): self.events = Events() self.initialized = False + self.big_model_loading = False + self.big_model_active = False + self.big_model_failed = False self.enabled = False self.active = False self.mismatch_counter = 0 @@ -270,6 +273,28 @@ class SelfdriveD(GapButtonActions): self.events.add(EventName.joystickDebug) self.startup_event = None + if self.sm['deviceState'].egpuDockPresent or self.params.get_bool("IQEgpuEnabled") or self.big_model_active: + loading = self.params.get_bool("UsbGpuLoading") + self.big_model_loading = loading + if self.big_model_loading: + self.events.add(EventName.bigModelLoading) + + big_active = self.params.get("UsbGpuActive") + dock_present = self.sm['deviceState'].egpuDockPresent + mac_active = self.params.get_bool("MacModelActive") + model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] + big_failed = (big_active is False or model_unavailable + or (self.big_model_active and not dock_present)) and not mac_active + if big_failed: + self.events.add(EventName.bigModelFailed) + self.big_model_failed = big_failed + + # soft disable if the big model fails + if big_active: + self.big_model_active = True + if mac_active or (not self.enabled and not model_unavailable): + self.big_model_active = False + if self.sm.recv_frame['lateralManeuverPlan'] > 0: self.events.add(EventName.lateralManeuver) self.startup_event = None diff --git a/iqpilot/selfdrive/test/process_replay/migration.py b/iqpilot/selfdrive/test/process_replay/migration.py index 09038a748..a32bc5485 100644 --- a/iqpilot/selfdrive/test/process_replay/migration.py +++ b/iqpilot/selfdrive/test/process_replay/migration.py @@ -135,7 +135,9 @@ def migrate_drivingModelData(msgs): setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field)) for meta_field in ["laneChangeState", "laneChangeState"]: setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field)) - if len(msg.modelV2.laneLines) and len(msg.modelV2.laneLineProbs): + lane_lines = msg.modelV2.laneLines + lane_probs = msg.modelV2.laneLineProbs + if len(lane_lines) > 2 and len(lane_probs) > 2 and len(lane_lines[1].y) and len(lane_lines[2].y): fill_lane_line_meta(dmd.drivingModelData.laneLineMeta, msg.modelV2.laneLines, msg.modelV2.laneLineProbs) if all(len(a) for a in [msg.modelV2.position.x, msg.modelV2.position.y, msg.modelV2.position.z]): fill_xyz_poly(dmd.drivingModelData.path, ModelConstants.POLY_PATH_DEGREE, msg.modelV2.position.x, msg.modelV2.position.y, msg.modelV2.position.z) diff --git a/iqpilot/selfdrive/test/process_replay/test_migration.py b/iqpilot/selfdrive/test/process_replay/test_migration.py new file mode 100644 index 000000000..68261faea --- /dev/null +++ b/iqpilot/selfdrive/test/process_replay/test_migration.py @@ -0,0 +1,19 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos +""" +from iqpilot.cereal import messaging +from iqpilot.selfdrive.test.process_replay.migration import migrate_drivingModelData + + +def test_driving_model_migration_ignores_incomplete_lane_metadata(): + msg = messaging.new_message("modelV2") + msg.modelV2.init("laneLines", 4) + for lane_line in msg.modelV2.laneLines: + lane_line.y = [1.0] + msg.modelV2.laneLineProbs = [0.5] + + _, added, _ = migrate_drivingModelData([(0, msg.as_reader())]) + + assert len(added) == 1 + assert added[0].drivingModelData.laneLineMeta.leftProb == 0.0 + assert added[0].drivingModelData.laneLineMeta.rightProb == 0.0 diff --git a/iqpilot/system/hardware/egpu_dock/TESTING.md b/iqpilot/system/hardware/egpu_dock/TESTING.md new file mode 100644 index 000000000..38542e401 --- /dev/null +++ b/iqpilot/system/hardware/egpu_dock/TESTING.md @@ -0,0 +1,47 @@ +# eGPU dock bring-up runbook + +Our flasher is a port of comma's known-working one, byte-identical firmware +bundle, but it has never touched real hardware. Order matters: everything +read-only first, evidence at every step. + +Run everything as root from the repo root on the device, dock on the USB-C +port, car ignition off. + +## 1. Read-only probe (safe, run first, share the output) + + sudo python3 iqpilot/system/hardware/egpu_dock/dock_probe.py + +Expected on a dock previously flashed by stock openpilot: product matches the +bundled `custom ed4e39b7-CLEAN`, USB3 speed, PCIe link L0, stable config read. +Any other result: stop and send the output before proceeding. + +## 2. Flash-path validation (writes, but writes the same bytes) + +A stock-flashed dock already runs our exact bundled firmware, so the +no-op path proves version detection: + + sudo python3 iqpilot/system/hardware/egpu_dock/flash.py + +Expected: "firmware matches" and no write. Then exercise the full write path +by reflashing the identical image: + + sudo python3 iqpilot/system/hardware/egpu_dock/flash.py --force + +This backs up the per-unit config page to /data/egpu_dock_config/ first and +verifies every sector; identical bytes make it the lowest-risk possible +full-path test. Re-run step 1 after; product string and config sha must be +unchanged. + +## 3. Runtime + +Set `IQEgpuEnabled`, go onroad (bench is fine), and confirm iqegpumodeld +downloads/compiles and the selector reports UsbGpu* status. The runtime gate +requires the exact bundled firmware product string, so a dock that failed +step 2 will be treated as absent by design. + +## If anything goes wrong + +The dock falling back to the ROM bootloader (product "USB 3.2 PCIe +TinyEnclosure" or AS2462*) is recoverable: flash.py handles ROM recovery, and +the config backup from step 2 is on disk. Do not improvise register writes; +capture output and stop. diff --git a/iqpilot/system/hardware/egpu_dock/__init__.py b/iqpilot/system/hardware/egpu_dock/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/iqpilot/system/hardware/egpu_dock/dock_probe.py b/iqpilot/system/hardware/egpu_dock/dock_probe.py new file mode 100644 index 000000000..748e01e21 --- /dev/null +++ b/iqpilot/system/hardware/egpu_dock/dock_probe.py @@ -0,0 +1,46 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import hashlib +import sys + +from iqpilot.system.hardware.egpu_dock.flash import ( + Flash, RomFallback, bundled_version, find_dock, in_rom_bootloader, link_up, stable_read, +) + + +def main() -> int: + path, vid_pid, product = find_dock() + if path is None: + print("no eGPU dock enumerated") + return 1 + print(f"dock at {path}") + print(f" vid:pid {vid_pid[0]}:{vid_pid[1]}") + print(f" product {product!r}") + print(f" bundled {bundled_version()!r}") + print(f" match {product == bundled_version()}") + with open(path + "/speed") as fs: + speed = int(fs.read()) + print(f" usb speed {speed} Mbps ({'USB3' if speed >= 5000 else 'USB2 - register reads capped at 64B'})") + if in_rom_bootloader(vid_pid, product): + print(" state ROM bootloader (config page lost or firmware invalid)") + return 2 + print(f" pcie link {'L0 (trained)' if link_up() else 'not trained'}") + + flash = Flash() + try: + flash.connect() + config = stable_read(flash, 0, 0x100, 3) + print(f" config sha256={hashlib.sha256(config).hexdigest()[:16]} " + f"(stable over 3 reads, {sum(1 for b in config if b != 0xFF)} non-blank bytes)") + except (RomFallback, OSError, RuntimeError, TimeoutError) as e: + print(f" config read failed: {type(e).__name__}: {e}") + return 3 + finally: + flash.close() + print("all read-only checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/iqpilot/system/hardware/egpu_dock/firmware_wrapped.bin b/iqpilot/system/hardware/egpu_dock/firmware_wrapped.bin new file mode 100644 index 0000000000000000000000000000000000000000..1d738e4a759d05a8905e6ddf1b5d5ee6a324b04d GIT binary patch literal 9207 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 L{?0EJ@n8NA?^oD} literal 0 HcmV?d00001 diff --git a/iqpilot/system/hardware/egpu_dock/flash.py b/iqpilot/system/hardware/egpu_dock/flash.py new file mode 100644 index 000000000..263a42dd0 --- /dev/null +++ b/iqpilot/system/hardware/egpu_dock/flash.py @@ -0,0 +1,617 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import argparse +import ctypes +import errno +import fcntl +import glob +import hashlib +import os +import re +import signal +import struct +import sys +import time +import zlib +from pathlib import Path + +VID_PIDS = (("add1", "0001"), ("3801", "0001")) +ROM_VID_PIDS = (("174c", "2464"), ("174c", "2463")) +ROM_PRODUCT = "USB 3.2 PCIe TinyEnclosure" +FIRMWARE_PATH = Path(__file__).with_name("firmware_wrapped.bin") +CONFIG_DIR = "/data/egpu_dock_config" +LEGACY_CONFIG_DIR = "/data/chestnut_config" +PM_PATHS = ("/sys/bus/platform/devices/a800000.ssusb", "/sys/bus/platform/devices/a600000.ssusb", + "/sys/bus/usb/devices/usb4") +VBUS_PATH = "/sys/kernel/debug/regulator/smb2-vbus/enable" +IMAGE_OFFSET = 0x100 +SECTOR, PAGE = 4096, 128 +MAX_REGISTER_READ_SIZE = 255 +MAX_CODE_SIZE = 0x10000 +FLASH_BUDGET = 600.0 +USBDEVFS_CONTROL = 0xC0185500 +USBDEVFS_BULK = 0xC0185502 +USBDEVFS_SETINTERFACE = 0x80085504 +USBDEVFS_SETCONFIGURATION = 0x80045505 +USBDEVFS_CLAIMINTERFACE = 0x8004550F +USBDEVFS_RESET = 0x5514 +USBDEVFS_CLEAR_HALT = 0x80045515 + +_deadline = float("inf") + + +def check_budget(): + if time.monotonic() > _deadline: + raise TimeoutError(f"flash did not converge within {FLASH_BUDGET:g}s") + + +class Ctrl(ctypes.Structure): + _fields_ = [("request_type", ctypes.c_uint8), ("request", ctypes.c_uint8), + ("value", ctypes.c_uint16), ("index", ctypes.c_uint16), + ("length", ctypes.c_uint16), ("timeout", ctypes.c_uint32), + ("data", ctypes.c_void_p)] + + +class Bulk(ctypes.Structure): + _fields_ = [("ep", ctypes.c_uint), ("len", ctypes.c_uint), + ("timeout", ctypes.c_uint), ("data", ctypes.c_void_p)] + + +class RomFallback(Exception): + pass + + +def find_dock(): + found = [] + for d in glob.glob("/sys/bus/usb/devices/*"): + try: + with open(d + "/idVendor") as fv, open(d + "/idProduct") as fp: + vid_pid = (fv.read().strip(), fp.read().strip()) + if vid_pid in VID_PIDS + ROM_VID_PIDS: + with open(d + "/product") as fpr: + found.append((d, vid_pid, fpr.read().strip())) + except OSError: + pass + if len(found) > 1: + raise RuntimeError(f"expected one eGPU dock, found {len(found)}") + return found[0] if found else (None, None, None) + + +def in_rom_bootloader(vid_pid, product): + return vid_pid in ROM_VID_PIDS or product == ROM_PRODUCT or (product or "").startswith("AS2462") + + +def disable_runtime_pm(path): + control = os.path.join(path, "power/control") + if not os.path.exists(control): + return + with open(control, "w") as f: + f.write("on\n") + with open(control) as fh: + applied = fh.read().strip() + if applied != "on": + raise RuntimeError(f"could not disable USB runtime PM: {control}") + delay = os.path.join(path, "power/autosuspend_delay_ms") + if os.path.exists(delay): + with open(delay, "w") as f: + f.write("-1\n") + + +def unbind_drivers(path): + for interface in glob.glob(path + ":*"): + driver = interface + "/driver" + if os.path.islink(driver): + with open(os.path.realpath(driver) + "/unbind", "w") as f: + f.write(os.path.basename(interface)) + + +def open_device(path): + with open(path + "/busnum") as fb, open(path + "/devnum") as fd_: + bus, dev = int(fb.read()), int(fd_.read()) + return os.open(f"/dev/bus/usb/{bus:03d}/{dev:03d}", os.O_RDWR) + + +def link_up() -> bool: + try: + path, _, _ = find_dock() + if path is None: + return False + fd = open_device(path) + except (OSError, RuntimeError): + return False + try: + fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0x40, 0xF3, 1, 0, 0, 2000, None)) + buf = (ctypes.c_ubyte * 1)() + fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0xC0, 0xE4, 0xB450, 0, 1, 1000, ctypes.cast(buf, ctypes.c_void_p))) + return buf[0] == 0x78 + except OSError: + return False + finally: + os.close(fd) + + +def claim_interface(path, setup=False): + disable_runtime_pm(path) + unbind_drivers(path) + fd = open_device(path) + try: + if setup: + fcntl.ioctl(fd, USBDEVFS_SETCONFIGURATION, struct.pack("I", 1)) + fcntl.ioctl(fd, USBDEVFS_CLAIMINTERFACE, struct.pack("I", 0)) + if setup: + fcntl.ioctl(fd, USBDEVFS_SETINTERFACE, struct.pack("II", 0, 0)) + except OSError as e: + os.close(fd) + if e.errno == errno.EBUSY: + raise RuntimeError("eGPU dock is in use, stop the model/GPU processes before flashing") from e + raise + return fd + + +class Flash: + def __init__(self): + self.fd = -1 + self.max_register_read_size = MAX_REGISTER_READ_SIZE + + def close(self): + if self.fd >= 0: + os.close(self.fd) + self.fd = -1 + + def connect(self, timeout=5.0): + self.close() + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + path, vid_pid, product = find_dock() + if in_rom_bootloader(vid_pid, product): + raise RomFallback("eGPU dock fell back to the ROM bootloader") + if path is not None: + try: + with open(path + "/speed") as fs: + speed = int(fs.read()) + except (OSError, ValueError): + speed = 0 + self.max_register_read_size = 64 if speed < 5000 else MAX_REGISTER_READ_SIZE + self.fd = claim_interface(path) + return + time.sleep(0.1) + raise RuntimeError(f"eGPU dock did not enumerate within {timeout:g}s") + + def reg_write(self, addr, value): + fcntl.ioctl(self.fd, USBDEVFS_CONTROL, + Ctrl(0x40, 0xE5, addr & 0xFFFF, value & 0xFFFF, 0, 2000, None)) + + def reg_read(self, addr, length=1): + buf = (ctypes.c_ubyte * length)() + fcntl.ioctl(self.fd, USBDEVFS_CONTROL, + Ctrl(0xC0, 0xE4, addr & 0xFFFF, 0, length, 2000, ctypes.cast(buf, ctypes.c_void_p))) + return bytes(buf) + + def write_buffer(self, data): + for i, value in enumerate(data): + self.reg_write(0x7000 + i, value) + + def wait_controller(self, timeout=2.0): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if not self.reg_read(0xC8A9)[0] & 1: + return + raise TimeoutError("flash controller timeout") + + def transaction(self, command, addr=0, length=0, addr_len=0x07, mode=0): + for reg, value in ((0xC8AD, mode), (0xC8AE, 0), (0xC8AF, 0), (0xC8AA, command), (0xC8AC, addr_len), + (0xC8A1, addr), (0xC8A2, addr >> 8), (0xC8AB, addr >> 16), (0xC8A3, length >> 8), (0xC8A4, length)): + self.reg_write(reg, value & 0xFF) + self.reg_write(0xC8A9, 1) + self.wait_controller() + for _ in range(4): + self.reg_write(0xC8AD, 0) + + def write_enable(self): + for reg, value in ((0xC8AD, 0), (0xC8AA, 0x06), (0xC8AC, 0x04), (0xC8A3, 0), (0xC8A4, 0), (0xC8A9, 1)): + self.reg_write(reg, value) + self.wait_controller() + + def status(self): + self.transaction(0x05, length=1, addr_len=0x04) + return self.reg_read(0x7000)[0] + + def wait_write_done(self, timeout=10.0): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if not self.status() & 1: + return + time.sleep(0.005) + raise TimeoutError("SPI flash WIP timeout") + + def init(self): + self.reg_write(0xCC33, 0x04) + self.reg_write(0xCA81, self.reg_read(0xCA81)[0] | 1) + self.reg_write(0xC805, 0x02) + self.reg_write(0xC8A6, 0x04) + for _ in range(5): + self.write_enable() + self.write_buffer(bytes(4)) + self.transaction(0x01, length=1, addr_len=0x04, mode=1) + time.sleep(0.01) + if not self.status() & 0x1C: + return + raise RuntimeError("could not clear SPI block protection") + + def read(self, addr, length): + out = bytearray() + while len(out) < length: + n = min(4096, length - len(out)) + self.transaction(0x03, addr + len(out), max(4096, n)) + for off in range(0, n, self.max_register_read_size): + out += self.reg_read(0x7000 + off, min(self.max_register_read_size, n - off)) + return bytes(out) + + def erase_sector(self, addr): + self.write_enable() + self.transaction(0x20, addr) + self.wait_write_done() + + def program(self, addr, data): + self.write_buffer(data + bytes((-len(data)) % 4)) + self.write_enable() + self.transaction(0x02, addr, len(data), mode=1) + self.wait_write_done() + + +def validate_image(data): + if len(data) < 10: + raise ValueError("wrapped firmware is too short") + body_len = int.from_bytes(data[:4], "little") + if body_len > MAX_CODE_SIZE: + raise ValueError(f"wrapped firmware body exceeds {MAX_CODE_SIZE} bytes") + if len(data) != body_len + 10 or data[4 + body_len] != 0xA5: + raise ValueError("invalid wrapped firmware length or magic") + body = data[4:4 + body_len] + if data[5 + body_len] != sum(body) & 0xFF: + raise ValueError("invalid wrapped firmware checksum") + if data[6 + body_len:] != zlib.crc32(body).to_bytes(4, "little"): + raise ValueError("invalid wrapped firmware CRC") + + +def image_product(image): + match = re.search(rb"custom [0-9a-f]{8}-CLEAN", image) + if match is None: + raise ValueError("no product string in wrapped firmware") + return match.group().decode() + + +def reconnect(flash): + attempt = 0 + while True: + attempt += 1 + check_budget() + try: + flash.connect() + flash.init() + return + except (OSError, TimeoutError, RuntimeError) as e: + print(f"waiting for eGPU dock (attempt {attempt}): {e}", flush=True) + time.sleep(1) + + +def with_retries(flash, label, operation): + attempt = 0 + while True: + attempt += 1 + try: + return operation() + except (OSError, TimeoutError, RuntimeError) as e: + check_budget() + print(f"{label} attempt {attempt}: {e}", flush=True) + reconnect(flash) + + +def stable_read(flash, addr, length, count=2): + def read(): + reads = [flash.read(addr, length) for _ in range(count)] + if any(x != reads[0] for x in reads[1:]): + raise RuntimeError(f"unstable flash read at 0x{addr:05x}") + return reads[0] + return with_retries(flash, f"read 0x{addr:05x}", read) + + +def program_sector(flash, addr, target): + def program(): + flash.erase_sector(addr) + if flash.read(addr, SECTOR) != bytes([0xFF]) * SECTOR: + raise RuntimeError("sector erase verification failed") + for off in range(0, SECTOR, PAGE): + chunk = target[off:off + PAGE] + if chunk != bytes([0xFF]) * len(chunk): + flash.program(addr + off, chunk) + if flash.read(addr + off, len(chunk)) != chunk: + raise RuntimeError(f"page verify failed at 0x{addr + off:05x}") + if flash.read(addr, SECTOR) != target: + raise RuntimeError("sector verification failed") + with_retries(flash, f"sector 0x{addr:05x}", program) + + +def config_path(): + return os.path.join(CONFIG_DIR, f"{os.uname().nodename}.bin") + + +def legacy_config_path(): + return os.path.join(LEGACY_CONFIG_DIR, f"{os.uname().nodename}.bin") + + +def saved_config(path, data): + os.makedirs(os.path.dirname(path), exist_ok=True) + try: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError as e: + with open(path, "rb") as fh: + backup = fh.read() + if len(backup) != 0x100: + raise RuntimeError(f"invalid config backup: {path}") from e + if backup != data: + print(f"restoring config from {path}", flush=True) + return backup + with os.fdopen(fd, "wb") as f: + f.write(data) + f.flush() + os.fsync(f.fileno()) + return data + + +def rom_write(image, config): + path, _, _ = find_dock() + if path is None: + raise RuntimeError("eGPU dock disappeared before recovery") + unbind_drivers(path) + fd = open_device(path) + try: + fcntl.ioctl(fd, USBDEVFS_RESET) + finally: + os.close(fd) + time.sleep(3) + path, _, _ = find_dock() + if path is None: + raise RuntimeError("eGPU dock did not re-enumerate after reset") + fd = claim_interface(path, setup=True) + for ep in (0x02, 0x81): + fcntl.ioctl(fd, USBDEVFS_CLEAR_HALT, struct.pack("I", ep)) + tag = 0 + + def bulk(ep, payload, timeout): + buf = ctypes.create_string_buffer(bytes(payload), len(payload)) + fcntl.ioctl(fd, USBDEVFS_BULK, Bulk(ep, len(payload), timeout, ctypes.cast(buf, ctypes.c_void_p))) + return buf.raw + + def cmd(cdb, data=b"", timeout=30000): + nonlocal tag + tag += 1 + bulk(0x02, struct.pack("BBB12x", 0xE1, 0x50, 0), config[:0x80]) + cmd(struct.pack(">BBB12x", 0xE1, 0x50, 1), config[0x80:]) + cmd(struct.pack(">BBI", 0xE3, 0x50, min(len(image), 0xFF00)), image[:0xFF00]) + if len(image) > 0xFF00: + cmd(struct.pack(">BBI", 0xE3, 0xD0, len(image) - 0xFF00), image[0xFF00:]) + cmd(struct.pack(">BB13x", 0xE8, 0x51)) + finally: + os.close(fd) + print("recovery flash done", flush=True) + + +def vbus_write(value): + try: + with open(VBUS_PATH, "w") as f: + f.write(value + "\n") + except OSError: + pass + + +def vbus_cycle(): + if os.path.exists(VBUS_PATH): + vbus_write("0") + time.sleep(2) + vbus_write("1") + time.sleep(5) + + +def activate(expected_product): + if not os.path.exists(VBUS_PATH): + print("no VBUS control, firmware activates on the next dock power cycle", flush=True) + return + print("power-cycling the eGPU dock VBUS", flush=True) + vbus_write("0") + disconnected = False + deadline = time.monotonic() + 5.0 + while time.monotonic() < deadline: + path, _, _ = find_dock() + if path is None: + disconnected = True + break + time.sleep(0.2) + time.sleep(1) + vbus_write("1") + if not disconnected: + print("dock stayed powered, firmware activates on its next power cycle", flush=True) + return + deadline = time.monotonic() + 15.0 + while time.monotonic() < deadline: + _, _, product = find_dock() + if product is not None: + if product == expected_product: + print(f"activated {expected_product}", flush=True) + else: + print(f"dock re-enumerated with {product!r}, firmware activates on its next power cycle", flush=True) + return + time.sleep(0.2) + print("dock did not re-enumerate, firmware activates on its next power cycle", flush=True) + + +def defer_signal(signum, _frame): + os.write(1, f"signal {signum} deferred until the dock is powered back up\n".encode()) + + +def flash_dock(expected_version=None, force=False): + global _deadline + + image = FIRMWARE_PATH.read_bytes() + validate_image(image) + expected_product = image_product(image) + if expected_version is not None and expected_product != f"custom {expected_version}-CLEAN": + raise RuntimeError(f"bundled firmware is {expected_product!r}, expected version {expected_version}") + + path, vid_pid, product = find_dock() + if path is None: + print("no eGPU dock connected", flush=True) + return + if product == expected_product and not force: + print(f"eGPU dock firmware is up to date ({expected_product})", flush=True) + return + + _deadline = time.monotonic() + FLASH_BUDGET + for pm_path in PM_PATHS: + disable_runtime_pm(pm_path) + + previous = {sig: signal.signal(sig, defer_signal) for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP)} + try: + if in_rom_bootloader(vid_pid, product): + if not recover_from_rom(image, expected_product): + return + force, product = True, None + write_image(image, expected_product, product, force) + finally: + for sig, handler in previous.items(): + signal.signal(sig, handler) + + +def recover_from_rom(image, expected_product): + backup = config_path() + if not os.path.isfile(backup) and os.path.isfile(legacy_config_path()): + backup = legacy_config_path() + if not os.path.isfile(backup): + raise RuntimeError(f"cannot recover from the ROM bootloader without a config backup at {config_path()}") + with open(backup, "rb") as fh: + config = fh.read() + if len(config) != 0x100: + raise RuntimeError(f"invalid config backup: {backup}") + + committed = False + while True: + check_budget() + path, vid_pid, product = find_dock() + if path is None: + if committed: + print("dock is offline, recovered firmware boots on its next power cycle", flush=True) + return False + vbus_cycle() + continue + if not in_rom_bootloader(vid_pid, product): + return True + if committed: + print("dock stayed powered, recovered firmware boots on its next power cycle", flush=True) + return False + try: + rom_write(image, config) + committed = True + except (OSError, TimeoutError, RuntimeError) as e: + print(f"ROM recovery failed, retrying: {e}", flush=True) + vbus_cycle() + continue + activate(expected_product) + + +def write_image(image, expected_product, product, force): + if force: + print(f"forced reflash of {expected_product}", flush=True) + else: + print(f"eGPU dock firmware mismatch: {product!r}; expected {expected_product!r}", flush=True) + + flash = Flash() + try: + reconnect(flash) + config = stable_read(flash, 0, 0x100, 3) + config = saved_config(config_path(), config) + image_end = IMAGE_OFFSET + len(image) + first_sector = IMAGE_OFFSET & ~(SECTOR - 1) + span = (image_end + SECTOR - 1) & ~(SECTOR - 1) + current = stable_read(flash, first_sector, span - first_sector) + target = bytearray(current) + target[:len(config)] = config + target[IMAGE_OFFSET - first_sector:image_end - first_sector] = image + target = bytes(target) + print(f"target {len(image)} bytes at 0x{IMAGE_OFFSET:05x}, sha256={hashlib.sha256(image).hexdigest()}", flush=True) + + if not _still_offroad(): + raise RuntimeError("device went onroad before any sector was written; aborting flash") + + for addr in range(first_sector, span, SECTOR): + off = addr - first_sector + wanted = target[off:off + SECTOR] + if current[off:off + SECTOR] == wanted: + print(f"sector 0x{addr:05x}: unchanged", flush=True) + else: + print(f"sector 0x{addr:05x}: programming", flush=True) + program_sector(flash, addr, wanted) + + verified = stable_read(flash, first_sector, span - first_sector, 3) + if verified != target: + raise RuntimeError("final full-image verification failed") + print(f"verified sha256={hashlib.sha256(verified).hexdigest()}", flush=True) + finally: + flash.close() + + activate(expected_product) + + +def bundled_version() -> str: + return image_product(FIRMWARE_PATH.read_bytes()) + + +def _still_offroad() -> bool: + try: + from iqpilot.common.params import Params + return bool(Params().get_bool("IsOffroad")) + except Exception: + return True + + +def dock_needs_flash(usb_devices: list[dict]) -> bool: + try: + expected = bundled_version() + except (OSError, ValueError): + return False + ids = tuple(tuple(int(x, 16) for x in p) for p in VID_PIDS + ROM_VID_PIDS) + return any((d.get("vendorId"), d.get("productId")) in ids and d.get("product") != expected + for d in usb_devices) + + +def main(): + parser = argparse.ArgumentParser(description="check and flash the bundled eGPU dock firmware") + parser.add_argument("version", nargs="?", help="expected firmware version hash") + parser.add_argument("--force", action="store_true", help="reflash even when the version matches") + args = parser.parse_args() + if os.geteuid() != 0: + raise RuntimeError("flash.py must run as root") + flash_dock(expected_version=args.version, force=args.force) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"FAIL: {type(e).__name__}: {e}", file=sys.stderr) + sys.exit(1) diff --git a/iqpilot/system/hardware/egpu_dock/status.py b/iqpilot/system/hardware/egpu_dock/status.py new file mode 100644 index 000000000..97077776f --- /dev/null +++ b/iqpilot/system/hardware/egpu_dock/status.py @@ -0,0 +1,89 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import time + +from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT, is_egpu_usb_device + +EGPU_POWERED_VOLTAGE = 5000 +GPU_TEMP_LIMIT = 110. +MEMORY_TEMP_LIMIT = 108. +TEMP_HYSTERESIS = 5. +FAN_START_GPU_TEMP = 60. +FAN_STOP_GPU_TEMP = 50. +FAN_START_MEMORY_TEMP = 70. +FAN_STOP_MEMORY_TEMP = 60. +FAN_STALLED_RPM = 250 + + +class EgpuDockStatus: + def __init__(self): + self.offroad = True + self.pcie_failed = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.overheated = False + self.fans_obstructed = False + self.usb_seen = False + self.usb_failed = False + + def update(self, offroad, usb_state, firmware_failed, model_loading, model_active, compiled, state, set_alert): + detected = [d for d in usb_state if is_egpu_usb_device(d["vendorId"], d["productId"], include_bootloader=True)] + devices = [d for d in detected if is_egpu_usb_device(d["vendorId"], d["productId"])] + firmware_ok = len(devices) == 1 and devices[0]["product"] == EGPU_DOCK_FW_PRODUCT + + if self.offroad and not offroad: + self.pcie_failed = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.usb_seen = firmware_ok + self.usb_failed = False + + self.model_loading_seen |= model_loading + self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + + if not offroad and self.usb_seen and not firmware_ok: + self.usb_failed = True + + if not offroad and self.model_attempted and state is not None: + power_lost = state.supplyFault or state.supplyVoltage < EGPU_POWERED_VOLTAGE + self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0 + self.pcie_failed |= self.link_failures >= 2 or power_lost + self.power_lost |= power_lost + + if self.pcie_failed and self.power_lost and state is not None: + self.power_restored |= not state.supplyFault and state.supplyVoltage >= EGPU_POWERED_VOLTAGE + if self.usb_failed: + self.pcie_failed = False + self.power_lost = False + self.power_restored = False + + if state is not None: + gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit + fan_hot = (state.tempC >= (FAN_STOP_GPU_TEMP if self.fans_obstructed else FAN_START_GPU_TEMP) or + state.memoryTempC >= (FAN_STOP_MEMORY_TEMP if self.fans_obstructed else FAN_START_MEMORY_TEMP)) + self.fans_obstructed = fan_hot and state.fanSpeedRpm < FAN_STALLED_RPM + + slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 + set_alert("Offroad_EgpuNotDetected", self.usb_failed) + set_alert("Offroad_EgpuFansObstructed", self.fans_obstructed) + set_alert("Offroad_EgpuOverheated", self.overheated) + set_alert("Offroad_EgpuUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) + if self.power_lost: + pcie_action = "12V power was interrupted, possibly by engine start-stop. " + pcie_action += ("Cycle ignition to reload the model." if self.power_restored else + "Check 12V, then cycle ignition to reload the model.") + else: + pcie_action = "Check 12V connection." + set_alert("Offroad_EgpuPcieUnavailable", self.pcie_failed, pcie_action) + set_alert("Offroad_EgpuUncompiled", offroad and firmware_ok and not compiled) + set_alert("Offroad_EgpuUpdateFailed", offroad and firmware_failed) + self.offroad = offroad diff --git a/iqpilot/system/hardware/fan_controller.py b/iqpilot/system/hardware/fan_controller.py index e4560bfb2..cc66c83e5 100755 --- a/iqpilot/system/hardware/fan_controller.py +++ b/iqpilot/system/hardware/fan_controller.py @@ -3,24 +3,13 @@ import numpy as np class FanController: - def __init__(self) -> None: - self.last_ignition = False - def update(self, cur_temp: float, ignition: bool, max_cool: bool = False) -> int: if max_cool: - self.last_ignition = ignition return 100 - if cur_temp < 70.0: - fan_pwr_out = 0 - elif cur_temp > 85.0: - fan_pwr_out = 100 - else: - # 70°C → 0%, 85°C → 80%, target 75°C - fan_pwr_out = int(np.interp(cur_temp, [70.0, 85.0], [0, 80])) + fan_pwr_out = int(np.interp(cur_temp, [70.0, 85.0, 90.0], [0, 80, 100])) if not ignition: fan_pwr_out = min(fan_pwr_out, 30) - self.last_ignition = ignition return fan_pwr_out diff --git a/iqpilot/system/hardware/hardwared.py b/iqpilot/system/hardware/hardwared.py index d57db2f81..c918d704b 100755 --- a/iqpilot/system/hardware/hardwared.py +++ b/iqpilot/system/hardware/hardwared.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import fcntl import os +import subprocess +import sys import queue import struct import threading @@ -16,10 +18,14 @@ from iqpilot.cereal.services import SERVICE_LIST from iqpilot.common.iq_perf import PerfSample, PerfTraceEmitter from iqpilot.common.utils import strip_deprecated_keys from iqpilot.common.filter_simple import FirstOrderFilter +from iqpilot.common.basedir import BASEDIR from iqpilot.common.params import Params from iqpilot.common.realtime import DT_HW from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from iqpilot.system.hardware import HARDWARE, TICI, AGNOS +from iqpilot.system.hardware.egpu_dock.flash import dock_needs_flash +from iqpilot.system.hardware.egpu_dock.status import EgpuDockStatus +from iqpilot.system.hardware.usb import get_link_error_count, get_usb_state, set_usb_state, usb3_lane from iqpilot.system.loggerd.config import get_available_percent from iqpilot.common.swaglog import cloudlog from iqpilot.system.hardware.power_monitoring import PowerMonitoring, VBATT_LOW_POWER_EXIT @@ -41,7 +47,8 @@ CAN_STARTUP_RECOVERY_MAX_ATTEMPTS = 2 ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', - 'network_metered', 'modem_temps']) + 'network_metered', 'modem_temps', 'usb_state', 'usb_link_errors', + 'usb3_lane']) # List of thermal bands. We will stay within this region as long as we are within the bounds. # When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict. @@ -161,6 +168,56 @@ class _CarParamsCache: +class EgpuDockFlasher: + """Flash the dock's firmware offroad when it does not match what we ship. + + Same policy as stock: the model runtime ignores a dock until its product + string matches, so a mismatched dock is unusable until this runs. Bounded + attempts, offroad only, one flash in flight at a time. + """ + MAX_ATTEMPTS = 3 + RETRY_INTERVAL = 20. + + def __init__(self): + self.thread: threading.Thread | None = None + self.attempts = 0 + self.last_attempt = 0. + self.flashed = False + self.mismatch = False + + @property + def failed(self) -> bool: + return (self.mismatch and self.attempts >= self.MAX_ATTEMPTS + and self.thread is not None and not self.thread.is_alive() and not self.flashed) + + def flash(self) -> None: + ret = subprocess.run(["sudo", sys.executable, + os.path.join(BASEDIR, "iqpilot/system/hardware/egpu_dock/flash.py")], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False) + cloudlog.event("egpu dock flash done", returncode=ret.returncode, output=ret.stdout[-1000:], + error=ret.returncode != 0) + self.flashed = ret.returncode == 0 + + def update(self, offroad: bool, usb_state: list[dict]) -> None: + self.mismatch = dock_needs_flash(usb_state) + if not self.mismatch: + self.flashed = False + return + + if not offroad or self.flashed or self.attempts >= self.MAX_ATTEMPTS: + return + if self.thread is not None and self.thread.is_alive(): + return + if time.monotonic() - self.last_attempt < self.RETRY_INTERVAL: + return + + self.attempts += 1 + self.last_attempt = time.monotonic() + cloudlog.warning(f"egpu dock firmware out of date, flashing (attempt {self.attempts})") + self.thread = threading.Thread(target=self.flash, daemon=True) + self.thread.start() + + def set_offroad_alert_if_changed(offroad_alert: str, show_alert: bool, extra_text: str | None=None): if prev_offroad_states.get(offroad_alert, None) == (show_alert, extra_text): return @@ -254,6 +311,9 @@ def hw_state_thread(end_event, hw_queue): network_stats={'wwanTx': tx, 'wwanRx': rx}, network_metered=HARDWARE.get_network_metered(network_type), modem_temps=modem_temps, + usb_state=get_usb_state(), + usb_link_errors=get_link_error_count(), + usb3_lane=usb3_lane(), ) try: @@ -280,7 +340,7 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: pm = messaging.PubMaster(['deviceState', 'iqPerfTrace']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "carState"], poll="pandaStates") + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "carState", "egpuDockState"], poll="pandaStates") perf = PerfTraceEmitter("hardwared", pubmaster=pm) count = 0 @@ -306,6 +366,9 @@ def hardware_thread(end_event, hw_queue) -> None: network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, modem_temps=[], + usb_state=[], + usb_link_errors=0, + usb3_lane="unknown", ) all_temp_filter = FirstOrderFilter(0., TEMP_TAU, DT_HW, initialized=False) @@ -332,6 +395,8 @@ def hardware_thread(end_event, hw_queue) -> None: thermal_config = HARDWARE.get_thermal_config() fan_controller = FanController() + egpu_dock_flasher = EgpuDockFlasher() + egpu_dock_status = EgpuDockStatus() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) @@ -427,6 +492,14 @@ def hardware_thread(end_event, hw_queue) -> None: msg.deviceState.networkInfo = last_hw_state.network_info msg.deviceState.modemTempC = last_hw_state.modem_temps + set_usb_state(msg.deviceState, last_hw_state.usb_state, last_hw_state.usb_link_errors, + last_hw_state.usb3_lane) + egpu_dock_flasher.update(started_ts is None, last_hw_state.usb_state) + egpu_valid = sm.alive["egpuDockState"] and sm.valid["egpuDockState"] + egpu_dock_status.update(started_ts is None, last_hw_state.usb_state, egpu_dock_flasher.failed, + params.get_bool("UsbGpuLoading"), params.get("UsbGpuActive"), + params.get_bool("UsbGpuCompiled"), + sm["egpuDockState"] if egpu_valid else None, set_offroad_alert_if_changed) msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() diff --git a/iqpilot/system/hardware/tests/test_egpu_dock_flash.py b/iqpilot/system/hardware/tests/test_egpu_dock_flash.py new file mode 100644 index 000000000..694fdd8b4 --- /dev/null +++ b/iqpilot/system/hardware/tests/test_egpu_dock_flash.py @@ -0,0 +1,132 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +The eGPU dock flasher writes SPI flash, so the parts that decide WHETHER and +WHAT to write are pinned here. The transfer path itself needs the hardware; the +image validator, product parsing, config preservation and the needs-flash +decision do not, and those are what stop a bad write. +""" +import os +import zlib + +import pytest + +from iqpilot.system.hardware.egpu_dock import flash as f + + +def _wrap(body: bytes, *, magic=0xA5, checksum=None, crc=None, body_len=None) -> bytes: + n = len(body) if body_len is None else body_len + cs = (sum(body) & 0xFF) if checksum is None else checksum + c = zlib.crc32(body).to_bytes(4, "little") if crc is None else crc + return n.to_bytes(4, "little") + body + bytes([magic, cs]) + c + + +def test_bundled_firmware_is_valid_and_named(): + image = f.FIRMWARE_PATH.read_bytes() + f.validate_image(image) # raises if the shipped blob is corrupt + assert f.image_product(image).startswith("custom ") + assert f.image_product(image).endswith("-CLEAN") + assert f.bundled_version() == f.image_product(image) + + +def test_validate_rejects_corruption(): + body = b"custom deadbeef-CLEAN" + bytes(64) + f.validate_image(_wrap(body)) # the good case + with pytest.raises(ValueError): + f.validate_image(b"\x00" * 4) # too short + with pytest.raises(ValueError): + f.validate_image(_wrap(body, magic=0x00)) # bad magic + with pytest.raises(ValueError): + f.validate_image(_wrap(body, checksum=0x00)) + with pytest.raises(ValueError): + f.validate_image(_wrap(body, crc=b"\x00\x00\x00\x00")) + with pytest.raises(ValueError): + f.validate_image(_wrap(body, body_len=len(body) + 1)) # length disagrees + with pytest.raises(ValueError): + f.validate_image((f.MAX_CODE_SIZE + 1).to_bytes(4, "little") + bytes(16)) + + +def test_image_product_requires_a_version_string(): + with pytest.raises(ValueError): + f.image_product(b"no version here") + + +def test_saved_config_preserves_the_first_backup(tmp_path): + # the config page is per-unit; a reflash must rewrite the ORIGINAL, never the + # bytes read back from a half-written dock + p = str(tmp_path / "dock.bin") + original = bytes(range(256)) + assert f.saved_config(p, original) == original + # later flash reads something different -> the stored original wins + assert f.saved_config(p, bytes(256)) == original + with open(p, "rb") as fh: + assert fh.read() == original + + +def test_saved_config_rejects_wrong_size_backup(tmp_path): + p = str(tmp_path / "dock.bin") + with open(p, "wb") as fh: + fh.write(b"\x00" * 8) + with pytest.raises(RuntimeError): + f.saved_config(p, bytes(256)) + + +def test_needs_flash_only_for_a_dock_on_wrong_firmware(): + expected = f.bundled_version() + vid, pid = (int(x, 16) for x in f.VID_PIDS[0]) + rom_vid, rom_pid = (int(x, 16) for x in f.ROM_VID_PIDS[0]) + + assert not f.dock_needs_flash([]) + assert not f.dock_needs_flash([{"vendorId": 0x1234, "productId": 0x5678, "product": "something else"}]) + assert not f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": expected}]) + assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}]) + # a ROM-mode board always needs flashing + assert f.dock_needs_flash([{"vendorId": rom_vid, "productId": rom_pid, "product": f.ROM_PRODUCT}]) + + +def test_both_shipped_ids_trigger_the_check(): + for pair in f.VID_PIDS: + vid, pid = (int(x, 16) for x in pair) + assert f.dock_needs_flash([{"vendorId": vid, "productId": pid, "product": "custom 00000000-CLEAN"}]) + + +def test_rom_detection(): + assert f.in_rom_bootloader(f.ROM_VID_PIDS[0], "anything") + assert f.in_rom_bootloader(("add1", "0001"), f.ROM_PRODUCT) + assert f.in_rom_bootloader(("add1", "0001"), "AS2462something") + assert not f.in_rom_bootloader(("add1", "0001"), f.bundled_version()) + assert not f.in_rom_bootloader(("add1", "0001"), None) + + +def test_config_paths_are_per_host_and_have_a_legacy_fallback(): + host = os.uname().nodename + assert f.config_path().endswith(f"{host}.bin") + assert f.config_path().startswith(f.CONFIG_DIR) + # a dock flashed on this device by stock openpilot left its backup elsewhere + assert f.legacy_config_path().startswith(f.LEGACY_CONFIG_DIR) + assert f.legacy_config_path() != f.config_path() + + +def test_we_do_not_autoflash(): + # upstream flashes from hardwared automatically; ours must stay deliberate + # until it has been validated against a real dock + import subprocess + root = os.path.join(os.path.dirname(f.__file__), "..", "..", "..") + hits = subprocess.run(["grep", "-rnI", "--exclude-dir=__pycache__", "flash_dock", os.path.join(root, "system"), + os.path.join(root, "iqpilot")], capture_output=True, text=True).stdout + callers = [ln for ln in hits.splitlines() if "egpu_dock/flash.py" not in ln and "test_" not in ln] + assert callers == [], f"unexpected automatic flash caller: {callers}" + + +def test_runtime_fw_gate_is_pinned_to_the_bundled_firmware(): + from iqpilot.system.hardware.usb import EGPU_DOCK_FW_PRODUCT + assert EGPU_DOCK_FW_PRODUCT == f.bundled_version() + + +def test_register_reads_default_to_superspeed_size(): + assert f.MAX_REGISTER_READ_SIZE == 255 + assert f.Flash().max_register_read_size == f.MAX_REGISTER_READ_SIZE + + +def test_link_up_is_false_without_a_dock(): + assert f.link_up() is False diff --git a/iqpilot/system/hardware/tests/test_fan_controller.py b/iqpilot/system/hardware/tests/test_fan_controller.py index 8e0492790..3284f5625 100644 --- a/iqpilot/system/hardware/tests/test_fan_controller.py +++ b/iqpilot/system/hardware/tests/test_fan_controller.py @@ -1,57 +1,41 @@ -import pytest +import numpy as np from iqpilot.system.hardware.fan_controller import FanController -ALL_CONTROLLERS = [FanController] - -def patched_controller(mocker, controller_class): - mocker.patch("os.system", new=mocker.Mock()) - return controller_class() class TestFanController: - def wind_up(self, controller, ignition=True): - for _ in range(1000): - controller.update(100, ignition) + def test_ramp_anchors(self): + c = FanController() + assert c.update(60, True) == 0 + assert c.update(70, True) == 0 + assert c.update(85, True) == 80 + assert c.update(90, True) == 100 + assert c.update(100, True) == 100 - def wind_down(self, controller, ignition=False): - for _ in range(1000): - controller.update(10, ignition) + def test_ramp_is_monotonic_and_continuous(self): + c = FanController() + temps = np.arange(50.0, 105.0, 0.25) + outs = [c.update(t, True) for t in temps] + assert all(b >= a for a, b in zip(outs, outs[1:])) + # no step may exceed the steepest segment's slope (4 %/deg) over a 0.25 deg move + assert max(b - a for a, b in zip(outs, outs[1:])) <= 2 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_hot_onroad(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_up(controller) - assert controller.update(100, True) >= 70 + def test_hot_onroad(self): + assert FanController().update(100, True) >= 70 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_offroad_limits(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_up(controller) - assert controller.update(100, False) <= 30 + def test_offroad_capped(self): + c = FanController() + for t in (60, 75, 85, 100): + assert c.update(t, False) <= 30 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_no_fan_wear(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_down(controller) - assert controller.update(10, False) == 0 + def test_no_fan_wear(self): + assert FanController().update(10, False) == 0 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_limited(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_up(controller, True) - assert controller.update(100, True) == 100 + def test_max_cool(self): + c = FanController() + assert c.update(80, True, True) == 100 + assert c.update(80, False, True) == 100 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_max_cool(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_down(controller) - assert controller.update(80, True, True) == 100 - assert controller.update(80, False, True) == 100 - - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) - def test_windup_speed(self, mocker, controller_class): - controller = patched_controller(mocker, controller_class) - self.wind_down(controller, True) - for _ in range(10): - controller.update(90, True) - assert controller.update(90, True) >= 60 + def test_target_band_has_airflow(self): + # the design centers on 75 C; the curve must actually move air there + assert 20 <= FanController().update(75, True) <= 40 diff --git a/iqpilot/system/hardware/tests/test_usb_state.py b/iqpilot/system/hardware/tests/test_usb_state.py new file mode 100644 index 000000000..ab4e6c148 --- /dev/null +++ b/iqpilot/system/hardware/tests/test_usb_state.py @@ -0,0 +1,267 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +deviceState.usbState carries a per-device USB bus snapshot plus the ssusb +link-error counter (IQ.OS 4.9.1+ `portli`) into every rlog. Drive it off a +synthetic sysfs tree so parsing, eGPU dock presence and the link-error +plumbing are pinned without hardware. +""" +from iqpilot.cereal import messaging +from iqpilot.system.hardware.usb import ( + EGPU_DOCK_FW_PRODUCT, EGPU_DOCK_ROM_USB_IDS, EGPU_DOCK_USB_IDS, controller, egpu_dock_present, + egpu_dock_ready, get_link_error_count, + get_usb_topology, get_usb_state, link_controller, read_hex_counter, set_usb_state, usb3_lane, +) + + +def _mkctrl(root, name="a800000.ssusb", portli="0x00000000"): + """Platform controller dir, mirroring /sys/devices/platform/soc/.ssusb.""" + ctrl = root / "soc" / name + ctrl.mkdir(parents=True) + if portli is not None: + (ctrl / "portli").write_text(portli + "\n") + return ctrl + + +def _mkdev(root, name, *, vid, pid, busnum=1, devnum=2, speed=5000, + manufacturer="ACME", product="Widget", ctrl=None): + """USB device under the controller, symlinked into the bus view like sysfs.""" + real = (ctrl / "usb1" / name) if ctrl is not None else (root / "bus" / name) + real.mkdir(parents=True) + (real / "idVendor").write_text(f"{vid:04x}\n") + (real / "idProduct").write_text(f"{pid:04x}\n") + (real / "busnum").write_text(f"{busnum}\n") + (real / "devnum").write_text(f"{devnum}\n") + (real / "speed").write_text(f"{speed}\n") + (real / "manufacturer").write_text(manufacturer + "\n") + (real / "product").write_text(product + "\n") + + bus = root / "bus" + bus.mkdir(parents=True, exist_ok=True) + link = bus / name + if real != link: + link.symlink_to(real) + return real + + +def test_missing_sysfs_is_empty(tmp_path): + assert get_usb_state(tmp_path / "nope") == [] + + +def test_entries_without_idvendor_are_skipped(tmp_path): + (tmp_path / "bus" / "usb1").mkdir(parents=True) # a root hub dir with no idVendor + _mkdev(tmp_path, "1-2", vid=0x1234, pid=0x5678) + state = get_usb_state(tmp_path / "bus") + assert len(state) == 1 and state[0]["vendorId"] == 0x1234 + + +def test_fields_parsed_with_hex_ids(tmp_path): + _mkdev(tmp_path, "1-2", vid=0x0BDA, pid=0x8153, busnum=3, devnum=7, + speed=480, manufacturer="Realtek", product="USB 10/100 LAN") + (dev,) = get_usb_state(tmp_path / "bus") + assert dev == { + "busnum": 3, "devnum": 7, + "vendorId": 0x0BDA, "productId": 0x8153, + "speedMbps": 480, + "manufacturer": "Realtek", "product": "USB 10/100 LAN", + "linkErrorCount": 0, # no controller in this device's path + "usb3Lane": "unknown", # not on the type-C port's controller + } + + +def test_unreadable_strings_default_empty(tmp_path): + real = _mkdev(tmp_path, "1-2", vid=0x1, pid=0x2) + (real / "manufacturer").unlink() + (real / "product").unlink() + (dev,) = get_usb_state(tmp_path / "bus") + assert dev["manufacturer"] == "" and dev["product"] == "" + + +def test_hex_counter_parsing(tmp_path): + f = tmp_path / "portli" + f.write_text("0x00000000\n") + assert read_hex_counter(f) == 0 + f.write_text("0x0000002a\n") + assert read_hex_counter(f) == 42 + f.write_text("0000002a\n") # bare hex, no 0x prefix + assert read_hex_counter(f) == 42 + f.write_text("garbage\n") + assert read_hex_counter(f) == 0 + assert read_hex_counter(tmp_path / "absent") == 0 # pre-4.9.1 IQ.OS + + +def test_controller_resolved_from_device(tmp_path): + ctrl = _mkctrl(tmp_path) + _mkdev(tmp_path, "1-2", vid=0x1, pid=0x2, ctrl=ctrl) + assert controller(tmp_path / "bus" / "1-2") == ctrl.resolve() + + +def test_device_carries_its_controllers_link_errors(tmp_path): + ctrl = _mkctrl(tmp_path, portli="0x0000000c") + _mkdev(tmp_path, "1-2", vid=0x1234, pid=0x5678, ctrl=ctrl) + (dev,) = get_usb_state(tmp_path / "bus") + assert dev["linkErrorCount"] == 12 + + +def test_controller_count_without_any_enumerated_device(tmp_path): + # peripheral mode (eMac gadget link): the peer never enumerates on our side, + # so the counter must still be readable off the controller + _mkctrl(tmp_path, portli="0x00000005") + assert get_usb_state(tmp_path / "bus") == [] + assert get_link_error_count(tmp_path / "soc") == 5 + + +def test_link_errors_summed_across_controllers(tmp_path): + _mkctrl(tmp_path, name="a800000.ssusb", portli="0x00000002") + _mkctrl(tmp_path, name="a600000.ssusb", portli="0x00000003") + assert get_link_error_count(tmp_path / "soc") == 5 + + +def test_missing_portli_reads_zero(tmp_path): + _mkctrl(tmp_path, portli=None) # pre-4.9.1 kernel: file absent + assert get_link_error_count(tmp_path / "soc") == 0 + + +def test_set_usb_state_populates_message_and_flags_dock(tmp_path): + ctrl = _mkctrl(tmp_path, portli="0x00000007") + _mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678, speed=480, ctrl=ctrl) + _mkdev(tmp_path, "1-2", vid=EGPU_DOCK_USB_IDS[0][0], pid=EGPU_DOCK_USB_IDS[0][1], speed=5000, ctrl=ctrl) + + msg = messaging.new_message('deviceState') + set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus"), get_link_error_count(tmp_path / "soc")) + + devices = list(msg.deviceState.usbState.devices) + assert len(devices) == 2 + assert {d.speedMbps for d in devices} == {480, 5000} + assert all(d.linkErrorCount == 7 for d in devices) + assert msg.deviceState.usbState.linkErrorCount == 7 + assert msg.deviceState.egpuDockPresent + + +def test_dock_absent_when_not_plugged(tmp_path): + _mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678) + msg = messaging.new_message('deviceState') + set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus")) + assert not msg.deviceState.egpuDockPresent + + +def test_both_shipped_dock_usb_ids_detected(tmp_path): + # comma ships the dock under two VID/PIDs; only the first was known before + for i, (vid, pid) in enumerate(EGPU_DOCK_USB_IDS): + root = tmp_path / f"v{i}" + _mkdev(root, "1-1", vid=vid, pid=pid) + assert egpu_dock_present(root / "bus"), f"{vid:#06x}:{pid:#06x} not detected" + + +def test_dock_in_rom_mode_is_not_present(tmp_path): + # bootloader/ROM state enumerates but cannot serve a GPU until flashed + vid, pid = EGPU_DOCK_ROM_USB_IDS[0] + _mkdev(tmp_path, "1-1", vid=vid, pid=pid) + assert not egpu_dock_present(tmp_path / "bus") + + +def test_empty_bus_clears_flag(): + msg = messaging.new_message('deviceState') + set_usb_state(msg.deviceState, []) + assert len(msg.deviceState.usbState.devices) == 0 + assert msg.deviceState.usbState.linkErrorCount == 0 + assert not msg.deviceState.egpuDockPresent + + +def test_link_error_count_masked_to_16_bits(tmp_path): + # the per-device field is UInt16 upstream; a wrapped counter must not overflow it + ctrl = _mkctrl(tmp_path, portli="0x0001ffff") + _mkdev(tmp_path, "1-2", vid=0x1, pid=0x2, ctrl=ctrl) + (dev,) = get_usb_state(tmp_path / "bus") + assert dev["linkErrorCount"] == 0xFFFF + + msg = messaging.new_message('deviceState') + set_usb_state(msg.deviceState, get_usb_state(tmp_path / "bus")) + assert list(msg.deviceState.usbState.devices)[0].linkErrorCount == 0xFFFF + + +def test_usb_topology_lists_bus_entries(tmp_path): + _mkdev(tmp_path, "1-1", vid=0x1, pid=0x2) + _mkdev(tmp_path, "1-2", vid=0x3, pid=0x4) + assert {"1-1", "1-2"} <= get_usb_topology(tmp_path / "bus") + assert get_usb_topology(tmp_path / "nope") == set() + + +def _mkudc(root, name="a600000.dwc3"): + udc = root / "udc" / name + udc.mkdir(parents=True, exist_ok=True) + (udc / "state").write_text("not attached\n") + return root / "udc" + + +def test_link_controller_derived_from_udc_not_hardcoded(tmp_path): + # comma pins "a600000.ssusb"; we derive it, so another board still resolves + assert link_controller(_mkudc(tmp_path)) == "a600000.ssusb" + assert link_controller(_mkudc(tmp_path / "other", "a800000.dwc3")) == "a800000.ssusb" + + +def test_link_controller_absent_udc_is_empty(tmp_path): + assert link_controller(tmp_path / "nope") == "" + + +def test_usb3_lane_mapping(): + assert usb3_lane(1) == "a" + assert usb3_lane(2) == "b" + assert usb3_lane(0) == "unknown" # unattached + assert usb3_lane(None if False else 7) == "unknown" + + +def test_port_lane_survives_gadget_mode(tmp_path): + """The case upstream cannot report: in peripheral mode nothing enumerates on + the link controller, so every Device row is 'unknown' while the eMac link is + up. The port-level field still carries it.""" + ctrl = _mkctrl(tmp_path, name="a800000.ssusb") + _mkdev(tmp_path, "1-1", vid=0x1234, pid=0x5678, ctrl=ctrl) # panda, host controller + _mkudc(tmp_path) # gadget is a600000 + + devices = get_usb_state(tmp_path / "bus", tmp_path / "udc") + assert all(d["usb3Lane"] == "unknown" for d in devices), "no device sits on the gadget controller" + + msg = messaging.new_message('deviceState') + set_usb_state(msg.deviceState, devices, 0, lane="b") + assert msg.deviceState.usbState.usb3Lane == "b" + assert all(d.usb3Lane == "unknown" for d in msg.deviceState.usbState.devices) + + +def test_device_on_link_controller_gets_the_lane(tmp_path): + # host mode on the type-C port (eGPU dock): upstream's per-device field populates + ctrl = _mkctrl(tmp_path, name="a600000.ssusb") + _mkdev(tmp_path, "1-1", vid=EGPU_DOCK_USB_IDS[0][0], pid=EGPU_DOCK_USB_IDS[0][1], ctrl=ctrl) + _mkudc(tmp_path) + import iqpilot.system.hardware.usb as usbmod + orig = usbmod.usb3_lane + usbmod.usb3_lane = lambda orientation=None: "a" + try: + devices = get_usb_state(tmp_path / "bus", tmp_path / "udc") + finally: + usbmod.usb3_lane = orig + assert devices[0]["usb3Lane"] == "a" + + +def test_dock_ready_requires_the_bundled_firmware_product(tmp_path): + root = tmp_path + vid, pid = EGPU_DOCK_USB_IDS[0] + _mkdev(root, "1-1", vid=vid, pid=pid, product=EGPU_DOCK_FW_PRODUCT) + assert egpu_dock_present(root / "bus") + assert egpu_dock_ready(root / "bus") + + +def test_dock_on_foreign_firmware_is_present_but_not_ready(tmp_path): + root = tmp_path + vid, pid = EGPU_DOCK_USB_IDS[0] + _mkdev(root, "1-1", vid=vid, pid=pid, product="custom deadbeef-CLEAN") + assert egpu_dock_present(root / "bus") + assert not egpu_dock_ready(root / "bus") + + +def test_rom_mode_dock_is_neither_present_nor_ready(tmp_path): + root = tmp_path + vid, pid = EGPU_DOCK_ROM_USB_IDS[0] + _mkdev(root, "1-1", vid=vid, pid=pid, product="USB 3.2 PCIe TinyEnclosure") + assert not egpu_dock_present(root / "bus") + assert not egpu_dock_ready(root / "bus") diff --git a/iqpilot/system/hardware/tici/set_usb_storage.sh b/iqpilot/system/hardware/tici/set_usb_storage.sh index 75e2d4f90..8c3ba1b8c 100755 --- a/iqpilot/system/hardware/tici/set_usb_storage.sh +++ b/iqpilot/system/hardware/tici/set_usb_storage.sh @@ -78,32 +78,38 @@ unbind() { fi } +set_attr() { + [ "$(cat "$1" 2>/dev/null)" = "$2" ] && return 0 + echo "$2" | sudo tee "$1" >/dev/null 2>&1 || true +} + ensure_base() { if ! mountpoint -q /config; then sudo mount -t configfs none /config fi sudo mkdir -p "$GADGET/strings/0x409" "$GADGET/configs/c.1/strings/0x409" cd "$GADGET" - [ -s idVendor ] || echo 0x04D8 | sudo tee idVendor >/dev/null - [ -s idProduct ] || echo 0x1235 | sudo tee idProduct >/dev/null - [ -s strings/0x409/serialnumber ] || echo "$(cat /proc/cmdline | sed -e 's/^.*androidboot.serialno=//' -e 's/ .*$//')" | sudo tee strings/0x409/serialnumber >/dev/null - [ -s strings/0x409/manufacturer ] || echo "comma.ai" | sudo tee strings/0x409/manufacturer >/dev/null - [ -s strings/0x409/product ] || echo "IQ.Pilot" | sudo tee strings/0x409/product >/dev/null - [ -s configs/c.1/MaxPower ] || echo 250 | sudo tee configs/c.1/MaxPower >/dev/null - [ -s configs/c.1/strings/0x409/configuration ] || echo "IQ.Pilot" | sudo tee configs/c.1/strings/0x409/configuration >/dev/null + # `[ -s ]` never guards a configfs attribute: an unset idVendor still reads back + # as "0x0000", so those writes were all skipped and the gadget stayed nameless + set_attr idVendor 0x04D8 + set_attr idProduct 0x1235 + set_attr strings/0x409/serialnumber "$(sed -e 's/^.*androidboot.serialno=//' -e 's/ .*$//' /proc/cmdline)" + set_attr strings/0x409/manufacturer "comma.ai" + set_attr strings/0x409/product "IQ.Pilot" + set_attr configs/c.1/MaxPower 250 + set_attr configs/c.1/strings/0x409/configuration "IQ.Pilot" } add_adb() { # same rationale as add_mass_storage: start from a clean slate to avoid stale busy attributes remove_adb cd "$GADGET" - sudo mkdir -p functions/ncm.0 functions/ffs.adb + sudo mkdir -p functions/ffs.adb sudo mkdir -p /dev/usb-ffs/adb if ! mountpoint -q /dev/usb-ffs/adb; then sudo mount -t functionfs adb /dev/usb-ffs/adb fi - sudo rm -f configs/c.1/ncm.0 configs/c.1/ffs.adb - sudo ln -s functions/ncm.0 configs/c.1/ + sudo rm -f configs/c.1/ffs.adb sudo ln -s functions/ffs.adb configs/c.1/ setprop service.adb.tcp.port -1 2>/dev/null || true sudo systemctl start adbd @@ -115,9 +121,42 @@ remove_adb() { sudo systemctl stop adbd || true if [ -d "$GADGET" ]; then cd "$GADGET" - sudo rm -f configs/c.1/ncm.0 configs/c.1/ffs.adb + sudo rm -f configs/c.1/ffs.adb sudo umount /dev/usb-ffs/adb 2>/dev/null || true - sudo rmdir functions/ncm.0 functions/ffs.adb 2>/dev/null || true + sudo rmdir functions/ffs.adb 2>/dev/null || true + fi +} + +# ncm carries the usb0 ethernet link. ADB needs it, but so does the Mac-backed +# model worker with ADB off, so it is enabled independently of either. +# the kernel randomises the ncm MACs every boot, so macOS sees a new adapter each +# time and orphans the network service holding the link's static address +ncm_id() { + local id + id=$(tr -dc '0-9a-f' < /data/params/d/DongleId 2>/dev/null | tail -c 6) + [ ${#id} -eq 6 ] || id="000001" + echo "$id" +} + +add_ncm() { + remove_ncm + cd "$GADGET" + sudo mkdir -p functions/ncm.0 + local id + id=$(ncm_id) + # best effort: some kernels create the ncm netdev lazily and fail these writes + # with ENODEV, and a pinned MAC is never worth losing the whole gadget over + echo "02:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/host_addr >/dev/null 2>&1 || true + echo "06:49:51:${id:0:2}:${id:2:2}:${id:4:2}" | sudo tee functions/ncm.0/dev_addr >/dev/null 2>&1 || true + sudo rm -f configs/c.1/ncm.0 + sudo ln -s functions/ncm.0 configs/c.1/ +} + +remove_ncm() { + if [ -d "$GADGET" ]; then + cd "$GADGET" + sudo rm -f configs/c.1/ncm.0 + sudo rmdir functions/ncm.0 2>/dev/null || true fi } @@ -162,10 +201,18 @@ USB_STORAGE_ENABLE=0 read_bool_param "/data/params/d/UsbStorageEnabled" && USB_STORAGE_ENABLE=1 ADB_ENABLE=0 read_bool_param "/data/params/d/AdbEnabled" && ADB_ENABLE=1 +EMAC_ENABLE=0 +read_bool_param "/data/params/d/IQEmacEnabled" && EMAC_ENABLE=1 unbind ensure_base +if [ "$ADB_ENABLE" == "1" ] || [ "$EMAC_ENABLE" == "1" ]; then + add_ncm +else + remove_ncm +fi + if [ "$ADB_ENABLE" == "1" ]; then add_adb else diff --git a/iqpilot/system/hardware/tici/usb_storage.py b/iqpilot/system/hardware/tici/usb_storage.py index d6f6ed835..7dcb52214 100644 --- a/iqpilot/system/hardware/tici/usb_storage.py +++ b/iqpilot/system/hardware/tici/usb_storage.py @@ -15,3 +15,57 @@ def apply_usb_storage_state(state: bool): subprocess.Popen(args) except OSError: pass + + +NCM_TRIED_MARKER = "/tmp/.iqemac_ncm_provisioned" +MAX_NCM_ATTEMPTS = 3 + + +def _ncm_attempts() -> int: + try: + with open(NCM_TRIED_MARKER) as f: + return int(f.read().strip() or 0) + except (OSError, ValueError): + return 0 + + +def ensure_ncm_gadget() -> bool: + # configfs is RAM backed, so the gadget must be rebuilt every boot; binding it + # enumerates on the host, which must not happen mid-drive + if os.path.isdir("/sys/class/net/usb0"): + return True + params = Params() + from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected + if not (params.get_bool("IQEmacEnabled") or egpu_selected(params)): + return False + attempts = _ncm_attempts() + if attempts >= MAX_NCM_ATTEMPTS: + return False + try: + # stamped before the run: the gadget build can wedge configfs in + # uninterruptible D state, and retrying that forever helps nobody + with open(NCM_TRIED_MARKER, "w") as f: + f.write(str(attempts + 1)) + subprocess.run(["sudo", "-n", SCRIPT_PATH], timeout=120, check=False) + except (OSError, subprocess.SubprocessError): + return False + return os.path.isdir("/sys/class/net/usb0") + + +INPUT_SUSPEND = "/sys/class/power_supply/battery/input_suspend" + + +def suspend_usb_input(suspend: bool = True) -> bool: + # a host on the data port makes the PMIC sink USB-PD while OBD-C feeds the same + # rail; the SOM browns out. This closes the charge path only, data is untouched + if not os.path.exists(INPUT_SUSPEND): + return False + try: + with open(INPUT_SUSPEND) as f: + if f.read().strip() == ("1" if suspend else "0"): + return True + except OSError: + pass + rc = subprocess.run(["sudo", "-n", "sh", "-c", f"echo {int(suspend)} > {INPUT_SUSPEND}"], + check=False, capture_output=True) + return rc.returncode == 0 diff --git a/iqpilot/system/hardware/usb.py b/iqpilot/system/hardware/usb.py new file mode 100644 index 000000000..15be6f295 --- /dev/null +++ b/iqpilot/system/hardware/usb.py @@ -0,0 +1,184 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +USB bus snapshot for deviceState: every enumerated device with its negotiated +speed and its controller's link-error count. Landing this in every rlog makes +cable/hub/link regressions diagnosable from a recorded route instead of only +live. + +Link errors come from `portli` on the ssusb controller (IQ.OS 4.9.1+); on older +builds the file is absent and the counts read 0. + +The USB eGPU dock is identified by VID/PID only. comma's internal codename for +it is deliberately not used here: IQ.Pilot runs these models on several +backends (eGPU dock, eMac), so the naming stays about the role, not the vendor. +""" +from pathlib import Path + +# comma's USB eGPU dock, both shipped USB IDs. The ROM ids are the same board +# sitting in its bootloader (ASMedia) before vendor firmware is flashed — it +# enumerates but cannot serve a GPU in that state. +EGPU_DOCK_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) +EGPU_DOCK_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) +# must equal image_product() of the bundled firmware; test_egpu_dock_flash pins them together +EGPU_DOCK_FW_PRODUCT = "custom ed4e39b7-CLEAN" + + +def is_egpu_usb_device(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool: + ids = EGPU_DOCK_USB_IDS + EGPU_DOCK_ROM_USB_IDS if include_bootloader else EGPU_DOCK_USB_IDS + return (vendor_id, product_id) in ids +USB_DEVICES_PATH = Path("/sys/bus/usb/devices") +UDC_PATH = Path("/sys/class/udc") +TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") +USB3_LANES = {1: "a", 2: "b"} # 0 = unattached +SOC_PLATFORM_PATH = Path("/sys/devices/platform/soc") +CONTROLLER_SUFFIX = ".ssusb" +LINK_ERRORS_FILE = "portli" + + +def read(path: Path) -> str | None: + # a controller in peripheral mode fails portli's show(); that surfaces as TypeError, not OSError + try: + return path.read_text().strip() + except Exception: + return None + + +def read_int(path: Path, base: int = 10) -> int: + try: + return int(path.read_text(), base) + except Exception: + return 0 + + +def read_hex_counter(path: Path) -> int: + """sysfs counter printed as '0x0000002a' (portli), tolerating a bare hex value.""" + raw = read(path) + if raw is None: + return 0 + try: + return int(raw, 0) if raw.lower().startswith("0x") else int(raw, 16) + except ValueError: + return 0 + + +def get_usb_topology(root: Path = USB_DEVICES_PATH) -> set[str]: + """Names of everything on the bus; a cheap way to detect hotplug without + re-reading every attribute.""" + try: + return {p.name for p in root.iterdir()} + except Exception: + return set() + + +def usb_devices(root: Path = USB_DEVICES_PATH) -> list[Path]: + try: + return sorted((d for d in root.glob("*") if (d / "idVendor").exists()), key=lambda p: p.name) + except Exception: + return [] + + +def controller(device: Path) -> Path | None: + """The SuperSpeed controller a device hangs off (…/a800000.ssusb).""" + try: + return next((p for p in device.resolve().parents if p.name.endswith(CONTROLLER_SUFFIX)), None) + except Exception: + return None + + +def usb_controllers(soc: Path = SOC_PLATFORM_PATH) -> list[Path]: + try: + return sorted(soc.glob(f"*{CONTROLLER_SUFFIX}")) + except Exception: + return [] + + +def link_controller(udc_root: Path = UDC_PATH) -> str: + """Name of the Type-C port's controller, derived from the UDC rather than + hardcoded: the gadget exposes `.dwc3`, whose address prefix is the + `.ssusb` controller behind the same connector. comma pins the 3X value + directly, which would be wrong on any other board.""" + try: + udc = next(iter(sorted(p.name for p in udc_root.iterdir())), "") + except Exception: + return "" + return f"{udc.split('.')[0]}{CONTROLLER_SUFFIX}" if udc else "" + + +def usb3_lane(orientation: int | None = None) -> str: + """Which SuperSpeed lane the Type-C connector landed on. Unattached reads 0, + which is 'unknown' rather than a lane.""" + if orientation is None: + orientation = read_int(TYPEC_CC_ORIENTATION_PATH) + return USB3_LANES.get(orientation, "unknown") + + +def link_errors(ctrl: Path | None) -> int: + return read_hex_counter(ctrl / LINK_ERRORS_FILE) if ctrl is not None else 0 + + +def get_link_error_count(soc: Path = SOC_PLATFORM_PATH) -> int: + """Cumulative SS port link errors, read off the controller rather than a + device: in peripheral mode (eMac gadget link) the peer never enumerates on + our side, so there is no device row to carry the count.""" + return sum(link_errors(c) for c in usb_controllers(soc)) + + +def egpu_dock_present(root: Path = USB_DEVICES_PATH) -> bool: + """A dock in ROM/bootloader state is deliberately NOT counted as present: it + enumerates but cannot serve a GPU until vendor firmware is flashed.""" + return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS + for d in usb_devices(root)) + + +def egpu_dock_ready(root: Path = USB_DEVICES_PATH) -> bool: + """Present AND running the exact firmware we ship. A dock on any other + firmware enumerates fine but has not been validated with this stack, so the + runtime refuses it; the flasher still sees it via egpu_dock_present.""" + return any((read_int(d / "idVendor", 16), read_int(d / "idProduct", 16)) in EGPU_DOCK_USB_IDS + and (read(d / "product") or "").strip() == EGPU_DOCK_FW_PRODUCT + for d in usb_devices(root)) + + +def get_usb_state(root: Path = USB_DEVICES_PATH, udc_root: Path = UDC_PATH) -> list[dict]: + devices = [] + lane, link_ctrl = usb3_lane(), link_controller(udc_root) + for device in usb_devices(root): + ctrl = controller(device) + devices.append({ + "usb3Lane": lane if ctrl is not None and ctrl.name == link_ctrl else "unknown", + "busnum": read_int(device / "busnum"), + "devnum": read_int(device / "devnum"), + "vendorId": read_int(device / "idVendor", 16), + "productId": read_int(device / "idProduct", 16), + "speedMbps": read_int(device / "speed"), + "manufacturer": read(device / "manufacturer") or "", + "product": read(device / "product") or "", + # 16-bit field upstream, so mask rather than let a wrapped counter overflow it + "linkErrorCount": link_errors(ctrl) & 0xFFFF, + }) + return devices + + +def set_usb_state(device_state, devices: list[dict], link_error_count: int = 0, + lane: str | None = None) -> None: + entries = device_state.usbState.init('devices', len(devices)) + + dock_present = False + for entry, device in zip(entries, devices, strict=True): + entry.busnum = device["busnum"] + entry.devnum = device["devnum"] + entry.vendorId = device["vendorId"] + entry.productId = device["productId"] + entry.speedMbps = device["speedMbps"] + entry.manufacturer = device["manufacturer"] + entry.product = device["product"] + entry.linkErrorCount = device.get("linkErrorCount", 0) & 0xFFFF + entry.usb3Lane = device.get("usb3Lane", "unknown") + + if (entry.vendorId, entry.productId) in EGPU_DOCK_USB_IDS: + dock_present = True + + device_state.usbState.linkErrorCount = link_error_count + device_state.usbState.usb3Lane = lane if lane is not None else usb3_lane() + device_state.egpuDockPresent = dock_present diff --git a/iqpilot/system/manager/manager.py b/iqpilot/system/manager/manager.py index 5edb2b626..c72e65329 100755 --- a/iqpilot/system/manager/manager.py +++ b/iqpilot/system/manager/manager.py @@ -53,6 +53,16 @@ def manager_init() -> None: except Exception: cloudlog.exception("recover_unclean_segments failed") + try: + from iqpilot.system.hardware import TICI + if TICI: + from iqpilot.system.hardware.tici.usb_storage import ensure_ncm_gadget, suspend_usb_input + ensure_ncm_gadget() + if Params().get_bool("IQEmacEnabled") or Params().get_bool("IQEgpuEnabled"): + suspend_usb_input(True) + except Exception: + cloudlog.exception("emac usb setup failed") + build_metadata = get_build_metadata() params = Params() diff --git a/iqpilot/system/manager/process_config.py b/iqpilot/system/manager/process_config.py index 7e1492154..449fa70e5 100644 --- a/iqpilot/system/manager/process_config.py +++ b/iqpilot/system/manager/process_config.py @@ -8,6 +8,7 @@ from iqpilot.system.hardware import HARDWARE, PC, TICI from iqpilot.system.hardware.hw import Paths from iqpilot.system.manager.process import PythonProcess, NativeProcess, BundleProcess +from iqpilot.selfdrive.iqmodeld.egpu_helpers import egpu_selected, resolve_backend, usbgpu_present from iqpilot.selfdrive.iqmodeld.models.helpers import get_active_model_runner from iqpilot.konn3kt.service_health import hephaestus_ready @@ -124,6 +125,22 @@ def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is tinygrad.""" return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad) +def _egpu_present(params) -> bool: + if params.get_bool("IQEgpuDisabled"): + return False + return usbgpu_present() + + +def emac_enabled(started, params, CP: car.CarParams) -> bool: + return resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "emac" + +def egpu_enabled(started, params, CP: car.CarParams) -> bool: + return (resolve_backend(params.get_bool("IQEmacEnabled"), egpu_selected(params), _egpu_present(params)) == "egpu" + and _egpu_present(params)) + +def big_model_enabled(started, params, CP: car.CarParams) -> bool: + return params.get_bool("IQEmacEnabled") or egpu_selected(params) + def hephaestus_ready_shim(started, params, CP: car.CarParams) -> bool: return hephaestus_ready(params) @@ -196,6 +213,15 @@ procs += [ # Models BundleProcess("models_manager", "iqpilot_model_selector_private", "iqpilot_private.models.manager", and_(only_offroad, not_low_power)), NativeProcess("iqmodeld", "iqpilot/selfdrive/iqmodeld", ["./iqmodeld"], and_(only_onroad, is_tinygrad_model), restart_if_crash=True), + # big-model backends: iqmodeld self-demotes to the small channel worker when + # either backend is enabled; the selector publishes, and exactly one big + # worker (Mac or eGPU, eMac wins) feeds the BIG channel + PythonProcess("modeld_selector", "iqpilot.selfdrive.iqmodeld.modeld_selector", + and_(only_onroad, and_(is_tinygrad_model, big_model_enabled)), restart_if_crash=True), + BundleProcess("maciqmodeld", "iqpilot_emac_private", "iqpilot_private.emac.maciqmodeld", + and_(only_onroad, and_(is_tinygrad_model, emac_enabled)), restart_if_crash=True), + PythonProcess("iqegpumodeld", "iqpilot.selfdrive.iqmodeld.iqegpumodeld", + and_(only_onroad, and_(is_tinygrad_model, egpu_enabled)), restart_if_crash=True), BundleProcess("backup_manager_k3", "iqpilot_hephaestusd_private", "iqpilot_private.konn3kt.backups.backup_orchestrator", and_(only_offroad, hephaestus_ready_shim, not_low_power)), diff --git a/iqpilot/tools/lib/url_file.py b/iqpilot/tools/lib/url_file.py index de86026f1..5200bb019 100644 --- a/iqpilot/tools/lib/url_file.py +++ b/iqpilot/tools/lib/url_file.py @@ -21,6 +21,10 @@ CACHE_SIZE = 10 * 1024 * 1024 * 1024 # total cache size in GB logging.getLogger("urllib3").setLevel(logging.WARNING) +USER_AGENT = os.getenv("IQPILOT_HTTP_USER_AGENT", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36") + + def _env_int(name: str, default: int) -> int: raw = os.getenv(name) if raw is None: @@ -113,6 +117,10 @@ class URLFile: pass def _request(self, method: str, url: str, headers: dict[str, str] | None = None) -> BaseHTTPResponse: + # the data host is behind cloudflare, which answers a default urllib3 agent + # with a 1010 block. It reads as 403 Forbidden on a correctly signed url, so + # it looks like an auth problem and is not one. + headers = {**(headers or {}), "User-Agent": USER_AGENT} try: return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) except MaxRetryError as e: diff --git a/iqpilot/tools/replay/camera.cc b/iqpilot/tools/replay/camera.cc index 671c0320b..715ad0a79 100644 --- a/iqpilot/tools/replay/camera.cc +++ b/iqpilot/tools/replay/camera.cc @@ -70,6 +70,9 @@ void CameraServer::cameraThread(Camera &cam) { .timestamp_eof = eidx.getTimestampEof(), }; vipc_server_->send(yuv, &extra); + if (++sent_count_ % 100 == 1) { + rInfo("camera[%d] vipc send #%lu frame_id=%u seg_frame=%d", cam.type, sent_count_, frame_id, segment_id); + } } else { rError("camera[%d] failed to get frame: %lu", cam.type, segment_id); } diff --git a/iqpilot/tools/replay/camera.h b/iqpilot/tools/replay/camera.h index 1d6967265..732c126d4 100644 --- a/iqpilot/tools/replay/camera.h +++ b/iqpilot/tools/replay/camera.h @@ -38,5 +38,6 @@ protected: {.type = WideRoadCam, .stream_type = VISION_STREAM_WIDE_ROAD}, }; std::atomic publishing_ = 0; + uint64_t sent_count_ = 0; std::unique_ptr vipc_server_; }; diff --git a/iqpilot/tools/replay/framereader.cc b/iqpilot/tools/replay/framereader.cc index c3c60acf6..8a2bcc45f 100644 --- a/iqpilot/tools/replay/framereader.cc +++ b/iqpilot/tools/replay/framereader.cc @@ -107,11 +107,21 @@ bool FrameReader::loadFromFile(CameraType type, const std::string &file, bool no packets_info.reserve(60 * 20); // 20fps, one minute while (!(abort && *abort) && av_read_frame(input_ctx, &pkt) == 0) { if (pkt.stream_index == video_stream_idx_) { - packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos}); + packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos, .ts = pkt.dts}); } av_packet_unref(&pkt); } - avio_seek(input_ctx->pb, 0, SEEK_SET); + // IQ.Pilot camera files are (fragmented) MP4: rewinding the raw pb leaves the + // mov demuxer's sample cursor at EOF, so rewind through the demuxer instead. + // comma's raw HEVC bitstreams have no index; only the pb rewind works there. + if (!packets_info.empty() && + avformat_seek_file(input_ctx, video_stream_idx_, INT64_MIN, + packets_info.front().ts, packets_info.front().ts, 0) < 0) { + avformat_flush(input_ctx); + avio_seek(input_ctx->pb, 0, SEEK_SET); + } + rInfo("frame index built: %zu packets, fmt=%s", packets_info.size(), + input_ctx->iformat ? input_ctx->iformat->name : "?"); return !packets_info.empty(); } @@ -147,6 +157,10 @@ bool FFmpegVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { } width = (decoder_ctx->width + 3) & ~3; height = decoder_ctx->height; + // frame-threaded software decode: single-threaded can't hold 2x1928x1208@20 + // on this SoC. The added output delay is absorbed by the EAGAIN-aware loop. + decoder_ctx->thread_count = 3; + decoder_ctx->thread_type = FF_THREAD_FRAME; if (hw_decoder && !initHardwareDecoder(HW_DEVICE_TYPE)) { rWarning("No device with hardware decoder found. fallback to CPU decoding."); @@ -188,6 +202,14 @@ bool FFmpegVideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { bool FFmpegVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { int current_idx = idx; if (idx != reader->prev_idx + 1) { + if (idx > reader->prev_idx && idx - reader->prev_idx <= 300) { + // forward catch-up: the decoder is already positioned at prev_idx+1, and + // sequential decode is cheaper and (for raw H.264) more reliable than a + // byte seek plus keyframe re-decode + current_idx = reader->prev_idx + 1; + reader->prev_idx = idx; + goto read_packets; + } // seeking to the nearest key frame for (int i = idx; i >= 0; --i) { if (reader->packets_info[i].flags & AV_PKT_FLAG_KEY) { @@ -198,6 +220,12 @@ bool FFmpegVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { auto pos = reader->packets_info[current_idx].pos; int ret = avformat_seek_file(reader->input_ctx, 0, pos, pos, pos, AVSEEK_FLAG_BYTE); + if (ret < 0) { + // mp4 containers reject byte seeks; seek the keyframe by timestamp + // through the mov index instead + auto ts = reader->packets_info[current_idx].ts; + ret = avformat_seek_file(reader->input_ctx, reader->video_stream_idx_, INT64_MIN, ts, ts, 0); + } if (ret < 0) { rError("Failed to seek to byte position %lld: %d", pos, AVERROR(ret)); return false; @@ -206,26 +234,42 @@ bool FFmpegVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { } reader->prev_idx = idx; +read_packets: + // H.264 has decoder delay: the first packets may legitimately yield no frame + // yet (EAGAIN), and one packet can release several buffered frames. comma's + // zero-delay HEVC never exercised either case. AVPacket pkt; - while (av_read_frame(reader->input_ctx, &pkt) >= 0) { - // Skip non-video packets + int rf_ret; + while ((rf_ret = av_read_frame(reader->input_ctx, &pkt)) >= 0) { if (pkt.stream_index != reader->video_stream_idx_) { av_packet_unref(&pkt); continue; } - - AVFrame *frame = decodeFrame(&pkt); + int ret = avcodec_send_packet(decoder_ctx, &pkt); av_packet_unref(&pkt); - if (!frame) { - rError("Failed to decode frame at index %d", current_idx); + if (ret < 0) { + rError("Error sending a packet for decoding: %d", ret); return false; } - - if (current_idx++ == idx) { - return copyBuffer(frame, buf); + while ((ret = avcodec_receive_frame(decoder_ctx, av_frame_)) == 0) { + AVFrame *frame = av_frame_; + if (av_frame_->format == hw_pix_fmt) { + if (av_hwframe_transfer_data(hw_frame_, av_frame_, 0) < 0) { + rError("error transferring frame data from GPU to CPU"); + return false; + } + frame = hw_frame_; + } + if (current_idx++ == idx) { + return copyBuffer(frame, buf); + } + } + if (ret != AVERROR(EAGAIN)) { + rError("avcodec_receive_frame error: %d", ret); + return false; } } - rError("Failed to find frame at index %d", idx); + rError("Failed to find frame at index %d (read ret=%d)", idx, rf_ret); return false; } @@ -268,20 +312,46 @@ bool FFmpegVideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { } #ifndef __APPLE__ +QcomVideoDecoder::~QcomVideoDecoder() { + if (bsf_) av_bsf_free(&bsf_); +} + bool QcomVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { - if (codecpar->codec_id != AV_CODEC_ID_HEVC) { - rError("Hardware decoder only supports HEVC codec"); + // msm_vidc decodes both; IQ.Pilot recordings are H.264 while comma's are HEVC + uint32_t v4l2_fmt; + if (codecpar->codec_id == AV_CODEC_ID_HEVC) { + v4l2_fmt = V4L2_PIX_FMT_HEVC; + } else if (codecpar->codec_id == AV_CODEC_ID_H264) { + v4l2_fmt = V4L2_PIX_FMT_H264; + if (codecpar->extradata && codecpar->extradata_size > 0) { + // mp4 carries AVCC (length-prefixed NALs, headers out-of-band); the V4L2 + // decoder wants an Annex-B bitstream with in-band SPS/PPS + const AVBitStreamFilter *f = av_bsf_get_by_name("h264_mp4toannexb"); + if (!f || av_bsf_alloc(f, &bsf_) < 0 || + avcodec_parameters_copy(bsf_->par_in, codecpar) < 0 || av_bsf_init(bsf_) < 0) { + rError("failed to set up h264_mp4toannexb filter"); + return false; + } + } + } else { + rError("Hardware decoder only supports HEVC and H.264 codecs"); return false; } width = codecpar->width; height = codecpar->height; - msm_vidc.init(VIDEO_DEVICE, width, height, V4L2_PIX_FMT_HEVC); + msm_vidc.init(VIDEO_DEVICE, width, height, v4l2_fmt); return true; } bool QcomVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { int from_idx = idx; if (idx != reader->prev_idx + 1) { + if (idx > reader->prev_idx && idx - reader->prev_idx <= 300) { + // forward catch-up: the decoder is already positioned at prev_idx+1, and + // sequential decode is cheaper and (for raw H.264) more reliable than a + // byte seek plus keyframe re-decode + from_idx = reader->prev_idx + 1; + } else { // seeking to the nearest key frame for (int i = idx; i >= 0; --i) { if (reader->packets_info[i].flags & AV_PKT_FLAG_KEY) { @@ -292,10 +362,17 @@ bool QcomVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { auto pos = reader->packets_info[from_idx].pos; int ret = avformat_seek_file(reader->input_ctx, 0, pos, pos, pos, AVSEEK_FLAG_BYTE); + if (ret < 0) { + // mp4 containers reject byte seeks; seek the keyframe by timestamp + // through the mov index instead + auto ts = reader->packets_info[from_idx].ts; + ret = avformat_seek_file(reader->input_ctx, reader->video_stream_idx_, INT64_MIN, ts, ts, 0); + } if (ret < 0) { rError("Failed to seek to byte position %lld: %d", pos, AVERROR(ret)); return false; } + } } reader->prev_idx = idx; bool result = false; @@ -303,6 +380,13 @@ bool QcomVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { msm_vidc.avctx = reader->input_ctx; for (int i = from_idx; i <= idx; ++i) { if (av_read_frame(reader->input_ctx, &pkt) == 0) { + if (bsf_ != nullptr) { + if (av_bsf_send_packet(bsf_, &pkt) < 0 || av_bsf_receive_packet(bsf_, &pkt) < 0) { + rError("h264_mp4toannexb failed at index %d", i); + av_packet_unref(&pkt); + return false; + } + } result = msm_vidc.decodeFrame(&pkt, buf) && (i == idx); av_packet_unref(&pkt); } diff --git a/iqpilot/tools/replay/framereader.h b/iqpilot/tools/replay/framereader.h index d8e86fce0..98c7ef1cc 100644 --- a/iqpilot/tools/replay/framereader.h +++ b/iqpilot/tools/replay/framereader.h @@ -37,6 +37,7 @@ public: struct PacketInfo { int flags; int64_t pos; + int64_t ts; // dts; byte pos is useless for seeking in mp4 containers }; std::vector packets_info; }; @@ -72,11 +73,12 @@ private: class QcomVideoDecoder : public VideoDecoder { public: QcomVideoDecoder() {}; - ~QcomVideoDecoder() override {}; + ~QcomVideoDecoder() override; bool open(AVCodecParameters *codecpar, bool hw_decoder) override; bool decode(FrameReader *reader, int idx, VisionBuf *buf) override; private: MsmVidc msm_vidc = MsmVidc(); + AVBSFContext *bsf_ = nullptr; // AVCC (mp4) -> Annex-B for msm_vidc }; #endif diff --git a/iqpilot/tools/replay/main.cc b/iqpilot/tools/replay/main.cc index 30f85b170..7cc737946 100644 --- a/iqpilot/tools/replay/main.cc +++ b/iqpilot/tools/replay/main.cc @@ -1,4 +1,5 @@ #include +#include #include #include @@ -176,6 +177,15 @@ int main(int argc, char *argv[]) { return 0; } + // REPLAY_HEADLESS: skip ncurses, which needs a real TTY and swallows all + // replay log output into its UI — required when driven from a service + if (getenv("REPLAY_HEADLESS") != nullptr) { + replay.start(config.start_seconds); + while (true) { + pause(); + } + } + ConsoleUI console_ui(&replay); replay.start(config.start_seconds); return console_ui.exec(); diff --git a/iqpilot/tools/replay/qcom_decoder.cc b/iqpilot/tools/replay/qcom_decoder.cc index eb5409daa..675db1a8f 100644 --- a/iqpilot/tools/replay/qcom_decoder.cc +++ b/iqpilot/tools/replay/qcom_decoder.cc @@ -43,7 +43,7 @@ bool MsmVidc::init(const char* dev, size_t width, size_t height, uint64_t codec) } subscribeEvents(); v4l2_buf_type out_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - setPlaneFormat(out_type, V4L2_PIX_FMT_HEVC); // Also allocates the output buffer + setPlaneFormat(out_type, codec); // Also allocates the output buffer setFPS(FPS); request_buffers(fd, out_type, OUTPUT_BUFFER_COUNT); util::safe_ioctl(fd, VIDIOC_STREAMON, &out_type, "VIDIOC_STREAMON OUTPUT failed"); diff --git a/iqpilot/ui/layouts/settings/iq_panels.py b/iqpilot/ui/layouts/settings/iq_panels.py index 512e7b06e..529f4c93f 100644 --- a/iqpilot/ui/layouts/settings/iq_panels.py +++ b/iqpilot/ui/layouts/settings/iq_panels.py @@ -1590,6 +1590,31 @@ _ACTIVE_BUNDLE_KEY = "ModelManager_ActiveBundle" _DOWNLOAD_INDEX_KEY = "ModelManager_DownloadIndex" _RUNNER_CACHE_KEY = "ModelRunnerTypeCache" +def _big_model_options() -> list[tuple[str, str]]: + try: + from iqpilot.selfdrive.iqmodeld.emac_model_meta import big_models + return big_models(ui_state.params) + except Exception: + return [] + + +def _big_model_label(key: str) -> str: + for name, display in _big_model_options(): + if name == key: + return display + return key + + +def _refresh_big_catalog() -> None: + def worker(): + try: + from iqpilot.selfdrive.iqmodeld.emac_model_meta import refresh_catalog + refresh_catalog(ui_state.params) + except Exception: + pass + threading.Thread(target=worker, daemon=True).start() + + class ModelsLayout(Widget): def __init__(self): super().__init__() @@ -1599,6 +1624,7 @@ class ModelsLayout(Widget): self.download_status = None self.prev_download_status = None self.model_dialog = None + self._big_model_dialog = None self.last_cache_calc_time = 0 self._initialize_items() @@ -1614,6 +1640,14 @@ class ModelsLayout(Widget): callback=self._handle_current_model_clicked ) + self.big_model_item = button_item( + lambda: tr("Big Model"), + lambda: tr("CHANGE"), + tr("Only works with external compute connected over USB."), + self._handle_big_model_clicked, + ) + self.big_model_item.action_item.set_value(self._big_model_value()) + self.supercombo_label = progress_item(tr("Combined Model")) self.vision_label = progress_item(tr("Vision Weights")) self.policy_label = progress_item(tr("Policy Weights")) @@ -1630,7 +1664,7 @@ class ModelsLayout(Widget): self.redownload_item = button_item(lambda: tr("Redownload Current Model"), lambda: tr("REDOWNLOAD"), "", self._redownload_model) self.cancel_download_item = button_item(tr("Stop Download"), tr("Cancel"), "", self._cancel_model_request) - self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, + self.items = [self.current_model_item, self.big_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, self.policy_label, self.redownload_item, self.refresh_item, self.clear_cache_item] def _is_downloading(self): @@ -1880,7 +1914,40 @@ class ModelsLayout(Widget): get_folders_fn=self._get_folders, on_exit=self._on_model_selected) gui_app.set_modal_overlay(self.model_dialog, callback=self._on_model_selected) + @staticmethod + def _big_model_value() -> str: + if not ui_state.params.get_bool("IQEmacEnabled"): + return tr("Off") + key = ui_state.params.get("IQEmacModel") + key = key.decode() if isinstance(key, bytes) else (key or "") + return _big_model_label(key) if key in [n for n, _ in _big_model_options()] else tr("Off") + + def _handle_big_model_clicked(self): + options = _big_model_options() + if len(options) <= 1: + _refresh_big_catalog() + keys = [n for n, _ in options] + labels = [tr("Off")] + [d for _, d in options] + self._big_model_dialog = MultiOptionDialog(tr("Big Model"), labels, self._big_model_value()) + + def handle_selection(result): + if result == DialogResult.CONFIRM and self._big_model_dialog is not None and self._big_model_dialog.selection: + selected = self._big_model_dialog.selection + if selected == tr("Off"): + ui_state.params.put_bool("IQEmacEnabled", False) + else: + for key in keys: + if _big_model_label(key) == selected: + ui_state.params.put("IQEmacModel", key) + ui_state.params.put_bool("IQEmacEnabled", True) + break + self.big_model_item.action_item.set_value(self._big_model_value()) + self._big_model_dialog = None + + gui_app.set_modal_overlay(self._big_model_dialog, callback=handle_selection) + def _on_refresh_models(self): + _refresh_big_catalog() ui_state.params.put("ModelManager_LastSyncTime", 0) self._refreshing = True self._refresh_start = time.monotonic() diff --git a/iqpilot/ui/mici/onroad/emac_source.py b/iqpilot/ui/mici/onroad/emac_source.py new file mode 100644 index 000000000..4adb1801a --- /dev/null +++ b/iqpilot/ui/mici/onroad/emac_source.py @@ -0,0 +1,43 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +import time + +import pyray as rl + +from iqpilot.common.params import Params +from iqpilot.ui.onroad.big_model_status import SourceState, draw_source_label, resolve_source +from iqpilot.selfdrive.ui.ui_state import ui_state +from iqpilot.system.ui.lib.application import FontWeight, gui_app +from iqpilot.system.ui.lib.text_measure import measure_text_cached + +_POLL_S = 1.0 +_FONT_SIZE = 44 +_WHEEL_H = 50 +_MARGIN_R = 12 +_MARGIN_B = 14 + + +class EmacSourceIndicator: + def __init__(self): + self._params = Params() + self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._last_poll = 0.0 + self._label = "" + self._state = SourceState.HIDDEN + + def update(self) -> None: + now = time.monotonic() + if now - self._last_poll < _POLL_S: + return + self._last_poll = now + self._label, self._state = resolve_source(self._params, ui_state.engaged) + + def render(self, rect: rl.Rectangle) -> None: + if self._state == SourceState.HIDDEN: + return + size = measure_text_cached(self._font, self._label, _FONT_SIZE) + x = rect.x + rect.width - _MARGIN_R - size.x + y = rect.y + rect.height - _MARGIN_B - (_WHEEL_H + size.y) / 2 + draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE) diff --git a/iqpilot/ui/mici/onroad/hud_renderer.py b/iqpilot/ui/mici/onroad/hud_renderer.py index f5fc67f4c..4dfc6fa21 100644 --- a/iqpilot/ui/mici/onroad/hud_renderer.py +++ b/iqpilot/ui/mici/onroad/hud_renderer.py @@ -5,11 +5,12 @@ import pyray as rl from iqpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from iqpilot.ui.onroad.hud_overlays import IQBlindSpotOverlay +from iqpilot.ui.mici.onroad.emac_source import EmacSourceIndicator class IQMiciHudRenderer(HudRenderer): def __init__(self): super().__init__() - self._overlays = [IQBlindSpotOverlay()] + self._overlays = [IQBlindSpotOverlay(), EmacSourceIndicator()] def _update_state(self) -> None: super()._update_state() diff --git a/iqpilot/ui/onroad/big_model_status.py b/iqpilot/ui/onroad/big_model_status.py new file mode 100644 index 000000000..a4e1537d3 --- /dev/null +++ b/iqpilot/ui/onroad/big_model_status.py @@ -0,0 +1,71 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" +import math +from enum import IntEnum + +import pyray as rl + +from iqpilot.common.params import Params +from iqpilot.system.ui.lib.text_measure import measure_text_cached + +class SourceState(IntEnum): + HIDDEN = 0 + LOADING = 1 + ACTIVE = 2 + FAILED = 3 + CROSSED = 4 + + +_GREEN = rl.Color(46, 204, 113, 255) +_ORANGE = rl.Color(255, 115, 0, 255) +_WHITE = rl.Color(255, 255, 255, 255) + + +def _emac_state(params: Params, engaged: bool) -> SourceState: + if not params.get_bool("MacModelReachable"): + return SourceState.HIDDEN + if params.get_bool("MacModelActive"): + return SourceState.ACTIVE + if params.get_bool("MacModelFailed"): + return SourceState.CROSSED if engaged else SourceState.FAILED + return SourceState.LOADING + + +def _egpu_state(params: Params, engaged: bool) -> SourceState: + if not params.get_bool("UsbGpuPresent"): + return SourceState.HIDDEN + if params.get_bool("UsbGpuActive"): + return SourceState.ACTIVE + if params.get_bool("UsbGpuFailed"): + return SourceState.CROSSED if engaged else SourceState.FAILED + return SourceState.LOADING + + +def resolve_source(params: Params, engaged: bool) -> tuple[str, SourceState]: + if params.get_bool("IQEmacEnabled"): + return "MAC", _emac_state(params, engaged) + if params.get_bool("UsbGpuPresent") or params.get_bool("IQEgpuEnabled"): + return "GPU", _egpu_state(params, engaged) + return "", SourceState.HIDDEN + + +def draw_source_label(font: rl.Font, label: str, state: SourceState, + pos: rl.Vector2, font_size: int) -> None: + if state == SourceState.HIDDEN or not label: + return + if state == SourceState.ACTIVE: + color, opacity, strike = _GREEN, 1.0, False + elif state == SourceState.FAILED: + color, opacity, strike = _ORANGE, 1.0, False + elif state == SourceState.CROSSED: + color, opacity, strike = _WHITE, 0.65, True + else: + color, opacity, strike = _WHITE, 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)), False + + col = rl.Color(color.r, color.g, color.b, int(255 * opacity)) + rl.draw_text_ex(font, label, pos, font_size, 0, col) + if strike: + size = measure_text_cached(font, label, font_size) + y = int(pos.y + size.y / 2) + rl.draw_line_ex(rl.Vector2(pos.x - 2, y), rl.Vector2(pos.x + size.x + 2, y), 4, col) diff --git a/iqpilot/ui/onroad/emac_status.py b/iqpilot/ui/onroad/emac_status.py new file mode 100644 index 000000000..efcb65955 --- /dev/null +++ b/iqpilot/ui/onroad/emac_status.py @@ -0,0 +1,44 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ +""" + +import time + +import pyray as rl + +from iqpilot.common.params import Params +from iqpilot.ui.onroad.big_model_status import SourceState, draw_source_label, resolve_source +from iqpilot.selfdrive.ui import UI_BORDER_SIZE +from iqpilot.selfdrive.ui.onroad.driver_state import BTN_SIZE +from iqpilot.selfdrive.ui.ui_state import ui_state +from iqpilot.system.ui.lib.application import FontWeight, gui_app +from iqpilot.system.ui.lib.text_measure import measure_text_cached +from iqpilot.system.ui.widgets import Widget + +_POLL_S = 1.0 +_FONT_SIZE = 70 + + +class EmacStatusRenderer(Widget): + def __init__(self): + super().__init__() + self._params = Params() + self._font = gui_app.font(FontWeight.SEMI_BOLD) + self._last_poll = 0.0 + self._label = "" + self._state = SourceState.HIDDEN + + def update(self): + now = time.monotonic() + if now - self._last_poll < _POLL_S: + return + self._last_poll = now + self._label, self._state = resolve_source(self._params, ui_state.engaged) + + def _render(self, rect: rl.Rectangle): + if self._state == SourceState.HIDDEN: + return + size = measure_text_cached(self._font, self._label, _FONT_SIZE) + x = rect.x + UI_BORDER_SIZE + BTN_SIZE // 2 - size.x / 2 + y = rect.y + rect.height / 2 - size.y / 2 + draw_source_label(self._font, self._label, self._state, rl.Vector2(x, y), _FONT_SIZE) diff --git a/iqpilot/ui/onroad/hud_renderer.py b/iqpilot/ui/onroad/hud_renderer.py index 55d9bffe1..c4c6d9b76 100644 --- a/iqpilot/ui/onroad/hud_renderer.py +++ b/iqpilot/ui/onroad/hud_renderer.py @@ -16,6 +16,7 @@ from iqpilot.ui.onroad.hud_overlays import ( ) from iqpilot.ui.onroad.nav_map_panel import NavMapPanel from iqpilot.ui.onroad.soft_warning import SoftWarningRenderer +from iqpilot.ui.onroad.emac_status import EmacStatusRenderer ENABLE_FLOATING_NAV_MAP_PANEL = False ENABLE_SPLIT_NAV_MAP_PANEL = True @@ -25,6 +26,7 @@ class IQHudRenderer(HudRenderer): def __init__(self): super().__init__() self.developer_ui = IQDevMetricsOverlay() + self.emac_status = EmacStatusRenderer() self.nav_map_panel = NavMapPanel() self.road_name_renderer = RoadNameRenderer() self.rocket_fuel = IQAccelBar() @@ -38,6 +40,7 @@ class IQHudRenderer(HudRenderer): super()._update_state() if ENABLE_FLOATING_NAV_MAP_PANEL or ENABLE_SPLIT_NAV_MAP_PANEL: self.nav_map_panel.update() + self.emac_status.update() self.road_name_renderer.update() self.speed_limit_renderer.update() has_limit = self.speed_limit_renderer.speed_limit_valid or self.speed_limit_renderer.speed_limit_last_valid @@ -65,6 +68,7 @@ class IQHudRenderer(HudRenderer): self.developer_ui.render(rect) if ENABLE_FLOATING_NAV_MAP_PANEL: self.nav_map_panel.render(rect) + self.emac_status.render(rect) self.road_name_renderer.render(torque_rect) self.turn_signal_controller.render(rect) self.soft_warning_renderer.render(rect) diff --git a/iqpilot/ui/tests/test_big_model_status.py b/iqpilot/ui/tests/test_big_model_status.py new file mode 100644 index 000000000..46af2707a --- /dev/null +++ b/iqpilot/ui/tests/test_big_model_status.py @@ -0,0 +1,66 @@ +""" +Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos/ + +The BIG (tici/tizi) and SMALL (mici) onroad source indicators share one +resolver (comma PR #38492 states, backend-aware label). Pin its truth table. +""" +from iqpilot.ui.onroad.big_model_status import SourceState, resolve_source + + +class _FakeParams: + def __init__(self, **flags): + self._flags = flags + + def get_bool(self, key: str) -> bool: + return bool(self._flags.get(key, False)) + + +def _resolve(engaged=False, **flags): + return resolve_source(_FakeParams(**flags), engaged) + + +def test_no_backend_enabled_is_hidden(): + assert _resolve() == ("", SourceState.HIDDEN) + + +def test_emac_label_is_mac(): + label, _ = _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelActive=True) + assert label == "MAC" + + +def test_egpu_label_is_gpu(): + label, _ = _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuActive=True) + assert label == "GPU" + + +def test_emac_wins_when_both_enabled(): + label, _ = _resolve(IQEmacEnabled=True, IQEgpuEnabled=True, + MacModelReachable=True, UsbGpuPresent=True) + assert label == "MAC" + + +def test_egpu_states_mirror_emac(): + assert _resolve(IQEgpuEnabled=True)[1] == SourceState.HIDDEN # not present + assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True)[1] == SourceState.LOADING + assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuActive=True)[1] == SourceState.ACTIVE + assert _resolve(IQEgpuEnabled=True, UsbGpuPresent=True, UsbGpuFailed=True)[1] == SourceState.FAILED + assert _resolve(engaged=True, IQEgpuEnabled=True, UsbGpuPresent=True, + UsbGpuFailed=True)[1] == SourceState.CROSSED + + +def test_emac_states(): + assert _resolve(IQEmacEnabled=True)[1] == SourceState.HIDDEN # unreachable + assert _resolve(IQEmacEnabled=True, MacModelReachable=True)[1] == SourceState.LOADING + assert _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelActive=True)[1] == SourceState.ACTIVE + assert _resolve(IQEmacEnabled=True, MacModelReachable=True, MacModelFailed=True)[1] == SourceState.FAILED + # engaged on small because big failed -> crossed + assert _resolve(engaged=True, IQEmacEnabled=True, MacModelReachable=True, + MacModelFailed=True)[1] == SourceState.CROSSED + # active always wins over a stale failed flag + assert _resolve(IQEmacEnabled=True, MacModelReachable=True, + MacModelActive=True, MacModelFailed=True)[1] == SourceState.ACTIVE + + +def test_failed_but_disconnected_is_hidden(): + # unplugged mid-drive: nothing to show, not a red/orange "failed" + assert _resolve(IQEmacEnabled=True, MacModelReachable=False, MacModelFailed=True)[1] == SourceState.HIDDEN

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 H9v#tNe2#J# literal 402072 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