diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 631ea04840..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,18 +0,0 @@ -**/.git -.DS_Store -*.dylib -*.DSYM -*.d -*.pyc -*.pyo -.*.swp -.*.swo -.*.un~ -*.tmp -*.o -*.o-* -*.os -*.os-* - -venv/ -.venv/ diff --git a/.gitattributes b/.gitattributes index efbe60f990..507eb73c13 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,4 +11,4 @@ *.wav filter=lfs diff=lfs merge=lfs -text selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text -system/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text +common/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text diff --git a/.gitmodules b/.gitmodules index 5c5d72a7dc..e2088276e2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,7 +6,7 @@ url = https://github.com/sunnypilot/opendbc.git [submodule "msgq"] path = msgq_repo - url = https://github.com/commaai/msgq.git + url = https://github.com/sunnypilot/msgq.git [submodule "rednose_repo"] path = rednose_repo url = https://github.com/commaai/rednose.git diff --git a/Dockerfile.openpilot b/Dockerfile.openpilot deleted file mode 100644 index 72d874b022..0000000000 --- a/Dockerfile.openpilot +++ /dev/null @@ -1,38 +0,0 @@ -FROM ubuntu:24.04 - -ENV PYTHONUNBUFFERED=1 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends sudo tzdata locales && \ - rm -rf /var/lib/apt/lists/* - -RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen -ENV LANG=en_US.UTF-8 -ENV LANGUAGE=en_US:en -ENV LC_ALL=en_US.UTF-8 - -ENV NVIDIA_VISIBLE_DEVICES=all -ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute - -ARG USER=batman -ARG USER_UID=1001 -RUN useradd -m -s /bin/bash -u $USER_UID $USER -RUN usermod -aG sudo $USER -RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -USER $USER - -ENV OPENPILOT_PATH=/home/$USER/openpilot -RUN mkdir -p ${OPENPILOT_PATH} -WORKDIR ${OPENPILOT_PATH} - -COPY --chown=$USER . ${OPENPILOT_PATH}/ - -ENV UV_BIN="/home/$USER/.local/bin/" -ENV VIRTUAL_ENV=${OPENPILOT_PATH}/.venv -ENV PATH="$UV_BIN:$VIRTUAL_ENV/bin:$PATH" -RUN tools/setup_dependencies.sh && \ - sudo rm -rf /var/lib/apt/lists/* - -USER root -RUN git config --global --add safe.directory '*' diff --git a/Jenkinsfile b/Jenkinsfile index a7f46ce17a..90f86b1967 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ def retryWithDelay(int maxRetries, int delay, Closure body) { def device(String ip, String step_label, String cmd) { withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) { def ssh_cmd = """ -ssh -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END' +ssh -o ControlMaster=auto -o ControlPath=/tmp/ssh_control_%C -o ControlPersist=yes -o ConnectTimeout=5 -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o BatchMode=yes -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' exec /usr/bin/bash <<'END' set -e @@ -210,7 +210,7 @@ node { 'HW + Unit Tests': { deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), - step("test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"), + step("test power draw", "pytest -s selfdrive/test//test_power_draw.py"), step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py", [diffPaths: ["system/loggerd/"]]), step("test manager", "pytest system/manager/test/test_manager.py"), ]) @@ -246,7 +246,7 @@ node { step("build openpilot", "cd system/manager && ./build.py"), step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), step("test pandad spi", "pytest selfdrive/pandad/tests/test_pandad_spi.py"), - step("test amp", "pytest system/hardware/tici/tests/test_amplifier.py"), + step("test amp", "pytest common/hardware/tici/tests/test_amplifier.py"), ]) }, diff --git a/SConstruct b/SConstruct index e714571d81..877351af07 100644 --- a/SConstruct +++ b/SConstruct @@ -276,6 +276,8 @@ def count_scons_nodes(nodes): if node in seen: continue seen.add(node) + if hasattr(node, 'has_builder') and node.has_builder(): + build_product_nodes.add(node) executor = node.get_executor() if executor is not None: stack += executor.get_all_prerequisites() + executor.get_all_children() @@ -284,6 +286,7 @@ def count_scons_nodes(nodes): progress_interval = 5 progress_count = 0 +build_product_nodes = set() progress_total = max(1, count_scons_nodes(env.arg2nodes(BUILD_TARGETS or [Dir('.')], env.fs.Entry))) def progress_function(node): @@ -299,3 +302,11 @@ def progress_function(node): Progress(progress_function, interval=progress_interval) AddPostAction(BUILD_TARGETS or [Dir('.')], prune_cache_dir) + +def check_build_product_size(target, source, env): + limit = 50 * 1024 * 1024 # GitHub max size + for t in target: + if hasattr(t, 'isfile') and t.isfile() and (size := os.path.getsize(t.abspath)) > limit: + raise SCons.Errors.UserError(f"{t} is {size / (1024 * 1024):.1f} MiB, exceeding the {limit / (1024 * 1024):.1f} MiB limit") +if not GetOption('extras'): + AddPostAction(list(build_product_nodes), Action(check_build_product_size, None)) diff --git a/cereal/deprecated.capnp b/cereal/deprecated.capnp index 45ce25c682..5e4b0b4de0 100644 --- a/cereal/deprecated.capnp +++ b/cereal/deprecated.capnp @@ -760,18 +760,6 @@ struct LateralLQRState @0x9024e2d790c82ade { steeringAngleDesiredDeg @6 :Float32; } -struct LateralCurvatureState @0xad9d8095c06f7c61 { - active @0 :Bool; - actualCurvature @1 :Float32; - desiredCurvature @2 :Float32; - error @3 :Float32; - p @4 :Float32; - i @5 :Float32; - f @6 :Float32; - output @7 :Float32; - saturated @8 :Bool; -} - struct LateralPlannerSolution @0x84caeca5a6b4acfe { x @0 :List(Float32); y @1 :List(Float32); diff --git a/cereal/log.capnp b/cereal/log.capnp index d2c7e2c896..baf947f672 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -818,7 +818,7 @@ struct ControlsState @0x97ff69c53601abf1 { debugState @59 :LateralDebugState; torqueState @60 :LateralTorqueState; - curvatureStateDEPRECATED @65 :Deprecated.LateralCurvatureState; + curvatureState @65 :LateralCurvatureState; lqrStateDEPRECATED @55 :Deprecated.LateralLQRState; indiStateDEPRECATED @52 :Deprecated.LateralINDIState; } @@ -867,6 +867,18 @@ struct ControlsState @0x97ff69c53601abf1 { saturated @3 :Bool; } + struct LateralCurvatureState @0xad9d8095c06f7c61 { + active @0 :Bool; + actualCurvature @1 :Float32; + desiredCurvature @2 :Float32; + error @3 :Float32; + p @4 :Float32; + i @5 :Float32; + f @6 :Float32; + output @7 :Float32; + saturated @8 :Bool; + } + deprecated :group { vEgo @0 :Float32; vEgoRaw @32 :Float32; @@ -2247,6 +2259,7 @@ struct LiveDelayData { lateralDelayEstimateStd @5 :Float32; points @4 :List(Float32); calPerc @6 :Int8; + version @7 :Int32; enum Status { unestimated @0; diff --git a/common/api/base.py b/common/api/base.py index 5cb59347ad..ddda6e6ae2 100644 --- a/common/api/base.py +++ b/common/api/base.py @@ -3,7 +3,7 @@ import os import requests import unicodedata from datetime import datetime, timedelta, UTC -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.version import get_version # name: jwt signature algorithm diff --git a/common/esim/__init__.py b/common/esim/__init__.py new file mode 100644 index 0000000000..5f83412c07 --- /dev/null +++ b/common/esim/__init__.py @@ -0,0 +1,3 @@ +from openpilot.common.esim.base import LPABase, LPAError, LPAProfileNotFoundError, Profile + +__all__ = ["LPABase", "LPAError", "LPAProfileNotFoundError", "Profile"] diff --git a/common/esim/base.py b/common/esim/base.py new file mode 100644 index 0000000000..b783a630b2 --- /dev/null +++ b/common/esim/base.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +class LPAError(RuntimeError): + pass + + +class LPAProfileNotFoundError(LPAError): + pass + + +@dataclass +class Profile: + iccid: str + nickname: str + enabled: bool + provider: str + + @property + def is_comma(self) -> bool: + return self.provider == 'Webbing' and self.iccid.startswith('8985235') + + +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 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 process_notifications(self) -> None: + pass + + @abstractmethod + def is_euicc(self) -> bool: + pass diff --git a/system/hardware/esim.py b/common/esim/esim.py similarity index 95% rename from system/hardware/esim.py rename to common/esim/esim.py index 18d84de983..8456d4dd47 100755 --- a/system/hardware/esim.py +++ b/common/esim/esim.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import argparse -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.base import LPABase, Profile +from openpilot.common.hardware import HARDWARE +from openpilot.common.esim.base import LPABase, Profile def sorted_profiles(lpa: LPABase) -> list[Profile]: diff --git a/system/hardware/tici/gsma_ci_bundle.pem b/common/esim/gsma_ci_bundle.pem similarity index 100% rename from system/hardware/tici/gsma_ci_bundle.pem rename to common/esim/gsma_ci_bundle.pem diff --git a/system/hardware/tici/lpa.py b/common/esim/lpa.py similarity index 99% rename from system/hardware/tici/lpa.py rename to common/esim/lpa.py index 618d11e5d1..4009823323 100644 --- a/system/hardware/tici/lpa.py +++ b/common/esim/lpa.py @@ -19,7 +19,7 @@ from typing import Any from pathlib import Path from openpilot.common.time_helpers import system_time_valid -from openpilot.system.hardware.base import LPABase, LPAError, LPAProfileNotFoundError, Profile +from openpilot.common.esim.base import LPABase, LPAError, LPAProfileNotFoundError, Profile GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem") diff --git a/system/hardware/__init__.py b/common/hardware/__init__.py similarity index 55% rename from system/hardware/__init__.py rename to common/hardware/__init__.py index 99079b5ef3..3387d8a0ca 100644 --- a/system/hardware/__init__.py +++ b/common/hardware/__init__.py @@ -1,9 +1,9 @@ import os from typing import cast -from openpilot.system.hardware.base import HardwareBase -from openpilot.system.hardware.tici.hardware import Tici -from openpilot.system.hardware.pc.hardware import Pc +from openpilot.common.hardware.base import HardwareBase +from openpilot.common.hardware.tici.hardware import Tici +from openpilot.common.hardware.pc.hardware import Pc TICI = os.path.isfile('/TICI') AGNOS = os.path.isfile('/AGNOS') diff --git a/system/hardware/base.h b/common/hardware/base.h similarity index 77% rename from system/hardware/base.h rename to common/hardware/base.h index 3eded659ac..3cb8add783 100644 --- a/system/hardware/base.h +++ b/common/hardware/base.h @@ -12,8 +12,6 @@ class HardwareNone { public: static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } - static int get_voltage() { return 0; } - static int get_current() { return 0; } static std::string get_serial() { return "cccccc"; } @@ -24,6 +22,4 @@ public: static void set_ir_power(int percentage) {} static bool PC() { return false; } - static bool TICI() { return false; } - static bool AGNOS() { return false; } }; diff --git a/system/hardware/base.py b/common/hardware/base.py similarity index 75% rename from system/hardware/base.py rename to common/hardware/base.py index 5bcf886ae8..bf6523db77 100644 --- a/system/hardware/base.py +++ b/common/hardware/base.py @@ -3,27 +3,11 @@ from abc import abstractmethod, ABC from dataclasses import dataclass, fields from cereal import log +from openpilot.common.esim.base import LPABase 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 - - @property - def is_comma(self) -> bool: - return self.provider == 'Webbing' and self.iccid.startswith('8985235') - @dataclass class ThermalZone: # a zone from /sys/class/thermal/thermal_zone* @@ -70,38 +54,6 @@ class ThermalConfig: 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 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 process_notifications(self) -> None: - pass - - @abstractmethod - def is_euicc(self) -> bool: - pass - class HardwareBase(ABC): @staticmethod def get_cmdline() -> dict[str, str]: @@ -133,7 +85,7 @@ class HardwareBase(ABC): def get_device_type(self): pass - def get_imei(self, slot) -> str: + def get_imei(self) -> str: return "" def get_serial(self): @@ -190,21 +142,12 @@ class HardwareBase(ABC): 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 get_networks(self): - return None - - def has_internal_panda(self) -> bool: - return False - def reset_internal_panda(self): pass @@ -214,11 +157,5 @@ class HardwareBase(ABC): 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/system/hardware/hw.h b/common/hardware/hw.h similarity index 92% rename from system/hardware/hw.h rename to common/hardware/hw.h index c9af7e8a33..7681479ccc 100644 --- a/system/hardware/hw.h +++ b/common/hardware/hw.h @@ -2,14 +2,14 @@ #include -#include "system/hardware/base.h" +#include "common/hardware/base.h" #include "common/util.h" #if __TICI__ -#include "system/hardware/tici/hardware.h" +#include "common/hardware/tici/hardware.h" #define Hardware HardwareTici #else -#include "system/hardware/pc/hardware.h" +#include "common/hardware/pc/hardware.h" #define Hardware HardwarePC #endif diff --git a/system/hardware/hw.py b/common/hardware/hw.py similarity index 98% rename from system/hardware/hw.py rename to common/hardware/hw.py index 3527aac872..245856fa16 100644 --- a/system/hardware/hw.py +++ b/common/hardware/hw.py @@ -2,7 +2,7 @@ import os import platform from pathlib import Path -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" diff --git a/selfdrive/debug/__init__.py b/common/hardware/pc/__init__.py similarity index 100% rename from selfdrive/debug/__init__.py rename to common/hardware/pc/__init__.py diff --git a/system/hardware/pc/hardware.h b/common/hardware/pc/hardware.h similarity index 63% rename from system/hardware/pc/hardware.h rename to common/hardware/pc/hardware.h index 71f58b188b..2c43b43292 100644 --- a/system/hardware/pc/hardware.h +++ b/common/hardware/pc/hardware.h @@ -2,13 +2,11 @@ #include -#include "system/hardware/base.h" +#include "common/hardware/base.h" class HardwarePC : public HardwareNone { public: static std::string get_name() { return "pc"; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::PC; } static bool PC() { return true; } - static bool TICI() { return util::getenv("TICI", 0) == 1; } - static bool AGNOS() { return util::getenv("TICI", 0) == 1; } }; diff --git a/common/hardware/pc/hardware.py b/common/hardware/pc/hardware.py new file mode 100644 index 0000000000..b387f03127 --- /dev/null +++ b/common/hardware/pc/hardware.py @@ -0,0 +1,10 @@ +from cereal import log +from openpilot.common.hardware.base import HardwareBase + +class Pc(HardwareBase): + def get_device_type(self): + return "pc" + + def get_network_type(self): + # some stuff is gated on wifi, so just assume for now + return log.DeviceState.NetworkType.wifi diff --git a/common/hardware/tici/__init__.py b/common/hardware/tici/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/hardware/tici/__init__.py @@ -0,0 +1 @@ + diff --git a/common/hardware/tici/agnos.json b/common/hardware/tici/agnos.json new file mode 100644 index 0000000000..07e2079ec9 --- /dev/null +++ b/common/hardware/tici/agnos.json @@ -0,0 +1,84 @@ +[ + { + "name": "xbl", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", + "hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", + "hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", + "size": 3282256, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3" + }, + { + "name": "xbl_config", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", + "hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", + "hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", + "size": 98124, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d" + }, + { + "name": "abl", + "url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", + "hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", + "hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", + "size": 274432, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c" + }, + { + "name": "aop", + "url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", + "hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", + "hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", + "size": 184364, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3" + }, + { + "name": "devcfg", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", + "hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", + "hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", + "size": 40336, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3" + }, + { + "name": "boot", + "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", + "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "size": 17487872, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" + }, + { + "name": "system", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", + "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", + "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "size": 4718592000, + "sparse": true, + "full_check": false, + "has_ab": true, + "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", + "alt": { + "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", + "size": 4718592000 + } + } +] \ No newline at end of file diff --git a/system/hardware/tici/agnos.py b/common/hardware/tici/agnos.py similarity index 81% rename from system/hardware/tici/agnos.py rename to common/hardware/tici/agnos.py index f5261953d5..c5ca7efb4a 100755 --- a/system/hardware/tici/agnos.py +++ b/common/hardware/tici/agnos.py @@ -10,10 +10,7 @@ from collections.abc import Generator import requests -import openpilot.system.updated.casync.casync as casync - SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') -CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/" AGNOS_MANIFEST_FILE = "system/hardware/tici/agnos.json" @@ -187,50 +184,6 @@ def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog) os.sync() -def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): - path = get_partition_path(target_slot_number, partition) - seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a') - - target = casync.parse_caibx(partition['casync_caibx']) - - sources: list[tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] - - # First source is the current partition. - try: - raw_hash = get_raw_hash(seed_path, partition['size']) - caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" - - try: - cloudlog.info(f"casync fetching {caibx_url}") - sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))] - except requests.RequestException: - cloudlog.error(f"casync failed to load {caibx_url}") - except Exception: - cloudlog.exception("casync failed to hash seed partition") - - # Second source is the target partition, this allows for resuming - sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] - - # Finally we add the remote source to download any missing chunks - sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))] - - last_p = 0 - - def progress(cur): - nonlocal last_p - p = int(cur / partition['size'] * 100) - if p != last_p: - last_p = p - print(f"Installing {partition['name']}: {p}", flush=True) - - stats = casync.extract(target, sources, path, progress) - cloudlog.error(f'casync done {json.dumps(stats)}') - - os.sync() - if not verify_partition(target_slot_number, partition, force_full_check=True): - raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") - - def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False): cloudlog.info(f"Downloading and writing {partition['name']}") @@ -245,10 +198,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalo path = get_partition_path(target_slot_number, partition) - if ('casync_caibx' in partition) and not standalone: - extract_casync_image(target_slot_number, partition, cloudlog) - else: - extract_compressed_image(target_slot_number, partition, cloudlog) + extract_compressed_image(target_slot_number, partition, cloudlog) # Write hash after successful flash if not full_check: diff --git a/system/hardware/tici/all-partitions.json b/common/hardware/tici/all-partitions.json similarity index 100% rename from system/hardware/tici/all-partitions.json rename to common/hardware/tici/all-partitions.json diff --git a/system/hardware/tici/amplifier.py b/common/hardware/tici/amplifier.py similarity index 100% rename from system/hardware/tici/amplifier.py rename to common/hardware/tici/amplifier.py diff --git a/system/hardware/tici/hardware.h b/common/hardware/tici/hardware.h similarity index 86% rename from system/hardware/tici/hardware.h rename to common/hardware/tici/hardware.h index a06ce43b35..a1a8090c35 100644 --- a/system/hardware/tici/hardware.h +++ b/common/hardware/tici/hardware.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -8,7 +7,7 @@ #include // for std::clamp #include "common/util.h" -#include "system/hardware/base.h" +#include "common/hardware/base.h" class HardwareTici : public HardwareNone { public: @@ -22,7 +21,6 @@ public: static cereal::InitData::DeviceType get_device_type() { static const std::map device_map = { - {"tici", cereal::InitData::DeviceType::TICI}, {"tizi", cereal::InitData::DeviceType::TIZI}, {"mici", cereal::InitData::DeviceType::MICI} }; @@ -31,9 +29,6 @@ public: return it->second; } - static int get_voltage() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/in1_input").c_str()); } - static int get_current() { return std::atoi(util::read_file("/sys/class/hwmon/hwmon1/curr1_input").c_str()); } - static std::string get_serial() { static std::string serial(""); if (serial.empty()) { @@ -54,8 +49,7 @@ public: static void set_ir_power(int percent) { auto device = get_device_type(); - if (device == cereal::InitData::DeviceType::TICI || - device == cereal::InitData::DeviceType::TIZI) { + if (device == cereal::InitData::DeviceType::TIZI) { return; } diff --git a/system/hardware/tici/hardware.py b/common/hardware/tici/hardware.py similarity index 91% rename from system/hardware/tici/hardware.py rename to common/hardware/tici/hardware.py index 34c426d6e7..bec4c90423 100644 --- a/system/hardware/tici/hardware.py +++ b/common/hardware/tici/hardware.py @@ -10,11 +10,11 @@ from pathlib import Path from cereal import log from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action -from openpilot.system.hardware.base import HardwareBase, LPABase, ThermalConfig, ThermalZone -from openpilot.system.hardware.tici import iwlist -from openpilot.system.hardware.tici.lpa import TiciLPA -from openpilot.system.hardware.tici.pins import GPIO -from openpilot.system.hardware.tici.amplifier import Amplifier +from openpilot.common.esim.base import LPABase +from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone +from openpilot.common.esim.lpa import TiciLPA +from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.amplifier import Amplifier MODEM_STATE_PATH = "/dev/shm/modem" @@ -147,9 +147,7 @@ class Tici(HardwareBase): def get_sim_lpa(self) -> LPABase: return TiciLPA() - def get_imei(self, slot): - if slot != 0: - return "" + def get_imei(self): return self.get_modem_state().get('imei', '') def get_network_info(self): @@ -233,9 +231,6 @@ class Tici(HardwareBase): return super().get_network_metered(network_type) - def get_modem_version(self): - return self.get_modem_state().get('modem_version') or None - def get_modem_temperatures(self): return self.get_modem_state().get('temperatures', []) @@ -313,6 +308,9 @@ class Tici(HardwareBase): continue gov = 'ondemand' if powersave_enabled else 'performance' sudo_write(gov, f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_governor') + if not powersave_enabled: + # cap max core freq to 1689 Mhz + sudo_write('1689600', f'/sys/devices/system/cpu/cpufreq/policy{n}/scaling_max_freq') # *** IRQ config *** @@ -380,43 +378,13 @@ class Tici(HardwareBase): 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]) - except subprocess.CalledProcessException as e: + except subprocess.CalledProcessError as e: print(str(e)) - 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): ms = self.get_modem_state() return ms.get('tx_bytes', -1), ms.get('rx_bytes', -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) diff --git a/system/hardware/tici/id_rsa b/common/hardware/tici/id_rsa similarity index 100% rename from system/hardware/tici/id_rsa rename to common/hardware/tici/id_rsa diff --git a/system/hardware/tici/modem.py b/common/hardware/tici/modem.py similarity index 100% rename from system/hardware/tici/modem.py rename to common/hardware/tici/modem.py diff --git a/system/hardware/tici/pins.py b/common/hardware/tici/pins.py similarity index 100% rename from system/hardware/tici/pins.py rename to common/hardware/tici/pins.py diff --git a/system/hardware/tici/power_monitor.py b/common/hardware/tici/power_monitor.py similarity index 100% rename from system/hardware/tici/power_monitor.py rename to common/hardware/tici/power_monitor.py diff --git a/common/hardware/tici/tests/__init__.py b/common/hardware/tici/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/common/hardware/tici/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/system/hardware/tici/tests/test_agnos_updater.py b/common/hardware/tici/tests/test_agnos_updater.py similarity index 100% rename from system/hardware/tici/tests/test_agnos_updater.py rename to common/hardware/tici/tests/test_agnos_updater.py diff --git a/system/hardware/tici/tests/test_amplifier.py b/common/hardware/tici/tests/test_amplifier.py similarity index 93% rename from system/hardware/tici/tests/test_amplifier.py rename to common/hardware/tici/tests/test_amplifier.py index 9ce00c3ff2..39628b53cc 100644 --- a/system/hardware/tici/tests/test_amplifier.py +++ b/common/hardware/tici/tests/test_amplifier.py @@ -4,8 +4,8 @@ import random import subprocess from panda import Panda -from openpilot.system.hardware import TICI, HARDWARE -from openpilot.system.hardware.tici.amplifier import Amplifier +from openpilot.common.hardware import TICI, HARDWARE +from openpilot.common.hardware.tici.amplifier import Amplifier class TestAmplifier: diff --git a/system/hardware/tici/updater b/common/hardware/tici/updater similarity index 100% rename from system/hardware/tici/updater rename to common/hardware/tici/updater diff --git a/common/params.cc b/common/params.cc index 39592cb905..bed6827e5a 100644 --- a/common/params.cc +++ b/common/params.cc @@ -12,7 +12,7 @@ #include "common/queue.h" #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" namespace { diff --git a/common/params_keys.h b/common/params_keys.h index 80d4c4c503..b1a256d6a6 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -58,6 +58,7 @@ inline static std::unordered_map keys = { {"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsEngaged", {PERSISTENT, BOOL}}, {"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"IsLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsMetric", {PERSISTENT | BACKUP, BOOL}}, {"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsOnroad", {PERSISTENT, BOOL}}, @@ -81,6 +82,7 @@ inline static std::unordered_map keys = { {"LiveParameters", {PERSISTENT, JSON}}, {"LiveParametersV2", {PERSISTENT, BYTES}}, {"LivestreamEncoderBitrate", {CLEAR_ON_MANAGER_START | DONT_LOG, INT}}, + {"LivestreamRequestKeyframe", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}}, {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, {"LocationFilterInitialState", {PERSISTENT, BYTES}}, {"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, diff --git a/common/prefix.h b/common/prefix.h index de3a94d71f..89f346b9e5 100644 --- a/common/prefix.h +++ b/common/prefix.h @@ -5,7 +5,7 @@ #include "common/params.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" class OpenpilotPrefix { public: diff --git a/common/prefix.py b/common/prefix.py index d0a5f92628..d0be8997ae 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -5,9 +5,9 @@ import uuid from openpilot.common.params import Params -from openpilot.system.hardware import PC -from openpilot.system.hardware.hw import Paths -from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT +from openpilot.common.hardware import PC +from openpilot.common.hardware.hw import Paths +from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: def __init__(self, prefix: str | None = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): diff --git a/common/realtime.py b/common/realtime.py index 91d2e165e9..e0b9e8a3a5 100644 --- a/common/realtime.py +++ b/common/realtime.py @@ -7,7 +7,7 @@ import time from setproctitle import getproctitle from openpilot.common.utils import MovingAverage -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC # time step for each process diff --git a/common/swaglog.cc b/common/swaglog.cc index 8811d61d8f..9834098929 100644 --- a/common/swaglog.cc +++ b/common/swaglog.cc @@ -13,7 +13,7 @@ #include #include "json11/json11.hpp" #include "common/version.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "sunnypilot/common/version.h" diff --git a/common/swaglog.py b/common/swaglog.py index d009f00e76..ea72766fcb 100644 --- a/common/swaglog.py +++ b/common/swaglog.py @@ -8,7 +8,7 @@ from logging.handlers import BaseRotatingHandler import zmq from openpilot.common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def get_file_handler(): diff --git a/common/tests/test_swaglog.cc b/common/tests/test_swaglog.cc index 26fd33436b..9a76b78fd5 100644 --- a/common/tests/test_swaglog.cc +++ b/common/tests/test_swaglog.cc @@ -6,7 +6,7 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/version.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "json11/json11.hpp" #include "sunnypilot/common/version.h" diff --git a/conftest.py b/conftest.py index de37572317..7fde82af8b 100644 --- a/conftest.py +++ b/conftest.py @@ -5,7 +5,7 @@ import pytest from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager -from openpilot.system.hardware import TICI, HARDWARE +from openpilot.common.hardware import TICI, HARDWARE # these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml collect_ignore = [ @@ -15,9 +15,12 @@ collect_ignore = [ # which corrupts JIT captures for test_warp.py in the same process. Run separately in CI. "sunnypilot/modeld_v2/tests/test_warp.py", ] -collect_ignore_glob = [ - "selfdrive/debug/*.py", -] + + +def pytest_sessionstart(session): + # TODO: fix tests and enable test order randomization + if session.config.pluginmanager.hasplugin('randomly'): + session.config.option.randomly_reorganize = False @pytest.hookimpl(hookwrapper=True, trylast=True) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 5e7b4fa0db..46b0a5d079 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -19,12 +19,12 @@ function agnos_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - AGNOS_PY="$DIR/system/hardware/tici/agnos.py" + AGNOS_PY="$DIR/common/hardware/tici/agnos.py" MANIFEST="$DIR/system/hardware/tici/agnos.json" if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi - $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + $DIR/common/hardware/tici/updater $AGNOS_PY $MANIFEST fi } diff --git a/msgq_repo b/msgq_repo index 9beb84af67..3e3611ee2d 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 9beb84af67527f7b6bfee349dcbe3dbb8d4f4789 +Subproject commit 3e3611ee2d485ed5b87ac233ee126729f22ad79f diff --git a/pyproject.toml b/pyproject.toml index 8b995a3a7b..8c4456efaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ dependencies = [ # these should be removed "psutil", - "pycryptodome", # used in updated/casync, panda, body, and a test + "pycryptodome", # used in panda, body, and a test "setproctitle", # logreader @@ -127,7 +127,7 @@ allow-direct-references = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" +addopts = "--ignore=openpilot/ --ignore=opendbc/ --ignore=panda/ --ignore=rednose_repo/ --ignore=tinygrad_repo/ --ignore=teleoprtc_repo/ --ignore=msgq/ --ignore=selfdrive/modeld -Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" cpp_files = "test_*" cpp_harness = "selfdrive/test/cpp_harness.py" python_files = "test_*.py" diff --git a/scripts/cell.sh b/scripts/cell.sh deleted file mode 100755 index 310a9694fd..0000000000 --- a/scripts/cell.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -nmcli connection modify --temporary esim ipv4.route-metric 1 ipv6.route-metric 1 -nmcli con up esim diff --git a/scripts/disable-powersave.py b/scripts/disable-powersave.py index 367b4108b0..093275fd77 100755 --- a/scripts/disable-powersave.py +++ b/scripts/disable-powersave.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE if __name__ == "__main__": HARDWARE.set_power_save(False) diff --git a/scripts/manage-powersave.py b/scripts/manage-powersave.py index 1a82810a7f..2c736ecd69 100755 --- a/scripts/manage-powersave.py +++ b/scripts/manage-powersave.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse import multiprocessing -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE def main(): diff --git a/scripts/reporter.py b/scripts/reporter.py index 9821a47ccf..199f1fae58 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -25,7 +25,9 @@ class MetadataOnnxPBParser(OnnxPBParser): def get_checkpoint(f): model = MetadataOnnxPBParser(f).parse() metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} - return metadata['model_checkpoint'].split('/')[0] + # "" or "...//"; combined models list vision then policy + parts = metadata['model_checkpoint'].split('/') + return parts[-2] if len(parts) > 1 else parts[0] if __name__ == "__main__": @@ -37,8 +39,8 @@ if __name__ == "__main__": master_path = MASTER_PATH + MODEL_PATH + fn if os.path.exists(master_path): master = get_checkpoint(master_path) - master_col = f"[{master}](https://reporter.comma.life/experiment/{master})" + master_col = f"[{master}](https://reporter.comma.life/{master})" else: master_col = "N/A (new model)" pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|") + print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/{pr})", "|") diff --git a/scripts/stop_updater.sh b/scripts/stop_updater.sh deleted file mode 100755 index 703b363928..0000000000 --- a/scripts/stop_updater.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env sh - -# Stop updater -pkill -2 -f system.updated.updated - -# Remove pending update -rm -f /data/safe_staging/finalized/.overlay_consistent diff --git a/scripts/usb.sh b/scripts/usb.sh deleted file mode 100755 index 5796cfa028..0000000000 --- a/scripts/usb.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -# testing the GPU box - -export XDG_CACHE_HOME=/data/tinycache -mkdir -p $XDG_CACHE_HOME - -cd /data/openpilot/tinygrad_repo/examples -while true; do - AMD=1 AMD_IFACE=usb python ./beautiful_cartpole.py - sleep 1 -done diff --git a/scripts/usbgpu/benchmark.sh b/scripts/usbgpu/benchmark.sh deleted file mode 100755 index 04a76d054e..0000000000 --- a/scripts/usbgpu/benchmark.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -cd $DIR/../../tinygrad_repo - -GREEN='\033[0;32m' -NC='\033[0m' - - -#export DEBUG=2 -export PYTHONPATH=. -export AM_RESET=1 -export AMD=1 -export AMD_IFACE=USB -export AMD_LLVM=1 - -python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \ - > /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1) -printf "${GREEN}Booted in ${SECONDS}s${NC}\n" -printf "${GREEN}=============${NC}\n" - -printf "\n\n" -printf "${GREEN}Transfer speeds:${NC}\n" -printf "${GREEN}================${NC}\n" -python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds diff --git a/selfdrive/assets/icons_mici/settings/software.png b/selfdrive/assets/icons_mici/settings/software.png new file mode 100644 index 0000000000..5cf528cbdd --- /dev/null +++ b/selfdrive/assets/icons_mici/settings/software.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4c38772e6080aa4b8bf5212d3619e949775468c64f3edb88a1a426d767c38d2 +size 1579 diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index 6e13d55b29..ef6795ef80 100644 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -1,9 +1,5 @@ -import os -from openpilot.common.basedir import BASEDIR from opendbc.car.docs import generate_cars_md, get_all_car_docs -from openpilot.selfdrive.debug.dump_car_docs import dump_car_docs -from openpilot.selfdrive.debug.print_docs_diff import print_car_docs_diff from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE @@ -14,9 +10,3 @@ class TestCarDocs: def test_generator(self): generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) - - def test_docs_diff(self): - dump_path = os.path.join(BASEDIR, "selfdrive", "car", "tests", "cars_dump") - dump_car_docs(dump_path) - print_car_docs_diff(dump_path) - os.remove(dump_path) diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index d9252dd4a8..c13061c0f7 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -21,7 +21,7 @@ from opendbc.safety.tests.libsafety import libsafety_py from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.selfdrive.test.helpers import read_segment_list -from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT +from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source from openpilot.tools.lib.route import SegmentName @@ -192,7 +192,7 @@ class TestCarModelBase(unittest.TestCase): # make sure car params are within a valid range self.assertGreater(self.CP.mass, 1) - if self.CP.steerControlType != SteerControlType.angle: + if self.CP.steerControlType not in (SteerControlType.angle, SteerControlType.curvature): tuning = self.CP.lateralTuning.which() if tuning == 'pid': self.assertTrue(len(self.CP.lateralTuning.pid.kpV)) @@ -442,8 +442,9 @@ class TestCarModelBase(unittest.TestCase): v_ego_raw < (self.safety.get_vehicle_speed_min() - 1e-3)) # check steering angle for angle control cars (panda stores angle_meas in CAN units) - # ford excluded since it tracks curvature, not steering angle - if self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar and self.CP.brand != "ford": + # ford and VW MEB excluded since they track curvature, not steering angle + # TODO: add curvature check, standardize CAN units to rm brand specific ANGLE_DEG_TO_CAN + if self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar and self.CP.brand not in ("ford", "volkswagen"): angle_can = (CS.steeringAngleDeg + CS.steeringAngleOffsetDeg) * ANGLE_DEG_TO_CAN[self.CP.brand] checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or angle_can < (self.safety.get_angle_meas_min() - 1)) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index c242871125..5c0fa30e4a 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -15,6 +15,7 @@ from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD +from openpilot.selfdrive.controls.lib.latcontrol_curvature import LatControlCurvature from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS @@ -59,6 +60,8 @@ class Controls(ControlsExt): self.LaC: LatControl if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.LaC = LatControlAngle(self.CP, self.CP_SP, self.CI, DT_CTRL) + elif self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + self.LaC = LatControlCurvature(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'pid': self.LaC = LatControlPID(self.CP, self.CP_SP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'torque': @@ -143,11 +146,14 @@ class Controls(ControlsExt): lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS actuators.curvature = self.desired_curvature - steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, - self.steer_limited_by_safety, self.desired_curvature, - self.calibrated_pose, curvature_limited, lat_delay) + steer, lateral_output, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, + self.steer_limited_by_safety, self.desired_curvature, + self.calibrated_pose, curvature_limited, lat_delay) actuators.torque = float(steer) - actuators.steeringAngleDeg = float(steeringAngleDeg) + if self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + actuators.curvature = float(lateral_output) + else: + actuators.steeringAngleDeg = float(lateral_output) # Ensure no NaNs/Infs for p in ACTUATOR_FIELDS: attr = getattr(actuators, p) @@ -215,9 +221,14 @@ class Controls(ControlsExt): cs.forceDecel = bool((self.sm['driverMonitoringState'].alertLevel == log.DriverMonitoringState.AlertLevel.three) or (self.sm['selfdriveState'].state == State.softDisabling)) + # trigger the car's stock driver monitoring escalation + CC.driverMonitoringEscalation = cs.forceDecel + lat_tuning = self.CP.lateralTuning.which() if self.CP.steerControlType == car.CarParams.SteerControlType.angle: cs.lateralControlState.angleState = lac_log + elif self.CP.steerControlType == car.CarParams.SteerControlType.curvature: + cs.lateralControlState.curvatureState = lac_log elif lat_tuning == 'pid': cs.lateralControlState.pidState = lac_log elif lat_tuning == 'torque': diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 16908fa3e3..f558f80d65 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -45,7 +45,6 @@ class DesireHelper: self.lane_change_direction = LaneChangeDirection.none self.lane_change_timer = 0.0 self.lane_change_ll_prob = 1.0 - self.keep_pulse_timer = 0.0 self.prev_one_blinker = False self.desire = log.Desire.none self.alc = AutoLaneChangeController(self) @@ -132,14 +131,4 @@ class DesireHelper: else: self.desire = DESIRES[self.lane_change_direction][self.lane_change_state] - # Send keep pulse once per second during LaneChangeStart.preLaneChange - if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting): - self.keep_pulse_timer = 0.0 - elif self.lane_change_state == LaneChangeState.preLaneChange: - self.keep_pulse_timer += DT_MDL - if self.keep_pulse_timer > 1.0: - self.keep_pulse_timer = 0.0 - elif self.desire in (log.Desire.keepLeft, log.Desire.keepRight): - self.desire = log.Desire.none - self.alc.update_state() diff --git a/selfdrive/controls/lib/latcontrol_curvature.py b/selfdrive/controls/lib/latcontrol_curvature.py new file mode 100644 index 0000000000..0f4e45687e --- /dev/null +++ b/selfdrive/controls/lib/latcontrol_curvature.py @@ -0,0 +1,58 @@ +import math + +from cereal import log +from openpilot.common.pid import PIDController +from openpilot.selfdrive.controls.lib.latcontrol import LatControl +from openpilot.selfdrive.controls.lib.drive_helpers import MAX_CURVATURE + +CURVATURE_SATURATION_THRESHOLD = 1e-3 # 1/m + + +class LatControlCurvature(LatControl): + def __init__(self, CP, CP_SP, CI, dt): + super().__init__(CP, CP_SP, CI, dt) + self.sat_check_min_speed = 5. + if CP.lateralTuning.which() == 'pid': + ct = CP.lateralTuning.pid + self.pid = PIDController((ct.kpBP, ct.kpV), (ct.kiBP, ct.kiV), + pos_limit=MAX_CURVATURE, neg_limit=-MAX_CURVATURE, rate=1 / dt) + self.kf = ct.kf + else: + self.pid = None + self.kf = 1. + + def reset(self): + super().reset() + if self.pid is not None: + self.pid.reset() + + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited, lat_delay): + curvature_log = log.ControlsState.LateralCurvatureState.new_message() + actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + error = desired_curvature - actual_curvature + + if not active: + output_curvature = 0.0 + curvature_log.active = False + if self.pid is not None: + self.pid.reset() + elif self.pid is None or CS.steeringPressed: + # no PID or override: feedforward only + if self.pid is not None: + self.pid.reset() + output_curvature = self.kf * desired_curvature + curvature_log.active = True + else: + output_curvature = self.pid.update(error, speed=CS.vEgo, feedforward=self.kf * desired_curvature) + curvature_log.p = float(self.pid.p) + curvature_log.i = float(self.pid.i) + curvature_log.f = float(self.pid.f) + curvature_log.active = True + + curvature_log.error = float(error) + curvature_log.actualCurvature = float(actual_curvature) + curvature_log.desiredCurvature = float(desired_curvature) + curvature_log.output = float(output_curvature) + curvature_log.saturated = bool(self._check_saturation(abs(error) > CURVATURE_SATURATION_THRESHOLD, CS, + False, curvature_limited)) + return 0.0, float(output_curvature), curvature_log diff --git a/selfdrive/debug/README.md b/selfdrive/debug/README.md deleted file mode 100644 index 83b8a994db..0000000000 --- a/selfdrive/debug/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# debug scripts - -## [can_printer.py](can_printer.py) - -``` -usage: can_printer.py [-h] [--bus BUS] [--max_msg MAX_MSG] [--addr ADDR] - -simple CAN data viewer - -optional arguments: - -h, --help show this help message and exit - --bus BUS CAN bus to print out (default: 0) - --max_msg MAX_MSG max addr (default: None) - --addr ADDR -``` - -## [dump.py](dump.py) - -``` -usage: dump.py [-h] [--pipe] [--raw] [--json] [--dump-json] [--no-print] [--addr ADDR] [--values VALUES] [socket [socket ...]] - -Dump communication sockets. See cereal/services.py for a complete list of available sockets. - -positional arguments: - socket socket names to dump. defaults to all services defined in cereal - -optional arguments: - -h, --help show this help message and exit - --pipe - --raw - --json - --dump-json - --no-print - --addr ADDR - --values VALUES values to monitor (instead of entire event) -``` - -## [vw_mqb_config.py](vw_mqb_config.py) - -``` -usage: vw_mqb_config.py [-h] [--debug] {enable,show,disable} - -Shows Volkswagen EPS software and coding info, and enables or disables Heading Control -Assist (Lane Assist). Useful for enabling HCA on cars without factory Lane Assist that want -to use openpilot integrated at the CAN gateway (J533). - -positional arguments: - {enable,show,disable} - show or modify current EPS HCA config - -optional arguments: - -h, --help show this help message and exit - --debug enable ISO-TP/UDS stack debugging output - -This tool is meant to run directly on a vehicle-installed comma three, with -the openpilot/tmux processes stopped. It should also work on a separate PC with a USB- -attached comma panda. Vehicle ignition must be on. Recommend engine not be running when -making changes. Must turn ignition off and on again for any changes to take effect. -``` diff --git a/selfdrive/debug/analyze-msg-size.py b/selfdrive/debug/analyze-msg-size.py deleted file mode 100755 index 69015a6be2..0000000000 --- a/selfdrive/debug/analyze-msg-size.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -import argparse -from tqdm import tqdm - -from cereal.services import SERVICE_LIST, QueueSize -from openpilot.tools.lib.logreader import LogReader - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Analyze message sizes from a log route") - parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255", - help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)") - args = parser.parse_args() - - lr = LogReader(args.route) - - szs = {} - for msg in tqdm(lr): - sz = len(msg.as_builder().to_bytes()) - msg_type = msg.which() - if msg_type not in szs: - szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1} - else: - szs[msg_type]['min'] = min(szs[msg_type]['min'], sz) - szs[msg_type]['max'] = max(szs[msg_type]['max'], sz) - szs[msg_type]['sum'] += sz - szs[msg_type]['count'] += 1 - - print() - print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}") - print("-" * 132) - def sort_key(x): - k, v = x - avg = v['sum'] / v['count'] - freq = SERVICE_LIST.get(k, None) - freq_val = freq.frequency if freq else 0.0 - kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 - return kb_per_min - total_kb_per_min = 0.0 - RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default - for k, v in sorted(szs.items(), key=sort_key, reverse=True): - avg = v['sum'] / v['count'] - service = SERVICE_LIST.get(k, None) - freq_val = service.frequency if service else 0.0 - queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL - kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 - kb_per_sec = kb_per_min / 60 - minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf') - seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf') - total_kb_per_min += kb_per_min - min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf" - sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf" - print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}") - - # Summary section - print() - print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min") - - # Calculate memory usage: old (10MB for all) vs new (from services.py) - OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default - old_total = len(SERVICE_LIST) * OLD_SIZE - - new_total = sum(s.queue_size for s in SERVICE_LIST.values()) - - # Count by queue size - size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0} - for s in SERVICE_LIST.values(): - size_counts[s.queue_size] += 1 - - savings_pct = (1 - new_total / old_total) * 100 - - print() - print(f"{'Queue Size Comparison':<40}") - print("-" * 60) - print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB") - print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB") - print(f"{'Savings:':<30} {savings_pct:>10.1f}%") - print() - print(f"{'Breakdown:':<30}") - print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services") - print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services") - print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services") diff --git a/selfdrive/debug/check_can_parser_performance.py b/selfdrive/debug/check_can_parser_performance.py deleted file mode 100755 index 20987b3cf4..0000000000 --- a/selfdrive/debug/check_can_parser_performance.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -import time -from tqdm import tqdm - -from cereal import car -from opendbc.car.tests.routes import CarTestRoute -from openpilot.selfdrive.car.tests.test_models import TestCarModelBase -from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE - -N_RUNS = 10 - - -class CarModelTestCase(TestCarModelBase): - test_route = CarTestRoute(DEMO_ROUTE, None) - - -if __name__ == '__main__': - # Get CAN messages and parsers - tm = CarModelTestCase() - tm.setUpClass() - tm.setUp() - - CC = car.CarControl.new_message() - ets = [] - for _ in tqdm(range(N_RUNS)): - start_t = time.process_time_ns() - for msg in tm.can_msgs: - for cp in tm.CI.can_parsers.values(): - if cp is not None: - cp.update_strings(msg) - ets.append((time.process_time_ns() - start_t) * 1e-6) - - print(f'{len(tm.can_msgs)} CAN packets, {N_RUNS} runs') - print(f'{np.mean(ets):.2f} mean ms, {max(ets):.2f} max ms, {min(ets):.2f} min ms, {np.std(ets):.2f} std ms') - print(f'{np.mean(ets) / len(tm.can_msgs):.4f} mean ms / CAN packet') diff --git a/selfdrive/debug/check_freq.py b/selfdrive/debug/check_freq.py deleted file mode 100755 index 1765aeb86b..0000000000 --- a/selfdrive/debug/check_freq.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import numpy as np -import time -from collections import defaultdict, deque -from collections.abc import MutableSequence - -import cereal.messaging as messaging - - -if __name__ == "__main__": - context = messaging.Context() - poller = messaging.Poller() - - parser = argparse.ArgumentParser() - parser.add_argument("socket", type=str, nargs='*', help="socket name") - args = parser.parse_args() - - socket_names = args.socket - sockets = {} - - rcv_times: defaultdict[str, MutableSequence[float]] = defaultdict(lambda: deque(maxlen=100)) - valids: defaultdict[str, deque[bool]] = defaultdict(lambda: deque(maxlen=100)) - - t = time.monotonic() - for name in socket_names: - sock = messaging.sub_sock(name, poller=poller) - sockets[sock] = name - - prev_print = t - while True: - for socket in poller.poll(100): - msg = messaging.recv_one(socket) - if msg is None: - continue - - name = msg.which() - - t = time.monotonic() - rcv_times[name].append(msg.logMonoTime / 1e9) - valids[name].append(msg.valid) - - if t - prev_print > 1: - print() - for name in socket_names: - dts = np.diff(rcv_times[name]) - mean = np.mean(dts) - print(f"{name}: Freq {1.0 / mean:.2f} Hz, Min {np.min(dts) / mean * 100:.2f}%, Max {np.max(dts) / mean * 100:.2f}%, valid ", all(valids[name])) - - prev_print = t diff --git a/selfdrive/debug/check_lag.py b/selfdrive/debug/check_lag.py deleted file mode 100755 index 341ae79c8b..0000000000 --- a/selfdrive/debug/check_lag.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -import cereal.messaging as messaging -from cereal.services import SERVICE_LIST - -TO_CHECK = ['carState'] - - -if __name__ == "__main__": - sm = messaging.SubMaster(TO_CHECK) - - prev_t: dict[str, float] = {} - - while True: - sm.update() - - for s in TO_CHECK: - if sm.updated[s]: - t = sm.logMonoTime[s] / 1e9 - - if s in prev_t: - expected = 1.0 / (SERVICE_LIST[s].frequency) - dt = t - prev_t[s] - if dt > 10 * expected: - print(t, s, dt) - - prev_t[s] = t diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py deleted file mode 100755 index fc527cd81b..0000000000 --- a/selfdrive/debug/check_timings.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -import sys -import time -import numpy as np -import datetime -from collections.abc import MutableSequence -from collections import defaultdict - -import cereal.messaging as messaging - - -if __name__ == "__main__": - ts: defaultdict[str, MutableSequence[float]] = defaultdict(list) - socks = {s: messaging.sub_sock(s, conflate=False) for s in sys.argv[1:]} - try: - st = time.monotonic() - while True: - print() - for s, sock in socks.items(): - msgs = messaging.drain_sock(sock) - for m in msgs: - ts[s].append(m.logMonoTime / 1e6) - - if len(ts[s]) > 2: - d = np.diff(ts[s])[-100:] - print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}") - time.sleep(1) - except KeyboardInterrupt: - print("\n") - print("="*5, "timing summary", "="*5) - for s, sock in socks.items(): - msgs = messaging.drain_sock(sock) - if len(ts[s]) > 2: - d = np.diff(ts[s]) - print(f"{s:25} {np.mean(d):7.2f} {np.std(d):7.2f} {np.max(d):7.2f} {np.min(d):7.2f}") - print("="*5, datetime.timedelta(seconds=time.monotonic()-st), "="*5) diff --git a/selfdrive/debug/dump_car_docs.py b/selfdrive/debug/dump_car_docs.py deleted file mode 100755 index f0e99cda24..0000000000 --- a/selfdrive/debug/dump_car_docs.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import pickle - -from opendbc.car.docs import get_all_car_docs - - -def dump_car_docs(path): - with open(path, 'wb') as f: - pickle.dump(get_all_car_docs(), f) - print(f'Dumping car info to {path}') - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", required=True) - args = parser.parse_args() - dump_car_docs(args.path) diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py deleted file mode 100755 index c7850939f0..0000000000 --- a/selfdrive/debug/print_docs_diff.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -import argparse -from collections import defaultdict -import difflib -import pickle - -from opendbc.car.docs import get_all_car_docs -from opendbc.car.docs_definitions import Column - -FOOTNOTE_TAG = "{}" -STAR_ICON = '' -VIDEO_ICON = '' + \ - '' -COLUMNS = "|" + "|".join([column.value for column in Column]) + "|" -COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3))) -ARROW_SYMBOL = "➡️" - - -def load_base_car_docs(path): - with open(path, "rb") as f: - return pickle.load(f) - - -def match_cars(base_cars, new_cars): - changes = [] - additions = [] - for new in new_cars: - # Addition if no close matches or close match already used - # Change if close match and not already used - matches = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.) - if not len(matches) or matches[0] in [c[1].name for c in changes]: - additions.append(new) - else: - changes.append((new, next(car for car in base_cars if car.name == matches[0]))) - - # Removal if base car not in changes - removals = [b for b in base_cars if b.name not in [c[1].name for c in changes]] - return changes, additions, removals - - -def build_column_diff(base_car, new_car): - row_builder = [] - for column in Column: - base_column = base_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) - new_column = new_car.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) - - if base_column != new_column: - row_builder.append(f"{base_column} {ARROW_SYMBOL} {new_column}") - else: - row_builder.append(new_column) - - return format_row(row_builder) - - -def format_row(builder): - return "|" + "|".join(builder) + "|" - - -def print_car_docs_diff(path): - base_car_docs = defaultdict(list) - new_car_docs = defaultdict(list) - - for car in load_base_car_docs(path): - base_car_docs[car.car_fingerprint].append(car) - for car in get_all_car_docs(): - new_car_docs[car.car_fingerprint].append(car) - - # Add new platforms to base cars so we can detect additions and removals in one pass - base_car_docs.update({car: [] for car in new_car_docs if car not in base_car_docs}) - - changes = defaultdict(list) - for base_car_model, base_cars in base_car_docs.items(): - # Match car info changes, and get additions and removals - new_cars = new_car_docs[base_car_model] - car_changes, car_additions, car_removals = match_cars(base_cars, new_cars) - - # Removals - for car_docs in car_removals: - changes["removals"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column])) - - # Additions - for car_docs in car_additions: - changes["additions"].append(format_row([car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in Column])) - - for new_car, base_car in car_changes: - # Column changes - row_diff = build_column_diff(base_car, new_car) - if ARROW_SYMBOL in row_diff: - changes["column"].append(row_diff) - - # Detail sentence changes - if base_car.detail_sentence != new_car.detail_sentence: - changes["detail"].append(f"- Sentence for {base_car.name} changed!\n" + - " ```diff\n" + - f" - {base_car.detail_sentence}\n" + - f" + {new_car.detail_sentence}\n" + - " ```") - - # Print diff - if any(len(c) for c in changes.values()): - markdown_builder = ["### ⚠️ This PR makes changes to [CARS.md](../blob/master/docs/CARS.md) ⚠️"] - - for title, category in (("## 🔀 Column Changes", "column"), ("## ❌ Removed", "removals"), - ("## ➕ Added", "additions"), ("## 📖 Detail Sentence Changes", "detail")): - if len(changes[category]): - markdown_builder.append(title) - if category not in ("detail",): - markdown_builder.append(COLUMNS) - markdown_builder.append(COLUMN_HEADER) - markdown_builder.extend(changes[category]) - - print("\n".join(markdown_builder)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", required=True) - args = parser.parse_args() - print_car_docs_diff(args.path) diff --git a/selfdrive/debug/touch_replay.py b/selfdrive/debug/touch_replay.py deleted file mode 100755 index 6e5ecbbe5b..0000000000 --- a/selfdrive/debug/touch_replay.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -import argparse - -import numpy as np -import matplotlib.pyplot as plt - -from openpilot.tools.lib.logreader import LogReader - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--width', default=2160, type=int) - parser.add_argument('--height', default=1080, type=int) - parser.add_argument('--route', default='rlog', type=str) - args = parser.parse_args() - - w = args.width - h = args.height - route = args.route - - fingers = [[-1, -1]] * 5 - touch_points = [] - current_slot = 0 - - lr = list(LogReader(route)) - for msg in lr: - if msg.which() == 'touch': - for event in msg.touch: - if event.type == 3 and event.code == 47: - current_slot = event.value - elif event.type == 3 and event.code == 57 and event.value == -1: - fingers[current_slot] = [-1, -1] - elif event.type == 3 and event.code == 53: - fingers[current_slot][1] = event.value - if fingers[current_slot][0] != -1: - touch_points.append(fingers[current_slot].copy()) - elif event.type == 3 and event.code == 54: - fingers[current_slot][0] = w - event.value - if fingers[current_slot][1] != -1: - touch_points.append(fingers[current_slot].copy()) - - if not touch_points: - print(f'No touch events found for {route}') - quit() - - unique_points, counts = np.unique(touch_points, axis=0, return_counts=True) - - plt.figure(figsize=(10, 3)) - plt.scatter(unique_points[:, 0], unique_points[:, 1], c=counts, s=counts * 20, edgecolors='red') - plt.colorbar() - plt.title(f'Touches for {route}') - plt.xlim(0, w) - plt.ylim(0, h) - plt.grid(True) - plt.show() diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 4423f39061..c444d43983 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -13,7 +13,7 @@ from typing import NoReturn from cereal import log, car import cereal.messaging as messaging -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index 2980e61e5e..a9377f3366 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -8,6 +8,7 @@ from functools import partial import cereal.messaging as messaging from cereal import car, log from cereal.services import SERVICE_LIST +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.swaglog import cloudlog @@ -20,7 +21,7 @@ BLOCK_NUM_NEEDED = 5 MOVING_WINDOW_SEC = 60.0 MIN_OKAY_WINDOW_SEC = 25.0 MIN_RECOVERY_BUFFER_SEC = 2.0 -MIN_VEGO = 15.0 +MIN_VEGO = 50.0 * CV.MPH_TO_MS MIN_ABS_YAW_RATE = 0.0 MAX_YAW_RATE_SANITY_CHECK = 1.0 MIN_NCC = 0.95 @@ -36,6 +37,8 @@ LAG_CANDIDATE_CORR_THRESHOLD = 0.9 SMOOTH_K = 5 SMOOTH_SIGMA = 1.0 +VERSION = 1 # bump this to invalidate old parameter caches + def masked_symmetric_moving_average(x: np.ndarray, mask: np.ndarray, k: int, sigma: float) -> np.ndarray: assert k >= 1 and k % 2 == 1, "k must be positive and odd" @@ -248,6 +251,7 @@ class LateralLagEstimator: (self.min_valid_block_count * self.block_size), 100) if debug: liveDelay.points = self.block_avg.values.flatten().tolist() + liveDelay.version = VERSION return msg @@ -368,9 +372,10 @@ def retrieve_initial_lag(params: Params, CP: car.CarParams): if last_CP.carFingerprint != CP.carFingerprint: raise Exception("Car model mismatch") - lag, valid_blocks, status = ld.lateralDelayEstimate, ld.validBlocks, ld.status + lag, valid_blocks, status, version = ld.lateralDelayEstimate, ld.validBlocks, ld.status, ld.version assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks" assert status != log.LiveDelayData.Status.invalid, "Lag estimate is invalid" + assert version == VERSION, f"Lag estimate is from a different version (got {version}, expected {VERSION})" return lag, valid_blocks except Exception as e: cloudlog.error(f"Failed to retrieve initial lag: {e}") diff --git a/selfdrive/locationd/locationd.py b/selfdrive/locationd/locationd.py index 57aecb22e7..41b80a1c41 100755 --- a/selfdrive/locationd/locationd.py +++ b/selfdrive/locationd/locationd.py @@ -206,9 +206,11 @@ class LocationEstimator: self._finite_check(t, new_x, new_P) return HandleLogResult.SUCCESS - def get_msg(self, sensors_valid: bool, inputs_valid: bool, filter_valid: bool): + def get_msg(self, sensors_valid: bool, inputs_valid: bool, filter_initialized: bool): state, cov = self.kf.x, self.kf.P std = np.sqrt(np.diag(cov)) + filter_time_valid = bool(np.isfinite(self.kf.t)) + filter_valid = filter_initialized and filter_time_valid 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] diff --git a/selfdrive/locationd/test/test_lagd.py b/selfdrive/locationd/test/test_lagd.py index 746f7ad478..63ea2b5fab 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -5,18 +5,18 @@ import pytest from cereal import messaging, log, car from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \ - BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC + BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC, VERSION from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE from openpilot.common.params import Params from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC MAX_ERR_FRAMES = 1 DT = 0.05 -def process_messages(estimator, lag_frames, n_frames, vego=20.0, rejection_threshold=0.0): +def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_threshold=0.0): for i in range(n_frames): t = i * estimator.dt desired_la = np.cos(10 * t) * 0.3 @@ -53,6 +53,7 @@ class TestLagd: msg = messaging.new_message('liveDelay') msg.liveDelay.lateralDelayEstimate = random.random() msg.liveDelay.validBlocks = random.randint(1, 10) + msg.liveDelay.version = VERSION params.put("LiveDelay", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) @@ -63,6 +64,20 @@ class TestLagd: assert lag == msg.liveDelay.lateralDelayEstimate assert valid_blocks == msg.liveDelay.validBlocks + def test_read_invalid_saved_params(self, subtests): + params = Params() + + lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams]) + CP = next(m for m in lr if m.which() == "carParams").carParams + + for msg_dict in [{'version': 0}, {'status': 'invalid'}, {'validBlocks': 100}]: + with subtests.test(msg=f"liveDelay={msg_dict}"): + msg = messaging.new_message('liveDelay') + msg.liveDelay = msg_dict + params.put("LiveDelay", msg.to_bytes(), block=True) + params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) + assert retrieve_initial_lag(params, CP) is None + def test_ncc(self): lag_frames = random.randint(1, 19) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 80a7df4515..4a40173629 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -79,22 +79,21 @@ compile_modeld_script = [ File(f"{modeld_dir}/compile_modeld.py"), File(f"{modeld_dir}/get_model_metadata.py"), File("#system/camerad/cameras/nv12_info.py"), - File("#system/hardware/hw.py"), + File("#common/hardware/hw.py"), ] model_w, model_h = MEDMODEL_INPUT_SIZE frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ for usbgpu in [False, True] if USBGPU else [False]: target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath - file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('', tg_flags) - driving_onnx_deps = [p for m in [f'{file_prefix}driving_vision', f'{file_prefix}driving_on_policy'] - for p in get_existing_chunks(File(f"models/{m}.onnx").abspath)] + # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU + file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) + driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py ' f'--model-size {model_w}x{model_h} ' f'--camera-resolutions {camera_res_args} ' - f'--vision-onnx {File(f"models/{file_prefix}driving_vision.onnx").abspath} ' - f'--on-policy-onnx {File(f"models/{file_prefix}driving_on_policy.onnx").abspath} ' + f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' f'--output {target_pkl_path} --frame-skip {frame_skip}') onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum)) diff --git a/selfdrive/modeld/compile_modeld.py b/selfdrive/modeld/compile_modeld.py index 2f91076ab7..35f92d344f 100755 --- a/selfdrive/modeld/compile_modeld.py +++ b/selfdrive/modeld/compile_modeld.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 import argparse import atexit +import math import os import pickle +import tempfile import time from functools import partial -from collections import namedtuple, defaultdict +from collections import namedtuple import numpy as np @@ -33,7 +35,7 @@ from tinygrad.engine.jit import TinyJit NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) WARP_INPUTS = ['img_q', 'big_img_q', 'tfm', 'big_tfm'] -POLICY_INPUTS = ['feat_q', 'desire_q', 'desire', 'traffic_convention', 'action_t'] +POLICY_INPUTS = ['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) @@ -49,8 +51,8 @@ def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, w_dst, h_dst = dst_shape h_src, w_src = src_shape - x = Tensor.arange(w_dst, device=WARP_DEV).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1) - y = Tensor.arange(h_dst, device=WARP_DEV).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1) + 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) # inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather) src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2] @@ -113,34 +115,51 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): return frame_prepare_tinygrad -def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device): +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]) - fb = policy_input_shapes['features_buffer'] # (1, 25, 512) - dp = policy_input_shapes['desire_pulse'] # (1, 25, 8) - tc = policy_input_shapes['traffic_convention'] # (1, 2) - #TODO action_t is hardcoded to match tc for future compatibility - at = tc - npy = { - 'desire': np.zeros(dp[2], dtype=np.float32), - 'traffic_convention': np.zeros(tc, dtype=np.float32), 'tfm': np.zeros((3, 3), dtype=np.float32), 'big_tfm': np.zeros((3, 3), dtype=np.float32), - 'action_t': np.zeros(at, 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(), - 'feat_q': Tensor(np.zeros((frame_skip * (fb[1] - 1) + 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(), **{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) + # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now + 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) + # views into the packed inputs, to be refilled at runtime + 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) @@ -163,49 +182,43 @@ def make_warp(nv12, model_w, model_h, frame_skip): big_tfm = big_tfm.to(WARP_DEV) Tensor.realize(tfm, big_tfm) - warped_frame = frame_prepare(frame, tfm).unsqueeze(0).to(Device.DEFAULT) - warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0).to(Device.DEFAULT) - img = shift_and_sample(img_q, warped_frame, sample_skip_fn) - big_img = shift_and_sample(big_img_q, warped_big_frame, sample_skip_fn) + warped_frame = frame_prepare(frame, tfm).unsqueeze(0) + warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) + warped = Tensor.cat(warped_frame, warped_big_frame).to(Device.DEFAULT) + 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) return img, big_img return warp_enqueue -def make_run_policy(vision_runner, on_policy_runner, vision_features_slice, frame_skip): +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(img, big_img, feat_q, desire_q, desire, traffic_convention, action_t): - desire = desire.to(Device.DEFAULT) - traffic_convention = traffic_convention.to(Device.DEFAULT) - action_t = action_t.to(Device.DEFAULT) - Tensor.realize(desire, traffic_convention, action_t) + def run_policy(img, big_img, feat_q, desire_q, packed_npy_inputs): + packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT).realize() + 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) - vision_out = next(iter(vision_runner({'img': img, 'big_img': big_img}).values())).cast('float32') - - new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0) - feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_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, } - on_policy_out = next(iter(on_policy_runner(inputs).values())).cast('float32') - #off_policy_out = next(iter(off_policy_runner(inputs).values())).cast('float32') - return vision_out, on_policy_out - + out = next(iter(model_runner(inputs).values())).cast('float32') + return out, return run_policy -def compile_jit(jit, make_random_inputs, input_keys, frame_skip, vision_metadata, policy_metadata): - vision_input_shapes = vision_metadata['input_shapes'] - policy_input_shapes = policy_metadata['input_shapes'] - +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_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT) + input_queues, npy = make_queues(Device.DEFAULT) np.random.seed(seed) Tensor.manual_seed(seed) @@ -252,12 +265,12 @@ def _parse_size(s): def read_file_chunked_to_shm(path): from openpilot.common.file_chunker import read_file_chunked - from openpilot.system.hardware.hw import Paths - shm_path = os.path.join(Paths.shm_path(), os.path.basename(path)) - atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path)) - with open(shm_path, 'wb') as f: + from openpilot.common.hardware.hw import Paths + with tempfile.NamedTemporaryFile(prefix='compile_modeld_', dir=Paths.shm_path(), delete=False) as f: f.write(read_file_chunked(path)) - return shm_path + tmp_path = f.name + atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path)) + return tmp_path if __name__ == "__main__": @@ -268,31 +281,30 @@ if __name__ == "__main__": 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('--vision-onnx', required=True) - p.add_argument('--on-policy-onnx', required=True) + p.add_argument('--onnx', required=True) p.add_argument('--output', required=True) p.add_argument('--frame-skip', type=int, required=True) args = p.parse_args() - out = defaultdict(dict) - vision_path, on_policy_path = read_file_chunked_to_shm(args.vision_onnx), read_file_chunked_to_shm(args.on_policy_onnx) + model_path = read_file_chunked_to_shm(args.onnx) model_w, model_h = args.model_size - vision_runner = OnnxRunner(vision_path) - on_policy_runner = OnnxRunner(on_policy_path) - vision_metadata, on_policy_metadata = make_metadata_dict(vision_path), make_metadata_dict(on_policy_path) + model_runner = OnnxRunner(model_path) + out = {'metadata': make_metadata_dict(model_path)} - run_policy_jit = TinyJit(make_run_policy(vision_runner, on_policy_runner, vision_metadata['output_slices']['hidden_state'], args.frame_skip), prune=True) - out['metadata']['vision'], out['metadata']['on_policy'] = vision_metadata, on_policy_metadata + run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) - make_random_model_inputs = partial(make_random_images, keys=['img', 'big_img'], shape=vision_metadata['input_shapes']['img']) - out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata) + make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip) + make_random_model_inputs = partial(make_random_images, keys=['img', 'big_img'], shape=out['metadata']['input_shapes']['img']) + out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, + make_policy_queues) 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) - out[(cam_w,cam_h)] = compile_jit(warp_enqueue, make_random_warp_inputs, WARP_INPUTS, args.frame_skip, vision_metadata, on_policy_metadata) + 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) with open(args.output, "wb") as f: pickle.dump(out, f) diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index 7273745c7b..b7a48ee01c 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -56,26 +56,29 @@ def fill_lane_line_meta(builder, lane_lines, lane_line_probs): builder.rightY = lane_lines[2].y[0] builder.rightProb = lane_line_probs[2] -def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, - net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action, +def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capnp._DynamicStructBuilder) -> None: + msg.valid = modelv2_send.valid + modelV2 = modelv2_send.modelV2 + driving_model_data = msg.drivingModelData + driving_model_data.frameId = modelV2.frameId + driving_model_data.frameIdExtra = modelV2.frameIdExtra + driving_model_data.frameDropPerc = modelV2.frameDropPerc + driving_model_data.modelExecutionTime = modelV2.modelExecutionTime + driving_model_data.action = modelV2.action + driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState + driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection + fill_lane_line_meta(driving_model_data.laneLineMeta, modelV2.laneLines, modelV2.laneLineProbs) + fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, modelV2.position.x, modelV2.position.y, modelV2.position.z) + +def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action, publish_state: PublishState, vipc_frame_id: int, vipc_frame_id_extra: int, frame_id: int, frame_drop: float, timestamp_eof: int, model_execution_time: float, valid: bool) -> None: frame_age = frame_id - vipc_frame_id if frame_id > vipc_frame_id else 0 frame_drop_perc = frame_drop * 100 - extended_msg.valid = valid - base_msg.valid = valid + msg.valid = valid - driving_model_data = base_msg.drivingModelData - - driving_model_data.frameId = vipc_frame_id - driving_model_data.frameIdExtra = vipc_frame_id_extra - driving_model_data.frameDropPerc = frame_drop_perc - driving_model_data.modelExecutionTime = model_execution_time - - driving_model_data.action = action - - modelV2 = extended_msg.modelV2 + modelV2 = msg.modelV2 modelV2.frameId = vipc_frame_id modelV2.frameIdExtra = vipc_frame_id_extra modelV2.frameAge = frame_age @@ -90,9 +93,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - # poly path - fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) - # action modelV2.action = action @@ -107,8 +107,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist() modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist() - fill_lane_line_meta(driving_model_data.laneLineMeta, modelV2.laneLines, modelV2.laneLineProbs) - # road edges modelV2.init('roadEdges', 2) for i in range(2): diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 9107cc6228..2b276479df 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -21,7 +21,7 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS -from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState +from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import read_file_chunked, get_manifest_path from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices @@ -84,19 +84,15 @@ class ModelState(ModelStateBase): input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] jits = pickle.loads(read_file_chunked(modeld_pkl_path(usbgpu))) - vision_metadata = jits['metadata']['vision'] - self.vision_input_shapes = vision_metadata['input_shapes'] - self.vision_input_names = list(self.vision_input_shapes.keys()) - self.vision_output_slices = vision_metadata['output_slices'] - - policy_metadata = jits['metadata']['on_policy'] - self.policy_input_shapes = policy_metadata['input_shapes'] - self.policy_output_slices = policy_metadata['output_slices'] + metadata = jits['metadata'] + self.input_shapes = metadata['input_shapes'] + self.vision_input_names = [k for k in self.input_shapes if 'img' in k] + self.output_slices = metadata['output_slices'] self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ - self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.full_frames: dict[str, Tensor] = {} self._blob_cache: dict[int, Tensor] = {} self.parser = Parser() @@ -133,19 +129,16 @@ class ModelState(ModelStateBase): if prepare_only: return None - vision_output, on_policy_output = self.run_policy( - **{k: self.input_queues[k] for k in POLICY_INPUTS}, img=img, big_img=big_img + outs, = self.run_policy( + **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, img=img, big_img=big_img ) - - vision_output = vision_output.numpy().flatten() - on_policy_output = on_policy_output.numpy().flatten() - vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices)) - policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(on_policy_output, self.policy_output_slices)) - combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} + model_output = outs.numpy()[0] + outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) + self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] if SEND_RAW_PRED: - combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), on_policy_output.copy()]) - return combined_outputs_dict + outputs_dict['raw_pred'] = model_output.copy() + return outputs_dict def main(demo=False): @@ -158,10 +151,7 @@ def main(demo=False): params.put_bool("UsbGpuPresent", _present) params.put_bool("UsbGpuCompiled", _compiled) - if not USBGPU: - # USB GPU currently saturates a core so can't do this yet, - # also need to move the aux USB interrupts for good timings - config_realtime_process(7, 54) + config_realtime_process(7, 54) # visionipc clients while True: @@ -317,7 +307,7 @@ def main(demo=False): action = get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego) prev_action = action - fill_model_msg(drivingdata_send, modelv2_send, model_output, action, + fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen) @@ -329,9 +319,8 @@ def main(demo=False): modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction - drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state - drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction + fill_driving_model_data(drivingdata_send, modelv2_send) fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen) pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) diff --git a/selfdrive/modeld/models/big_driving_on_policy.onnx b/selfdrive/modeld/models/big_driving_on_policy.onnx deleted file mode 100644 index f7b49c018a..0000000000 --- a/selfdrive/modeld/models/big_driving_on_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:565e53c38dcd64c50dd3fe4d5ee1530213aeefd66c3f6b67ea6a72a32612a6bf -size 14061419 diff --git a/selfdrive/modeld/models/big_driving_supercombo.onnx b/selfdrive/modeld/models/big_driving_supercombo.onnx new file mode 100644 index 0000000000..de84325912 --- /dev/null +++ b/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:471f372751d5931e939320c211e35b7255f8fa8015125b3b3fd48ef43020257e +size 195490097 diff --git a/selfdrive/modeld/models/big_driving_vision.onnx b/selfdrive/modeld/models/big_driving_vision.onnx deleted file mode 100644 index d14f1969e0..0000000000 --- a/selfdrive/modeld/models/big_driving_vision.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f0cab5033fe9e3bc5e174a2e790fa277f7d9fc44c65822d734064d2f899a9a0 -size 296203378 diff --git a/selfdrive/modeld/models/driving_on_policy.onnx b/selfdrive/modeld/models/driving_on_policy.onnx deleted file mode 100644 index 611ae9fe85..0000000000 --- a/selfdrive/modeld/models/driving_on_policy.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15 -size 14060847 diff --git a/selfdrive/modeld/models/driving_supercombo.onnx b/selfdrive/modeld/models/driving_supercombo.onnx new file mode 100644 index 0000000000..f0672eab48 --- /dev/null +++ b/selfdrive/modeld/models/driving_supercombo.onnx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b +size 60881999 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx deleted file mode 100644 index 6c9fc4c84d..0000000000 --- a/selfdrive/modeld/models/driving_vision.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66 -size 46877473 diff --git a/selfdrive/pandad/main.cc b/selfdrive/pandad/main.cc index ef30d6037c..08e88b6163 100644 --- a/selfdrive/pandad/main.cc +++ b/selfdrive/pandad/main.cc @@ -3,7 +3,7 @@ #include "selfdrive/pandad/pandad.h" #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" int main(int argc, char *argv[]) { LOGW("starting pandad"); diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index a771326f9d..7ca074dae9 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -1,7 +1,6 @@ #include "selfdrive/pandad/pandad.h" #include -#include #include #include #include @@ -16,7 +15,7 @@ #include "common/swaglog.h" #include "common/timing.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #define MAX_IR_PANDA_VAL 50 #define CUTOFF_IL 400 @@ -24,14 +23,6 @@ ExitHandler do_exit; -struct HwmonState { - std::atomic voltage{0}; - std::atomic current{0}; - std::atomic initialized{false}; -}; - -HwmonState hwmon_state; - bool check_connected(Panda *panda) { if (!panda->connected()) { do_exit = true; @@ -126,26 +117,6 @@ void can_recv(Panda *panda, PubMaster *pm) { } } -void hwmon_thread() { - util::set_thread_name("pandad_hwmon"); - - while (!do_exit) { - double read_time = millis_since_boot(); - uint32_t voltage = Hardware::get_voltage(); - uint32_t current = Hardware::get_current(); - read_time = millis_since_boot() - read_time; - if (read_time > 50) { - LOGW("reading hwmon took %lfms", read_time); - } - - hwmon_state.voltage.store(voltage); - hwmon_state.current.store(current); - hwmon_state.initialized.store(true); - - util::sleep_for(500); - } -} - void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::PandaType hw_type, const health_t &health) { ps.setVoltage(health.voltage_pkt); ps.setCurrent(health.current_pkt); @@ -278,7 +249,8 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } void send_peripheral_state(Panda *panda, PubMaster *pm) { - if (!hwmon_state.initialized.load()) { + auto health_opt = panda->get_state(); + if (!health_opt) { return; } @@ -290,18 +262,9 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { auto ps = evt.initPeripheralState(); ps.setPandaType(panda->hw_type); - ps.setVoltage(hwmon_state.voltage.load()); - ps.setCurrent(hwmon_state.current.load()); - - // fall back to panda's voltage and current measurement - if (ps.getVoltage() == 0 && ps.getCurrent() == 0) { - auto health_opt = panda->get_state(); - if (health_opt) { - health_t health = *health_opt; - ps.setVoltage(health.voltage_pkt); - ps.setCurrent(health.current_pkt); - } - } + health_t health = *health_opt; + ps.setVoltage(health.voltage_pkt); + ps.setCurrent(health.current_pkt); uint16_t fan_speed_rpm = panda->get_fan_speed(); ps.setFanSpeedRpm(fan_speed_rpm); @@ -413,9 +376,8 @@ void pandad_run(Panda *panda) { const bool spoofing_started = getenv("STARTED") != nullptr; const bool fake_send = getenv("FAKESEND") != nullptr; - // Start helper threads for event-driven sendcan and slow non-Panda reads. + // Start helper thread for event-driven sendcan. std::thread send_thread(can_send_thread, panda, fake_send); - std::thread hardware_thread(hwmon_thread); RateKeeper rk("pandad", 100); SubMaster sm({"selfdriveState", "deviceState", "selfdriveStateSP"}); @@ -475,7 +437,6 @@ void pandad_run(Panda *panda) { } send_thread.join(); - hardware_thread.join(); } void pandad_main_thread(std::string serial) { diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index 6846abcadd..1b5de9f87e 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -9,7 +9,7 @@ import subprocess from panda import Panda, PandaDFU, PandaProtocolMismatch, McuType, FW_PATH from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.selfdrive.pandad.rivian_long_flasher import flash_rivian_long diff --git a/selfdrive/pandad/tests/test_pandad.py b/selfdrive/pandad/tests/test_pandad.py index 9dacd3f5a8..88022dda8d 100644 --- a/selfdrive/pandad/tests/test_pandad.py +++ b/selfdrive/pandad/tests/test_pandad.py @@ -7,8 +7,8 @@ from cereal import log from openpilot.common.gpio import gpio_set, gpio_init from panda import Panda, PandaDFU from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.tici.pins import GPIO HERE = os.path.dirname(os.path.realpath(__file__)) diff --git a/selfdrive/pandad/tests/test_pandad_spi.py b/selfdrive/pandad/tests/test_pandad_spi.py index 69dfb67e93..50021d8998 100644 --- a/selfdrive/pandad/tests/test_pandad_spi.py +++ b/selfdrive/pandad/tests/test_pandad_spi.py @@ -80,7 +80,6 @@ class TestBoarddSpi: ps = m.peripheralState assert ps.pandaType == "tres" assert 4000 < ps.voltage < 14000 - assert 50 < ps.current < 1000 assert ps.fanSpeedRpm < 10000 time.sleep(0.5) diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index c9b281436c..ea9d47d714 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -10,7 +10,7 @@ from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 30605d0a27..fcd9461f30 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -22,7 +22,7 @@ from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert from openpilot.system.version import get_build_metadata -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot import get_sanitize_int_param @@ -376,12 +376,13 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.cameraFrameRate) if not REPLAY and self.rk.lagging: self.events.add(EventName.selfdrivedLagging) - if self.sm['radarState'].radarErrors.canError: - self.events.add(EventName.canError) - elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: - self.events.add(EventName.radarTempUnavailable) - elif any(self.sm['radarState'].radarErrors.to_dict().values()): - self.events.add(EventName.radarFault) + if self.CP.openpilotLongitudinalControl: + if self.sm['radarState'].radarErrors.canError: + self.events.add(EventName.canError) + elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: + self.events.add(EventName.radarTempUnavailable) + elif any(self.sm['radarState'].radarErrors.to_dict().values()): + self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) if CS.canTimeout: diff --git a/selfdrive/selfdrived/state.py b/selfdrive/selfdrived/state.py index 073ddb56eb..25c22684ad 100644 --- a/selfdrive/selfdrived/state.py +++ b/selfdrive/selfdrived/state.py @@ -24,14 +24,14 @@ class StateMachine: # ENABLED, SOFT DISABLING, PRE ENABLING, OVERRIDING if self.state != State.disabled: # user and immediate disable always have priority in a non-disabled state - if events.contains(ET.USER_DISABLE): - self.state = State.disabled - self.current_alert_types.append(ET.USER_DISABLE) - - elif events.contains(ET.IMMEDIATE_DISABLE): + if events.contains(ET.IMMEDIATE_DISABLE): self.state = State.disabled self.current_alert_types.append(ET.IMMEDIATE_DISABLE) + elif events.contains(ET.USER_DISABLE): + self.state = State.disabled + self.current_alert_types.append(ET.USER_DISABLE) + else: # ENABLED if self.state == State.enabled: diff --git a/selfdrive/test/.gitignore b/selfdrive/test/.gitignore index b8c6bebd95..377696afae 100644 --- a/selfdrive/test/.gitignore +++ b/selfdrive/test/.gitignore @@ -1,5 +1,4 @@ out/ -docker_out/ process_replay/diff.txt process_replay/model_diff.txt diff --git a/selfdrive/test/docker_build.sh b/selfdrive/test/docker_build.sh deleted file mode 100755 index 8d1fa82249..0000000000 --- a/selfdrive/test/docker_build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -e - -SCRIPT_DIR=$(dirname "$0") -OPENPILOT_DIR=$SCRIPT_DIR/../../ - -DOCKER_IMAGE=openpilot -DOCKER_FILE=Dockerfile.openpilot -DOCKER_REGISTRY=ghcr.io/commaai -COMMIT_SHA=$(git rev-parse HEAD) - -if [ -n "$TARGET_ARCHITECTURE" ]; then - PLATFORM="linux/$TARGET_ARCHITECTURE" - TAG_SUFFIX="-$TARGET_ARCHITECTURE" -else - PLATFORM="linux/$(uname -m)" - TAG_SUFFIX="" -fi - -LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX -REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG -REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA - -DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR - -if [ -n "$PUSH_IMAGE" ]; then - docker push $REMOTE_TAG - docker tag $REMOTE_TAG $REMOTE_SHA_TAG - docker push $REMOTE_SHA_TAG -fi diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 266f66aaf2..3e5616e702 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -12,7 +12,7 @@ import numpy as np from openpilot.common.utils import tabulate from openpilot.common.git import get_commit -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC from openpilot.tools.lib.openpilotci import get_url from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff from openpilot.selfdrive.test.process_replay.process_replay import get_process_config, replay_process diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 77254cecee..432161ceb7 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -12,7 +12,7 @@ from typing import Any from collections.abc import Callable, Iterable from tqdm import tqdm import capnp -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths import cereal.messaging as messaging from cereal import car diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 8161dca130..99d0279c4c 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -18,7 +18,7 @@ from openpilot.common.timeout import Timeout from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.events import EVENTS, ET from openpilot.selfdrive.test.helpers import set_params_enabled, release_only -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.log_time_series import msgs_to_time_series @@ -68,7 +68,7 @@ PROCS = { "system.loggerd.deleter": 1.0, "./pandad": 19.0, "system.qcomgpsd.qcomgpsd": 1.0, - "system.hardware.tici.modem": 10.0, + "common.hardware.tici.modem": 10.0, } TIMINGS = { @@ -284,7 +284,7 @@ class TestOnroad: print("--------------- Memory Usage -------------------") print("------------------------------------------------") - from openpilot.selfdrive.debug.mem_usage import print_report + from openpilot.tools.scripts.mem_usage import print_report print_report(self.msgs['procLog'], self.msgs['deviceState']) offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) diff --git a/system/hardware/tici/tests/test_power_draw.py b/selfdrive/test/test_power_draw.py similarity index 98% rename from system/hardware/tici/tests/test_power_draw.py rename to selfdrive/test/test_power_draw.py index 6656addf73..ad27292945 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/selfdrive/test/test_power_draw.py @@ -10,7 +10,7 @@ from cereal.services import SERVICE_LIST from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.mock import mock_messages from openpilot.common.params import Params -from openpilot.system.hardware.tici.power_monitor import get_power +from openpilot.common.hardware.tici.power_monitor import get_power from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.manager import manager_cleanup diff --git a/selfdrive/test/test_updated.py b/selfdrive/test/test_updated.py deleted file mode 100644 index e4cc714b87..0000000000 --- a/selfdrive/test/test_updated.py +++ /dev/null @@ -1,295 +0,0 @@ -import datetime -import os -import pytest -import time -import tempfile -import shutil -import signal -import subprocess -import random - -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params - - -@pytest.mark.tici -class TestUpdated: - - def setup_method(self): - self.updated_proc = None - - self.tmp_dir = tempfile.TemporaryDirectory() - org_dir = os.path.join(self.tmp_dir.name, "commaai") - - self.basedir = os.path.join(org_dir, "openpilot") - self.git_remote_dir = os.path.join(org_dir, "openpilot_remote") - self.staging_dir = os.path.join(org_dir, "safe_staging") - for d in [org_dir, self.basedir, self.git_remote_dir, self.staging_dir]: - os.mkdir(d) - - self.neos_version = os.path.join(org_dir, "neos_version") - self.neosupdate_dir = os.path.join(org_dir, "neosupdate") - with open(self.neos_version, "w") as f: - v = subprocess.check_output(r"bash -c 'source launch_env.sh && echo $REQUIRED_NEOS_VERSION'", - cwd=BASEDIR, shell=True, encoding='utf8').strip() - f.write(v) - - self.upper_dir = os.path.join(self.staging_dir, "upper") - self.merged_dir = os.path.join(self.staging_dir, "merged") - self.finalized_dir = os.path.join(self.staging_dir, "finalized") - - # setup local submodule remotes - submodules = subprocess.check_output("git submodule --quiet foreach 'echo $name'", - shell=True, cwd=BASEDIR, encoding='utf8').split() - for s in submodules: - sub_path = os.path.join(org_dir, s.split("_repo")[0]) - self._run(f"git clone {s} {sub_path}.git", cwd=BASEDIR) - - # setup two git repos, a remote and one we'll run updated in - self._run([ - f"git clone {BASEDIR} {self.git_remote_dir}", - f"git clone {self.git_remote_dir} {self.basedir}", - f"cd {self.basedir} && git submodule init && git submodule update", - f"cd {self.basedir} && scons cereal/ common/" - ]) - - self.params = Params(os.path.join(self.basedir, "persist/params")) - self.params.clear_all() - os.sync() - - def teardown_method(self): - try: - if self.updated_proc is not None: - self.updated_proc.terminate() - self.updated_proc.wait(30) - except Exception as e: - print(e) - self.tmp_dir.cleanup() - - - # *** test helpers *** - - - def _run(self, cmd, cwd=None): - if not isinstance(cmd, list): - cmd = (cmd,) - - for c in cmd: - subprocess.check_output(c, cwd=cwd, shell=True) - - def _get_updated_proc(self): - os.environ["PYTHONPATH"] = self.basedir - os.environ["GIT_AUTHOR_NAME"] = "testy tester" - os.environ["GIT_COMMITTER_NAME"] = "testy tester" - os.environ["GIT_AUTHOR_EMAIL"] = "testy@tester.test" - os.environ["GIT_COMMITTER_EMAIL"] = "testy@tester.test" - os.environ["UPDATER_TEST_IP"] = "localhost" - os.environ["UPDATER_LOCK_FILE"] = os.path.join(self.tmp_dir.name, "updater.lock") - os.environ["UPDATER_STAGING_ROOT"] = self.staging_dir - os.environ["UPDATER_NEOS_VERSION"] = self.neos_version - os.environ["UPDATER_NEOSUPDATE_DIR"] = self.neosupdate_dir - updated_path = os.path.join(self.basedir, "system/updated.py") - return subprocess.Popen(updated_path, env=os.environ) - - def _start_updater(self, offroad=True, nosleep=False): - self.params.put_bool("IsOffroad", offroad, block=True) - self.updated_proc = self._get_updated_proc() - if not nosleep: - time.sleep(1) - - def _update_now(self): - self.updated_proc.send_signal(signal.SIGHUP) - - # TODO: this should be implemented in params - def _read_param(self, key, timeout=1): - ret = None - start_time = time.monotonic() - while ret is None: - ret = self.params.get(key) - if time.monotonic() - start_time > timeout: - break - time.sleep(0.01) - return ret - - def _wait_for_update(self, timeout=30, clear_param=False): - if clear_param: - self.params.remove("LastUpdateTime") - - self._update_now() - t = self._read_param("LastUpdateTime", timeout=timeout) - if t is None: - raise Exception("timed out waiting for update to complete") - - def _make_commit(self): - all_dirs, all_files = [], [] - for root, dirs, files in os.walk(self.git_remote_dir): - if ".git" in root: - continue - for d in dirs: - all_dirs.append(os.path.join(root, d)) - for f in files: - all_files.append(os.path.join(root, f)) - - # make a new dir and some new files - new_dir = os.path.join(self.git_remote_dir, "this_is_a_new_dir") - os.mkdir(new_dir) - for _ in range(random.randrange(5, 30)): - for d in (new_dir, random.choice(all_dirs)): - with tempfile.NamedTemporaryFile(dir=d, delete=False) as f: - f.write(os.urandom(random.randrange(1, 1000000))) - - # modify some files - for f in random.sample(all_files, random.randrange(5, 50)): - with open(f, "w+") as ff: - txt = ff.readlines() - ff.seek(0) - for line in txt: - ff.write(line[::-1]) - - # remove some files - for f in random.sample(all_files, random.randrange(5, 50)): - os.remove(f) - - # remove some dirs - for d in random.sample(all_dirs, random.randrange(1, 10)): - shutil.rmtree(d) - - # commit the changes - self._run([ - "git add -A", - "git commit -m 'an update'", - ], cwd=self.git_remote_dir) - - def _check_update_state(self, update_available): - # make sure LastUpdateTime is recent - last_update_time = self._read_param("LastUpdateTime") - td = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_update_time - assert td.total_seconds() < 10 - self.params.remove("LastUpdateTime") - - # wait a bit for the rest of the params to be written - time.sleep(0.1) - - # check params - update = self._read_param("UpdateAvailable") - assert update == "1" == update_available, f"UpdateAvailable: {repr(update)}" - assert self._read_param("UpdateFailedCount") == 0 - - # TODO: check that the finalized update actually matches remote - # check the .overlay_init and .overlay_consistent flags - assert os.path.isfile(os.path.join(self.basedir, ".overlay_init")) - assert os.path.isfile(os.path.join(self.finalized_dir, ".overlay_consistent")) == update_available - - - # *** test cases *** - - - # Run updated for 100 cycles with no update - def test_no_update(self): - self._start_updater() - for _ in range(100): - self._wait_for_update(clear_param=True) - self._check_update_state(False) - - # Let the updater run with no update for a cycle, then write an update - def test_update(self): - self._start_updater() - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - self._check_update_state(False) - - # write an update to our remote - self._make_commit() - - # run for a cycle to get the update - self._wait_for_update(timeout=60, clear_param=True) - self._check_update_state(True) - - # run another cycle with no update - self._wait_for_update(clear_param=True) - self._check_update_state(True) - - # Let the updater run for 10 cycles, and write an update every cycle - @pytest.mark.skip("need to make this faster") - def test_update_loop(self): - self._start_updater() - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - for _ in range(10): - time.sleep(0.5) - self._make_commit() - self._wait_for_update(timeout=90, clear_param=True) - self._check_update_state(True) - - # Test overlay re-creation after tracking a new file in basedir's git - def test_overlay_reinit(self): - self._start_updater() - - overlay_init_fn = os.path.join(self.basedir, ".overlay_init") - - # run for a cycle with no update - self._wait_for_update(clear_param=True) - self.params.remove("LastUpdateTime") - first_mtime = os.path.getmtime(overlay_init_fn) - - # touch a file in the basedir - self._run("touch new_file && git add new_file", cwd=self.basedir) - - # run another cycle, should have a new mtime - self._wait_for_update(clear_param=True) - second_mtime = os.path.getmtime(overlay_init_fn) - assert first_mtime != second_mtime - - # run another cycle, mtime should be same as last cycle - self._wait_for_update(clear_param=True) - new_mtime = os.path.getmtime(overlay_init_fn) - assert second_mtime == new_mtime - - # Make sure updated exits if another instance is running - def test_multiple_instances(self): - # start updated and let it run for a cycle - self._start_updater() - time.sleep(1) - self._wait_for_update(clear_param=True) - - # start another instance - second_updated = self._get_updated_proc() - ret_code = second_updated.wait(timeout=5) - assert ret_code is not None - - - # *** test cases with NEOS updates *** - - - # Run updated with no update, make sure it clears the old NEOS update - def test_clear_neos_cache(self): - # make the dir and some junk files - os.mkdir(self.neosupdate_dir) - for _ in range(15): - with tempfile.NamedTemporaryFile(dir=self.neosupdate_dir, delete=False) as f: - f.write(os.urandom(random.randrange(1, 1000000))) - - self._start_updater() - self._wait_for_update(clear_param=True) - self._check_update_state(False) - assert not os.path.isdir(self.neosupdate_dir) - - # Let the updater run with no update for a cycle, then write an update - @pytest.mark.skip("TODO: only runs on device") - def test_update_with_neos_update(self): - # bump the NEOS version and commit it - self._run([ - "echo 'export REQUIRED_NEOS_VERSION=3' >> launch_env.sh", - "git -c user.name='testy' -c user.email='testy@tester.test' \ - commit -am 'a neos update'", - ], cwd=self.git_remote_dir) - - # run for a cycle to get the update - self._start_updater() - self._wait_for_update(timeout=60, clear_param=True) - self._check_update_state(True) - - # TODO: more comprehensive check - assert os.path.isdir(self.neosupdate_dir) diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index 176c2d510f..07e824c649 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -5,7 +5,7 @@ #include "common/swaglog.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "raylib.h" int freshClone(); diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index a9fbca768d..ef9c7f9e03 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -135,6 +135,7 @@ class DeveloperLayout(Widget): long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled) + self._lat_maneuver_toggle.action_item.set_enabled(ui_state.is_offroad()) else: self._long_maneuver_toggle.action_item.set_enabled(False) self._lat_maneuver_toggle.action_item.set_enabled(False) diff --git a/selfdrive/ui/mici/layouts/offroad_alerts.py b/selfdrive/ui/mici/layouts/offroad_alerts.py index e0c574dc07..0f4694a85f 100644 --- a/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -7,7 +7,7 @@ from enum import IntEnum from openpilot.common.params import Params from openpilot.common.realtime import drop_realtime from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import Scroller diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index 2f0250ff5f..2d7dc0899d 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -152,6 +152,7 @@ class DeveloperLayoutMici(NavScroller): long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() self._long_maneuver_toggle.set_enabled(long_man_enabled) + self._lat_maneuver_toggle.set_enabled(ui_state.is_offroad()) else: self._long_maneuver_toggle.set_enabled(False) self._lat_maneuver_toggle.set_enabled(False) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 4a8fc50d53..fd8bacf441 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -1,7 +1,5 @@ import os -import threading import pyray as rl -from enum import IntEnum from collections.abc import Callable from openpilot.common.basedir import BASEDIR @@ -122,12 +120,6 @@ class DeviceInfoLayoutMici(Widget): self._serial_number_text_label.render() -class UpdaterState(IntEnum): - IDLE = 0 - WAITING_FOR_UPDATER = 1 - UPDATER_RESPONDING = 2 - - class PairBigButton(BigButton): def __init__(self): super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) @@ -164,128 +156,6 @@ class PairBigButton(BigButton): gui_app.push_widget(dlg) -UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond - - -class UpdateOpenpilotBigButton(BigButton): - def __init__(self): - self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) - self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70) - self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) - super().__init__("update sunnypilot", "", self._txt_update_icon) - - self._waiting_for_updater_t: float | None = None - self._hide_value_t: float | None = None - self._state: UpdaterState = UpdaterState.IDLE - - ui_state.add_offroad_transition_callback(self.offroad_transition) - - def offroad_transition(self): - if ui_state.is_offroad(): - self.set_enabled(True) - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not system_time_valid(): - dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) - gui_app.push_widget(dlg) - return - - self.set_enabled(False) - self._state = UpdaterState.WAITING_FOR_UPDATER - self.set_icon(self._txt_update_icon) - - def run(): - if self.get_value() == "download update": - os.system("pkill -SIGHUP -f system.updated.updated") - elif self.get_value() == "update now": - ui_state.params.put_bool("DoReboot", True, block=True) - else: - os.system("pkill -SIGUSR1 -f system.updated.updated") - - threading.Thread(target=run, daemon=True).start() - - def set_value(self, value: str): - super().set_value(value) - if value: - self.set_text("") - else: - self.set_text("update sunnypilot") - - def _update_state(self): - super()._update_state() - - if ui_state.started: - self.set_enabled(False) - return - - updater_state = ui_state.params.get("UpdaterState") or "" - failed_count = ui_state.params.get("UpdateFailedCount") - failed = False if failed_count is None else int(failed_count) > 0 - - if ui_state.params.get_bool("UpdateAvailable"): - self.set_rotate_icon(False) - self.set_enabled(True) - if self.get_value() != "update now": - self.set_value("update now") - self.set_icon(self._txt_reboot_icon) - - elif self._state == UpdaterState.WAITING_FOR_UPDATER: - self.set_rotate_icon(True) - if updater_state != "idle": - self._state = UpdaterState.UPDATER_RESPONDING - - # Recover from updater not responding (time invalid shortly after boot) - if self._waiting_for_updater_t is None: - self._waiting_for_updater_t = rl.get_time() - - if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: - self.set_rotate_icon(False) - self.set_value("updater failed\nto respond") - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - - elif self._state == UpdaterState.UPDATER_RESPONDING: - if updater_state == "idle": - self.set_rotate_icon(False) - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - else: - if self.get_value() != updater_state: - self.set_value(updater_state) - - elif self._state == UpdaterState.IDLE: - self.set_rotate_icon(False) - if failed: - self.set_enabled(True) # allow retry when failure came from updater param - if self.get_value() != "failed to update": - self.set_value("failed to update") - - elif ui_state.params.get_bool("UpdaterFetchAvailable"): - self.set_enabled(True) - if self.get_value() != "download update": - self.set_value("download update") - - elif self._hide_value_t is not None: - self.set_enabled(True) - if self.get_value() == "checking...": - self.set_value("up to date") - self.set_icon(self._txt_up_to_date_icon) - - # Hide previous text after short amount of time (up to date or failed) - if rl.get_time() - self._hide_value_t > 3.0: - self._hide_value_t = None - self.set_value("") - self.set_icon(self._txt_update_icon) - else: - if self.get_value() != "": - self.set_value("") - - if self._state != UpdaterState.WAITING_FOR_UPDATER: - self._waiting_for_updater_t = None - - class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() @@ -307,16 +177,9 @@ class DeviceLayoutMici(NavScroller): params.remove("LiveDelay") params.put_bool("OnroadCycleRequested", True, block=True) - def uninstall_openpilot_callback(): - ui_state.params.put_bool("DoUninstall", True, block=True) - reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), reset_calibration_callback) - uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", - gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), - uninstall_openpilot_callback, exit_on_confirm=False) - reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), reboot_callback, exit_on_confirm=False) @@ -340,14 +203,12 @@ class DeviceLayoutMici(NavScroller): self._scroller.add_widgets([ DeviceInfoLayoutMici(), - UpdateOpenpilotBigButton(), PairBigButton(), review_training_guide_btn, driver_cam_btn, terms_btn, regulatory_btn, reset_calibration_btn, - uninstall_openpilot_btn, reboot_btn, self._power_off_btn, ]) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 10490bc43c..90bfad4d0d 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -5,6 +5,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici +from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight @@ -31,6 +32,10 @@ class SettingsLayout(NavScroller): device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + software_panel = SoftwareLayoutMici() + software_btn = SettingsBigButton("software", "", gui_app.texture("icons_mici/settings/software.png", 64, 75)) + software_btn.set_click_callback(lambda: gui_app.push_widget(software_panel)) + developer_panel = DeveloperLayoutMici() developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60)) developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) @@ -43,6 +48,7 @@ class SettingsLayout(NavScroller): toggles_btn, network_btn, device_btn, + software_btn, PairBigButton(), #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, diff --git a/selfdrive/ui/mici/layouts/settings/software.py b/selfdrive/ui/mici/layouts/settings/software.py new file mode 100644 index 0000000000..8f451cdd0c --- /dev/null +++ b/selfdrive/ui/mici/layouts/settings/software.py @@ -0,0 +1,274 @@ +import os +import threading +import pyray as rl +from enum import IntEnum +from collections.abc import Callable + +from openpilot.common.time_helpers import system_time_valid +from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton +from openpilot.selfdrive.ui.mici.widgets.button import BigButton +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.scroller import NavScroller + +UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond + + +def _split_description(desc: str) -> tuple[str, str, str, str] | None: + # UpdaterCurrentDescription/UpdaterNewDescription format: "version / branch / commit / date" + parts = [p.strip() for p in desc.split(" / ")] + if len(parts) != 4: + return None + version, branch, commit, date = parts + return version, branch, commit, date + + +class UpdaterState(IntEnum): + IDLE = 0 + WAITING_FOR_UPDATER = 1 + UPDATER_RESPONDING = 2 + + +class SoftwareInfoLayoutMici(Widget): + def __init__(self): + super().__init__() + + self.set_rect(rl.Rectangle(0, 0, 360, 180)) + + subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) + max_width = int(self._rect.width - 20) + self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) + self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, + font_weight=FontWeight.ROMAN, wrap_text=False) + + def _update_state(self): + desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") + if desc is not None: + version, branch, commit, date = desc + self._version_text_label.set_text(f"{version} ({date})") + self._branch_text_label.set_text(f"{branch} ({commit})") + else: + self._version_text_label.set_text(ui_state.params.get("Version") or "N/A") + self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A") + + def _render(self, _): + self._version_label.set_position(self._rect.x + 20, self._rect.y - 10) + self._version_label.render() + + self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) + self._version_text_label.render() + + self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) + self._branch_label.render() + + self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) + self._branch_text_label.render() + + +class CheckUpdateButton(BigButton): + def __init__(self): + self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) + self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + super().__init__("check for update", "", self._txt_update_icon) + + self._waiting_for_updater_t: float | None = None + self._hide_value_t: float | None = None + self._state: UpdaterState = UpdaterState.IDLE + + ui_state.add_offroad_transition_callback(self.offroad_transition) + + def offroad_transition(self): + if ui_state.is_offroad(): + self.set_enabled(True) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + if not system_time_valid(): + dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) + gui_app.push_widget(dlg) + return + + self.set_enabled(False) + self._state = UpdaterState.WAITING_FOR_UPDATER + self.set_icon(self._txt_update_icon) + + def run(): + if self.get_value() == "download update": + os.system("pkill -SIGHUP -f system.updated.updated") + else: + os.system("pkill -SIGUSR1 -f system.updated.updated") + + threading.Thread(target=run, daemon=True).start() + + def set_value(self, value: str): + super().set_value(value) + if value: + self.set_text("") + else: + self.set_text("check for update") + + def _update_state(self): + super()._update_state() + + if ui_state.started: + self.set_enabled(False) + return + + updater_state = ui_state.params.get("UpdaterState") or "" + failed_count = ui_state.params.get("UpdateFailedCount") or 0 + failed = int(failed_count) > 0 + + if self._state == UpdaterState.WAITING_FOR_UPDATER: + self.set_rotate_icon(True) + if updater_state != "idle": + self._state = UpdaterState.UPDATER_RESPONDING + + # Recover from updater not responding (time invalid shortly after boot) + if self._waiting_for_updater_t is None: + self._waiting_for_updater_t = rl.get_time() + + if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: + self.set_rotate_icon(False) + self.set_value("updater failed\nto respond") + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + + elif self._state == UpdaterState.UPDATER_RESPONDING: + if updater_state == "idle": + self.set_rotate_icon(False) + self._state = UpdaterState.IDLE + self._hide_value_t = rl.get_time() + else: + if self.get_value() != updater_state: + self.set_value(updater_state) + + elif self._state == UpdaterState.IDLE: + self.set_rotate_icon(False) + if failed: + self.set_enabled(True) # allow retry when failure came from updater param + if self.get_value() != "failed to update": + self.set_value("failed to update") + + elif ui_state.params.get_bool("UpdaterFetchAvailable"): + self.set_enabled(True) + if self.get_value() != "download update": + self.set_value("download update") + + elif self._hide_value_t is not None: + self.set_enabled(True) + if self.get_value() == "checking...": + self.set_value("up to date") + self.set_icon(self._txt_up_to_date_icon) + + # Hide previous text after short amount of time (up to date or failed) + if rl.get_time() - self._hide_value_t > 3.0: + self._hide_value_t = None + self.set_value("") + self.set_icon(self._txt_update_icon) + else: + if self.get_value() != "": + self.set_value("") + + if self._state != UpdaterState.WAITING_FOR_UPDATER: + self._waiting_for_updater_t = None + + +class InstallUpdateButton(BigButton): + def __init__(self): + super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) + + def _update_state(self): + super()._update_state() + + desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "") + value = f"{desc[0]} ({desc[1]})" if desc is not None else "" + if self.get_value() != value: + self.set_value(value) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + + self.set_enabled(False) + + def run(): + ui_state.params.put_bool("DoReboot", True, block=True) + + threading.Thread(target=run, daemon=True).start() + + +class BranchSelectPage(NavScroller): + def __init__(self, on_select: Callable[[str], None]): + super().__init__() + + params = ui_state.params + current_git_branch = params.get("GitBranch") or "" + branches_str = params.get("UpdaterAvailableBranches") or "" + branches = [b for b in branches_str.split(",") if b] + + for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: + if b in branches: + branches.remove(b) + branches.insert(0, b) + + current_target = params.get("UpdaterTargetBranch") or "" + check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) + + buttons = [] + for branch in branches: + btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True) + btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b))) + buttons.append(btn) + self._scroller.add_widgets(buttons) + + +class TargetBranchButton(BigButton): + def __init__(self): + super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") + self.set_click_callback(self._on_click) + self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) + self.set_enabled(lambda: ui_state.is_offroad()) + + def _update_state(self): + super()._update_state() + + target = ui_state.params.get("UpdaterTargetBranch") or "" + if self.get_value() != target: + self.set_value(target) + + def _on_click(self): + gui_app.push_widget(BranchSelectPage(self._on_select)) + + def _on_select(self, branch: str): + ui_state.params.put("UpdaterTargetBranch", branch, block=True) + self.set_value(branch) + os.system("pkill -SIGUSR1 -f system.updated.updated") + + +class SoftwareLayoutMici(NavScroller): + def __init__(self): + super().__init__() + + def uninstall_openpilot_callback(): + ui_state.params.put_bool("DoUninstall", True, block=True) + + uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", + gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), + uninstall_openpilot_callback, exit_on_confirm=False) + + self._scroller.add_widgets([ + SoftwareInfoLayoutMici(), + CheckUpdateButton(), + InstallUpdateButton(), + TargetBranchButton(), + uninstall_openpilot_btn, + ]) diff --git a/selfdrive/ui/mici/onroad/alert_renderer.py b/selfdrive/ui/mici/onroad/alert_renderer.py index 5b550030de..6a227edec4 100644 --- a/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/selfdrive/ui/mici/onroad/alert_renderer.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from cereal import messaging, log, car from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 3ccbd2429a..6377740d7c 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -330,10 +330,13 @@ class AugmentedRoadView(CameraView): w, h = self._content_rect.width, self._content_rect.height cx, cy = intrinsic[0, 2], intrinsic[1, 2] + # Ensure zoom views the whole area + zoom = max(zoom, w / (2 * cx), h / (2 * cy)) + # Calculate max allowed offsets with margins margin = 5 - max_x_offset = cx * zoom - w / 2 - margin - max_y_offset = cy * zoom - h / 2 - margin + max_x_offset = max(0.0, cx * zoom - w / 2 - margin) + max_y_offset = max(0.0, cy * zoom - h / 2 - margin) # Calculate and clamp offsets to prevent out-of-bounds issues try: diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/selfdrive/ui/mici/onroad/cameraview.py index 86dd95f466..991349dbf0 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/selfdrive/ui/mici/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget diff --git a/selfdrive/ui/mici/onroad/torque_bar.py b/selfdrive/ui/mici/onroad/torque_bar.py index 861bfe493e..85edd89e98 100644 --- a/selfdrive/ui/mici/onroad/torque_bar.py +++ b/selfdrive/ui/mici/onroad/torque_bar.py @@ -165,7 +165,7 @@ class TorqueBar(Widget): return # torque line - if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState': + if ui_state.sm['controlsState'].lateralControlState.which() in ('angleState', 'curvatureState'): controls_state = ui_state.sm['controlsState'] car_state = ui_state.sm['carState'] live_parameters = ui_state.sm['liveParameters'] diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index e35cb44776..09142a8e11 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -1,26 +1,6 @@ -import pyray as rl -rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) import gc import weakref import pytest -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import Widget - -# mici dialogs -from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog -from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal - -# tici dialogs -from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog -from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog -from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog -from openpilot.system.ui.widgets.html_render import HtmlModal -from openpilot.system.ui.widgets.keyboard import Keyboard # FIXME: known small leaks not worth worrying about at the moment KNOWN_LEAKS = { @@ -52,7 +32,8 @@ KNOWN_LEAKS = { } -def get_child_widgets(widget: Widget) -> list[Widget]: +def get_child_widgets(widget) -> list: + from openpilot.system.ui.widgets import Widget children = [] for val in widget.__dict__.values(): items = val if isinstance(val, (list, tuple)) else (val,) @@ -62,6 +43,26 @@ def get_child_widgets(widget: Widget) -> list[Widget]: @pytest.mark.skip(reason="segfaults") def test_dialogs_do_not_leak(): + import pyray as rl + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + from openpilot.system.ui.lib.application import gui_app + + # mici dialogs + from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow + from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog + from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog + from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog + from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal + + # tici dialogs + from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog + from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow + from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog + from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog + from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog + from openpilot.system.ui.widgets.html_render import HtmlModal + from openpilot.system.ui.widgets.keyboard import Keyboard + gui_app.init_window("ref-test") leaked_widgets = set() diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py index 6e79d23253..6fab21c4b9 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index 9ea1a64cee..cd8423ef7b 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -96,7 +96,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP): ) # Render the base camera view - super()._render(rect) + super()._render(self._content_rect) # Draw all UI overlays self.model_renderer.render(self._content_rect) @@ -200,10 +200,13 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewSP): w, h = self._content_rect.width, self._content_rect.height cx, cy = intrinsic[0, 2], intrinsic[1, 2] + # Ensure zoom views the whole area + zoom = max(zoom, w / (2 * cx), h / (2 * cy)) + # Calculate max allowed offsets with margins margin = 5 - max_x_offset = cx * zoom - w / 2 - margin - max_y_offset = cy * zoom - h / 2 - margin + max_x_offset = max(0.0, cx * zoom - w / 2 - margin) + max_y_offset = max(0.0, cy * zoom - h / 2 - margin) # Calculate and clamp offsets to prevent out-of-bounds issues try: diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 53c2ff8b0b..846bf20bbc 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index 6b64289766..1722f608e1 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -12,7 +12,7 @@ from openpilot.common.utils import retry from openpilot.common.swaglog import cloudlog from openpilot.system import micd -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.sunnypilot.selfdrive.ui.quiet_mode import QuietMode @@ -123,6 +123,8 @@ class Soundd(QuietMode): ret[written_frames:written_frames+frames_to_write] = sound_data[current_sound_frame:current_sound_frame+frames_to_write] written_frames += frames_to_write self.current_sound_frame += frames_to_write + current_sound_frame = self.current_sound_frame % len(sound_data) + loops = self.current_sound_frame // len(sound_data) return ret * self.current_volume @@ -132,7 +134,7 @@ class Soundd(QuietMode): data_out[:frames, 0] = self.get_sound_data(frames) def update_alert(self, new_alert): - current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame > len(self.loaded_sounds[self.current_alert]) + current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert]) if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once): if new_alert == AudibleAlert.warningImmediate: self.ramp_start_volume = self.current_volume @@ -177,10 +179,11 @@ class Soundd(QuietMode): self.load_param() - # Always update volume, even when alert is playing + # freeze volume during alerts to avoid mic feedback increasing volume if sm.updated['soundPressure']: self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) - self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) + if self.current_alert == AudibleAlert.none: + self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.get_audible_alert(sm) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/developer.py b/selfdrive/ui/sunnypilot/layouts/settings/developer.py index 4cac66e316..a8768af5be 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/developer.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/developer.py @@ -10,8 +10,8 @@ from pathlib import Path from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout -from openpilot.system.hardware import PC -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import PC +from openpilot.common.hardware.hw import Paths from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult diff --git a/selfdrive/ui/sunnypilot/layouts/settings/device.py b/selfdrive/ui/sunnypilot/layouts/settings/device.py index 1fb9314739..e5c956cd81 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/device.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/device.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, multiple_button_item_sp, button_item_sp, \ diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py index 58236961b9..7b30e880f9 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -16,7 +16,7 @@ from time import monotonic from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.layouts.settings.software import time_ago -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult, Widget diff --git a/selfdrive/ui/sunnypilot/layouts/settings/software.py b/selfdrive/ui/sunnypilot/layouts/settings/software.py index 7765a58b65..a07bda1e34 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/software.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/software.py @@ -8,7 +8,7 @@ import os from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 346262b86c..bfa311c357 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -14,7 +14,7 @@ from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.hud_renderer import UI_CONFIG from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached diff --git a/selfdrive/ui/tests/diff/diff.py b/selfdrive/ui/tests/diff/diff.py index 974edb42a3..b861d848eb 100755 --- a/selfdrive/ui/tests/diff/diff.py +++ b/selfdrive/ui/tests/diff/diff.py @@ -4,6 +4,7 @@ import sys import subprocess import webbrowser import argparse +from concurrent.futures import ThreadPoolExecutor from pathlib import Path from openpilot.common.basedir import BASEDIR @@ -11,8 +12,8 @@ DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report" HTML_TEMPLATE_PATH = Path(__file__).with_name("diff_template.html") -def extract_framehashes(video_path): - cmd = ['ffmpeg', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-'] +def extract_framehashes(video_path: Path) -> list[str]: + cmd = ['ffmpeg', '-nostdin', '-i', video_path, '-map', '0:v:0', '-vsync', '0', '-f', 'framehash', '-hash', 'md5', '-'] result = subprocess.run(cmd, capture_output=True, text=True, check=True) hashes = [] for line in result.stdout.splitlines(): @@ -25,42 +26,33 @@ def extract_framehashes(video_path): return hashes -def create_diff_video(video1, video2, output_path): - """Create a diff video using ffmpeg blend filter with difference mode.""" - print("Creating diff video...") - cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path] +def get_video_frame_hashes(video1: Path, video2: Path) -> tuple[list[str], list[str]]: + """Hash every frame of both videos in parallel and return the two hash lists.""" + with ThreadPoolExecutor(max_workers=2) as executor: + print("Generating frame hashes for both videos...") + future1 = executor.submit(extract_framehashes, video1) + future2 = executor.submit(extract_framehashes, video2) + hashes1 = future1.result() + hashes2 = future2.result() + return hashes1, hashes2 + + +def create_diff_video(video1: Path, video2: Path, output: Path) -> None: + """Create a diff video of two clips using ffmpeg blend filter with difference mode.""" + cmd = ['ffmpeg', '-nostdin', '-i', video1, '-i', video2, '-filter_complex', 'blend=all_mode=difference', '-vsync', '0', '-y', output] subprocess.run(cmd, capture_output=True, check=True) -def find_differences(video1, video2) -> tuple[list[int], tuple[int, int]]: - print(f"Hashing frames from {video1}...") - hashes1 = extract_framehashes(video1) - - print(f"Hashing frames from {video2}...") - hashes2 = extract_framehashes(video2) - - print(f"Comparing {len(hashes1)} frames...") +def find_frame_differences(hashes1: list[str], hashes2: list[str]) -> list[int]: + """Compare two lists of frame hashes and return the indices of different frames.""" different_frames = [] - for i, (h1, h2) in enumerate(zip(hashes1, hashes2, strict=False)): if h1 != h2: different_frames.append(i) - - return different_frames, (len(hashes1), len(hashes2)) + return different_frames -def generate_html_report(videos: tuple[str, str], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name): - chunks = [] - if different_frames: - current_chunk = [different_frames[0]] - for i in range(1, len(different_frames)): - if different_frames[i] == different_frames[i - 1] + 1: - current_chunk.append(different_frames[i]) - else: - chunks.append(current_chunk) - current_chunk = [different_frames[i]] - chunks.append(current_chunk) - +def generate_html_report(videos: tuple[Path, Path], basedir: str, different_frames: list[int], frame_counts: tuple[int, int], diff_video_name: str) -> str: total_frames = max(frame_counts) frame_delta = frame_counts[1] - frame_counts[0] different_total = len(different_frames) + abs(frame_delta) @@ -71,12 +63,13 @@ def generate_html_report(videos: tuple[str, str], basedir: str, different_frames else f"❌ Found {different_total} different frames out of {total_frames} total ({different_total / total_frames * 100:.1f}%)." + (f" Video {'2' if frame_delta > 0 else '1'} is longer by {abs(frame_delta)} frames." if frame_delta != 0 else "") ) + print(f" Results: {result_text}") # Load HTML template and replace placeholders html = HTML_TEMPLATE_PATH.read_text() placeholders = { - "VIDEO1_SRC": os.path.join(basedir, os.path.basename(videos[0])), - "VIDEO2_SRC": os.path.join(basedir, os.path.basename(videos[1])), + "VIDEO1_SRC": os.path.join(basedir, videos[0].name), + "VIDEO2_SRC": os.path.join(basedir, videos[1].name), "DIFF_SRC": os.path.join(basedir, diff_video_name), "RESULT_TEXT": result_text, } @@ -91,7 +84,7 @@ def main(): parser.add_argument('video1', help='First video file') parser.add_argument('video2', help='Second video file') parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)') - parser.add_argument("--basedir", type=str, help="Base directory for output", default="") + parser.add_argument("--basedir", type=str, help="Base path for files in HTML report", default="") parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser') args = parser.parse_args() @@ -99,37 +92,56 @@ def main(): if not args.output.lower().endswith('.html'): args.output += '.html' + video1, video2 = Path(args.video1), Path(args.video2) + missing = [str(p) for p in (video1, video2) if not p.is_file()] + if missing: + parser.error(f"Video file(s) not found: {', '.join(missing)}") + + diff_video_name = f"{Path(args.output).stem}.mp4" # diff video name derived from output HTML name + os.makedirs(DIFF_OUT_DIR, exist_ok=True) print("=" * 60) - print("VIDEO DIFF - HTML REPORT") + print("UI VIDEO DIFF - HTML REPORT") print("=" * 60) - print(f"Video 1: {args.video1}") - print(f"Video 2: {args.video2}") - print(f"Output: {args.output}") + print(f"Video 1: {video1}") + print(f"Video 2: {video2}") + print(f"HTML output: {args.output}") + print(f"Diff video: {diff_video_name}") print() - # Create diff video with name derived from output HTML - diff_video_name = Path(args.output).stem + '.mp4' - diff_video_path = str(DIFF_OUT_DIR / diff_video_name) - create_diff_video(args.video1, args.video2, diff_video_path) + print("[1/4] Starting full video diff generation in background thread...") + diff_executor = ThreadPoolExecutor(max_workers=1) + diff_future = diff_executor.submit(create_diff_video, video1, video2, DIFF_OUT_DIR / diff_video_name) - different_frames, frame_counts = find_differences(args.video1, args.video2) + print("[2/4] Hashing frames...") + hashes1, hashes2 = get_video_frame_hashes(video1, video2) + frame_counts = (len(hashes1), len(hashes2)) + print(f" Found {frame_counts[0]} frames in video 1 and {frame_counts[1]} frames in video 2.") - if different_frames is None: - sys.exit(1) + print("[3/4] Finding different frames...") + different_frames = find_frame_differences(hashes1, hashes2) + print(f" Found {len(different_frames)} different frames.") - print() - print("Generating HTML report...") - html = generate_html_report((args.video1, args.video2), args.basedir, different_frames, frame_counts, diff_video_name) + print("[4/4] Generating HTML report...") + html = generate_html_report((video1, video2), args.basedir, different_frames, frame_counts, diff_video_name) - with open(DIFF_OUT_DIR / args.output, 'w') as f: + output_path = DIFF_OUT_DIR / args.output + with open(output_path, 'w') as f: f.write(html) + print(f" Report generated at: {output_path}") + # Open in browser by default if not args.no_open: print(f"Opening {args.output} in browser...") - webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}') + webbrowser.open(f'file://{os.path.abspath(output_path)}') + + # Wait for diff video generation to finish before exiting + if not diff_future.done(): + print("Waiting for diff video generation to finish...") + diff_future.result() + diff_executor.shutdown() extra_frames = abs(frame_counts[0] - frame_counts[1]) return 0 if (len(different_frames) + extra_frames) == 0 else 1 diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index e01ddb2513..fc7e40920a 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -3,7 +3,7 @@ import os import time from cereal import messaging -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.common.realtime import Priority, config_realtime_process, set_core_affinity from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 7ba537ce6f..4a93ae40d2 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -1,4 +1,3 @@ -import pyray as rl import numpy as np import time import threading @@ -11,7 +10,7 @@ from openpilot.common.realtime import drop_realtime from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP @@ -148,7 +147,7 @@ class UIState(UIStateSP): # Check ignition status across all pandas if self.panda_type != log.PandaState.PandaType.unknown: self.ignition = any(state.ignitionLine or state.ignitionCan for state in panda_states) - elif self.sm.frame - self.sm.recv_frame["pandaStates"] > 5 * rl.get_fps(): + elif not self.sm.alive["pandaStates"]: self.panda_type = log.PandaState.PandaType.unknown # Handle wide road camera state updates diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index 110ca714a9..88ccc5df7e 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel diff --git a/sunnypilot/mads/mads.py b/sunnypilot/mads/mads.py index bd9c8d78ff..b0a90f82b5 100644 --- a/sunnypilot/mads/mads.py +++ b/sunnypilot/mads/mads.py @@ -69,7 +69,7 @@ class ModularAssistiveDrivingSystem: return False def should_silent_lkas_enable(self, CS: structs.CarState) -> bool: - if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and self.pedal_pressed_non_gas_pressed(CS): + if self.steering_mode_on_brake == MadsSteeringModeOnBrake.PAUSE and (CS.brakePressed or CS.regenBraking or self.pedal_pressed_non_gas_pressed(CS)): return False if self.events_sp.contains_in_list(GEARS_ALLOW_PAUSED_SILENT): diff --git a/sunnypilot/mads/tests/test_mads_steering_mode.py b/sunnypilot/mads/tests/test_mads_steering_mode.py new file mode 100644 index 0000000000..8765324cfd --- /dev/null +++ b/sunnypilot/mads/tests/test_mads_steering_mode.py @@ -0,0 +1,242 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import pytest + +from cereal import log, custom +from opendbc.car import structs +from openpilot.selfdrive.selfdrived.events import Events +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param +from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem +from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP + +State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState +EventName = log.OnroadEvent.EventName +EventNameSP = custom.OnroadEventSP.EventName +SafetyModel = structs.CarParams.SafetyModel + + +def make_car_state(brake_pressed=False, regen_braking=False, standstill=False, v_ego=0.0): + cs = structs.CarState() + cs.brakePressed = brake_pressed + cs.regenBraking = regen_braking + cs.standstill = standstill + cs.vEgo = v_ego + cs.cruiseState.available = True + return cs + + +def make_panda_state(mocker, controls_allowed_lateral=True): + ps = mocker.MagicMock() + ps.controlsAllowedLateral = controls_allowed_lateral + ps.safetyModel = SafetyModel.hyundai + return ps + + +def make_mads(mocker, steering_mode): + sd = mocker.MagicMock() + sd.CP = structs.CarParams() + sd.CP.brand = "hyundai" + sd.CP_SP = structs.CarParamsSP() + sd.params = mocker.MagicMock() + sd.params.get_bool = mocker.MagicMock(side_effect=lambda k: { + "Mads": True, "MadsMainCruiseAllowed": False, + "DisengageOnAccelerator": True, "MadsUnifiedEngagementMode": False, + }.get(k, False)) + sd.params.get = mocker.MagicMock(return_value=steering_mode) + sd.events = Events() + sd.events_sp = EventsSP() + sd.enabled = False + sd.enabled_prev = False + sd.initialized = True + sd.CS_prev = make_car_state() + sd.sm = {'pandaStates': [make_panda_state(mocker)]} + sd.state_machine = mocker.MagicMock() + + mads = ModularAssistiveDrivingSystem(sd) + mads.enabled_toggle = True + mads.steering_mode_on_brake = steering_mode + return mads, sd + + +def run_frames(mads, sd, cs, n=1): + for _ in range(n): + mads.update(cs) + sd.CS_prev = cs + sd.events.clear() + sd.events_sp.clear() + + +# should_silent_lkas_enable across all modes + +class TestShouldSilentLkasEnable: + @pytest.mark.parametrize("brake,regen", [(True, False), (False, True)]) + def test_pause_blocks_reenable_on_braking_at_standstill(self, mocker, brake, regen): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + cs = make_car_state(brake_pressed=brake, regen_braking=regen, standstill=True) + assert mads.should_silent_lkas_enable(cs) is False + + def test_pause_allows_reenable_on_brake_release(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + cs = make_car_state(standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + def test_remain_active_ignores_brake(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.REMAIN_ACTIVE) + cs = make_car_state(brake_pressed=True, standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + def test_disengage_ignores_brake_for_silent_enable(self, mocker): + mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + cs = make_car_state(brake_pressed=True, standstill=True) + assert mads.should_silent_lkas_enable(cs) is True + + +# pause + +class TestPauseMode: + def test_stays_paused_at_standstill_brake_held(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=15.0)) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.state_machine.state == State.paused + + def test_resumes_on_brake_release_at_standstill(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.paused + mads.enabled = True + mads.active = False + + run_frames(mads, sd, make_car_state(standstill=True)) + assert mads.state_machine.state == State.enabled + + def test_full_cycle_moving_to_standstill(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=15.0)) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.state_machine.state == State.paused + + sd.sm['pandaStates'] = [make_panda_state(mocker, True)] + run_frames(mads, sd, make_car_state(standstill=True)) + assert mads.state_machine.state == State.enabled + + +# disengage + +class TestDisengageMode: + def test_brake_while_enabled_disables(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=10.0)) + assert mads.state_machine.state == State.disabled + + def test_brake_sends_lkas_disable_when_enabled(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + mads.update_events(make_car_state(brake_pressed=True, v_ego=5.0)) + assert sd.events_sp.has(EventNameSP.lkasDisable) + + +# remain active + +class TestRemainActiveMode: + def test_brake_does_not_pause_or_disable(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.REMAIN_ACTIVE) + mads.state_machine.state = State.enabled + mads.enabled = True + mads.active = True + + sd.events.add(EventName.pedalPressed) + run_frames(mads, sd, make_car_state(brake_pressed=True, v_ego=10.0)) + assert mads.state_machine.state == State.enabled + + +# lateral mismatch counter + +class TestLateralMismatchCounter: + def test_no_accumulation_while_paused(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.state_machine.state = State.paused + mads.enabled = True + mads.active = False + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + + run_frames(mads, sd, make_car_state(brake_pressed=True, standstill=True), n=250) + assert mads.lateral_mismatch_counter == 0 + + def test_accumulates_when_active_and_panda_disagrees(self, mocker): + mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) + mads.enabled = True + mads.active = True + sd.sm['pandaStates'] = [make_panda_state(mocker, False)] + + for _ in range(200): + mads.data_sample() + assert mads.lateral_mismatch_counter == 200 + + +# brand restrictions + +class TestBrandSteeringModeRestrictions: + def test_rivian_forced_to_disengage(self, mocker): + CP = structs.CarParams() + CP.brand = "rivian" + CP_SP = structs.CarParamsSP() + params = mocker.MagicMock() + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE + params.get.assert_not_called() + + def test_tesla_without_vehicle_bus_forced_to_disengage(self, mocker): + CP = structs.CarParams() + CP.brand = "tesla" + CP_SP = structs.CarParamsSP() + CP_SP.flags = 0 + params = mocker.MagicMock() + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE + + def test_tesla_with_vehicle_bus_uses_param(self, mocker): + CP = structs.CarParams() + CP.brand = "tesla" + CP_SP = structs.CarParamsSP() + CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS + params = mocker.MagicMock() + params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE + + @pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"]) + def test_other_brands_use_param(self, mocker, brand): + CP = structs.CarParams() + CP.brand = brand + CP_SP = structs.CarParamsSP() + params = mocker.MagicMock() + params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE) + assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE diff --git a/sunnypilot/mapd/mapd_installer.py b/sunnypilot/mapd/mapd_installer.py index 0e287ed405..1e36732dda 100755 --- a/sunnypilot/mapd/mapd_installer.py +++ b/sunnypilot/mapd/mapd_installer.py @@ -16,7 +16,7 @@ from urllib.request import urlopen from cereal import messaging from openpilot.common.params import Params -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.spinner import Spinner from openpilot.system.version import is_prebuilt from openpilot.sunnypilot.mapd import MAPD_PATH, MAPD_BIN_DIR diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 88ee564190..084af50408 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -17,7 +17,7 @@ from openpilot.common.realtime import Ratekeeper, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.mapd import MAPD_PATH from openpilot.sunnypilot.mapd.mapd_installer import VERSION, update_installed_version diff --git a/sunnypilot/modeld_v2/SConscript b/sunnypilot/modeld_v2/SConscript index a5b0bad862..723a1b3408 100644 --- a/sunnypilot/modeld_v2/SConscript +++ b/sunnypilot/modeld_v2/SConscript @@ -3,7 +3,7 @@ import glob from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC Import('env', 'arch', 'release') lenv = env.Clone() diff --git a/sunnypilot/modeld_v2/install_models_pc.py b/sunnypilot/modeld_v2/install_models_pc.py index 1bba001abd..8000c01cbe 100755 --- a/sunnypilot/modeld_v2/install_models_pc.py +++ b/sunnypilot/modeld_v2/install_models_pc.py @@ -5,7 +5,7 @@ import pickle import codecs from pathlib import Path -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index b124e3645a..08d3e9d476 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. """ import os -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI os.environ['DEV'] = 'QCOM' if TICI else 'CPU' USBGPU = "USBGPU" in os.environ if USBGPU: @@ -54,7 +54,7 @@ def _find_driving_pkl(bundle): return override if bundle is None or not bundle.models: return None - from openpilot.system.hardware.hw import Paths + from openpilot.common.hardware.hw import Paths model_root = Paths.model_root() pkl_name = bundle.models[0].artifact.fileName diff --git a/sunnypilot/modeld_v2/tests/conftest.py b/sunnypilot/modeld_v2/tests/conftest.py index 89e2f2d4cc..f79cbe10b2 100644 --- a/sunnypilot/modeld_v2/tests/conftest.py +++ b/sunnypilot/modeld_v2/tests/conftest.py @@ -192,7 +192,7 @@ def patch_modeld(monkeypatch): @pytest.fixture def model_state_factory(tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw def _create(archetype): write_pkl(tmp_path, archetype) diff --git a/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index 38f6cd446b..b13a7abecf 100644 --- a/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -35,7 +35,7 @@ class TestFindDrivingPkl: def test_finds_pkl_by_artifact_name(self, tmp_path, monkeypatch): (tmp_path / 'driving_fof_tinygrad.pkl').write_bytes(b'fake') - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path))) bundle = DummyBundle(models=[ @@ -67,13 +67,20 @@ class TestStockEquivalence: state = model_state_factory(ARCHETYPES['vision_policy_split']) frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES) - stock_queues, stock_npy = make_input_queues(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES, frame_skip, - device='NPY') + # action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below + stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)} + stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY') # TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue) - skip_keys = {'action_t'} - assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \ - f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}" + # prev_feat is a stock QCOM corruption workaround handled inside the SP loader's JIT path + skip_keys = {'action_t', 'prev_feat'} + # stock packs the per-key policy inputs into packed_npy_inputs; the npy views carry the individual keys + stock_queue_keys = set(stock_queues.keys()) + if 'packed_npy_inputs' in stock_queue_keys: + stock_queue_keys.remove('packed_npy_inputs') + stock_queue_keys |= set(stock_npy.keys()) + assert set(state.input_queues.keys()) == stock_queue_keys - skip_keys, \ + f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={stock_queue_keys}" assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \ f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}" @@ -223,7 +230,7 @@ class TestMlsimProperty: assert state.mlsim is False def test_mlsim_true_for_gen11(self, tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl, ARCHETYPES as A arch = A['supercombo_non20hz'] @@ -238,7 +245,7 @@ class TestMlsimProperty: class TestCrossArchetypeMismatch: def test_wrong_is_20hz_changes_constants(self, tmp_path, monkeypatch, patch_modeld): - from openpilot.system.hardware import hw + from openpilot.common.hardware import hw from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl from openpilot.sunnypilot.modeld_v2.constants import ModelConstants diff --git a/sunnypilot/models/default_model.py b/sunnypilot/models/default_model.py index 1d1064863f..552f3a5769 100755 --- a/sunnypilot/models/default_model.py +++ b/sunnypilot/models/default_model.py @@ -8,15 +8,13 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "model_name.py") MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash") -VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx") -POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx") +SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_supercombo.onnx") def update_model_hash(): - vision_hash = get_file_hash(VISION_ONNX_PATH) - policy_hash = get_file_hash(POLICY_ONNX_PATH) + supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() with open(MODEL_HASH_PATH, "w") as f: f.write(combined_hash) diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 0fa94e39ff..e81c3a5365 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -15,7 +15,7 @@ from cereal import custom from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO REQUIRED_JSON_VERSION = 15 diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 518671181e..02730dd270 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -13,7 +13,7 @@ import aiohttp from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from cereal import messaging, custom from openpilot.sunnypilot.models.fetcher import ModelFetcher diff --git a/sunnypilot/models/runners/constants.py b/sunnypilot/models/runners/constants.py index acb316888c..797a1d04b5 100644 --- a/sunnypilot/models/runners/constants.py +++ b/sunnypilot/models/runners/constants.py @@ -1,6 +1,6 @@ import os import numpy as np -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from cereal import custom # Type definitions for clarity diff --git a/sunnypilot/models/runners/model_runner.py b/sunnypilot/models/runners/model_runner.py index 051fa349db..cbf2fc5e20 100644 --- a/sunnypilot/models/runners/model_runner.py +++ b/sunnypilot/models/runners/model_runner.py @@ -3,7 +3,7 @@ from abc import abstractmethod, ABC import numpy as np from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.runners.constants import NumpyDict, ShapeDict, Model, SliceDict, SEND_RAW_PRED -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths import pickle CUSTOM_MODEL_PATH = Paths.model_root() diff --git a/sunnypilot/models/runners/tinygrad/model_types.py b/sunnypilot/models/runners/tinygrad/model_types.py index 015adc035f..295e75afb5 100644 --- a/sunnypilot/models/runners/tinygrad/model_types.py +++ b/sunnypilot/models/runners/tinygrad/model_types.py @@ -6,7 +6,7 @@ from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as Combine from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser from openpilot.sunnypilot.models.runners.constants import ModelType, NumpyDict from openpilot.sunnypilot.models.runners.model_runner import ModularRunner -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index f363f8309a..13a16294ff 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf \ No newline at end of file +49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809 \ No newline at end of file diff --git a/sunnypilot/models/tests/test_default_model.py b/sunnypilot/models/tests/test_default_model.py index 7c2fde70a8..ab51027442 100644 --- a/sunnypilot/models/tests/test_default_model.py +++ b/sunnypilot/models/tests/test_default_model.py @@ -6,16 +6,15 @@ See the LICENSE.md file in the root directory for more details. """ from openpilot.sunnypilot import get_file_hash -from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH +from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH import hashlib class TestDefaultModel: def test_compare_onnx_hashes(self): - vision_hash = get_file_hash(VISION_ONNX_PATH) - policy_hash = get_file_hash(POLICY_ONNX_PATH) + supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) - combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest() + combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() with open(MODEL_HASH_PATH) as f: current_hash = f.read().strip() diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 27b44fb4fc..e9b0e99153 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -10,7 +10,7 @@ from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus diff --git a/sunnypilot/selfdrive/selfdrived/events_base.py b/sunnypilot/selfdrive/selfdrived/events_base.py index 5f9f8edf94..402121f2fe 100644 --- a/sunnypilot/selfdrive/selfdrived/events_base.py +++ b/sunnypilot/selfdrive/selfdrived/events_base.py @@ -6,7 +6,7 @@ from collections.abc import Callable from cereal import log, car import cereal.messaging as messaging from openpilot.common.realtime import DT_CTRL -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index a881faca04..7ca619721b 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -8,8 +8,8 @@ from datetime import datetime, timedelta, UTC from openpilot.common.api.base import BaseApi from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.hw import Paths API_HOST = os.getenv('SUNNYLINK_API_HOST', 'https://stg.api.sunnypilot.ai') UNREGISTERED_SUNNYLINK_DONGLE_ID = "UnregisteredDevice" @@ -47,16 +47,16 @@ class SunnylinkApi(BaseApi): return sunnylink_dongle_id, comma_dongle_id def _resolve_imeis(self): - imei1, imei2 = None, None + imei = None imei_try = 0 - while imei1 is None and imei2 is None and imei_try < MAX_RETRIES: + while imei is None and imei_try < MAX_RETRIES: try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) + imei = HARDWARE.get_imei() except Exception: self._status_update(f"Error getting imei, trying again... [{imei_try + 1}/{MAX_RETRIES}]") time.sleep(1) imei_try += 1 - return imei1, imei2 + return imei, "" def _resolve_serial(self): return (self.params.get("HardwareSerial") diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index fff6e1bfe6..1a7e69f31c 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -21,7 +21,7 @@ from functools import partial from openpilot.common.params import Params, ParamKeyType from openpilot.common.realtime import set_core_affinity from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler, stat_handler from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, diff --git a/sunnypilot/sunnylink/backups/utils.py b/sunnypilot/sunnylink/backups/utils.py index eb59205128..b479b0aaf5 100644 --- a/sunnypilot/sunnylink/backups/utils.py +++ b/sunnypilot/sunnylink/backups/utils.py @@ -18,7 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa, ec from openpilot.common.api.base import KEYS from openpilot.sunnypilot.sunnylink.backups.AESCipher import AESCipher -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class KeyDerivation: diff --git a/sunnypilot/sunnylink/capabilities.py b/sunnypilot/sunnylink/capabilities.py index fbdcf8f04a..197e404314 100644 --- a/sunnypilot/sunnylink/capabilities.py +++ b/sunnypilot/sunnylink/capabilities.py @@ -12,7 +12,7 @@ from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE # Wire-protocol version for the capabilities payload. Bump on breaking changes diff --git a/sunnypilot/sunnylink/statsd.py b/sunnypilot/sunnylink/statsd.py index 233b531e85..f1ee18decd 100755 --- a/sunnypilot/sunnylink/statsd.py +++ b/sunnypilot/sunnylink/statsd.py @@ -14,9 +14,9 @@ from datetime import datetime, UTC from openpilot.common.params import Params from cereal.messaging import SubMaster -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.utils import atomic_write from openpilot.system.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py index 0b7eb78edb..c1da47fa0c 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -15,7 +15,7 @@ from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog diff --git a/sunnypilot/system/sensord/tests/test_sensord.py b/sunnypilot/system/sensord/tests/test_sensord.py index dc5f93d97b..9e09930f0c 100644 --- a/sunnypilot/system/sensord/tests/test_sensord.py +++ b/sunnypilot/system/sensord/tests/test_sensord.py @@ -9,7 +9,7 @@ from cereal import log from cereal.services import SERVICE_LIST from openpilot.common.gpio import get_irqs_for_action from openpilot.common.timeout import Timeout -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.manager.process_config import managed_processes BMX = { diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 9b3262ad99..3d61a723f6 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -4,6 +4,7 @@ from __future__ import annotations import base64 import hashlib import io +import itertools import json import os import queue @@ -35,17 +36,16 @@ from openpilot.common.api import Api, get_key_pair from openpilot.common.utils import CallbackReader, get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_build_metadata -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths ATHENA_HOST = os.getenv('ATHENA_HOST', 'wss://athena.comma.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) LOCAL_PORT_WHITELIST = {22, } # SSH -WEBRTCD_PORT = 5001 LOG_ATTR_NAME = 'user.upload' LOG_ATTR_VALUE_MAX_UNIX_TIME = int.to_bytes(2147483647, 4, sys.byteorder) @@ -58,6 +58,9 @@ WS_FRAME_SIZE = 4096 DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority +SEND_PRIORITY_HIGH = 0 +SEND_PRIORITY_LOW = 1 + # https://bytesolutions.com/dscp-tos-cos-precedence-conversion-chart, # https://en.wikipedia.org/wiki/Differentiated_services UPLOAD_TOS = 0x20 # CS1, low priority background traffic @@ -127,14 +130,18 @@ class UploadItem: dispatcher["echo"] = lambda s: s recv_queue: Queue[str] = queue.Queue() -send_queue: Queue[str] = queue.Queue() +send_queue: Queue[tuple[int, int, str]] = queue.PriorityQueue() upload_queue: Queue[UploadItem] = queue.PriorityQueue() -low_priority_send_queue: Queue[str] = queue.Queue() log_recv_queue: Queue[str] = queue.Queue() cancelled_uploads: set[str] = set() cur_upload_items: dict[int, UploadItem | None] = {} +send_seq = itertools.count() +def send_queue_push(data: str, priority: int) -> None: + assert priority is not None, "send queue priority must be specified" + send_queue.put_nowait((priority, next(send_seq), data)) # tie-break with a monotonic counter + # TODO-SP: adapt zst for sunnylink def strip_zst_extension(fn: str) -> str: @@ -210,7 +217,7 @@ def jsonrpc_handler(end_event: threading.Event, localProxyHandler = None) -> Non if "method" in data: cloudlog.event("athena.jsonrpc_handler.call_method", data=data) response = JSONRPCResponseManager.handle(data, dispatcher) - send_queue.put_nowait(response.json) + send_queue_push(response.json, SEND_PRIORITY_HIGH) elif "id" in data and ("result" in data or "error" in data): log_recv_queue.put_nowait(data) else: @@ -219,7 +226,7 @@ def jsonrpc_handler(end_event: threading.Event, localProxyHandler = None) -> Non pass except Exception as e: cloudlog.exception("athena jsonrpc handler failed") - send_queue.put_nowait(json.dumps({"error": str(e)})) + send_queue_push(json.dumps({"error": str(e)}), SEND_PRIORITY_HIGH) def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = True) -> None: @@ -595,37 +602,28 @@ def getNetworkMetered() -> bool: @dispatcher.add_method -def getNetworks(): - return HARDWARE.get_networks() - - -@dispatcher.add_method -def startStream(sdp: str) -> dict: - from openpilot.system.webrtc.webrtcd import StreamRequestBody +def startStream(sdp: str, enabled: bool) -> dict: + from openpilot.system.webrtc.helpers import StreamRequestBody, post_stream_request, wait_for_webrtcd + params = Params() bridge_services_in = [] - # get live car params to avoid stale notCar edge case - cp_bytes = Params().get("CarParams") + # stale car params case taken care of by webrtcd being shut off on ignition + cp_bytes = Params().get("CarParamsPersistent") if cp_bytes is not None: with car.CarParams.from_bytes(cp_bytes) as CP: if CP.notCar: bridge_services_in.append("testJoystick") + else: + raise Exception("failed to get CarParamsPersistent") - body = StreamRequestBody(sdp, "wideRoad", bridge_services_in, ["carState"]) - try: - resp = requests.post(f"http://localhost:{WEBRTCD_PORT}/stream", - json=asdict(body), timeout=10) - if not resp.ok: - try: - error_body = resp.json() - raise Exception(error_body.get("message", f"webrtcd returned {resp.status_code}")) - except ValueError: - resp.raise_for_status() - return resp.json() - except requests.ConnectTimeout as e: - raise Exception("webrtc took too long to respond. is it on?") from e - except requests.ConnectionError as e: - raise Exception("webrtc is not running. turn on comma body ignition.") from e + if not params.get_bool("IsOnroad"): + # manager owns camerad/stream_encoderd/webrtcd; flip the param and let it bring them up. + # webrtcd clears IsLiveStreaming when the session ends + params.put_bool("IsLiveStreaming", True) + # wait for webrtcd end points to wake up + wait_for_webrtcd() + + return post_stream_request(StreamRequestBody(sdp, "wideRoad", enabled, bridge_services_in, ["carState", "deviceState"])) @dispatcher.add_method @@ -716,12 +714,12 @@ def add_log_to_queue(log_path, log_id, is_sunnylink=False): if is_sunnylink and size_in_bytes <= MAX_SIZE_BYTES: cloudlog.debug(f"Target is sunnylink and log file {log_path} is small enough to send in one request ({size_in_bytes} bytes).") - low_priority_send_queue.put_nowait(jsonrpc_str) + send_queue_push(jsonrpc_str, SEND_PRIORITY_LOW) elif is_sunnylink: cloudlog.warning(f"Target is sunnylink and log file {log_path} is too large to send in one request.") else: cloudlog.debug(f"Target is not sunnylink, proceeding to send log file {log_path} in one request ({size_in_bytes} bytes).") - low_priority_send_queue.put_nowait(jsonrpc_str) + send_queue_push(jsonrpc_str, SEND_PRIORITY_LOW) def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None: @@ -815,7 +813,7 @@ def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) if is_sunnylink and is_compressed: jsonrpc["params"]["compressed"] = is_compressed - low_priority_send_queue.put_nowait(json.dumps(jsonrpc)) + send_queue_push(json.dumps(jsonrpc), SEND_PRIORITY_LOW) os.remove(stat_path) last_scan = curr_scan except Exception: @@ -897,10 +895,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: def ws_send(ws: WebSocket, end_event: threading.Event) -> None: while not end_event.is_set(): try: - try: - data = send_queue.get_nowait() - except queue.Empty: - data = low_priority_send_queue.get(timeout=1) + _, _, data = send_queue.get(timeout=1) for i in range(0, len(data), WS_FRAME_SIZE): frame = data[i:i+WS_FRAME_SIZE] last = i + WS_FRAME_SIZE >= len(data) diff --git a/system/athena/manage_athenad.py b/system/athena/manage_athenad.py index 98557db06a..67682dd86e 100755 --- a/system/athena/manage_athenad.py +++ b/system/athena/manage_athenad.py @@ -6,7 +6,7 @@ from multiprocessing import Process from openpilot.common.params import Params from openpilot.system.manager.process import launcher from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.version import get_build_metadata ATHENA_MGR_PID_PARAM = "AthenadPid" diff --git a/system/athena/registration.py b/system/athena/registration.py index bc7cf6c748..f8ff40ca79 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -10,8 +10,8 @@ from openpilot.common.api import api_get, get_key_pair from openpilot.common.params import Params from openpilot.common.spinner import Spinner from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, PC -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE, PC +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog @@ -54,17 +54,16 @@ def register(show_spinner=False) -> str | None: # Block until we get the imei serial = HARDWARE.get_serial() start_time = time.monotonic() - imei1: str | None = None - imei2: str | None = None - while imei1 is None and imei2 is None: + imei: str | None = None + while imei is None: try: - imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1) + imei = HARDWARE.get_imei() except Exception: cloudlog.exception("Error getting imei, trying again...") time.sleep(1) if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + spinner.update(f"registering device - serial: {serial}, IMEI: {imei}") backoff = 0 start_time = time.monotonic() @@ -75,7 +74,7 @@ def register(show_spinner=False) -> str | None: 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) + imei=imei, imei2="", serial=serial, public_key=public_key, register_token=register_token) if resp.status_code in (402, 403): cloudlog.info(f"Unable to register device, got {resp.status_code}") @@ -90,7 +89,7 @@ def register(show_spinner=False) -> str | None: time.sleep(backoff) if time.monotonic() - start_time > 60 and show_spinner: - spinner.update(f"registering device - serial: {serial}, IMEI: ({imei1}, {imei2})") + spinner.update(f"registering device - serial: {serial}, IMEI: {imei}") return UNREGISTERED_DONGLE_ID # hotfix to prevent an infinite wait for registration if show_spinner: diff --git a/system/athena/tests/test_athenad.py b/system/athena/tests/test_athenad.py index b0c20a26a9..0e3ae68d4e 100644 --- a/system/athena/tests/test_athenad.py +++ b/system/athena/tests/test_athenad.py @@ -22,7 +22,7 @@ from openpilot.system.athena import athenad from openpilot.system.athena.athenad import MAX_RETRY_COUNT, UPLOAD_SESS, dispatcher from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket from openpilot.selfdrive.test.helpers import http_server_context -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def seed_athena_server(host, port): @@ -422,11 +422,11 @@ class TestAthenadMethods: try: # with params athenad.recv_queue.put_nowait(json.dumps({"method": "echo", "params": ["hello"], "jsonrpc": "2.0", "id": 0})) - resp = athenad.send_queue.get(timeout=3) + _, _, resp = athenad.send_queue.get(timeout=3) assert json.loads(resp) == {'result': 'hello', 'id': 0, 'jsonrpc': '2.0'} # without params athenad.recv_queue.put_nowait(json.dumps({"method": "getNetworkType", "jsonrpc": "2.0", "id": 0})) - resp = athenad.send_queue.get(timeout=3) + _, _, resp = athenad.send_queue.get(timeout=3) assert json.loads(resp) == {'result': 1, 'id': 0, 'jsonrpc': '2.0'} # log forwarding athenad.recv_queue.put_nowait(json.dumps({'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'})) diff --git a/system/athena/tests/test_athenad_ping.py b/system/athena/tests/test_athenad_ping.py index 8ff1e37a5d..7f44263162 100644 --- a/system/athena/tests/test_athenad_ping.py +++ b/system/athena/tests/test_athenad_ping.py @@ -8,7 +8,7 @@ from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad from openpilot.system.manager.helpers import write_onroad_params -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI TIMEOUT_TOLERANCE = 20 # seconds diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py index f5d900d4db..bb1523de80 100644 --- a/system/athena/tests/test_registration.py +++ b/system/athena/tests/test_registration.py @@ -5,7 +5,7 @@ from pathlib import Path from openpilot.common.params import Params from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.system.athena.tests.helpers import MockResponse -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class TestRegistration: diff --git a/system/camerad/snapshot.py b/system/camerad/snapshot.py index ec6cbc8ad3..241f027044 100755 --- a/system/camerad/snapshot.py +++ b/system/camerad/snapshot.py @@ -9,7 +9,7 @@ import cereal.messaging as messaging from msgq.visionipc import VisionIpcClient, VisionStreamType from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.manager.process_config import managed_processes diff --git a/tools/webcam/README.md b/system/camerad/webcam/README.md similarity index 63% rename from tools/webcam/README.md rename to system/camerad/webcam/README.md index 6abbc47935..2c0ce6a8af 100644 --- a/tools/webcam/README.md +++ b/system/camerad/webcam/README.md @@ -1,13 +1,5 @@ # Run openpilot with webcam on PC -What's needed: -- Ubuntu 24.04 ([WSL2 is not supported](https://github.com/commaai/openpilot/issues/34216)) or macOS -- GPU (recommended) -- One USB webcam, at least 720p and 78 degrees FOV (e.g. Logitech C920/C615, NexiGo N60) -- [Car harness](https://comma.ai/shop/products/comma-car-harness) -- [panda](https://comma.ai/shop/panda) -- USB-A to USB-A cable to connect panda to your computer - ## Setup openpilot - Follow [this readme](../README.md) to install and build the requirements @@ -16,6 +8,7 @@ What's needed: - Connect your computer to panda ## GO + ``` USE_WEBCAM=1 system/manager/manager.py ``` diff --git a/tools/webcam/camera.py b/system/camerad/webcam/camera.py similarity index 100% rename from tools/webcam/camera.py rename to system/camerad/webcam/camera.py diff --git a/tools/webcam/camerad.py b/system/camerad/webcam/camerad.py similarity index 97% rename from tools/webcam/camerad.py rename to system/camerad/webcam/camerad.py index f1e70d948a..401eec106f 100755 --- a/tools/webcam/camerad.py +++ b/system/camerad/webcam/camerad.py @@ -7,7 +7,7 @@ from collections import namedtuple from msgq.visionipc import VisionIpcServer, VisionStreamType from cereal import messaging -from openpilot.tools.webcam.camera import Camera +from openpilot.system.camerad.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper ROAD_CAM = os.getenv("ROAD_CAM", "0") diff --git a/system/hardware/fan_controller.py b/system/hardware/fan_controller.py index eafde599ad..869fe19c3d 100755 --- a/system/hardware/fan_controller.py +++ b/system/hardware/fan_controller.py @@ -2,7 +2,7 @@ import numpy as np from openpilot.common.pid import PIDController -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE # raise fan setpoint on tici/tizi to reduce noise # after raising LMH threshold in AGNOS 18.1 to prevent CPU throttling diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 6c84ccfe8a..eb7848edfd 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -17,7 +17,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import HARDWARE, TICI, AGNOS +from openpilot.common.hardware import HARDWARE, TICI from openpilot.system.loggerd.config import get_available_percent from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog @@ -106,8 +106,6 @@ def hw_state_thread(end_event, hw_queue): count = 0 prev_hw_state = None - modem_version = None - while not end_event.is_set(): # these are expensive calls. update every 10s if (count % int(10. / DT_HW)) == 0: @@ -117,13 +115,6 @@ def hw_state_thread(end_event, hw_queue): if len(modem_temps) == 0 and prev_hw_state is not None: modem_temps = prev_hw_state.modem_temps - # Log modem version once - if AGNOS and (modem_version is None): - modem_version = HARDWARE.get_modem_version() - - if modem_version is not None: - cloudlog.event("modem version", version=modem_version) - tx, rx = HARDWARE.get_modem_data_usage() hw_state = HardwareState( diff --git a/system/hardware/pc/hardware.py b/system/hardware/pc/hardware.py deleted file mode 100644 index f3d527429c..0000000000 --- a/system/hardware/pc/hardware.py +++ /dev/null @@ -1,12 +0,0 @@ -from cereal import log -from openpilot.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/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index 50a83682cf..fdafc5e15d 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -2,7 +2,7 @@ import time import threading from openpilot.common.params import Params -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.swaglog import cloudlog from openpilot.system.statsd import statlog diff --git a/system/hardware/tici/__init__.py b/system/hardware/tici/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json deleted file mode 100644 index 07e2079ec9..0000000000 --- a/system/hardware/tici/agnos.json +++ /dev/null @@ -1,84 +0,0 @@ -[ - { - "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb.img.xz", - "hash": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", - "hash_raw": "e8acf2a9cc7f0ce84cb803bfea9477f765c0d7b4daf26048e59651b9e6a7bfbb", - "size": 3282256, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "bea7f1a24428c3ededf672fa4fc78baf180cfbd8aafb77c974655b38517283e3" - }, - { - "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85.img.xz", - "hash": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", - "hash_raw": "758552ecf92b5569677197783bf0ccb73d7f961685308e45d3276ac9dd974f85", - "size": 98124, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "fb18cde08a98a168961ecd357e92474823046752b94e112f59fe51a6acd7197d" - }, - { - "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", - "hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", - "hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", - "size": 274432, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c" - }, - { - "name": "aop", - "url": "https://commadist.azureedge.net/agnosupdate/aop-78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1.img.xz", - "hash": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", - "hash_raw": "78b2287ca219a0811b3004c523fa0f4749e4d1fd92be3aba61699305b7943ad1", - "size": 184364, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "6c9135446bd3fc075fcee59b887a12e49029ab1f98ed8d6d1e32c73569d47de3" - }, - { - "name": "devcfg", - "url": "https://commadist.azureedge.net/agnosupdate/devcfg-f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc.img.xz", - "hash": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", - "hash_raw": "f71df3a86958c093ba3969254c4db025187eef9385427f1ade946742939b43cc", - "size": 40336, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "2a67971602012c1b43544964709da13c322786b456a8e78568b117e8b1540ce3" - }, - { - "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", - "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "size": 17487872, - "sparse": false, - "full_check": true, - "has_ab": true, - "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" - }, - { - "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", - "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", - "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", - "size": 4718592000, - "sparse": true, - "full_check": false, - "has_ab": true, - "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", - "alt": { - "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", - "size": 4718592000 - } - } -] \ No newline at end of file diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json new file mode 120000 index 0000000000..ffee992f25 --- /dev/null +++ b/system/hardware/tici/agnos.json @@ -0,0 +1 @@ +../../../common/hardware/tici/agnos.json \ No newline at end of file diff --git a/system/hardware/tici/iwlist.py b/system/hardware/tici/iwlist.py deleted file mode 100644 index 1e7c428b40..0000000000 --- a/system/hardware/tici/iwlist.py +++ /dev/null @@ -1,35 +0,0 @@ -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/system/hardware/tici/precise_power_measure.py b/system/hardware/tici/precise_power_measure.py deleted file mode 100755 index 52fe0850ab..0000000000 --- a/system/hardware/tici/precise_power_measure.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -from openpilot.system.hardware.tici.power_monitor import sample_power - -if __name__ == '__main__': - print("measuring for 5 seconds") - for _ in range(3): - pwrs = sample_power() - print(f"mean {np.mean(pwrs):.2f} std {np.std(pwrs):.2f}") diff --git a/system/hardware/tici/tests/__init__.py b/system/hardware/tici/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/system/hardware/tici/tests/compare_casync_manifest.py b/system/hardware/tici/tests/compare_casync_manifest.py deleted file mode 100755 index 7de66d91d0..0000000000 --- a/system/hardware/tici/tests/compare_casync_manifest.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import collections -import multiprocessing -import os - -import requests -from tqdm import tqdm - -import openpilot.system.hardware.tici.casync as casync - - -def get_chunk_download_size(chunk): - sha = chunk.sha.hex() - path = os.path.join(remote_url, sha[:4], sha + ".cacnk") - if os.path.isfile(path): - return os.path.getsize(path) - else: - r = requests.head(path, timeout=10) - r.raise_for_status() - return int(r.headers['content-length']) - - -if __name__ == "__main__": - - parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests') - parser.add_argument('frm') - parser.add_argument('to') - args = parser.parse_args() - - frm = casync.parse_caibx(args.frm) - to = casync.parse_caibx(args.to) - remote_url = args.to.replace('.caibx', '') - - most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0] - - frm_dict = casync.build_chunk_dict(frm) - - # Get content-length for each chunk - with multiprocessing.Pool() as pool: - szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to))) - chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs, strict=True)} - - sources: dict[str, list[int]] = { - 'seed': [], - 'remote_uncompressed': [], - 'remote_compressed': [], - } - - for chunk in to: - # Assume most common chunk is the zero chunk - if chunk.sha == most_common: - continue - - if chunk.sha in frm_dict: - sources['seed'].append(chunk.length) - else: - sources['remote_uncompressed'].append(chunk.length) - sources['remote_compressed'].append(chunk_sizes[chunk.sha]) - - print() - print("Update statistics (excluding zeros)") - print() - print("Download only with no seed:") - print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}") - print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}") - print() - print("Upgrade with seed partition:") - print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}") - sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed']) - print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") - sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed']) - print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") diff --git a/system/loggerd/config.py b/system/loggerd/config.py index d9befb5613..e6bd97b99d 100644 --- a/system/loggerd/config.py +++ b/system/loggerd/config.py @@ -1,5 +1,5 @@ import os -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths CAMERA_FPS = 20 diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py index 058f5c301d..d5c12474f8 100755 --- a/system/loggerd/deleter.py +++ b/system/loggerd/deleter.py @@ -4,7 +4,7 @@ import time import shutil import threading from pathlib import Path -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.config import get_available_bytes, get_available_percent from openpilot.system.loggerd.uploader import listdir_by_creation diff --git a/system/loggerd/encoder/encoder.h b/system/loggerd/encoder/encoder.h index 0a136f769c..696cb86737 100644 --- a/system/loggerd/encoder/encoder.h +++ b/system/loggerd/encoder/encoder.h @@ -27,6 +27,7 @@ public: virtual void encoder_open() = 0; virtual void encoder_close() = 0; virtual void set_bitrate(int bitrate) = 0; + virtual void request_keyframe() = 0; void publisher_publish(int segment_num, uint32_t idx, VisionIpcBufExtra &extra, unsigned int flags, kj::ArrayPtr header, kj::ArrayPtr dat); diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index f6399659b4..40f3bdee42 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -76,6 +76,10 @@ void FfmpegEncoder::set_bitrate(int bitrate) { LOGE("adaptive bitrate is not supported for ffmpeg encoder %s", encoder_info.publish_name); } +void FfmpegEncoder::request_keyframe() { + LOGE("keyframe request is not supported for ffmpeg encoder %s", encoder_info.publish_name); +} + int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) { assert(buf->width == this->in_width); assert(buf->height == this->in_height); diff --git a/system/loggerd/encoder/ffmpeg_encoder.h b/system/loggerd/encoder/ffmpeg_encoder.h index a85e1d3816..3f7c674db3 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.h +++ b/system/loggerd/encoder/ffmpeg_encoder.h @@ -22,6 +22,7 @@ public: void encoder_open(); void encoder_close(); void set_bitrate(int bitrate); + void request_keyframe(); private: int segment_num = -1; diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index 96a565f4ae..1b4d976b93 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -156,7 +156,6 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei EncoderSettings encoder_settings = encoder_info.get_settings(in_width); current_bitrate = encoder_settings.bitrate; - adaptive_bitrate = encoder_info.adaptive_bitrate; bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C; struct v4l2_format fmt_out = { @@ -307,7 +306,7 @@ void V4LEncoder::encoder_close() { } void V4LEncoder::set_bitrate(int bitrate) { - if (!adaptive_bitrate || bitrate == current_bitrate) return; + if (bitrate == current_bitrate) return; if (bitrate <= 0) { LOGE("invalid livestream encoder bitrate %d", bitrate); return; @@ -325,6 +324,17 @@ void V4LEncoder::set_bitrate(int bitrate) { current_bitrate = bitrate; } +void V4LEncoder::request_keyframe() { + struct v4l2_control ctrl = { + .id = V4L2_CID_MPEG_VIDC_VIDEO_REQUEST_IFRAME, + .value = 1, + }; + + if (util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) { + LOGE("failed to request keyframe for %s", encoder_info.publish_name); + } +} + V4LEncoder::~V4LEncoder() { encoder_close(); v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; diff --git a/system/loggerd/encoder/v4l_encoder.h b/system/loggerd/encoder/v4l_encoder.h index 2985409605..b1a84e654f 100644 --- a/system/loggerd/encoder/v4l_encoder.h +++ b/system/loggerd/encoder/v4l_encoder.h @@ -14,6 +14,7 @@ public: void encoder_open(); void encoder_close(); void set_bitrate(int bitrate); + void request_keyframe(); private: int fd; @@ -22,7 +23,6 @@ private: int segment_num = -1; int counter = 0; int current_bitrate = -1; - bool adaptive_bitrate; SafeQueue extras; diff --git a/system/loggerd/encoderd.cc b/system/loggerd/encoderd.cc index 546990041a..11db07671d 100644 --- a/system/loggerd/encoderd.cc +++ b/system/loggerd/encoderd.cc @@ -44,14 +44,18 @@ bool sync_encoders(EncoderdState *s, VisionStreamType cam_type, uint32_t frame_i } } -void apply_bitrate(std::vector> &encoders) { +void encoder_set_bitrate(std::unique_ptr &e) { static Params params; std::string val = params.get("LivestreamEncoderBitrate"); if (val.empty()) return; int bitrate = std::stoi(val); - for (auto &e : encoders) { - e->set_bitrate(bitrate); - } + e->set_bitrate(bitrate); +} + +void encoder_request_keyframe(std::unique_ptr &e) { + static Params params; + if (!params.getBool("LivestreamRequestKeyframe")) return; + e->request_keyframe(); } void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { @@ -59,9 +63,6 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { std::vector> encoders; - bool has_adaptive = std::any_of(cam_info.encoder_infos.begin(), cam_info.encoder_infos.end(), - [](const auto &ei) { return ei.adaptive_bitrate; }); - VisionIpcClient vipc_client = VisionIpcClient("camerad", cam_info.stream_type, false); std::unique_ptr jpeg_encoder; @@ -121,10 +122,13 @@ void encoder_thread(EncoderdState *s, const LogCameraInfo &cam_info) { ++cur_seg; } - if (has_adaptive) apply_bitrate(encoders); - // encode a frame for (int i = 0; i < encoders.size(); ++i) { + if (cam_info.encoder_infos[i].is_live) { + encoder_set_bitrate(encoders[i]); + encoder_request_keyframe(encoders[i]); + } + int out_id = encoders[i]->encode_frame(buf, &extra); if (out_id == -1) { diff --git a/system/loggerd/logger.h b/system/loggerd/logger.h index 18d07b5f38..dba7c2f44c 100644 --- a/system/loggerd/logger.h +++ b/system/loggerd/logger.h @@ -6,7 +6,7 @@ #include "cereal/messaging/messaging.h" #include "common/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "system/loggerd/zstd_writer.h" constexpr int LOG_COMPRESSION_LEVEL = 10; diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 340e72e6fd..7877e9dbb8 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -6,7 +6,7 @@ #include "cereal/messaging/messaging.h" #include "cereal/services.h" #include "msgq/visionipc/visionipc_client.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "common/params.h" #include "common/swaglog.h" #include "common/util.h" @@ -59,7 +59,7 @@ public: const char *filename = NULL; bool record = true; bool include_audio = false; - bool adaptive_bitrate = false; + bool is_live = false; int frame_width = -1; int frame_height = -1; int fps = MAIN_FPS; @@ -105,7 +105,7 @@ const EncoderInfo stream_road_encoder_info = { .publish_name = "livestreamRoadEncodeData", //.thumbnail_name = "thumbnail", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; @@ -113,7 +113,7 @@ const EncoderInfo stream_road_encoder_info = { const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; @@ -121,7 +121,7 @@ const EncoderInfo stream_wide_road_encoder_info = { const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", .record = false, - .adaptive_bitrate = true, + .is_live = true, .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py index fd071486df..757b61d17e 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/system/loggerd/tests/loggerd_tests_common.py @@ -6,7 +6,7 @@ from pathlib import Path import openpilot.system.loggerd.deleter as deleter import openpilot.system.loggerd.uploader as uploader from openpilot.common.params import Params -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import setxattr diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py index 2b74fe2276..416c158ed3 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/system/loggerd/tests/test_encoder.py @@ -12,10 +12,10 @@ from tqdm import trange from openpilot.common.params import Params from openpilot.common.timeout import Timeout -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.system.manager.process_config import managed_processes from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 9fd1b2d434..882de583b4 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -15,8 +15,8 @@ from cereal.services import SERVICE_LIST from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.timeout import Timeout -from openpilot.system.hardware.hw import Paths -from openpilot.system.hardware import TICI +from openpilot.common.hardware.hw import Paths +from openpilot.common.hardware import TICI from openpilot.system.loggerd.xattr_cache import getxattr from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE from openpilot.system.manager.process_config import managed_processes diff --git a/system/loggerd/tests/test_uploader.py b/system/loggerd/tests/test_uploader.py index 562bc068eb..eec85eda2e 100644 --- a/system/loggerd/tests/test_uploader.py +++ b/system/loggerd/tests/test_uploader.py @@ -4,7 +4,7 @@ import threading import logging import json from pathlib import Path -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.uploader import main, UPLOAD_ATTR_NAME, UPLOAD_ATTR_VALUE diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index 8ac38b6df6..809a302b6f 100755 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -15,7 +15,7 @@ from openpilot.common.api import Api from openpilot.common.utils import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr from openpilot.common.swaglog import cloudlog diff --git a/system/logmessaged.py b/system/logmessaged.py index c095c26192..a64a849b02 100755 --- a/system/logmessaged.py +++ b/system/logmessaged.py @@ -4,7 +4,7 @@ from typing import NoReturn import cereal.messaging as messaging from openpilot.common.logging_extra import SwagLogFileFormatter -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import get_file_handler diff --git a/system/manager/build.py b/system/manager/build.py index 470cfccdaa..75a7dc63eb 100755 --- a/system/manager/build.py +++ b/system/manager/build.py @@ -6,7 +6,7 @@ import subprocess from openpilot.common.basedir import BASEDIR from openpilot.common.spinner import Spinner from openpilot.common.text_window import TextWindow -from openpilot.system.hardware import HARDWARE, AGNOS +from openpilot.common.hardware import HARDWARE, AGNOS def build() -> None: spinner = Spinner() diff --git a/system/manager/manager.py b/system/manager/manager.py index 0eb55c2727..f2ff5c282a 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -12,15 +12,14 @@ import openpilot.system.sentry as sentry from openpilot.common.utils import atomic_write from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata -from openpilot.system.hardware.hw import Paths -from openpilot.system.hardware import PC +from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.system.params_migration import run_migration diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2b8def95b5..510395b93b 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -4,9 +4,9 @@ import platform from cereal import car, custom from openpilot.common.params import Params -from openpilot.system.hardware import PC, TICI +from openpilot.common.hardware import PC, TICI from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH @@ -64,6 +64,9 @@ def only_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: return not started +def livestream(started: bool, params: Params, CP: car.CarParams) -> bool: + return params.get_bool("IsLiveStreaming") + def use_github_runner(started, params, CP: car.CarParams) -> bool: return not PC and params.get_bool("EnableGithubRunner") and ( not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage")) @@ -106,16 +109,19 @@ def or_(*fns): def and_(*fns): return lambda *args: operator.and_(*(fn(*args) for fn in fns)) +def not_(*fns): + return lambda *args: operator.not_(*(fn(*args) for fn in fns)) + procs = [ DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"), NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging), NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad), - NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], notcar), + NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)), PythonProcess("logmessaged", "system.logmessaged", always_run), - NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), - PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), + NativeProcess("camerad", "system/camerad", ["./camerad"], or_(driverview, livestream), enabled=not WEBCAM), + PythonProcess("webcamerad", "system.camerad.webcam.camerad", driverview, enabled=WEBCAM), PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"), PythonProcess("journald", "system.journald", only_onroad, platform.system() != "Darwin"), PythonProcess("micd", "system.micd", iscar), @@ -148,7 +154,7 @@ procs = [ PythonProcess("lateral_maneuversd", "tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), PythonProcess("radard", "selfdrive.controls.radard", only_onroad), PythonProcess("hardwared", "system.hardware.hardwared", always_run), - PythonProcess("modem", "system.hardware.tici.modem", always_run, enabled=TICI), + PythonProcess("modem", "common.hardware.tici.modem", always_run, enabled=TICI), PythonProcess("tombstoned", "system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "system.loggerd.uploader", uploader_ready), @@ -157,8 +163,7 @@ procs = [ # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), - PythonProcess("webrtcd", "system.webrtc.webrtcd", notcar), - PythonProcess("webjoystick", "tools.bodyteleop.web", notcar), + PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(and_(livestream, not_(iscar)), notcar)), PythonProcess("joystick", "tools.joystick.joystick_control", and_(joystick, iscar)), # sunnylink <3 diff --git a/system/manager/test/test_manager.py b/system/manager/test/test_manager.py index 34d07c6724..73c66ff3fc 100644 --- a/system/manager/test/test_manager.py +++ b/system/manager/test/test_manager.py @@ -8,7 +8,7 @@ from openpilot.common.params import Params import openpilot.system.manager.manager as manager from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes, procs -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE os.environ['FAKEUPLOAD'] = "1" diff --git a/system/qcomgpsd/nmeaport.py b/system/qcomgpsd/nmeaport.py index 10b8516ed0..0e695b65e5 100644 --- a/system/qcomgpsd/nmeaport.py +++ b/system/qcomgpsd/nmeaport.py @@ -119,7 +119,7 @@ def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: def main() -> NoReturn: from openpilot.common.gpio import gpio_init, gpio_set - from openpilot.system.hardware.tici.pins import GPIO + from openpilot.common.hardware.tici.pins import GPIO from openpilot.system.qcomgpsd.qcomgpsd import at_cmd try: diff --git a/system/qcomgpsd/qcomgpsd.py b/system/qcomgpsd/qcomgpsd.py index 7e0d304872..78731f434b 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/system/qcomgpsd/qcomgpsd.py @@ -16,7 +16,7 @@ import cereal.messaging as messaging from openpilot.common.gpio import gpio_init, gpio_set from openpilot.common.utils import retry from openpilot.common.time_helpers import system_time_valid -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO from openpilot.common.swaglog import cloudlog from openpilot.system.qcomgpsd.modemdiag import ModemDiag, DIAG_LOG_F, setup_logs, send_recv from openpilot.system.qcomgpsd.structs import (dict_unpacker, position_report, relist, diff --git a/system/sentry.py b/system/sentry.py index a1fb604dea..901e53cf5f 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -8,8 +8,8 @@ from sentry_sdk.integrations.threading import ThreadingIntegration from openpilot.common.params import Params from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -from openpilot.system.hardware import HARDWARE -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware import HARDWARE +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_build_metadata, get_version diff --git a/system/statsd.py b/system/statsd.py index 3a67dd44c2..4b2e67caeb 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -14,9 +14,9 @@ from typing import NoReturn from openpilot.common.params import Params from cereal.messaging import SubMaster -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.common.utils import atomic_write from openpilot.system.version import get_build_metadata from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/system/tests/test_logmessaged.py b/system/tests/test_logmessaged.py index 9ccc8ef53b..92bcd9d1ad 100644 --- a/system/tests/test_logmessaged.py +++ b/system/tests/test_logmessaged.py @@ -4,7 +4,7 @@ import time import cereal.messaging as messaging from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog, ipchandler diff --git a/system/tombstoned.py b/system/tombstoned.py index 5bcced2666..88a2a879ed 100755 --- a/system/tombstoned.py +++ b/system/tombstoned.py @@ -10,7 +10,7 @@ import glob from typing import NoReturn import openpilot.system.sentry as sentry -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_build_metadata diff --git a/system/ubloxd/pigeond.py b/system/ubloxd/pigeond.py index e458a9d65f..fe2634e333 100755 --- a/system/ubloxd/pigeond.py +++ b/system/ubloxd/pigeond.py @@ -12,9 +12,9 @@ from cereal import messaging from openpilot.common.time_helpers import system_time_valid from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from openpilot.common.gpio import gpio_init, gpio_set -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO UBLOX_TTY = "/dev/ttyHS0" diff --git a/system/ubloxd/tests/test_pigeond.py b/system/ubloxd/tests/test_pigeond.py index 202820e412..fb979a9372 100644 --- a/system/ubloxd/tests/test_pigeond.py +++ b/system/ubloxd/tests/test_pigeond.py @@ -6,7 +6,7 @@ from cereal.services import SERVICE_LIST from openpilot.common.gpio import gpio_read from openpilot.selfdrive.test.helpers import with_processes from openpilot.system.manager.process_config import managed_processes -from openpilot.system.hardware.tici.pins import GPIO +from openpilot.common.hardware.tici.pins import GPIO # TODO: test TTFF when we have good A-GNSS diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 42c0db7545..44bfa63b08 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.multilang import multilang from openpilot.common.realtime import Ratekeeper diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 7fae60119c..b6193672c4 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -5,7 +5,7 @@ from collections.abc import Callable from enum import Enum from typing import cast from openpilot.system.ui.lib.application import gui_app, MouseEvent -from openpilot.system.hardware import TICI +from openpilot.common.hardware import TICI from collections import deque MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state diff --git a/system/ui/mici_reset.py b/system/ui/mici_reset.py index 9cc6e7f3f8..90de7843f9 100755 --- a/system/ui/mici_reset.py +++ b/system/ui/mici_reset.py @@ -7,7 +7,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.mici_setup import GreyBigButton, FailedPage diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 8f4541e9e6..65506ef3a0 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -13,7 +13,7 @@ import pyray as rl from cereal import log from openpilot.common.filter_simple import BounceFilter -from openpilot.system.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, TICI from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 8437e6fa60..a072354cf0 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -5,7 +5,7 @@ import threading import pyray as rl from openpilot.common.realtime import config_realtime_process, set_core_affinity -from openpilot.system.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, TICI from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets.nav_widget import NavWidget diff --git a/system/ui/text.py b/system/ui/text.py index 17e8a507cb..f6bcf4eddf 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -2,7 +2,7 @@ import re import sys import pyray as rl -from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.hardware import HARDWARE, PC from openpilot.system.ui.lib.application import BIG_UI, gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.text_measure import measure_text_cached diff --git a/system/ui/tici_reset.py b/system/ui/tici_reset.py index a6603d547e..cb5441a406 100755 --- a/system/ui/tici_reset.py +++ b/system/ui/tici_reset.py @@ -7,7 +7,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.hardware import PC +from openpilot.common.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle diff --git a/system/ui/tici_setup.py b/system/ui/tici_setup.py index 9eefb6af53..0e0c6fdc7c 100755 --- a/system/ui/tici_setup.py +++ b/system/ui/tici_setup.py @@ -11,7 +11,7 @@ from enum import IntEnum import pyray as rl from cereal import log -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import DialogResult, Widget diff --git a/system/ui/tici_updater.py b/system/ui/tici_updater.py index 3a3b0987d0..27cf6579a8 100755 --- a/system/ui/tici_updater.py +++ b/system/ui/tici_updater.py @@ -5,7 +5,7 @@ import threading import pyray as rl from enum import IntEnum -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget diff --git a/system/updated/casync/casync.py b/system/updated/casync/casync.py deleted file mode 100755 index 2bd46a1ffb..0000000000 --- a/system/updated/casync/casync.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 -import io -import lzma -import os -import pathlib -import struct -import sys -import time -from abc import ABC, abstractmethod -from collections import defaultdict, namedtuple -from collections.abc import Callable -from typing import IO - -import requests -from Crypto.Hash import SHA512 -from openpilot.system.updated.casync import tar -from openpilot.system.updated.casync.common import create_casync_tar_package - -CA_FORMAT_INDEX = 0x96824d9c7b129ff9 -CA_FORMAT_TABLE = 0xe75b9e112f17417d -CA_FORMAT_TABLE_TAIL_MARKER = 0xe75b9e112f17417 -FLAGS = 0xb000000000000000 - -CA_HEADER_LEN = 48 -CA_TABLE_HEADER_LEN = 16 -CA_TABLE_ENTRY_LEN = 40 -CA_TABLE_MIN_LEN = CA_TABLE_HEADER_LEN + CA_TABLE_ENTRY_LEN - -CHUNK_DOWNLOAD_TIMEOUT = 60 -CHUNK_DOWNLOAD_RETRIES = 3 - -CAIBX_DOWNLOAD_TIMEOUT = 120 - -Chunk = namedtuple('Chunk', ['sha', 'offset', 'length']) -ChunkDict = dict[bytes, Chunk] - - -class ChunkReader(ABC): - @abstractmethod - def read(self, chunk: Chunk) -> bytes: - ... - - -class BinaryChunkReader(ChunkReader): - """Reads chunks from a local file""" - def __init__(self, file_like: IO[bytes]) -> None: - super().__init__() - self.f = file_like - - def read(self, chunk: Chunk) -> bytes: - self.f.seek(chunk.offset) - return self.f.read(chunk.length) - - -class FileChunkReader(BinaryChunkReader): - def __init__(self, path: str) -> None: - super().__init__(open(path, 'rb')) - - def __del__(self): - self.f.close() - - -class RemoteChunkReader(ChunkReader): - """Reads lzma compressed chunks from a remote store""" - - def __init__(self, url: str) -> None: - super().__init__() - self.url = url - self.session = requests.Session() - - def read(self, chunk: Chunk) -> bytes: - sha_hex = chunk.sha.hex() - url = os.path.join(self.url, sha_hex[:4], sha_hex + ".cacnk") - - if os.path.isfile(url): - with open(url, 'rb') as f: - contents = f.read() - else: - for i in range(CHUNK_DOWNLOAD_RETRIES): - try: - resp = self.session.get(url, timeout=CHUNK_DOWNLOAD_TIMEOUT) - break - except Exception: - if i == CHUNK_DOWNLOAD_RETRIES - 1: - raise - time.sleep(CHUNK_DOWNLOAD_TIMEOUT) - - resp.raise_for_status() - contents = resp.content - - decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) - return decompressor.decompress(contents) - - -class DirectoryTarChunkReader(BinaryChunkReader): - """creates a tar archive of a directory and reads chunks from it""" - - def __init__(self, path: str, cache_file: str) -> None: - create_casync_tar_package(pathlib.Path(path), pathlib.Path(cache_file)) - - self.f = open(cache_file, "rb") - super().__init__(self.f) - - def __del__(self): - self.f.close() - os.unlink(self.f.name) - - -def parse_caibx(caibx_path: str) -> list[Chunk]: - """Parses the chunks from a caibx file. Can handle both local and remote files. - Returns a list of chunks with hash, offset and length""" - caibx: io.BufferedIOBase - if os.path.isfile(caibx_path): - caibx = open(caibx_path, 'rb') - else: - resp = requests.get(caibx_path, timeout=CAIBX_DOWNLOAD_TIMEOUT) - resp.raise_for_status() - caibx = io.BytesIO(resp.content) - - caibx.seek(0, os.SEEK_END) - caibx_len = caibx.tell() - caibx.seek(0, os.SEEK_SET) - - # Parse header - length, magic, flags, min_size, _, max_size = struct.unpack("= min_size - - chunks.append(Chunk(sha, offset, length)) - offset = new_offset - - caibx.close() - return chunks - - -def build_chunk_dict(chunks: list[Chunk]) -> ChunkDict: - """Turn a list of chunks into a dict for faster lookups based on hash. - Keep first chunk since it's more likely to be already downloaded.""" - r = {} - for c in chunks: - if c.sha not in r: - r[c.sha] = c - return r - - -def extract(target: list[Chunk], - sources: list[tuple[str, ChunkReader, ChunkDict]], - out_path: str, - progress: Callable[[int], None] | None = None): - stats: dict[str, int] = defaultdict(int) - - mode = 'rb+' if os.path.exists(out_path) else 'wb' - with open(out_path, mode) as out: - for cur_chunk in target: - - # Find source for desired chunk - for name, chunk_reader, store_chunks in sources: - if cur_chunk.sha in store_chunks: - bts = chunk_reader.read(store_chunks[cur_chunk.sha]) - - # Check length - if len(bts) != cur_chunk.length: - continue - - # Check hash - if SHA512.new(bts, truncate="256").digest() != cur_chunk.sha: - continue - - # Write to output - out.seek(cur_chunk.offset) - out.write(bts) - - stats[name] += cur_chunk.length - - if progress is not None: - progress(sum(stats.values())) - - break - else: - raise RuntimeError("Desired chunk not found in provided stores") - - return stats - - -def extract_directory(target: list[Chunk], - sources: list[tuple[str, ChunkReader, ChunkDict]], - out_path: str, - tmp_file: str, - progress: Callable[[int], None] | None = None): - """extract a directory stored as a casync tar archive""" - - stats = extract(target, sources, tmp_file, progress) - - with open(tmp_file, "rb") as f: - tar.extract_tar_archive(f, pathlib.Path(out_path)) - - return stats - - -def print_stats(stats: dict[str, int]): - total_bytes = sum(stats.values()) - print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") - for name, total in stats.items(): - print(f" {name}: {total / 1024 / 1024:.2f} MB ({total / total_bytes * 100:.1f}%)") - - -def extract_simple(caibx_path, out_path, store_path): - # (name, callback, chunks) - target = parse_caibx(caibx_path) - sources = [ - # (store_path, RemoteChunkReader(store_path), build_chunk_dict(target)), - (store_path, FileChunkReader(store_path), build_chunk_dict(target)), - ] - - return extract(target, sources, out_path) - - -if __name__ == "__main__": - caibx = sys.argv[1] - out = sys.argv[2] - store = sys.argv[3] - - stats = extract_simple(caibx, out, store) - print_stats(stats) diff --git a/system/updated/casync/common.py b/system/updated/casync/common.py deleted file mode 100644 index 6979f5cb06..0000000000 --- a/system/updated/casync/common.py +++ /dev/null @@ -1,61 +0,0 @@ -import dataclasses -import json -import pathlib -import subprocess - -from openpilot.system.version import BUILD_METADATA_FILENAME, BuildMetadata -from openpilot.system.updated.casync import tar - - -CASYNC_ARGS = ["--with=symlinks", "--with=permissions", "--compression=xz", "--chunk-size=16M"] -CASYNC_FILES = [BUILD_METADATA_FILENAME] - - -def run(cmd): - return subprocess.check_output(cmd) - - -def get_exclude_set(path) -> set[str]: - exclude_set = set(CASYNC_FILES) - - for file in path.rglob("*"): - if file.is_file() or file.is_symlink(): - - while file.resolve() != path.resolve(): - exclude_set.add(str(file.relative_to(path))) - - file = file.parent - - return exclude_set - - -def create_build_metadata_file(path: pathlib.Path, build_metadata: BuildMetadata): - with open(path / BUILD_METADATA_FILENAME, "w") as f: - build_metadata_dict = dataclasses.asdict(build_metadata) - build_metadata_dict["openpilot"].pop("is_dirty") # this is determined at runtime - build_metadata_dict.pop("channel") # channel is unrelated to the build itself - f.write(json.dumps(build_metadata_dict)) - - -def is_not_git(path: pathlib.Path) -> bool: - return ".git" not in path.parts - - -def create_casync_tar_package(target_dir: pathlib.Path, output_path: pathlib.Path): - tar.create_tar_archive(output_path, target_dir, is_not_git) - - -def create_casync_from_file(file: pathlib.Path, output_dir: pathlib.Path, caibx_name: str): - caibx_file = output_dir / f"{caibx_name}.caibx" - run(["casync", "make", *CASYNC_ARGS, caibx_file, str(file)]) - - return caibx_file - - -def create_casync_release(target_dir: pathlib.Path, output_dir: pathlib.Path, caibx_name: str): - tar_file = output_dir / f"{caibx_name}.tar" - create_casync_tar_package(target_dir, tar_file) - caibx_file = create_casync_from_file(tar_file, output_dir, caibx_name) - tar_file.unlink() - digest = run(["casync", "digest", *CASYNC_ARGS, target_dir]).decode("utf-8").strip() - return digest, caibx_file diff --git a/system/updated/casync/tar.py b/system/updated/casync/tar.py deleted file mode 100644 index a5a8238bba..0000000000 --- a/system/updated/casync/tar.py +++ /dev/null @@ -1,39 +0,0 @@ -import pathlib -import tarfile -from typing import IO -from collections.abc import Callable - - -def include_default(_) -> bool: - return True - - -def create_tar_archive(filename: pathlib.Path, directory: pathlib.Path, include: Callable[[pathlib.Path], bool] = include_default): - """Creates a tar archive of a directory""" - - with tarfile.open(filename, 'w') as tar: - for file in sorted(directory.rglob("*"), key=lambda f: f.stat().st_size if f.is_file() else 0, reverse=True): - if not include(file): - continue - relative_path = str(file.relative_to(directory)) - if file.is_symlink(): - info = tarfile.TarInfo(relative_path) - info.type = tarfile.SYMTYPE - info.linkpath = str(file.readlink()) - tar.addfile(info) - - elif file.is_file(): - info = tarfile.TarInfo(relative_path) - info.size = file.stat().st_size - info.type = tarfile.REGTYPE - info.mode = file.stat().st_mode - with file.open('rb') as f: - tar.addfile(info, f) - - -def extract_tar_archive(fh: IO[bytes], directory: pathlib.Path): - """Extracts a tar archive to a directory""" - - tar = tarfile.open(fileobj=fh, mode='r') - tar.extractall(str(directory), filter=lambda info, path: info) - tar.close() diff --git a/system/updated/casync/tests/test_casync.py b/system/updated/casync/tests/test_casync.py deleted file mode 100644 index bc171e7432..0000000000 --- a/system/updated/casync/tests/test_casync.py +++ /dev/null @@ -1,264 +0,0 @@ -import pytest -import os -import pathlib -import tempfile -import subprocess - -from openpilot.system.updated.casync import casync -from openpilot.system.updated.casync import tar - -# dd if=/dev/zero of=/tmp/img.raw bs=1M count=2 -# sudo losetup -f /tmp/img.raw -# losetup -a | grep img.raw -LOOPBACK = os.environ.get('LOOPBACK', None) - - -@pytest.mark.skip("not used yet") -class TestCasync: - @classmethod - def setup_class(cls): - cls.tmpdir = tempfile.TemporaryDirectory() - - # Build example contents - chunk_a = [i % 256 for i in range(1024)] * 512 - chunk_b = [(256 - i) % 256 for i in range(1024)] * 512 - zeroes = [0] * (1024 * 128) - contents = chunk_a + chunk_b + zeroes + chunk_a - - cls.contents = bytes(contents) - - # Write to file - cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.bin') - with open(cls.orig_fn, 'wb') as f: - f.write(cls.contents) - - # Create casync files - cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx') - cls.store_fn = os.path.join(cls.tmpdir.name, 'store') - subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn]) - - target = casync.parse_caibx(cls.manifest_fn) - hashes = [c.sha.hex() for c in target] - - # Ensure we have chunk reuse - assert len(hashes) > len(set(hashes)) - - def setup_method(self): - # Clear target_lo - if LOOPBACK is not None: - self.target_lo = LOOPBACK - with open(self.target_lo, 'wb') as f: - f.write(b"0" * len(self.contents)) - - self.target_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names())) - self.seed_fn = os.path.join(self.tmpdir.name, next(tempfile._get_candidate_names())) - - def teardown_method(self): - for fn in [self.target_fn, self.seed_fn]: - try: - os.unlink(fn) - except FileNotFoundError: - pass - - def test_simple_extract(self): - target = casync.parse_caibx(self.manifest_fn) - - sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as target_f: - assert target_f.read() == self.contents - - assert stats['remote'] == len(self.contents) - - def test_seed(self): - target = casync.parse_caibx(self.manifest_fn) - - # Populate seed with half of the target contents - with open(self.seed_fn, 'wb') as seed_f: - seed_f.write(self.contents[:len(self.contents) // 2]) - - sources = [('seed', casync.FileChunkReader(self.seed_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as target_f: - assert target_f.read() == self.contents - - assert stats['seed'] > 0 - assert stats['remote'] < len(self.contents) - - def test_already_done(self): - """Test that an already flashed target doesn't download any chunks""" - target = casync.parse_caibx(self.manifest_fn) - - with open(self.target_fn, 'wb') as f: - f.write(self.contents) - - sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as f: - assert f.read() == self.contents - - assert stats['target'] == len(self.contents) - - def test_chunk_reuse(self): - """Test that chunks that are reused are only downloaded once""" - target = casync.parse_caibx(self.manifest_fn) - - # Ensure target exists - with open(self.target_fn, 'wb'): - pass - - sources = [('target', casync.FileChunkReader(self.target_fn), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_fn) - - with open(self.target_fn, 'rb') as f: - assert f.read() == self.contents - - assert stats['remote'] < len(self.contents) - - @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device") - def test_lo_simple_extract(self): - target = casync.parse_caibx(self.manifest_fn) - sources = [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_lo) - - with open(self.target_lo, 'rb') as target_f: - assert target_f.read(len(self.contents)) == self.contents - - assert stats['remote'] == len(self.contents) - - @pytest.mark.skipif(not LOOPBACK, reason="requires loopback device") - def test_lo_chunk_reuse(self): - """Test that chunks that are reused are only downloaded once""" - target = casync.parse_caibx(self.manifest_fn) - - sources = [('target', casync.FileChunkReader(self.target_lo), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract(target, sources, self.target_lo) - - with open(self.target_lo, 'rb') as f: - assert f.read(len(self.contents)) == self.contents - - assert stats['remote'] < len(self.contents) - - -@pytest.mark.skip("not used yet") -class TestCasyncDirectory: - """Tests extracting a directory stored as a casync tar archive""" - - NUM_FILES = 16 - - @classmethod - def setup_cache(cls, directory, files=None): - if files is None: - files = range(cls.NUM_FILES) - - chunk_a = [i % 256 for i in range(1024)] * 512 - chunk_b = [(256 - i) % 256 for i in range(1024)] * 512 - zeroes = [0] * (1024 * 128) - cls.contents = chunk_a + chunk_b + zeroes + chunk_a - cls.contents = bytes(cls.contents) - - for i in files: - with open(os.path.join(directory, f"file_{i}.txt"), "wb") as f: - f.write(cls.contents) - - os.symlink(f"file_{i}.txt", os.path.join(directory, f"link_{i}.txt")) - - @classmethod - def setup_class(cls): - cls.tmpdir = tempfile.TemporaryDirectory() - - # Create casync files - cls.manifest_fn = os.path.join(cls.tmpdir.name, 'orig.caibx') - cls.store_fn = os.path.join(cls.tmpdir.name, 'store') - - cls.directory_to_extract = tempfile.TemporaryDirectory() - cls.setup_cache(cls.directory_to_extract.name) - - cls.orig_fn = os.path.join(cls.tmpdir.name, 'orig.tar') - tar.create_tar_archive(cls.orig_fn, pathlib.Path(cls.directory_to_extract.name)) - - subprocess.check_output(["casync", "make", "--compression=xz", "--store", cls.store_fn, cls.manifest_fn, cls.orig_fn]) - - @classmethod - def teardown_class(cls): - cls.tmpdir.cleanup() - cls.directory_to_extract.cleanup() - - def setup_method(self): - self.cache_dir = tempfile.TemporaryDirectory() - self.working_dir = tempfile.TemporaryDirectory() - self.out_dir = tempfile.TemporaryDirectory() - - def teardown_method(self): - self.cache_dir.cleanup() - self.working_dir.cleanup() - self.out_dir.cleanup() - - def run_test(self): - target = casync.parse_caibx(self.manifest_fn) - - cache_filename = os.path.join(self.working_dir.name, "cache.tar") - tmp_filename = os.path.join(self.working_dir.name, "tmp.tar") - - sources = [('cache', casync.DirectoryTarChunkReader(self.cache_dir.name, cache_filename), casync.build_chunk_dict(target))] - sources += [('remote', casync.RemoteChunkReader(self.store_fn), casync.build_chunk_dict(target))] - - stats = casync.extract_directory(target, sources, pathlib.Path(self.out_dir.name), tmp_filename) - - with open(os.path.join(self.out_dir.name, "file_0.txt"), "rb") as f: - assert f.read() == self.contents - - with open(os.path.join(self.out_dir.name, "link_0.txt"), "rb") as f: - assert f.read() == self.contents - assert os.readlink(os.path.join(self.out_dir.name, "link_0.txt")) == "file_0.txt" - - return stats - - def test_no_cache(self): - self.setup_cache(self.cache_dir.name, []) - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] == 0 - - def test_full_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - stats = self.run_test() - assert stats['remote'] == 0 - assert stats['cache'] > 0 - - def test_one_file_cache(self): - self.setup_cache(self.cache_dir.name, range(1)) - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] < stats['remote'] - - def test_one_file_incorrect_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - with open(os.path.join(self.cache_dir.name, "file_0.txt"), "wb") as f: - f.write(b"1234") - - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] > stats['remote'] - - def test_one_file_missing_cache(self): - self.setup_cache(self.cache_dir.name, range(self.NUM_FILES)) - os.unlink(os.path.join(self.cache_dir.name, "file_12.txt")) - - stats = self.run_test() - assert stats['remote'] > 0 - assert stats['cache'] > 0 - assert stats['cache'] > stats['remote'] diff --git a/system/updated/tests/test_base.py b/system/updated/tests/test_base.py deleted file mode 100644 index db38bfa840..0000000000 --- a/system/updated/tests/test_base.py +++ /dev/null @@ -1,259 +0,0 @@ -import os -import pathlib -import shutil -import signal -import stat -import subprocess -import tempfile -import time -import pytest - -from openpilot.common.params import Params -from openpilot.system.manager.process import ManagerProcess -from openpilot.selfdrive.test.helpers import processes_context - - -def get_consistent_flag(path: str) -> bool: - consistent_file = pathlib.Path(os.path.join(path, ".overlay_consistent")) - return consistent_file.is_file() - - -def run(args, **kwargs): - return subprocess.check_output(args, **kwargs) - - -def update_release(directory, name, version, agnos_version, release_notes): - with open(directory / "RELEASES.md", "w") as f: - f.write(release_notes) - - (directory / "common").mkdir(exist_ok=True) - - with open(directory / "common" / "version.h", "w") as f: - f.write(f'#define COMMA_VERSION "{version}"') - - launch_env = directory / "launch_env.sh" - with open(launch_env, "w") as f: - f.write(f'export AGNOS_VERSION="{agnos_version}"') - - st = os.stat(launch_env) - os.chmod(launch_env, st.st_mode | stat.S_IEXEC) - - test_symlink = directory / "test_symlink" - if not os.path.exists(str(test_symlink)): - os.symlink("common/version.h", test_symlink) - - -def get_version(path: str) -> str: - with open(os.path.join(path, "common", "version.h")) as f: - return f.read().split('"')[1] - - -@pytest.mark.slow # TODO: can we test overlayfs in GHA? -class TestBaseUpdate: - @classmethod - def setup_class(cls): - if "Base" in cls.__name__: - pytest.skip() - - def setup_method(self): - self.tmpdir = tempfile.mkdtemp() - - run(["sudo", "mount", "-t", "tmpfs", "tmpfs", self.tmpdir]) # overlayfs doesn't work inside of docker unless this is a tmpfs - - self.mock_update_path = pathlib.Path(self.tmpdir) - - self.params = Params() - - self.basedir = self.mock_update_path / "openpilot" - self.basedir.mkdir() - - self.staging_root = self.mock_update_path / "safe_staging" - self.staging_root.mkdir() - - self.remote_dir = self.mock_update_path / "remote" - self.remote_dir.mkdir() - - os.environ["UPDATER_STAGING_ROOT"] = str(self.staging_root) - os.environ["UPDATER_LOCK_FILE"] = str(self.mock_update_path / "safe_staging_overlay.lock") - - self.MOCK_RELEASES = { - "release3": ("0.1.2", "1.2", "0.1.2 release notes"), - "master": ("0.1.3", "1.2", "0.1.3 release notes"), - } - - @pytest.fixture(autouse=True) - def mock_basedir(self, mocker): - mocker.patch("openpilot.common.basedir.BASEDIR", self.basedir) - - def set_target_branch(self, branch): - self.params.put("UpdaterTargetBranch", branch, block=True) - - def setup_basedir_release(self, release): - self.params = Params() - self.set_target_branch(release) - - def update_remote_release(self, release): - raise NotImplementedError("") - - def setup_remote_release(self, release): - raise NotImplementedError("") - - def additional_context(self): - raise NotImplementedError("") - - def teardown_method(self): - try: - run(["sudo", "umount", "-l", str(self.staging_root / "merged")]) - run(["sudo", "umount", "-l", self.tmpdir]) - shutil.rmtree(self.tmpdir) - except Exception: - print("cleanup failed...") - - def wait_for_condition(self, condition, timeout=12): - start = time.monotonic() - while True: - waited = time.monotonic() - start - if condition(): - print(f"waited {waited}s for condition ") - return waited - - if waited > timeout: - raise TimeoutError("timed out waiting for condition") - - time.sleep(1) - - def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert get_version(str(self.staging_root / "finalized")) == version - assert get_consistent_flag(str(self.staging_root / "finalized")) - assert os.access(str(self.staging_root / "finalized" / "launch_env.sh"), os.X_OK) - - with open(self.staging_root / "finalized" / "test_symlink") as f: - assert version in f.read() - -class ParamsBaseUpdateTest(TestBaseUpdate): - def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert self.params.get("UpdaterNewDescription").startswith(f"{version} / {branch}") - assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n".encode() - super()._test_finalized_update(branch, version, agnos_version, release_notes) - - def send_check_for_updates_signal(self, updated: ManagerProcess): - updated.signal(signal.SIGUSR1.value) - - def send_download_signal(self, updated: ManagerProcess): - updated.signal(signal.SIGHUP.value) - - def _test_params(self, branch, fetch_available, update_available): - assert self.params.get("UpdaterTargetBranch") == branch - assert self.params.get_bool("UpdaterFetchAvailable") == fetch_available - assert self.params.get_bool("UpdateAvailable") == update_available - - def wait_for_idle(self): - self.wait_for_condition(lambda: self.params.get("UpdaterState") == "idle") - - def wait_for_failed(self): - self.wait_for_condition(lambda: self.params.get("UpdateFailedCount") is not None and \ - self.params.get("UpdateFailedCount") > 0) - - def wait_for_fetch_available(self): - self.wait_for_condition(lambda: self.params.get_bool("UpdaterFetchAvailable")) - - def wait_for_update_available(self): - self.wait_for_condition(lambda: self.params.get_bool("UpdateAvailable")) - - def test_no_update(self): - # Start on release3, ensure we don't fetch any updates - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.send_check_for_updates_signal(updated) - - self.wait_for_idle() - - self._test_params("release3", False, False) - - def test_new_release(self): - # Start on release3, simulate a release3 commit, ensure we fetch that update properly - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.MOCK_RELEASES["release3"] = ("0.1.3", "1.2", "0.1.3 release notes") - self.update_remote_release("release3") - - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("release3", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("release3", False, True) - self._test_finalized_update("release3", *self.MOCK_RELEASES["release3"]) - - def test_switch_branches(self): - # Start on release3, request to switch to master manually, ensure we switched - self.setup_remote_release("release3") - self.setup_remote_release("master") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.set_target_branch("master") - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("master", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("master", False, True) - self._test_finalized_update("master", *self.MOCK_RELEASES["master"]) - - def test_agnos_update(self, mocker): - # Start on release3, push an update with an agnos change - self.setup_remote_release("release3") - self.setup_basedir_release("release3") - - with self.additional_context(), processes_context(["updated"]) as [updated]: - mocker.patch("openpilot.system.hardware.AGNOS", "True") - mocker.patch("openpilot.system.hardware.tici.hardware.Tici.get_os_version", "1.2") - mocker.patch("openpilot.system.hardware.tici.agnos.get_target_slot_number") - mocker.patch("openpilot.system.hardware.tici.agnos.flash_agnos_update") - - self._test_params("release3", False, False) - self.wait_for_idle() - self._test_params("release3", False, False) - - self.MOCK_RELEASES["release3"] = ("0.1.3", "1.3", "0.1.3 release notes") - self.update_remote_release("release3") - - self.send_check_for_updates_signal(updated) - - self.wait_for_fetch_available() - - self._test_params("release3", True, False) - - self.send_download_signal(updated) - - self.wait_for_update_available() - - self._test_params("release3", False, True) - self._test_finalized_update("release3", *self.MOCK_RELEASES["release3"]) diff --git a/system/updated/tests/test_git.py b/system/updated/tests/test_git.py deleted file mode 100644 index 5a5a27000b..0000000000 --- a/system/updated/tests/test_git.py +++ /dev/null @@ -1,22 +0,0 @@ -import contextlib -from openpilot.system.updated.tests.test_base import ParamsBaseUpdateTest, run, update_release - - -class TestUpdateDGitStrategy(ParamsBaseUpdateTest): - def update_remote_release(self, release): - update_release(self.remote_dir, release, *self.MOCK_RELEASES[release]) - run(["git", "add", "."], cwd=self.remote_dir) - run(["git", "commit", "-m", f"openpilot release {release}"], cwd=self.remote_dir) - - def setup_remote_release(self, release): - run(["git", "init"], cwd=self.remote_dir) - run(["git", "checkout", "-b", release], cwd=self.remote_dir) - self.update_remote_release(release) - - def setup_basedir_release(self, release): - super().setup_basedir_release(release) - run(["git", "clone", "-b", release, self.remote_dir, self.basedir]) - - @contextlib.contextmanager - def additional_context(self): - yield diff --git a/system/updated/tests/test_updated.py b/system/updated/tests/test_updated.py deleted file mode 100644 index 18ee0e706a..0000000000 --- a/system/updated/tests/test_updated.py +++ /dev/null @@ -1,38 +0,0 @@ -import pytest - -from openpilot.common.params import Params -from openpilot.system.updated.updated import Updater - - -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_target_branch_migration_from_current_branch(mocker, device_type, branch, expected): - params = Params() - params.remove("UpdaterTargetBranch") - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - mocker.patch.object(Updater, "get_branch", return_value=branch) - - assert Updater().target_branch == expected - - -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_target_branch_migration_from_param(mocker, device_type, branch, expected): - params = Params() - params.put("UpdaterTargetBranch", branch, block=True) - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - - try: - assert Updater().target_branch == expected - finally: - params.remove("UpdaterTargetBranch") diff --git a/system/updated/updated.py b/system/updated/updated.py index 8e28f7c52d..53f200fe5c 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -17,7 +17,7 @@ from openpilot.common.time_helpers import system_time_valid from openpilot.common.markdown import parse_markdown from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.system.hardware import AGNOS, HARDWARE +from openpilot.common.hardware import AGNOS, HARDWARE from openpilot.system.version import get_build_metadata, SP_BRANCH_MIGRATIONS LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock") @@ -194,7 +194,7 @@ def finalize_update() -> None: def handle_agnos_update() -> None: - from openpilot.system.hardware.tici.agnos import flash_agnos_update, get_target_slot_number + from openpilot.common.hardware.tici.agnos import flash_agnos_update, get_target_slot_number cur_version = HARDWARE.get_os_version() updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \ diff --git a/system/webrtc/device/video.py b/system/webrtc/device/video.py index 8a07c5b9be..69f628cb11 100644 --- a/system/webrtc/device/video.py +++ b/system/webrtc/device/video.py @@ -4,9 +4,15 @@ import time import av from teleoprtc.tracks import TiciVideoStreamTrack +from aiortc import MediaStreamError from cereal import messaging -from openpilot.common.realtime import DT_MDL, DT_DMON +from openpilot.common.realtime import DT_MDL +from openpilot.common.params import Params + + +# v4l2 buffer flag marking an encoded keyframe (linux/videodev2.h) +V4L2_BUF_FLAG_KEYFRAME = 0x8 # arbitrary 16-byte UUID identifying openpilot frame-timing SEI messages TIMING_SEI_UUID = bytes([ @@ -23,14 +29,20 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): "road": "livestreamRoadEncodeData", } - def __init__(self, camera_type: str): - dt = DT_DMON if camera_type == "driver" else DT_MDL - super().__init__(camera_type, dt) + def __init__(self, camera_type: str, video_enabled: bool = True): + super().__init__(camera_type, DT_MDL) self._sock = self._make_sock(camera_type) self._pts = 0 self._t0_ns = time.monotonic_ns() self.timing_sei_enabled = False + self.params = Params() + self._seen_keyframe = False + self.video_enabled = video_enabled + + def stop(self) -> None: + super().stop() + self._sock = None def _make_sock(self, camera_type: str) -> messaging.SubSocket: return messaging.sub_sock(self.camera_to_sock_mapping[camera_type], conflate=True) @@ -38,6 +50,11 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): def switch_camera(self, camera_type: str) -> None: self._sock = self._make_sock(camera_type) + def enable(self, enabled: bool): + self.video_enabled = enabled + if not enabled: + self._seen_keyframe = False + def _build_frame_data(self, msg) -> bytes: encode_data = getattr(msg, msg.which()) if not self.timing_sei_enabled: @@ -54,8 +71,19 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): async def recv(self): while True: + if self.readyState != "live": + raise MediaStreamError + + # while video is disabled, pause here without returning + if not self.video_enabled: + await asyncio.sleep(0.005) + continue + msg = messaging.recv_one_or_none(self._sock) if msg is not None: + if not self._seen_keyframe and (getattr(msg, msg.which()).idx.flags & V4L2_BUF_FLAG_KEYFRAME): + self._seen_keyframe = True + self.params.put("LivestreamRequestKeyframe", False, block=False) break await asyncio.sleep(0.005) diff --git a/system/webrtc/helpers.py b/system/webrtc/helpers.py new file mode 100644 index 0000000000..776b64573e --- /dev/null +++ b/system/webrtc/helpers.py @@ -0,0 +1,40 @@ +import time +import requests +from dataclasses import asdict, dataclass, field + + +WEBRTCD_PORT = 5001 + +@dataclass +class StreamRequestBody: + sdp: str + init_camera: str + enabled: bool + bridge_services_in: list[str] = field(default_factory=list) + bridge_services_out: list[str] = field(default_factory=list) + + +def post_stream_request(body: StreamRequestBody) -> dict: + t_start = time.monotonic() + try: + resp = requests.post(f"http://localhost:{WEBRTCD_PORT}/stream", json=asdict(body), timeout=10) + t_end = time.monotonic() + ret = resp.json() + ret["time"] = (t_end - t_start) * 1000 + return ret + except requests.ConnectTimeout as e: + raise Exception("webrtc took too long to respond.") from e + except requests.ConnectionError as e: + raise Exception("webrtc server on device is not running.") from e + + +def wait_for_webrtcd(max_retries: float = 10) -> None: + attempts = 0 + while attempts < max_retries: + try: + if requests.get(f"http://localhost:{WEBRTCD_PORT}/schema", timeout=1).ok: + return + except requests.ConnectionError: + attempts += 1 + time.sleep(0.5) + raise TimeoutError("webrtcd did not initialize in time.") diff --git a/system/webrtc/tests/test_stream_session.py b/system/webrtc/tests/test_stream_session.py index f44d217d58..bf413eeeba 100644 --- a/system/webrtc/tests/test_stream_session.py +++ b/system/webrtc/tests/test_stream_session.py @@ -32,12 +32,11 @@ class TestStreamSession: expected_json = json.dumps(expected_dict).encode() channel = mocker.Mock(spec=RTCDataChannel) - mocked_submaster = messaging.SubMaster(["customReservedRawData0"]) + proxy = CerealOutgoingMessageProxy(["customReservedRawData0"]) def mocked_update(t): - mocked_submaster.update_msgs(0, [test_msg]) + proxy.sm.update_msgs(0, [test_msg]) mocker.patch.object(messaging.SubMaster, "update", side_effect=mocked_update) - proxy = CerealOutgoingMessageProxy(mocked_submaster) proxy.add_channel(channel) proxy.update() diff --git a/system/webrtc/webrtcd.py b/system/webrtc/webrtcd.py index aac020dd06..da10855a59 100755 --- a/system/webrtc/webrtcd.py +++ b/system/webrtc/webrtcd.py @@ -2,6 +2,7 @@ from abc import abstractmethod import os +import socket import time import argparse import asyncio @@ -9,7 +10,6 @@ import contextlib import json import uuid import logging -from dataclasses import dataclass, field from typing import Any, TYPE_CHECKING # aiortc and its dependencies have lots of internal warnings :( @@ -21,12 +21,37 @@ import capnp from aiohttp import web if TYPE_CHECKING: from aiortc.rtcdatachannel import RTCDataChannel +import aioice.ice +from openpilot.system.webrtc.helpers import StreamRequestBody from openpilot.system.webrtc.schema import generate_field from openpilot.common.params import Params from cereal import messaging, log +# socket trick: route lookup for 8.8.8.8 (nothing is sent or actually connected to) +# return the source interfaces IP which is the default interface of the device +def _default_route_ip() -> str | None: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 53)) # selects a route, sends nothing + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() + +# aioice patch: gather ICE candidates only on the default-route interface +_get_host_addresses = aioice.ice.get_host_addresses +def _primary_host_addresses(use_ipv4: bool, use_ipv6: bool) -> list[str]: + addresses = _get_host_addresses(use_ipv4, use_ipv6) + primary = _default_route_ip() + if primary not in addresses: + return addresses + return [primary, ] +aioice.ice.get_host_addresses = _primary_host_addresses + + class AsyncTaskRunner: def __init__(self): self.is_running = False @@ -54,14 +79,19 @@ class AsyncTaskRunner: class CerealOutgoingMessageProxy(AsyncTaskRunner): - def __init__(self, sm: messaging.SubMaster): + def __init__(self, services: list[str], enabled: bool = True): super().__init__() - self.sm = sm + self.services = list(services) + self.sm = messaging.SubMaster(self.services) self.channels: list[RTCDataChannel] = [] + self._enabled = enabled def add_channel(self, channel: 'RTCDataChannel'): self.channels.append(channel) + def enable(self, enable: bool): + self._enabled = enable + def to_json(self, msg_content: Any): if isinstance(msg_content, capnp._DynamicStructReader): msg_dict = msg_content.to_dict() @@ -91,6 +121,9 @@ class CerealOutgoingMessageProxy(AsyncTaskRunner): from aiortc.exceptions import InvalidStateError while True: + if not self._enabled: + await asyncio.sleep(0.01) + continue try: self.update() except InvalidStateError: @@ -139,20 +172,27 @@ class LivestreamBitrateController(AsyncTaskRunner): down_samples = 5 # 1s param_name = "LivestreamEncoderBitrate" - def __init__(self, peer_connection: Any): + def __init__(self, peer_connection: Any, params: Params, enabled: bool = True): super().__init__() self.pc = peer_connection - self.params = Params() + self.params = params - self.level = 0 + self.level = 2 + self._publish(self.bitrates[self.level]) self.prev_lost, self.prev_sent = None, None self.counter = 0 self.up_samples = 5 # 1s self._auto = True + self._enabled = enabled + + def enable(self, enable: bool): + self._enabled = enable async def run(self): while True: await asyncio.sleep(self.sample_interval) + if not self._enabled: + continue if not self._auto: continue @@ -204,36 +244,38 @@ class LivestreamBitrateController(AsyncTaskRunner): class StreamSession: shared_pub_master = DynamicPubMaster([]) - def __init__(self, sdp: str, init_camera: str, incoming_services: list[str], outgoing_services: list[str], debug_mode: bool = False): - from aiortc.mediastreams import VideoStreamTrack + def __init__(self, body: StreamRequestBody, debug_mode: bool = False): + if debug_mode: + from aiortc.mediastreams import VideoStreamTrack from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack - from teleoprtc import WebRTCAnswerBuilder + from teleoprtc.builder import WebRTCAnswerBuilder - builder = WebRTCAnswerBuilder(sdp) - - self.video_track = LiveStreamVideoStreamTrack(init_camera) if not debug_mode else VideoStreamTrack() - builder.add_video_stream(init_camera, self.video_track) - - self.stream = builder.stream() self.identifier = str(uuid.uuid4()) + self.params = Params() + builder = WebRTCAnswerBuilder(body.sdp) + + self.enabled = body.enabled + self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled) if not debug_mode else VideoStreamTrack() + builder.add_video_stream(body.init_camera, self.video_track) + self.stream = builder.stream() self.incoming_bridge: CerealIncomingMessageProxy | None = None - self.incoming_bridge_services = incoming_services + self.incoming_bridge_services = body.bridge_services_in self.outgoing_bridge: CerealOutgoingMessageProxy | None = None self.bitrate_controller: LivestreamBitrateController | None = None - if len(incoming_services) > 0: + if len(body.bridge_services_in) > 0: self.incoming_bridge = CerealIncomingMessageProxy(self.shared_pub_master) - if len(outgoing_services) > 0: - self.outgoing_bridge = CerealOutgoingMessageProxy(messaging.SubMaster(outgoing_services)) - self.bitrate_controller = LivestreamBitrateController(self.stream.peer_connection) + if len(body.bridge_services_out) > 0: + self.outgoing_bridge = CerealOutgoingMessageProxy(body.bridge_services_out, self.enabled) + self.bitrate_controller = LivestreamBitrateController(self.stream.peer_connection, self.params, self.enabled) self.run_task: asyncio.Task | None = None self._cleanup_lock = asyncio.Lock() self._cleanup_done = False self.logger = logging.getLogger("webrtcd") self.logger.info( - "New stream session (%s), init camera %s, incoming services %s, outgoing services %s", - self.identifier, init_camera, incoming_services, outgoing_services, + "New stream session (%s), init camera %s, video enabled %s, incoming services %s, outgoing services %s", + self.identifier, body.init_camera, body.enabled, body.bridge_services_in, body.bridge_services_out, ) def start(self): @@ -251,7 +293,6 @@ class StreamSession: return await self.stream.start() def message_handler(self, message: bytes): - assert self.incoming_bridge is not None try: payload = json.loads(message) if isinstance(message, (bytes, str)) else None if isinstance(payload, dict): @@ -262,6 +303,14 @@ class StreamSession: self.video_track.switch_camera(payload["data"]["camera"]) case "livestreamSettings": self.bitrate_controller.set_quality(payload["data"]["quality"]) + case "livestreamVideoEnable": + enabled = payload["data"]["enabled"] + self.enabled = enabled + self.video_track.enable(enabled) + self.outgoing_bridge.enable(enabled) + self.bitrate_controller.enable(enabled) + if not enabled: + self.params.put("LivestreamRequestKeyframe", True) case "clockSync": pong = json.dumps({"type": "clockSync", "data": { "action": "pong", "browserSendTime": payload["data"]["browserSendTime"], "deviceTime": time.time() * 1000, # noqa: TID251 @@ -279,11 +328,12 @@ class StreamSession: async def run(self): try: - await self.stream.wait_for_connection() + self.params.put("LivestreamRequestKeyframe", True) + await asyncio.wait_for(self.stream.wait_for_connection(), timeout=15) if self.stream.has_messaging_channel(): + self.stream.set_message_handler(self.message_handler) if self.incoming_bridge is not None: await self.shared_pub_master.add_services_if_needed(self.incoming_bridge_services) - self.stream.set_message_handler(self.message_handler) if self.outgoing_bridge is not None: channel = self.stream.get_messaging_channel() self.outgoing_bridge.add_channel(channel) @@ -291,9 +341,7 @@ class StreamSession: self.bitrate_controller.start() self.logger.info("Stream session (%s) connected", self.identifier) - await self.stream.wait_for_disconnection() - self.logger.info("Stream session (%s) ended", self.identifier) except Exception: self.logger.exception("Stream session failure") @@ -305,19 +353,25 @@ class StreamSession: if self._cleanup_done: return self._cleanup_done = True - if self.bitrate_controller is not None: - await self.bitrate_controller.stop() + self.params.put("LivestreamRequestKeyframe", False) + await self.bitrate_controller.stop() if self.outgoing_bridge is not None: await self.outgoing_bridge.stop() + if self.video_track is not None: + self.video_track.stop() + self.video_track = None await self.stream.stop() -@dataclass -class StreamRequestBody: - sdp: str - initCamera: str - bridge_services_in: list[str] = field(default_factory=list) - bridge_services_out: list[str] = field(default_factory=list) +def schedule_teardown(app): + # if nothing connects for 5 seconds, tear down livestreaming processes + h = app.get('teardown') + if h: + h.cancel() + def clear(): + if not app['streams']: + Params().put_bool("IsLiveStreaming", False) + app['teardown'] = asyncio.get_running_loop().call_later(5.0, clear) async def get_stream(request: 'web.Request'): @@ -326,38 +380,42 @@ async def get_stream(request: 'web.Request'): body = StreamRequestBody(**raw_body) async with request.app['stream_lock']: - # Fully disconnect any other active stream before starting the replacement. + # don't remove existing connection on prewarm request + enabled = any(s.run_task and not s.run_task.done() and s.enabled for s in stream_dict.values()) + if enabled and not body.enabled: + return web.json_response({"error": "busy", "message": "someone else is connected."}) + for sid, s in list(stream_dict.items()): if s.run_task and not s.run_task.done(): try: ch = s.stream.get_messaging_channel() - ch.send(json.dumps({"type": "connectionReplaced", "data": "Another device has connected, closing this session."})) + ch.send(json.dumps({"type": "disconnect", "data": "Another device has connected, closing this session."})) except Exception: pass await s.stop() - del stream_dict[sid] + stream_dict.pop(sid, None) - session = StreamSession(body.sdp, body.initCamera, body.bridge_services_in, body.bridge_services_out, debug_mode) + session = StreamSession(body, debug_mode) + stream_dict[session.identifier] = session try: answer = await session.get_answer() - except ValueError as e: - await session.stop() - raise web.HTTPBadRequest( - text=json.dumps({"error": "invalid_sdp", "message": str(e)}), - content_type="application/json", - ) from e except Exception: await session.stop() + stream_dict.pop(session.identifier, None) + logging.getLogger("webrtcd").exception("Failed to create stream answer") raise session.start() - stream_dict[session.identifier] = session + def remove_finished_session(_: asyncio.Task) -> None: + stream_dict.pop(session.identifier, None) + schedule_teardown(request.app) + session.run_task.add_done_callback(remove_finished_session) return web.json_response({"sdp": answer.sdp, "type": answer.type}) async def get_schema(request: 'web.Request'): - services = request.query["services"].split(",") + services = request.query.get("services", "").split(",") services = [s for s in services if s] assert all(s in log.Event.schema.fields and not s.endswith("DEPRECATED") for s in services), "Invalid service name" schema_dict = {s: generate_field(log.Event.schema.fields[s]) for s in services} @@ -381,18 +439,43 @@ async def post_notify(request: 'web.Request'): async def on_shutdown(app: 'web.Application'): - for session in app['streams'].values(): + for session in list(app['streams'].values()): + try: + ch = session.stream.get_messaging_channel() + ch.send(json.dumps({"type": "disconnect", "data": "device streaming has been stopped."})) + except Exception: + pass await session.stop() del app['streams'] +@web.middleware +async def error_middleware(request: 'web.Request', handler): + try: + return await handler(request) + except Exception as e: + logging.getLogger("webrtcd").exception("Unhandled error handling %s", request.path) + return web.json_response({"error": "exception", "message": f"{type(e).__name__}: {e}"}, status=500) + + +def prewarm_stream_session_imports(debug_mode: bool = False) -> None: + if debug_mode: + from aiortc.mediastreams import VideoStreamTrack + assert VideoStreamTrack + from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack + from teleoprtc.builder import WebRTCAnswerBuilder + assert LiveStreamVideoStreamTrack + assert WebRTCAnswerBuilder + + def webrtcd_thread(host: str, port: int, debug: bool): logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()]) - logging_level = logging.DEBUG if debug else logging.INFO - logging.getLogger("WebRTCStream").setLevel(logging_level) - logging.getLogger("webrtcd").setLevel(logging_level) + prewarm_start = time.monotonic() + prewarm_stream_session_imports(debug) + prewarm_end = time.monotonic() + logging.getLogger("webrtcd").info(f"webrtc prewarm finished in {(prewarm_end - prewarm_start) * 1000} ms") - app = web.Application() + app = web.Application(middlewares=[error_middleware]) app['streams'] = dict() app['stream_lock'] = asyncio.Lock() diff --git a/tools/CTF.md b/tools/CTF.md index 32891cd389..609c82ad5a 100644 --- a/tools/CTF.md +++ b/tools/CTF.md @@ -6,7 +6,7 @@ Welcome to the first part of the comma CTF! * everything you'll need to find the flags is in the openpilot repo * grep is also your friend * first, [setup](https://github.com/commaai/openpilot/tree/master/tools#setup-your-pc) your PC - * read the docs & checkout out the tools in tools/ and selfdrive/debug/ + * read the docs & checkout out the tools in tools/ * tip: once you get the replay and UI up, start by familiarizing yourself with seeking in replay getting started diff --git a/tools/bodyteleop/static/index.html b/tools/bodyteleop/static/index.html deleted file mode 100644 index 48672dbbf0..0000000000 --- a/tools/bodyteleop/static/index.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - commabody - - - - - - - - - - -
-

comma body

- -
-
-
-
-
W
-
-
-
A
-
S
-
D
-
-
-
-
-
-
-
-
-
- - -
-

ping time

-
-
-
- - -
-

battery

-
-
-
-
-
-
-
-
-
-
-
- - - diff --git a/tools/bodyteleop/static/js/controls.js b/tools/bodyteleop/static/js/controls.js deleted file mode 100644 index 3a11f78b9e..0000000000 --- a/tools/bodyteleop/static/js/controls.js +++ /dev/null @@ -1,20 +0,0 @@ -const keyVals = {w: 0, a: 0, s: 0, d: 0} - -export function getXY() { - let x = -keyVals.w + keyVals.s - let y = -keyVals.d + keyVals.a - return {x, y} -} - -export const handleKeyX = (key, setValue) => { - if (['w', 'a', 's', 'd'].includes(key)){ - keyVals[key] = setValue; - let color = "#333"; - if (setValue === 1){ - color = "#e74c3c"; - } - $("#key-"+key).css('background', color); - const {x, y} = getXY(); - $("#pos-vals").text(x+","+y); - } -}; diff --git a/tools/bodyteleop/static/js/jsmain.js b/tools/bodyteleop/static/js/jsmain.js deleted file mode 100644 index 0db1dcd9b3..0000000000 --- a/tools/bodyteleop/static/js/jsmain.js +++ /dev/null @@ -1,21 +0,0 @@ -import { handleKeyX } from "./controls.js"; -import { start, stop, lastChannelMessageTime } from "./webrtc.js"; - -export var pc = null; -export var dc = null; - -document.addEventListener('keydown', (e)=>(handleKeyX(e.key.toLowerCase(), 1))); -document.addEventListener('keyup', (e)=>(handleKeyX(e.key.toLowerCase(), 0))); -$(".keys").bind("mousedown touchstart", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 1)); -$(".keys").bind("mouseup touchend", (e)=>handleKeyX($(e.target).attr('id').replace('key-', ''), 0)); -setInterval( () => { - const dt = new Date().getTime(); - if ((dt - lastChannelMessageTime) > 1000) { - $(".pre-blob").removeClass('blob'); - $("#battery").text("-"); - $("#ping-time").text('-'); - $("video")[0].load(); - } -}, 5000); - -start(pc, dc); diff --git a/tools/bodyteleop/static/js/plots.js b/tools/bodyteleop/static/js/plots.js deleted file mode 100644 index 5327bf71be..0000000000 --- a/tools/bodyteleop/static/js/plots.js +++ /dev/null @@ -1,53 +0,0 @@ -export const pingPoints = []; -export const batteryPoints = []; - -function getChartConfig(pts, color, title, ymax=100) { - return { - type: 'line', - data: { - datasets: [{ - label: title, - data: pts, - borderWidth: 1, - borderColor: color, - backgroundColor: color, - fill: 'origin' - }] - }, - options: { - scales: { - x: { - type: 'time', - time: { - unit: 'minute', - displayFormats: { - second: 'h:mm a' - } - }, - grid: { - color: '#222', // Grid lines color - }, - ticks: { - source: 'data', - fontColor: 'rgba(255, 255, 255, 1.0)', // Y-axis label color - } - }, - y: { - beginAtZero: true, - max: ymax, - grid: { - color: 'rgba(255, 255, 255, 0.1)', // Grid lines color - }, - ticks: { - fontColor: 'rgba(255, 255, 255, 0.7)', // Y-axis label color - } - } - } - } - } -} - -const ctxPing = document.getElementById('chart-ping'); -const ctxBattery = document.getElementById('chart-battery'); -export const chartPing = new Chart(ctxPing, getChartConfig(pingPoints, 'rgba(192, 57, 43, 0.7)', 'Controls Ping Time (ms)', 250)); -export const chartBattery = new Chart(ctxBattery, getChartConfig(batteryPoints, 'rgba(41, 128, 185, 0.7)', 'Battery %', 100)); diff --git a/tools/bodyteleop/static/js/webrtc.js b/tools/bodyteleop/static/js/webrtc.js deleted file mode 100644 index 28bea238e6..0000000000 --- a/tools/bodyteleop/static/js/webrtc.js +++ /dev/null @@ -1,175 +0,0 @@ -import { getXY } from "./controls.js"; -import { pingPoints, batteryPoints, chartPing, chartBattery } from "./plots.js"; - -export let controlCommandInterval = null; -export let latencyInterval = null; -export let lastChannelMessageTime = null; - - -export function offerRtcRequest(sdp, type) { - return fetch('/offer', { - body: JSON.stringify({sdp: sdp, type: type}), - headers: {'Content-Type': 'application/json'}, - method: 'POST' - }); -} - - -export function pingHeadRequest() { - return fetch('/', { - method: 'HEAD' - }); -} - - -export function createPeerConnection(pc) { - var config = { - sdpSemantics: 'unified-plan' - }; - - pc = new RTCPeerConnection(config); - - // connect video - pc.addEventListener('track', function(evt) { - console.log("Adding Tracks!") - if (evt.track.kind == 'video') - document.getElementById('video').srcObject = evt.streams[0]; - }); - return pc; -} - - -export function negotiate(pc) { - return pc.createOffer({offerToReceiveVideo:true}).then(function(offer) { - return pc.setLocalDescription(offer); - }).then(function() { - return new Promise(function(resolve) { - if (pc.iceGatheringState === 'complete') { - resolve(); - } - else { - function checkState() { - if (pc.iceGatheringState === 'complete') { - pc.removeEventListener('icegatheringstatechange', checkState); - resolve(); - } - } - pc.addEventListener('icegatheringstatechange', checkState); - } - }); - }).then(function() { - var offer = pc.localDescription; - return offerRtcRequest(offer.sdp, offer.type); - }).then(function(response) { - console.log(response); - return response.json(); - }).then(function(answer) { - return pc.setRemoteDescription(answer); - }).catch(function(e) { - alert(e); - }); -} - - -function isMobile() { - let check = false; - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); - return check; -}; - - -export const constraints = { - video: isMobile() -}; - - -export function start(pc, dc) { - pc = createPeerConnection(pc); - - // add a local video track on mobile - (constraints.video ? navigator.mediaDevices.getUserMedia(constraints) : Promise.resolve(null)) - .then(function(stream) { - if (stream) { - stream.getTracks().forEach(function(track) { - pc.addTrack(track, stream); - }); - } - - return negotiate(pc); - }) - .catch(function(err) { - alert('Could not acquire media: ' + err); - }); - - var parameters = {"ordered": true}; - dc = pc.createDataChannel('data', parameters); - dc.onclose = function() { - clearInterval(controlCommandInterval); - clearInterval(latencyInterval); - }; - - function sendJoystickOverDataChannel() { - const {x, y} = getXY(); - var message = JSON.stringify({type: "testJoystick", data: {axes: [x, y], buttons: [false]}}) - dc.send(message); - } - function checkLatency() { - const initialTime = new Date().getTime(); - pingHeadRequest().then(function() { - const currentTime = new Date().getTime(); - if (Math.abs(currentTime - lastChannelMessageTime) < 1000) { - const pingtime = currentTime - initialTime; - pingPoints.push({'x': currentTime, 'y': pingtime}); - if (pingPoints.length > 1000) { - pingPoints.shift(); - } - chartPing.update(); - $("#ping-time").text((pingtime) + "ms"); - } - }) - } - dc.onopen = function() { - controlCommandInterval = setInterval(sendJoystickOverDataChannel, 50); - latencyInterval = setInterval(checkLatency, 1000); - sendJoystickOverDataChannel(); - }; - - const textDecoder = new TextDecoder(); - var carStaterIndex = 0; - dc.onmessage = function(evt) { - const text = textDecoder.decode(evt.data); - const msg = JSON.parse(text); - if (carStaterIndex % 100 == 0 && msg.type === 'carState') { - const batteryLevel = Math.round(msg.data.fuelGauge * 100); - $("#battery").text(batteryLevel + "%"); - batteryPoints.push({'x': new Date().getTime(), 'y': batteryLevel}); - if (batteryPoints.length > 1000) { - batteryPoints.shift(); - } - chartBattery.update(); - } - carStaterIndex += 1; - lastChannelMessageTime = new Date().getTime(); - $(".pre-blob").addClass('blob'); - }; -} - - -export function stop(pc, dc) { - if (dc) { - dc.close(); - } - if (pc.getTransceivers) { - pc.getTransceivers().forEach(function(transceiver) { - if (transceiver.stop) { - transceiver.stop(); - } - }); - } - pc.getSenders().forEach(function(sender) { - sender.track.stop(); - }); - setTimeout(function() { - pc.close(); - }, 500); -} diff --git a/tools/bodyteleop/static/main.css b/tools/bodyteleop/static/main.css deleted file mode 100644 index 79fe8052ff..0000000000 --- a/tools/bodyteleop/static/main.css +++ /dev/null @@ -1,178 +0,0 @@ -body { - background: #333 !important; - color: #fff !important; - display: flex; - justify-content: center; - align-items: start; -} - -p { - margin: 0px !important; -} - -i { - font-style: normal; -} - -.small { - font-size: 1em !important -} - -.jumbo { - font-size: 8rem; -} - - -@media (max-width: 600px) { - .small { - font-size: 0.5em !important - } - .jumbo { - display: none; - } - -} - -#main { - display: flex; - flex-direction: column; - align-content: center; - justify-content: center; - align-items: center; - font-size: 30px; - width: 100%; - max-width: 1200px; -} - -video { - width: 95%; -} - -.pre-blob { - display: flex; - background: #333; - border-radius: 50%; - margin: 10px; - height: 45px; - width: 45px; - justify-content: center; - align-items: center; - font-size: 1rem; -} - -.blob { - background: rgba(231, 76, 60,1.0); - box-shadow: 0 0 0 0 rgba(231, 76, 60,1.0); - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { - box-shadow: 0 0 0 0px rgba(192, 57, 43, 1); - } - 100% { - box-shadow: 0 0 0 20px rgba(192, 57, 43, 0); - } -} - - -.icon-sup-panel { - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: center; - background: #222; - border-radius: 10px; - padding: 5px; - margin: 5px 0px 5px 0px; -} - -.icon-sub-panel { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; -} - -#icon-panel { - display: flex; - width: 100%; - justify-content: space-between; - margin-top: 5px; -} - -.icon-sub-sub-panel { - display: flex; - flex-direction: row; -} - -.keys, #key-val { - background: #333; - padding: 2rem; - margin: 5px; - color: #fff; - display: flex; - justify-content: center; - align-items: center; - border-radius: 10px; - cursor: pointer; -} - -#key-val { - pointer-events: none; - background: #fff; - color: #333; - line-height: 1; - font-size: 20px; - flex-direction: column; -} - -.wasd-row { - display: flex; - flex-direction: row; - justify-content: center; - align-items: stretch; -} - -#wasd { - margin: 5px 0px 5px 0px; - background: #222; - border-radius: 10px; - width: 100%; - padding: 20px; - display: flex; - flex-direction: row; - justify-content: space-around; - align-items: stretch; - - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - touch-action: manipulation; -} - -.panel { - display: flex; - justify-content: center; - margin: 5px 0px 5px 0px !important; - background: #222; - border-radius: 10px; - width: 100%; - padding: 10px; -} - -#ping-time, #battery { - font-size: 25px; -} - -#stop { - display: none; -} - -.details { - display: flex; - padding: 0px 10px 0px 10px; -} diff --git a/tools/bodyteleop/static/poster.png b/tools/bodyteleop/static/poster.png deleted file mode 100644 index 2f2b02dd8a..0000000000 --- a/tools/bodyteleop/static/poster.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8740da2be7faac198b5e10780c646166056a76ebbe3d64499e0cdc49280c8a4f -size 8297 diff --git a/tools/bodyteleop/web.py b/tools/bodyteleop/web.py deleted file mode 100644 index d357561b22..0000000000 --- a/tools/bodyteleop/web.py +++ /dev/null @@ -1,86 +0,0 @@ -import dataclasses -import json -import logging -import os -import ssl -import subprocess - -from aiohttp import web -from aiohttp import ClientSession - -from openpilot.common.basedir import BASEDIR -from openpilot.system.webrtc.webrtcd import StreamRequestBody -from openpilot.common.params import Params - -logger = logging.getLogger("bodyteleop") -logging.basicConfig(level=logging.INFO) - -TELEOPDIR = f"{BASEDIR}/tools/bodyteleop" -WEBRTCD_HOST, WEBRTCD_PORT = "localhost", 5001 - - -## SSL -def create_ssl_cert(cert_path: str, key_path: str): - try: - proc = subprocess.run(f'openssl req -x509 -newkey rsa:4096 -nodes -out {cert_path} -keyout {key_path} \ - -days 365 -subj "/C=US/ST=California/O=commaai/OU=comma body"', - capture_output=True, shell=True) - proc.check_returncode() - except subprocess.CalledProcessError as ex: - raise ValueError(f"Error creating SSL certificate:\n[stdout]\n{proc.stdout.decode()}\n[stderr]\n{proc.stderr.decode()}") from ex - - -def create_ssl_context(): - cert_path = os.path.join(TELEOPDIR, "cert.pem") - key_path = os.path.join(TELEOPDIR, "key.pem") - if not os.path.exists(cert_path) or not os.path.exists(key_path): - logger.info("Creating certificate...") - create_ssl_cert(cert_path, key_path) - else: - logger.info("Certificate exists!") - ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) - ssl_context.load_cert_chain(cert_path, key_path) - - return ssl_context - -## ENDPOINTS -async def index(request: 'web.Request'): - with open(os.path.join(TELEOPDIR, "static", "index.html")) as f: - content = f.read() - return web.Response(content_type="text/html", text=content) - - -async def ping(request: 'web.Request'): - return web.Response(text="pong") - - -async def offer(request: 'web.Request'): - params = await request.json() - body = StreamRequestBody(params["sdp"], "driver", ["testJoystick"], ["carState"]) - body_json = json.dumps(dataclasses.asdict(body)) - - logger.info("Sending offer to webrtcd...") - webrtcd_url = f"http://{WEBRTCD_HOST}:{WEBRTCD_PORT}/stream" - async with ClientSession() as session, session.post(webrtcd_url, data=body_json) as resp: - assert resp.status == 200 - answer = await resp.json() - return web.json_response(answer) - - -def main(): - # Enable joystick debug mode - Params().put_bool("JoystickDebugMode", True, block=True) - - # App needs to be HTTPS for WebRTC to work on the browser - ssl_context = create_ssl_context() - - app = web.Application() - app.router.add_get("/", index) - app.router.add_get("/ping", ping, allow_head=True) - app.router.add_post("/offer", offer) - app.router.add_static('/static', os.path.join(TELEOPDIR, 'static')) - web.run_app(app, access_log=None, host="0.0.0.0", port=5000, ssl_context=ssl_context) - - -if __name__ == "__main__": - main() diff --git a/tools/cabana/tools/findsignal.cc b/tools/cabana/tools/findsignal.cc index b131893942..d538d676b1 100644 --- a/tools/cabana/tools/findsignal.cc +++ b/tools/cabana/tools/findsignal.cc @@ -98,9 +98,9 @@ FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags( message_group = new QGroupBox(tr("Messages"), this); QFormLayout *message_layout = new QFormLayout(message_group); message_layout->addRow(tr("Bus"), bus_edit = new QLineEdit()); - bus_edit->setPlaceholderText(tr("comma-seperated values. Leave blank for all")); + bus_edit->setPlaceholderText(tr("comma-separated values. Leave blank for all")); message_layout->addRow(tr("Address"), address_edit = new QLineEdit()); - address_edit->setPlaceholderText(tr("comma-seperated hex values. Leave blank for all")); + address_edit->setPlaceholderText(tr("comma-separated hex values. Leave blank for all")); QHBoxLayout *hlayout = new QHBoxLayout(); hlayout->addWidget(first_time_edit = new QLineEdit("0")); hlayout->addWidget(new QLabel("-")); diff --git a/tools/clip/run.py b/tools/clip/run.py index d3a0496b1e..fa4b203862 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -312,8 +312,15 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, camera_paths = route.qcamera_paths() if use_qcam else route.camera_paths() frame_queue = FrameQueue(camera_paths, start, end, fps=FRAMERATE, use_qcam=use_qcam) + ecamera_paths = route.ecamera_paths() if not use_qcam else [] + wide_frame_queue: FrameQueue | None = None + if any(p for p in ecamera_paths[seg_start:seg_end] if p): + wide_frame_queue = FrameQueue(ecamera_paths, start, end, fps=FRAMERATE) + vipc = VisionIpcServer("camerad") vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) + if wide_frame_queue: + vipc.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 4, wide_frame_queue.frame_w, wide_frame_queue.frame_h) vipc.start_listener() patch_submaster(message_chunks, ui_state) @@ -331,6 +338,9 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, break _, frame_bytes = frame_queue.get() vipc.send(VisionStreamType.VISION_STREAM_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) + if wide_frame_queue: + _, wide_bytes = wide_frame_queue.get() + vipc.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, wide_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) ui_state.update() if should_render: road_view.render() @@ -340,6 +350,8 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, timer.lap("render") frame_queue.stop() + if wide_frame_queue: + wide_frame_queue.stop() gui_app.close() timer.lap("ffmpeg") diff --git a/tools/jotpluggler/app.cc b/tools/jotpluggler/app.cc index 22a9f9021c..f80443e569 100644 --- a/tools/jotpluggler/app.cc +++ b/tools/jotpluggler/app.cc @@ -3,7 +3,7 @@ #include "tools/jotpluggler/common.h" #include "tools/jotpluggler/internal.h" #include "tools/jotpluggler/map.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "imgui_impl_glfw.h" #include "imgui_internal.h" diff --git a/tools/jotpluggler/layout.cc b/tools/jotpluggler/layout.cc index 0d3b82c254..bf2a3f208c 100644 --- a/tools/jotpluggler/layout.cc +++ b/tools/jotpluggler/layout.cc @@ -1,5 +1,5 @@ #include "tools/jotpluggler/internal.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include diff --git a/tools/joystick/joystick_control.py b/tools/joystick/joystick_control.py index b1f14043b7..d9d4f0156f 100755 --- a/tools/joystick/joystick_control.py +++ b/tools/joystick/joystick_control.py @@ -8,7 +8,7 @@ from inputs import UnpluggedError, get_gamepad from cereal import messaging from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE from openpilot.tools.lib.kbhit import KBHit EXPO = 0.4 diff --git a/tools/lateral_maneuvers/generate_report.py b/tools/lateral_maneuvers/generate_report.py index 9a6fe1b979..5db486a70d 100755 --- a/tools/lateral_maneuvers/generate_report.py +++ b/tools/lateral_maneuvers/generate_report.py @@ -15,7 +15,7 @@ from cereal import car from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.controls.lib.latcontrol_torque import LP_FILTER_CUTOFF_HZ from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.common.constants import CV from openpilot.tools.longitudinal_maneuvers.generate_report import format_car_params @@ -51,6 +51,8 @@ def report(platform, route, _description, CP, ID, maneuvers): builder.append("
\n") builder.append(f"

{description}

\n") for run, msgs in enumerate(completed_runs): + last_active = max(m.logMonoTime for m in msgs if m.which() == 'lateralManeuverPlan' and m.valid) + msgs = [m for m in msgs if m.logMonoTime <= last_active] t_carControl, carControl = zip(*[(m.logMonoTime, m.carControl) for m in msgs if m.which() == 'carControl'], strict=True) t_carState, carState = zip(*[(m.logMonoTime, m.carState) for m in msgs if m.which() == 'carState'], strict=True) t_controlsState, controlsState = zip(*[(m.logMonoTime, m.controlsState) for m in msgs if m.which() == 'controlsState'], strict=True) @@ -77,7 +79,7 @@ def report(platform, route, _description, CP, ID, maneuvers): v_ego = [m.vEgo for m in carState] cross_markers = [] - if description.startswith('sine'): + if description.startswith(('sine', 'jitter')): amplitude = max(abs(lat_accel(lp.desiredCurvature, v) - baseline_accel) for lp, v in zip(lateralPlan, v_ego, strict=False)) threshold = amplitude * 0.5 @@ -128,58 +130,58 @@ def report(platform, route, _description, CP, ID, maneuvers): target_cross_times.setdefault(description, []) plt.rcParams['font.size'] = 40 - fig = plt.figure(figsize=(30, 30)) - ax = fig.subplots(4, 1, sharex=True, gridspec_kw={'height_ratios': [5, 3, 3, 3]}) + fig = plt.figure(figsize=(30, 40)) + ax = fig.subplots(5, 1, sharex=True, gridspec_kw={'height_ratios': [5, 5, 3, 3, 3]}) ax[0].grid(linewidth=4) + desired_label = 'lateralManeuverPlan.desiredCurvature * vEgo^2' desired_lat_accel = [lat_accel(m.desiredCurvature, v) for m, v in zip(lateralPlan, v_ego, strict=False)] - if description.startswith('sine'): - ax[0].plot(t_lateralPlan[:len(desired_lat_accel)], desired_lat_accel, label='desired lat accel', linewidth=6) + if description.startswith(('sine', 'jitter')): + ax[0].plot(t_lateralPlan[:len(desired_lat_accel)], desired_lat_accel, 'C1', label=desired_label, linewidth=6) else: t_desired = [t_lateralPlan[0]] + t_lateralPlan[:len(desired_lat_accel)] desired_lat_accel = [baseline_accel] + desired_lat_accel - ax[0].step(t_desired, desired_lat_accel, label='desired lat accel', linewidth=6, where='post') + ax[0].step(t_desired, desired_lat_accel, 'C1', label=desired_label, linewidth=6, where='post') actual_lat_accel = [lat_accel(cs.curvature, v) for cs, v in zip(controlsState, v_ego, strict=False)] - ax[0].plot(t_controlsState[:len(actual_lat_accel)], actual_lat_accel, label='actual lat accel', linewidth=6) + ax[0].plot(t_controlsState[:len(actual_lat_accel)], actual_lat_accel, 'g', label='controlsState.curvature * vEgo^2', linewidth=6) ax[0].set_ylabel('Lateral Accel (m/s^2)') - for ct, cv in cross_markers: ax[0].plot(ct, cv, marker='o', markersize=50, markeredgewidth=7, markeredgecolor='black', markerfacecolor='None') - - ax2 = ax[0].twinx() - if CP.steerControlType == car.CarParams.SteerControlType.angle: - ax2.plot(t_carOutput, [-m.actuatorsOutput.steeringAngleDeg for m in carOutput], 'C2', label='steer angle', linewidth=6) - else: - ax2.plot(t_carOutput, [-m.actuatorsOutput.torque for m in carOutput], 'C2', label='steer torque', linewidth=6) - - h1, l1 = ax[0].get_legend_handles_labels() - h2, l2 = ax2.get_legend_handles_labels() - ax[0].legend(h1 + h2, l1 + l2, prop={'size': 30}) + ax[0].legend(prop={'size': 30}) ax[1].grid(linewidth=4) - ax[1].plot(t_carState, [v * CV.MS_TO_MPH for v in v_ego], label='vEgo', linewidth=6) - ax[1].set_ylabel('Velocity (mph)') - ax[1].yaxis.set_major_formatter(plt.FormatStrFormatter('%.1f')) - ax[1].legend() + if CP.steerControlType == car.CarParams.SteerControlType.angle: + steer_field, steer_ylabel = 'steeringAngleDeg', 'Steer angle (deg)' + elif CP.steerControlType == car.CarParams.SteerControlType.curvature: + steer_field, steer_ylabel = 'curvature', 'Curvature (1/m)' + else: + steer_field, steer_ylabel = 'torque', 'Steer torque' + ax[1].plot(t_carControl, [getattr(m.actuators, steer_field) for m in carControl], 'C1', label=f'carControl.actuators.{steer_field}', linewidth=6) + ax[1].plot(t_carOutput, [getattr(m.actuatorsOutput, steer_field) for m in carOutput], 'g', label=f'carOutput.actuatorsOutput.{steer_field}', linewidth=6) + ax[1].set_ylabel(steer_ylabel) + ax[1].legend(prop={'size': 30}) + + ax[2].grid(linewidth=4) + ax[2].plot(t_carState, [v * CV.MS_TO_MPH for v in v_ego], label='carState.vEgo', linewidth=6) + ax[2].set_ylabel('Velocity (mph)') + ax[2].yaxis.set_major_formatter(plt.FormatStrFormatter('%.1f')) + ax[2].legend() t_accel = np.array(t_controlsState[:len(actual_lat_accel)]) raw_jerk = np.gradient(actual_lat_accel, t_accel) dt_avg = np.mean(np.diff(t_accel)) jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), dt_avg) filtered_jerk = [jerk_filter.update(j) for j in raw_jerk] - ax[2].grid(linewidth=4) - ax[2].plot(t_accel, filtered_jerk, label='actual jerk', linewidth=6) - if CP.steerControlType == car.CarParams.SteerControlType.torque: - desired_jerk = [cs.lateralControlState.torqueState.desiredLateralJerk for cs in controlsState] - ax[2].plot(t_controlsState[:len(controlsState)], desired_jerk, label='desired jerk', linewidth=6) - ax[2].set_ylabel('Jerk (m/s^3)') - ax[2].legend() - ax[3].grid(linewidth=4) - ax[3].plot(t_carControl, [math.degrees(m.orientationNED[0]) for m in carControl], label='roll', linewidth=6) - ax[3].set_ylabel('Roll (deg)') + ax[3].plot(t_accel, filtered_jerk, label='d/dt(controlsState.curvature * vEgo^2)', linewidth=6) + ax[3].set_ylabel('Jerk (m/s^3)') ax[3].legend() + ax[4].grid(linewidth=4) + ax[4].plot(t_carControl, [math.degrees(m.orientationNED[0]) for m in carControl], label='carControl.orientationNED[0]', linewidth=6) + ax[4].set_ylabel('Roll (deg)') + ax[4].legend() + ax[-1].set_xlabel("Time (s)") fig.tight_layout() diff --git a/tools/lateral_maneuvers/lateral_maneuversd.py b/tools/lateral_maneuvers/lateral_maneuversd.py index 4f68d9be08..1cc2d3560e 100755 --- a/tools/lateral_maneuvers/lateral_maneuversd.py +++ b/tools/lateral_maneuvers/lateral_maneuversd.py @@ -12,7 +12,7 @@ from openpilot.tools.longitudinal_maneuvers.maneuversd import Action, Maneuver a # thresholds for starting maneuvers MAX_SPEED_DEV = 0.7 # deviation in m/s -MAX_CURV = 0.002 # 500 m radius +MAX_CURV = 0.004 # 250 m radius MAX_ROLL = 0.12 # 6.8° TIMER = 2.0 # sec stable conditions before starting maneuver @@ -66,6 +66,12 @@ MANEUVERS = [ repeat=2, initial_speed=20. * CV.MPH_TO_MS, ), + Maneuver( + "jitter 20mph", + [Action([-0.5 if i % 2 == 0 else 0.5], [0.1]) for i in range(10)], + repeat=2, + initial_speed=20. * CV.MPH_TO_MS, + ), Maneuver( "step right 30mph", [Action([0.5], [1.0]), Action([-0.5], [1.5])], @@ -84,6 +90,12 @@ MANEUVERS = [ repeat=2, initial_speed=30. * CV.MPH_TO_MS, ), + Maneuver( + "jitter 30mph", + [Action([-0.5 if i % 2 == 0 else 0.5], [0.1]) for i in range(10)], + repeat=2, + initial_speed=30. * CV.MPH_TO_MS, + ), ] @@ -98,6 +110,8 @@ def main(): maneuvers = iter(MANEUVERS) maneuver = None complete_cnt = 0 + aborted_cnt = 0 + abort_reason = '' display_holdoff = 0 prev_text = '' @@ -121,8 +135,14 @@ def main(): alert_msg.alertDebug.alertText1 = 'Completed' alert_msg.alertDebug.alertText2 = maneuver.description elif maneuver is not None: - # reset maneuver on steering override or out of range speed - if sm['carState'].steeringPressed or (maneuver.active and abs(v_ego - maneuver.initial_speed) > MAX_SPEED_DEV): + # any driver input aborts the maneuver + CS = sm['carState'] + if CS.steeringPressed or CS.gasPressed: + aborted_cnt = int(1.0 / DT_MDL) + abort_reason = ('steering pressed' if CS.steeringPressed else 'gas pressed').ljust(20) + aborted = aborted_cnt > 0 + speed_out_of_range = maneuver.active and abs(v_ego - maneuver.initial_speed) > MAX_SPEED_DEV + if aborted or speed_out_of_range: maneuver.reset() roll = sm['carControl'].orientationNED[0] if len(sm['carControl'].orientationNED) == 3 else 0.0 @@ -140,6 +160,9 @@ def main(): else: alert_msg.alertDebug.alertText1 = f'Active {accel:+.1f}m/s² {max(action_remaining, 0):.1f}s' alert_msg.alertDebug.alertText2 = maneuver.description + elif aborted_cnt > 0: + aborted_cnt -= 1 + alert_msg.alertDebug.alertText1 = abort_reason elif not (abs(v_ego - maneuver.initial_speed) < MAX_SPEED_DEV and sm['carControl'].latActive): alert_msg.alertDebug.alertText1 = f'Set speed to {maneuver.initial_speed * CV.MS_TO_MPH:0.0f} mph' elif maneuver._ready_cnt > 0: diff --git a/tools/lib/auth_config.py b/tools/lib/auth_config.py index c4e7b6261b..5966bd34a9 100644 --- a/tools/lib/auth_config.py +++ b/tools/lib/auth_config.py @@ -1,6 +1,6 @@ import json import os -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths class MissingAuthConfigError(Exception): diff --git a/tools/lib/file_downloader.py b/tools/lib/file_downloader.py index 5b31a5894c..68061b201e 100755 --- a/tools/lib/file_downloader.py +++ b/tools/lib/file_downloader.py @@ -17,7 +17,7 @@ import sys import tempfile import shutil -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.api import CommaApi, UnauthorizedError, APIError from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.url_file import URLFile diff --git a/tools/lib/tests/test_caching.py b/tools/lib/tests/test_caching.py index cb14098e6d..0753ef1d3c 100644 --- a/tools/lib/tests/test_caching.py +++ b/tools/lib/tests/test_caching.py @@ -6,7 +6,7 @@ import tempfile import pytest from openpilot.selfdrive.test.helpers import http_server_context -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.url_file import URLFile, prune_cache import openpilot.tools.lib.url_file as url_file_module diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index de12070465..3f72429b94 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -9,7 +9,7 @@ from urllib3.response import BaseHTTPResponse from urllib3.util import Timeout from openpilot.common.utils import atomic_write -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths from urllib3.exceptions import MaxRetryError # Cache chunk size diff --git a/tools/longitudinal_maneuvers/generate_report.py b/tools/longitudinal_maneuvers/generate_report.py index 32bdb5b1c4..dbd9f6db91 100755 --- a/tools/longitudinal_maneuvers/generate_report.py +++ b/tools/longitudinal_maneuvers/generate_report.py @@ -12,7 +12,7 @@ import matplotlib.pyplot as plt from openpilot.common.utils import tabulate from openpilot.tools.lib.logreader import LogReader -from openpilot.system.hardware.hw import Paths +from openpilot.common.hardware.hw import Paths def format_car_params(CP): diff --git a/tools/op.sh b/tools/op.sh index 538d689569..ecef240b5b 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -48,7 +48,7 @@ function retry() { } function op_run_command() { - CMD="$@" + CMD="$*" echo -e "${BOLD}Running command →${NC} $CMD │" for ((i=0; i<$((19 + ${#CMD})); i++)); do @@ -57,7 +57,7 @@ function op_run_command() { echo -e "┘\n" if [[ -z "$DRY" ]]; then - eval "$CMD" + "$@" fi } @@ -298,7 +298,7 @@ function op_check() { function op_esim() { op_before_cmd - op_run_command system/hardware/esim.py "$@" + op_run_command common/esim/esim.py "$@" } function op_build() { @@ -310,33 +310,33 @@ function op_build() { op_run_command system/manager/build.py else # scons is fine on PC - op_run_command scons $@ + op_run_command scons "$@" fi } function op_juggle() { op_before_cmd - op_run_command tools/plotjuggler/juggle.py $@ + op_run_command tools/plotjuggler/juggle.py "$@" } function op_lint() { op_before_cmd - op_run_command scripts/lint/lint.sh $@ + op_run_command scripts/lint/lint.sh "$@" } function op_test() { op_before_cmd - op_run_command pytest $@ + op_run_command pytest "$@" } function op_replay() { op_before_cmd - op_run_command tools/replay/replay $@ + op_run_command tools/replay/replay "$@" } function op_cabana() { op_before_cmd - op_run_command tools/cabana/cabana $@ + op_run_command tools/cabana/cabana "$@" } function op_sim() { @@ -347,7 +347,7 @@ function op_sim() { function op_clip() { op_before_cmd - op_run_command tools/clip/run.py $@ + op_run_command tools/clip/run.py "$@" } function op_switch() { @@ -489,4 +489,4 @@ function _op() { esac } -_op $@ +_op "$@" diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index a643f0d3a7..52db658add 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -9,7 +9,7 @@ #include "libyuv.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #ifdef __APPLE__ #define HW_DEVICE_TYPE AV_HWDEVICE_TYPE_VIDEOTOOLBOX diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index d8d59e41a4..d6d02b1a9f 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -193,7 +193,7 @@ void Replay::startStream(const std::shared_ptr segment) { auto event = reader.getRoot(); uint64_t wall_time = event.getInitData().getWallTimeNanos(); if (wall_time > 0) { - route_date_time_ = wall_time / 1e6; + route_date_time_ = wall_time / 1e9; } } diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 3e2bc7c00e..ce6d75bd6d 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -100,7 +100,7 @@ private: std::atomic exit_ = false; std::atomic interrupt_requested_ = false; bool events_ready_ = false; - std::time_t route_date_time_; + std::time_t route_date_time_ = 0; uint64_t route_start_ts_ = 0; std::atomic cur_mono_time_ = 0; cereal::Event::Which cur_which_ = cereal::Event::Which::INIT_DATA; diff --git a/tools/replay/route.cc b/tools/replay/route.cc index df4fa813cf..1560f1dce8 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -5,7 +5,7 @@ #include #include "json11/json11.hpp" -#include "system/hardware/hw.h" +#include "common/hardware/hw.h" #include "tools/replay/py_downloader.h" #include "tools/replay/replay.h" #include "tools/replay/util.h" diff --git a/system/hardware/pc/__init__.py b/tools/scripts/__init__.py similarity index 100% rename from system/hardware/pc/__init__.py rename to tools/scripts/__init__.py diff --git a/selfdrive/debug/can_print_changes.py b/tools/scripts/car/can_print_changes.py similarity index 98% rename from selfdrive/debug/can_print_changes.py rename to tools/scripts/car/can_print_changes.py index 97d60b2b05..c2e152e1e0 100755 --- a/selfdrive/debug/can_print_changes.py +++ b/tools/scripts/car/can_print_changes.py @@ -5,7 +5,7 @@ import time from collections import defaultdict import cereal.messaging as messaging -from openpilot.selfdrive.debug.can_table import can_table +from openpilot.tools.scripts.can_table import can_table from openpilot.tools.lib.logreader import LogIterable, LogReader RED = '\033[91m' diff --git a/selfdrive/debug/can_printer.py b/tools/scripts/car/can_printer.py similarity index 100% rename from selfdrive/debug/can_printer.py rename to tools/scripts/car/can_printer.py diff --git a/selfdrive/debug/can_table.py b/tools/scripts/car/can_table.py similarity index 100% rename from selfdrive/debug/can_table.py rename to tools/scripts/car/can_table.py diff --git a/selfdrive/debug/car/clear_dtc.py b/tools/scripts/car/clear_dtc.py similarity index 100% rename from selfdrive/debug/car/clear_dtc.py rename to tools/scripts/car/clear_dtc.py diff --git a/selfdrive/debug/car/disable_ecu.py b/tools/scripts/car/disable_ecu.py similarity index 100% rename from selfdrive/debug/car/disable_ecu.py rename to tools/scripts/car/disable_ecu.py diff --git a/selfdrive/debug/car/ecu_addrs.py b/tools/scripts/car/ecu_addrs.py similarity index 100% rename from selfdrive/debug/car/ecu_addrs.py rename to tools/scripts/car/ecu_addrs.py diff --git a/selfdrive/debug/car/fw_versions.py b/tools/scripts/car/fw_versions.py similarity index 100% rename from selfdrive/debug/car/fw_versions.py rename to tools/scripts/car/fw_versions.py diff --git a/selfdrive/debug/car/hyundai_enable_radar_points.py b/tools/scripts/car/hyundai_enable_radar_points.py similarity index 100% rename from selfdrive/debug/car/hyundai_enable_radar_points.py rename to tools/scripts/car/hyundai_enable_radar_points.py diff --git a/selfdrive/debug/max_lat_accel.py b/tools/scripts/car/max_lat_accel.py similarity index 100% rename from selfdrive/debug/max_lat_accel.py rename to tools/scripts/car/max_lat_accel.py diff --git a/selfdrive/debug/measure_torque_time_to_max.py b/tools/scripts/car/measure_torque_time_to_max.py similarity index 100% rename from selfdrive/debug/measure_torque_time_to_max.py rename to tools/scripts/car/measure_torque_time_to_max.py diff --git a/selfdrive/debug/read_dtc_status.py b/tools/scripts/car/read_dtc_status.py similarity index 100% rename from selfdrive/debug/read_dtc_status.py rename to tools/scripts/car/read_dtc_status.py diff --git a/selfdrive/debug/car/toyota_eps_factor.py b/tools/scripts/car/toyota_eps_factor.py similarity index 100% rename from selfdrive/debug/car/toyota_eps_factor.py rename to tools/scripts/car/toyota_eps_factor.py diff --git a/selfdrive/debug/car/vin.py b/tools/scripts/car/vin.py similarity index 100% rename from selfdrive/debug/car/vin.py rename to tools/scripts/car/vin.py diff --git a/selfdrive/debug/car/vw_mqb_config.py b/tools/scripts/car/vw_mqb_config.py similarity index 100% rename from selfdrive/debug/car/vw_mqb_config.py rename to tools/scripts/car/vw_mqb_config.py diff --git a/selfdrive/debug/count_events.py b/tools/scripts/count_events.py similarity index 100% rename from selfdrive/debug/count_events.py rename to tools/scripts/count_events.py diff --git a/selfdrive/debug/cpu_usage_stat.py b/tools/scripts/cpu_usage_stat.py similarity index 98% rename from selfdrive/debug/cpu_usage_stat.py rename to tools/scripts/cpu_usage_stat.py index 089685103f..5b72eacc65 100755 --- a/selfdrive/debug/cpu_usage_stat.py +++ b/tools/scripts/cpu_usage_stat.py @@ -8,7 +8,7 @@ System tools like top/htop can only show current cpu usage values, so I write th Calculate minumium/maximum/accumulated_average cpu usage as long term inspections. Monitor multiple processes simuteneously. Sample usage: - root@localhost:/data/openpilot$ python selfdrive/debug/cpu_usage_stat.py pandad,ubloxd + root@localhost:/data/openpilot$ python tools/scripts/cpu_usage_stat.py pandad,ubloxd ('Add monitored proc:', './pandad') ('Add monitored proc:', 'python locationd/ubloxd.py') pandad: 1.96%, min: 1.96%, max: 1.96%, acc: 1.96% diff --git a/selfdrive/debug/cycle_alerts.py b/tools/scripts/cycle_alerts.py similarity index 100% rename from selfdrive/debug/cycle_alerts.py rename to tools/scripts/cycle_alerts.py diff --git a/selfdrive/debug/debug_fw_fingerprinting_offline.py b/tools/scripts/debug_fw_fingerprinting_offline.py similarity index 100% rename from selfdrive/debug/debug_fw_fingerprinting_offline.py rename to tools/scripts/debug_fw_fingerprinting_offline.py diff --git a/selfdrive/debug/dump.py b/tools/scripts/dump.py similarity index 100% rename from selfdrive/debug/dump.py rename to tools/scripts/dump.py diff --git a/tools/scripts/fetch_image_from_route.py b/tools/scripts/fetch_image_from_route.py deleted file mode 100755 index 77bf48f946..0000000000 --- a/tools/scripts/fetch_image_from_route.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -import sys - -if len(sys.argv) < 4: - print(f"{sys.argv[0]} [front|wide|driver]") - print('example: ./fetch_image_from_route.py "02c45f73a2e5c6e9|2020-06-01--18-03-08" 3 500 driver') - exit(0) - -cameras = { - "front": "cameras", - "wide": "ecameras", - "driver": "dcameras" -} - -import requests -from PIL import Image -from openpilot.tools.lib.auth_config import get_token -from openpilot.tools.lib.framereader import FrameReader - -jwt = get_token() - -route = sys.argv[1] -segment = int(sys.argv[2]) -frame = int(sys.argv[3]) -camera = cameras[sys.argv[4]] if len(sys.argv) > 4 and sys.argv[4] in cameras else "cameras" - -url = f'https://api.commadotai.com/v1/route/{route}/files' -r = requests.get(url, headers={"Authorization": f"JWT {jwt}"}, timeout=10) -assert r.status_code == 200 -print("got api response") - -segments = r.json()[camera] -if segment >= len(segments): - raise Exception(f"segment {segment} not found, got {len(segments)} segments") - -fr = FrameReader(segments[segment]) -if frame >= fr.frame_count: - raise Exception("frame {frame} not found, got {fr.frame_count} frames") - -im = Image.fromarray(fr.get(frame)) -fn = f"uxxx_{route.replace('|', '_')}_{segment}_{frame}.png" -im.save(fn) -print(f"saved {fn}") diff --git a/selfdrive/debug/filter_log_message.py b/tools/scripts/filter_log_message.py similarity index 100% rename from selfdrive/debug/filter_log_message.py rename to tools/scripts/filter_log_message.py diff --git a/selfdrive/debug/fingerprint_from_route.py b/tools/scripts/fingerprint_from_route.py similarity index 100% rename from selfdrive/debug/fingerprint_from_route.py rename to tools/scripts/fingerprint_from_route.py diff --git a/selfdrive/debug/fuzz_fw_fingerprint.py b/tools/scripts/fuzz_fw_fingerprint.py similarity index 100% rename from selfdrive/debug/fuzz_fw_fingerprint.py rename to tools/scripts/fuzz_fw_fingerprint.py diff --git a/selfdrive/debug/get_fingerprint.py b/tools/scripts/get_fingerprint.py similarity index 100% rename from selfdrive/debug/get_fingerprint.py rename to tools/scripts/get_fingerprint.py diff --git a/selfdrive/debug/live_cpu_and_temp.py b/tools/scripts/live_cpu_and_temp.py similarity index 100% rename from selfdrive/debug/live_cpu_and_temp.py rename to tools/scripts/live_cpu_and_temp.py diff --git a/selfdrive/debug/mem_usage.py b/tools/scripts/mem_usage.py similarity index 100% rename from selfdrive/debug/mem_usage.py rename to tools/scripts/mem_usage.py diff --git a/selfdrive/debug/print_flags.py b/tools/scripts/print_flags.py similarity index 100% rename from selfdrive/debug/print_flags.py rename to tools/scripts/print_flags.py diff --git a/tools/profiling/clpeak/.gitignore b/tools/scripts/profiling/clpeak/.gitignore similarity index 100% rename from tools/profiling/clpeak/.gitignore rename to tools/scripts/profiling/clpeak/.gitignore diff --git a/tools/profiling/clpeak/build.sh b/tools/scripts/profiling/clpeak/build.sh similarity index 100% rename from tools/profiling/clpeak/build.sh rename to tools/scripts/profiling/clpeak/build.sh diff --git a/tools/profiling/clpeak/no_print.patch b/tools/scripts/profiling/clpeak/no_print.patch similarity index 100% rename from tools/profiling/clpeak/no_print.patch rename to tools/scripts/profiling/clpeak/no_print.patch diff --git a/tools/profiling/clpeak/run_continuously.patch b/tools/scripts/profiling/clpeak/run_continuously.patch similarity index 100% rename from tools/profiling/clpeak/run_continuously.patch rename to tools/scripts/profiling/clpeak/run_continuously.patch diff --git a/tools/profiling/ftrace.sh b/tools/scripts/profiling/ftrace.sh similarity index 100% rename from tools/profiling/ftrace.sh rename to tools/scripts/profiling/ftrace.sh diff --git a/tools/profiling/palanteer/.gitignore b/tools/scripts/profiling/palanteer/.gitignore similarity index 100% rename from tools/profiling/palanteer/.gitignore rename to tools/scripts/profiling/palanteer/.gitignore diff --git a/tools/profiling/palanteer/setup.sh b/tools/scripts/profiling/palanteer/setup.sh similarity index 100% rename from tools/profiling/palanteer/setup.sh rename to tools/scripts/profiling/palanteer/setup.sh diff --git a/tools/profiling/perfetto/.gitignore b/tools/scripts/profiling/perfetto/.gitignore similarity index 100% rename from tools/profiling/perfetto/.gitignore rename to tools/scripts/profiling/perfetto/.gitignore diff --git a/tools/profiling/perfetto/build.sh b/tools/scripts/profiling/perfetto/build.sh similarity index 100% rename from tools/profiling/perfetto/build.sh rename to tools/scripts/profiling/perfetto/build.sh diff --git a/tools/profiling/perfetto/copy.sh b/tools/scripts/profiling/perfetto/copy.sh similarity index 100% rename from tools/profiling/perfetto/copy.sh rename to tools/scripts/profiling/perfetto/copy.sh diff --git a/tools/profiling/perfetto/record.sh b/tools/scripts/profiling/perfetto/record.sh similarity index 100% rename from tools/profiling/perfetto/record.sh rename to tools/scripts/profiling/perfetto/record.sh diff --git a/tools/profiling/perfetto/server.sh b/tools/scripts/profiling/perfetto/server.sh similarity index 100% rename from tools/profiling/perfetto/server.sh rename to tools/scripts/profiling/perfetto/server.sh diff --git a/tools/profiling/perfetto/traces.sh b/tools/scripts/profiling/perfetto/traces.sh similarity index 100% rename from tools/profiling/perfetto/traces.sh rename to tools/scripts/profiling/perfetto/traces.sh diff --git a/tools/profiling/py-spy/profile.sh b/tools/scripts/profiling/py-spy/profile.sh similarity index 100% rename from tools/profiling/py-spy/profile.sh rename to tools/scripts/profiling/py-spy/profile.sh diff --git a/tools/profiling/snapdragon/.gitignore b/tools/scripts/profiling/snapdragon/.gitignore similarity index 100% rename from tools/profiling/snapdragon/.gitignore rename to tools/scripts/profiling/snapdragon/.gitignore diff --git a/tools/profiling/snapdragon/README.md b/tools/scripts/profiling/snapdragon/README.md similarity index 100% rename from tools/profiling/snapdragon/README.md rename to tools/scripts/profiling/snapdragon/README.md diff --git a/tools/profiling/snapdragon/setup-agnos.sh b/tools/scripts/profiling/snapdragon/setup-agnos.sh similarity index 100% rename from tools/profiling/snapdragon/setup-agnos.sh rename to tools/scripts/profiling/snapdragon/setup-agnos.sh diff --git a/tools/profiling/snapdragon/setup-profiler.sh b/tools/scripts/profiling/snapdragon/setup-profiler.sh similarity index 100% rename from tools/profiling/snapdragon/setup-profiler.sh rename to tools/scripts/profiling/snapdragon/setup-profiler.sh diff --git a/tools/profiling/watch-irqs.sh b/tools/scripts/profiling/watch-irqs.sh similarity index 100% rename from tools/profiling/watch-irqs.sh rename to tools/scripts/profiling/watch-irqs.sh diff --git a/selfdrive/debug/qlog_size.py b/tools/scripts/qlog_size.py similarity index 100% rename from selfdrive/debug/qlog_size.py rename to tools/scripts/qlog_size.py diff --git a/selfdrive/debug/run_process_on_route.py b/tools/scripts/run_process_on_route.py similarity index 100% rename from selfdrive/debug/run_process_on_route.py rename to tools/scripts/run_process_on_route.py diff --git a/tools/scripts/save_ubloxraw_stream.py b/tools/scripts/save_ubloxraw_stream.py deleted file mode 100755 index b5354a7831..0000000000 --- a/tools/scripts/save_ubloxraw_stream.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -from openpilot.common.basedir import BASEDIR -from openpilot.tools.lib.logreader import LogReader - -os.environ['BASEDIR'] = BASEDIR - - -def get_arg_parser(): - parser = argparse.ArgumentParser( - description="Unlogging and save to file", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - parser.add_argument("route", type=(lambda x: x.replace("#", "|")), nargs="?", - help="The route whose messages will be published.") - parser.add_argument("--out_path", nargs='?', default='/data/ubloxRaw.stream', - help="Output pickle file path") - return parser - - -def main(): - args = get_arg_parser().parse_args(sys.argv[1:]) - - lr = LogReader(args.route) - - with open(args.out_path, 'wb') as f: - try: - done = False - i = 0 - while not done: - msg = next(lr) - if not msg: - break - smsg = msg.as_builder() - typ = smsg.which() - if typ == 'ubloxRaw': - f.write(smsg.to_bytes()) - i += 1 - except StopIteration: - print('All done') - print(f'Writed {i} msgs') - - -if __name__ == "__main__": - main() diff --git a/selfdrive/debug/set_car_params.py b/tools/scripts/set_car_params.py similarity index 100% rename from selfdrive/debug/set_car_params.py rename to tools/scripts/set_car_params.py diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py index e454afb691..33eac4081d 100755 --- a/tools/scripts/ssh.py +++ b/tools/scripts/ssh.py @@ -14,7 +14,7 @@ if __name__ == "__main__": parser.add_argument("device", help="device name or dongle id") parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai") parser.add_argument("--port", help="ssh jump server port", default=22, type=int) - parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "system/hardware/tici/id_rsa")) + parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "common/hardware/tici/id_rsa")) parser.add_argument("--debug", help="enable debug output", action="store_true") args = parser.parse_args() diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/tools/scripts/test_fw_query_on_routes.py similarity index 100% rename from selfdrive/debug/test_fw_query_on_routes.py rename to tools/scripts/test_fw_query_on_routes.py diff --git a/selfdrive/debug/uiview.py b/tools/scripts/uiview.py similarity index 97% rename from selfdrive/debug/uiview.py rename to tools/scripts/uiview.py index 2cd541363c..d2244cf4b6 100755 --- a/selfdrive/debug/uiview.py +++ b/tools/scripts/uiview.py @@ -4,7 +4,7 @@ import time from cereal import car, log, messaging from openpilot.common.params import Params from openpilot.system.manager.process_config import managed_processes, is_tinygrad_model, is_stock_model -from openpilot.system.hardware import HARDWARE +from openpilot.common.hardware import HARDWARE if __name__ == "__main__": CP = car.CarParams(notCar=True, wheelbase=1, steerRatio=10) diff --git a/tools/scripts/watch_timings.py b/tools/scripts/watch_timings.py new file mode 100755 index 0000000000..874720dd22 --- /dev/null +++ b/tools/scripts/watch_timings.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import argparse +import datetime +import time +from collections import deque +from dataclasses import dataclass, field + +import numpy as np + +import cereal.messaging as messaging +from cereal.services import SERVICE_LIST + + +@dataclass +class ServiceTiming: + times: list[float] = field(default_factory=list) + window: deque[float] = field(default_factory=lambda: deque(maxlen=100)) + valids: deque[bool] = field(default_factory=lambda: deque(maxlen=100)) + lag_events: list[tuple[float, float]] = field(default_factory=list) + + def add(self, mono_time: float, valid: bool, expected_interval: float | None, lag_threshold: float) -> None: + if self.times: + dt = mono_time - self.times[-1] + self.window.append(dt) + if expected_interval is not None and dt > lag_threshold * expected_interval: + self.lag_events.append((mono_time, dt)) + + self.times.append(mono_time) + self.valids.append(valid) + + def intervals(self, latest_only: bool) -> np.ndarray: + if latest_only: + return np.array(self.window) + return np.diff(self.times) + + +def format_row(name: str, timing: ServiceTiming, latest_only: bool) -> str: + dts = timing.intervals(latest_only) + if len(dts) == 0: + return f"{name:25} waiting for messages" + + mean = np.mean(dts) + hz = 1.0 / mean if mean > 0 else 0.0 + valid = all(timing.valids) if timing.valids else False + return f"{name:25} {hz:8.2f}Hz {mean * 1e3:8.2f}ms {np.std(dts) * 1e3:8.2f}ms {np.max(dts) * 1e3:8.2f}ms {np.min(dts) * 1e3:8.2f}ms valid={valid}" + + +def print_lag_events(name: str, timing: ServiceTiming, printed_lags: dict[str, int]) -> None: + start = printed_lags.get(name, 0) + for mono_time, dt in timing.lag_events[start:]: + print(f"{mono_time:.3f} {name} lag {dt:.3f}s", flush=True) + printed_lags[name] = len(timing.lag_events) + + +def monitor_services(socket_names: list[str], print_interval: float, lag_threshold: float, lag_only: bool) -> None: + sockets = {name: messaging.sub_sock(name, conflate=False) for name in socket_names} + timings = {name: ServiceTiming() for name in socket_names} + printed_lags: dict[str, int] = {} + + start_time = time.monotonic() + last_print = start_time + + try: + while True: + for name, sock in sockets.items(): + for msg in messaging.drain_sock(sock): + expected_interval = 1.0 / SERVICE_LIST[name].frequency if name in SERVICE_LIST else None + timings[name].add(msg.logMonoTime / 1e9, msg.valid, expected_interval, lag_threshold) + + now = time.monotonic() + if now - last_print < print_interval: + time.sleep(0.01) + continue + + if not lag_only: + print(flush=True) + print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True) + for name in socket_names: + print(format_row(name, timings[name], latest_only=True), flush=True) + + for name in socket_names: + print_lag_events(name, timings[name], printed_lags) + + last_print = now + except KeyboardInterrupt: + print("\n", flush=True) + print("=" * 5, "timing summary", "=" * 5, flush=True) + print(f"{'service':25} {'freq':>10} {'mean':>10} {'std':>10} {'max':>10} {'min':>10} valid", flush=True) + for name in socket_names: + print(format_row(name, timings[name], latest_only=False), flush=True) + print("=" * 5, datetime.timedelta(seconds=time.monotonic() - start_time), "=" * 5, flush=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check live service timing, frequency, validity, and lag") + parser.add_argument("socket", nargs="*", default=["carState"], help="service/socket name") + parser.add_argument("--lag-threshold", type=float, default=10.0, help="report intervals above this multiple of the expected service interval") + parser.add_argument("--lag-only", action="store_true", help="only print lag events") + parser.add_argument("--print-interval", type=float, default=1.0, help="seconds between table updates") + args = parser.parse_args() + + monitor_services(args.socket, args.print_interval, args.lag_threshold, args.lag_only)