forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0798119
This commit is contained in:
9
selfdrive/ui/tests/.gitignore
vendored
Normal file
9
selfdrive/ui/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
test
|
||||
test_translations
|
||||
test_ui/report_1
|
||||
test_ui/raylib_report
|
||||
|
||||
diff/*.mp4
|
||||
diff/*.html
|
||||
diff/.coverage
|
||||
diff/htmlcov/
|
||||
0
selfdrive/ui/tests/__init__.py
Normal file
0
selfdrive/ui/tests/__init__.py
Normal file
22
selfdrive/ui/tests/body.py
Executable file
22
selfdrive/ui/tests/body.py
Executable 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)
|
||||
35
selfdrive/ui/tests/cycle_offroad_alerts.py
Executable file
35
selfdrive/ui/tests/cycle_offroad_alerts.py
Executable 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
201
selfdrive/ui/tests/diff/diff.py
Executable 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
128
selfdrive/ui/tests/diff/replay.py
Executable 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()
|
||||
111
selfdrive/ui/tests/profile_onroad.py
Executable file
111
selfdrive/ui/tests/profile_onroad.py
Executable 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")
|
||||
53
selfdrive/ui/tests/test_feedbackd.py
Normal file
53
selfdrive/ui/tests/test_feedbackd.py
Normal 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()
|
||||
39
selfdrive/ui/tests/test_local_routes.py
Normal file
39
selfdrive/ui/tests/test_local_routes.py
Normal 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) == []
|
||||
31
selfdrive/ui/tests/test_motd.py
Normal file
31
selfdrive/ui/tests/test_motd.py
Normal 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)
|
||||
71
selfdrive/ui/tests/test_nav_helpers.py
Normal file
71
selfdrive/ui/tests/test_nav_helpers.py
Normal 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)
|
||||
72
selfdrive/ui/tests/test_nav_map_utils.py
Normal file
72
selfdrive/ui/tests/test_nav_map_utils.py
Normal 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
|
||||
39
selfdrive/ui/tests/test_offline_raster_pipeline.py
Normal file
39
selfdrive/ui/tests/test_offline_raster_pipeline.py
Normal 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
|
||||
128
selfdrive/ui/tests/test_offline_tiles.py
Normal file
128
selfdrive/ui/tests/test_offline_tiles.py
Normal 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
|
||||
8
selfdrive/ui/tests/test_raylib_ui.py
Normal file
8
selfdrive/ui/tests/test_raylib_ui.py
Normal 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)
|
||||
35
selfdrive/ui/tests/test_soundd.py
Normal file
35
selfdrive/ui/tests/test_soundd.py
Normal 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
|
||||
|
||||
124
selfdrive/ui/tests/test_translations.py
Normal file
124
selfdrive/ui/tests/test_translations.py
Normal 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)}"
|
||||
186
selfdrive/ui/tests/test_ui/nav_demo_capture.py
Normal file
186
selfdrive/ui/tests/test_ui/nav_demo_capture.py
Normal 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()
|
||||
705
selfdrive/ui/tests/test_ui/nav_demo_common.py
Normal file
705
selfdrive/ui/tests/test_ui/nav_demo_common.py
Normal 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)
|
||||
3637
selfdrive/ui/tests/test_ui/nav_demo_fixture_bolingbrook_mapbox.json
Normal file
3637
selfdrive/ui/tests/test_ui/nav_demo_fixture_bolingbrook_mapbox.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
36
selfdrive/ui/tests/test_ui/print_mouse_coords.py
Executable file
36
selfdrive/ui/tests/test_ui/print_mouse_coords.py
Executable 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...")
|
||||
392
selfdrive/ui/tests/test_ui/raylib_screenshots.py
Executable file
392
selfdrive/ui/tests/test_ui/raylib_screenshots.py
Executable 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()
|
||||
34
selfdrive/ui/tests/test_ui/template.html
Normal file
34
selfdrive/ui/tests/test_ui/template.html
Normal 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>
|
||||
20
selfdrive/ui/tests/test_ui/test_scroll_panel2.py
Normal file
20
selfdrive/ui/tests/test_ui/test_scroll_panel2.py
Normal 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
|
||||
Reference in New Issue
Block a user