IQ.Pilot Release Commit @ b79954e

This commit is contained in:
IQ.Lvbs CI [bot]
2026-07-25 18:44:03 -05:00
parent f94087822f
commit 563022daa3
179 changed files with 696 additions and 610 deletions

View File

@@ -1,15 +1,21 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Shared tunables and a small debug logger for the offline road-name / turn-speed path.
"""
from openpilot.common.swaglog import cloudlog
LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on iqLiveData msg.
# seconds of road ahead we scan for upcoming turn-speed zones published on iqLiveData
LOOK_AHEAD_HORIZON_TIME = 15.0
# clear the on-screen road name once it has gone this long without a refresh (s)
ROAD_NAME_TIMEOUT = 30
R = 6373000.0 # mean Earth radius in metres (great-circle distance math)
QUERY_RADIUS = 3000 # online OSM query reach, metres
QUERY_RADIUS_OFFLINE = 2250 # offline-tile OSM query reach, metres
_DEBUG = False
_CLOUDLOG_DEBUG = False
ROAD_NAME_TIMEOUT = 30 # secs
R = 6373000.0 # approximate radius of earth in mts
QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries.
QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries.
def debug_road_data(msg, log_to_cloud=True):

View File

@@ -1,11 +1,6 @@
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
@@ -13,26 +8,27 @@ from openpilot.common.realtime import Ratekeeper, config_realtime_process
from openpilot.iqpilot.iq_maps.road_data import debug_road_data
from openpilot.iqpilot.iq_maps.road_data.iq_road_layer import IQRoadLayer
ROAD_LAYER_HZ = 1
ROAD_LAYER_CORES = [0, 1, 2, 3]
def excepthook(args):
debug_road_data(f'IQ maps threading exception:\n{args}')
def _log_thread_exception(args) -> None:
debug_road_data(f"IQ maps threading exception:\n{args}")
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_iq_thread():
config_realtime_process([0, 1, 2, 3], 5)
live_map_iq = IQRoadLayer()
rk = Ratekeeper(1, print_delay_threshold=None)
def run() -> None:
config_realtime_process(ROAD_LAYER_CORES, 5)
layer = IQRoadLayer()
rk = Ratekeeper(ROAD_LAYER_HZ, print_delay_threshold=None)
while True:
live_map_iq.step()
layer.step()
rk.keep_time()
def main():
threading.excepthook = excepthook
live_map_data_iq_thread()
def main() -> None:
threading.excepthook = _log_thread_exception
run()
if __name__ == "__main__":

View File

@@ -1,94 +1,72 @@
#!/usr/bin/env python3
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Maintainer utility: pin a new pfeiferj/mapd release tag and refresh the checked-in
binary hash. Not used at runtime.
"""
import argparse
import os
import re
import sys
from openpilot.iqpilot.iq_maps.vendor_mapd_installer import get_file_hash
from openpilot.common.basedir import BASEDIR
from openpilot.iqpilot.iq_maps import VENDOR_MAPD_PATH
from openpilot.iqpilot.iq_maps.vendor_mapd_installer import (
VENDOR_RELEASE_TAG,
get_file_hash,
)
MAPD_HASH_PATH = os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash")
MAPD_VERSION_PATH = os.path.join(BASEDIR, "iqpilot", "iq_maps", "vendor_mapd_installer.py")
RELEASE_SYMBOL = "VENDOR_RELEASE_TAG"
_RELEASE_SYMBOL = "VENDOR_RELEASE_TAG"
_INSTALLER_SRC = os.path.join(BASEDIR, "iqpilot", "iq_maps", "vendor_mapd_installer.py")
_HASH_FILE = os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash")
_TAG_ASSIGN = re.compile(rf'^{_RELEASE_SYMBOL}\s*=\s*["\'][^"\']*["\']', re.MULTILINE)
def update_mapd_hash():
mapd_hash = get_file_hash(VENDOR_MAPD_PATH)
def rewrite_pinned_tag(new_tag: str) -> bool:
with open(_INSTALLER_SRC) as f:
src = f.read()
with open(MAPD_HASH_PATH, "w") as f:
f.write(mapd_hash)
patched, count = _TAG_ASSIGN.subn(f'{_RELEASE_SYMBOL} = "{new_tag}"', src, count=1)
if count != 1:
print(f"could not locate the {_RELEASE_SYMBOL} assignment in {_INSTALLER_SRC}; nothing written")
return False
print(f"Generated and updated new mapd hash to {MAPD_HASH_PATH}")
with open(_INSTALLER_SRC, "w") as f:
f.write(patched)
print(f"pinned {_RELEASE_SYMBOL} -> {new_tag}")
return True
def get_current_mapd_version(path: str) -> str:
print("[GET CURRENT MAPD VERSION]")
with open(path) as f:
for line in f:
if line.strip().startswith(RELEASE_SYMBOL):
match = re.search(rf'{RELEASE_SYMBOL}\s*=\s*[\'"]([^\'"]+)[\'"]', line)
if match:
ver = match.group(1)
print(f'Current mapd version: "{ver}"')
return ver
else:
print(f"[ERROR] {RELEASE_SYMBOL} line found but no quoted value detected.")
return ""
print(f"[ERROR] {RELEASE_SYMBOL} not found in file!")
return ""
def refresh_hash_file() -> None:
digest = get_file_hash(VENDOR_MAPD_PATH)
with open(_HASH_FILE, "w") as f:
f.write(digest)
print(f"wrote binary hash {digest} -> {_HASH_FILE}")
def update_mapd_version(ver: str, path: str):
print("[CHANGE CURRENT MAPD VERSION]")
with open(path) as f:
lines = f.readlines()
found = False
new_lines = []
for line in lines:
if not found and line.startswith(f"{RELEASE_SYMBOL} ="):
new_lines.append(f'{RELEASE_SYMBOL} = "{ver}"\n')
found = True
new_lines.extend(lines[lines.index(line) + 1:])
break
else:
new_lines.append(line)
if not found:
print(f"[ERROR] {RELEASE_SYMBOL} line not found! Aborting without writing.")
return
with open(path, "w") as f:
f.writelines(new_lines)
print(f'New mapd version: "{ver}"')
print("[DONE]")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update mapd version and hash")
parser.add_argument("--new_ver", type=str, help="New mapd version")
def main() -> int:
parser = argparse.ArgumentParser(description="Pin a new mapd release tag and refresh its hash")
parser.add_argument("--new_ver", type=str, help='e.g. --new_ver "v2.1.0"')
args = parser.parse_args()
if not args.new_ver:
print("Warning: No new mapd version provided. Use --new_ver to specify")
print("Example:")
print(" python iqpilot/iq_maps/update_vendor_version.py --new_ver \"v1.12.0\"")
print("Current mapd version and hash will not be updated! (aborted)")
exit(0)
parser.print_help()
print(f'\ncurrently pinned: {VENDOR_RELEASE_TAG} (unchanged)')
return 0
current_ver = get_current_mapd_version(MAPD_VERSION_PATH)
new_ver = f"{args.new_ver}"
if current_ver == new_ver:
print(f'Proposed mapd version: "{new_ver}"')
confirm = input("Proposed mapd version is the same as the current mapd version. Confirm? (y/n): ").upper().strip()
if confirm != "Y":
print("Current mapd version and hash will not be updated! (aborted)")
exit(0)
target = args.new_ver.strip()
if target == VENDOR_RELEASE_TAG:
reply = input(f"{target} is already the pinned tag — re-run anyway? (y/N): ").strip().lower()
if reply != "y":
print("aborted; nothing changed")
return 0
update_mapd_version(new_ver, MAPD_VERSION_PATH)
update_mapd_hash()
if not rewrite_pinned_tag(target):
return 1
refresh_hash_file()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,21 +1,24 @@
#!/usr/bin/env python3
import hashlib
"""
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
Provisions the `mapd` routing binary authored by Jacob Pfeifer (github.com/pfeiferj/mapd).
The binary itself is his work; this module only fetches, verifies and stages it on-device.
"""
import hashlib
import logging
import os
import stat
import time
import traceback
import requests
from pathlib import Path
from urllib.request import urlopen
import requests
from cereal import messaging
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.system.hardware.hw import Paths
from openpilot.common.spinner import Spinner
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import is_prebuilt
from openpilot.iqpilot.iq_maps import VENDOR_MAPD_BIN_DIR, VENDOR_MAPD_PATH
import openpilot.system.sentry as sentry
@@ -23,17 +26,25 @@ import openpilot.system.sentry as sentry
VENDOR_RELEASE_TAG = "v2.0.6"
VENDOR_RELEASE_URL = f"https://github.com/pfeiferj/mapd/releases/download/{VENDOR_RELEASE_TAG}/mapd"
_VERSION_PARAM = "MapdVersion"
_HASH_FILE = os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash")
_HTTP_TIMEOUT_S = 60
_FETCH_ATTEMPTS = 5
_NET_PROBE_ATTEMPTS = 10
_NET_PROBE_INTERVAL_S = 2
def get_file_hash(path: str) -> str:
"""Hex SHA-256 digest of a file on disk."""
digest = hashlib.sha256()
with open(path, "rb") as handle:
for block in iter(lambda: handle.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
def stamp_vendor_version(version: str, params: Params | None = None) -> None:
if params is None:
params = Params()
params.put("MapdVersion", version)
def _expected_vendor_hash_path() -> str:
from openpilot.common.basedir import BASEDIR
return os.path.join(BASEDIR, "iqpilot", "iq_maps", "tests", "mapd_hash")
(params or Params()).put(_VERSION_PARAM, version)
class VendorMapdInstaller:
@@ -41,141 +52,130 @@ class VendorMapdInstaller:
self._spinner = spinner_ref
self._params = Params()
def fetch(self) -> None:
self.ensure_directories_exist()
self._download_file()
stamp_vendor_version(VENDOR_RELEASE_TAG, self._params)
def check_and_download(self) -> None:
if self.download_needed():
self.fetch()
def download_needed(self) -> bool:
if not os.path.exists(VENDOR_MAPD_PATH):
return True
if self.get_installed_version() != VENDOR_RELEASE_TAG:
return True
return not self._binary_hash_matches()
@staticmethod
def _binary_hash_matches() -> bool:
try:
hash_path = _expected_vendor_hash_path()
with open(hash_path) as f:
expected = f.read().strip()
except Exception:
return True
if not expected:
return True
try:
return get_file_hash(VENDOR_MAPD_PATH) == expected
except Exception:
return True
# --- externally consumed surface -----------------------------------------
def get_installed_version(self) -> str:
return str(self._params.get(_VERSION_PARAM) or "")
@staticmethod
def ensure_directories_exist() -> None:
if not os.path.exists(Paths.mapd_root()):
os.makedirs(Paths.mapd_root())
if not os.path.exists(VENDOR_MAPD_BIN_DIR):
os.makedirs(VENDOR_MAPD_BIN_DIR)
for directory in (Paths.mapd_root(), VENDOR_MAPD_BIN_DIR):
os.makedirs(directory, exist_ok=True)
@staticmethod
def _safe_write_and_set_executable(file_path: Path, content: bytes) -> None:
with open(file_path, 'wb') as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode)
os.chmod(file_path, current_permissions | stat.S_IEXEC)
def _download_file(self, num_retries=5) -> None:
temp_file = Path(VENDOR_MAPD_PATH + ".tmp")
download_timeout = 60
for cnt in range(num_retries):
try:
response = requests.get(VENDOR_RELEASE_URL, stream=True, timeout=download_timeout)
response.raise_for_status()
self._safe_write_and_set_executable(temp_file, response.content)
temp_file.replace(VENDOR_MAPD_PATH)
return
except requests.exceptions.ReadTimeout:
self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]")
time.sleep(0.5)
except requests.exceptions.RequestException as e:
self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]")
time.sleep(0.5)
# Delete temp file if the process was not successful.
if temp_file.exists():
temp_file.unlink()
logging.error("Failed to download file after all retries")
def get_installed_version(self) -> str:
return str(self._params.get("MapdVersion") or "")
def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool:
max_retries = 10
for retries in range(max_retries + 1):
self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]")
time.sleep(2)
try:
_ = urlopen('https://sentry.io', timeout=10)
return True
except Exception as e:
print(f'Wait for internet failed: {e}')
if return_on_failure and retries == max_retries:
return False
return False
def check_and_download(self) -> None:
if not self._binary_up_to_date():
self._provision()
def non_prebuilt_install(self) -> None:
sm = messaging.SubMaster(['deviceState'])
metered = sm['deviceState'].networkMetered
if metered:
self._spinner.update("Can't proceed with mapd install since network is metered!")
if self._on_metered_link():
self._say("Metered connection detected — offline maps engine will not download here.")
time.sleep(5)
return
try:
self.ensure_directories_exist()
if not self.download_needed():
self._spinner.update("Offline maps binary is ready.")
if self._binary_up_to_date():
self._say("Offline maps engine already present and current.")
time.sleep(0.1)
return
if self.wait_for_internet_connection(return_on_failure=True):
self._spinner.update(f"Downloading vendor mapd [{self.get_installed_version()}] => [{VENDOR_RELEASE_TAG}].")
if self._block_until_online():
self._say(f"Retrieving offline maps engine [{self.get_installed_version() or 'none'}] -> [{VENDOR_RELEASE_TAG}]")
time.sleep(0.1)
self.check_and_download()
self._provision()
self._spinner.close()
except Exception as exc: # noqa: BLE001
self._announce_failure(exc)
except Exception:
for i in range(6):
self._spinner.update("Failed to download OSM maps won't work until properly downloaded!" +
"Try again manually rebooting. " +
f"Boot will continue in {5 - i}s...")
time.sleep(1)
# --- internal ------------------------------------------------------------
def _expected_hash(self) -> str:
try:
with open(_HASH_FILE) as f:
return f.read().strip()
except OSError:
return ""
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
def _binary_up_to_date(self) -> bool:
if not os.path.exists(VENDOR_MAPD_PATH):
return False
if self.get_installed_version() != VENDOR_RELEASE_TAG:
return False
reference = self._expected_hash()
if not reference:
return True
try:
return get_file_hash(VENDOR_MAPD_PATH) == reference
except OSError:
return False
def _provision(self) -> None:
self.ensure_directories_exist()
if self._retrieve_binary():
stamp_vendor_version(VENDOR_RELEASE_TAG, self._params)
def _retrieve_binary(self) -> bool:
staging = Path(f"{VENDOR_MAPD_PATH}.part")
last_error: Exception | None = None
for attempt in range(1, _FETCH_ATTEMPTS + 1):
try:
with requests.get(VENDOR_RELEASE_URL, stream=True, timeout=_HTTP_TIMEOUT_S) as resp:
resp.raise_for_status()
with open(staging, "wb") as out:
for chunk in resp.iter_content(chunk_size=1 << 16):
out.write(chunk)
out.flush()
os.fsync(out.fileno())
os.chmod(staging, os.lstat(staging).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
staging.replace(VENDOR_MAPD_PATH)
return True
except requests.exceptions.RequestException as exc:
last_error = exc
self._say(f"offline maps fetch attempt {attempt}/{_FETCH_ATTEMPTS} did not complete ({exc})")
time.sleep(0.5)
staging.unlink(missing_ok=True)
logging.error("offline maps engine could not be fetched after %d attempts: %s", _FETCH_ATTEMPTS, last_error)
return False
def _on_metered_link(self) -> bool:
sm = messaging.SubMaster(["deviceState"])
return bool(sm["deviceState"].networkMetered)
def _block_until_online(self) -> bool:
for i in range(1, _NET_PROBE_ATTEMPTS + 1):
self._say(f"Waiting for a usable network connection... [{i}/{_NET_PROBE_ATTEMPTS}]")
if self._link_reachable():
return True
time.sleep(_NET_PROBE_INTERVAL_S)
return False
@staticmethod
def _link_reachable() -> bool:
try:
requests.head(VENDOR_RELEASE_URL, timeout=10, allow_redirects=True)
return True
except requests.exceptions.RequestException as exc:
logging.debug("network probe failed: %s", exc)
return False
def _announce_failure(self, exc: Exception) -> None:
for remaining in range(5, 0, -1):
self._say(f"Offline maps engine unavailable; navigation stays online-only. Boot continues in {remaining}s...")
time.sleep(1)
logging.exception("vendor mapd install failed")
sentry.init(sentry.SentryProject.SELFDRIVE)
sentry.capture_exception(exc)
def _say(self, text: str) -> None:
self._spinner.update(text)
if __name__ == "__main__":
spinner = Spinner()
install_manager = VendorMapdInstaller(spinner)
install_manager.ensure_directories_exist()
installer = VendorMapdInstaller(spinner)
installer.ensure_directories_exist()
if is_prebuilt():
debug_msg = f"[DEBUG] This is prebuilt, no vendor mapd install required. VERSION: [{VENDOR_RELEASE_TAG}], Param [{install_manager.get_installed_version()}]"
spinner.update(debug_msg)
spinner.update(f"[DEBUG] Prebuilt build; vendor mapd install skipped. "
f"target [{VENDOR_RELEASE_TAG}], param [{installer.get_installed_version()}]")
stamp_vendor_version(VENDOR_RELEASE_TAG)
else:
spinner.update(f"Checking if vendor mapd is installed and valid. Prebuilt [{is_prebuilt()}]")
install_manager.non_prebuilt_install()
def get_file_hash(path: str) -> str:
"""Hex SHA-256 of a file's contents."""
with open(path, "rb") as handle:
return hashlib.file_digest(handle, "sha256").hexdigest()
spinner.update(f"Verifying vendor mapd install. prebuilt [{is_prebuilt()}]")
installer.non_prebuilt_install()