1
0
forked from IQ.Lvbs/IQ.Pilot

IQ.Pilot Release Commit @ 5bc9cd3

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit d037016281
4504 changed files with 1129827 additions and 0 deletions

9
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

22
selfdrive/ui/tests/body.py Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import time
import cereal.messaging as messaging
if __name__ == "__main__":
while True:
pm = messaging.PubMaster(['carParams', 'carState'])
batt = 1.
while True:
msg = messaging.new_message('carParams')
msg.carParams.brand = "body"
msg.carParams.notCar = True
pm.send('carParams', msg)
for b in range(100, 0, -1):
msg = messaging.new_message('carState')
msg.carState.charging = True
msg.carState.fuelGauge = b / 100.
pm.send('carState', msg)
time.sleep(0.1)
time.sleep(1)

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import os
import sys
import time
import json
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.updated.updated import parse_release_notes
if __name__ == "__main__":
params = Params()
with open(os.path.join(BASEDIR, "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)

201
selfdrive/ui/tests/diff/diff.py Executable file
View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
import os
import sys
import subprocess
import tempfile
import base64
import webbrowser
import argparse
from pathlib import Path
from openpilot.common.basedir import BASEDIR
DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report"
def extract_frames(video_path, output_dir):
output_pattern = str(output_dir / "frame_%04d.png")
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
subprocess.run(cmd, capture_output=True, check=True)
frames = sorted(output_dir.glob("frame_*.png"))
return frames
def compare_frames(frame1_path, frame2_path):
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
return result.returncode == 0
def frame_to_data_url(frame_path):
with open(frame_path, 'rb') as f:
data = f.read()
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
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]
subprocess.run(cmd, capture_output=True, check=True)
def find_differences(video1, video2):
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir = Path(tmpdir)
print(f"Extracting frames from {video1}...")
frames1_dir = tmpdir / "frames1"
frames1_dir.mkdir()
frames1 = extract_frames(video1, frames1_dir)
print(f"Extracting frames from {video2}...")
frames2_dir = tmpdir / "frames2"
frames2_dir.mkdir()
frames2 = extract_frames(video2, frames2_dir)
if len(frames1) != len(frames2):
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
min_frames = min(len(frames1), len(frames2))
frames1 = frames1[:min_frames]
frames2 = frames2[:min_frames]
print(f"Comparing {len(frames1)} frames...")
different_frames = []
frame_data = []
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
is_different = not compare_frames(f1, f2)
if is_different:
different_frames.append(i)
if i < 10 or i >= len(frames1) - 10 or is_different:
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
return different_frames, frame_data, len(frames1)
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
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)
result_text = (
f"✅ Videos are identical! ({total_frames} frames)"
if len(different_frames) == 0
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
)
html = f"""<h2>UI Diff</h2>
<table>
<tr>
<td width='33%'>
<p><strong>Video 1</strong></p>
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video1))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Video 2</strong></p>
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
<source src='{os.path.join(basedir, os.path.basename(video2))}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
<td width='33%'>
<p><strong>Pixel Diff</strong></p>
<video id='diffVideo' width='100%' autoplay muted loop>
<source src='{os.path.join(basedir, 'diff.mp4')}' type='video/mp4'>
Your browser does not support the video tag.
</video>
</td>
</tr>
</table>
<script>
function syncVideos() {{
const video1 = document.getElementById('video1');
const video2 = document.getElementById('video2');
const diffVideo = document.getElementById('diffVideo');
video1.currentTime = video2.currentTime = diffVideo.currentTime;
}}
video1.addEventListener('timeupdate', () => {{
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
video2.currentTime = video1.currentTime;
}}
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video1.currentTime;
}}
}});
video2.addEventListener('timeupdate', () => {{
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = video2.currentTime;
}}
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
diffVideo.currentTime = video2.currentTime;
}}
}});
diffVideo.addEventListener('timeupdate', () => {{
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
video1.currentTime = diffVideo.currentTime;
video2.currentTime = diffVideo.currentTime;
}}
}});
</script>
<hr>
<p><strong>Results:</strong> {result_text}</p>
"""
return html
def main():
parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report')
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('--no-open', action='store_true', help='Do not open HTML report in browser')
args = parser.parse_args()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
print("=" * 60)
print("VIDEO DIFF - HTML REPORT")
print("=" * 60)
print(f"Video 1: {args.video1}")
print(f"Video 2: {args.video2}")
print(f"Output: {args.output}")
print()
# Create diff video
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
create_diff_video(args.video1, args.video2, diff_video_path)
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
if different_frames is None:
sys.exit(1)
print()
print("Generating HTML report...")
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
with open(DIFF_OUT_DIR / args.output, 'w') as f:
f.write(html)
# 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)}')
return 0 if len(different_frames) == 0 else 1
if __name__ == "__main__":
sys.exit(main())

128
selfdrive/ui/tests/diff/replay.py Executable file
View File

@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import os
import time
import coverage
import pyray as rl
from dataclasses import dataclass
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
os.environ["RECORD"] = "1"
if "RECORD_OUTPUT" not in os.environ:
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
from openpilot.common.params import Params
from openpilot.system.version import terms_version, training_version
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
FPS = 60
HEADLESS = os.getenv("WINDOWED", "0") == "1"
@dataclass
class DummyEvent:
click: bool = False
# TODO: add some kind of intensity
swipe_left: bool = False
swipe_right: bool = False
swipe_down: bool = False
SCRIPT = [
(0, DummyEvent()),
(FPS * 1, DummyEvent(click=True)),
(FPS * 2, DummyEvent(click=True)),
(FPS * 3, DummyEvent()),
]
def setup_state():
params = Params()
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put("DongleId", "test123456789")
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
return None
def inject_click(coords):
events = []
x, y = coords[0]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic()))
for x, y in coords[1:]:
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic()))
x, y = coords[-1]
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()))
with gui_app._mouse._lock:
gui_app._mouse._events.extend(events)
def handle_event(event: DummyEvent):
if event.click:
inject_click([(gui_app.width // 2, gui_app.height // 2)])
if event.swipe_left:
inject_click([(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width // 4, gui_app.height // 2),
(0, gui_app.height // 2)])
if event.swipe_right:
inject_click([(gui_app.width // 4, gui_app.height // 2),
(gui_app.width * 3 // 4, gui_app.height // 2),
(gui_app.width, gui_app.height // 2)])
if event.swipe_down:
inject_click([(gui_app.width // 2, gui_app.height // 4),
(gui_app.width // 2, gui_app.height * 3 // 4),
(gui_app.width // 2, gui_app.height)])
def run_replay():
setup_state()
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
if not HEADLESS:
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
gui_app.init_window("ui diff test", fps=FPS)
main_layout = MiciMainLayout()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
frame = 0
script_index = 0
for should_render in gui_app.render():
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
_, event = SCRIPT[script_index]
handle_event(event)
script_index += 1
ui_state.update()
if should_render:
main_layout.render()
frame += 1
if script_index >= len(SCRIPT):
break
gui_app.close()
print(f"Total frames: {frame}")
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
def main():
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
with cov.collect():
run_replay()
cov.stop()
cov.save()
cov.report()
cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov'))
print("HTML report: htmlcov/index.html")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
import os
import time
import cProfile
import pyray as rl
import numpy as np
from msgq.visionipc import VisionIpcServer, VisionStreamType
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
from openpilot.system.ui.lib.application import gui_app
from openpilot.tools.lib.logreader import LogReader
FPS = 60
def chunk_messages_by_time(messages):
dt_ns = 1e9 / FPS
chunks = []
current_services = {}
next_time = messages[0].logMonoTime + dt_ns if messages else 0
for msg in messages:
if msg.logMonoTime >= next_time:
chunks.append(current_services)
current_services = {}
next_time += dt_ns * ((msg.logMonoTime - next_time) // dt_ns + 1)
current_services[msg.which()] = msg
if current_services:
chunks.append(current_services)
return chunks
def patch_submaster(message_chunks):
def mock_update(timeout=None):
sm = ui_state.sm
sm.updated = dict.fromkeys(sm.services, False)
current_time = time.monotonic()
for service, msg in message_chunks[sm.frame].items():
if service in sm.data:
sm.seen[service] = True
sm.updated[service] = True
msg_builder = msg.as_builder()
sm.data[service] = getattr(msg_builder, service)
sm.logMonoTime[service] = msg.logMonoTime
sm.recv_time[service] = current_time
sm.recv_frame[service] = sm.frame
sm.valid[service] = True
sm.frame += 1
ui_state.sm.update = mock_update
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='Profile openpilot UI rendering and state updates')
parser.add_argument('route', type=str, nargs='?', default="302bab07c1511180/00000006--0b9a7005f1/3",
help='Route to use for profiling')
parser.add_argument('--loop', type=int, default=1,
help='Number of times to loop the log (default: 1)')
parser.add_argument('--output', type=str, default='cachegrind.out.ui',
help='Output file prefix (default: cachegrind.out.ui)')
parser.add_argument('--max-seconds', type=float, default=None,
help='Maximum seconds of messages to process (default: all)')
parser.add_argument('--headless', action='store_true',
help='Run in headless mode without GPU (for CI/testing)')
args = parser.parse_args()
print(f"Loading log from {args.route}...")
lr = LogReader(args.route, sort_by_time=True)
messages = list(lr) * args.loop
print("Chunking messages...")
message_chunks = chunk_messages_by_time(messages)
if args.max_seconds:
message_chunks = message_chunks[:int(args.max_seconds * FPS)]
print("Initializing UI with GPU rendering...")
if args.headless:
os.environ['SDL_VIDEODRIVER'] = 'dummy'
gui_app.init_window("UI Profiling", fps=600)
main_layout = MiciMainLayout()
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
print("Running...")
patch_submaster(message_chunks)
W, H = 2048, 1216
vipc = VisionIpcServer("camerad")
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H)
vipc.start_listener()
yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2
yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()
with cProfile.Profile() as pr:
for should_render in gui_app.render():
if ui_state.sm.frame >= len(message_chunks):
break
if ui_state.sm.frame % 3 == 0:
eof = int((ui_state.sm.frame % 3) * 0.05 * 1e9)
vipc.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof)
ui_state.update()
if should_render:
main_layout.render()
pr.dump_stats(f'{args.output}_deterministic.stats')
rl.close_window()
print("\nProfiling complete!")
print(f" run: python -m pstats {args.output}_deterministic.stats")

View File

@@ -0,0 +1,53 @@
import pytest
import cereal.messaging as messaging
from cereal import car
from openpilot.common.params import Params
from openpilot.system.manager.process_config import managed_processes
@pytest.mark.skip("tmp disabled")
class TestFeedbackd:
def setup_method(self):
self.pm = messaging.PubMaster(['carState', 'rawAudioData'])
self.sm = messaging.SubMaster(['audioFeedback'])
def _send_lkas_button(self, pressed: bool):
msg = messaging.new_message('carState')
msg.carState.canValid = True
msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}]
self.pm.send('carState', msg)
def _send_audio_data(self, count: int = 5):
for _ in range(count):
audio_msg = messaging.new_message('rawAudioData')
audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16
audio_msg.rawAudioData.sampleRate = 16000
self.pm.send('rawAudioData', audio_msg)
self.sm.update(timeout=100)
@pytest.mark.parametrize("record_feedback", [False, True])
def test_audio_feedback(self, record_feedback):
Params().put_bool("RecordAudioFeedback", record_feedback)
managed_processes["feedbackd"].start()
assert self.pm.wait_for_readers_to_update('carState', timeout=5)
assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5)
self._send_lkas_button(pressed=True)
self._send_audio_data()
self._send_lkas_button(pressed=False)
self._send_audio_data()
if record_feedback:
assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled"
else:
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled"
self._send_lkas_button(pressed=True)
self._send_audio_data()
self._send_lkas_button(pressed=False)
self._send_audio_data()
assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press"
managed_processes["feedbackd"].stop()

View File

@@ -0,0 +1,39 @@
from pathlib import Path
from openpilot.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 = "aaaaaaaaaaaaaaaa|2026-07-03--12-30-00"
_touch(tmp_path / f"{route_name}--0" / "fcamera.hevc")
_touch(tmp_path / f"{route_name}--1" / "rlog.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].camera_count == 1
assert "Jul 3" in routes[0].label
def test_list_local_routes_from_nested_route_directory(tmp_path):
route_name = "bbbbbbbbbbbbbbbb|2026-07-03--13-45-00"
_touch(tmp_path / route_name / "0" / "fcamera.hevc")
routes = list_local_routes(tmp_path)
assert len(routes) == 1
assert routes[0].name == route_name
assert routes[0].subtitle == "1 segment - road camera"
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,31 @@
from types import SimpleNamespace
from openpilot.selfdrive.ui.lib import motd
def test_load_motds_uses_verified_module_and_dongle_override(monkeypatch):
calls = []
def import_verified_module(bundle, module):
calls.append((bundle, module))
return SimpleNamespace(messages_for_dongle=lambda dongle_id: (" Staff fleet ", "", 42))
monkeypatch.setattr(
"openpilot.system.proprietary_runtime._verified_import.import_verified_module",
import_verified_module,
)
assert motd.load_motds("0123456789ABCDEF") == ["Staff fleet"]
assert calls == [(motd._BUNDLE_NAME, motd._MODULE_NAME)]
def test_load_motds_falls_back_when_verified_import_is_unavailable(monkeypatch):
def fail_import(*_args):
raise ImportError("bundle unavailable")
monkeypatch.setattr(
"openpilot.system.proprietary_runtime._verified_import.import_verified_module",
fail_import,
)
assert motd.load_motds("0123456789abcdef") == list(motd.FALLBACK_MOTDS)

View File

@@ -0,0 +1,71 @@
import json
from openpilot.common.params import Params
from openpilot.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 openpilot.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-day-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-day-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,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 openpilot.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 openpilot.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,8 @@
import time
from openpilot.selfdrive.test.helpers import with_processes
@with_processes(["ui"])
def test_raylib_ui():
"""Test initialization of the UI widgets is successful."""
time.sleep(1)

View File

@@ -0,0 +1,35 @@
from cereal import car
from cereal import messaging
from cereal.messaging import SubMaster, PubMaster
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
import time
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
class TestSoundd:
def test_check_selfdrive_timeout_alert(self):
sm = SubMaster(['selfdriveState'])
pm = PubMaster(['selfdriveState'])
for _ in range(100):
cs = messaging.new_message('selfdriveState')
cs.selfdriveState.enabled = True
pm.send("selfdriveState", cs)
time.sleep(0.01)
sm.update(0)
assert not check_selfdrive_timeout_alert(sm)
for _ in range(SELFDRIVE_STATE_TIMEOUT * 110):
sm.update(0)
time.sleep(0.01)
assert check_selfdrive_timeout_alert(sm)
# TODO: add test with micd for checking that soundd actually outputs sounds

View File

@@ -0,0 +1,124 @@
import pytest
import json
import os
import re
import xml.etree.ElementTree as ET
import string
import requests
from parameterized import parameterized_class
from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, LANGUAGES_FILE
with open(str(LANGUAGES_FILE)) as f:
translation_files = json.load(f)
UNFINISHED_TRANSLATION_TAG = "<translation type=\"unfinished\"" # non-empty translations can be marked unfinished
LOCATION_TAG = "<location "
FORMAT_ARG = re.compile("%[0-9]+")
@pytest.mark.skip("TODO: update for raylib")
@parameterized_class(("name", "file"), translation_files.items())
class TestTranslations:
name: str
file: str
@staticmethod
def _read_translation_file(path, file):
tr_file = os.path.join(path, f"{file}.ts")
with open(tr_file) as f:
return f.read()
def test_missing_translation_files(self):
assert os.path.exists(os.path.join(str(TRANSLATIONS_DIR), f"{self.file}.ts")), \
f"{self.name} has no XML translation file, run selfdrive/ui/update_translations.py"
@pytest.mark.skip("Only test unfinished translations before going to release")
def test_unfinished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
assert UNFINISHED_TRANSLATION_TAG not in cur_translations, \
f"{self.file} ({self.name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist"
def test_vanished_translations(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
assert "<translation type=\"vanished\">" not in cur_translations, \
f"{self.file} ({self.name}) translation file has obsolete translations. Run selfdrive/ui/update_translations.py --vanish to remove them"
def test_finished_translations(self):
"""
Tests ran on each translation marked "finished"
Plural:
- that any numerus (plural) translations have all plural forms non-empty
- that the correct format specifier is used (%n)
Non-plural:
- that translation is not empty
- that translation format arguments are consistent
"""
tr_xml = ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts"))
for context in tr_xml.getroot():
for message in context.iterfind("message"):
translation = message.find("translation")
source_text = message.find("source").text
# Do not test unfinished translations
if translation.get("type") == "unfinished":
continue
if message.get("numerus") == "yes":
numerusform = [t.text for t in translation.findall("numerusform")]
for nf in numerusform:
assert nf is not None, f"Ensure all plural translation forms are completed: {source_text}"
assert "%n" in nf, "Ensure numerus argument (%n) exists in translation."
assert FORMAT_ARG.search(nf) is None, f"Plural translations must use %n, not %1, %2, etc.: {numerusform}"
else:
assert translation.text is not None, f"Ensure translation is completed: {source_text}"
source_args = FORMAT_ARG.findall(source_text)
translation_args = FORMAT_ARG.findall(translation.text)
assert sorted(source_args) == sorted(translation_args), \
f"Ensure format arguments are consistent: `{source_text}` vs. `{translation.text}`"
def test_no_locations(self):
for line in self._read_translation_file(TRANSLATIONS_DIR, self.file).splitlines():
assert not line.strip().startswith(LOCATION_TAG), \
f"Line contains location tag: {line.strip()}, remove all line numbers."
def test_entities_error(self):
cur_translations = self._read_translation_file(TRANSLATIONS_DIR, self.file)
matches = re.findall(r'@(\w+);', cur_translations)
assert len(matches) == 0, f"The string(s) {matches} were found with '@' instead of '&'"
def test_bad_language(self):
IGNORED_WORDS = {'pédale'}
match = re.search(r'([a-zA-Z]{2,3})', self.file)
assert match, f"{self.name} - could not parse language"
try:
response = requests.get(
f"https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/{match.group(1)}"
)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
if e.response is not None and e.response.status_code == 429:
pytest.skip("word list rate limited")
raise
banned_words = {line.strip() for line in response.text.splitlines()}
for context in ET.parse(os.path.join(TRANSLATIONS_DIR, f"{self.file}.ts")).getroot():
for message in context.iterfind("message"):
translation = message.find("translation")
if translation.get("type") == "unfinished":
continue
translation_text = " ".join([t.text for t in translation.findall("numerusform")]) if message.get("numerus") == "yes" else translation.text
if not translation_text:
continue
words = set(translation_text.translate(str.maketrans('', '', string.punctuation + '%n')).lower().split())
bad_words_found = words & (banned_words - IGNORED_WORDS)
assert not bad_words_found, f"Bad language found in {self.name}: '{translation_text}'. Bad word(s): {', '.join(bad_words_found)}"

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
import argparse
import os
import pathlib
import subprocess
import shutil
import sys
import time
import importlib
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.test.helpers import with_processes
TEST_DIR = pathlib.Path(__file__).parent
OUTPUT_DIR = TEST_DIR / "nav_demo_report"
UI_DELAY = float(os.getenv("IQPILOT_NAV_DEMO_UI_DELAY", "0.75"))
VERSION = "0.10.1 / nav-ui-demo / 7864838 / Mar 09"
SCENE_SETTLE_S = float(os.getenv("IQPILOT_NAV_DEMO_SCENE_SETTLE_S", "0.04"))
SEED_REPEATS = int(os.getenv("IQPILOT_NAV_DEMO_SEED_REPEATS", "2"))
SEED_DELAY_S = float(os.getenv("IQPILOT_NAV_DEMO_SEED_DELAY_S", "0.02"))
SEED_REFRESH_EVERY = int(os.getenv("IQPILOT_NAV_DEMO_SEED_REFRESH_EVERY", "24"))
NAV_REPEATS = int(os.getenv("IQPILOT_NAV_DEMO_NAV_REPEATS", "2"))
NAV_DELAY_S = float(os.getenv("IQPILOT_NAV_DEMO_NAV_DELAY_S", "0.02"))
SEED_PUBLISH_DURATION_S = SEED_REPEATS * SEED_DELAY_S
NAV_SCENE_DURATION_S = NAV_REPEATS * NAV_DELAY_S
NAV_SCENES = ()
NAV_TIMELINE = ()
build_ui_pubmaster = None
publish_nav_scene = None
publish_onroad_seed = None
seed_ui_test_params = None
class NavDemoCapture:
def __init__(self, output_dir: pathlib.Path):
os.environ["SCALE"] = os.getenv("SCALE", "1")
os.environ["BIG"] = "1"
os.environ["RECORD"] = "1"
os.environ["RECORD_OUTPUT"] = str(output_dir / "nav_demo")
sys.modules["mouseinfo"] = False
self.output_dir = output_dir
self.pm = build_ui_pubmaster()
self.frames = []
self._image_lib = None
self.video_path = self.output_dir / "nav_demo.mp4"
def _load_image_lib(self):
if self._image_lib is not None:
return self._image_lib
try:
self._image_lib = importlib.import_module("PIL.Image")
except ModuleNotFoundError:
self._image_lib = None
return self._image_lib
def setup(self):
publish_onroad_seed(self.pm)
time.sleep(UI_DELAY)
@with_processes(["ui"])
def run(self):
self.setup()
for idx, scene in enumerate(NAV_TIMELINE):
if idx % SEED_REFRESH_EVERY == 0:
publish_onroad_seed(self.pm, repeats=SEED_REPEATS, delay=SEED_DELAY_S)
publish_nav_scene(self.pm, scene, repeats=NAV_REPEATS, delay=NAV_DELAY_S)
time.sleep(SCENE_SETTLE_S)
def extract_video_stills(self) -> list[pathlib.Path]:
if not self.video_path.exists():
return []
extracted = []
current_ts = UI_DELAY
for idx, scene in enumerate(NAV_TIMELINE):
if idx % SEED_REFRESH_EVERY == 0:
current_ts += SEED_PUBLISH_DURATION_S
capture_ts = current_ts + (NAV_SCENE_DURATION_S * 0.6)
capture_name = scene.get("capture_name")
if capture_name:
output_path = self.output_dir / f"{capture_name}.png"
subprocess.run(
[
"ffmpeg",
"-y",
"-loglevel", "error",
"-ss", f"{capture_ts:.2f}",
"-i", str(self.video_path),
"-frames:v", "1",
str(output_path),
],
check=True,
)
extracted.append(output_path)
current_ts += NAV_SCENE_DURATION_S + SCENE_SETTLE_S
return extracted
def write_gif(self) -> pathlib.Path | None:
if not self.frames:
for scene in NAV_SCENES:
image_path = self.output_dir / f"{scene['name']}.png"
if image_path.exists():
self.frames.append(self._load_image_lib().open(image_path).copy())
if not self.frames:
return None
if self._load_image_lib() is None:
return None
gif_path = self.output_dir / "nav_demo.gif"
self.frames[0].save(
gif_path,
save_all=True,
append_images=self.frames[1:],
duration=900,
loop=0,
)
return gif_path
def main():
parser = argparse.ArgumentParser(description="Run BIG raylib UI with a hard-coded nav route and capture screenshots/GIF.")
parser.add_argument("--output-dir", type=pathlib.Path, default=OUTPUT_DIR, help="Directory for screenshots and gif.")
parser.add_argument("--no-gif", action="store_true", help="Skip animated GIF creation.")
parser.add_argument("--mapbox-token", default="", help="Mapbox token for the demo prefix. Falls back to MAPBOX_TOKEN if omitted.")
parser.add_argument("--no-mapbox", action="store_true", help="Disable Mapbox for the demo and use only cached/offline tiles.")
parser.add_argument("--force-local-offline", action="store_true", help="Bypass both live and cached Mapbox so only the local offline provider can render.")
parser.add_argument("--offline-mbtiles", type=pathlib.Path, default=None, help="Path to a local raster MBTiles file for offline rendering.")
parser.add_argument("--offline-tile-root", type=pathlib.Path, default=None, help="Path to a local XYZ raster tile directory for offline rendering.")
parser.add_argument("--fixture", type=pathlib.Path, default=None, help="Route fixture JSON to drive the nav demo.")
parser.add_argument("--osmaps-mode", choices=("online", "offline", "both"), default=None,
help="Set the OnlineOSMaps/OfflineOSMaps params (the production source selection) for the demo prefix.")
args = parser.parse_args()
if args.output_dir.exists():
shutil.rmtree(args.output_dir)
args.output_dir.mkdir(parents=True)
mapbox_token = args.mapbox_token
if not mapbox_token and not args.no_mapbox:
existing = Params().get("MapboxToken")
if isinstance(existing, bytes):
existing = existing.decode("utf-8")
mapbox_token = existing or ""
with OpenpilotPrefix():
if args.fixture is not None:
os.environ["IQPILOT_NAV_DEMO_FIXTURE"] = str(args.fixture)
if args.force_local_offline:
os.environ["IQPILOT_DISABLE_MAPBOX_PROVIDER"] = "1"
os.environ["IQPILOT_DISABLE_MAPBOX_CACHE"] = "1"
elif args.no_mapbox:
os.environ.pop("IQPILOT_DISABLE_MAPBOX_PROVIDER", None)
os.environ.pop("IQPILOT_DISABLE_MAPBOX_CACHE", None)
if args.offline_mbtiles is not None:
os.environ["IQPILOT_OFFLINE_MBTILES"] = str(args.offline_mbtiles)
if args.offline_tile_root is not None:
os.environ["IQPILOT_OFFLINE_TILE_ROOT"] = str(args.offline_tile_root)
global NAV_SCENES, NAV_TIMELINE, build_ui_pubmaster, publish_nav_scene, publish_onroad_seed, seed_ui_test_params
nav_demo_common = importlib.import_module("openpilot.selfdrive.ui.tests.test_ui.nav_demo_common")
NAV_SCENES = nav_demo_common.NAV_SCENES
NAV_TIMELINE = nav_demo_common.NAV_TIMELINE
build_ui_pubmaster = nav_demo_common.build_ui_pubmaster
publish_nav_scene = nav_demo_common.publish_nav_scene
publish_onroad_seed = nav_demo_common.publish_onroad_seed
seed_ui_test_params = nav_demo_common.seed_ui_test_params
seed_ui_test_params(Params(), VERSION, mapbox_token=mapbox_token)
if args.osmaps_mode is not None:
demo_params = Params()
demo_params.put_bool("OnlineOSMaps", args.osmaps_mode in ("online", "both"))
demo_params.put_bool("OfflineOSMaps", args.osmaps_mode in ("offline", "both"))
demo = NavDemoCapture(args.output_dir)
demo.run()
demo.extract_video_stills()
gif_path = None if args.no_gif else demo.write_gif()
print(f"Screenshots written to: {args.output_dir}")
if demo.video_path.exists():
print(f"Recorded preview written to: {demo.video_path}")
if gif_path is not None:
print(f"Animated preview written to: {gif_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,705 @@
#!/usr/bin/env python3
import json
import os
import time
from math import atan2, cos, radians, sqrt
from pathlib import Path
from cereal import car, custom, log, messaging
from cereal.messaging import PubMaster
from openpilot.common.params import Params
from openpilot.system.hardware import HARDWARE
from openpilot.system.updated.updated import parse_release_notes
from openpilot.system.version import terms_version, training_version
from openpilot.common.basedir import BASEDIR
DEFAULT_DEMO_FIXTURE_PATH = Path(__file__).with_name("nav_demo_fixture_bolingbrook_mapbox.json")
DEMO_ROUTE_STEP_M_MIN = 12.0
TARGET_TIMELINE_SCENES = 180
def _load_demo_fixture() -> dict:
fixture_path = Path(os.getenv("IQPILOT_NAV_DEMO_FIXTURE", str(DEFAULT_DEMO_FIXTURE_PATH)))
return json.loads(fixture_path.read_text())
def _lerp(a: float, b: float, t: float) -> float:
return a + (b - a) * t
def _segment_length_m(a: tuple[float, float], b: tuple[float, float]) -> float:
lat_scale = 111_320.0
lon_scale = 111_320.0 * cos(radians((a[0] + b[0]) * 0.5))
dx = (b[1] - a[1]) * lon_scale
dy = (b[0] - a[0]) * lat_scale
return sqrt(dx * dx + dy * dy)
def _route_length_m(points: list[tuple[float, float]]) -> float:
return sum(_segment_length_m(points[idx], points[idx + 1]) for idx in range(len(points) - 1))
def _cumulative_distances(points: list[tuple[float, float]]) -> list[float]:
out = [0.0]
for idx in range(len(points) - 1):
out.append(out[-1] + _segment_length_m(points[idx], points[idx + 1]))
return out
def _interpolate_along(points: list[tuple[float, float]], distance_m: float) -> tuple[float, float]:
if len(points) < 2:
return points[0]
remaining = max(distance_m, 0.0)
for idx in range(len(points) - 1):
a, b = points[idx], points[idx + 1]
seg_len = _segment_length_m(a, b)
if remaining <= seg_len:
t = 0.0 if seg_len < 1e-3 else remaining / seg_len
return _lerp(a[0], b[0], t), _lerp(a[1], b[1], t)
remaining -= seg_len
return points[-1]
def _bearing_between(a: tuple[float, float], b: tuple[float, float]) -> float:
lon_scale = cos(radians((a[0] + b[0]) * 0.5))
dx = (b[1] - a[1]) * lon_scale
dy = b[0] - a[0]
return (90.0 - (180.0 / 3.141592653589793) * atan2(dy, dx)) % 360.0
def _slice_route_ahead(points: list[tuple[float, float]], current_idx: int, lookbehind: int = 1) -> list[tuple[float, float]]:
start = max(current_idx - lookbehind, 0)
return points[start:]
def _decode_polyline6(polyline: str) -> list[tuple[float, float]]:
if not polyline:
return []
points = []
index = 0
lat = 0
lon = 0
while index < len(polyline):
result = 0
shift = 0
while True:
byte = ord(polyline[index]) - 63
index += 1
result |= (byte & 0x1F) << shift
shift += 5
if byte < 0x20:
break
lat += ~(result >> 1) if result & 1 else (result >> 1)
result = 0
shift = 0
while True:
byte = ord(polyline[index]) - 63
index += 1
result |= (byte & 0x1F) << shift
shift += 5
if byte < 0x20:
break
lon += ~(result >> 1) if result & 1 else (result >> 1)
points.append((lat / 1_000_000.0, lon / 1_000_000.0))
return points
def _modifier_to_direction(modifier: str | None) -> int:
modifier = (modifier or "").lower()
if "left" in modifier:
return custom.NavDirection.left
if "right" in modifier:
return custom.NavDirection.right
return custom.NavDirection.none
def _modifier_to_turn_direction(modifier: str | None) -> int:
modifier = (modifier or "").lower()
if "left" in modifier:
return custom.IQTurnSignalDirection.turnLeft
if "right" in modifier:
return custom.IQTurnSignalDirection.turnRight
return custom.IQTurnSignalDirection.none
def _valhalla_modifier_from_type(type_code: int) -> str:
mapping = {
9: "slight_right",
10: "right",
11: "sharp_right",
12: "uturn_right",
13: "uturn_left",
14: "sharp_left",
15: "left",
16: "slight_left",
17: "straight",
18: "right",
19: "left",
20: "straight",
21: "roundabout",
22: "roundabout",
24: "right",
25: "left",
26: "straight",
27: "straight",
31: "straight",
32: "right",
33: "left",
36: "straight",
}
return mapping.get(type_code, "straight")
def _normalize_mapbox_fixture(data: dict) -> dict:
route = data["routes"][0]
leg = route["legs"][0]
points = [(lat, lon) for lon, lat in route["geometry"]["coordinates"]]
waypoint_entries = data.get("waypoints", [{}, {}])
start_waypoint = waypoint_entries[0] if waypoint_entries else {}
destination_location = data.get("waypoints", [{}, {}])[-1].get("location", route["geometry"]["coordinates"][-1])
destination = (float(destination_location[1]), float(destination_location[0]))
steps = []
previous_name = os.getenv("IQPILOT_NAV_DEMO_START_NAME", "185 Brandon Ct")
for step in leg["steps"]:
maneuver = step["maneuver"]
name = step["name"] or previous_name
if step["name"]:
previous_name = step["name"]
steps.append({
"name": name,
"banner_name": step["name"],
"location": (float(maneuver["location"][1]), float(maneuver["location"][0])),
"raw_type": maneuver.get("type", "none"),
"modifier": maneuver.get("modifier", "straight") or "straight",
"description": maneuver.get("instruction") or name or "Continue",
"distance": float(step["distance"]),
"duration": float(step["duration"]),
})
return {
"provider": "mapbox",
"route_points": points,
"duration_s": float(route["duration"]),
"distance_m": float(route["distance"]),
"steps": steps,
"start_name": os.getenv("IQPILOT_NAV_DEMO_START_NAME", start_waypoint.get("name") or "Start"),
"destination_name": os.getenv("IQPILOT_NAV_DEMO_DESTINATION_NAME", waypoint_entries[-1].get("name") or "Destination"),
"destination": destination,
}
def _normalize_valhalla_fixture(data: dict) -> dict:
trip = data["trip"]
leg = trip["legs"][0]
points = _decode_polyline6(leg["shape"])
start_loc = trip["locations"][0]
destination_loc = trip["locations"][-1]
destination = (float(destination_loc["lat"]), float(destination_loc["lon"]))
steps = []
previous_name = os.getenv("IQPILOT_NAV_DEMO_START_NAME", "185 Brandon Ct")
for maneuver in leg["maneuvers"]:
type_code = int(maneuver.get("type", 8) or 8)
modifier = _valhalla_modifier_from_type(type_code)
begin_idx = min(max(int(maneuver.get("begin_shape_index", 0) or 0), 0), len(points) - 1)
street_names = maneuver.get("street_names") or []
banner_name = street_names[0] if street_names else ""
name = banner_name or previous_name
if banner_name:
previous_name = banner_name
steps.append({
"name": name,
"banner_name": banner_name,
"location": points[begin_idx],
"raw_type": f"valhalla:{type_code}",
"modifier": modifier,
"description": maneuver.get("instruction") or name or "Continue",
"distance": float(maneuver.get("length", 0.0) or 0.0) * 1000.0,
"duration": float(maneuver.get("time", 0.0) or 0.0),
"type_code": type_code,
})
return {
"provider": "valhalla",
"route_points": points,
"duration_s": float(trip["summary"]["time"]),
"distance_m": float(trip["summary"]["length"]) * 1000.0,
"steps": steps,
"start_name": os.getenv("IQPILOT_NAV_DEMO_START_NAME", start_loc.get("name") or "Start"),
"destination_name": os.getenv("IQPILOT_NAV_DEMO_DESTINATION_NAME", destination_loc.get("name") or "Destination"),
"destination": destination,
}
def _normalize_demo_fixture(data: dict) -> dict:
if "routes" in data:
return _normalize_mapbox_fixture(data)
if "trip" in data:
return _normalize_valhalla_fixture(data)
raise ValueError("Unsupported nav demo fixture format")
def _map_step_type(step_type: str, modifier: str | None, type_code: int | None = None) -> int:
if type_code is not None:
if type_code in {4, 5, 6}:
return custom.IQNavState.ManeuverType.arrive
if type_code in {21, 22, 23, 37}:
return custom.IQNavState.ManeuverType.roundabout
if type_code in {18, 19, 24, 25}:
return custom.IQNavState.ManeuverType.exit
if type_code in {17, 20, 26, 27}:
return custom.IQNavState.ManeuverType.merge
if type_code in {1, 2, 3, 7, 8, 31, 36}:
return custom.IQNavState.ManeuverType.continueStraight
return custom.IQNavState.ManeuverType.turn
step_type = step_type or "none"
modifier = (modifier or "").lower()
if step_type == "arrive":
return custom.IQNavState.ManeuverType.arrive
if step_type in {"off ramp"}:
return custom.IQNavState.ManeuverType.exit
if step_type in {"merge", "on ramp"}:
return custom.IQNavState.ManeuverType.merge
if step_type in {"fork"}:
return custom.IQNavState.ManeuverType.fork
if step_type in {"roundabout", "rotary", "roundabout turn"}:
return custom.IQNavState.ManeuverType.roundabout
if step_type in {"continue", "new name", "depart", "notification"}:
return custom.IQNavState.ManeuverType.continueStraight
if step_type in {"turn", "end of road"}:
return custom.IQNavState.ManeuverType.turn
if modifier == "straight":
return custom.IQNavState.ManeuverType.continueStraight
return custom.IQNavState.ManeuverType.turn
def _nearest_route_index(points: list[tuple[float, float]], target: tuple[float, float]) -> int:
best_idx = 0
best_distance = None
for idx, point in enumerate(points):
d = _segment_length_m(point, target)
if best_distance is None or d < best_distance:
best_distance = d
best_idx = idx
return best_idx
DEMO_FIXTURE = _load_demo_fixture()
DEMO_CONTEXT = _normalize_demo_fixture(DEMO_FIXTURE)
DEMO_START_NAME = DEMO_CONTEXT["start_name"]
DEMO_DESTINATION_NAME = DEMO_CONTEXT["destination_name"]
DEMO_DESTINATION = DEMO_CONTEXT["destination"]
DEMO_ROUTE_POINTS = DEMO_CONTEXT["route_points"]
ROUTE_DISTANCES = _cumulative_distances(DEMO_ROUTE_POINTS)
ROUTE_TOTAL_DISTANCE_M = ROUTE_DISTANCES[-1]
DEMO_DURATION_S = float(DEMO_CONTEXT["duration_s"])
DEMO_ROUTE_STEP_M = max(DEMO_ROUTE_STEP_M_MIN, ROUTE_TOTAL_DISTANCE_M / TARGET_TIMELINE_SCENES)
DEMO_STEPS = []
previous_name = DEMO_START_NAME
for idx, step in enumerate(DEMO_CONTEXT["steps"]):
maneuver_location = step["location"]
route_index = _nearest_route_index(DEMO_ROUTE_POINTS, maneuver_location)
route_distance = ROUTE_DISTANCES[route_index]
step_name = step["name"] or previous_name
if step["name"]:
previous_name = step["name"]
step_type = _map_step_type(step.get("raw_type", "none"), step.get("modifier"), step.get("type_code"))
direction = _modifier_to_direction(step.get("modifier"))
step_speed = max(float(step["distance"]) / max(float(step["duration"]), 1.0), 3.5)
DEMO_STEPS.append({
"index": idx,
"name": step_name,
"banner_name": step.get("banner_name", ""),
"route_index": route_index,
"route_distance": route_distance,
"type": step_type,
"raw_type": step.get("raw_type", "none"),
"modifier": step.get("modifier", "straight") or "straight",
"direction": direction,
"description": step.get("description") or step_name or "Continue",
"distance": float(step["distance"]),
"duration": float(step["duration"]),
"speed": step_speed,
"location": maneuver_location,
})
DEMO_NAV_STEPS = tuple(step for step in DEMO_STEPS if step["raw_type"] not in {"depart", "valhalla:1"})
DEMO_DESTINATION_ROUTE_POINT = DEMO_STEPS[-1]["location"]
def _find_route_index_for_distance(distance_m: float) -> int:
for idx, route_distance in enumerate(ROUTE_DISTANCES):
if route_distance >= distance_m:
return idx
return len(ROUTE_DISTANCES) - 1
def _find_upcoming_steps(distance_m: float) -> tuple[dict | None, dict | None]:
upcoming = [step for step in DEMO_NAV_STEPS if step["route_distance"] > distance_m + 1e-3]
first = upcoming[0] if upcoming else None
second = upcoming[1] if len(upcoming) > 1 else None
return first, second
def _current_road_name(distance_m: float) -> str:
current = DEMO_START_NAME
for step in DEMO_STEPS:
if step["route_distance"] <= distance_m and step["banner_name"]:
current = step["banner_name"]
return current
def _phase_for_step(step: dict | None, distance_to_next: float) -> int:
if step is None or step["type"] == custom.IQNavState.ManeuverType.arrive:
return custom.IQNavState.ManeuverPhase.none
if step["type"] in (custom.IQNavState.ManeuverType.exit, custom.IQNavState.ManeuverType.merge, custom.IQNavState.ManeuverType.fork):
if distance_to_next <= 90.0:
return custom.IQNavState.ManeuverPhase.highwayCommit
if distance_to_next <= 260.0:
return custom.IQNavState.ManeuverPhase.highwayPrepare
return custom.IQNavState.ManeuverPhase.none
if distance_to_next <= 45.0:
return custom.IQNavState.ManeuverPhase.turnActive
if distance_to_next <= 180.0:
return custom.IQNavState.ManeuverPhase.turnPrepare
return custom.IQNavState.ManeuverPhase.none
def _zoom_for_distance(distance_to_next: float, maneuver_type: int) -> float:
if maneuver_type == custom.IQNavState.ManeuverType.arrive:
return 17.2
if maneuver_type in (custom.IQNavState.ManeuverType.exit, custom.IQNavState.ManeuverType.merge, custom.IQNavState.ManeuverType.fork):
if distance_to_next <= 120.0:
return 16.9
return 16.3
if distance_to_next <= 70.0:
return 17.1
if distance_to_next <= 160.0:
return 16.8
return 16.4
def _speed_target_for_step(current_step: dict | None, next_step: dict | None, distance_to_next: float) -> float:
current_speed = current_step["speed"] if current_step is not None else 12.0
next_speed = next_step["speed"] if next_step is not None else current_speed
if next_step is not None and next_step["type"] == custom.IQNavState.ManeuverType.arrive:
if distance_to_next <= 25.0:
return 3.0
if distance_to_next <= 80.0:
return 4.5
if distance_to_next <= 35.0:
return max(next_speed * 0.85, 4.5)
if distance_to_next <= 140.0:
return max(min(current_speed, next_speed + 1.5), 6.0)
return max(current_speed, 7.0)
def _capture_distance_targets() -> dict[str, float]:
captures = {}
selected_steps = [step for step in DEMO_NAV_STEPS if step["type"] != custom.IQNavState.ManeuverType.continueStraight]
for idx, step in enumerate(selected_steps[:4], start=1):
captures[f"nav_step_{idx:02d}"] = max(step["route_distance"] - min(120.0, max(step["distance"] * 0.35, 45.0)), 0.0)
captures["nav_arrival"] = max(DEMO_NAV_STEPS[-1]["route_distance"] - 35.0, 0.0)
return captures
def _timeline_distances() -> list[float]:
base = [idx * DEMO_ROUTE_STEP_M for idx in range(int(ROUTE_TOTAL_DISTANCE_M // DEMO_ROUTE_STEP_M) + 1)]
points = set(base)
for step in DEMO_NAV_STEPS:
for offset in (260.0, 180.0, 120.0, 80.0, 50.0, 25.0):
if step["type"] == custom.IQNavState.ManeuverType.arrive and offset > 120.0:
continue
points.add(max(step["route_distance"] - offset, 0.0))
points.add(ROUTE_TOTAL_DISTANCE_M - 15.0)
points.add(ROUTE_TOTAL_DISTANCE_M - 5.0)
return sorted(d for d in points if 0.0 <= d <= ROUTE_TOTAL_DISTANCE_M)
def _make_timeline_scene(distance_m: float) -> dict:
current_lat, current_lon = _interpolate_along(DEMO_ROUTE_POINTS, distance_m)
next_lat, next_lon = _interpolate_along(DEMO_ROUTE_POINTS, min(distance_m + 18.0, ROUTE_TOTAL_DISTANCE_M))
route_idx = _find_route_index_for_distance(distance_m)
bearing = _bearing_between((current_lat, current_lon), (next_lat, next_lon))
current_step_idx = max(0, max((idx for idx, step in enumerate(DEMO_STEPS) if step["route_distance"] <= distance_m), default=0))
current_step = DEMO_STEPS[current_step_idx]
next_step, second_step = _find_upcoming_steps(distance_m)
if next_step is None:
next_step = DEMO_NAV_STEPS[-1]
next_distance = max(next_step["route_distance"] - distance_m, 0.0)
remaining_distance = max(ROUTE_TOTAL_DISTANCE_M - distance_m, 0.0)
remaining_time = max(DEMO_DURATION_S * (remaining_distance / max(ROUTE_TOTAL_DISTANCE_M, 1.0)), 10.0)
scene = {
"name": f"nav_timeline_{int(distance_m):04d}",
"capture_name": "",
"road_name": _current_road_name(distance_m),
"distance_m": next_distance,
"time_remaining": remaining_time,
"distance_remaining": remaining_distance,
"speed_limit": max(current_step["speed"], 8.0),
"speed_limit_ahead": max(next_step["speed"], 6.0),
"speed_limit_ahead_distance": max(min(next_distance, 220.0), 0.0),
"phase": _phase_for_step(next_step, next_distance),
"direction": next_step["direction"],
"next_type": next_step["type"],
"next_modifier": next_step["modifier"],
"next_description": next_step["description"],
"second_type": second_step["type"] if second_step is not None else custom.IQNavState.ManeuverType.arrive,
"second_direction": second_step["direction"] if second_step is not None else custom.NavDirection.none,
"second_modifier": second_step["modifier"] if second_step is not None else "straight",
"second_distance": max(second_step["route_distance"] - distance_m, 0.0) if second_step is not None else 0.0,
"second_valid": second_step is not None,
"provider": custom.IQNavState.LongitudinalProvider.route,
"route_speed_target": _speed_target_for_step(current_step, next_step, next_distance),
"current_latitude": current_lat,
"current_longitude": current_lon,
"bearing_deg": bearing,
"zoom_hint": _zoom_for_distance(next_distance, next_step["type"]),
"destination_latitude": DEMO_DESTINATION[0],
"destination_longitude": DEMO_DESTINATION[1],
"destination_name": DEMO_DESTINATION_NAME,
"route_points": _slice_route_ahead(DEMO_ROUTE_POINTS, route_idx, lookbehind=1),
"next_maneuver_latitude": next_step["location"][0],
"next_maneuver_longitude": next_step["location"][1],
}
return scene
CAPTURE_TARGETS = _capture_distance_targets()
_timeline_scenes = [_make_timeline_scene(distance_m) for distance_m in _timeline_distances()]
for capture_name, target_distance in CAPTURE_TARGETS.items():
best_scene = min(
_timeline_scenes,
key=lambda scene: abs((ROUTE_TOTAL_DISTANCE_M - scene["distance_remaining"]) - target_distance),
)
best_scene["capture_name"] = capture_name
NAV_TIMELINE = tuple(_timeline_scenes)
NAV_SCENES = tuple(scene for scene in NAV_TIMELINE if scene.get("capture_name"))
def seed_ui_test_params(params: Params, version: str, mapbox_token: str = "") -> None:
params.put("DongleId", "123456789012345")
params.put("UpdaterCurrentDescription", version)
params.put("UpdaterNewDescription", version)
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put_bool("OnScreenNavigation", True)
cp = car.CarParams(notCar=True, wheelbase=2.7, steerRatio=15.0)
cp.openpilotLongitudinalControl = True
cp_bytes = cp.to_bytes()
params.put("CarParamsPersistent", cp_bytes)
params.put("CarParams", cp_bytes)
token = mapbox_token or os.getenv("MAPBOX_TOKEN", "")
if token:
params.put("MapboxToken", token)
def build_ui_pubmaster() -> PubMaster:
return PubMaster([
"deviceState",
"pandaStates",
"driverStateV2",
"selfdriveState",
"carState",
"carControl",
"controlsState",
"iqPlan",
"iqLiveData",
"iqNavState",
"iqNavRenderState",
"gpsLocationExternal",
])
def publish_onroad_seed(pm: PubMaster, repeats: int = 8, delay: float = 0.05) -> None:
device_state = messaging.new_message("deviceState")
device_state.deviceState.started = True
device_state.deviceState.networkType = log.DeviceState.NetworkType.wifi
device_state.deviceState.deviceType = HARDWARE.get_device_type()
panda_states = messaging.new_message("pandaStates", 1)
panda_states.pandaStates[0].pandaType = log.PandaState.PandaType.dos
panda_states.pandaStates[0].ignitionLine = True
driver_state = messaging.new_message("driverStateV2")
driver_state.driverStateV2.leftDriverData.faceOrientation = [0.0, 0.0, 0.0]
selfdrive_state = messaging.new_message("selfdriveState")
selfdrive_state.selfdriveState.enabled = True
selfdrive_state.selfdriveState.state = log.SelfdriveState.OpenpilotState.enabled
car_state = messaging.new_message("carState")
car_state.carState.vEgo = 22.0
car_state.carState.aEgo = -0.2
car_state.carState.vCruise = 72.0
car_state.carState.vCruiseCluster = 72.0
car_control = messaging.new_message("carControl")
car_control.carControl.enabled = True
car_control.carControl.latActive = True
car_control.carControl.cruiseControl.override = False
controls_state = messaging.new_message("controlsState")
controls_state.controlsState.vCruiseDEPRECATED = 72.0
controls_state.controlsState.vCruiseClusterDEPRECATED = 72.0
controls_state.controlsState.curvature = 0.0
gps = messaging.new_message("gpsLocationExternal")
gps.gpsLocationExternal.flags = 1
gps.gpsLocationExternal.hasFix = True
gps.gpsLocationExternal.verticalAccuracy = 1.0
gps.gpsLocationExternal.speedAccuracy = 0.5
gps.gpsLocationExternal.bearingAccuracyDeg = 1.0
gps.gpsLocationExternal.vNED = [0.0, 0.0, 0.0]
gps.gpsLocationExternal.latitude = DEMO_ROUTE_POINTS[0][0]
gps.gpsLocationExternal.longitude = DEMO_ROUTE_POINTS[0][1]
gps.gpsLocationExternal.altitude = 181.0
gps.gpsLocationExternal.speed = 22.0
gps.gpsLocationExternal.bearingDeg = _bearing_between(DEMO_ROUTE_POINTS[0], DEMO_ROUTE_POINTS[1])
gps.gpsLocationExternal.unixTimestampMillis = int(time.time() * 1000)
for _ in range(repeats):
pm.send("deviceState", device_state)
pm.send("pandaStates", panda_states)
pm.send("driverStateV2", driver_state)
pm.send("selfdriveState", selfdrive_state)
pm.send("carState", car_state)
pm.send("carControl", car_control)
pm.send("controlsState", controls_state)
pm.send("gpsLocationExternal", gps)
time.sleep(delay)
def publish_nav_scene(pm: PubMaster, scene: dict, repeats: int = 6, delay: float = 0.05) -> None:
iq_plan = messaging.new_message("iqPlan")
iq_plan.iqPlan.longitudinalPlanSource = custom.IQPlan.LongitudinalPlanSource.nav
iq_plan.iqPlan.vTarget = float(scene["route_speed_target"])
iq_plan.iqPlan.aTarget = -0.7
resolver = iq_plan.iqPlan.speedLimit.resolver
resolver.speedLimit = float(scene["speed_limit"])
resolver.speedLimitLast = float(scene["speed_limit"])
resolver.speedLimitFinal = float(scene["speed_limit"])
resolver.speedLimitFinalLast = float(scene["speed_limit"])
resolver.speedLimitValid = True
resolver.speedLimitLastValid = True
resolver.speedLimitOffset = 0.0
resolver.distToSpeedLimit = 0.0
resolver.source = custom.IQPlan.SpeedLimit.Source.map
assist = iq_plan.iqPlan.speedLimit.assist
assist.enabled = False
assist.active = False
assist.state = custom.IQPlan.SpeedLimit.AssistState.disabled
assist.vTarget = 255.0
assist.aTarget = 0.0
nav_summary = iq_plan.iqPlan.iqNavState.nav
nav_summary.engaged = True
nav_summary.provider = scene["provider"]
nav_summary.state = custom.IQNavState.LongitudinalState.active
nav_summary.speedTarget = float(scene["route_speed_target"])
nav_summary.accelTarget = -0.7
nav_summary.valid = True
iq_live_data = messaging.new_message("iqLiveData")
iq_live_data.iqLiveData.speedLimitValid = True
iq_live_data.iqLiveData.speedLimit = float(scene["speed_limit"])
iq_live_data.iqLiveData.speedLimitAheadValid = True
iq_live_data.iqLiveData.speedLimitAhead = float(scene["speed_limit_ahead"])
iq_live_data.iqLiveData.speedLimitAheadDistance = float(scene["speed_limit_ahead_distance"])
iq_live_data.iqLiveData.roadName = scene["road_name"]
iq_nav_state = messaging.new_message("iqNavState")
nav_state = iq_nav_state.iqNavState
nav_state.active = True
nav_state.destinationValid = True
nav_state.destinationLatitude = float(scene["destination_latitude"])
nav_state.destinationLongitude = float(scene["destination_longitude"])
nav_state.destinationName = scene.get("destination_name", "Navigation destination")
nav_state.distanceRemaining = float(scene["distance_remaining"])
nav_state.timeRemaining = float(scene["time_remaining"])
nav_state.nextManeuverValid = True
nav_state.nextManeuverDistance = float(scene["distance_m"])
nav_state.nextManeuverType = scene["next_type"]
nav_state.nextManeuverDirection = scene["direction"]
nav_state.nextManeuverModifier = scene["next_modifier"]
nav_state.nextManeuverDescription = scene["next_description"]
nav_state.secondNextManeuverValid = scene.get("second_valid", True)
nav_state.secondNextManeuverType = scene["second_type"]
nav_state.secondNextManeuverDirection = scene["second_direction"]
nav_state.secondNextManeuverDistance = float(scene["second_distance"])
nav_state.secondNextManeuverModifier = scene.get("second_modifier", "")
nav_state.longitudinalProvider = scene["provider"]
nav_state.longitudinalState = custom.IQNavState.LongitudinalState.active
nav_state.longitudinalEngaged = True
nav_state.speedTarget = float(scene["route_speed_target"])
nav_state.accelTarget = -0.7
nav_state.valid = True
nav_state.targetSpeed = float(scene["route_speed_target"])
nav_state.targetSpeedValid = True
nav_state.maneuverPhase = scene["phase"]
nav_state.maneuverDirection = scene["direction"]
nav_state.navSpeedTargetActive = True
if scene["phase"] in (custom.IQNavState.ManeuverPhase.highwayPrepare, custom.IQNavState.ManeuverPhase.highwayCommit):
nav_state.shouldSendLanePositioning = True
nav_state.lanePositioningDirection = custom.IQTurnSignalDirection.turnRight if scene["direction"] == custom.NavDirection.right else custom.IQTurnSignalDirection.turnLeft
nav_state.command = custom.IQNavState.Command.laneChange
nav_state.commandDirection = scene["direction"]
nav_state.commandIndex = 1
elif scene["phase"] in (custom.IQNavState.ManeuverPhase.turnPrepare, custom.IQNavState.ManeuverPhase.turnActive):
nav_state.shouldSendTurnDesire = True
nav_state.turnDesireDirection = custom.IQTurnSignalDirection.turnLeft if scene["direction"] == custom.NavDirection.left else custom.IQTurnSignalDirection.turnRight
iq_nav_render = messaging.new_message("iqNavRenderState")
render_state = iq_nav_render.iqNavRenderState
render_state.active = True
render_state.currentLatitude = float(scene["current_latitude"])
render_state.currentLongitude = float(scene["current_longitude"])
render_state.bearingDeg = float(scene["bearing_deg"])
render_state.zoomHint = float(scene["zoom_hint"])
route_points = scene["route_points"]
render_state.init("routePolyline", len(route_points))
render_state.init("routePolylineSimplified", len(route_points))
for idx, (lat, lon) in enumerate(route_points):
render_state.routePolyline[idx].latitude = lat
render_state.routePolyline[idx].longitude = lon
render_state.routePolylineSimplified[idx].latitude = lat
render_state.routePolylineSimplified[idx].longitude = lon
render_state.nextManeuverLatitude = float(scene["next_maneuver_latitude"])
render_state.nextManeuverLongitude = float(scene["next_maneuver_longitude"])
render_state.nextManeuverType = scene["next_type"]
render_state.nextManeuverDirection = scene["direction"]
render_state.nextManeuverDistance = float(scene["distance_m"])
render_state.destinationLatitude = float(scene["destination_latitude"])
render_state.destinationLongitude = float(scene["destination_longitude"])
gps = messaging.new_message("gpsLocationExternal")
gps.gpsLocationExternal.flags = 1
gps.gpsLocationExternal.hasFix = True
gps.gpsLocationExternal.verticalAccuracy = 1.0
gps.gpsLocationExternal.speedAccuracy = 0.5
gps.gpsLocationExternal.bearingAccuracyDeg = 1.0
gps.gpsLocationExternal.vNED = [0.0, 0.0, 0.0]
gps.gpsLocationExternal.latitude = float(scene["current_latitude"])
gps.gpsLocationExternal.longitude = float(scene["current_longitude"])
gps.gpsLocationExternal.altitude = 181.0
gps.gpsLocationExternal.speed = max(float(scene["route_speed_target"]), 4.5)
gps.gpsLocationExternal.bearingDeg = float(scene["bearing_deg"])
gps.gpsLocationExternal.unixTimestampMillis = int(time.time() * 1000)
for _ in range(repeats):
pm.send("iqPlan", iq_plan)
pm.send("iqLiveData", iq_live_data)
pm.send("iqNavState", iq_nav_state)
pm.send("iqNavRenderState", iq_nav_render)
pm.send("gpsLocationExternal", gps)
time.sleep(delay)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,404 @@
{
"trip": {
"locations": [
{
"type": "break",
"lat": 41.690091,
"lon": -88.078806,
"name": "185 Brandon Ct, Bolingbrook, IL 60440",
"side_of_street": "left",
"original_index": 0
},
{
"type": "break",
"lat": 41.908129,
"lon": -88.115063,
"name": "505 E North Ave, Carol Stream, IL 60188",
"original_index": 1
}
],
"legs": [
{
"maneuvers": [
{
"type": 3,
"instruction": "Drive east on Cinnamon Court.",
"verbal_succinct_transition_instruction": "Drive east. Then, in 300 feet, Turn left to stay on Cinnamon Court.",
"verbal_pre_transition_instruction": "Drive east on Cinnamon Court. Then, in 300 feet, Turn left to stay on Cinnamon Court.",
"verbal_post_transition_instruction": "Continue for 300 feet.",
"street_names": [
"Cinnamon Court"
],
"bearing_after": 86,
"time": 9.98,
"length": 0.0515,
"cost": 15.202,
"begin_shape_index": 0,
"end_shape_index": 2,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left to stay on Cinnamon Court.",
"verbal_transition_alert_instruction": "Turn left to stay on Cinnamon Court.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left to stay on Cinnamon Court.",
"verbal_post_transition_instruction": "Continue for 400 feet.",
"street_names": [
"Cinnamon Court"
],
"bearing_before": 89,
"bearing_after": 354,
"time": 17.532,
"length": 0.0764,
"cost": 24.474,
"begin_shape_index": 2,
"end_shape_index": 5,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto Lily Cache Lane.",
"verbal_transition_alert_instruction": "Turn right onto Lily Cache Lane.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto Lily Cache Lane.",
"verbal_post_transition_instruction": "Continue for a half mile.",
"street_names": [
"Lily Cache Lane"
],
"bearing_before": 356,
"bearing_after": 88,
"time": 62.436,
"length": 0.5014,
"cost": 151.84,
"begin_shape_index": 5,
"end_shape_index": 18,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto South Bolingbrook Drive/IL 53.",
"verbal_transition_alert_instruction": "Turn right onto South Bolingbrook Drive.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto South Bolingbrook Drive, IL 53.",
"verbal_post_transition_instruction": "Continue for a half mile.",
"street_names": [
"South Bolingbrook Drive",
"IL 53"
],
"bearing_before": 89,
"bearing_after": 179,
"time": 52.006,
"length": 0.4473,
"cost": 129.587,
"begin_shape_index": 18,
"end_shape_index": 34,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 19,
"instruction": "Turn left to take the I 55 North ramp toward Chicago.",
"verbal_transition_alert_instruction": "Turn left to take the I 55 North ramp.",
"verbal_pre_transition_instruction": "Turn left to take the I 55 North ramp toward Chicago.",
"verbal_post_transition_instruction": "Continue for 1.5 miles.",
"street_names": [
"I 55 North",
"Adlai Stevenson Expressway"
],
"bearing_before": 177,
"bearing_after": 66,
"time": 99.799,
"length": 1.4676,
"cost": 180.0,
"begin_shape_index": 34,
"end_shape_index": 55,
"highway": true,
"sign": {
"exit_branch_elements": [
{
"text": "I 55 North",
"consecutive_count": 1
}
],
"exit_toward_elements": [
{
"text": "Chicago"
}
]
},
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 20,
"instruction": "Take exit 269 on the right onto I 355 Toll toward Northwest Suburbs.",
"verbal_transition_alert_instruction": "Take exit 269 on the right.",
"verbal_pre_transition_instruction": "Take exit 269 on the right onto I 355 Toll toward Northwest Suburbs.",
"bearing_before": 55,
"bearing_after": 60,
"time": 56.476,
"length": 0.5294,
"cost": 60.572,
"begin_shape_index": 55,
"end_shape_index": 63,
"toll": true,
"sign": {
"exit_number_elements": [
{
"text": "269"
}
],
"exit_branch_elements": [
{
"text": "I 355 Toll"
}
],
"exit_toward_elements": [
{
"text": "Northwest Suburbs",
"consecutive_count": 1
},
{
"text": "Southwest Suburbs"
}
]
},
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 24,
"instruction": "Keep left to take I 355 North toward Northwest Suburbs.",
"verbal_transition_alert_instruction": "Keep left to take I 355 North.",
"verbal_pre_transition_instruction": "Keep left to take I 355 North toward Northwest Suburbs.",
"verbal_post_transition_instruction": "Continue for 16 miles.",
"street_names": [
"I 355 North",
"Veterans Memorial Tollway"
],
"bearing_before": 58,
"bearing_after": 58,
"time": 940.064,
"length": 15.5504,
"cost": 1049.088,
"begin_shape_index": 63,
"end_shape_index": 353,
"toll": true,
"highway": true,
"sign": {
"exit_branch_elements": [
{
"text": "I 355 North",
"consecutive_count": 1
}
],
"exit_toward_elements": [
{
"text": "Northwest Suburbs",
"consecutive_count": 1
}
]
},
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 20,
"instruction": "Take exit 27 on the right onto IL 64 toward North Avenue.",
"verbal_transition_alert_instruction": "Take exit 27 on the right.",
"verbal_pre_transition_instruction": "Take exit 27 on the right onto IL 64 toward North Avenue.",
"bearing_before": 358,
"bearing_after": 3,
"time": 27.824,
"length": 0.4268,
"cost": 29.178,
"begin_shape_index": 353,
"end_shape_index": 365,
"toll": true,
"sign": {
"exit_number_elements": [
{
"text": "27"
}
],
"exit_branch_elements": [
{
"text": "IL 64",
"consecutive_count": 2
}
],
"exit_toward_elements": [
{
"text": "North Avenue"
}
]
},
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 24,
"instruction": "Keep left to take IL 64 toward Glendale Heights/Carol Stream.",
"verbal_transition_alert_instruction": "Keep left to take IL 64.",
"verbal_pre_transition_instruction": "Keep left to take IL 64 toward Glendale Heights, Carol Stream. Then Turn left onto North Avenue.",
"bearing_before": 9,
"bearing_after": 1,
"time": 5.008,
"length": 0.0813,
"cost": 24.782,
"begin_shape_index": 365,
"end_shape_index": 369,
"toll": true,
"sign": {
"exit_branch_elements": [
{
"text": "IL 64",
"consecutive_count": 2
}
],
"exit_toward_elements": [
{
"text": "Glendale Heights"
},
{
"text": "Carol Stream"
}
]
},
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left onto North Avenue/IL 64.",
"verbal_transition_alert_instruction": "Turn left onto North Avenue.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left onto North Avenue, IL 64.",
"verbal_post_transition_instruction": "Continue for 3 miles.",
"street_names": [
"North Avenue",
"IL 64"
],
"bearing_before": 13,
"bearing_after": 266,
"time": 287.201,
"length": 3.3088,
"cost": 330.452,
"begin_shape_index": 369,
"end_shape_index": 476,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto North Schmale Road.",
"verbal_transition_alert_instruction": "Turn right onto North Schmale Road.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto North Schmale Road.",
"verbal_post_transition_instruction": "Continue for a half mile.",
"street_names": [
"North Schmale Road"
],
"bearing_before": 268,
"bearing_after": 2,
"time": 46.619,
"length": 0.5163,
"cost": 104.948,
"begin_shape_index": 476,
"end_shape_index": 507,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left onto Kehoe Boulevard.",
"verbal_transition_alert_instruction": "Turn left onto Kehoe Boulevard.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left onto Kehoe Boulevard.",
"verbal_post_transition_instruction": "Continue for a half mile.",
"street_names": [
"Kehoe Boulevard"
],
"bearing_before": 0,
"bearing_after": 270,
"time": 146.003,
"length": 0.7431,
"cost": 260.047,
"begin_shape_index": 507,
"end_shape_index": 533,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left.",
"verbal_transition_alert_instruction": "Turn left.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left.",
"verbal_post_transition_instruction": "Continue for 400 feet.",
"bearing_before": 269,
"bearing_after": 180,
"time": 21.653,
"length": 0.0677,
"cost": 113.812,
"begin_shape_index": 533,
"end_shape_index": 538,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 4,
"instruction": "You have arrived at 505 E North Ave, Carol Stream, IL 60188.",
"verbal_transition_alert_instruction": "You will arrive at 505 E North Ave, Carol Stream, IL 60188.",
"verbal_pre_transition_instruction": "You have arrived at 505 E North Ave, Carol Stream, IL 60188.",
"bearing_before": 150,
"time": 0.0,
"length": 0.0,
"cost": 0.0,
"begin_shape_index": 538,
"end_shape_index": 538,
"travel_mode": "drive",
"travel_type": "car"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": true,
"has_highway": true,
"has_ferry": false,
"min_lat": 41.684661,
"min_lon": -88.115648,
"max_lat": 41.909558,
"max_lon": -88.027976,
"time": 1772.608,
"length": 23.7686,
"cost": 2473.988
},
"shape": "mpponAb{{~fDe@}TWog@aRnAwh@jBwFT]ie@c@cj@]k^c@ae@]qa@q@u|@Yql@Sud@OcZnA_L_@efA[u{@o@ueBde@c@dKKjJQpq@mAjS_@nS_@vl@o@rEGpOO~SaAja@cArf@YjPK`GC`IQtr@aBv@mE[gIuBsIiD_PoIiWgJcSsGuJmMwPaPuP_WyWkYoh@_RiXwVil@{p@g~Agj@orAeZis@wqAo`DagCqdGsn@{zA{dBiiEmqBowE}z@{gCal@{cBm[c}@oq@}lBaO}b@wJwX_Reh@qOwb@imAugDcv@gtBkk@qgBqTue@sFcJcE{FgE{EgJkIwGaFaHqDgGiCkHqBuFiAcG_A}Ge@_FS_HDiFN}JhAoHdBwJfCiJbEkIhFoKpIs`@r^ex@x{@w^f^cTpP{GbE}OxIkLlEaRbEs]xEyn@jJuiBpUct@bNa}B~WogCbYqeCjXojAzMe~BbPofBbHm\\rAqiHnGkzCd@ymDzIimDzAafBbC}m@f@kdH~G_dCtAccHzIyrAzAiaAvA_iBlAqjAZ}fAdCmpAlKefA|ZmmAre@asA`{@spDpiDiS~Rii@~h@saCv|BgxCdtCk}@`{@yrA~nAkRrRwY|YoaA`~@mrCzlCa^n[qb@hZke@rZ_^zQ}IxFwQ|Io^hOil@`T{gAx\\mz@~SyfBp]kdA~Mwq@hImp@hEazAjIwr@dBkdBdCcJJ_\\j@o`AbAeiA|AkXb@uRFmOJ{i@Ci_ApEem@}FytBmReqBod@cSqFcRyF_\\oKk\\_M}\\yM{ZsO_\\yQmZoSsYcUcY_XmR}Q}QkSsL{NmLqPmLoPeL{PuKyQqK_TuIsRmH_R_JmWeImYsGiZaC}MkBaN_ByLqAyLmAwLeAcNq@yKk@iNe@_MUaNMkMBaM@yLPmM\\sL^gNhAqZ~LyrCrUgnFzCk}@y@ob@kEcjAyUwpBkQ}x@eZe}@wo@itA_q@y~@qv@{p@ks@y`@geAoVcS_D{RoBsTu@uTC}}@zB_dAb@eMP_yAdCal@fAg}AjC{DFuSh@wq@Yeo@xEq`B~AinAlB}|@~@oxAhCc{@`BwgAvAmrAxAodAvBusAnBu~@bBmfA`Bgm@zAwi@lAqqAbC_c@l@ms@k@knBtFgQ~Au[nDgZnGgSdFgUlIgSnJiYrNcNxHyPdMsP~MeQzP{U|VwR`XkY`e@gSva@iMrXsMd\\oY|t@a\\nv@kQv]gOlW}NzUaSrXqSvXoL~MqMlPcG`GiJpJcHrGwMrLqXjUsPxLsZpRsU~MyH`EoV|KmUvJoR|G{RhGoPbEmUxF}j@rJaKjAyE\\cLnAiXdBwT~@qY`@qf@a@eUiAu[sBiWcDqZyEce@eJcs@yPo_AmVch@eOyb@{N_Cu@wZeKyi@kQqZwLmk@gUkn@yWqaAgd@{a@oTwbBm~@ilA{u@wjA}v@oa@{Wo]yQkn@{WuXeJe_@gKgc@mI_a@mFmYiC}_@yAwU_@kZNqWj@m^`Bug@jGya@bIeQdEse@lNi]zMsf@fUij@jZ}bBj_A_o@l]ou@za@kZ|Mcl@jSog@bKsf@nF}]vAu}@hAah@n@u^l@oi@p@gk@`@_b@n@crBxB}_@h@sg@{Aaj@Ric@d@qq@Cik@o@a[Y}^_C_^}DoHyAy[{EqTgDaQ{Byd@c@sMZoDHkN{C\\zPTjLj@pz@Rb[`@|]~C~lA^r]J|KJbIb@jd@l@rp@F~JDjLB|Fd@leAZxs@FzMd@|c@N~Zr@hyAF`FHxGtA~jAJ`Fp@x^VvMPvJBfCb@|i@r@z|@RjVJ`OFzIJdP|@reA@tF~@zh@vAdiATdQJzHRbPRrORp\\h@ps@FlMB|IxA~qBVxUDzENvLv@p}@HjIFpHLvIjA~|@ThSn@vk@fB~bB@d@t@fs@n@zl@@p@vA|tARfRNrRP`UFjH^ld@xAbdB`@jZBzAxAdeAf@pk@n@`v@xA~cBjAluAP|MJlKJvLPjP@lBj@bj@j@vi@HzHZxUPtO\\tXx@rr@v@~o@z@pcAVnYhAzrA|@hdAvBziBj@xd@^zb@d@nl@RhTjBh{AhEfsD`@`a@jAplAFnF^~\\^l\\f@fe@p@br@aDpKk@hBaArB}@hAkFhF{R_@}n@uAgErCqJ]cEKu[{@_LWy^}@gTg@y@A{LUwEOuL]oESgJo@_PyBmLsCga@qJqLkCiG}@aEa@eIa@yGSmYG_L@qi@K?hI?l@?r]?lo@?dQ?~fA?h`@?pX?rgA?pe@?rE?|e@Brg@BlPEjdA?js@?vR@bM@dlBG`b@@ti@Bxa@?fSAjO?hV@pHdMBpCvA~E|BxRBzKyH"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": true,
"has_highway": true,
"has_ferry": false,
"min_lat": 41.684661,
"min_lon": -88.115648,
"max_lat": 41.909558,
"max_lon": -88.027976,
"time": 1772.608,
"length": 23.7686,
"cost": 2473.988
},
"status_message": "Found route between points",
"status": 0,
"units": "miles",
"language": "en-US"
}
}

View File

@@ -0,0 +1,206 @@
{
"trip": {
"locations": [
{
"type": "break",
"lat": 41.701966,
"lon": -88.086597,
"original_index": 0
},
{
"type": "break",
"lat": 41.704616,
"lon": -88.067058,
"original_index": 1
}
],
"legs": [
{
"maneuvers": [
{
"type": 1,
"instruction": "Drive north on Brandon Court.",
"verbal_succinct_transition_instruction": "Drive north. Then Turn left onto Blair Lane.",
"verbal_pre_transition_instruction": "Drive north on Brandon Court. Then Turn left onto Blair Lane.",
"verbal_post_transition_instruction": "Continue for 80 meters.",
"street_names": [
"Brandon Court"
],
"bearing_after": 359,
"time": 10.107,
"length": 0.084,
"cost": 10.846,
"begin_shape_index": 0,
"end_shape_index": 2,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left onto Blair Lane.",
"verbal_transition_alert_instruction": "Turn left onto Blair Lane.",
"verbal_succinct_transition_instruction": "Turn left. Then Turn right onto North Schmidt Road.",
"verbal_pre_transition_instruction": "Turn left onto Blair Lane. Then Turn right onto North Schmidt Road.",
"verbal_post_transition_instruction": "Continue for 90 meters.",
"street_names": [
"Blair Lane"
],
"bearing_before": 359,
"bearing_after": 269,
"time": 12.751,
"length": 0.09,
"cost": 20.96,
"begin_shape_index": 2,
"end_shape_index": 4,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto North Schmidt Road.",
"verbal_transition_alert_instruction": "Turn right onto North Schmidt Road.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto North Schmidt Road.",
"verbal_post_transition_instruction": "Continue for 500 meters.",
"street_names": [
"North Schmidt Road"
],
"bearing_before": 269,
"bearing_after": 358,
"time": 43.917,
"length": 0.533,
"cost": 77.944,
"begin_shape_index": 4,
"end_shape_index": 17,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto West Boughton Road/CH 67.",
"verbal_transition_alert_instruction": "Turn right onto West Boughton Road.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto West Boughton Road, CH 67.",
"verbal_post_transition_instruction": "Continue for 1.5 kilometers.",
"street_names": [
"West Boughton Road",
"CH 67"
],
"bearing_before": 359,
"bearing_after": 80,
"time": 104.058,
"length": 1.628,
"cost": 143.853,
"begin_shape_index": 17,
"end_shape_index": 48,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto North Bolingbrook Drive/IL 53.",
"verbal_transition_alert_instruction": "Turn right onto North Bolingbrook Drive.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto North Bolingbrook Drive, IL 53.",
"verbal_post_transition_instruction": "Continue for 700 meters.",
"street_names": [
"North Bolingbrook Drive",
"IL 53"
],
"bearing_before": 67,
"bearing_after": 178,
"time": 51.743,
"length": 0.723,
"cost": 70.552,
"begin_shape_index": 48,
"end_shape_index": 62,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left.",
"verbal_transition_alert_instruction": "Turn left.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left.",
"verbal_post_transition_instruction": "Continue for 100 meters.",
"bearing_before": 178,
"bearing_after": 89,
"time": 24.024,
"length": 0.126,
"cost": 146.206,
"begin_shape_index": 62,
"end_shape_index": 66,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left.",
"verbal_transition_alert_instruction": "Turn left.",
"verbal_succinct_transition_instruction": "Turn left. Then You will arrive at your destination.",
"verbal_pre_transition_instruction": "Turn left. Then You will arrive at your destination.",
"verbal_post_transition_instruction": "Continue for 30 meters.",
"bearing_before": 88,
"bearing_after": 359,
"time": 7.643,
"length": 0.031,
"cost": 14.839,
"begin_shape_index": 66,
"end_shape_index": 68,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 4,
"instruction": "You have arrived at your destination.",
"verbal_transition_alert_instruction": "You will arrive at your destination.",
"verbal_pre_transition_instruction": "You have arrived at your destination.",
"bearing_before": 359,
"time": 0.0,
"length": 0.0,
"cost": 0.0,
"begin_shape_index": 68,
"end_shape_index": 68,
"travel_mode": "drive",
"travel_type": "car"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 41.701966,
"min_lon": -88.087852,
"max_lat": 41.710795,
"max_lon": -88.06705,
"time": 254.245,
"length": 3.216,
"cost": 485.201
},
"shape": "{chpnAhck_gDki@d@yCBR|z@@rFyc@n@yn@j@yo@`AgCDwDDoo@h@gEDaZTue@`@sLJiDDkIFsEDg@wHoBsYyD_k@uBq[gCo`@iB}XQoCWqDaBaWkBeXkBmZsCy_@{D_i@gFsw@gSe~CcEer@e\\mxEyDy^gGge@wIqf@}Nsz@eGm]cA_GaDwQaBqJy@_F{I}g@sB}KqByKqLwi@qE{S~S_@hQ[jS]dIOlJQpHMvo@o@f^]bmAsApSW~j@s@pY_@jMQvr@{@G}GKyUUo_@W_^{IHcFD"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 41.701966,
"min_lon": -88.087852,
"max_lat": 41.710795,
"max_lon": -88.06705,
"time": 254.245,
"length": 3.216,
"cost": 485.201
},
"status_message": "Found route between points",
"status": 0,
"units": "kilometers",
"language": "en-US"
}
}

View File

@@ -0,0 +1,148 @@
{
"trip": {
"locations": [
{
"type": "break",
"lat": 41.701966,
"lon": -88.086597,
"original_index": 0
},
{
"type": "break",
"lat": 41.699213,
"lon": -88.102372,
"original_index": 1
}
],
"legs": [
{
"maneuvers": [
{
"type": 1,
"instruction": "Drive north on Brandon Court.",
"verbal_succinct_transition_instruction": "Drive north. Then Turn left onto Blair Lane.",
"verbal_pre_transition_instruction": "Drive north on Brandon Court. Then Turn left onto Blair Lane.",
"verbal_post_transition_instruction": "Continue for 80 meters.",
"street_names": [
"Brandon Court"
],
"bearing_after": 359,
"time": 13.766,
"length": 0.084,
"cost": 14.791,
"begin_shape_index": 0,
"end_shape_index": 2,
"verbal_multi_cue": true,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left onto Blair Lane.",
"verbal_transition_alert_instruction": "Turn left onto Blair Lane.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left onto Blair Lane.",
"verbal_post_transition_instruction": "Continue for 90 meters.",
"street_names": [
"Blair Lane"
],
"bearing_before": 359,
"bearing_after": 269,
"time": 20.128,
"length": 0.09,
"cost": 32.463,
"begin_shape_index": 2,
"end_shape_index": 4,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 15,
"instruction": "Turn left onto North Schmidt Road.",
"verbal_transition_alert_instruction": "Turn left onto North Schmidt Road.",
"verbal_succinct_transition_instruction": "Turn left.",
"verbal_pre_transition_instruction": "Turn left onto North Schmidt Road.",
"verbal_post_transition_instruction": "Continue for 500 meters.",
"street_names": [
"North Schmidt Road"
],
"bearing_before": 269,
"bearing_after": 179,
"time": 48.162,
"length": 0.522,
"cost": 72.474,
"begin_shape_index": 4,
"end_shape_index": 11,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 10,
"instruction": "Turn right onto West Briarcliff Road.",
"verbal_transition_alert_instruction": "Turn right onto West Briarcliff Road.",
"verbal_succinct_transition_instruction": "Turn right.",
"verbal_pre_transition_instruction": "Turn right onto West Briarcliff Road.",
"verbal_post_transition_instruction": "Continue for 1.5 kilometers.",
"street_names": [
"West Briarcliff Road"
],
"bearing_before": 178,
"bearing_after": 267,
"time": 211.369,
"length": 1.278,
"cost": 252.521,
"begin_shape_index": 11,
"end_shape_index": 39,
"travel_mode": "drive",
"travel_type": "car"
},
{
"type": 4,
"instruction": "You have arrived at your destination.",
"verbal_transition_alert_instruction": "You will arrive at your destination.",
"verbal_pre_transition_instruction": "You have arrived at your destination.",
"bearing_before": 274,
"time": 0.0,
"length": 0.0,
"cost": 0.0,
"begin_shape_index": 39,
"end_shape_index": 39,
"travel_mode": "drive",
"travel_type": "car"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 41.697911,
"min_lon": -88.102372,
"max_lat": 41.702721,
"max_lon": -88.086596,
"time": 293.426,
"length": 1.974,
"cost": 372.251
},
"shape": "{chpnAhck_gDki@d@yCBR|z@@rF~vB_C|DCzGElfAq@r_@i@|PWbc@s@HxGhC~zBjA|aAuKf|@cGrl@K`IxE|jEj@rx@B~LNhm@x@bgALjKCfLiAdQwBbOaD|McDzIiElHiIbLcMtPmD~F_EnJoDlNeE|Vu@bF]nEe@`K@lD"
}
],
"summary": {
"has_time_restrictions": false,
"has_toll": false,
"has_highway": false,
"has_ferry": false,
"min_lat": 41.697911,
"min_lon": -88.102372,
"max_lat": 41.702721,
"max_lon": -88.086596,
"time": 293.426,
"length": 1.974,
"cost": 372.251
},
"status_message": "Found route between points",
"status": 0,
"units": "kilometers",
"language": "en-US"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
Simple script to print mouse coordinates on Ubuntu.
Run with: python print_mouse_coords.py
Press Ctrl+C to exit.
"""
from pynput import mouse
print("Mouse coordinate printer - Press Ctrl+C to exit")
print("Click to set the top left origin")
origin: tuple[int, int] | None = None
clicks: list[tuple[int, int]] = []
def on_click(x, y, button, pressed):
global origin, clicks
if pressed: # Only on mouse down, not up
if origin is None:
origin = (x, y)
print(f"Origin set to: {x},{y}")
else:
rel_x = x - origin[0]
rel_y = y - origin[1]
clicks.append((rel_x, rel_y))
print(f"Clicks: {clicks}")
if __name__ == "__main__":
try:
# Start mouse listener
with mouse.Listener(on_click=on_click) as listener:
listener.join()
except KeyboardInterrupt:
print("\nExiting...")

View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import time
import pathlib
from collections import namedtuple
import pyautogui
import pywinctl
from cereal import car, log
from cereal import messaging
from cereal.messaging import PubMaster
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.test.helpers import with_processes
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.system.updated.updated import parse_release_notes
from openpilot.system.version import terms_version, training_version
from openpilot.selfdrive.ui.tests.test_ui.nav_demo_common import (
NAV_SCENES,
build_ui_pubmaster,
publish_nav_scene,
)
AlertSize = log.SelfdriveState.AlertSize
AlertStatus = log.SelfdriveState.AlertStatus
TEST_DIR = pathlib.Path(__file__).parent
TEST_OUTPUT_DIR = TEST_DIR / "raylib_report"
SCREENSHOTS_DIR = TEST_OUTPUT_DIR / "screenshots"
UI_DELAY = 0.5
BRANCH_NAME = "this-is-a-really-super-mega-ultra-max-extreme-ultimate-long-branch-name"
VERSION = f"0.10.1 / {BRANCH_NAME} / 7864838 / Oct 03"
# Offroad alerts to test
OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot']
def put_update_params(params: Params):
params.put("UpdaterCurrentReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterNewReleaseNotes", parse_release_notes(BASEDIR))
params.put("UpdaterTargetBranch", BRANCH_NAME)
def setup_homescreen(click, pm: PubMaster, scroll=None):
pass
def setup_homescreen_update_available(click, pm: PubMaster, scroll=None):
params = Params()
params.put_bool("UpdateAvailable", True)
put_update_params(params)
setup_offroad_alert(click, pm)
def setup_settings(click, pm: PubMaster, scroll=None):
click(100, 100)
def close_settings(click, pm: PubMaster, scroll=None):
click(140, 120)
def setup_settings_network(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 450)
def setup_settings_network_advanced(click, pm: PubMaster, scroll=None):
setup_settings_network(click, pm, scroll=scroll)
click(1880, 100)
def setup_settings_toggles(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 620)
def setup_settings_software(click, pm: PubMaster, scroll=None):
put_update_params(Params())
setup_settings(click, pm)
click(278, 730)
def setup_settings_software_download(click, pm: PubMaster, scroll=None):
params = Params()
# setup_settings_software but with "DOWNLOAD" button to test long text
params.put("UpdaterState", "idle")
params.put_bool("UpdaterFetchAvailable", True)
setup_settings_software(click, pm)
def setup_settings_software_release_notes(click, pm: PubMaster, scroll=None):
setup_settings_software(click, pm, scroll=scroll)
click(588, 110) # expand description for current version
def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
setup_settings_software(click, pm, scroll=scroll)
params = Params()
params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}")
params.put("GitBranch", BRANCH_NAME) # should be on top
params.put("UpdaterTargetBranch", "nightly") # should be selected
click(1984, 449)
def setup_settings_developer(click, pm: PubMaster, scroll=None):
CP = car.CarParams()
CP.alphaLongitudinalAvailable = True # show alpha long control toggle
Params().put("CarParamsPersistent", CP.to_bytes())
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 950)
def setup_keyboard(click, pm: PubMaster, scroll=None):
setup_settings_developer(click, pm, scroll=scroll)
click(1930, 470)
def setup_pair_device(click, pm: PubMaster, scroll=None):
click(1950, 800)
def setup_offroad_alert(click, pm: PubMaster, scroll=None):
put_update_params(Params())
set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C')
set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal')
for alert in OFFROAD_ALERTS:
set_offroad_alert(alert, True)
setup_settings(click, pm)
close_settings(click, pm)
def setup_confirmation_dialog(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(1985, 791) # reset calibration
def setup_experimental_mode_description(click, pm: PubMaster, scroll=None):
setup_settings_toggles(click, pm)
click(1200, 280) # expand description for experimental mode
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster, scroll=None):
setup_settings_developer(click, pm, scroll=scroll)
click(650, 960) # toggle IQ.Pilot longitudinal control
def setup_settings_models(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 840)
def setup_settings_steering(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 950)
def setup_settings_cruise(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-4, 278, 950)
click(278, 860)
def setup_settings_visuals(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 330)
def setup_settings_display(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 420)
def setup_settings_osm(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 520)
def setup_settings_trips(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 630)
def setup_settings_vehicle(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-20, 278, 950)
click(278, 750)
def setup_onroad(click, pm: PubMaster, scroll=None):
ds = messaging.new_message('deviceState')
ds.deviceState.started = True
ps = messaging.new_message('pandaStates', 1)
ps.pandaStates[0].pandaType = log.PandaState.PandaType.dos
ps.pandaStates[0].ignitionLine = True
driverState = messaging.new_message('driverStateV2')
driverState.driverStateV2.leftDriverData.faceOrientation = [0, 0, 0]
for _ in range(5):
pm.send('deviceState', ds)
pm.send('pandaStates', ps)
pm.send('driverStateV2', driverState)
ds.clear_write_flag()
ps.clear_write_flag()
driverState.clear_write_flag()
time.sleep(0.05)
def setup_onroad_nav_demo(click, pm: PubMaster, scroll=None):
setup_onroad(click, pm)
publish_nav_scene(pm, NAV_SCENES[1])
def setup_onroad_sidebar(click, pm: PubMaster, scroll=None):
setup_onroad(click, pm)
click(100, 100) # open sidebar
def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize, text1: str, text2: str, status: log.SelfdriveState.AlertStatus):
setup_onroad(click, pm)
alert = messaging.new_message('selfdriveState')
ss = alert.selfdriveState
ss.alertSize = size
ss.alertText1 = text1
ss.alertText2 = text2
ss.alertStatus = status
for _ in range(5):
pm.send('selfdriveState', alert)
alert.clear_write_flag()
time.sleep(0.05)
def setup_onroad_small_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal)
def setup_onroad_medium_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt)
def setup_onroad_full_alert(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical)
def setup_onroad_full_alert_multiline(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal)
def setup_onroad_full_alert_long_text(click, pm: PubMaster, scroll=None):
setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt)
CASES = {
"homescreen": setup_homescreen,
"homescreen_paired": setup_homescreen,
"homescreen_prime": setup_homescreen,
"homescreen_update_available": setup_homescreen_update_available,
"homescreen_unifont": setup_homescreen,
"settings_device": setup_settings,
"settings_network": setup_settings_network,
"settings_network_advanced": setup_settings_network_advanced,
"settings_toggles": setup_settings_toggles,
"settings_software": setup_settings_software,
"settings_software_download": setup_settings_software_download,
"settings_software_release_notes": setup_settings_software_release_notes,
"settings_software_branch_switcher": setup_settings_software_branch_switcher,
"settings_developer": setup_settings_developer,
"keyboard": setup_keyboard,
"pair_device": setup_pair_device,
"offroad_alert": setup_offroad_alert,
"confirmation_dialog": setup_confirmation_dialog,
"experimental_mode_description": setup_experimental_mode_description,
"openpilot_long_confirmation_dialog": setup_openpilot_long_confirmation_dialog,
"onroad": setup_onroad,
"onroad_nav_demo": setup_onroad_nav_demo,
"onroad_sidebar": setup_onroad_sidebar,
"onroad_small_alert": setup_onroad_small_alert,
"onroad_medium_alert": setup_onroad_medium_alert,
"onroad_full_alert": setup_onroad_full_alert,
"onroad_full_alert_multiline": setup_onroad_full_alert_multiline,
"onroad_full_alert_long_text": setup_onroad_full_alert_long_text,
}
# IQ.Pilot cases
CASES.update({
"settings_models": setup_settings_models,
"settings_steering": setup_settings_steering,
"settings_cruise": setup_settings_cruise,
"settings_visuals": setup_settings_visuals,
"settings_display": setup_settings_display,
"settings_osm": setup_settings_osm,
"settings_trips": setup_settings_trips,
"settings_vehicle": setup_settings_vehicle,
})
class TestUI:
def __init__(self):
os.environ["SCALE"] = os.getenv("SCALE", "1")
os.environ["BIG"] = "1"
sys.modules["mouseinfo"] = False
def setup(self):
# Seed minimal offroad state
self.pm = build_ui_pubmaster()
ds = messaging.new_message('deviceState')
ds.deviceState.networkType = log.DeviceState.NetworkType.wifi
for _ in range(5):
self.pm.send('deviceState', ds)
ds.clear_write_flag()
time.sleep(0.05)
time.sleep(0.5)
try:
self.ui = pywinctl.getWindowsWithTitle("UI")[0]
except Exception as e:
print(f"failed to find ui window, assuming that it's in the top left (for Xvfb) {e}")
self.ui = namedtuple("bb", ["left", "top", "width", "height"])(0, 0, 2160, 1080)
def screenshot(self, name: str):
full_screenshot = pyautogui.screenshot()
cropped = full_screenshot.crop((self.ui.left, self.ui.top, self.ui.left + self.ui.width, self.ui.top + self.ui.height))
cropped.save(SCREENSHOTS_DIR / f"{name}.png")
def click(self, x: int, y: int, *args, **kwargs):
pyautogui.mouseDown(self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(0.01)
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)
def scroll(self, clicks: int, x, y, *args, **kwargs):
if clicks == 0:
return
click = -1 if clicks < 0 else 1 # -1 = down, 1 = up
for _ in range(abs(clicks)):
pyautogui.scroll(click, self.ui.left + x, self.ui.top + y, *args, **kwargs) # scroll for individual clicks since we need to delay between clicks
time.sleep(0.01) # small delay between scroll clicks to work properly
time.sleep(2) # wait for scroll to fully settle
@with_processes(["ui"])
def test_ui(self, name, setup_case):
self.setup()
time.sleep(UI_DELAY) # wait for UI to start
setup_case(self.click, self.pm, self.scroll)
self.screenshot(name)
def create_screenshots():
if TEST_OUTPUT_DIR.exists():
shutil.rmtree(TEST_OUTPUT_DIR)
SCREENSHOTS_DIR.mkdir(parents=True)
t = TestUI()
for name, setup in CASES.items():
with OpenpilotPrefix():
params = Params()
params.put("DongleId", "123456789012345")
# Set branch name
params.put("UpdaterCurrentDescription", VERSION)
params.put("UpdaterNewDescription", VERSION)
# Set terms and training version (to skip onboarding)
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
# PrimeState uses PRIME_TYPE env var (not Params('PrimeType')) to avoid clobbering stock/Connect state.
os.environ.pop("PRIME_TYPE", None)
if name == "homescreen_paired":
os.environ["PRIME_TYPE"] = "0" # NONE
elif name == "homescreen_prime":
os.environ["PRIME_TYPE"] = "2" # LITE
elif name == "homescreen_unifont":
params.put("LanguageSetting", "zh-CHT") # Traditional Chinese
t.test_ui(name, setup)
if __name__ == "__main__":
create_screenshots()

View File

@@ -0,0 +1,34 @@
<html>
<style>
.column {
float: left;
width: 50%;
padding: 5px;
}
.row::after {
content: "";
clear: both;
display: table;
}
.image {
width: 100%;
}
</style>
{% for name, (image, ref_image) in cases.items() %}
<h1>{{name}}</h1>
<div class="row">
<div class="column">
<img class="image" src="{{ image }}" />
</div>
</div>
<br>
{% endfor %}
</html>

View File

@@ -0,0 +1,20 @@
from collections import deque
from openpilot.system.ui.lib.scroll_panel2 import weighted_velocity
def test_weighted_velocity_empty():
assert weighted_velocity(deque()) == 0.0
def test_weighted_velocity_single():
assert weighted_velocity(deque([120.0])) == 120.0
def test_weighted_velocity_two_samples():
assert weighted_velocity(deque([100.0, 200.0])) == 130.0
def test_weighted_velocity_three_samples_biases_older():
velocity = weighted_velocity(deque([300.0, 180.0, 20.0]))
assert velocity == 244.0