IQ.Pilot Release Commit @ bec7652

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:41 -05:00
commit 58039e647c
4603 changed files with 1236178 additions and 0 deletions

9
iqpilot/selfdrive/ui/tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
test
test_translations
test_ui/report_1
test_ui/raylib_report
diff/*.mp4
diff/*.html
diff/.coverage
diff/htmlcov/

View File

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import os
import sys
import time
import json
from iqpilot.common.basedir import BASEDIR
from iqpilot.common.params import Params
from iqpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from iqpilot.system.updated.updated import parse_release_notes
if __name__ == "__main__":
params = Params()
with open(os.path.join(BASEDIR, "iqpilot/selfdrive/selfdrived/alerts_offroad.json")) as f:
offroad_alerts = json.load(f)
t = 10 if len(sys.argv) < 2 else int(sys.argv[1])
while True:
print("setting alert update")
params.put_bool("UpdateAvailable", True)
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
time.sleep(t)
params.put_bool("UpdateAvailable", False)
# cycle through normal alerts
for a in offroad_alerts:
print("setting alert:", a)
set_offroad_alert(a, True)
time.sleep(t)
set_offroad_alert(a, False)
print("no alert")
time.sleep(t)

View File

@@ -0,0 +1,23 @@
import time
import pytest
import iqpilot.cereal.messaging as messaging
from iqpilot.system.manager.process_config import managed_processes
@pytest.mark.linux
def test_feedbackd_publishes_bookmark():
publisher = messaging.PubMaster(["bookmarkButton"])
subscriber = messaging.SubMaster(["userBookmark"])
process = managed_processes["feedbackd"]
process.start()
try:
assert publisher.wait_for_readers_to_update("bookmarkButton", timeout=5)
deadline = time.monotonic() + 5
while time.monotonic() < deadline and not subscriber.updated["userBookmark"]:
publisher.send("bookmarkButton", messaging.new_message("bookmarkButton"))
subscriber.update(100)
assert subscriber.updated["userBookmark"]
finally:
process.stop()

View File

@@ -0,0 +1,38 @@
from pathlib import Path
from iqpilot.selfdrive.ui.lib.local_routes import list_local_routes
def _touch(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"")
def test_list_local_routes_from_segment_directories(tmp_path):
route_name = "00000051--3141cf1d76"
_touch(tmp_path / f"{route_name}--0" / "qcamera.ts")
_touch(tmp_path / f"{route_name}--1" / "qlog.zst")
routes = list_local_routes(tmp_path)
assert len(routes) == 1
assert routes[0].name == route_name
assert routes[0].segment_count == 2
assert routes[0].cameras == ("road",)
def test_list_local_routes_from_single_segment_directory(tmp_path):
route_name = "00000052--3141cf1d77"
_touch(tmp_path / f"{route_name}--0" / "qcamera.ts")
routes = list_local_routes(tmp_path)
assert len(routes) == 1
assert routes[0].name == route_name
assert routes[0].subtitle == "1:00 · Road Cam"
def test_list_local_routes_ignores_invalid_entries(tmp_path):
_touch(tmp_path / "not-a-route" / "fcamera.hevc")
assert list_local_routes(tmp_path) == []

View File

@@ -0,0 +1,71 @@
import json
from iqpilot.common.params import Params
from iqpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position, resolve_mapbox_token
def test_resolve_mapbox_token_reads_param(tmp_path):
params = Params(tmp_path.as_posix())
params.put("MapboxToken", "pk.test-token")
assert resolve_mapbox_token(params) == "pk.test-token"
def test_resolve_mapbox_token_missing_returns_empty(tmp_path):
params = Params(tmp_path.as_posix())
assert resolve_mapbox_token(params) == ""
def test_current_or_last_gps_position_uses_last_position_param(tmp_path):
params = Params(tmp_path.as_posix())
params.put("LastGPSPosition", json.dumps({
"latitude": 37.7749,
"longitude": -122.4194,
"bearing": 91.5,
}))
lat, lon, bearing, valid = current_or_last_gps_position(params)
assert valid
assert lat == 37.7749
assert lon == -122.4194
assert bearing == 91.5
def test_current_or_last_gps_position_uses_iqloc_position_param(tmp_path):
params = Params(tmp_path.as_posix())
params.put("LastGPSPositionIQLoc", json.dumps({
"latitude": 34.0522,
"longitude": -118.2437,
"bearingDeg": 12.0,
}))
lat, lon, bearing, valid = current_or_last_gps_position(params)
assert valid
assert lat == 34.0522
assert lon == -118.2437
assert bearing == 12.0
def test_current_or_last_gps_position_accepts_lat_lon_aliases(tmp_path):
params = Params(tmp_path.as_posix())
params.put("LastGPSPosition", json.dumps({
"lat": 40.7128,
"lng": -74.006,
}))
lat, lon, bearing, valid = current_or_last_gps_position(params)
assert valid
assert lat == 40.7128
assert lon == -74.006
assert bearing == 0.0
def test_current_or_last_gps_position_rejects_zero_position(tmp_path):
params = Params(tmp_path.as_posix())
params.put("LastGPSPosition", "{}")
assert current_or_last_gps_position(params) == (0.0, 0.0, 0.0, False)

View File

@@ -0,0 +1,72 @@
from types import SimpleNamespace
from iqpilot.ui.onroad.nav_map_utils import (
build_mapbox_static_url,
build_mapbox_tile_url,
choose_nav_camera,
mercator_world_px,
mercator_world_px_at_zoom,
project_nav_point,
project_nav_polyline,
tile_world_size,
)
def test_mercator_world_px_changes_with_longitude():
x1, y1 = mercator_world_px(41.8826, -87.6393, 16.0)
x2, y2 = mercator_world_px(41.8826, -87.6293, 16.0)
assert x2 > x1
assert abs(y2 - y1) < 1.0
def test_project_nav_point_centers_current_position():
x, y = project_nav_point(41.8826, -87.6393, 41.8826, -87.6393, 16.0, 90.0, 420.0, 420.0)
assert round(x, 3) == 210.0
assert round(y, 3) == 210.0
def test_project_nav_polyline_preserves_point_count():
points = [
SimpleNamespace(latitude=41.8826, longitude=-87.6422),
SimpleNamespace(latitude=41.8826, longitude=-87.6393),
SimpleNamespace(latitude=41.8830, longitude=-87.6366),
]
projected = project_nav_polyline(points, 41.8826, -87.6393, 16.0, 90.0, 420.0, 420.0)
assert len(projected) == len(points)
def test_choose_nav_camera_looks_ahead_of_vehicle():
points = [
SimpleNamespace(latitude=41.8826, longitude=-87.6393),
SimpleNamespace(latitude=41.8835, longitude=-87.6355),
]
center_lat, center_lon, zoom = choose_nav_camera(41.8826, -87.6393, 90.0, points, 420.0, 420.0, 16.2)
assert center_lon > -87.6393
assert 16.0 <= zoom <= 17.8
def test_build_mapbox_static_url_contains_expected_components():
url = build_mapbox_static_url(41.8826, -87.6393, 16.2, 90.0, 420, 420)
assert "navigation-night-v1/static/" in url
assert "-87.639300,41.882600,16.20,90.0,0/420x420@2x" in url
def test_build_mapbox_tile_url_contains_expected_components():
url = build_mapbox_tile_url(16, 10619, 24322)
assert "navigation-night-v1/tiles/256/16/10619/24322@2x" in url
def test_world_size_and_world_px_align_at_integer_zoom():
world_size = tile_world_size(16)
x, y = mercator_world_px_at_zoom(41.8826, -87.6393, 16)
assert 0.0 <= x <= world_size
assert 0.0 <= y <= world_size

View File

@@ -0,0 +1,95 @@
"""
Copyright (c) IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from types import SimpleNamespace
from iqpilot.selfdrive.ui.lib import nav_search
class FakeParams:
def get(self, key, *args, **kwargs):
return {
"AmapWebServiceKey": "amap-key",
"OsmLocationName": "CN",
"MapboxToken": "mapbox-key",
}.get(key)
def test_china_search_uses_amap_without_calling_mapbox(monkeypatch):
search = nav_search.NavSearch.__new__(nav_search.NavSearch)
search._params = FakeParams()
search._seq = 1
search._results = []
search._searching = True
search._amap_adcode = ""
import threading
search._lock = threading.Lock()
amap_client = SimpleNamespace(
is_mainland_china_configured=lambda *args, **kwargs: True,
get_key=lambda *args, **kwargs: "amap-key",
status=lambda: "ok",
reverse_adcode=lambda *args, **kwargs: "",
autocomplete=lambda *args, **kwargs: [],
)
monkeypatch.setattr(nav_search, "_amap_client", amap_client)
monkeypatch.setattr(nav_search, "current_or_last_gps_position", lambda *_: (39.9, 116.4, 0.0, True))
monkeypatch.setattr(amap_client, "reverse_adcode", lambda *args, **kwargs: "110000")
monkeypatch.setattr(amap_client, "autocomplete", lambda *args, **kwargs: [
SimpleNamespace(
name="Tiananmen",
address="Dongcheng, Beijing",
provider_id="B000A83M61",
latitude=39.9087,
longitude=116.3975,
),
])
monkeypatch.setattr(nav_search.requests, "get", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("Mapbox called")))
search._do_search("Tiananmen", 1)
assert len(search._results) == 1
assert search._results[0].provider == "amap"
assert search._results[0].has_coords
def test_non_china_search_preserves_mapbox_request(monkeypatch):
search = nav_search.NavSearch.__new__(nav_search.NavSearch)
search._params = FakeParams()
search._session = "session"
search._seq = 1
search._results = []
search._searching = True
search._amap_adcode = ""
import threading
search._lock = threading.Lock()
captured = {}
class Response:
def raise_for_status(self):
return None
def json(self):
return {"suggestions": []}
def get(url, *, params, timeout):
captured.update(url=url, params=params, timeout=timeout)
return Response()
monkeypatch.setattr(nav_search, "current_or_last_gps_position", lambda *_: (41.3, -90.2, 0.0, True))
monkeypatch.setattr(nav_search, "resolve_mapbox_token", lambda *_: "mapbox-key")
monkeypatch.setattr(nav_search, "_amap_client", None)
monkeypatch.setattr(nav_search.requests, "get", get)
search._do_search("Home", 1)
assert captured["url"] == f"{nav_search.SEARCHBOX}/suggest"
assert captured["params"] == {
"q": "Home",
"access_token": "mapbox-key",
"session_token": "session",
"limit": nav_search.MAX_RESULTS,
"language": "en",
"proximity": "-90.2,41.3",
}
assert captured["timeout"] == 8

View File

@@ -0,0 +1,39 @@
import sqlite3
from scripts.iqpilot.package_xyz_tiles_to_mbtiles import build_mbtiles
from scripts.iqpilot.render_raster_tiles_from_vector_mbtiles import tile_range_for_bounds
PNG_1X1 = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8\xcf"
b"\xc0\xf0\x1f\x00\x05\x00\x01\xff\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
)
def test_tile_range_for_bounds_returns_non_empty_ranges():
x_range, y_range = tile_range_for_bounds((-88.15, 41.65, -88.02, 41.76), 14)
assert len(list(x_range)) > 0
assert len(list(y_range)) > 0
def test_build_mbtiles_from_xyz_tiles(tmp_path):
source = tmp_path / "xyz"
tile_dir = source / "14" / "2625"
tile_dir.mkdir(parents=True)
(tile_dir / "6335@2x.png").write_bytes(PNG_1X1)
output = tmp_path / "offline.mbtiles"
build_mbtiles(source, output, bounds="-88.15,41.65,-88.02,41.76")
conn = sqlite3.connect(output)
try:
fmt = conn.execute("SELECT value FROM metadata WHERE name='format'").fetchone()[0]
bounds = conn.execute("SELECT value FROM metadata WHERE name='bounds'").fetchone()[0]
count = conn.execute("SELECT COUNT(*) FROM tiles").fetchone()[0]
finally:
conn.close()
assert fmt == "png"
assert bounds == "-88.15,41.65,-88.02,41.76"
assert count == 1

View File

@@ -0,0 +1,128 @@
import os
import sqlite3
import json
from iqpilot.ui.onroad import offline_tiles
PNG_1X1 = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8\xcf"
b"\xc0\xf0\x1f\x00\x05\x00\x01\xff\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
)
def _write_mbtiles(path):
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE metadata (name text, value text)")
conn.execute("CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob)")
conn.execute("INSERT INTO metadata (name, value) VALUES ('format', 'png')")
conn.execute("INSERT INTO metadata (name, value) VALUES ('minzoom', '1')")
conn.execute("INSERT INTO metadata (name, value) VALUES ('maxzoom', '3')")
conn.execute(
"INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)",
(1, 1, offline_tiles.xyz_to_tms_y(1, 0), PNG_1X1),
)
conn.commit()
conn.close()
def test_xyz_to_tms_y():
assert offline_tiles.xyz_to_tms_y(1, 0) == 1
assert offline_tiles.xyz_to_tms_y(1, 1) == 0
def test_find_offline_mbtiles_path_from_env(tmp_path, monkeypatch):
mbtiles = tmp_path / "demo.mbtiles"
_write_mbtiles(mbtiles)
monkeypatch.setenv(offline_tiles.OFFLINE_MBTILES_ENV, str(mbtiles))
assert offline_tiles.find_offline_mbtiles_path() == mbtiles
def test_find_offline_xyz_root(tmp_path, monkeypatch):
root = tmp_path / "tiles"
(root / "15" / "10500").mkdir(parents=True)
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root))
assert offline_tiles.find_offline_xyz_root() == root
def test_load_raster_tile_blob_from_mbtiles(tmp_path):
mbtiles = tmp_path / "offline.mbtiles"
_write_mbtiles(mbtiles)
conn = offline_tiles.open_mbtiles(mbtiles)
try:
assert offline_tiles.mbtiles_is_raster(conn) is True
assert offline_tiles.mbtiles_zoom_bounds(conn) == (1, 3)
assert offline_tiles.load_raster_tile_blob(conn, 1, 1, 0) == PNG_1X1
finally:
conn.close()
def test_load_raster_tile_blob_from_xyz_dir(tmp_path, monkeypatch):
root = tmp_path / "offline_tiles"
tile_path = root / "14" / "2625"
tile_path.mkdir(parents=True)
(tile_path / "6335@2x.png").write_bytes(PNG_1X1)
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root))
assert offline_tiles.xyz_zoom_bounds(root) == (14, 14)
assert offline_tiles.load_raster_xyz_tile_blob(root, 14, 2625, 6335) == PNG_1X1
def test_find_offline_region_root_by_bounds(tmp_path, monkeypatch):
root = tmp_path / "offline_maps"
il = root / "regions" / "illinois"
ca = root / "regions" / "california"
(il / "tiles").mkdir(parents=True)
(ca / "tiles").mkdir(parents=True)
(il / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-91.6,36.9,-87.4,42.6"}}))
(ca / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-124.5,32.4,-114.1,42.1"}}))
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
assert offline_tiles.find_offline_region_root(41.88, -87.63) == il
assert offline_tiles.find_offline_region_root(34.05, -118.24) == ca
def test_find_offline_mbtiles_path_uses_selected_region(tmp_path, monkeypatch):
root = tmp_path / "offline_maps"
il = root / "regions" / "illinois"
ca = root / "regions" / "california"
(il / "tiles").mkdir(parents=True)
(ca / "tiles").mkdir(parents=True)
(il / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-91.6,36.9,-87.4,42.6"}}))
(ca / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-124.5,32.4,-114.1,42.1"}}))
_write_mbtiles(il / "tiles" / "offline.mbtiles")
_write_mbtiles(ca / "tiles" / "offline.mbtiles")
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
assert offline_tiles.find_offline_mbtiles_path(41.88, -87.63) == il / "tiles" / "offline.mbtiles"
assert offline_tiles.find_offline_mbtiles_path(34.05, -118.24) == ca / "tiles" / "offline.mbtiles"
def test_day_variant_selection(tmp_path, monkeypatch):
root = tmp_path / "offline_maps"
region = root / "regions" / "us_state.IL"
(region / "tiles").mkdir(parents=True)
(region / "manifest.json").write_text(json.dumps({"mbtiles": {"bounds": "-88.3,41.5,-87.8,41.9"}}))
_write_mbtiles(region / "tiles" / "offline.mbtiles")
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(root / "tiles"))
offline_tiles._region_roots_cache = None
offline_tiles._region_bounds_cache.clear()
# no day variant yet: day request falls back to the night set
night = region / "tiles" / "offline.mbtiles"
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=True) == night
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=False) == night
# day variant installed: day requests prefer it, night unchanged
_write_mbtiles(region / "tiles" / "offline_day.mbtiles")
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=True) == region / "tiles" / "offline_day.mbtiles"
assert offline_tiles.find_offline_mbtiles_path(41.7, -88.0, day=False) == night
def test_solar_elevation_day_night():
from iqpilot.ui.onroad.nav_map_utils import solar_elevation_deg
# Chicago 2026-07-11: 18:00 UTC (1pm CDT) is day; 06:00 UTC (1am CDT) is night
noon_utc = 1783792800.0 # 2026-07-11 18:00:00 UTC
night_utc = noon_utc - 12 * 3600
assert solar_elevation_deg(41.88, -87.63, noon_utc) > 30.0
assert solar_elevation_deg(41.88, -87.63, night_utc) < -10.0

View File

@@ -0,0 +1,27 @@
import time
import pytest
from iqpilot.selfdrive.test.helpers import with_processes
from iqpilot.system.ui.lib import application
from iqpilot.system.ui.lib.utils import gui_style_color
@pytest.mark.linux
@with_processes(["ui"])
def test_raylib_ui():
"""Test initialization of the UI widgets is successful."""
time.sleep(1)
def test_style_colors_match_gui_style_abi(monkeypatch):
calls = []
monkeypatch.setattr(application.rl, "gui_set_style", lambda *args: calls.append(args))
monkeypatch.setattr(application, "gui_style_color", lambda color: 27)
application.GuiApplication._set_styles(None)
assert [call[2] for call in calls[-3:]] == [27, 27, 27]
def test_gui_style_color_uses_binding_value_type():
color = application.rl.Color(229, 229, 229, 255)
value_type = application.rl.ffi.typeof(application.rl.raylib.GuiSetStyle).args[2]
expected = int(application.rl.ffi.cast(value_type, application.rl.color_to_int(color)))
assert gui_style_color(color) == expected

View File

@@ -0,0 +1,34 @@
from types import SimpleNamespace
from iqpilot.cereal import car
from iqpilot.selfdrive.ui import soundd
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
class TestSoundd:
def test_check_selfdrive_timeout_alert(self, monkeypatch):
class FakeSubMaster:
recv_time = {'selfdriveState': 100.0}
def __init__(self, enabled):
self.state = SimpleNamespace(enabled=enabled)
def __getitem__(self, service):
assert service == 'selfdriveState'
return self.state
enabled = FakeSubMaster(True)
disabled = FakeSubMaster(False)
monkeypatch.setattr(soundd.time, "monotonic", lambda: 100.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
assert not soundd.check_selfdrive_timeout_alert(enabled)
monkeypatch.setattr(soundd.time, "monotonic", lambda: 101.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
assert soundd.check_selfdrive_timeout_alert(enabled)
assert not soundd.check_selfdrive_timeout_alert(disabled)
monkeypatch.setattr(soundd.time, "monotonic", lambda: 110.0 + soundd.SELFDRIVE_STATE_TIMEOUT)
assert not soundd.check_selfdrive_timeout_alert(enabled)
# TODO: add test with micd for checking that soundd actually outputs sounds

View File

@@ -0,0 +1,48 @@
import gettext
import json
import re
import pytest
from iqpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR
FORMAT_ARG = re.compile(r"%(?:\([^)]+\))?[#0+\-]?(?:\d+|\*)?(?:\.\d+|\.\*)?[hlL]?[diouxXeEfFgGcrsa%]")
with LANGUAGES_FILE.open(encoding="utf-8") as stream:
LANGUAGES = json.load(stream)
def load_catalog(language_code: str) -> dict[str | tuple[str, int], str]:
with TRANSLATIONS_DIR.joinpath(f"app_{language_code}.mo").open("rb") as stream:
return gettext.GNUTranslations(stream)._catalog
def message_keys(catalog: dict[str | tuple[str, int], str]) -> set[str]:
return {key for key in catalog if isinstance(key, str) and key}
def format_args(text: str) -> list[str]:
return sorted(match for match in FORMAT_ARG.findall(text) if match != "%%")
def test_language_codes_are_unique():
assert len(LANGUAGES) == len(set(LANGUAGES.values()))
@pytest.mark.parametrize("language_code", LANGUAGES.values(), ids=LANGUAGES.keys())
def test_translation_catalog_is_complete(language_code):
source = load_catalog("en")
translated = load_catalog(language_code)
assert message_keys(translated) == message_keys(source)
@pytest.mark.parametrize("language_code", LANGUAGES.values(), ids=LANGUAGES.keys())
def test_translation_catalog_entries(language_code):
catalog = load_catalog(language_code)
for source, translated in catalog.items():
if not isinstance(source, str) or not source:
continue
assert translated
assert format_args(translated) == format_args(source)