From 98a30f10f447b3c28d92f089012ec66c93066bf0 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Thu, 20 Jul 2023 21:30:26 +0100 Subject: [PATCH 1/4] deleter: preserve flagged segments (#28814) --- system/loggerd/deleter.py | 41 +++++++- system/loggerd/loggerd.cc | 19 ++++ system/loggerd/loggerd.h | 3 + system/loggerd/tests/loggerd_tests_common.py | 15 ++- system/loggerd/tests/test_deleter.py | 99 ++++++++++++-------- system/loggerd/tests/test_loggerd.py | 87 +++++++++-------- system/loggerd/tests/test_uploader.py | 4 +- 7 files changed, 176 insertions(+), 92 deletions(-) diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py index 5fb2b9eb41f8123..5e7b31f583dbae6 100644 --- a/system/loggerd/deleter.py +++ b/system/loggerd/deleter.py @@ -2,15 +2,48 @@ import os import shutil import threading +from typing import List + from system.swaglog import cloudlog from system.loggerd.config import ROOT, get_available_bytes, get_available_percent from system.loggerd.uploader import listdir_by_creation +from system.loggerd.xattr_cache import getxattr MIN_BYTES = 5 * 1024 * 1024 * 1024 MIN_PERCENT = 10 DELETE_LAST = ['boot', 'crash'] +PRESERVE_ATTR_NAME = 'user.preserve' +PRESERVE_ATTR_VALUE = b'1' +PRESERVE_COUNT = 5 + + +def has_preserve_xattr(d: str) -> bool: + return getxattr(os.path.join(ROOT, d), PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE + + +def get_preserved_segments(dirs_by_creation: List[str]) -> List[str]: + preserved = [] + for n, d in enumerate(filter(has_preserve_xattr, reversed(dirs_by_creation))): + if n == PRESERVE_COUNT: + break + date_str, _, seg_str = d.rpartition("--") + + # ignore non-segment directories + if not date_str: + continue + try: + seg_num = int(seg_str) + except ValueError: + continue + + # preserve segment and its prior + preserved.append(d) + preserved.append(f"{date_str}--{seg_num - 1}") + + return preserved + def deleter_thread(exit_event): while not exit_event.is_set(): @@ -18,9 +51,13 @@ def deleter_thread(exit_event): out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT if out_of_percent or out_of_bytes: + dirs = listdir_by_creation(ROOT) + + # skip deleting most recent N preserved segments (and their prior segment) + preserved_dirs = get_preserved_segments(dirs) + # remove the earliest directory we can - dirs = sorted(listdir_by_creation(ROOT), key=lambda x: x in DELETE_LAST) - for delete_dir in dirs: + for delete_dir in sorted(dirs, key=lambda d: (d in DELETE_LAST, d in preserved_dirs)): delete_path = os.path.join(ROOT, delete_dir) if any(name.endswith(".lock") for name in os.listdir(delete_path)): diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index d1d9596e02e71db..ced95958964c4c7 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -1,3 +1,5 @@ +#include + #include #include "system/loggerd/encoder/encoder.h" @@ -170,6 +172,19 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct return bytes_count; } +void handle_user_flag(LoggerdState *s) { + LOGW("preserving %s", s->segment_path); + +#ifdef __APPLE__ + int ret = setxattr(s->segment_path, PRESERVE_ATTR_NAME, &PRESERVE_ATTR_VALUE, 1, 0, 0); +#else + int ret = setxattr(s->segment_path, PRESERVE_ATTR_NAME, &PRESERVE_ATTR_VALUE, 1, 0); +#endif + if (ret) { + LOGE("setxattr %s failed for %s: %s", PRESERVE_ATTR_NAME, s->segment_path, strerror(errno)); + } +} + void loggerd_thread() { // setup messaging typedef struct QlogState { @@ -228,6 +243,10 @@ void loggerd_thread() { while (!do_exit && (msg = sock->receive(true))) { const bool in_qlog = qs.freq != -1 && (qs.counter++ % qs.freq == 0); + if (qs.name == "userFlag") { + handle_user_flag(&s); + } + if (qs.encoder) { s.last_camera_seen_tms = millis_since_boot(); bytes_count += handle_encoder_msg(&s, msg, qs.name, remote_encoders[sock], encoder_infos_dict[qs.name]); diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index e648c0e38a9f601..4100f12f8db7552 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -24,6 +24,9 @@ const int MAIN_BITRATE = 10000000; const bool LOGGERD_TEST = getenv("LOGGERD_TEST"); const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) : 60; +constexpr char PRESERVE_ATTR_NAME[] = "user.preserve"; +constexpr char PRESERVE_ATTR_VALUE = '1'; + class EncoderInfo { public: const char *publish_name; diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py index 6d1303ca6cdf9d2..7d71516dfede75b 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/system/loggerd/tests/loggerd_tests_common.py @@ -7,10 +7,12 @@ from pathlib import Path from typing import Optional +import system.loggerd.deleter as deleter import system.loggerd.uploader as uploader +from system.loggerd.xattr_cache import setxattr -def create_random_file(file_path: Path, size_mb: float, lock: bool = False, xattr: Optional[bytes] = None) -> None: +def create_random_file(file_path: Path, size_mb: float, lock: bool = False, upload_xattr: Optional[bytes] = None) -> None: file_path.parent.mkdir(parents=True, exist_ok=True) if lock: @@ -25,8 +27,8 @@ def create_random_file(file_path: Path, size_mb: float, lock: bool = False, xatt for _ in range(chunks): f.write(data) - if xattr is not None: - uploader.setxattr(str(file_path), uploader.UPLOAD_ATTR_NAME, xattr) + if upload_xattr is not None: + setxattr(str(file_path), uploader.UPLOAD_ATTR_NAME, upload_xattr) class MockResponse(): def __init__(self, text, status_code): @@ -105,8 +107,11 @@ def tearDown(self): raise def make_file_with_data(self, f_dir: str, fn: str, size_mb: float = .1, lock: bool = False, - xattr: Optional[bytes] = None) -> Path: + upload_xattr: Optional[bytes] = None, preserve_xattr: Optional[bytes] = None) -> Path: file_path = self.root / f_dir / fn - create_random_file(file_path, size_mb, lock, xattr) + create_random_file(file_path, size_mb, lock, upload_xattr) + + if preserve_xattr is not None: + setxattr(str(file_path.parent), deleter.PRESERVE_ATTR_NAME, preserve_xattr) return file_path diff --git a/system/loggerd/tests/test_deleter.py b/system/loggerd/tests/test_deleter.py index 596545cdeb3b6f9..9474b30f82c4485 100755 --- a/system/loggerd/tests/test_deleter.py +++ b/system/loggerd/tests/test_deleter.py @@ -3,9 +3,11 @@ import threading import unittest from collections import namedtuple +from pathlib import Path +from typing import Sequence -from common.timeout import Timeout, TimeoutException import system.loggerd.deleter as deleter +from common.timeout import Timeout, TimeoutException from system.loggerd.tests.loggerd_tests_common import UploaderTestCase Stats = namedtuple("Stats", ['f_bavail', 'f_blocks', 'f_frsize']) @@ -37,30 +39,59 @@ def test_delete(self): self.start_thread() - with Timeout(5, "Timeout waiting for file to be deleted"): - while f_path.exists(): - time.sleep(0.01) - self.join_thread() - - self.assertFalse(f_path.exists(), "File not deleted") + try: + with Timeout(2, "Timeout waiting for file to be deleted"): + while f_path.exists(): + time.sleep(0.01) + finally: + self.join_thread() - def test_delete_files_in_create_order(self): - f_path_1 = self.make_file_with_data(self.seg_dir, self.f_type) - time.sleep(1) - self.seg_num += 1 - self.seg_dir = self.seg_format.format(self.seg_num) - f_path_2 = self.make_file_with_data(self.seg_dir, self.f_type) + def assertDeleteOrder(self, f_paths: Sequence[Path], timeout: int = 5) -> None: + deleted_order = [] self.start_thread() + try: + with Timeout(timeout, "Timeout waiting for files to be deleted"): + while True: + for f in f_paths: + if not f.exists() and f not in deleted_order: + deleted_order.append(f) + if len(deleted_order) == len(f_paths): + break + time.sleep(0.01) + except TimeoutException: + print("Not deleted:", [f for f in f_paths if f not in deleted_order]) + raise + finally: + self.join_thread() - with Timeout(5, "Timeout waiting for file to be deleted"): - while f_path_1.exists() and f_path_2.exists(): - time.sleep(0.01) - - self.join_thread() - - self.assertFalse(f_path_1.exists(), "Older file not deleted") - self.assertTrue(f_path_2.exists(), "Newer file deleted before older file") + self.assertEqual(deleted_order, f_paths, "Files not deleted in expected order") + + def test_delete_order(self): + self.assertDeleteOrder([ + self.make_file_with_data(self.seg_format.format(0), self.f_type), + self.make_file_with_data(self.seg_format.format(1), self.f_type), + self.make_file_with_data(self.seg_format2.format(0), self.f_type), + ]) + + def test_delete_many_preserved(self): + self.assertDeleteOrder([ + self.make_file_with_data(self.seg_format.format(0), self.f_type), + self.make_file_with_data(self.seg_format.format(1), self.f_type, preserve_xattr=deleter.PRESERVE_ATTR_VALUE), + self.make_file_with_data(self.seg_format.format(2), self.f_type), + ] + [ + self.make_file_with_data(self.seg_format2.format(i), self.f_type, preserve_xattr=deleter.PRESERVE_ATTR_VALUE) + for i in range(5) + ]) + + def test_delete_last(self): + self.assertDeleteOrder([ + self.make_file_with_data(self.seg_format.format(1), self.f_type), + self.make_file_with_data(self.seg_format2.format(0), self.f_type), + self.make_file_with_data(self.seg_format.format(0), self.f_type, preserve_xattr=deleter.PRESERVE_ATTR_VALUE), + self.make_file_with_data("boot", self.seg_format[:-4]), + self.make_file_with_data("crash", self.seg_format2[:-4]), + ]) def test_no_delete_when_available_space(self): f_path = self.make_file_with_data(self.seg_dir, self.f_type) @@ -70,15 +101,10 @@ def test_no_delete_when_available_space(self): self.fake_stats = Stats(f_bavail=available, f_blocks=10, f_frsize=block_size) self.start_thread() - - try: - with Timeout(2, "Timeout waiting for file to be deleted"): - while f_path.exists(): - time.sleep(0.01) - except TimeoutException: - pass - finally: - self.join_thread() + start_time = time.monotonic() + while f_path.exists() and time.monotonic() - start_time < 2: + time.sleep(0.01) + self.join_thread() self.assertTrue(f_path.exists(), "File deleted with available space") @@ -86,15 +112,10 @@ def test_no_delete_with_lock_file(self): f_path = self.make_file_with_data(self.seg_dir, self.f_type, lock=True) self.start_thread() - - try: - with Timeout(2, "Timeout waiting for file to be deleted"): - while f_path.exists(): - time.sleep(0.01) - except TimeoutException: - pass - finally: - self.join_thread() + start_time = time.monotonic() + while f_path.exists() and time.monotonic() - start_time < 2: + time.sleep(0.01) + self.join_thread() self.assertTrue(f_path.exists(), "File deleted when locked") diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index a2166016e024c3f..7365b256d2e9746 100755 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -8,6 +8,7 @@ import unittest from collections import defaultdict from pathlib import Path +from typing import Dict, List import cereal.messaging as messaging from cereal import log @@ -16,6 +17,8 @@ from common.params import Params from common.timeout import Timeout from system.loggerd.config import ROOT +from system.loggerd.xattr_cache import getxattr +from system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE from selfdrive.manager.process_config import managed_processes from system.version import get_version from tools.lib.logreader import LogReader @@ -71,6 +74,30 @@ def _check_sentinel(self, msgs, route): end_type = SentinelType.endOfRoute if route else SentinelType.endOfSegment self.assertTrue(msgs[-1].sentinel.type == end_type) + def _publish_random_messages(self, services: List[str]) -> Dict[str, list]: + pm = messaging.PubMaster(services) + + managed_processes["loggerd"].start() + for s in services: + self.assertTrue(pm.wait_for_readers_to_update(s, timeout=5)) + + sent_msgs = defaultdict(list) + for _ in range(random.randint(2, 10) * 100): + for s in services: + try: + m = messaging.new_message(s) + except Exception: + m = messaging.new_message(s, random.randint(2, 10)) + pm.send(s, m) + sent_msgs[s].append(m) + time.sleep(0.01) + + for s in services: + self.assertTrue(pm.wait_for_readers_to_update(s, timeout=5)) + managed_processes["loggerd"].stop() + + return sent_msgs + def test_init_data_values(self): os.environ["CLEAN"] = random.choice(["0", "1"]) @@ -193,29 +220,7 @@ def test_qlog(self): services = random.sample(qlog_services, random.randint(2, min(10, len(qlog_services)))) + \ random.sample(no_qlog_services, random.randint(2, min(10, len(no_qlog_services)))) - - pm = messaging.PubMaster(services) - - # sleep enough for the first poll to time out - # TODO: fix loggerd bug dropping the msgs from the first poll - managed_processes["loggerd"].start() - for s in services: - while not pm.all_readers_updated(s): - time.sleep(0.1) - - sent_msgs = defaultdict(list) - for _ in range(random.randint(2, 10) * 100): - for s in services: - try: - m = messaging.new_message(s) - except Exception: - m = messaging.new_message(s, random.randint(2, 10)) - pm.send(s, m) - sent_msgs[s].append(m) - time.sleep(0.01) - - time.sleep(1) - managed_processes["loggerd"].stop() + sent_msgs = self._publish_random_messages(services) qlog_path = os.path.join(self._get_latest_log_dir(), "qlog") lr = list(LogReader(qlog_path)) @@ -241,27 +246,7 @@ def test_qlog(self): def test_rlog(self): services = random.sample(CEREAL_SERVICES, random.randint(5, 10)) - pm = messaging.PubMaster(services) - - # sleep enough for the first poll to time out - # TODO: fix loggerd bug dropping the msgs from the first poll - managed_processes["loggerd"].start() - for s in services: - while not pm.all_readers_updated(s): - time.sleep(0.1) - - sent_msgs = defaultdict(list) - for _ in range(random.randint(2, 10) * 100): - for s in services: - try: - m = messaging.new_message(s) - except Exception: - m = messaging.new_message(s, random.randint(2, 10)) - pm.send(s, m) - sent_msgs[s].append(m) - - time.sleep(2) - managed_processes["loggerd"].stop() + sent_msgs = self._publish_random_messages(services) lr = list(LogReader(os.path.join(self._get_latest_log_dir(), "rlog"))) @@ -276,6 +261,20 @@ def test_rlog(self): sent.clear_write_flag() self.assertEqual(sent.to_bytes(), m.as_builder().to_bytes()) + def test_preserving_flagged_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userFlag"} + self._publish_random_messages(services) + + segment_dir = self._get_latest_log_dir() + self.assertEqual(getxattr(segment_dir, PRESERVE_ATTR_NAME), PRESERVE_ATTR_VALUE) + + def test_not_preserving_unflagged_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userFlag"} + self._publish_random_messages(services) + + segment_dir = self._get_latest_log_dir() + self.assertIsNone(getxattr(segment_dir, PRESERVE_ATTR_NAME)) + if __name__ == "__main__": unittest.main() diff --git a/system/loggerd/tests/test_uploader.py b/system/loggerd/tests/test_uploader.py index 9346b770a9ca3e0..580d1efae209c81 100755 --- a/system/loggerd/tests/test_uploader.py +++ b/system/loggerd/tests/test_uploader.py @@ -55,10 +55,10 @@ def join_thread(self): def gen_files(self, lock=False, xattr: Optional[bytes] = None, boot=True) -> List[Path]: f_paths = [] for t in ["qlog", "rlog", "dcamera.hevc", "fcamera.hevc"]: - f_paths.append(self.make_file_with_data(self.seg_dir, t, 1, lock=lock, xattr=xattr)) + f_paths.append(self.make_file_with_data(self.seg_dir, t, 1, lock=lock, upload_xattr=xattr)) if boot: - f_paths.append(self.make_file_with_data("boot", f"{self.seg_dir}", 1, lock=lock, xattr=xattr)) + f_paths.append(self.make_file_with_data("boot", f"{self.seg_dir}", 1, lock=lock, upload_xattr=xattr)) return f_paths def gen_order(self, seg1: List[int], seg2: List[int], boot=True) -> List[str]: From 942a2f97608b0b371c5ca48564d83b3744c82c77 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 21 Jul 2023 04:58:46 +0800 Subject: [PATCH 2/4] ui/map: move `MapInstructions` & `MapETA` to separate files (#28976) --- selfdrive/ui/SConscript | 3 +- selfdrive/ui/qt/maps/map.cc | 187 ----------------------- selfdrive/ui/qt/maps/map.h | 41 +---- selfdrive/ui/qt/maps/map_eta.cc | 55 +++++++ selfdrive/ui/qt/maps/map_eta.h | 23 +++ selfdrive/ui/qt/maps/map_instructions.cc | 146 ++++++++++++++++++ selfdrive/ui/qt/maps/map_instructions.h | 27 ++++ 7 files changed, 255 insertions(+), 227 deletions(-) create mode 100644 selfdrive/ui/qt/maps/map_eta.cc create mode 100644 selfdrive/ui/qt/maps/map_eta.h create mode 100644 selfdrive/ui/qt/maps/map_instructions.cc create mode 100644 selfdrive/ui/qt/maps/map_instructions.h diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 3cf97acd35a5cc8..3a4077e4f69a646 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -29,7 +29,8 @@ widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", "qt/ qt_env['CPPDEFINES'] = [] if maps: base_libs += ['qmapboxgl'] - widgets_src += ["qt/maps/map_helpers.cc", "qt/maps/map_settings.cc", "qt/maps/map.cc", "qt/maps/map_panel.cc"] + widgets_src += ["qt/maps/map_helpers.cc", "qt/maps/map_settings.cc", "qt/maps/map.cc", "qt/maps/map_panel.cc", + "qt/maps/map_eta.cc", "qt/maps/map_instructions.cc"] qt_env['CPPDEFINES'] += ["ENABLE_MAPS"] widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc index 9a143d33f4dfe8d..ba8aab3ca952c99 100644 --- a/selfdrive/ui/qt/maps/map.cc +++ b/selfdrive/ui/qt/maps/map.cc @@ -3,7 +3,6 @@ #include #include -#include #include "common/transformations/coordinates.hpp" #include "selfdrive/ui/qt/maps/map_helpers.h" @@ -12,7 +11,6 @@ const int PAN_TIMEOUT = 100; -const float MANEUVER_TRANSITION_THRESHOLD = 10; const float MAX_ZOOM = 17; const float MIN_ZOOM = 14; @@ -20,8 +18,6 @@ const float MAX_PITCH = 50; const float MIN_PITCH = 0; const float MAP_SCALE = 2; -const QString ICON_SUFFIX = ".png"; - MapWindow::MapWindow(const QMapboxGLSettings &settings) : m_settings(settings), velocity_filter(0, 10, 0.05) { QObject::connect(uiState(), &UIState::uiUpdate, this, &MapWindow::updateState); @@ -428,186 +424,3 @@ void MapWindow::updateDestinationMarker() { m_map->setPaintProperty("pinLayer", "icon-opacity", 1); } } - -MapInstructions::MapInstructions(QWidget *parent) : QWidget(parent) { - is_rhd = Params().getBool("IsRhdDetected"); - QHBoxLayout *main_layout = new QHBoxLayout(this); - main_layout->setContentsMargins(11, 50, 11, 11); - main_layout->addWidget(icon_01 = new QLabel, 0, Qt::AlignTop); - - QWidget *right_container = new QWidget(this); - right_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - QVBoxLayout *layout = new QVBoxLayout(right_container); - - layout->addWidget(distance = new QLabel); - distance->setStyleSheet(R"(font-size: 90px;)"); - - layout->addWidget(primary = new QLabel); - primary->setStyleSheet(R"(font-size: 60px;)"); - primary->setWordWrap(true); - - layout->addWidget(secondary = new QLabel); - secondary->setStyleSheet(R"(font-size: 50px;)"); - secondary->setWordWrap(true); - - layout->addLayout(lane_layout = new QHBoxLayout); - main_layout->addWidget(right_container); - - setStyleSheet("color:white"); - QPalette pal = palette(); - pal.setColor(QPalette::Background, QColor(0, 0, 0, 150)); - setAutoFillBackground(true); - setPalette(pal); - - buildPixmapCache(); -} - -void MapInstructions::buildPixmapCache() { - QDir dir("../assets/navigation"); - for (QString fn : dir.entryList({"*" + ICON_SUFFIX}, QDir::Files)) { - QPixmap pm(dir.filePath(fn)); - QString key = fn.left(fn.size() - ICON_SUFFIX.length()); - pm = pm.scaledToWidth(200, Qt::SmoothTransformation); - - // Maneuver icons - pixmap_cache[key] = pm; - // lane direction icons - if (key.contains("turn_")) { - pixmap_cache["lane_" + key] = pm.scaled({125, 125}, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - } - - // for rhd, reflect direction and then flip - if (key.contains("_left")) { - pixmap_cache["rhd_" + key.replace("_left", "_right")] = pm.transformed(QTransform().scale(-1, 1)); - } else if (key.contains("_right")) { - pixmap_cache["rhd_" + key.replace("_right", "_left")] = pm.transformed(QTransform().scale(-1, 1)); - } - } -} - -QString MapInstructions::getDistance(float d) { - d = std::max(d, 0.0f); - if (uiState()->scene.is_metric) { - return (d > 500) ? QString::number(d / 1000, 'f', 1) + tr(" km") - : QString::number(50 * int(d / 50)) + tr(" m"); - } else { - float feet = d * METER_TO_FOOT; - return (feet > 500) ? QString::number(d * METER_TO_MILE, 'f', 1) + tr(" mi") - : QString::number(50 * int(feet / 50)) + tr(" ft"); - } -} - -void MapInstructions::updateInstructions(cereal::NavInstruction::Reader instruction) { - setUpdatesEnabled(false); - - // Show instruction text - QString primary_str = QString::fromStdString(instruction.getManeuverPrimaryText()); - QString secondary_str = QString::fromStdString(instruction.getManeuverSecondaryText()); - - primary->setText(primary_str); - secondary->setVisible(secondary_str.length() > 0); - secondary->setText(secondary_str); - distance->setText(getDistance(instruction.getManeuverDistance())); - - // Show arrow with direction - QString type = QString::fromStdString(instruction.getManeuverType()); - QString modifier = QString::fromStdString(instruction.getManeuverModifier()); - if (!type.isEmpty()) { - QString fn = "direction_" + type; - if (!modifier.isEmpty()) { - fn += "_" + modifier; - } - fn = fn.replace(' ', '_'); - bool rhd = is_rhd && (fn.contains("_left") || fn.contains("_right")); - icon_01->setPixmap(pixmap_cache[!rhd ? fn : "rhd_" + fn]); - icon_01->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); - icon_01->setVisible(true); - } - - // Show lanes - auto lanes = instruction.getLanes(); - for (int i = 0; i < lanes.size(); ++i) { - bool active = lanes[i].getActive(); - - // TODO: only use active direction if active - bool left = false, straight = false, right = false; - for (auto const &direction: lanes[i].getDirections()) { - left |= direction == cereal::NavInstruction::Direction::LEFT; - right |= direction == cereal::NavInstruction::Direction::RIGHT; - straight |= direction == cereal::NavInstruction::Direction::STRAIGHT; - } - - // TODO: Make more images based on active direction and combined directions - QString fn = "lane_direction_"; - if (left) { - fn += "turn_left"; - } else if (right) { - fn += "turn_right"; - } else if (straight) { - fn += "turn_straight"; - } - - if (!active) { - fn += "_inactive"; - } - - QLabel *label = (i < lane_labels.size()) ? lane_labels[i] : lane_labels.emplace_back(new QLabel); - if (!label->parentWidget()) { - lane_layout->addWidget(label); - } - label->setPixmap(pixmap_cache[fn]); - label->setVisible(true); - } - - for (int i = lanes.size(); i < lane_labels.size(); ++i) { - lane_labels[i]->setVisible(false); - } - - setUpdatesEnabled(true); - setVisible(true); -} - -MapETA::MapETA(QWidget *parent) : QWidget(parent) { - setVisible(false); - setAttribute(Qt::WA_TranslucentBackground); - eta_doc.setUndoRedoEnabled(false); - eta_doc.setDefaultStyleSheet("body {font-family:Inter;font-size:60px;color:white;} b{font-size:70px;font-weight:600}"); -} - -void MapETA::paintEvent(QPaintEvent *event) { - if (!eta_doc.isEmpty()) { - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.setPen(Qt::NoPen); - p.setBrush(QColor(0, 0, 0, 150)); - QSizeF txt_size = eta_doc.size(); - p.drawRoundedRect((width() - txt_size.width()) / 2 - UI_BORDER_SIZE, 0, txt_size.width() + UI_BORDER_SIZE * 2, height() + 25, 25, 25); - p.translate((width() - txt_size.width()) / 2, (height() - txt_size.height()) / 2); - eta_doc.drawContents(&p); - } -} - -void MapETA::updateETA(float s, float s_typical, float d) { - // ETA - auto eta_t = QDateTime::currentDateTime().addSecs(s).time(); - auto eta = format_24h ? std::array{eta_t.toString("HH:mm"), tr("eta")} - : std::array{eta_t.toString("h:mm a").split(' ')[0], eta_t.toString("a")}; - - // Remaining time - auto remaining = s < 3600 ? std::array{QString::number(int(s / 60)), tr("min")} - : std::array{QString("%1:%2").arg((int)s / 3600).arg(((int)s % 3600) / 60, 2, 10, QLatin1Char('0')), tr("hr")}; - QString color = "#25DA6E"; - if (s / s_typical > 1.5) color = "#DA3025"; - else if (s / s_typical > 1.2) color = "#DAA725"; - - // Distance - float num = uiState()->scene.is_metric ? (d / 1000.0) : (d * METER_TO_MILE); - auto distance = std::array{QString::number(num, 'f', num < 100 ? 1 : 0), - uiState()->scene.is_metric ? tr("km") : tr("mi")}; - - eta_doc.setHtml(QString(R"(%1%2 %4%5 %6%7)") - .arg(eta[0], eta[1], color, remaining[0], remaining[1], distance[0], distance[1])); - - setVisible(d >= MANEUVER_TRANSITION_THRESHOLD); - update(); -} diff --git a/selfdrive/ui/qt/maps/map.h b/selfdrive/ui/qt/maps/map.h index bcf2f79b3d74689..83b0118f96da371 100644 --- a/selfdrive/ui/qt/maps/map.h +++ b/selfdrive/ui/qt/maps/map.h @@ -4,8 +4,6 @@ #include #include -#include -#include #include #include #include @@ -15,7 +13,6 @@ #include #include #include -#include #include #include @@ -23,42 +20,8 @@ #include "common/params.h" #include "common/util.h" #include "selfdrive/ui/ui.h" - -class MapInstructions : public QWidget { - Q_OBJECT - -private: - QLabel *distance; - QLabel *primary; - QLabel *secondary; - QLabel *icon_01; - QHBoxLayout *lane_layout; - bool is_rhd = false; - std::vector lane_labels; - QHash pixmap_cache; - -public: - MapInstructions(QWidget * parent=nullptr); - void buildPixmapCache(); - QString getDistance(float d); - void updateInstructions(cereal::NavInstruction::Reader instruction); -}; - -class MapETA : public QWidget { - Q_OBJECT - -public: - MapETA(QWidget * parent=nullptr); - void updateETA(float seconds, float seconds_typical, float distance); - -private: - void paintEvent(QPaintEvent *event) override; - void showEvent(QShowEvent *event) override { format_24h = param.getBool("NavSettingTime24h"); } - - bool format_24h = false; - QTextDocument eta_doc; - Params param; -}; +#include "selfdrive/ui/qt/maps/map_eta.h" +#include "selfdrive/ui/qt/maps/map_instructions.h" class MapWindow : public QOpenGLWidget { Q_OBJECT diff --git a/selfdrive/ui/qt/maps/map_eta.cc b/selfdrive/ui/qt/maps/map_eta.cc new file mode 100644 index 000000000000000..23366efbe2bfb72 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_eta.cc @@ -0,0 +1,55 @@ +#include "selfdrive/ui/qt/maps/map_eta.h" + +#include +#include + +#include "selfdrive/ui/ui.h" + +const float MANEUVER_TRANSITION_THRESHOLD = 10; + +MapETA::MapETA(QWidget *parent) : QWidget(parent) { + setVisible(false); + setAttribute(Qt::WA_TranslucentBackground); + eta_doc.setUndoRedoEnabled(false); + eta_doc.setDefaultStyleSheet("body {font-family:Inter;font-size:60px;color:white;} b{font-size:70px;font-weight:600}"); +} + +void MapETA::paintEvent(QPaintEvent *event) { + if (!eta_doc.isEmpty()) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 150)); + QSizeF txt_size = eta_doc.size(); + p.drawRoundedRect((width() - txt_size.width()) / 2 - UI_BORDER_SIZE, 0, txt_size.width() + UI_BORDER_SIZE * 2, height() + 25, 25, 25); + p.translate((width() - txt_size.width()) / 2, (height() - txt_size.height()) / 2); + eta_doc.drawContents(&p); + } +} + +void MapETA::updateETA(float s, float s_typical, float d) { + // ETA + auto eta_t = QDateTime::currentDateTime().addSecs(s).time(); + auto eta = format_24h ? std::array{eta_t.toString("HH:mm"), tr("eta")} + : std::array{eta_t.toString("h:mm a").split(' ')[0], eta_t.toString("a")}; + + // Remaining time + auto remaining = s < 3600 ? std::array{QString::number(int(s / 60)), tr("min")} + : std::array{QString("%1:%2").arg((int)s / 3600).arg(((int)s % 3600) / 60, 2, 10, QLatin1Char('0')), tr("hr")}; + QString color = "#25DA6E"; + if (s / s_typical > 1.5) + color = "#DA3025"; + else if (s / s_typical > 1.2) + color = "#DAA725"; + + // Distance + float num = uiState()->scene.is_metric ? (d / 1000.0) : (d * METER_TO_MILE); + auto distance = std::array{QString::number(num, 'f', num < 100 ? 1 : 0), + uiState()->scene.is_metric ? tr("km") : tr("mi")}; + + eta_doc.setHtml(QString(R"(%1%2 %4%5 %6%7)") + .arg(eta[0], eta[1], color, remaining[0], remaining[1], distance[0], distance[1])); + + setVisible(d >= MANEUVER_TRANSITION_THRESHOLD); + update(); +} diff --git a/selfdrive/ui/qt/maps/map_eta.h b/selfdrive/ui/qt/maps/map_eta.h new file mode 100644 index 000000000000000..6e59837de3d7461 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_eta.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include "common/params.h" + +class MapETA : public QWidget { + Q_OBJECT + +public: + MapETA(QWidget * parent=nullptr); + void updateETA(float seconds, float seconds_typical, float distance); + +private: + void paintEvent(QPaintEvent *event) override; + void showEvent(QShowEvent *event) override { format_24h = param.getBool("NavSettingTime24h"); } + + bool format_24h = false; + QTextDocument eta_doc; + Params param; +}; diff --git a/selfdrive/ui/qt/maps/map_instructions.cc b/selfdrive/ui/qt/maps/map_instructions.cc new file mode 100644 index 000000000000000..fc7f80690a2edf0 --- /dev/null +++ b/selfdrive/ui/qt/maps/map_instructions.cc @@ -0,0 +1,146 @@ +#include "selfdrive/ui/qt/maps/map_instructions.h" + +#include +#include + +#include "selfdrive/ui/ui.h" + +const QString ICON_SUFFIX = ".png"; + +MapInstructions::MapInstructions(QWidget *parent) : QWidget(parent) { + is_rhd = Params().getBool("IsRhdDetected"); + QHBoxLayout *main_layout = new QHBoxLayout(this); + main_layout->setContentsMargins(11, 50, 11, 11); + main_layout->addWidget(icon_01 = new QLabel, 0, Qt::AlignTop); + + QWidget *right_container = new QWidget(this); + right_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + QVBoxLayout *layout = new QVBoxLayout(right_container); + + layout->addWidget(distance = new QLabel); + distance->setStyleSheet(R"(font-size: 90px;)"); + + layout->addWidget(primary = new QLabel); + primary->setStyleSheet(R"(font-size: 60px;)"); + primary->setWordWrap(true); + + layout->addWidget(secondary = new QLabel); + secondary->setStyleSheet(R"(font-size: 50px;)"); + secondary->setWordWrap(true); + + layout->addLayout(lane_layout = new QHBoxLayout); + main_layout->addWidget(right_container); + + setStyleSheet("color:white"); + QPalette pal = palette(); + pal.setColor(QPalette::Background, QColor(0, 0, 0, 150)); + setAutoFillBackground(true); + setPalette(pal); + + buildPixmapCache(); +} + +void MapInstructions::buildPixmapCache() { + QDir dir("../assets/navigation"); + for (QString fn : dir.entryList({"*" + ICON_SUFFIX}, QDir::Files)) { + QPixmap pm(dir.filePath(fn)); + QString key = fn.left(fn.size() - ICON_SUFFIX.length()); + pm = pm.scaledToWidth(200, Qt::SmoothTransformation); + + // Maneuver icons + pixmap_cache[key] = pm; + // lane direction icons + if (key.contains("turn_")) { + pixmap_cache["lane_" + key] = pm.scaled({125, 125}, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + + // for rhd, reflect direction and then flip + if (key.contains("_left")) { + pixmap_cache["rhd_" + key.replace("_left", "_right")] = pm.transformed(QTransform().scale(-1, 1)); + } else if (key.contains("_right")) { + pixmap_cache["rhd_" + key.replace("_right", "_left")] = pm.transformed(QTransform().scale(-1, 1)); + } + } +} + +QString MapInstructions::getDistance(float d) { + d = std::max(d, 0.0f); + if (uiState()->scene.is_metric) { + return (d > 500) ? QString::number(d / 1000, 'f', 1) + tr(" km") + : QString::number(50 * int(d / 50)) + tr(" m"); + } else { + float feet = d * METER_TO_FOOT; + return (feet > 500) ? QString::number(d * METER_TO_MILE, 'f', 1) + tr(" mi") + : QString::number(50 * int(feet / 50)) + tr(" ft"); + } +} + +void MapInstructions::updateInstructions(cereal::NavInstruction::Reader instruction) { + setUpdatesEnabled(false); + + // Show instruction text + QString primary_str = QString::fromStdString(instruction.getManeuverPrimaryText()); + QString secondary_str = QString::fromStdString(instruction.getManeuverSecondaryText()); + + primary->setText(primary_str); + secondary->setVisible(secondary_str.length() > 0); + secondary->setText(secondary_str); + distance->setText(getDistance(instruction.getManeuverDistance())); + + // Show arrow with direction + QString type = QString::fromStdString(instruction.getManeuverType()); + QString modifier = QString::fromStdString(instruction.getManeuverModifier()); + if (!type.isEmpty()) { + QString fn = "direction_" + type; + if (!modifier.isEmpty()) { + fn += "_" + modifier; + } + fn = fn.replace(' ', '_'); + bool rhd = is_rhd && (fn.contains("_left") || fn.contains("_right")); + icon_01->setPixmap(pixmap_cache[!rhd ? fn : "rhd_" + fn]); + icon_01->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + icon_01->setVisible(true); + } + + // Show lanes + auto lanes = instruction.getLanes(); + for (int i = 0; i < lanes.size(); ++i) { + bool active = lanes[i].getActive(); + + // TODO: only use active direction if active + bool left = false, straight = false, right = false; + for (auto const &direction : lanes[i].getDirections()) { + left |= direction == cereal::NavInstruction::Direction::LEFT; + right |= direction == cereal::NavInstruction::Direction::RIGHT; + straight |= direction == cereal::NavInstruction::Direction::STRAIGHT; + } + + // TODO: Make more images based on active direction and combined directions + QString fn = "lane_direction_"; + if (left) { + fn += "turn_left"; + } else if (right) { + fn += "turn_right"; + } else if (straight) { + fn += "turn_straight"; + } + + if (!active) { + fn += "_inactive"; + } + + QLabel *label = (i < lane_labels.size()) ? lane_labels[i] : lane_labels.emplace_back(new QLabel); + if (!label->parentWidget()) { + lane_layout->addWidget(label); + } + label->setPixmap(pixmap_cache[fn]); + label->setVisible(true); + } + + for (int i = lanes.size(); i < lane_labels.size(); ++i) { + lane_labels[i]->setVisible(false); + } + + setUpdatesEnabled(true); + setVisible(true); +} diff --git a/selfdrive/ui/qt/maps/map_instructions.h b/selfdrive/ui/qt/maps/map_instructions.h new file mode 100644 index 000000000000000..83ad3b87a47c77c --- /dev/null +++ b/selfdrive/ui/qt/maps/map_instructions.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include "cereal/gen/cpp/log.capnp.h" + +class MapInstructions : public QWidget { + Q_OBJECT + +private: + QLabel *distance; + QLabel *primary; + QLabel *secondary; + QLabel *icon_01; + QHBoxLayout *lane_layout; + bool is_rhd = false; + std::vector lane_labels; + QHash pixmap_cache; + +public: + MapInstructions(QWidget * parent=nullptr); + void buildPixmapCache(); + QString getDistance(float d); + void updateInstructions(cereal::NavInstruction::Reader instruction); +}; From 0b71c395ceb3ec532698b98863a46ba959c9bc83 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 20 Jul 2023 14:44:26 -0700 Subject: [PATCH 3/4] add seg preservation to release notes --- RELEASES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index fe61edf6cf436a1..6b5751507bb968c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,7 +8,8 @@ Version 0.9.4 (2023-XX-XX) * Navigation settings moved to home screen and map * UI alerts rework * Border color always shows engagement status. Blue means disengaged, green means engaged, and grey means engaged with human overriding - * Alerts are shown inside the border. Black/grey means info, orange means warning, and red means critical alert + * Alerts are shown inside the border. Black/grey means info, orange means warning, and red means critical alert +* Bookmarked segments are preserved on the device's storage * Ford Focus 2018 support * Kia Carnival 2023 support thanks to sunnyhaibin! From 2ff33663a7092b182f74bc9961deee7b92dfd37b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 20 Jul 2023 15:29:29 -0700 Subject: [PATCH 4/4] bump panda (#29066) * bump panda * update bootstub --- panda | 2 +- selfdrive/boardd/tests/bootstub.panda_h7.bin | Bin 17000 -> 17000 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 5d873444b2cf801..dd78b2bf6c9d63e 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 5d873444b2cf801ba73f4a457993260df3a412b8 +Subproject commit dd78b2bf6c9d63ef59e81d0c400e85c8b477a8be diff --git a/selfdrive/boardd/tests/bootstub.panda_h7.bin b/selfdrive/boardd/tests/bootstub.panda_h7.bin index 56c562a080ceadf0f28dcb1b9a36af2a57e02141..1d9445004e33e5d9a28a119681866e92096d668f 100755 GIT binary patch delta 30 mcmaFS!uX