diff --git a/launch_env.sh b/launch_env.sh index b474bc2a7..8799bd88e 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -21,7 +21,7 @@ fi export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="12.8.28" + export AGNOS_VERSION="19.6.1" fi if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then diff --git a/panda/board/obj/version b/panda/board/obj/version index 2b0ed1aeb..dd978cab4 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-230ed14e-DEBUG \ No newline at end of file +DEV-230ed14e-DEBUG diff --git a/release/pack.py b/release/pack.py index b20a3ea26..9e303a98e 100755 --- a/release/pack.py +++ b/release/pack.py @@ -21,6 +21,10 @@ def copy(src, dest): shutil.copy2(src, dest, follow_symlinks=True) +def ignore_broken_symlinks(src, names): + return [name for name in names if Path(src, name).is_symlink() and not Path(src, name).exists()] + + if __name__ == '__main__': parser = ArgumentParser(prog='pack.py', description="package script into a portable executable", epilog='comma.ai') parser.add_argument('-e', '--entrypoint', help="function to call in module, default is 'main'", default='main') @@ -43,7 +47,7 @@ if __name__ == '__main__': with tempfile.TemporaryDirectory() as tmp: for directory in DIRS: - shutil.copytree(BASEDIR + '/' + directory, tmp + '/' + directory, symlinks=False, ignore_dangling_symlinks=True, + shutil.copytree(BASEDIR + '/' + directory, tmp + '/' + directory, symlinks=False, ignore=ignore_broken_symlinks, dirs_exist_ok=True, copy_function=copy) entry = f'{args.module}:{args.entrypoint}' zipapp.create_archive(tmp, target=args.output, interpreter=INTERPRETER, main=entry) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 3b4d4be82..f82ef6b3c 100644 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -384,8 +384,8 @@ class Controls: self.ecu_disable_failed = self.params.get_bool("EcuDisableFailed") self.ecu_disable_failed_checked = True if self.ecu_disable_failed and self.CP.carFingerprint == NISSAN_CAR.NISSAN_LEAF: - self.CP.openpilotLongitudinalControl = False - self.CP.pcmCruise = True + self.CP = messaging.log_from_bytes(self.params.get("CarParams"), car.CarParams) + self.FPCP = messaging.log_from_bytes(self.params.get("StarPilotCarParams"), custom.StarPilotCarParams) def state_control(self): CS = self.sm['carState'] diff --git a/selfdrive/controls/tests/test_nissan_leaf_fallback.py b/selfdrive/controls/tests/test_nissan_leaf_fallback.py new file mode 100644 index 000000000..e27f16f9f --- /dev/null +++ b/selfdrive/controls/tests/test_nissan_leaf_fallback.py @@ -0,0 +1,56 @@ +import cereal.messaging as messaging + +from cereal import car, custom +from opendbc.car.nissan.values import CAR as NISSAN_CAR + +from openpilot.selfdrive.car.cruise_state import should_cancel_stock_cruise +from openpilot.selfdrive.controls.controlsd import Controls + + +class FakeFallbackParams: + def __init__(self, fallback_cp, fallback_fpcp): + self.values = { + "CarParams": fallback_cp.to_bytes(), + "StarPilotCarParams": fallback_fpcp.to_bytes(), + } + + def get_bool(self, key): + return key in ("ControlsReady", "EcuDisableFailed") + + def get(self, key): + return self.values[key] + + +def test_leaf_ecu_disable_fallback_reloads_read_only_car_params(): + initial_cp = car.CarParams.new_message() + initial_cp.carFingerprint = NISSAN_CAR.NISSAN_LEAF + initial_cp.openpilotLongitudinalControl = True + initial_cp.pcmCruise = False + initial_fpcp = custom.StarPilotCarParams.new_message() + initial_fpcp.safetyConfigs = [custom.StarPilotCarParams.SafetyConfig.new_message(safetyParam=2)] + + fallback_cp = car.CarParams.new_message() + fallback_cp.carFingerprint = NISSAN_CAR.NISSAN_LEAF + fallback_cp.openpilotLongitudinalControl = False + fallback_cp.pcmCruise = True + fallback_fpcp = custom.StarPilotCarParams.new_message() + fallback_fpcp.safetyConfigs = [custom.StarPilotCarParams.SafetyConfig.new_message(safetyParam=0)] + + initial_cp_reader = messaging.log_from_bytes(initial_cp.to_bytes(), car.CarParams) + controls = Controls.__new__(Controls) + controls.CP = initial_cp_reader + controls.FPCP = messaging.log_from_bytes(initial_fpcp.to_bytes(), custom.StarPilotCarParams) + controls.params = FakeFallbackParams(fallback_cp, fallback_fpcp) + controls.ecu_disable_failed = False + controls.ecu_disable_failed_checked = False + + controls.update_ecu_disable_failed() + + assert controls.ecu_disable_failed + assert controls.ecu_disable_failed_checked + assert not controls.CP.openpilotLongitudinalControl + assert controls.CP.pcmCruise + assert controls.FPCP.safetyConfigs[0].safetyParam == 0 + assert initial_cp_reader.openpilotLongitudinalControl + assert not initial_cp_reader.pcmCruise + assert not should_cancel_stock_cruise(controls.CP, cruise_enabled=True, controls_enabled=True) diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index c8b48609b..12474c1d8 100644 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -297,11 +297,9 @@ class SelfdriveD: self.ecu_disable_failed = self.params.get_bool("EcuDisableFailed") self.ecu_disable_failed_checked = True if self.ecu_disable_failed: - fallback_cp = messaging.log_from_bytes(self.params.get("CarParams"), car.CarParams) - fallback_fpcp = messaging.log_from_bytes(self.params.get("StarPilotCarParams"), custom.StarPilotCarParams) - self.CP.openpilotLongitudinalControl = fallback_cp.openpilotLongitudinalControl - self.CP.pcmCruise = fallback_cp.pcmCruise - self.FPCP = fallback_fpcp + self.CP = messaging.log_from_bytes(self.params.get("CarParams"), car.CarParams) + self.FPCP = messaging.log_from_bytes(self.params.get("StarPilotCarParams"), custom.StarPilotCarParams) + self.car_events = CarSpecificEvents(self.CP) def clear_longitudinal_excessive_actuation_alert(self): alert = self.params.get("Offroad_ExcessiveActuation") diff --git a/selfdrive/selfdrived/tests/test_selfdrived.py b/selfdrive/selfdrived/tests/test_selfdrived.py index 965bd5553..09fdb2eb8 100644 --- a/selfdrive/selfdrived/tests/test_selfdrived.py +++ b/selfdrive/selfdrived/tests/test_selfdrived.py @@ -1,4 +1,6 @@ -from cereal import car, custom +import cereal.messaging as messaging + +from cereal import car, custom, log from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR from opendbc.car.nissan.values import CAR as NISSAN_CAR @@ -63,8 +65,9 @@ def test_ecu_disable_fallback_synchronizes_behavior_and_safety_params(): fallback_fpcp.safetyConfigs = [custom.StarPilotCarParams.SafetyConfig.new_message(safetyParam=0)] selfdrived = SelfdriveD.__new__(SelfdriveD) - selfdrived.CP = initial_cp - selfdrived.FPCP = initial_fpcp + initial_cp_reader = messaging.log_from_bytes(initial_cp.to_bytes(), car.CarParams) + selfdrived.CP = initial_cp_reader + selfdrived.FPCP = messaging.log_from_bytes(initial_fpcp.to_bytes(), custom.StarPilotCarParams) selfdrived.params = FakeFallbackParams(True, True, fallback_cp, fallback_fpcp) selfdrived.ecu_disable_failed = False selfdrived.ecu_disable_failed_checked = False @@ -76,6 +79,16 @@ def test_ecu_disable_fallback_synchronizes_behavior_and_safety_params(): assert not selfdrived.CP.openpilotLongitudinalControl assert selfdrived.CP.pcmCruise assert selfdrived.FPCP.safetyConfigs[0].safetyParam == 0 + assert initial_cp_reader.openpilotLongitudinalControl + assert not initial_cp_reader.pcmCruise + + CS = car.CarState.new_message() + CS.gearShifter = car.CarState.GearShifter.drive + CS.cruiseState.available = True + CS.cruiseState.enabled = True + CS_prev = car.CarState.new_message() + events = selfdrived.car_events.update(CS, CS_prev, car.CarControl.new_message()) + assert log.OnroadEvent.EventName.pcmEnable in events.names def test_ecu_disable_fallback_does_not_change_other_cars(): @@ -93,8 +106,10 @@ def test_ecu_disable_fallback_does_not_change_other_cars(): fallback_fpcp.safetyConfigs = [custom.StarPilotCarParams.SafetyConfig.new_message(safetyParam=0)] selfdrived = SelfdriveD.__new__(SelfdriveD) - selfdrived.CP = initial_cp - selfdrived.FPCP = initial_fpcp + initial_cp_reader = messaging.log_from_bytes(initial_cp.to_bytes(), car.CarParams) + initial_fpcp_reader = messaging.log_from_bytes(initial_fpcp.to_bytes(), custom.StarPilotCarParams) + selfdrived.CP = initial_cp_reader + selfdrived.FPCP = initial_fpcp_reader selfdrived.params = FakeFallbackParams(True, True, fallback_cp, fallback_fpcp) selfdrived.ecu_disable_failed = False selfdrived.ecu_disable_failed_checked = False @@ -105,3 +120,5 @@ def test_ecu_disable_fallback_does_not_change_other_cars(): assert selfdrived.CP.openpilotLongitudinalControl assert not selfdrived.CP.pcmCruise assert selfdrived.FPCP.safetyConfigs[0].safetyParam == 4 + assert selfdrived.CP is initial_cp_reader + assert selfdrived.FPCP is initial_fpcp_reader diff --git a/system/camerad/camerad b/system/camerad/camerad index b9639caef..442d0b999 100755 Binary files a/system/camerad/camerad and b/system/camerad/camerad differ diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index c2b113393..1f4e34ee9 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -24,6 +24,8 @@ #include "common/clutil.h" #include "common/params.h" #include "common/swaglog.h" +#include "common/timing.h" +#include "common/watchdog.h" ExitHandler do_exit; @@ -285,6 +287,7 @@ void camerad_thread() { // poll events LOG("-- Dequeueing Video events"); while (!do_exit) { + watchdog_kick(nanos_since_boot()); struct pollfd fds[1] = {{.fd = m.video0_fd, .events = POLLPRI}}; int ret = poll(fds, std::size(fds), 1000); if (ret < 0) { diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 3b5888b73..babb1a085 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -276,7 +276,9 @@ int SpectraCamera::clear_req_queue() { .flush_type = CAM_FLUSH_TYPE_ALL, }; int err = do_cam_control(m->icp_fd, CAM_FLUSH_REQ, &cmd, sizeof(cmd)); - assert(err == 0); + if (err != 0) { + LOGE("failed to flush BPS requests: %d", err); + } LOGD("flushed bps: %d", err); } @@ -1012,7 +1014,7 @@ void SpectraCamera::enqueue_frame(uint64_t request_id) { } if (icp_dev_handle > 0) { - ret = do_cam_control(m->cam_sync_fd, CAM_SYNC_CREATE, &sync_create, sizeof(sync_create)); + ret = do_sync_control(m->cam_sync_fd, CAM_SYNC_CREATE, &sync_create, sizeof(sync_create)); if (ret != 0) { LOGE("failed to create fence: %d %d", ret, sync_create.sync_obj); } else { diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 3c46f277d..759eb169d 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -56,23 +56,24 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", - "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "size": 18515968, + "url": "https://www.dropbox.com/scl/fi/9l9io42qfx2shr9er5jqx/boot9.img.xz?rlkey=lbtxz862kxbvn3jn98ejitj8p&st=vr35tgz7&dl=1", + "hash": "ab2eba0f96b2f48efa376330c3eb509158361adf3ad9c20f269ec92457aa841f", + "hash_raw": "ab2eba0f96b2f48efa376330c3eb509158361adf3ad9c20f269ec92457aa841f", + "size": 48343040, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" + "ondevice_hash": "c12865a879012d6ad3da538c2ae8aff7d58c308c303fc5d8af309cb1707bc036" }, { "name": "system", - "url": "https://www.dropbox.com/scl/fi/8fd3w7hbhsyq146kqqdab/system8.img.xz?rlkey=5zt15mtuehdlj8ncj6pwn7zjy&st=j6tr4d47&dl=1", - "hash": "4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793", - "hash_raw": "4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793", - "size": 5368709120, + "url": "https://www.dropbox.com/scl/fi/3gtw78kxn6xs6dil10fmc/system9.img.xz?rlkey=uwj6tss7lw3j1di09xvtdugcy&st=ghnm9teb&dl=1", + "hash": "b6f1e5ade7baa0078990e881b9f392b6a29164347df8f54be20c81e9c9dbabfe", + "hash_raw": "b6f1e5ade7baa0078990e881b9f392b6a29164347df8f54be20c81e9c9dbabfe", + "size": 4718592000, "sparse": false, "full_check": false, - "has_ab": true + "has_ab": true, + "ondevice_hash": "b6f1e5ade7baa0078990e881b9f392b6a29164347df8f54be20c81e9c9dbabfe" } ] diff --git a/system/manager/process.py b/system/manager/process.py index 516d34159..434c917b0 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -522,6 +522,14 @@ class ManagerProcess(ABC): if dt > self.watchdog_max_dt and ENABLE_WATCHDOG: self.capture_watchdog_debug_dump_async(f"watchdog_timeout started={started}", dt) cloudlog.error(f"Watchdog timeout for {self.name} (exitcode {self.proc.exitcode}) restarting ({started=})") + if isinstance(self, NativeProcess): + sentry.capture_message( + f"Native process watchdog timeout: {self.name}", + level="fatal", + tags={"process": self.name, "process_kind": "native", "failure": "watchdog"}, + extras={"pid": self.proc.pid, "watchdog_dt": dt, "started": started}, + flush_timeout=0.5, + ) self.restart() def stop(self, retry: bool = True, block: bool = True, sig: signal.Signals = None) -> int | None: @@ -715,7 +723,16 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None for p in procs: # Reap crashed processes so they can be cleanly restarted below. if p.proc is not None and p.proc.exitcode is not None and not p.shutting_down: - cloudlog.error(f"Process {p.name} crashed with exitcode {p.proc.exitcode}, restarting") + exitcode = p.proc.exitcode + cloudlog.error(f"Process {p.name} crashed with exitcode {exitcode}, restarting") + if isinstance(p, NativeProcess): + sentry.capture_message( + f"Native process crashed: {p.name}", + level="fatal", + tags={"process": p.name, "process_kind": "native"}, + extras={"exitcode": exitcode, "started": started}, + flush_timeout=0.5, + ) p.stop(retry=False) if p.enabled and p.name not in not_run and p.should_run(started, params, CP, starpilot_toggles): diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2ae8bf309..ce878a925 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -12,6 +12,7 @@ from openpilot.system.manager.process import PythonProcess, NativeProcess, Daemo WEBCAM = os.getenv("USE_WEBCAM") is not None UI_WATCHDOG_MAX_DT = int(os.getenv("UI_WATCHDOG_MAX_DT", "10")) +CAMERAD_WATCHDOG_MAX_DT = int(os.getenv("CAMERAD_WATCHDOG_MAX_DT", "5")) def driverview(started: bool, params: Params, CP: car.CarParams, starpilot_toggles: SimpleNamespace) -> bool: return started or params.get_bool("IsDriverViewEnabled") @@ -123,7 +124,8 @@ procs = [ NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)), PythonProcess("logmessaged", "system.logmessaged", always_run), - NativeProcess("camerad", "system/camerad", ["./camerad"], or_(camera_run, livestream), enabled=not WEBCAM), + NativeProcess("camerad", "system/camerad", ["./camerad"], or_(camera_run, livestream), enabled=not WEBCAM, + watchdog_max_dt=CAMERAD_WATCHDOG_MAX_DT), PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), PythonProcess("proclogd", "system.proclogd", and_(allow_logging, only_onroad), enabled=platform.system() != "Darwin"), PythonProcess("journald", "system.journald", and_(allow_logging, only_onroad), platform.system() != "Darwin"), diff --git a/tools/agnos/flash_local_agnos_pair_to_comma.sh b/tools/agnos/flash_local_agnos_pair_to_comma.sh new file mode 100755 index 000000000..a615928bd --- /dev/null +++ b/tools/agnos/flash_local_agnos_pair_to_comma.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +set -euo pipefail + +HOST="${1:-comma@192.168.3.110}" +MANIFEST="${2:?usage: $0 [host] manifest.json boot.img.xz system.img.xz}" +BOOT_IMAGE="${3:?missing boot image}" +SYSTEM_IMAGE="${4:?missing system image}" +EXPECTED_VERSION="${EXPECTED_VERSION:-19.6.1}" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" + +SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null) +if [[ -n "${SSH_KEY:-}" ]]; then + SSH_OPTS=(-i "$SSH_KEY" "${SSH_OPTS[@]}") +fi + +for path in "$MANIFEST" "$BOOT_IMAGE" "$SYSTEM_IMAGE"; do + if [[ ! -f "$path" ]]; then + echo "missing file: $path" >&2 + exit 1 + fi +done + +SESSION="local_agnos_pair_flash" +REMOTE_DIR="/data/local_agnos_pair_flash" +REMOTE_MANIFEST="${REMOTE_DIR}/agnos-local.json" +REMOTE_RUNNER="${REMOTE_DIR}/run_flash.sh" +REMOTE_AGNOS="${REMOTE_DIR}/agnos.py" +PORT="8989" + +LOCAL_DIR="$(mktemp -d "${TMPDIR:-/tmp}/agnos-pair.XXXXXX")" +trap 'rm -rf "$LOCAL_DIR"' EXIT +LOCAL_MANIFEST="${LOCAL_DIR}/agnos-local.json" +LOCAL_AGNOS="${LOCAL_DIR}/agnos.py" + +python3 - "$MANIFEST" "$LOCAL_MANIFEST" "$PORT" "$BOOT_IMAGE" "$SYSTEM_IMAGE" <<'PY' +import json +import sys +from pathlib import Path + +source, destination, port, boot_image, system_image = sys.argv[1:] +images = {"boot": Path(boot_image), "system": Path(system_image)} +manifest = json.loads(Path(source).read_text(encoding="utf-8")) + +if len(manifest) != len(images) or {entry.get("name") for entry in manifest} != set(images): + raise SystemExit("local test manifest must contain exactly boot and system entries") + +for entry in manifest: + name = entry["name"] + if not entry.get("has_ab"): + raise SystemExit(f"{name} must be an A/B partition") + if entry.get("sparse"): + raise SystemExit(f"{name} must use a raw, non-sparse payload for local flashing") + entry["url"] = f"http://127.0.0.1:{port}/{images[name].name}" + entry.pop("alt", None) + entry.pop("casync_caibx", None) + entry.pop("casync_store", None) + +Path(destination).write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") +PY + +python3 - "$REPO_ROOT/system/hardware/tici/agnos.py" "$LOCAL_AGNOS" <<'PY' +import sys +from pathlib import Path + +src, dst = map(Path, sys.argv[1:]) +data = src.read_text(encoding="utf-8") +needle = "import openpilot.system.updated.casync.casync as casync" +if data.count(needle) != 1: + raise SystemExit("could not isolate the casync dependency in agnos.py") +data = data.replace( + needle, + '''class _UnusedCasync: + ChunkReader = object + ChunkDict = object + + def __getattr__(self, name): + raise RuntimeError("casync support is unavailable in local AGNOS flash runner") +casync = _UnusedCasync()''', +) +dst.write_text(data, encoding="utf-8") +PY + +ssh "${SSH_OPTS[@]}" "$HOST" "mkdir -p '$REMOTE_DIR'" +scp "${SSH_OPTS[@]}" "$LOCAL_MANIFEST" "$HOST:$REMOTE_MANIFEST" +scp "${SSH_OPTS[@]}" "$LOCAL_AGNOS" "$HOST:$REMOTE_AGNOS" +scp "${SSH_OPTS[@]}" "$BOOT_IMAGE" "$SYSTEM_IMAGE" "$HOST:$REMOTE_DIR/" + +ssh "${SSH_OPTS[@]}" "$HOST" "cat > '$REMOTE_RUNNER' && chmod +x '$REMOTE_RUNNER'" <<'REMOTE_RUNNER' +#!/usr/bin/env bash +set -euo pipefail + +: "${REMOTE_DIR:?}" +: "${REMOTE_MANIFEST:?}" +: "${REMOTE_AGNOS:?}" +: "${PORT:?}" +: "${EXPECTED_VERSION:?}" + +exec > >(tee -a "${REMOTE_DIR}/flash.log") 2>&1 + +echo "[STEP] Local AGNOS boot+system flash" +echo "[CHECK] Device: $(tr -d '\0' /dev/null || echo unknown)" +echo "[CHECK] Target AGNOS: ${EXPECTED_VERSION}" +echo "[CHECK] Active slot before flash: $(abctl --boot_slot)" +df -h /data + +if [[ -x /usr/local/venv/bin/python3 ]]; then + PYTHON_BIN="/usr/local/venv/bin/python3" +else + PYTHON_BIN="python3" +fi + +pkill -f "http.server ${PORT}.*${REMOTE_DIR}" >/dev/null 2>&1 || true +"$PYTHON_BIN" -m http.server "$PORT" --bind 127.0.0.1 --directory "$REMOTE_DIR" >"${REMOTE_DIR}/http.log" 2>&1 & +http_pid="$!" +trap 'kill "$http_pid" >/dev/null 2>&1 || true' EXIT + +http_ready=0 +for _ in $(seq 1 20); do + if "$PYTHON_BIN" - "$REMOTE_MANIFEST" <<'PY' +import json +import sys +import urllib.request +from pathlib import Path + +for entry in json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")): + with urllib.request.urlopen(entry["url"], timeout=2) as response: + response.read(1) +PY + then + http_ready=1 + break + fi + sleep 0.25 +done + +if [[ "$http_ready" != "1" ]]; then + echo "[ERROR] Local image HTTP server did not become ready" >&2 + cat "${REMOTE_DIR}/http.log" >&2 || true + exit 1 +fi + +echo "[FLASH] Writing boot and system to the inactive AGNOS slot" +PYTHONPATH="$(dirname "$REMOTE_AGNOS")" "$PYTHON_BIN" "$REMOTE_AGNOS" --swap "$REMOTE_MANIFEST" + +echo "[DONE] Both partitions verified and the inactive slot was selected" +echo "[REBOOT] Rebooting now" +sudo reboot +REMOTE_RUNNER + +ssh "${SSH_OPTS[@]}" "$HOST" "tmux kill-session -t '$SESSION' >/dev/null 2>&1 || true" +ssh "${SSH_OPTS[@]}" "$HOST" "rm -f '$REMOTE_DIR/flash.log' '$REMOTE_DIR/http.log'" +ssh "${SSH_OPTS[@]}" "$HOST" \ + "tmux new-session -d -s '$SESSION' \"REMOTE_DIR='$REMOTE_DIR' REMOTE_MANIFEST='$REMOTE_MANIFEST' REMOTE_AGNOS='$REMOTE_AGNOS' PORT='$PORT' EXPECTED_VERSION='$EXPECTED_VERSION' bash '$REMOTE_RUNNER'\"" + +echo "Started remote tmux session: $SESSION" +echo "Watch it with: ssh $HOST 'tmux attach -t $SESSION'" diff --git a/tools/agnos/patch_system_reset_image.py b/tools/agnos/patch_system_reset_image.py index 1aab299df..9b9f1c595 100644 --- a/tools/agnos/patch_system_reset_image.py +++ b/tools/agnos/patch_system_reset_image.py @@ -263,6 +263,9 @@ def patch_setup_branding_script(original: bytes, entry_name: str) -> bytes: if entry_name == MICI_SETUP_ENTRY_IN_SETUP_ZIPAPP: text = text.replace('LargerSlider("slide to use\\nopenpilot"', 'LargerSlider("slide to use\\nstarpilot"') + text = text.replace('LargerSlider("slide to install\\nopenpilot"', 'LargerSlider("slide to install\\nstarpilot"') + text = text.replace('BigPillButton("install openpilot"', 'BigPillButton("install StarPilot"') + text = text.replace('set_text("install openpilot"', 'set_text("install StarPilot"') elif entry_name == TICI_SETUP_ENTRY_IN_SETUP_ZIPAPP: text = text.replace('ButtonRadio("openpilot"', 'ButtonRadio("StarPilot"') @@ -810,6 +813,9 @@ def setup_zipapp_has_expected_content(data: bytes) -> bool: return False if b"installer.comma.ai/firestar5683/StarPilot" not in setup_script: return False + mici_setup = z.read(MICI_SETUP_ENTRY_IN_SETUP_ZIPAPP) + if b"install openpilot" in mici_setup or b"slide to install\\nopenpilot" in mici_setup: + return False except KeyError: return False return True diff --git a/tools/agnos/test_patch_system_reset_image.py b/tools/agnos/test_patch_system_reset_image.py index 3bb857f62..c2742978a 100644 --- a/tools/agnos/test_patch_system_reset_image.py +++ b/tools/agnos/test_patch_system_reset_image.py @@ -10,6 +10,7 @@ from tools.agnos.patch_system_reset_image import ( find_default_reference_manifest, format_debugfs_mode, patch_comma_sh_display_wait, + patch_setup_branding_script, sha256_zstd_payload, ) @@ -50,6 +51,22 @@ def test_patch_comma_sh_display_wait_rejects_unknown_layout(): patch_comma_sh_display_wait(b"#!/usr/bin/env bash\nexec /data/continue.sh\n") +@pytest.mark.parametrize("slider_text", ["slide to use", "slide to install"]) +def test_patch_mici_setup_branding_handles_old_and_new_labels(slider_text): + original = f'''OPENPILOT_URL = "https://openpilot.comma.ai" +self._openpilot_slider = LargerSlider("{slider_text}\\nopenpilot", callback) +self._continue_button = BigPillButton("install openpilot", green=True) +self._continue_button.set_text("install openpilot" if not custom_software else "choose software") +'''.encode() + + patched = patch_setup_branding_script(original, "openpilot/system/ui/mici_setup.py") + + assert b"installer.comma.ai/firestar5683/StarPilot" in patched + assert f"{slider_text}\\nstarpilot".encode() in patched + assert b"install StarPilot" in patched + assert b"install openpilot" not in patched + + @pytest.mark.parametrize(("mode", "expected"), [ ("100775", "0100775"), ("100644", "0100644"),