From 61608db786e163a55b11bdd1c557f0cd663c5d11 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 15 Jul 2026 21:39:47 -0700 Subject: [PATCH 001/325] jp: fix linking on macOS (#38353) * jp: fix linking on macOS * mv to root --- SConstruct | 5 +++++ openpilot/tools/jotpluggler/test_jotpluggler.py | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 openpilot/tools/jotpluggler/test_jotpluggler.py diff --git a/SConstruct b/SConstruct index 72a25e12d4..2a69cd2e38 100644 --- a/SConstruct +++ b/SConstruct @@ -166,6 +166,11 @@ env = Environment( tools=["default", "cython", "compilation_db", "rednose_filter"], toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], ) +# SCons' Darwin linker tool doesn't define the variables used to expand RPATH. +if arch == "Darwin": + env["RPATHPREFIX"] = "-Wl,-rpath," + env["RPATHSUFFIX"] = "" + env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}" if arch != "larch64": env['_LIBFLAGS'] = _libflags diff --git a/openpilot/tools/jotpluggler/test_jotpluggler.py b/openpilot/tools/jotpluggler/test_jotpluggler.py new file mode 100644 index 0000000000..b9b751c4a5 --- /dev/null +++ b/openpilot/tools/jotpluggler/test_jotpluggler.py @@ -0,0 +1,11 @@ +import subprocess +from pathlib import Path + + +JOTPLUGGLER_DIR = Path(__file__).parent + + +def test_help(): + result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert "Usage:" in result.stderr From 60716edc3752339a1c83e38745dd5835c8b93060 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 15 Jul 2026 22:51:41 -0700 Subject: [PATCH 002/325] Fix thumbnail creation (#38354) it's not a stream --- openpilot/system/loggerd/encoder/jpeg_encoder.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/system/loggerd/encoder/jpeg_encoder.cc b/openpilot/system/loggerd/encoder/jpeg_encoder.cc index 79f5e1b80f..6258e8144d 100644 --- a/openpilot/system/loggerd/encoder/jpeg_encoder.cc +++ b/openpilot/system/loggerd/encoder/jpeg_encoder.cc @@ -93,7 +93,7 @@ void JpegEncoder::compressToJpeg(uint8_t *y_plane, uint8_t *u_plane, uint8_t *v_ frame->data[2] = v_plane; // Required for MJPEG qscale to take effect (global_quality alone is not enough). frame->quality = FF_QP2LAMBDA * MJPEG_QSCALE; - frame->pts = 0; + frame->pts = AV_NOPTS_VALUE; int err = avcodec_send_frame(codec_ctx, frame); if (err < 0) { From 06a73f538e58c87597b83460011b1aef675ccac8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 16 Jul 2026 13:45:57 -0700 Subject: [PATCH 003/325] cabana: de-Qt, part 1 (#38357) * cabana: de-Qt, part 1 * lil more --- openpilot/tools/cabana/.gitignore | 1 + openpilot/tools/cabana/SConscript | 15 +- openpilot/tools/cabana/binaryview.cc | 13 +- openpilot/tools/cabana/chart/chart.cc | 19 +- openpilot/tools/cabana/chart/chartswidget.cc | 5 +- .../tools/cabana/chart/signalselector.cc | 3 +- openpilot/tools/cabana/chart/sparkline.cc | 2 +- openpilot/tools/cabana/core/can_data.h | 46 +++ openpilot/tools/cabana/core/color.h | 79 ++++ openpilot/tools/cabana/core/message_id.h | 27 ++ openpilot/tools/cabana/core/settings.h | 34 ++ openpilot/tools/cabana/dbc/dbc.cc | 16 +- openpilot/tools/cabana/dbc/dbc.h | 50 +-- openpilot/tools/cabana/dbc/dbcfile.cc | 350 +++++++++--------- openpilot/tools/cabana/dbc/dbcfile.h | 13 +- openpilot/tools/cabana/dbc/dbcmanager.cc | 35 +- openpilot/tools/cabana/dbc/dbcmanager.h | 35 +- openpilot/tools/cabana/dbc/dbcqt.cc | 18 + openpilot/tools/cabana/dbc/dbcqt.h | 27 ++ openpilot/tools/cabana/detailwidget.cc | 3 +- openpilot/tools/cabana/historylog.cc | 11 +- openpilot/tools/cabana/historylog.h | 2 +- openpilot/tools/cabana/mainwin.cc | 65 ++-- openpilot/tools/cabana/messageswidget.cc | 3 +- openpilot/tools/cabana/settings.cc | 40 +- openpilot/tools/cabana/settings.h | 36 +- openpilot/tools/cabana/signalview.cc | 19 +- .../tools/cabana/streams/abstractstream.cc | 23 +- .../tools/cabana/streams/abstractstream.h | 44 +-- openpilot/tools/cabana/streams/livestream.cc | 13 +- openpilot/tools/cabana/streams/livestream.h | 4 +- .../tools/cabana/streams/replaystream.cc | 4 +- openpilot/tools/cabana/streams/replaystream.h | 4 +- openpilot/tools/cabana/streamselector.cc | 4 +- openpilot/tools/cabana/tests/test_cabana.cc | 80 +++- openpilot/tools/cabana/tests/test_runner.cc | 3 - openpilot/tools/cabana/utils/export.cc | 32 +- openpilot/tools/cabana/utils/export.h | 5 +- openpilot/tools/cabana/utils/util.cc | 4 +- openpilot/tools/cabana/utils/util.h | 5 + openpilot/tools/cabana/videowidget.cc | 2 +- 41 files changed, 723 insertions(+), 471 deletions(-) create mode 100644 openpilot/tools/cabana/core/can_data.h create mode 100644 openpilot/tools/cabana/core/color.h create mode 100644 openpilot/tools/cabana/core/message_id.h create mode 100644 openpilot/tools/cabana/core/settings.h create mode 100644 openpilot/tools/cabana/dbc/dbcqt.cc create mode 100644 openpilot/tools/cabana/dbc/dbcqt.h diff --git a/openpilot/tools/cabana/.gitignore b/openpilot/tools/cabana/.gitignore index 927b05e34a..e4212612df 100644 --- a/openpilot/tools/cabana/.gitignore +++ b/openpilot/tools/cabana/.gitignore @@ -7,3 +7,4 @@ assets.cc _cabana dbc/car_fingerprint_to_dbc.json tests/test_cabana +tests/test_dbc_core diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 26c20ddba1..e9c1a976bb 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -97,7 +97,7 @@ cabana_env.Depends(assets_src, str(bootstrap_icons.SVG_PATH)) cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', @@ -110,6 +110,19 @@ cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_lib if GetOption('extras'): cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) + # This target deliberately uses the base environment and links no Qt libraries. + # It prevents Qt dependencies from creeping back into the DBC core. + dbc_core_test_env = env.Clone() + dbc_core_test_env['CXXFLAGS'] += [opendbc_path] + dbc_core_test_objects = [ + dbc_core_test_env.Object('tests/dbc_core_test_runner', 'tests/test_runner.cc'), + dbc_core_test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'), + dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), + dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), + dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), + ] + dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) + output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, ['dbc/generate_dbc_json.py'], diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index c86b3ebdae..1c7c7b938e 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -1,4 +1,5 @@ #include "tools/cabana/binaryview.h" +#include "tools/cabana/dbc/dbcqt.h" #include @@ -34,7 +35,7 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { setMouseTracking(true); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &BinaryView::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh); addShortcuts(); @@ -334,7 +335,7 @@ void BinaryViewModel::updateState() { color.setAlpha(alpha); updateItem(i, j, bit_val, color); } - updateItem(i, 8, binary[i], last_msg.colors[i]); + updateItem(i, 8, binary[i], toQColor(last_msg.colors[i])); } } @@ -421,14 +422,14 @@ void BinaryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op painter->fillRect(option.rect, item->bg_color); } } else if (option.state & QStyle::State_Selected) { - auto color = bin_view->resize_sig ? bin_view->resize_sig->color : option.palette.color(QPalette::Active, QPalette::Highlight); + auto color = bin_view->resize_sig ? toQColor(bin_view->resize_sig->color) : option.palette.color(QPalette::Active, QPalette::Highlight); painter->fillRect(option.rect, color); painter->setPen(option.palette.color(QPalette::BrightText)); } else if (!bin_view->selectionModel()->hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), bin_view->resize_sig) == item->sigs.end()) { // not resizing if (item->sigs.size() > 0) { for (auto &s : item->sigs) { if (s == bin_view->hovered_sig) { - painter->fillRect(option.rect, s->color.darker(125)); // 4/5x brightness + painter->fillRect(option.rect, toQColor(s->color.darker(125))); // 4/5x brightness } else { drawSignalCell(painter, option, index, s); } @@ -483,14 +484,14 @@ void BinaryItemDelegate::drawSignalCell(QPainter *painter, const QStyleOptionVie painter->setClipRegion(QRegion(rc).subtracted(subtract)); auto item = (const BinaryViewModel::Item *)index.internalPointer(); - QColor color = sig->color; + QColor color = toQColor(sig->color); color.setAlpha(item->bg_color.alpha()); // Mixing the signal color with the Base background color to fade it painter->fillRect(rc, option.palette.color(QPalette::Base)); painter->fillRect(rc, color); // Draw edges - color = sig->color.darker(125); + color = toQColor(sig->color.darker(125)); painter->setPen(QPen(color, 1)); if (draw_left) painter->drawLine(rc.topLeft(), rc.bottomLeft()); if (draw_right) painter->drawLine(rc.topRight(), rc.bottomRight()); diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index 9dfdc595f0..b5ddcf0479 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -1,4 +1,5 @@ #include "tools/cabana/chart/chart.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -52,10 +53,10 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, this, &ChartView::resetChartCache); QObject::connect(window()->windowHandle(), &QWindow::screenChanged, this, &ChartView::resetChartCache); - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated); } void ChartView::createToolButtons() { @@ -115,14 +116,14 @@ void ChartView::setTheme(QChart::ChartTheme theme) { axis_x->setLineVisible(false); axis_y->setLineVisible(false); for (auto &s : sigs) { - s.series->setColor(s.sig->color); + s.series->setColor(toQColor(s.sig->color)); } } void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { if (hasSignal(msg_id, sig)) return; - QXYSeries *series = createSeries(series_type, sig->color); + QXYSeries *series = createSeries(series_type, toQColor(sig->color)); sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); updateSeries(sig); updateSeriesPoints(); @@ -157,8 +158,8 @@ void ChartView::removeIf(std::function predicate) { void ChartView::signalUpdated(const cabana::Signal *sig) { auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); if (it != sigs.end()) { - if (it->series->color() != sig->color) { - setSeriesColor(it->series, sig->color); + if (it->series->color() != toQColor(sig->color)) { + setSeriesColor(it->series, toQColor(sig->color)); } updateTitle(); updateSeries(sig); @@ -838,7 +839,7 @@ void ChartView::setSeriesType(SeriesType type) { s.series->deleteLater(); } for (auto &s : sigs) { - s.series = createSeries(series_type, s.sig->color); + s.series = createSeries(series_type, toQColor(s.sig->color)); const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; s.series->replace(QVector(points.cbegin(), points.cend())); } diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 44dca42152..e5c70bc536 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -1,4 +1,5 @@ #include "tools/cabana/chart/chartswidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -115,7 +116,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { align_timer->setSingleShot(true); QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll); QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState); QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState); @@ -309,7 +310,7 @@ void ChartsWidget::splitChart(ChartView *src_chart) { src_chart->chart()->removeSeries(it->series); // Restore to the original color - it->series->setColor(it->sig->color); + it->series->setColor(toQColor(it->sig->color)); c->addSeries(it->series); c->sigs.emplace_back(std::move(*it)); diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index 6f2fd8de46..12a9c81564 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -1,4 +1,5 @@ #include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -92,7 +93,7 @@ void SignalSelector::updateAvailableList(int index) { } void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString("■ %1").arg(sig->color.name(), QString::fromStdString(sig->name)); + QString text = QString("■ %1").arg(toQColor(sig->color).name(), QString::fromStdString(sig->name)); if (show_msg_name) text += QString(" %0 %1").arg(QString::fromStdString(msgName(id)), QString::fromStdString(id.toString())); QLabel *label = new QLabel(text); diff --git a/openpilot/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc index 91435cd5ac..f5bef0fc2e 100644 --- a/openpilot/tools/cabana/chart/sparkline.cc +++ b/openpilot/tools/cabana/chart/sparkline.cc @@ -31,7 +31,7 @@ void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIt } freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0); - render(sig->color, range, size); + render(toQColor(sig->color), range, size); } void Sparkline::render(const QColor &color, int range, QSize size) { diff --git a/openpilot/tools/cabana/core/can_data.h b/openpilot/tools/cabana/core/can_data.h new file mode 100644 index 0000000000..38141b1890 --- /dev/null +++ b/openpilot/tools/cabana/core/can_data.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include "tools/cabana/core/color.h" +#include "tools/cabana/core/message_id.h" + +struct CanData { + void compute(const MessageId &msg_id, const uint8_t *data, int size, double current_sec, + double playback_speed, const std::vector &mask, double frequency = 0); + + double ts = 0.; + uint32_t count = 0; + double freq = 0; + std::vector dat; + std::vector colors; + + struct ByteLastChange { + double ts = 0; + int delta = 0; + int same_delta_counter = 0; + bool suppressed = false; + }; + std::vector last_changes; + std::vector> bit_flip_counts; + double last_freq_update_ts = 0; +}; + +struct CanEvent { + uint8_t src; + uint32_t address; + uint64_t mono_time; + uint8_t size; + uint8_t dat[]; +}; + +struct CompareCanEvent { + constexpr bool operator()(const CanEvent *const event, uint64_t ts) const { return event->mono_time < ts; } + constexpr bool operator()(uint64_t ts, const CanEvent *const event) const { return ts < event->mono_time; } +}; + +using MessageEventsMap = std::unordered_map>; +using CanEventIter = std::vector::const_iterator; diff --git a/openpilot/tools/cabana/core/color.h b/openpilot/tools/cabana/core/color.h new file mode 100644 index 0000000000..c29704dd44 --- /dev/null +++ b/openpilot/tools/cabana/core/color.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +struct CabanaColor { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 255; + + constexpr CabanaColor() = default; + constexpr CabanaColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255) + : r(red), g(green), b(blue), a(alpha) {} + + static CabanaColor fromHsv(float hue, float saturation, float value, float alpha = 1.0f) { + const float h = hue - std::floor(hue); + const float c = value * saturation; + const float x = c * (1.0f - std::fabs(std::fmod(h * 6.0f, 2.0f) - 1.0f)); + const float m = value - c; + float red = 0, green = 0, blue = 0; + switch (static_cast(h * 6.0f) % 6) { + case 0: red = c; green = x; break; + case 1: red = x; green = c; break; + case 2: green = c; blue = x; break; + case 3: green = x; blue = c; break; + case 4: red = x; blue = c; break; + default: red = c; blue = x; break; + } + auto channel = [m](float v) { return static_cast(std::clamp((v + m) * 255.0f, 0.0f, 255.0f) + 0.5f); }; + return {channel(red), channel(green), channel(blue), + static_cast(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f)}; + } + + CabanaColor darker(int factor = 200) const { + if (factor <= 0) return *this; + if (factor < 100) return lighter(10000 / factor); + auto [hue, saturation, value] = hsv(); + return fromHsv(hue, saturation, value * 100.0f / factor, a / 255.0f); + } + + CabanaColor lighter(int factor = 150) const { + if (factor <= 0) return *this; + if (factor < 100) return darker(10000 / factor); + auto [hue, saturation, value] = hsv(); + const float scaled_value = value * factor / 100.0f; + if (scaled_value > 1.0f) saturation = std::max(0.0f, saturation - (scaled_value - 1.0f)); + return fromHsv(hue, saturation, std::min(1.0f, scaled_value), a / 255.0f); + } + + constexpr int red() const { return r; } + constexpr int green() const { return g; } + constexpr int blue() const { return b; } + constexpr int alpha() const { return a; } + float alphaF() const { return a / 255.0f; } + void setAlphaF(float alpha) { a = static_cast(std::clamp(alpha * 255.0f, 0.0f, 255.0f) + 0.5f); } + + constexpr bool operator==(const CabanaColor &other) const { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + +private: + struct Hsv { float hue; float saturation; float value; }; + Hsv hsv() const { + const float red = r / 255.0f, green = g / 255.0f, blue = b / 255.0f; + const float maximum = std::max({red, green, blue}); + const float minimum = std::min({red, green, blue}); + const float delta = maximum - minimum; + float hue = 0; + if (delta > 0) { + if (maximum == red) hue = std::fmod((green - blue) / delta, 6.0f) / 6.0f; + else if (maximum == green) hue = ((blue - red) / delta + 2.0f) / 6.0f; + else hue = ((red - green) / delta + 4.0f) / 6.0f; + if (hue < 0) hue += 1.0f; + } + return {hue, maximum == 0 ? 0 : delta / maximum, maximum}; + } +}; diff --git a/openpilot/tools/cabana/core/message_id.h b/openpilot/tools/cabana/core/message_id.h new file mode 100644 index 0000000000..ecac279631 --- /dev/null +++ b/openpilot/tools/cabana/core/message_id.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include +#include + +constexpr int INVALID_SOURCE = 0xff; + +struct MessageId { + uint8_t source = 0; + uint32_t address = 0; + std::string toString() const { char b[64]; snprintf(b, sizeof(b), "%u:%X", source, address); return b; } + static MessageId fromString(const std::string &s) { + const auto p = s.find(':'); + if (p == std::string::npos) return {}; + return {.source = static_cast(std::stoul(s.substr(0, p))), .address = static_cast(std::stoul(s.substr(p + 1), nullptr, 16))}; + } + bool operator==(const MessageId &o) const { return source == o.source && address == o.address; } + bool operator!=(const MessageId &o) const { return !(*this == o); } + bool operator<(const MessageId &o) const { return std::tie(source, address) < std::tie(o.source, o.address); } + bool operator>(const MessageId &o) const { return o < *this; } +}; + +template <> struct std::hash { + size_t operator()(const MessageId &id) const noexcept { return std::hash{}(id.source) ^ (std::hash{}(id.address) << 1); } +}; diff --git a/openpilot/tools/cabana/core/settings.h b/openpilot/tools/cabana/core/settings.h new file mode 100644 index 0000000000..cad152de92 --- /dev/null +++ b/openpilot/tools/cabana/core/settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +constexpr int LIGHT_THEME = 1; +constexpr int DARK_THEME = 2; + +struct CabanaSettingsState { + enum DragDirection { MsbFirst, LsbFirst, AlwaysLE, AlwaysBE }; + + bool absolute_time = false; + int fps = 10; + int max_cached_minutes = 30; + int chart_height = 200; + int chart_column_count = 1; + int chart_range = 3 * 60; + int chart_series_type = 0; + int theme = 0; + int sparkline_range = 15; + bool multiple_lines_hex = false; + bool log_livestream = true; + bool suppress_defined_signals = false; + std::string log_path; + std::string last_dir; + std::string last_route_dir; + std::vector recent_files; + DragDirection drag_direction = MsbFirst; + + std::string recent_dbc_file; + std::string active_msg_id; + std::vector selected_msg_ids; + std::vector active_charts; +}; diff --git a/openpilot/tools/cabana/dbc/dbc.cc b/openpilot/tools/cabana/dbc/dbc.cc index 8e41cf54e3..c8a794c73f 100644 --- a/openpilot/tools/cabana/dbc/dbc.cc +++ b/openpilot/tools/cabana/dbc/dbc.cc @@ -1,8 +1,18 @@ #include "tools/cabana/dbc/dbc.h" #include +#include -#include "tools/cabana/utils/util.h" +namespace { +int numDecimals(double value) { + int decimals = 0; + while (decimals < 6 && std::fabs(value - std::round(value)) > 1e-9) { + value *= 10.0; + ++decimals; + } + return decimals; +} +} // cabana::Msg @@ -135,8 +145,8 @@ void cabana::Signal::update() { float s = 0.25 + 0.25 * (float)(hash & 0xff) / 255.0; float v = 0.75 + 0.25 * (float)((hash >> 8) & 0xff) / 255.0; - color = QColor::fromHsvF(h, s, v); - precision = std::max(num_decimals(factor), num_decimals(offset)); + color = CabanaColor::fromHsv(h, s, v); + precision = std::max(numDecimals(factor), numDecimals(offset)); } std::string cabana::Signal::formatValue(double value, bool with_unit) const { diff --git a/openpilot/tools/cabana/dbc/dbc.h b/openpilot/tools/cabana/dbc/dbc.h index a10e7871fe..585325d391 100644 --- a/openpilot/tools/cabana/dbc/dbc.h +++ b/openpilot/tools/cabana/dbc/dbc.h @@ -8,58 +8,14 @@ #include #include -#include -#include +#include "tools/cabana/core/color.h" +#include "tools/cabana/core/message_id.h" const std::string UNTITLED = "untitled"; const std::string DEFAULT_NODE_NAME = "XXX"; constexpr int CAN_MAX_DATA_BYTES = 64; -struct MessageId { - uint8_t source = 0; - uint32_t address = 0; - - std::string toString() const { - char buf[64]; - snprintf(buf, sizeof(buf), "%u:%X", source, address); - return buf; - } - - inline static MessageId fromString(const std::string &str) { - auto pos = str.find(':'); - if (pos == std::string::npos) return {}; - return MessageId{.source = uint8_t(std::stoul(str.substr(0, pos))), - .address = uint32_t(std::stoul(str.substr(pos + 1), nullptr, 16))}; - } - - bool operator==(const MessageId &other) const { - return source == other.source && address == other.address; - } - - bool operator!=(const MessageId &other) const { - return !(*this == other); - } - - bool operator<(const MessageId &other) const { - return std::tie(source, address) < std::tie(other.source, other.address); - } - - bool operator>(const MessageId &other) const { - return std::tie(source, address) > std::tie(other.source, other.address); - } -}; - -Q_DECLARE_METATYPE(MessageId); - -template <> -struct std::hash { - std::size_t operator()(const MessageId &k) const noexcept { - return std::hash{}(k.source) ^ (std::hash{}(k.address) << 1); - } -}; - typedef std::vector> ValueDescription; -Q_DECLARE_METATYPE(ValueDescription); namespace cabana { @@ -92,7 +48,7 @@ public: std::string receiver_name; ValueDescription val_desc; int precision = 0; - QColor color; + CabanaColor color; // Multiplexed int multiplex_value = 0; diff --git a/openpilot/tools/cabana/dbc/dbcfile.cc b/openpilot/tools/cabana/dbc/dbcfile.cc index d9c129ee81..99a5b71822 100644 --- a/openpilot/tools/cabana/dbc/dbcfile.cc +++ b/openpilot/tools/cabana/dbc/dbcfile.cc @@ -1,23 +1,60 @@ #include "tools/cabana/dbc/dbcfile.h" -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include -DBCFile::DBCFile(const std::string &dbc_file_name) { - QFile file(QString::fromStdString(dbc_file_name)); - if (file.open(QIODevice::ReadOnly)) { - name_ = QFileInfo(QString::fromStdString(dbc_file_name)).baseName().toStdString(); - filename = dbc_file_name; - parse(file.readAll()); - } else { - throw std::runtime_error("Failed to open file."); - } +namespace { + +std::string trim(const std::string &value) { + const auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return {}; + return value.substr(first, value.find_last_not_of(" \t\r\n") - first + 1); } -DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name), filename("") { - parse(QString::fromStdString(content)); +bool startsWith(const std::string &value, const char *prefix) { + return value.rfind(prefix, 0) == 0; +} + +std::string unescapeComment(std::string value) { + for (size_t pos = 0; (pos = value.find("\\\"", pos)) != std::string::npos; ++pos) { + value.replace(pos, 2, "\""); + } + return trim(value); +} + +bool commentComplete(const std::string &line) { + bool escaped = false; + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] == '\\' && !escaped) { + escaped = true; + continue; + } + if (line[i] == '"' && !escaped) { + size_t next = line.find_first_not_of(" \t\r\n", i + 1); + if (next != std::string::npos && line[next] == ';') return true; + } + escaped = false; + } + return false; +} + +} // namespace + +DBCFile::DBCFile(const std::string &dbc_file_name) { + std::ifstream file(dbc_file_name, std::ios::binary); + if (!file) throw std::runtime_error("Failed to open file."); + filename = dbc_file_name; + name_ = std::filesystem::path(dbc_file_name).stem().string(); + parse(std::string(std::istreambuf_iterator(file), std::istreambuf_iterator())); +} + +DBCFile::DBCFile(const std::string &name, const std::string &content) : name_(name) { + parse(content); } bool DBCFile::save() { @@ -31,15 +68,14 @@ bool DBCFile::saveAs(const std::string &new_filename) { } bool DBCFile::writeContents(const std::string &fn) { - QFile file(QString::fromStdString(fn)); - if (file.open(QIODevice::WriteOnly)) { - std::string content = generateDBC(); - return file.write(content.c_str(), content.size()) >= 0; - } - return false; + std::ofstream file(fn, std::ios::binary | std::ios::trunc); + if (!file) return false; + file << generateDBC(); + return file.good(); } -void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size, const std::string &node, const std::string &comment) { +void DBCFile::updateMsg(const MessageId &id, const std::string &name, uint32_t size, + const std::string &node, const std::string &comment) { auto &m = msgs[id.address]; m.address = id.address; m.name = name; @@ -55,178 +91,143 @@ cabana::Msg *DBCFile::msg(uint32_t address) { cabana::Msg *DBCFile::msg(const std::string &name) { auto it = std::find_if(msgs.begin(), msgs.end(), [&name](auto &m) { return m.second.name == name; }); - return it != msgs.end() ? &(it->second) : nullptr; + return it != msgs.end() ? &it->second : nullptr; } cabana::Signal *DBCFile::signal(uint32_t address, const std::string &name) { auto m = msg(address); - return m ? (cabana::Signal *)m->sig(name) : nullptr; + return m ? m->sig(name) : nullptr; } -void DBCFile::parse(const QString &content) { +void DBCFile::parse(const std::string &content) { msgs.clear(); - - int line_num = 0; - QString line; + header.clear(); + std::istringstream input(content); + std::string raw_line; cabana::Msg *current_msg = nullptr; int multiplexor_cnt = 0; + int line_num = 0; bool seen_first = false; - QTextStream stream((QString *)&content); - while (!stream.atEnd()) { + while (std::getline(input, raw_line)) { ++line_num; - QString raw_line = stream.readLine(); - line = raw_line.trimmed(); + const size_t first_nonspace = raw_line.find_first_not_of(" \t\r"); + std::string line = first_nonspace == std::string::npos ? std::string() : raw_line.substr(first_nonspace); + const int statement_line = line_num; + if ((startsWith(line, "CM_ BO_") || startsWith(line, "CM_ SG_ ")) && !commentComplete(line)) { + std::string continuation; + while (std::getline(input, continuation)) { + ++line_num; + line += "\n" + continuation; + if (commentComplete(line)) break; + } + } bool seen = true; try { - if (line.startsWith("BO_ ")) { + if (startsWith(line, "BO_ ")) { multiplexor_cnt = 0; current_msg = parseBO(line); - } else if (line.startsWith("SG_ ")) { + } else if (startsWith(line, "SG_ ")) { parseSG(line, current_msg, multiplexor_cnt); - } else if (line.startsWith("VAL_ ")) { + } else if (startsWith(line, "VAL_ ")) { parseVAL(line); - } else if (line.startsWith("CM_ BO_")) { - parseCM_BO(line, content, raw_line, stream); - } else if (line.startsWith("CM_ SG_ ")) { - parseCM_SG(line, content, raw_line, stream); + } else if (startsWith(line, "CM_ BO_")) { + parseCM_BO(line); + } else if (startsWith(line, "CM_ SG_ ")) { + parseCM_SG(line); } else { seen = false; } - } catch (std::exception &e) { - throw std::runtime_error(QString("[%1:%2]%3: %4").arg(QString::fromStdString(filename)).arg(line_num).arg(e.what()).arg(line).toStdString()); - } - - if (seen) { - seen_first = true; - } else if (!seen_first) { - header += raw_line.toStdString() + "\n"; + } catch (const std::exception &e) { + throw std::runtime_error("[" + filename + ":" + std::to_string(statement_line) + "]" + e.what() + ": " + line); } + if (seen) seen_first = true; + else if (!seen_first) header += raw_line + "\n"; } - - for (auto &[_, m] : msgs) { - m.update(); - } + for (auto &[_, message] : msgs) message.update(); } -cabana::Msg *DBCFile::parseBO(const QString &line) { - static QRegularExpression bo_regexp(R"(^BO_ (?
\w+) (?\w+) *: (?\w+) (?\w+))"); - - QRegularExpressionMatch match = bo_regexp.match(line); - if (!match.hasMatch()) - throw std::runtime_error("Invalid BO_ line format"); - - uint32_t address = match.captured("address").toUInt(); - if (msgs.count(address) > 0) - throw std::runtime_error(QString("Duplicate message address: %1").arg(address).toStdString()); - - // Create a new message object - cabana::Msg *msg = &msgs[address]; - msg->address = address; - msg->name = match.captured("name").toStdString(); - msg->size = match.captured("size").toULong(); - msg->transmitter = match.captured("transmitter").trimmed().toStdString(); - return msg; +cabana::Msg *DBCFile::parseBO(const std::string &line) { + static const std::regex pattern(R"(^BO_ ([[:alnum:]_]+) ([[:alnum:]_]+) *: ([[:alnum:]_]+) ([[:alnum:]_]+))"); + std::smatch match; + if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid BO_ line format"); + const uint32_t address = std::stoul(match[1].str()); + if (msgs.count(address)) throw std::runtime_error("Duplicate message address: " + std::to_string(address)); + auto &message = msgs[address]; + message.address = address; + message.name = match[2].str(); + message.size = std::stoul(match[3].str()); + message.transmitter = trim(match[4].str()); + return &message; } -void DBCFile::parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) { - static QRegularExpression msg_comment_regexp(R"(^CM_ BO_ *(?
\w+) *\"(?(?:[^"\\]|\\.)*)\"\s*;)"); +void DBCFile::parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt) { + static const std::regex pattern(R"dbc(^SG_ ([[:alnum:]_]+)(?: +([[:alnum:]_]+))? *: ([0-9]+)\|([0-9]+)@([0-9]+)([+-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] "(.*)" (.*))dbc"); + if (!current_msg) throw std::runtime_error("No Message"); + std::smatch match; + if (!std::regex_search(line, match, pattern)) throw std::runtime_error("Invalid SG_ line format"); + if (current_msg->sig(match[1].str())) throw std::runtime_error("Duplicate signal name"); - QString parse_line = line; - if (!parse_line.endsWith("\";")) { - int pos = stream.pos() - raw_line.length() - 1; - parse_line = content.mid(pos, content.indexOf("\";", pos)); - } - auto match = msg_comment_regexp.match(parse_line); - if (!match.hasMatch()) - throw std::runtime_error("Invalid message comment format"); - - if (auto m = (cabana::Msg *)msg(match.captured("address").toUInt())) - m->comment = match.captured("comment").trimmed().replace("\\\"", "\"").toStdString(); -} - -void DBCFile::parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt) { - static QRegularExpression sg_regexp(R"(^SG_ (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); - static QRegularExpression sgm_regexp(R"(^SG_ (\w+) (\w+) *: (\d+)\|(\d+)@(\d+)([\+|\-]) \(([0-9.+\-eE]+),([0-9.+\-eE]+)\) \[([0-9.+\-eE]+)\|([0-9.+\-eE]+)\] \"(.*)\" (.*))"); - - if (!current_msg) - throw std::runtime_error("No Message"); - - int offset = 0; - auto match = sg_regexp.match(line); - if (!match.hasMatch()) { - match = sgm_regexp.match(line); - offset = 1; - } - if (!match.hasMatch()) - throw std::runtime_error("Invalid SG_ line format"); - - std::string name = match.captured(1).toStdString(); - if (current_msg->sig(name) != nullptr) - throw std::runtime_error("Duplicate signal name"); - - cabana::Signal s{}; - if (offset == 1) { - auto indicator = match.captured(2); + cabana::Signal signal{}; + const std::string indicator = match[2].str(); + if (!indicator.empty()) { if (indicator == "M") { - ++multiplexor_cnt; - // Only one signal within a single message can be the multiplexer switch. - if (multiplexor_cnt >= 2) - throw std::runtime_error("Multiple multiplexor"); - - s.type = cabana::Signal::Type::Multiplexor; + if (++multiplexor_cnt >= 2) throw std::runtime_error("Multiple multiplexor"); + signal.type = cabana::Signal::Type::Multiplexor; } else { - s.type = cabana::Signal::Type::Multiplexed; - s.multiplex_value = indicator.mid(1).toInt(); + signal.type = cabana::Signal::Type::Multiplexed; + signal.multiplex_value = indicator.size() > 1 ? std::stoi(indicator.substr(1)) : 0; } } - s.name = name; - s.start_bit = match.captured(offset + 2).toInt(); - s.size = match.captured(offset + 3).toInt(); - s.is_little_endian = match.captured(offset + 4).toInt() == 1; - s.is_signed = match.captured(offset + 5) == "-"; - s.factor = match.captured(offset + 6).toDouble(); - s.offset = match.captured(offset + 7).toDouble(); - s.min = match.captured(8 + offset).toDouble(); - s.max = match.captured(9 + offset).toDouble(); - s.unit = match.captured(10 + offset).toStdString(); - s.receiver_name = match.captured(11 + offset).trimmed().toStdString(); - current_msg->sigs.push_back(new cabana::Signal(s)); + signal.name = match[1].str(); + signal.start_bit = std::stoi(match[3].str()); + signal.size = std::stoi(match[4].str()); + signal.is_little_endian = match[5].str() == "1"; + signal.is_signed = match[6].str() == "-"; + signal.factor = std::stod(match[7].str()); + signal.offset = std::stod(match[8].str()); + signal.min = std::stod(match[9].str()); + signal.max = std::stod(match[10].str()); + signal.unit = match[11].str(); + signal.receiver_name = trim(match[12].str()); + current_msg->sigs.push_back(new cabana::Signal(signal)); } -void DBCFile::parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream) { - static QRegularExpression sg_comment_regexp(R"(^CM_ SG_ *(\w+) *(\w+) *\"((?:[^"\\]|\\.)*)\"\s*;)"); - - QString parse_line = line; - if (!parse_line.endsWith("\";")) { - int pos = stream.pos() - raw_line.length() - 1; - parse_line = content.mid(pos, content.indexOf("\";", pos)); +void DBCFile::parseCM_BO(const std::string &line) { + std::istringstream prefix(line.substr(7)); + uint32_t address = 0; + prefix >> address; + const size_t first_quote = line.find('"'); + const size_t last_quote = line.rfind('"'); + if (!prefix || first_quote == std::string::npos || last_quote <= first_quote) { + throw std::runtime_error("Invalid message comment format"); } - auto match = sg_comment_regexp.match(parse_line); - if (!match.hasMatch()) + if (auto message = msg(address)) message->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1)); +} + +void DBCFile::parseCM_SG(const std::string &line) { + std::istringstream prefix(line.substr(7)); + uint32_t address = 0; + std::string name; + prefix >> address >> name; + const size_t first_quote = line.find('"'); + const size_t last_quote = line.rfind('"'); + if (!prefix || name.empty() || first_quote == std::string::npos || last_quote <= first_quote) { throw std::runtime_error("Invalid CM_ SG_ line format"); - - if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) { - s->comment = match.captured(3).trimmed().replace("\\\"", "\"").toStdString(); } + if (auto sig = signal(address, name)) sig->comment = unescapeComment(line.substr(first_quote + 1, last_quote - first_quote - 1)); } -void DBCFile::parseVAL(const QString &line) { - static QRegularExpression val_regexp(R"(VAL_ (\w+) (\w+) (\s*[-+]?[0-9]+\s+\".+?\"[^;]*))"); - - auto match = val_regexp.match(line); - if (!match.hasMatch()) - throw std::runtime_error("invalid VAL_ line format"); - - if (auto s = signal(match.captured(1).toUInt(), match.captured(2).toStdString())) { - QStringList desc_list = match.captured(3).trimmed().split('"'); - for (int i = 0; i < desc_list.size(); i += 2) { - auto val = desc_list[i].trimmed(); - if (!val.isEmpty() && (i + 1) < desc_list.size()) { - auto desc = desc_list[i + 1].trimmed(); - s->val_desc.push_back({val.toDouble(), desc.toStdString()}); - } +void DBCFile::parseVAL(const std::string &line) { + static const std::regex header_pattern(R"(^VAL_ ([[:alnum:]_]+) ([[:alnum:]_]+) (.*))"); + static const std::regex entry_pattern(R"dbc(([+-]?[0-9]+(?:\.[0-9]+)?)\s+"([^"]*)")dbc"); + std::smatch match; + if (!std::regex_search(line, match, header_pattern)) throw std::runtime_error("invalid VAL_ line format"); + if (auto sig = signal(std::stoul(match[1].str()), match[2].str())) { + const std::string entries = match[3].str(); + for (std::sregex_iterator it(entries.begin(), entries.end(), entry_pattern), end; it != end; ++it) { + sig->val_desc.emplace_back(std::stod((*it)[1].str()), trim((*it)[2].str())); } } } @@ -237,40 +238,29 @@ std::string DBCFile::generateDBC() { const std::string &transmitter = m.transmitter.empty() ? DEFAULT_NODE_NAME : m.transmitter; dbc_string += "BO_ " + std::to_string(address) + " " + m.name + ": " + std::to_string(m.size) + " " + transmitter + "\n"; if (!m.comment.empty()) { - std::string escaped_comment = m.comment; - // Replace " with \" - for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2) - escaped_comment.replace(pos, 1, "\\\""); - comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped_comment + "\";\n"; + std::string escaped = m.comment; + for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\""); + comment += "CM_ BO_ " + std::to_string(address) + " \"" + escaped + "\";\n"; } for (auto sig : m.getSignals()) { - std::string multiplexer_indicator; - if (sig->type == cabana::Signal::Type::Multiplexor) { - multiplexer_indicator = "M "; - } else if (sig->type == cabana::Signal::Type::Multiplexed) { - multiplexer_indicator = "m" + std::to_string(sig->multiplex_value) + " "; - } - const std::string &recv = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name; - dbc_string += " SG_ " + sig->name + " " + multiplexer_indicator + ": " + - std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" + - std::string(1, sig->is_little_endian ? '1' : '0') + - std::string(1, sig->is_signed ? '-' : '+') + + std::string mux; + if (sig->type == cabana::Signal::Type::Multiplexor) mux = "M "; + else if (sig->type == cabana::Signal::Type::Multiplexed) mux = "m" + std::to_string(sig->multiplex_value) + " "; + const std::string &receiver = sig->receiver_name.empty() ? DEFAULT_NODE_NAME : sig->receiver_name; + dbc_string += " SG_ " + sig->name + " " + mux + ": " + std::to_string(sig->start_bit) + "|" + std::to_string(sig->size) + "@" + + (sig->is_little_endian ? "1" : "0") + (sig->is_signed ? "-" : "+") + " (" + doubleToString(sig->factor) + "," + doubleToString(sig->offset) + ")" + - " [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "]" + - " \"" + sig->unit + "\" " + recv + "\n"; + " [" + doubleToString(sig->min) + "|" + doubleToString(sig->max) + "] \"" + sig->unit + "\" " + receiver + "\n"; if (!sig->comment.empty()) { - std::string escaped_comment = sig->comment; - for (size_t pos = 0; (pos = escaped_comment.find('"', pos)) != std::string::npos; pos += 2) - escaped_comment.replace(pos, 1, "\\\""); - comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped_comment + "\";\n"; + std::string escaped = sig->comment; + for (size_t pos = 0; (pos = escaped.find('"', pos)) != std::string::npos; pos += 2) escaped.replace(pos, 1, "\\\""); + comment += "CM_ SG_ " + std::to_string(address) + " " + sig->name + " \"" + escaped + "\";\n"; } if (!sig->val_desc.empty()) { std::string text; - for (auto &[val, desc] : sig->val_desc) { + for (const auto &[value, description] : sig->val_desc) { if (!text.empty()) text += " "; - char val_buf[64]; - snprintf(val_buf, sizeof(val_buf), "%g", val); - text += std::string(val_buf) + " \"" + desc + "\""; + text += doubleToString(value) + " \"" + description + "\""; } val_desc += "VAL_ " + std::to_string(address) + " " + sig->name + " " + text + ";\n"; } diff --git a/openpilot/tools/cabana/dbc/dbcfile.h b/openpilot/tools/cabana/dbc/dbcfile.h index decb566abd..13841a94fa 100644 --- a/openpilot/tools/cabana/dbc/dbcfile.h +++ b/openpilot/tools/cabana/dbc/dbcfile.h @@ -2,7 +2,6 @@ #include #include -#include #include "tools/cabana/dbc/dbc.h" @@ -32,12 +31,12 @@ public: std::string filename; private: - void parse(const QString &content); - cabana::Msg *parseBO(const QString &line); - void parseSG(const QString &line, cabana::Msg *current_msg, int &multiplexor_cnt); - void parseCM_BO(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream); - void parseCM_SG(const QString &line, const QString &content, const QString &raw_line, const QTextStream &stream); - void parseVAL(const QString &line); + void parse(const std::string &content); + cabana::Msg *parseBO(const std::string &line); + void parseSG(const std::string &line, cabana::Msg *current_msg, int &multiplexor_cnt); + void parseCM_BO(const std::string &line); + void parseCM_SG(const std::string &line); + void parseVAL(const std::string &line); std::string header; std::map msgs; diff --git a/openpilot/tools/cabana/dbc/dbcmanager.cc b/openpilot/tools/cabana/dbc/dbcmanager.cc index 2236a93da1..7a95a4f809 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.cc +++ b/openpilot/tools/cabana/dbc/dbcmanager.cc @@ -1,9 +1,10 @@ #include "tools/cabana/dbc/dbcmanager.h" #include +#include #include -bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, QString *error) { +bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error) { try { auto it = std::find_if(dbc_files.begin(), dbc_files.end(), [&](auto &f) { return f.second && f.second->filename == dbc_file_name; }); @@ -16,11 +17,11 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name return false; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); return true; } -bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error) { +bool DBCManager::open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error) { try { auto file = std::make_shared(name, content); for (auto s : sources) { @@ -31,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s return false; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); return true; } @@ -39,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) { for (auto s : sources) { dbc_files[s] = nullptr; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::close(DBCFile *dbc_file) { for (auto &[_, f] : dbc_files) { if (f.get() == dbc_file) f = nullptr; } - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::closeAll() { dbc_files.clear(); - emit DBCFileChanged(); + if (callbacks_.file_changed) callbacks_.file_changed(); } void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->addSignal(sig)) { - emit signalAdded(id, s); - emit maskUpdated(); + if (callbacks_.signal_added) callbacks_.signal_added(id, s); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -66,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->updateSignal(sig_name, sig)) { - emit signalUpdated(s); - emit maskUpdated(); + if (callbacks_.signal_updated) callbacks_.signal_updated(s); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -75,9 +76,9 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) { if (auto m = msg(id)) { if (auto s = m->sig(sig_name)) { - emit signalRemoved(s); + if (callbacks_.signal_removed) callbacks_.signal_removed(s); m->removeSignal(sig_name); - emit maskUpdated(); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } } } @@ -86,15 +87,15 @@ void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_ auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->updateMsg(id, name, size, node, comment); - emit msgUpdated(id); + if (callbacks_.msg_updated) callbacks_.msg_updated(id); } void DBCManager::removeMsg(const MessageId &id) { auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->removeMsg(id); - emit msgRemoved(id); - emit maskUpdated(); + if (callbacks_.msg_removed) callbacks_.msg_removed(id); + if (callbacks_.mask_updated) callbacks_.mask_updated(); } std::string DBCManager::newMsgName(const MessageId &id) { @@ -176,6 +177,6 @@ std::string toString(const SourceSet &ss) { } DBCManager *dbc() { - static DBCManager dbc_manager(nullptr); + static DBCManager dbc_manager; return &dbc_manager; } diff --git a/openpilot/tools/cabana/dbc/dbcmanager.h b/openpilot/tools/cabana/dbc/dbcmanager.h index 4a122073ea..5a09fae03d 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.h +++ b/openpilot/tools/cabana/dbc/dbcmanager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -11,17 +11,23 @@ typedef std::set SourceSet; const SourceSet SOURCE_ALL = {-1}; -const int INVALID_SOURCE = 0xff; inline bool operator<(const std::shared_ptr &l, const std::shared_ptr &r) { return l.get() < r.get(); } -class DBCManager : public QObject { - Q_OBJECT - +class DBCManager { public: - DBCManager(QObject *parent) : QObject(parent) {} - ~DBCManager() {} - bool open(const SourceSet &sources, const std::string &dbc_file_name, QString *error = nullptr); - bool open(const SourceSet &sources, const std::string &name, const std::string &content, QString *error = nullptr); + struct Callbacks { + std::function signal_added; + std::function signal_removed; + std::function signal_updated; + std::function msg_updated; + std::function msg_removed; + std::function file_changed; + std::function mask_updated; + }; + + DBCManager() = default; + bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr); + bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr); void close(const SourceSet &sources); void close(DBCFile *dbc_file); void closeAll(); @@ -48,18 +54,11 @@ public: DBCFile *findDBCFile(const uint8_t source); inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); } std::set allDBCFiles(); - -signals: - void signalAdded(MessageId id, const cabana::Signal *sig); - void signalRemoved(const cabana::Signal *sig); - void signalUpdated(const cabana::Signal *sig); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id); - void DBCFileChanged(); - void maskUpdated(); + void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } private: std::map> dbc_files; + Callbacks callbacks_; }; DBCManager *dbc(); diff --git a/openpilot/tools/cabana/dbc/dbcqt.cc b/openpilot/tools/cabana/dbc/dbcqt.cc new file mode 100644 index 0000000000..4354caf3f5 --- /dev/null +++ b/openpilot/tools/cabana/dbc/dbcqt.cc @@ -0,0 +1,18 @@ +#include "tools/cabana/dbc/dbcqt.h" + +QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) { + dbc()->setCallbacks({ + .signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); }, + .signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); }, + .signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); }, + .msg_updated = [this](MessageId id) { emit msgUpdated(id); }, + .msg_removed = [this](MessageId id) { emit msgRemoved(id); }, + .file_changed = [this]() { emit DBCFileChanged(); }, + .mask_updated = [this]() { emit maskUpdated(); }, + }); +} + +QtDBCNotifier *dbcNotifier() { + static QtDBCNotifier notifier; + return ¬ifier; +} diff --git a/openpilot/tools/cabana/dbc/dbcqt.h b/openpilot/tools/cabana/dbc/dbcqt.h new file mode 100644 index 0000000000..b889f854ed --- /dev/null +++ b/openpilot/tools/cabana/dbc/dbcqt.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "tools/cabana/dbc/dbcmanager.h" + +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + +class QtDBCNotifier : public QObject { + Q_OBJECT + +public: + explicit QtDBCNotifier(QObject *parent = nullptr); + +signals: + void signalAdded(MessageId id, const cabana::Signal *sig); + void signalRemoved(const cabana::Signal *sig); + void signalUpdated(const cabana::Signal *sig); + void msgUpdated(MessageId id); + void msgRemoved(MessageId id); + void DBCFileChanged(); + void maskUpdated(); +}; + +QtDBCNotifier *dbcNotifier(); diff --git a/openpilot/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc index 148b059e5b..36a95ff96e 100644 --- a/openpilot/tools/cabana/detailwidget.cc +++ b/openpilot/tools/cabana/detailwidget.cc @@ -1,4 +1,5 @@ #include "tools/cabana/detailwidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -56,7 +57,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight); QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { diff --git a/openpilot/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc index fb79ff9cea..51c2481f3b 100644 --- a/openpilot/tools/cabana/historylog.cc +++ b/openpilot/tools/cabana/historylog.cc @@ -1,4 +1,5 @@ #include "tools/cabana/historylog.h" +#include "tools/cabana/dbc/dbcqt.h" #include @@ -54,7 +55,7 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i return unit.isEmpty() ? name : QString("%1 (%2)").arg(name, unit); } else if (role == Qt::BackgroundRole && section > 0 && !isHexMode()) { // Alpha-blend the signal color with the background to ensure contrast - QColor sigColor = sigs[section - 1]->color; + QColor sigColor = toQColor(sigs[section - 1]->color); sigColor.setAlpha(128); return QBrush(sigColor); } @@ -207,7 +208,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged); QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &HistoryLogModel::reset); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &HistoryLogModel::reset); QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); @@ -238,11 +239,11 @@ void LogsWidget::filterChanged() { } void LogsWidget::exportToCSV() { - QString dir = QString("%1/%2_%3.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id))); + QString dir = QString("%1/%2_%3.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())).arg(QString::fromStdString(msgName(model->msg_id))); QString fn = QFileDialog::getSaveFileName(this, QString("Export %1 to CSV file").arg(QString::fromStdString(msgName(model->msg_id))), dir, tr("csv (*.csv)")); if (!fn.isEmpty()) { - model->isHexMode() ? utils::exportToCSV(fn, model->msg_id) - : utils::exportSignalsToCSV(fn, model->msg_id); + model->isHexMode() ? utils::exportToCSV(fn.toStdString(), model->msg_id) + : utils::exportSignalsToCSV(fn.toStdString(), model->msg_id); } } diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 1ac6e5bbad..1d3200b200 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -40,7 +40,7 @@ public: uint64_t mono_time = 0; std::vector sig_values; std::vector data; - std::vector colors; + std::vector colors; }; void fetchData(std::deque::iterator insert_pos, uint64_t from_time, uint64_t min_time); diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 39fb979c79..8d932163a6 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -1,4 +1,5 @@ #include "tools/cabana/mainwin.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -65,7 +66,7 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged); QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); @@ -253,16 +254,16 @@ void MainWindow::selectAndOpenStream() { void MainWindow::closeStream() { openStream(new DummyStream(this)); if (dbc()->nonEmptyDBCCount() > 0) { - emit dbc()->DBCFileChanged(); + emit dbcNotifier()->DBCFileChanged(); } statusBar()->showMessage(tr("stream closed")); } void MainWindow::exportToCSV() { - QString dir = QString("%1/%2.csv").arg(settings.last_dir).arg(QString::fromStdString(can->routeName())); + QString dir = QString("%1/%2.csv").arg(QString::fromStdString(settings.last_dir)).arg(QString::fromStdString(can->routeName())); QString fn = QFileDialog::getSaveFileName(this, "Export stream to CSV file", dir, tr("csv (*.csv)")); if (!fn.isEmpty()) { - utils::exportToCSV(fn); + utils::exportToCSV(fn.toStdString()); } } @@ -273,7 +274,7 @@ void MainWindow::newFile(SourceSet s) { void MainWindow::openFile(SourceSet s) { remindSaveChanges(); - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); if (!fn.isEmpty()) { loadFile(fn, s); } @@ -283,13 +284,13 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) { if (!fn.isEmpty()) { closeFile(s); - QString error; + std::string error; if (dbc()->open(s, fn.toStdString(), &error)) { updateRecentFiles(fn); statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); } else { QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC file"), tr("Failed to parse DBC file %1").arg(fn)); - msg_box.setDetailedText(error); + msg_box.setDetailedText(QString::fromStdString(error)); msg_box.exec(); } } @@ -303,13 +304,13 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { closeFile(s); QString dbc_str = QGuiApplication::clipboard()->text(); - QString error; + std::string error; bool ret = dbc()->open(s, std::string(""), dbc_str.toStdString(), &error); if (ret && dbc()->nonEmptyDBCCount() > 0) { QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); } else { QMessageBox msg_box(QMessageBox::Warning, tr("Failed to load DBC from clipboard"), tr("Make sure that you paste the text with correct format.")); - msg_box.setDetailedText(error); + msg_box.setDetailedText(QString::fromStdString(error)); msg_box.exec(); } } @@ -427,7 +428,7 @@ void MainWindow::saveFile(DBCFile *dbc_file) { void MainWindow::saveFileAs(DBCFile *dbc_file) { QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file)))); - QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)")); + QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(QString::fromStdString(settings.last_dir) + "/untitled.dbc"), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { dbc_file->saveAs(fn.toStdString()); UndoStack::instance()->setClean(); @@ -481,12 +482,13 @@ void MainWindow::updateLoadSaveMenus() { } void MainWindow::updateRecentFiles(const QString &fn) { - settings.recent_files.removeAll(fn); - settings.recent_files.prepend(fn); + const std::string filename = fn.toStdString(); + settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), filename), settings.recent_files.end()); + settings.recent_files.insert(settings.recent_files.begin(), filename); while (settings.recent_files.size() > MAX_RECENT_FILES) { - settings.recent_files.removeLast(); + settings.recent_files.pop_back(); } - settings.last_dir = QFileInfo(fn).absolutePath(); + settings.last_dir = QFileInfo(fn).absolutePath().toStdString(); } void MainWindow::updateRecentFileMenu() { @@ -499,8 +501,8 @@ void MainWindow::updateRecentFileMenu() { } for (int i = 0; i < num_recent_files; ++i) { - QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(settings.recent_files[i]).fileName()); - open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(file); }); + QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(QString::fromStdString(settings.recent_files[i])).fileName()); + open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); }); } } @@ -627,30 +629,39 @@ void MainWindow::saveSessionState() { settings.active_charts.clear(); for (auto &f : dbc()->allDBCFiles()) - if (!f->isEmpty()) { settings.recent_dbc_file = QString::fromStdString(f->filename); break; } + if (!f->isEmpty()) { settings.recent_dbc_file = f->filename; break; } if (auto *detail = center_widget->getDetailWidget()) { auto [active_id, ids] = detail->serializeMessageIds(); - settings.active_msg_id = active_id; - settings.selected_msg_ids = ids; + settings.active_msg_id = active_id.toStdString(); + settings.selected_msg_ids.clear(); + for (const auto &id : ids) settings.selected_msg_ids.push_back(id.toStdString()); + } + if (charts_widget) { + settings.active_charts.clear(); + for (const auto &id : charts_widget->serializeChartIds()) settings.active_charts.push_back(id.toStdString()); } - if (charts_widget) - settings.active_charts = charts_widget->serializeChartIds(); } void MainWindow::restoreSessionState() { - if (settings.recent_dbc_file.isEmpty() || dbc()->nonEmptyDBCCount() == 0) return; + if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return; QString dbc_file; for (auto& f : dbc()->allDBCFiles()) if (!f->isEmpty()) { dbc_file = QString::fromStdString(f->filename); break; } - if (dbc_file != settings.recent_dbc_file) return; + if (dbc_file.toStdString() != settings.recent_dbc_file) return; - if (!settings.selected_msg_ids.isEmpty()) - center_widget->ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids); + if (!settings.selected_msg_ids.empty()) { + QStringList ids; + for (const auto &id : settings.selected_msg_ids) ids.push_back(QString::fromStdString(id)); + center_widget->ensureDetailWidget()->restoreTabs(QString::fromStdString(settings.active_msg_id), ids); + } - if (charts_widget != nullptr && !settings.active_charts.empty()) - charts_widget->restoreChartsFromIds(settings.active_charts); + if (charts_widget != nullptr && !settings.active_charts.empty()) { + QStringList ids; + for (const auto &id : settings.active_charts) ids.push_back(QString::fromStdString(id)); + charts_widget->restoreChartsFromIds(ids); + } } // HelpOverlay diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index 75cdaa7cc3..22de1350ec 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -1,4 +1,5 @@ #include "tools/cabana/messageswidget.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -43,7 +44,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index e7b1129a30..e385d83eca 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -17,8 +17,31 @@ const int MAX_CACHE_MINIUTES = 120; Settings settings; +template +void readSetting(QSettings &settings_store, const char *key, T &value) { + if (auto stored = settings_store.value(key); stored.canConvert()) value = stored.value(); +} + +void readSetting(QSettings &settings_store, const char *key, std::string &value) { + value = settings_store.value(key, QString::fromStdString(value)).toString().toStdString(); +} + +void readSetting(QSettings &settings_store, const char *key, std::vector &value) { + value.clear(); + for (const auto &item : settings_store.value(key).toStringList()) value.push_back(item.toStdString()); +} + +template +void writeSetting(QSettings &settings_store, const char *key, const T &value) { settings_store.setValue(key, value); } +void writeSetting(QSettings &settings_store, const char *key, const std::string &value) { settings_store.setValue(key, QString::fromStdString(value)); } +void writeSetting(QSettings &settings_store, const char *key, const std::vector &value) { + QStringList items; + for (const auto &item : value) items.push_back(QString::fromStdString(item)); + settings_store.setValue(key, items); +} + template -void settings_op(SettingOperation op) { +void settingsOp(SettingOperation op) { QSettings s("cabana"); op(s, "absolute_time", settings.absolute_time); op(s, "fps", settings.fps); @@ -48,16 +71,13 @@ void settings_op(SettingOperation op) { } Settings::Settings() { - last_dir = last_route_dir = QDir::homePath(); - log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; - settings_op([](QSettings &s, const QString &key, auto &value) { - if (auto v = s.value(key); v.canConvert>()) - value = v.value>(); - }); + last_dir = last_route_dir = QDir::homePath().toStdString(); + log_path = (QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/").toStdString(); + settingsOp([](QSettings &s, const char *key, auto &value) { readSetting(s, key, value); }); } Settings::~Settings() { - settings_op([](QSettings &s, const QString &key, auto &v) { s.setValue(key, v); }); + settingsOp([](QSettings &s, const char *key, const auto &value) { writeSetting(s, key, value); }); } // SettingsDlg @@ -102,7 +122,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { log_livestream = new QGroupBox(tr("Enable live stream logging"), this); log_livestream->setCheckable(true); QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); - path_layout->addWidget(log_path = new QLineEdit(settings.log_path, this)); + path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); log_path->setReadOnly(true); auto browse_btn = new QPushButton(tr("B&rowse...")); path_layout->addWidget(browse_btn); @@ -134,7 +154,7 @@ void SettingsDlg::save() { settings.max_cached_minutes = cached_minutes->value(); settings.chart_height = chart_height->value(); settings.log_livestream = log_livestream->isChecked(); - settings.log_path = log_path->text(); + settings.log_path = log_path->text().toStdString(); settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); emit settings.changed(); QDialog::accept(); diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 7ab50d1494..4254a8009d 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -7,50 +7,20 @@ #include #include -#define LIGHT_THEME 1 -#define DARK_THEME 2 +#include "tools/cabana/core/settings.h" -class Settings : public QObject { +class Settings : public QObject, public CabanaSettingsState { Q_OBJECT public: - enum DragDirection { - MsbFirst, - LsbFirst, - AlwaysLE, - AlwaysBE, - }; - Settings(); ~Settings(); - bool absolute_time = false; - int fps = 10; - int max_cached_minutes = 30; - int chart_height = 200; - int chart_column_count = 1; - int chart_range = 3 * 60; // 3 minutes - int chart_series_type = 0; - int theme = 0; - int sparkline_range = 15; // 15 seconds - bool multiple_lines_hex = false; - bool log_livestream = true; - bool suppress_defined_signals = false; - QString log_path; - QString last_dir; - QString last_route_dir; + // Qt frontend layout state. This intentionally stays outside CabanaSettingsState. QByteArray geometry; QByteArray video_splitter_state; QByteArray window_state; - QStringList recent_files; QByteArray message_header_state; - DragDirection drag_direction = MsbFirst; - - // session data - QString recent_dbc_file; - QString active_msg_id; - QStringList selected_msg_ids; - QStringList active_charts; signals: void changed(); diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index a204512e83..5048cc6aa8 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -1,4 +1,5 @@ #include "tools/cabana/signalview.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -25,12 +26,12 @@ static QString signalTypeToString(cabana::Signal::Type type) { } SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &SignalModel::handleMsgChanged); - QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated); - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &SignalModel::refresh); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &SignalModel::handleMsgChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &SignalModel::handleMsgChanged); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalModel::handleSignalAdded); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalModel::handleSignalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &SignalModel::handleSignalRemoved); } void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { @@ -304,7 +305,7 @@ void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op path.addRoundedRect(icon_rect, 3, 3); painter->setPen(item->highlight ? Qt::white : Qt::black); painter->setFont(label_font); - painter->fillPath(path, item->sig->color.darker(item->highlight ? 125 : 0)); + painter->fillPath(path, toQColor(item->sig->color.darker(item->highlight ? 125 : 0))); painter->drawText(icon_rect, Qt::AlignCenter, QString::number(item->row() + 1)); rect.setLeft(icon_rect.right() + h_margin * 2); @@ -481,8 +482,8 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); - QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalView::handleSignalAdded); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalView::handleSignalUpdated); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalView::handleSignalAdded); + QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalView::handleSignalUpdated); QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index 1582fcd34d..c58a98084f 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,4 +1,5 @@ #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -18,8 +19,8 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks); - QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks); + QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks); + QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks); } void AbstractStream::updateMasks() { @@ -233,18 +234,18 @@ std::pair AbstractStream::eventsInRange(const Messag namespace { enum Color { GREYISH_BLUE, CYAN, RED}; -QColor getColor(int c) { +CabanaColor getColor(int c) { constexpr int start_alpha = 128; - static const QColor colors[] = { - [GREYISH_BLUE] = QColor(102, 86, 169, start_alpha / 2), - [CYAN] = QColor(0, 187, 255, start_alpha), - [RED] = QColor(255, 0, 0, start_alpha), + static const CabanaColor colors[] = { + [GREYISH_BLUE] = CabanaColor(102, 86, 169, start_alpha / 2), + [CYAN] = CabanaColor(0, 187, 255, start_alpha), + [RED] = CabanaColor(255, 0, 0, start_alpha), }; return settings.theme == LIGHT_THEME ? colors[c] : colors[c].lighter(135); } -inline QColor blend(const QColor &a, const QColor &b) { - return QColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); +inline CabanaColor blend(const CabanaColor &a, const CabanaColor &b) { + return CabanaColor((a.red() + b.red()) / 2, (a.green() + b.green()) / 2, (a.blue() + b.blue()) / 2, (a.alpha() + b.alpha()) / 2); } // Calculate the frequency from the past one minute data @@ -271,7 +272,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in if (dat.size() != size) { dat.assign(can_data, can_data + size); - colors.assign(size, QColor(0, 0, 0, 0)); + colors.assign(size, CabanaColor(0, 0, 0, 0)); last_changes.resize(size); bit_flip_counts.resize(size); std::for_each(last_changes.begin(), last_changes.end(), [current_sec](auto &c) { c.ts = current_sec; }); @@ -317,7 +318,7 @@ void CanData::compute(const MessageId &msg_id, const uint8_t *can_data, const in last_change.delta = delta; } else { // Fade out - colors[i].setAlphaF(std::max(0.0, colors[i].alphaF() - alpha_delta)); + colors[i].setAlphaF(std::max(0.0f, colors[i].alphaF() - alpha_delta)); } } } diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 2f3b26fe2a..26f05d9846 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -11,51 +12,12 @@ #include #include -#include -#include - #include "openpilot/cereal/messaging/messaging.h" +#include "tools/cabana/core/can_data.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/utils/util.h" #include "tools/replay/util.h" -struct CanData { - void compute(const MessageId &msg_id, const uint8_t *dat, const int size, double current_sec, - double playback_speed, const std::vector &mask, double in_freq = 0); - - double ts = 0.; - uint32_t count = 0; - double freq = 0; - std::vector dat; - std::vector colors; - - struct ByteLastChange { - double ts = 0; - int delta = 0; - int same_delta_counter = 0; - bool suppressed = false; - }; - std::vector last_changes; - std::vector> bit_flip_counts; - double last_freq_update_ts = 0; -}; - -struct CanEvent { - uint8_t src; - uint32_t address; - uint64_t mono_time; - uint8_t size; - uint8_t dat[]; -}; - -struct CompareCanEvent { - constexpr bool operator()(const CanEvent *const e, uint64_t ts) const { return e->mono_time < ts; } - constexpr bool operator()(uint64_t ts, const CanEvent *const e) const { return ts < e->mono_time; } -}; - -typedef std::unordered_map> MessageEventsMap; -using CanEventIter = std::vector::const_iterator; - class AbstractStream : public QObject { Q_OBJECT @@ -67,7 +29,7 @@ public: virtual void seekTo(double ts) {} virtual std::string routeName() const = 0; virtual std::string carFingerprint() const { return ""; } - virtual QDateTime beginDateTime() const { return {}; } + virtual std::chrono::system_clock::time_point beginDateTime() const { return {}; } virtual uint64_t beginMonoTime() const { return 0; } virtual double minSeconds() const { return 0; } virtual double maxSeconds() const { return 0; } diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index ac9a6fa105..5aa4628bc0 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include "common/timing.h" #include "common/util.h" @@ -14,9 +16,14 @@ struct LiveStream::Logger { void write(kj::ArrayPtr data) { int n = (seconds_since_epoch() - start_ts) / 60.0; if (std::exchange(segment_num, n) != segment_num) { + const time_t start_time = start_ts; + std::tm local_time = {}; + localtime_r(&start_time, &local_time); + std::ostringstream date; + date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S"); QString dir = QString("%1/%2--%3") - .arg(settings.log_path) - .arg(QDateTime::fromSecsSinceEpoch(start_ts).toString("yyyy-MM-dd--hh-mm-ss")) + .arg(QString::fromStdString(settings.log_path)) + .arg(QString::fromStdString(date.str())) .arg(n); util::create_directories(dir.toStdString(), 0755); fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); @@ -55,7 +62,7 @@ void LiveStream::startUpdateTimer() { void LiveStream::start() { stream_thread->start(); startUpdateTimer(); - begin_date_time = QDateTime::currentDateTime(); + begin_date_time = std::chrono::system_clock::now(); } void LiveStream::stop() { diff --git a/openpilot/tools/cabana/streams/livestream.h b/openpilot/tools/cabana/streams/livestream.h index 24b9285092..e88fbadd8e 100644 --- a/openpilot/tools/cabana/streams/livestream.h +++ b/openpilot/tools/cabana/streams/livestream.h @@ -16,7 +16,7 @@ public: virtual ~LiveStream(); void start() override; void stop(); - inline QDateTime beginDateTime() const { return begin_date_time; } + inline std::chrono::system_clock::time_point beginDateTime() const override { return begin_date_time; } inline uint64_t beginMonoTime() const override { return begin_event_ts; } double maxSeconds() const override { return std::max(1.0, (lastest_event_ts - begin_event_ts) / 1e9); } void setSpeed(float speed) override { speed_ = speed; } @@ -41,7 +41,7 @@ private: int timer_id; QBasicTimer update_timer; - QDateTime begin_date_time; + std::chrono::system_clock::time_point begin_date_time; uint64_t begin_event_ts = 0; uint64_t lastest_event_ts = 0; uint64_t current_event_ts = 0; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index b00c6e52c6..c74aa49821 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -136,10 +136,10 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p setMinimumWidth(550); QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); if (!dir.isEmpty()) { route_edit->setText(dir); - settings.last_route_dir = QFileInfo(dir).absolutePath(); + settings.last_route_dir = QFileInfo(dir).absolutePath().toStdString(); } }); QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { diff --git a/openpilot/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h index 40f8ec8cfb..eecd345715 100644 --- a/openpilot/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -26,7 +26,9 @@ public: inline std::string carFingerprint() const override { return replay->carFingerprint(); } double minSeconds() const override { return replay->minSeconds(); } double maxSeconds() const { return replay->maxSeconds(); } - inline QDateTime beginDateTime() const { return QDateTime::fromSecsSinceEpoch(replay->routeDateTime()); } + inline std::chrono::system_clock::time_point beginDateTime() const override { + return std::chrono::system_clock::from_time_t(replay->routeDateTime()); + } inline uint64_t beginMonoTime() const override { return replay->routeStartNanos(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } inline float getSpeed() const { return replay->getSpeed(); } diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 4ad552d4b4..75fdf384b2 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -52,10 +52,10 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { setEnabled(true); }); QObject::connect(file_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); if (!fn.isEmpty()) { dbc_file->setText(fn); - settings.last_dir = QFileInfo(fn).absolutePath(); + settings.last_dir = QFileInfo(fn).absolutePath().toStdString(); } }); } diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index 833cfbe4b5..c66f57d593 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -1,9 +1,16 @@ #undef INFO -#include +#include +#include #include "catch2/catch.hpp" +#include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/core/settings.h" + +#ifdef QT_CORE_LIB +#include +#endif const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -143,15 +150,76 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\""; } TEST_CASE("parse_opendbc") { - QDir dir(OPENDBC_FILE_PATH); - QStringList errors; - for (auto fn : dir.entryList({"*.dbc"}, QDir::Files, QDir::Name)) { + std::vector errors; + for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH)) { + if (!entry.is_regular_file() || entry.path().extension() != ".dbc") continue; try { - auto dbc = DBCFile(dir.filePath(fn).toStdString()); + auto dbc = DBCFile(entry.path().string()); } catch (std::exception &e) { errors.push_back(e.what()); } } - INFO(errors.join("\n").toStdString()); + std::ostringstream details; + for (const auto &error : errors) details << error << '\n'; + INFO(details.str()); REQUIRE(errors.empty()); } + +TEST_CASE("DBCManager core callbacks") { + DBCManager manager; + int files_changed = 0; + int signals_added = 0; + int masks_updated = 0; + manager.setCallbacks({ + .signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; }, + .file_changed = [&]() { ++files_changed; }, + .mask_updated = [&]() { ++masks_updated; }, + }); + + std::string error; + REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error)); + REQUIRE(error.empty()); + REQUIRE(files_changed == 1); + + cabana::Signal signal{}; + signal.name = "speed"; + signal.start_bit = 0; + signal.size = 8; + signal.is_little_endian = true; + manager.addSignal({.source = 0, .address = 160}, signal); + REQUIRE(signals_added == 1); + REQUIRE(masks_updated == 1); + REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr); +} + +TEST_CASE("Cabana settings core defaults") { + CabanaSettingsState state; + REQUIRE(state.fps == 10); + REQUIRE(state.chart_range == 180); + REQUIRE(state.drag_direction == CabanaSettingsState::MsbFirst); + REQUIRE(state.recent_files.empty()); +} + +#ifdef QT_CORE_LIB +TEST_CASE("CabanaColor preserves QColor transformations") { + const std::vector colors = { + QColor(102, 86, 169, 64), QColor(0, 187, 255, 128), QColor(255, 0, 0, 128), QColor(45, 120, 75, 255), + }; + for (const auto &qt_color : colors) { + CabanaColor color(qt_color.red(), qt_color.green(), qt_color.blue(), qt_color.alpha()); + for (int factor : {75, 100, 135, 150, 200}) { + const auto lighter = color.lighter(factor); + const auto qt_lighter = qt_color.lighter(factor); + CHECK(std::abs(lighter.red() - qt_lighter.red()) <= 1); + CHECK(std::abs(lighter.green() - qt_lighter.green()) <= 1); + CHECK(std::abs(lighter.blue() - qt_lighter.blue()) <= 1); + + const auto darker = color.darker(factor); + const auto qt_darker = qt_color.darker(factor); + CHECK(std::abs(darker.red() - qt_darker.red()) <= 1); + CHECK(std::abs(darker.green() - qt_darker.green()) <= 1); + CHECK(std::abs(darker.blue() - qt_darker.blue()) <= 1); + } + } +} +#endif diff --git a/openpilot/tools/cabana/tests/test_runner.cc b/openpilot/tools/cabana/tests/test_runner.cc index b20ac86c64..a76c8e16b9 100644 --- a/openpilot/tools/cabana/tests/test_runner.cc +++ b/openpilot/tools/cabana/tests/test_runner.cc @@ -1,10 +1,7 @@ #define CATCH_CONFIG_RUNNER #include "catch2/catch.hpp" -#include int main(int argc, char **argv) { - // unit tests for Qt - QCoreApplication app(argc, argv); const int res = Catch::Session().run(argc, argv); return (res < 0xff ? res : 0xff); } diff --git a/openpilot/tools/cabana/utils/export.cc b/openpilot/tools/cabana/utils/export.cc index a7f910193f..d585827ef5 100644 --- a/openpilot/tools/cabana/utils/export.cc +++ b/openpilot/tools/cabana/utils/export.cc @@ -1,41 +1,41 @@ #include "tools/cabana/utils/export.h" -#include -#include +#include +#include #include "tools/cabana/streams/abstractstream.h" namespace utils { -void exportToCSV(const QString &file_name, std::optional msg_id) { - QFile file(file_name); - if (file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - QTextStream stream(&file); +void exportToCSV(const std::string &file_name, std::optional msg_id) { + std::ofstream stream(file_name, std::ios::trunc); + if (stream) { stream << "time,addr,bus,data\n"; for (auto e : msg_id ? can->events(*msg_id) : can->allEvents()) { - stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," - << "0x" << QString::number(e->address, 16) << "," << e->src << "," - << "0x" << QByteArray::fromRawData((const char *)e->dat, e->size).toHex().toUpper() << "\n"; + stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << "," + << "0x" << std::hex << e->address << std::dec << "," << static_cast(e->src) << ",0x" + << std::uppercase << std::hex << std::setfill('0'); + for (int i = 0; i < e->size; ++i) stream << std::setw(2) << static_cast(e->dat[i]); + stream << std::nouppercase << std::dec << "\n"; } } } -void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id) { - QFile file(file_name); - if (auto msg = dbc()->msg(msg_id); msg && msg->sigs.size() && file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { - QTextStream stream(&file); +void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id) { + std::ofstream stream(file_name, std::ios::trunc); + if (auto msg = dbc()->msg(msg_id); msg && !msg->sigs.empty() && stream) { stream << "time,addr,bus"; for (auto s : msg->sigs) stream << "," << s->name.c_str(); stream << "\n"; for (auto e : can->events(msg_id)) { - stream << QString::number(can->toSeconds(e->mono_time), 'f', 3) << "," - << "0x" << QString::number(e->address, 16) << "," << e->src; + stream << std::fixed << std::setprecision(3) << can->toSeconds(e->mono_time) << "," + << "0x" << std::hex << e->address << std::dec << "," << static_cast(e->src); for (auto s : msg->sigs) { double value = 0; s->getValue(e->dat, e->size, &value); - stream << "," << QString::number(value, 'f', s->precision); + stream << "," << std::fixed << std::setprecision(s->precision) << value; } stream << "\n"; } diff --git a/openpilot/tools/cabana/utils/export.h b/openpilot/tools/cabana/utils/export.h index 270906b163..ee110e8321 100644 --- a/openpilot/tools/cabana/utils/export.h +++ b/openpilot/tools/cabana/utils/export.h @@ -1,10 +1,11 @@ #pragma once #include +#include #include "tools/cabana/dbc/dbcmanager.h" namespace utils { -void exportToCSV(const QString &file_name, std::optional msg_id = std::nullopt); -void exportSignalsToCSV(const QString &file_name, const MessageId &msg_id); +void exportToCSV(const std::string &file_name, std::optional msg_id = std::nullopt); +void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id); } // namespace utils diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 50ab764423..415f6f1784 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -100,7 +100,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & // Paint hex column const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); + const auto &colors = *static_cast *>(index.data(ColorsRole).value()); painter->setFont(fixed_font); const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); @@ -115,7 +115,7 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & painter->setPen(option.palette.color(QPalette::Text)); painter->fillRect(r, option.palette.color(QPalette::Window)); } - painter->fillRect(r, colors[i]); + painter->fillRect(r, toQColor(colors[i])); } else { painter->setPen(text_pen); } diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index f839ffe7fe..f4d8342b63 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,10 @@ #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +inline QColor toQColor(const CabanaColor &color) { + return QColor(color.r, color.g, color.b, color.a); +} + class LogSlider : public QSlider { Q_OBJECT diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 87d3cbec95..5b680ad454 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -203,7 +203,7 @@ void VideoWidget::timeRangeChanged() { QString VideoWidget::formatTime(double sec, bool include_milliseconds) { if (settings.absolute_time) - sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0; + sec += std::chrono::duration(can->beginDateTime().time_since_epoch()).count(); return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); } From 5d23a78c77c461226206c3a0ac624bab73637cbb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 16 Jul 2026 14:35:45 -0700 Subject: [PATCH 004/325] cabana: de-Qt, part 2 (#38359) --- openpilot/tools/cabana/binaryview.cc | 6 +- openpilot/tools/cabana/cabana.cc | 180 +++++++++++++---- openpilot/tools/cabana/cameraview.cc | 5 +- openpilot/tools/cabana/chart/chart.cc | 65 +------ openpilot/tools/cabana/chart/chart.h | 1 - openpilot/tools/cabana/chart/chartswidget.cc | 1 - .../tools/cabana/chart/signalselector.cc | 3 - openpilot/tools/cabana/deqt.md | 70 +++++++ openpilot/tools/cabana/mainwin.cc | 28 ++- openpilot/tools/cabana/mainwin.h | 5 +- openpilot/tools/cabana/signalview.cc | 16 +- .../tools/cabana/streams/devicestream.cc | 87 +++++++-- openpilot/tools/cabana/streams/devicestream.h | 5 +- openpilot/tools/cabana/streams/pandastream.cc | 13 +- openpilot/tools/cabana/streams/routes.cc | 99 +++++++--- .../tools/cabana/streams/socketcanstream.cc | 11 +- openpilot/tools/cabana/utils/util.cc | 184 +++++++++++++++--- openpilot/tools/cabana/utils/util.h | 51 +++-- openpilot/tools/cabana/videowidget.cc | 10 +- openpilot/tools/cabana/videowidget.h | 3 - 20 files changed, 607 insertions(+), 236 deletions(-) create mode 100644 openpilot/tools/cabana/deqt.md diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 1c7c7b938e..f396d1c13f 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -3,7 +3,8 @@ #include -#include +#include + #include #include #include @@ -260,7 +261,8 @@ void BinaryViewModel::refresh() { int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j; int idx = column_count * (pos / 8) + pos % 8; if (idx >= items.size()) { - qWarning() << "signal " << sig->name.c_str() << "out of bounds.start_bit:" << sig->start_bit << "size:" << sig->size; + fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", + sig->name.c_str(), sig->start_bit, sig->size); break; } if (j == 0) sig->is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index db26b4067a..56b43361f8 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -1,5 +1,9 @@ +#include +#include +#include +#include + #include -#include #include "tools/cabana/mainwin.h" #include "tools/cabana/streams/devicestream.h" @@ -9,6 +13,120 @@ #include "tools/cabana/streams/socketcanstream.h" #endif +namespace { + +struct CabanaArgs { + bool demo = false; + bool auto_source = false; + bool qcam = false; + bool ecam = false; + bool dcam = false; + bool msgq = false; + bool panda = false; + bool no_vipc = false; + std::string panda_serial; + std::string socketcan; + std::string zmq; + std::string data_dir; + std::string dbc; + std::string route; +}; + +void printUsage(const char *argv0) { + fprintf(stderr, + "Usage: %s [options] [route]\n" + "\n" + " route the drive to replay. find your drives at connect.comma.ai\n" + "\n" + "Options:\n" + " --help show this help\n" + " --demo use a demo route instead of providing your own\n" + " --auto Auto load the route from the best available source (no video):\n" + " internal, openpilotci, comma_api, car_segments, testing_closet\n" + " --qcam load qcamera\n" + " --ecam load wide road camera\n" + " --dcam load driver camera\n" + " --msgq read can messages from the msgq\n" + " --panda read can messages from panda\n" + " --panda-serial read can messages from panda with given serial\n" +#ifdef __linux__ + " --socketcan read can messages from given SocketCAN device\n" +#endif + " --zmq read can messages from zmq at the specified ip-address\n" + " --data_dir local directory with routes\n" + " --no-vipc do not output video\n" + " --dbc dbc file to open\n", + argv0); +} + +// Returns true if value was consumed from argv[i+1]. +bool takeValue(int argc, char *argv[], int &i, std::string &out) { + if (i + 1 >= argc) { + fprintf(stderr, "error: %s requires a value\n", argv[i]); + return false; + } + out = argv[++i]; + return true; +} + +// Returns 0 to continue, or a process exit code (0 for --help, 1 for errors). +int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) { + ok = false; + for (int i = 1; i < argc; ++i) { + const char *a = argv[i]; + if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) { + printUsage(argv[0]); + return 0; + } else if (std::strcmp(a, "--demo") == 0) { + args.demo = true; + } else if (std::strcmp(a, "--auto") == 0) { + args.auto_source = true; + } else if (std::strcmp(a, "--qcam") == 0) { + args.qcam = true; + } else if (std::strcmp(a, "--ecam") == 0) { + args.ecam = true; + } else if (std::strcmp(a, "--dcam") == 0) { + args.dcam = true; + } else if (std::strcmp(a, "--msgq") == 0) { + args.msgq = true; + } else if (std::strcmp(a, "--panda") == 0) { + args.panda = true; + } else if (std::strcmp(a, "--panda-serial") == 0) { + if (!takeValue(argc, argv, i, args.panda_serial)) return 1; + args.panda = true; + } else if (std::strcmp(a, "--socketcan") == 0) { + if (!takeValue(argc, argv, i, args.socketcan)) return 1; +#ifdef __linux__ +#else + fprintf(stderr, "error: --socketcan is only supported on Linux\n"); + return 1; +#endif + } else if (std::strcmp(a, "--zmq") == 0) { + if (!takeValue(argc, argv, i, args.zmq)) return 1; + } else if (std::strcmp(a, "--data_dir") == 0) { + if (!takeValue(argc, argv, i, args.data_dir)) return 1; + } else if (std::strcmp(a, "--no-vipc") == 0) { + args.no_vipc = true; + } else if (std::strcmp(a, "--dbc") == 0) { + if (!takeValue(argc, argv, i, args.dbc)) return 1; + } else if (a[0] == '-') { + fprintf(stderr, "error: unknown option %s\n", a); + printUsage(argv[0]); + return 1; + } else if (args.route.empty()) { + args.route = a; + } else { + fprintf(stderr, "error: unexpected argument %s\n", a); + printUsage(argv[0]); + return 1; + } + } + ok = true; + return 0; +} + +} // namespace + int main(int argc, char *argv[]) { QCoreApplication::setApplicationName("Cabana"); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); @@ -20,69 +138,51 @@ int main(int argc, char *argv[]) { UnixSignalHandler signalHandler; utils::setTheme(settings.theme); - QCommandLineParser cmd_parser; - cmd_parser.addHelpOption(); - cmd_parser.addPositionalArgument("route", "the drive to replay. find your drives at connect.comma.ai"); - cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); - cmd_parser.addOption({"auto", "Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet"}); - cmd_parser.addOption({"qcam", "load qcamera"}); - cmd_parser.addOption({"ecam", "load wide road camera"}); - cmd_parser.addOption({"dcam", "load driver camera"}); - cmd_parser.addOption({"msgq", "read can messages from the msgq"}); - cmd_parser.addOption({"panda", "read can messages from panda"}); - cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"}); -#ifdef __linux__ - if (SocketCanStream::available()) { - cmd_parser.addOption({"socketcan", "read can messages from given SocketCAN device", "socketcan"}); + CabanaArgs args; + bool args_ok = false; + if (const int code = parseArgs(argc, argv, args, args_ok); !args_ok) { + return code; } -#endif - cmd_parser.addOption({"zmq", "read can messages from zmq at the specified ip-address", "ip-address"}); - cmd_parser.addOption({"data_dir", "local directory with routes", "data_dir"}); - cmd_parser.addOption({"no-vipc", "do not output video"}); - cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); - cmd_parser.process(app); AbstractStream *stream = nullptr; - if (cmd_parser.isSet("msgq")) { + if (args.msgq) { stream = new DeviceStream(&app); - } else if (cmd_parser.isSet("zmq")) { - stream = new DeviceStream(&app, cmd_parser.value("zmq")); - } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { + } else if (!args.zmq.empty()) { + stream = new DeviceStream(&app, QString::fromStdString(args.zmq)); + } else if (args.panda || !args.panda_serial.empty()) { try { - stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial").toStdString()}); + stream = new PandaStream(&app, {.serial = args.panda_serial}); } catch (std::exception &e) { - qWarning() << e.what(); + fprintf(stderr, "%s\n", e.what()); return 0; } #ifdef __linux__ - } else if (SocketCanStream::available() && cmd_parser.isSet("socketcan")) { - stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan").toStdString()}); + } else if (SocketCanStream::available() && !args.socketcan.empty()) { + stream = new SocketCanStream(&app, {.device = args.socketcan}); #endif } else { uint32_t replay_flags = REPLAY_FLAG_NONE; - if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; - if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA; - if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM; - if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC; + if (args.ecam) replay_flags |= REPLAY_FLAG_ECAM; + if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA; + if (args.dcam) replay_flags |= REPLAY_FLAG_DCAM; + if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC; - const QStringList args = cmd_parser.positionalArguments(); QString route; - if (args.size() > 0) { - route = args.first(); - } else if (cmd_parser.isSet("demo")) { + if (!args.route.empty()) { + route = QString::fromStdString(args.route); + } else if (args.demo) { route = DEMO_ROUTE; } if (!route.isEmpty()) { auto replay_stream = std::make_unique(&app); - bool auto_source = cmd_parser.isSet("auto"); - if (!replay_stream->loadRoute(route.toStdString(), cmd_parser.value("data_dir").toStdString(), replay_flags, auto_source)) { + if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { return 0; } stream = replay_stream.release(); } } - MainWindow w(stream, cmd_parser.value("dbc")); + MainWindow w(stream, QString::fromStdString(args.dbc)); return app.exec(); } diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc index 13c838efd8..997bd19198 100644 --- a/openpilot/tools/cabana/cameraview.cc +++ b/openpilot/tools/cabana/cameraview.cc @@ -6,6 +6,8 @@ #include #endif +#include + #include namespace { @@ -221,7 +223,8 @@ void CameraWidget::vipcThread() { while (!QThread::currentThread()->isInterruptionRequested()) { if (!vipc_client || cur_stream != requested_stream_type) { clearFrames(); - qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; + fprintf(stderr, "connecting to stream %d, was connected to %d\n", + (int)requested_stream_type, (int)cur_stream); cur_stream = requested_stream_type; vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); } diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index b5ddcf0479..0ce5d051b3 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -3,18 +3,15 @@ #include #include +#include #include #include #include #include -#include #include -#include #include #include -#include -#include #include #include #include @@ -421,45 +418,6 @@ qreal ChartView::niceNumber(qreal x, bool ceiling) { return q * z; } -QPixmap getBlankShadowPixmap(const QPixmap &px, int radius) { - QGraphicsDropShadowEffect *e = new QGraphicsDropShadowEffect; - e->setColor(QColor(40, 40, 40, 245)); - e->setOffset(0, 0); - e->setBlurRadius(radius); - - qreal dpr = px.devicePixelRatio(); - QPixmap blank(px.size()); - blank.setDevicePixelRatio(dpr); - blank.fill(Qt::white); - - QGraphicsScene scene; - QGraphicsPixmapItem item(blank); - item.setGraphicsEffect(e); - scene.addItem(&item); - - QPixmap shadow(px.size() + QSize(radius * dpr * 2, radius * dpr * 2)); - shadow.setDevicePixelRatio(dpr); - shadow.fill(Qt::transparent); - QPainter p(&shadow); - scene.render(&p, {QPoint(), shadow.size() / dpr}, item.boundingRect().adjusted(-radius, -radius, radius, radius)); - return shadow; -} - -static QPixmap getDropPixmap(const QPixmap &src) { - static QPixmap shadow_px; - const int radius = 10; - if (shadow_px.size() != src.size() + QSize(radius * 2, radius * 2)) { - shadow_px = getBlankShadowPixmap(src, radius); - } - QPixmap px = shadow_px; - QPainter p(&px); - QRectF target_rect(QPointF(radius, radius), src.size() / src.devicePixelRatio()); - p.drawPixmap(target_rect.topLeft(), src); - p.setCompositionMode(QPainter::CompositionMode_DestinationIn); - p.fillRect(target_rect, QColor(0, 0, 0, 200)); - return px; -} - void ChartView::contextMenuEvent(QContextMenuEvent *event) { QMenu context_menu(this); context_menu.addActions(menu->actions()); @@ -479,7 +437,7 @@ void ChartView::mousePressEvent(QMouseEvent *event) { charts_widget->stopAutoScroll(); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); - drag->setPixmap(getDropPixmap(px)); + drag->setPixmap(px); drag->setHotSpot(-QPoint(5, 5)); drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction); } else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { @@ -628,7 +586,6 @@ void ChartView::dropEvent(QDropEvent *event) { sigs.insert(sigs.end(), std::move_iterator(source_chart->sigs.begin()), std::move_iterator(source_chart->sigs.end())); updateAxisY(); updateTitle(); - startAnimation(); source_chart->sigs.clear(); charts_widget->removeChart(source_chart); @@ -643,17 +600,6 @@ void ChartView::resetChartCache() { viewport()->update(); } -void ChartView::startAnimation() { - QGraphicsOpacityEffect *eff = new QGraphicsOpacityEffect(this); - viewport()->setGraphicsEffect(eff); - QPropertyAnimation *a = new QPropertyAnimation(eff, "opacity"); - a->setDuration(250); - a->setStartValue(0.3); - a->setEndValue(1); - a->setEasingCurve(QEasingCurve::InBack); - a->start(QPropertyAnimation::DeleteWhenStopped); -} - void ChartView::paintEvent(QPaintEvent *event) { if (!can->liveStreaming()) { if (chart_pixmap.isNull()) { @@ -822,9 +768,12 @@ void ChartView::setSeriesColor(QXYSeries *series, QColor color) { if (s != series && std::abs(color.hueF() - qobject_cast(s)->color().hueF()) < 0.1) { // use different color to distinguish it from others. auto last_color = qobject_cast(existing_series.back())->color(); + static thread_local std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution sat(35, 99); + std::uniform_int_distribution val(85, 99); color.setHsvF(std::fmod(last_color.hueF() + 60 / 360.0, 1.0), - QRandomGenerator::global()->bounded(35, 100) / 100.0, - QRandomGenerator::global()->bounded(85, 100) / 100.0); + sat(rng) / 100.0, + val(rng) / 100.0); break; } } diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index f9472bd4f6..ae050269bf 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -38,7 +38,6 @@ public: void updatePlotArea(int left, bool force = false); void showTip(double sec); void hideTip(); - void startAnimation(); double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); } struct SigItem { diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index e5c70bc536..93d8570996 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -570,7 +570,6 @@ void ChartsContainer::dropEvent(QDropEvent *event) { charts_widget->updateLayout(true); charts_widget->updateTabBar(); event->acceptProposedAction(); - chart->startAnimation(); } drawDropIndicator({}); } diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index 12a9c81564..85832e796b 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -1,7 +1,6 @@ #include "tools/cabana/chart/signalselector.h" #include "tools/cabana/dbc/dbcqt.h" -#include #include #include #include @@ -21,8 +20,6 @@ SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) msgs_combo->setEditable(true); msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); msgs_combo->setInsertPolicy(QComboBox::NoInsert); - msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); - msgs_combo->completer()->setFilterMode(Qt::MatchContains); main_layout->addWidget(available_list = new QListWidget(this), 2, 0); diff --git a/openpilot/tools/cabana/deqt.md b/openpilot/tools/cabana/deqt.md new file mode 100644 index 0000000000..6d2aed8264 --- /dev/null +++ b/openpilot/tools/cabana/deqt.md @@ -0,0 +1,70 @@ +we're migrating cabana away from Qt and to eventually entirely use imgui + +we are doing it incrementally, in small pieces that are easy to execute and verify. +we will repeat this until we're all done. + +# Cabana Qt API inventory + +these are all still in cabana. we remove them from this list once they're gone. +each bullet is an atomic unit of work. + +our workflow is: +- pick the easiest of the bulleted items from below +- implement it and make] sure it builds +- spin up reviewer agents to review the code in a clean context and a separate one to click around in xvfb as a gui test +- then implement the fixes from the above reviewer agents + +some rules +- do not add more Qt usage ever + +- `QObject`, `QMetaObject`, `QMetaType` +- `QApplication`, `QCoreApplication`, `QGuiApplication` +- `QString`, `QStringList`, `QStringBuilder`, `QChar`, `QLatin1Char` +- `QByteArray` +- `QVariant` +- `QVector`, `QMap`, `QSet`, `QPointer` +- `QSettings` +- `QFile`, `QFileInfo`, `QDir`, `QIODevice`, `QStandardPaths` +- `QThread` +- `QTimer`, `QBasicTimer`, `QTimerEvent` +- `QWidget`, `QMainWindow`, `QWindow` +- `QDialog`, `QDialogButtonBox`, `QMessageBox`, `QProgressDialog` +- `QFileDialog` +- `QMenu`, `QMenuBar`, `QAction`, `QActionGroup`, `QWidgetAction` +- `QToolBar`, `QToolButton`, `QPushButton` +- `QCheckBox`, `QRadioButton`, `QButtonGroup`, `QAbstractButton` +- `QComboBox`, `QLineEdit`, `QTextEdit`, `QSpinBox`, `QSlider` +- `QLabel`, `QGroupBox`, `QFrame` +- `QTabBar`, `QTabWidget`, `QSplitter`, `QScrollArea`, `QScrollBar` +- `QDockWidget`, `QStatusBar`, `QProgressBar`, `QRubberBand` +- `QFormLayout`, `QGridLayout`, `QHBoxLayout`, `QVBoxLayout` +- `QSizePolicy` +- `QAbstractItemModel`, `QAbstractTableModel`, `QModelIndex` +- `QAbstractItemView`, `QTableView`, `QTreeView` +- `QTableWidget`, `QTableWidgetItem`, `QListWidget`, `QListWidgetItem` +- `QItemSelection`, `QItemSelectionModel`, `QItemSelectionRange` +- `QHeaderView`, `QStyledItemDelegate`, `QStyleOptionViewItem` +- `QValidator`, `QIntValidator`, `QDoubleValidator` +- `QColor`, `QRgb`, `QPalette` +- `QBrush`, `QPen` +- `QPainter`, `QPainterPath`, `QStylePainter` +- `QPixmap`, `QPixmapCache`, `QIcon`, `QStaticText` +- `QFont`, `QFontDatabase`, `QFontMetrics`, `QTextDocument` +- `QStyle`, `QStyleOption`, `QStyleOptionFrame`, `QStyleOptionSlider` +- `QPoint`, `QPointF`, `QRect`, `QRectF`, `QRegion` +- `QSize`, `QSizeF` +- `QCursor`, `QClipboard`, `QScreen`, `QDesktopWidget` +- `QEvent`, `QPaintEvent`, `QResizeEvent`, `QShowEvent`, `QCloseEvent` +- `QMouseEvent`, `QWheelEvent`, `QNativeGestureEvent`, `QContextMenuEvent` +- `QDrag`, `QMimeData` +- `QDragEnterEvent`, `QDragLeaveEvent`, `QDragMoveEvent`, `QDropEvent` +- `QKeySequence`, `QShortcut`, `QToolTip` +- `QChart`, `QChartView`, `QAbstractAxis`, `QValueAxis` +- `QXYSeries`, `QLineSeries`, `QScatterSeries` +- `QLegend`, `QLegendMarker` +- `QGraphicsScene`, `QGraphicsView`, `QGraphicsItemGroup`, `QGraphicsLayout` +- `QGraphicsPixmapItem`, `QGraphicsProxyWidget` +- `QOpenGLWidget`, `QOpenGLFunctions` +- `QOpenGLShader`, `QOpenGLShaderProgram` +- `QMatrix4x4`, `QSurfaceFormat` +- `QUndoCommand`, `QUndoStack`, `QUndoView` diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 8d932163a6..6372ea9295 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -21,6 +20,7 @@ #include #include +#include "json11/json11.hpp" #include "tools/cabana/commands.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" @@ -53,11 +53,9 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { emit static_main_win->updateProgressBar(cur, total, success); }); - qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) { - if (type == QtDebugMsg) return; - emit static_main_win->showMessage(msg, 2000); + installMessageHandler([](ReplyMsgType type, const std::string msg) { + emit static_main_win->showMessage(QString::fromStdString(msg), 2000); }); - installMessageHandler([](ReplyMsgType type, const std::string msg) { qInfo() << msg.c_str(); }); setStyleSheet(QString(R"(QMainWindow::separator { width: %1px; /* when vertical */ @@ -76,8 +74,15 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW void MainWindow::loadFingerprints() { QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json"); - if (json_file.open(QIODevice::ReadOnly)) { - fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); + if (!json_file.open(QIODevice::ReadOnly)) return; + std::string err; + auto doc = json11::Json::parse(json_file.readAll().toStdString(), err); + if (!err.empty() || !doc.is_object()) return; + fingerprint_to_dbc.clear(); + for (const auto &kv : doc.object_items()) { + if (kv.second.is_string()) { + fingerprint_to_dbc.emplace(kv.first, kv.second.string_value()); + } } } @@ -374,8 +379,11 @@ void MainWindow::eventsMerged() { .arg(QString::fromStdString(can->routeName())) .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); // Don't overwrite already loaded DBC - if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) { - QTimer::singleShot(0, this, [this]() { loadDBCFromOpendbc(fingerprint_to_dbc[car_fingerprint].toString() + ".dbc"); }); + auto it = fingerprint_to_dbc.find(car_fingerprint.toStdString()); + if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc.end()) { + QTimer::singleShot(0, this, [this, dbc_name = QString::fromStdString(it->second)]() { + loadDBCFromOpendbc(dbc_name + ".dbc"); + }); } } } @@ -562,7 +570,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { remindSaveChanges(); installDownloadProgressHandler(nullptr); - qInstallMessageHandler(nullptr); + installMessageHandler(nullptr); if (floating_window) floating_window->deleteLater(); diff --git a/openpilot/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h index 92c2714ae7..3526a58202 100644 --- a/openpilot/tools/cabana/mainwin.h +++ b/openpilot/tools/cabana/mainwin.h @@ -1,13 +1,14 @@ #pragma once #include -#include #include #include #include #include #include #include +#include +#include #include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -85,7 +86,7 @@ protected: QVBoxLayout *charts_layout; QProgressBar *progress_bar; QLabel *status_label; - QJsonDocument fingerprint_to_dbc; + std::unordered_map fingerprint_to_dbc; QSplitter *video_splitter = nullptr; enum { MAX_RECENT_FILES = 15 }; QMenu *open_recent_menu = nullptr; diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 5048cc6aa8..87294b41f7 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -16,6 +15,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/utils/util.h" // SignalModel @@ -252,7 +252,7 @@ void SignalModel::handleSignalRemoved(const cabana::Signal *sig) { SignalItemDelegate::SignalItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { name_validator = new NameValidator(this); - node_validator = new QRegExpValidator(QRegExp("^\\w+(,\\w+)*$"), this); + node_validator = new NodeValidator(this); double_validator = new DoubleValidator(this); label_font.setPointSize(8); @@ -376,15 +376,6 @@ QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionVie else if (item->type == SignalModel::Item::Node) e->setValidator(node_validator); else e->setValidator(double_validator); - if (item->type == SignalModel::Item::Name) { - auto names = dbc()->signalNames(); - QStringList qnames; - for (const auto &n : names) qnames.push_back(QString::fromStdString(n)); - QCompleter *completer = new QCompleter(qnames, e); - completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setFilterMode(Qt::MatchContains); - e->setCompleter(completer); - } return e; } else if (item->type == SignalModel::Item::Size) { QSpinBox *spin = new QSpinBox(parent); @@ -429,8 +420,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QHBoxLayout *hl = new QHBoxLayout(title_bar); hl->addWidget(signal_count_lb = new QLabel()); filter_edit = new QLineEdit(this); - QRegularExpression re("\\S+"); - filter_edit->setValidator(new QRegularExpressionValidator(re, this)); + filter_edit->setValidator(new NonWhitespaceValidator(this)); filter_edit->setClearButtonEnabled(true); filter_edit->setPlaceholderText(tr("Filter Signal")); hl->addWidget(filter_edit); diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 96fae908ba..1ce0dbb71f 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -1,7 +1,13 @@ #include "tools/cabana/streams/devicestream.h" +#include +#include +#include +#include #include #include +#include +#include #include "openpilot/cereal/services.h" @@ -10,36 +16,86 @@ #include #include #include -#include -#include #include +#include "tools/cabana/utils/util.h" + // DeviceStream DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { } DeviceStream::~DeviceStream() { - if (!bridge_process) - return; + stopBridge(); +} - bridge_process->terminate(); - if (!bridge_process->waitForFinished(3000)) { - bridge_process->kill(); - bridge_process->waitForFinished(); +void DeviceStream::stopBridge() { + if (bridge_pid <= 0) return; + + ::kill(bridge_pid, SIGTERM); + for (int i = 0; i < 30; ++i) { + int status = 0; + pid_t r = ::waitpid(bridge_pid, &status, WNOHANG); + if (r == bridge_pid || (r < 0 && errno == ECHILD)) { + bridge_pid = -1; + return; + } + usleep(100000); // 100ms, up to ~3s } + ::kill(bridge_pid, SIGKILL); + ::waitpid(bridge_pid, nullptr, 0); + bridge_pid = -1; } void DeviceStream::start() { if (!zmq_address.isEmpty()) { - bridge_process = new QProcess(this); - QString bridge_path = QCoreApplication::applicationDirPath() + "/../../openpilot/cereal/messaging/bridge"; - bridge_process->start(QFileInfo(bridge_path).absoluteFilePath(), QStringList { zmq_address, "/\"can/\"" }); + stopBridge(); + QString bridge_path = QFileInfo(QCoreApplication::applicationDirPath() + + "/../../openpilot/cereal/messaging/bridge").absoluteFilePath(); + const std::string path = bridge_path.toStdString(); + const std::string addr = zmq_address.toStdString(); + const char *can_filter = "/\"can/\""; - if (!bridge_process->waitForStarted()) { - QMessageBox::warning(nullptr, tr("Error"), tr("Failed to start bridge: %1").arg(bridge_process->errorString())); + // Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec + // fails, the child writes errno and the parent aborts stream start. + int err_pipe[2] = {-1, -1}; + if (::pipe(err_pipe) != 0) { + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); return; } + + pid_t pid = ::fork(); + if (pid == 0) { + ::close(err_pipe[0]); + ::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC); + execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast(nullptr)); + const int err = errno; + (void)!::write(err_pipe[1], &err, sizeof(err)); + _exit(127); + } + + ::close(err_pipe[1]); + if (pid < 0) { + ::close(err_pipe[0]); + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + return; + } + + int exec_errno = 0; + const ssize_t n = ::read(err_pipe[0], &exec_errno, sizeof(exec_errno)); + ::close(err_pipe[0]); + if (n == static_cast(sizeof(exec_errno))) { + // Child failed to exec; reap and surface the error. + int status = 0; + ::waitpid(pid, &status, 0); + QMessageBox::warning(nullptr, tr("Error"), + tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + return; + } + + bridge_pid = pid; } LiveStream::start(); @@ -69,10 +125,7 @@ OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(p QRadioButton *zmq = new QRadioButton(tr("ZMQ")); ip_address = new QLineEdit(this); ip_address->setPlaceholderText(tr("Enter device Ip Address")); - QString ip_range = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; - QString pattern("^" + ip_range + "\\." + ip_range + "\\." + ip_range + "\\." + ip_range + "$"); - QRegularExpression re(pattern); - ip_address->setValidator(new QRegularExpressionValidator(re, this)); + ip_address->setValidator(new IpAddressValidator(this)); group = new QButtonGroup(this); group->addButton(msgq, 0); diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 4bcdb5351d..0e6951c92c 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -2,7 +2,7 @@ #include "tools/cabana/streams/livestream.h" -#include +#include class DeviceStream : public LiveStream { Q_OBJECT @@ -16,7 +16,8 @@ public: protected: void start() override; void streamThread() override; - QProcess *bridge_process = nullptr; + void stopBridge(); + pid_t bridge_pid = -1; const QString zmq_address; }; diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 3692f71a11..aa1e01c5a3 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -1,6 +1,7 @@ #include "tools/cabana/streams/pandastream.h" -#include +#include + #include #include #include @@ -16,10 +17,10 @@ PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(co bool PandaStream::connect() { try { - qDebug() << "Connecting to panda " << config.serial.c_str(); + fprintf(stderr, "Connecting to panda %s\n", config.serial.c_str()); panda.reset(new Panda(config.serial)); config.bus_config.resize(3); - qDebug() << "Connected"; + fprintf(stderr, "Connected\n"); } catch (const std::exception& e) { return false; } @@ -48,7 +49,7 @@ void PandaStream::streamThread() { QThread::msleep(1); if (!panda->connected()) { - qDebug() << "Connection to panda lost. Attempting reconnect."; + fprintf(stderr, "Connection to panda lost. Attempting reconnect.\n"); if (!connect()){ QThread::msleep(1000); continue; @@ -57,7 +58,7 @@ void PandaStream::streamThread() { raw_can_data.clear(); if (!panda->can_receive(raw_can_data)) { - qDebug() << "failed to receive"; + fprintf(stderr, "failed to receive\n"); continue; } @@ -123,7 +124,7 @@ void OpenPandaWidget::buildConfigForm() { Panda panda(serial.toStdString()); has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); } catch (const std::exception& e) { - qDebug() << "failed to open panda" << serial; + fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); has_panda = false; } } diff --git a/openpilot/tools/cabana/streams/routes.cc b/openpilot/tools/cabana/streams/routes.cc index e3e5cb1b6e..9c19219ce2 100644 --- a/openpilot/tools/cabana/streams/routes.cc +++ b/openpilot/tools/cabana/streams/routes.cc @@ -1,18 +1,20 @@ #include "tools/cabana/streams/routes.h" +#include +#include +#include +#include +#include + #include -#include #include #include -#include -#include -#include #include #include #include #include -#include +#include "json11/json11.hpp" #include "tools/replay/py_downloader.h" namespace { @@ -20,13 +22,52 @@ namespace { // Parse a PyDownloader JSON response into (success, error_code). std::pair checkApiResponse(const std::string &result) { if (result.empty()) return {false, 500}; - auto doc = QJsonDocument::fromJson(QByteArray::fromStdString(result)); - if (doc.isObject() && doc.object().contains("error")) { - return {false, doc.object()["error"].toString() == "unauthorized" ? 401 : 500}; + std::string err; + auto doc = json11::Json::parse(result, err); + if (!err.empty()) return {false, 500}; + if (doc.is_object() && doc["error"].is_string()) { + return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; } return {true, 0}; } +int64_t nowUnixMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. +int64_t parseIsoToUnixMs(const std::string &s) { + std::string bytes = s; + if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); + int millis = 0; + auto dot = bytes.find('.'); + if (dot != std::string::npos) { + std::string frac = bytes.substr(dot + 1); + bytes = bytes.substr(0, dot); + while (frac.size() < 3) frac.push_back('0'); + millis = std::atoi(frac.substr(0, 3).c_str()); + } + std::tm tm{}; + const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); + if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); + if (!ret) return 0; + tm.tm_isdst = -1; + time_t secs = timegm(&tm); + if (secs == static_cast(-1)) return 0; + return static_cast(secs) * 1000 + millis; +} + +QString formatUnixMs(int64_t ms) { + time_t secs = static_cast(ms / 1000); + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + return QString::fromUtf8(buf); +} + } // namespace // The RouteListWidget class extends QListWidget to display a custom message when empty @@ -83,9 +124,13 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { if (success) { device_list_->clear(); - for (const QJsonValue &device : QJsonDocument::fromJson(json.toUtf8()).array()) { - QString dongle_id = device["dongle_id"].toString(); - device_list_->addItem(dongle_id, dongle_id); + std::string err; + auto doc = json11::Json::parse(json.toStdString(), err); + if (err.empty() && doc.is_array()) { + for (const auto &device : doc.array_items()) { + QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); + device_list_->addItem(dongle_id, dongle_id); + } } } else { QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with openpilot/tools/lib/auth.py") : tr("Network error")); @@ -106,9 +151,8 @@ void RoutesDialog::fetchRoutes() { bool preserved = (period == -1); int64_t start_ms = 0, end_ms = 0; if (!preserved) { - QDateTime now = QDateTime::currentDateTime(); - start_ms = now.addDays(-period).toMSecsSinceEpoch(); - end_ms = now.toMSecsSinceEpoch(); + end_ms = nowUnixMs(); + start_ms = end_ms - static_cast(period) * 24LL * 60LL * 60LL * 1000LL; } int request_id = ++fetch_id_; @@ -124,18 +168,23 @@ void RoutesDialog::fetchRoutes() { void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { if (success) { - for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) { - QDateTime from, to; - if (period_selector_->currentData().toInt() == -1) { - from = QDateTime::fromString(route["start_time"].toString(), Qt::ISODateWithMs); - to = QDateTime::fromString(route["end_time"].toString(), Qt::ISODateWithMs); - } else { - from = QDateTime::fromMSecsSinceEpoch(route["start_time_utc_millis"].toDouble()); - to = QDateTime::fromMSecsSinceEpoch(route["end_time_utc_millis"].toDouble()); + std::string err; + auto doc = json11::Json::parse(json.toStdString(), err); + if (err.empty() && doc.is_array()) { + for (const auto &route : doc.array_items()) { + int64_t from_ms = 0, to_ms = 0; + if (period_selector_->currentData().toInt() == -1) { + from_ms = parseIsoToUnixMs(route["start_time"].string_value()); + to_ms = parseIsoToUnixMs(route["end_time"].string_value()); + } else { + from_ms = static_cast(route["start_time_utc_millis"].number_value()); + to_ms = static_cast(route["end_time_utc_millis"].number_value()); + } + const int mins = static_cast((to_ms - from_ms) / 60000); + auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins)); + item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); + route_list_->addItem(item); } - auto item = new QListWidgetItem(QString("%1 %2min").arg(from.toString()).arg(from.secsTo(to) / 60)); - item->setData(Qt::UserRole, route["fullname"].toString()); - route_list_->addItem(item); } if (route_list_->count() > 0) route_list_->setCurrentRow(0); } else { diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index 768465d5a3..cedbddf99e 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -7,7 +7,8 @@ #include #include -#include +#include + #include #include #include @@ -20,7 +21,7 @@ SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) throw std::runtime_error("SocketCAN not available"); } - qDebug() << "Connecting to SocketCAN device" << config.device.c_str(); + fprintf(stderr, "Connecting to SocketCAN device %s\n", config.device.c_str()); if (!connect()) { throw std::runtime_error("Failed to connect to SocketCAN device"); } @@ -44,7 +45,7 @@ bool SocketCanStream::available() { bool SocketCanStream::connect() { sock_fd = socket(PF_CAN, SOCK_RAW, CAN_RAW); if (sock_fd < 0) { - qDebug() << "Failed to create CAN socket"; + fprintf(stderr, "Failed to create CAN socket\n"); return false; } @@ -55,7 +56,7 @@ bool SocketCanStream::connect() { struct ifreq ifr = {}; strncpy(ifr.ifr_name, config.device.c_str(), IFNAMSIZ - 1); if (ioctl(sock_fd, SIOCGIFINDEX, &ifr) < 0) { - qDebug() << "Failed to get interface index for" << config.device.c_str(); + fprintf(stderr, "Failed to get interface index for %s\n", config.device.c_str()); ::close(sock_fd); sock_fd = -1; return false; @@ -65,7 +66,7 @@ bool SocketCanStream::connect() { addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - qDebug() << "Failed to bind CAN socket"; + fprintf(stderr, "Failed to bind CAN socket\n"); ::close(sock_fd); sock_fd = -1; return false; diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 415f6f1784..5c55bcf015 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -1,7 +1,12 @@ #include "tools/cabana/utils/util.h" #include +#include +#include +#include +#include #include +#include #include #include #include @@ -9,10 +14,8 @@ #include #include -#include #include #include -#include #include #include #include @@ -148,18 +151,35 @@ void TabBar::closeTabClicked() { // UnixSignalHandler -UnixSignalHandler::UnixSignalHandler(QObject *parent) : QObject(nullptr) { +UnixSignalHandler::UnixSignalHandler() { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { qFatal("Couldn't create TERM socketpair"); } - sn = new QSocketNotifier(sig_fd[1], QSocketNotifier::Read, this); - connect(sn, &QSocketNotifier::activated, this, &UnixSignalHandler::handleSigTerm); + waiter = std::thread([this]() { + int tmp = 0; + while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { + if (errno != EINTR) return; + } + if (shutting_down.load()) return; + + // Marshal exit onto the GUI thread (qApp methods are not thread-safe). + QMetaObject::invokeMethod(qApp, []() { + printf("\nexiting...\n"); + qApp->closeAllWindows(); + qApp->exit(); + }, Qt::QueuedConnection); + }); + std::signal(SIGINT, signalHandler); std::signal(SIGTERM, UnixSignalHandler::signalHandler); } UnixSignalHandler::~UnixSignalHandler() { + shutting_down.store(true); + int dummy = 0; + (void)!::write(sig_fd[0], &dummy, sizeof(dummy)); + if (waiter.joinable()) waiter.join(); ::close(sig_fd[0]); ::close(sig_fd[1]); } @@ -168,30 +188,118 @@ void UnixSignalHandler::signalHandler(int s) { (void)!::write(sig_fd[0], &s, sizeof(s)); } -void UnixSignalHandler::handleSigTerm() { - sn->setEnabled(false); - int tmp; - (void)!::read(sig_fd[1], &tmp, sizeof(tmp)); - - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); -} - // NameValidator -NameValidator::NameValidator(QObject *parent) : QRegExpValidator(QRegExp("^(\\w+)"), parent) {} +NameValidator::NameValidator(QObject *parent) : QValidator(parent) {} QValidator::State NameValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); input.replace(' ', '_'); - return QRegExpValidator::validate(input, pos); + if (input.isEmpty()) return QValidator::Intermediate; + for (const QChar &c : input) { + if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid; + } + return QValidator::Acceptable; } -DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) { - // Match locale of QString::toDouble() instead of system - QLocale locale(QLocale::C); - locale.setNumberOptions(QLocale::RejectGroupSeparator); - setLocale(locale); +// NodeValidator + +NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State NodeValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + // Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing). + bool need_word = true; + for (const QChar &c : input) { + if (c.isLetterOrNumber() || c == '_') { + need_word = false; + } else if (c == ',' && !need_word) { + need_word = true; + } else { + return QValidator::Invalid; + } + } + return need_word ? QValidator::Intermediate : QValidator::Acceptable; +} + +// NonWhitespaceValidator + +NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + for (const QChar &c : input) { + if (c.isSpace()) return QValidator::Invalid; + } + return QValidator::Acceptable; +} + +// IpAddressValidator + +IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + + int dots = 0; + int value = 0; + bool has_digit = false; + for (const QChar &c : input) { + if (c.isDigit()) { + value = has_digit ? value * 10 + c.digitValue() : c.digitValue(); + if (value > 255) return QValidator::Invalid; + has_digit = true; + } else if (c == '.') { + if (!has_digit || dots >= 3) return QValidator::Invalid; + ++dots; + has_digit = false; + value = 0; + } else { + return QValidator::Invalid; + } + } + return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate; +} + +DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {} + +QValidator::State DoubleValidator::validate(QString &input, int &pos) const { + Q_UNUSED(pos); + if (input.isEmpty()) return QValidator::Intermediate; + + // Match QString::toDouble(): C locale, no hex floats / inf / nan. + const QByteArray bytes = input.toLatin1(); + // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. + if (bytes.contains('x') || bytes.contains('X') || bytes.contains('p') || bytes.contains('P')) { + return QValidator::Invalid; + } + + const char *start = bytes.constData(); + char *end = nullptr; + const double value = std::strtod(start, &end); + if (end == start) { + // Still typing a sign, decimal point, or exponent prefix. + if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") { + return QValidator::Intermediate; + } + return QValidator::Invalid; + } + if (*end == '\0') { + // Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not). + return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid; + } + + // Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1."). + for (const char *p = end; *p; ++p) { + const char c = *p; + if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) { + return QValidator::Invalid; + } + } + return QValidator::Intermediate; } namespace utils { @@ -257,10 +365,34 @@ void setTheme(int theme) { } QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { - QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss" - : (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss"); - if (include_milliseconds) format += ".zzz"; - return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format); + if (absolute_time) { + const auto ms_total = static_cast(std::llround(sec * 1000.0)); + const std::time_t secs = static_cast(ms_total / 1000); + int millis = static_cast(ms_total % 1000); + if (millis < 0) millis = -millis; + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + if (include_milliseconds) { + return QString::asprintf("%s.%03d", buf, millis); + } + return QString::fromUtf8(buf); + } + + // Relative duration (not wall-clock). + const bool show_hours = sec > 60 * 60; + int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); + const int hours = total_ms / (3600 * 1000); + const int minutes = (total_ms / (60 * 1000)) % 60; + const int seconds = (total_ms / 1000) % 60; + const int millis = total_ms % 1000; + if (show_hours) { + return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) + : QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); + } + return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis) + : QString::asprintf("%02d:%02d", minutes, seconds); } } // namespace utils diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index f4d8342b63..e0318f5c16 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -1,23 +1,23 @@ #pragma once #include +#include #include +#include #include #include #include #include #include -#include #include #include #include -#include -#include #include #include #include #include +#include #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" @@ -89,17 +89,44 @@ private: int h_margin, v_margin; }; -class NameValidator : public QRegExpValidator { +// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'. +class NameValidator : public QValidator { Q_OBJECT public: NameValidator(QObject *parent=nullptr); QValidator::State validate(QString &input, int &pos) const override; }; -class DoubleValidator : public QDoubleValidator { +// Accepts comma-separated identifiers: \w+(,\w+)* +class NodeValidator : public QValidator { + Q_OBJECT +public: + NodeValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// Accepts one or more non-whitespace characters (\S+). +class NonWhitespaceValidator : public QValidator { + Q_OBJECT +public: + NonWhitespaceValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// Accepts a dotted IPv4 address (0-255 per octet). +class IpAddressValidator : public QValidator { + Q_OBJECT +public: + IpAddressValidator(QObject *parent=nullptr); + QValidator::State validate(QString &input, int &pos) const override; +}; + +// C-locale floating-point validator (matches QString::toDouble). +class DoubleValidator : public QValidator { Q_OBJECT public: DoubleValidator(QObject *parent = nullptr); + QValidator::State validate(QString &input, int &pos) const override; }; namespace utils { @@ -152,20 +179,18 @@ private: void closeTabClicked(); }; -class UnixSignalHandler : public QObject { - Q_OBJECT - +// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread +// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread. +class UnixSignalHandler { public: - UnixSignalHandler(QObject *parent = nullptr); + UnixSignalHandler(); ~UnixSignalHandler(); static void signalHandler(int s); -public slots: - void handleSigTerm(); - private: inline static int sig_fd[2] = {}; - QSocketNotifier *sn; + std::atomic shutting_down{false}; + std::thread waiter; }; int num_decimals(double num); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 5b680ad454..60f7b4d2b8 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -157,7 +157,7 @@ QWidget *VideoWidget::createCameraWidget() { slider->setTimeRange(can->minSeconds(), can->maxSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(can, &AbstractStream::paused, cam_widget, [c = cam_widget]() { c->showPausedOverlay(); }); + QObject::connect(can, &AbstractStream::paused, cam_widget, qOverload<>(&StreamCameraView::update)); QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); }); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); @@ -324,12 +324,6 @@ void Slider::mousePressEvent(QMouseEvent *e) { // StreamCameraView StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent) : CameraWidget(stream_name, stream_type, parent) { - fade_animation = new QPropertyAnimation(this, "overlayOpacity"); - fade_animation->setDuration(500); - fade_animation->setStartValue(0.2f); - fade_animation->setEndValue(0.7f); - fade_animation->setEasingCurve(QEasingCurve::InOutQuad); - connect(fade_animation, &QPropertyAnimation::valueChanged, this, QOverload<>::of(&StreamCameraView::update)); } void StreamCameraView::parseQLog(std::shared_ptr qlog) { @@ -376,7 +370,7 @@ void StreamCameraView::paintGL() { } if (can->isPaused()) { - p.setPen(QColor(200, 200, 200, static_cast(255 * fade_animation->currentValue().toFloat()))); + p.setPen(QColor(200, 200, 200, static_cast(255 * 0.7f))); p.setFont(QFont(font().family(), 16, QFont::Bold)); p.drawText(rect(), Qt::AlignCenter, tr("PAUSED")); } diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index e52e92ebd1..d45695904e 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -37,7 +36,6 @@ class StreamCameraView : public CameraWidget { public: StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr); void paintGL() override; - void showPausedOverlay() { fade_animation->start(); } void parseQLog(std::shared_ptr qlog); private: @@ -47,7 +45,6 @@ private: void drawScrubThumbnail(QPainter &p); void drawTime(QPainter &p, const QRect &rect, double seconds); - QPropertyAnimation *fade_animation; std::map big_thumbnails; std::map thumbnails; double thumbnail_dispaly_time = -1; From 9aa5c3d2c6fcd3f94925d3dfe4575f4db7178313 Mon Sep 17 00:00:00 2001 From: rkdune Date: Thu, 16 Jul 2026 18:16:08 -0700 Subject: [PATCH 005/325] expose submodule dependencies as an extra --- pyproject.toml | 14 +++++++++----- uv.lock | 34 ++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9f13917190..51f07d3870 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,10 +98,6 @@ tools = [ #"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", ] -[project.urls] -Homepage = "https://github.com/commaai/openpilot" - -[dependency-groups] submodules = [ "msgq", "opendbc", @@ -111,6 +107,14 @@ submodules = [ "tinygrad", ] +[project.urls] +Homepage = "https://github.com/commaai/openpilot" + +[dependency-groups] +standalone = [ + "openpilot[submodules]", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -207,7 +211,7 @@ not-subscriptable = "ignore" # false positives from dynamic types [tool.uv] python-preference = "only-managed" -default-groups = ["submodules"] +default-groups = ["standalone"] override-dependencies = [ "opendbc", # panda pins opendbc from git for standalone use; always use our submodule "av", # teleoprtc's av<13 pin is stale diff --git a/uv.lock b/uv.lock index 2284243b55..bb6a1cb59c 100644 --- a/uv.lock +++ b/uv.lock @@ -922,6 +922,14 @@ docs = [ { name = "jinja2" }, { name = "zensical" }, ] +submodules = [ + { name = "msgq" }, + { name = "opendbc" }, + { name = "pandacan" }, + { name = "rednose" }, + { name = "teleoprtc" }, + { name = "tinygrad" }, +] testing = [ { name = "codespell" }, { name = "coverage" }, @@ -940,13 +948,8 @@ tools = [ ] [package.dev-dependencies] -submodules = [ - { name = "msgq" }, - { name = "opendbc" }, - { name = "pandacan" }, - { name = "rednose" }, - { name = "teleoprtc" }, - { name = "tinygrad" }, +standalone = [ + { name = "openpilot", extra = ["submodules"] }, ] [package.metadata] @@ -977,7 +980,10 @@ requires-dist = [ { name = "jeepney" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "matplotlib", marker = "extra == 'dev'" }, + { name = "msgq", marker = "extra == 'submodules'", editable = "msgq_repo" }, { name = "numpy", specifier = ">=2.0" }, + { name = "opendbc", marker = "extra == 'submodules'", editable = "opendbc_repo" }, + { name = "pandacan", marker = "extra == 'submodules'", editable = "panda" }, { name = "pillow" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "pycapnp", specifier = "==2.1.0" }, @@ -989,6 +995,7 @@ requires-dist = [ { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pyzmq" }, { name = "qrcode" }, + { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, { name = "scons" }, @@ -996,6 +1003,8 @@ requires-dist = [ { name = "setproctitle" }, { name = "setuptools" }, { name = "sounddevice" }, + { name = "teleoprtc", marker = "extra == 'submodules'", editable = "teleoprtc_repo" }, + { name = "tinygrad", marker = "extra == 'submodules'", editable = "tinygrad_repo" }, { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, { name = "websocket-client" }, @@ -1003,17 +1012,10 @@ requires-dist = [ { name = "zensical", marker = "extra == 'docs'" }, { name = "zstandard" }, ] -provides-extras = ["docs", "testing", "dev", "tools"] +provides-extras = ["docs", "testing", "dev", "tools", "submodules"] [package.metadata.requires-dev] -submodules = [ - { name = "msgq", editable = "msgq_repo" }, - { name = "opendbc", editable = "opendbc_repo" }, - { name = "pandacan", editable = "panda" }, - { name = "rednose", editable = "rednose_repo" }, - { name = "teleoprtc", editable = "teleoprtc_repo" }, - { name = "tinygrad", editable = "tinygrad_repo" }, -] +standalone = [{ name = "openpilot", extras = ["submodules"] }] [[package]] name = "packaging" From a04c045cd79b04751ad378bb0a8e1c87c7c7ff21 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 17 Jul 2026 09:31:16 -0700 Subject: [PATCH 006/325] cabana: de-Qt, part 3 (#38360) --- openpilot/tools/cabana/.gitignore | 1 + openpilot/tools/cabana/SConscript | 40 +- openpilot/tools/cabana/assets/assets.qrc | 1 - openpilot/tools/cabana/binaryview.cc | 16 +- openpilot/tools/cabana/binaryview.h | 1 + openpilot/tools/cabana/cabana.cc | 3 +- openpilot/tools/cabana/cameraview.cc | 221 +---- openpilot/tools/cabana/cameraview.h | 33 +- openpilot/tools/cabana/chart/chart.cc | 764 +++++++++--------- openpilot/tools/cabana/chart/chart.h | 72 +- openpilot/tools/cabana/chart/chartswidget.cc | 180 +++-- openpilot/tools/cabana/chart/chartswidget.h | 29 +- openpilot/tools/cabana/commands.cc | 103 ++- openpilot/tools/cabana/commands.h | 80 +- openpilot/tools/cabana/deqt.md | 25 +- openpilot/tools/cabana/detailwidget.cc | 6 +- openpilot/tools/cabana/historylog.cc | 2 +- openpilot/tools/cabana/mainwin.cc | 102 ++- openpilot/tools/cabana/mainwin.h | 7 +- openpilot/tools/cabana/messageswidget.cc | 52 +- openpilot/tools/cabana/messageswidget.h | 19 +- openpilot/tools/cabana/settings.cc | 507 +++++++++++- openpilot/tools/cabana/settings.h | 14 +- openpilot/tools/cabana/signalview.cc | 4 +- openpilot/tools/cabana/signalview.h | 7 +- .../tools/cabana/streams/abstractstream.h | 2 +- .../tools/cabana/streams/devicestream.cc | 15 +- openpilot/tools/cabana/streams/livestream.cc | 70 +- openpilot/tools/cabana/streams/livestream.h | 17 +- openpilot/tools/cabana/streams/pandastream.cc | 9 +- .../tools/cabana/streams/replaystream.cc | 4 +- openpilot/tools/cabana/streams/routes.cc | 16 +- openpilot/tools/cabana/streams/routes.h | 4 + .../tools/cabana/streams/socketcanstream.cc | 20 +- openpilot/tools/cabana/streamselector.cc | 4 +- openpilot/tools/cabana/tools/findsignal.cc | 9 +- openpilot/tools/cabana/utils/util.cc | 142 ++-- openpilot/tools/cabana/utils/util.h | 24 +- openpilot/tools/cabana/videowidget.cc | 4 +- openpilot/tools/cabana/videowidget.h | 2 +- tools/setup_dependencies.sh | 2 +- 41 files changed, 1579 insertions(+), 1054 deletions(-) diff --git a/openpilot/tools/cabana/.gitignore b/openpilot/tools/cabana/.gitignore index e4212612df..7f9ac0fde0 100644 --- a/openpilot/tools/cabana/.gitignore +++ b/openpilot/tools/cabana/.gitignore @@ -3,6 +3,7 @@ moc_* *.generated.qrc assets.cc +bootstrap_icons.cc _cabana dbc/car_fingerprint_to_dbc.json diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index e9c1a976bb..fcaa3b7930 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -30,7 +30,7 @@ if arch == "Darwin": ] qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules] qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")] - qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"] + qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin")) else: qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip() @@ -46,7 +46,7 @@ else: qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else [] qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules] - qt_libs = [f"Qt5{m}" for m in qt_modules] + ["GL"] + qt_libs = [f"Qt5{m}" for m in qt_modules] qt_env['QT3DIR'] = qt_env['QTDIR'] qt_env.Tool('qt3') @@ -67,9 +67,7 @@ base_frameworks = qt_env['FRAMEWORKS'] base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"] if arch == "Darwin": - base_frameworks += ['QtCharts', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox'] -else: - base_libs.append('Qt5Charts') + base_frameworks += ['CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox'] cabana_env = qt_env.Clone() cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR] @@ -79,22 +77,26 @@ cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['bz2', opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../../opendbc_repo/opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] -def write_assets_qrc(target, source, env): - with open(str(source[0])) as f: - qrc = f.read() - with open(str(target[0]), "w") as f: - f.write(qrc.replace("@BOOTSTRAP_ICONS_SVG@", str(bootstrap_icons.SVG_PATH))) +# embed the bootstrap icons SVG into the binary +def build_bootstrap_icons_src(target, source, env): + data = open(str(source[0]), 'rb').read() + with open(str(target[0]), 'w') as f: + f.write('#include \n') + f.write('extern const unsigned char bootstrap_icons_svg[];\n') + f.write('extern const size_t bootstrap_icons_svg_len;\n') + f.write('const unsigned char bootstrap_icons_svg[] = {\n') + for i in range(0, len(data), 32): + f.write(','.join(str(b) for b in data[i:i+32]) + ',\n') + f.write('};\n') + f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n') + return None + +bootstrap_icons_src = cabana_env.Command('assets/bootstrap_icons.cc', str(bootstrap_icons.SVG_PATH), build_bootstrap_icons_src) # build assets assets = "assets/assets.cc" -assets_src = cabana_env.Command( - "assets/assets.generated.qrc", - "assets/assets.qrc", - write_assets_qrc, -) -cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET") -cabana_env.Depends(assets_src, str(bootstrap_icons.SVG_PATH)) -cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"])) +cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") +cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', @@ -104,7 +106,7 @@ cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc' 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] if arch != "Darwin": cabana_srcs += ['streams/socketcanstream.cc'] -cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs, LIBS=cabana_libs, FRAMEWORKS=base_frameworks) +cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs + [bootstrap_icons_src], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): diff --git a/openpilot/tools/cabana/assets/assets.qrc b/openpilot/tools/cabana/assets/assets.qrc index f5880e5580..009d63f008 100644 --- a/openpilot/tools/cabana/assets/assets.qrc +++ b/openpilot/tools/cabana/assets/assets.qrc @@ -1,6 +1,5 @@ - @BOOTSTRAP_ICONS_SVG@ cabana-icon.png diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index f396d1c13f..5e919dc6a3 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -37,7 +37,7 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh); addShortcuts(); setWhatsThis(R"( @@ -66,7 +66,7 @@ void BinaryView::addShortcuts() { QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated); QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{ if (hovered_sig != nullptr) { - UndoStack::push(new RemoveSigCommand(model->msg_id, hovered_sig)); + UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig)); hovered_sig = nullptr; } }); @@ -126,7 +126,7 @@ void BinaryView::highlight(const cabana::Signal *sig) { } void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) { - auto index = indexAt(viewport()->mapFromGlobal(QCursor::pos())); + auto index = indexAt(last_mouse_pos); if (!anchor_index.isValid() || !index.isValid()) return; @@ -141,7 +141,7 @@ void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionF void BinaryView::mousePressEvent(QMouseEvent *event) { resize_sig = nullptr; - if (auto index = indexAt(event->pos()); index.isValid() && index.column() != 8) { + if (auto index = indexAt(last_mouse_pos = event->pos()); index.isValid() && index.column() != 8) { anchor_index = index; auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); int bit_pos = get_bit_pos(anchor_index); @@ -158,7 +158,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) { } void BinaryView::highlightPosition(const QPoint &pos) { - if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) { + if (auto index = indexAt(pos); index.isValid()) { auto item = (BinaryViewModel::Item *)index.internalPointer(); const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back(); highlight(sig); @@ -166,7 +166,7 @@ void BinaryView::highlightPosition(const QPoint &pos) { } void BinaryView::mouseMoveEvent(QMouseEvent *event) { - highlightPosition(event->globalPos()); + highlightPosition(last_mouse_pos = event->pos()); QTableView::mouseMoveEvent(event); } @@ -179,7 +179,7 @@ void BinaryView::mouseReleaseEvent(QMouseEvent *event) { auto sig = resize_sig ? *resize_sig : cabana::Signal{}; std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index); resize_sig ? emit editSignal(resize_sig, sig) - : UndoStack::push(new AddSigCommand(model->msg_id, sig)); + : UndoStack::instance()->push(new AddSigCommand(model->msg_id, sig)); } else { auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer(); if (item && item->sigs.size() > 0) @@ -208,7 +208,7 @@ void BinaryView::refresh() { resize_sig = nullptr; hovered_sig = nullptr; model->refresh(); - highlightPosition(QCursor::pos()); + if (underMouse()) highlightPosition(last_mouse_pos); } std::set BinaryView::getOverlappingSignals() const { diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index e568228b37..c49067a1a2 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -94,6 +94,7 @@ private: void highlightPosition(const QPoint &pt); QModelIndex anchor_index; + QPoint last_mouse_pos{-1, -1}; BinaryViewModel *model; BinaryItemDelegate *delegate; bool is_message_active = false; diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 56b43361f8..d8e21815dc 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -129,11 +129,10 @@ int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) { int main(int argc, char *argv[]) { QCoreApplication::setApplicationName("Cabana"); - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); initApp(argc, argv, false); QApplication app(argc, argv); app.setApplicationDisplayName("Cabana"); - app.setWindowIcon(QIcon(":cabana-icon.png")); + //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui UnixSignalHandler signalHandler; utils::setTheme(settings.theme); diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc index 997bd19198..a8df373dc4 100644 --- a/openpilot/tools/cabana/cameraview.cc +++ b/openpilot/tools/cabana/cameraview.cc @@ -1,139 +1,40 @@ #include "tools/cabana/cameraview.h" -#ifdef __APPLE__ -#include -#else -#include -#endif - +#include +#include +#include #include #include +#include -namespace { - -const char frame_vertex_shader[] = -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" -#endif - "layout(location = 0) in vec4 aPosition;\n" - "layout(location = 1) in vec2 aTexCoord;\n" - "uniform mat4 uTransform;\n" - "out vec2 vTexCoord;\n" - "void main() {\n" - " gl_Position = uTransform * aPosition;\n" - " vTexCoord = aTexCoord;\n" - "}\n"; - -const char frame_fragment_shader[] = -#ifdef __APPLE__ - "#version 330 core\n" -#else - "#version 300 es\n" - "precision mediump float;\n" -#endif - "uniform sampler2D uTextureY;\n" - "uniform sampler2D uTextureUV;\n" - "in vec2 vTexCoord;\n" - "out vec4 colorOut;\n" - "void main() {\n" - " float y = texture(uTextureY, vTexCoord).r;\n" - " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" - " float r = y + 1.402 * uv.y;\n" - " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" - " float b = y + 1.772 * uv.x;\n" - " colorOut = vec4(r, g, b, 1.0);\n" - "}\n"; - -} // namespace +#include "common/yuv.h" CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : - stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { + stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); qRegisterMetaType>("availableStreams"); - QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); } CameraWidget::~CameraWidget() { - makeCurrent(); stopVipcThread(); - if (isValid()) { - glDeleteVertexArrays(1, &frame_vao); - glDeleteBuffers(1, &frame_vbo); - glDeleteBuffers(1, &frame_ibo); - glDeleteTextures(2, textures); - shader_program_.reset(); - } - doneCurrent(); -} - -void CameraWidget::initializeGL() { - initializeOpenGLFunctions(); - - shader_program_ = std::make_unique(context()); - shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); - shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); - shader_program_->link(); - - GLint frame_pos_loc = shader_program_->attributeLocation("aPosition"); - GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord"); - - auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); - const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; - const float frame_coords[4][4] = { - {-1.0, -1.0, x2, y1}, // bl - {-1.0, 1.0, x2, y2}, // tl - { 1.0, 1.0, x1, y2}, // tr - { 1.0, -1.0, x1, y1}, // br - }; - - glGenVertexArrays(1, &frame_vao); - glBindVertexArray(frame_vao); - glGenBuffers(1, &frame_vbo); - glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); - glEnableVertexAttribArray(frame_pos_loc); - glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)0); - glEnableVertexAttribArray(frame_texcoord_loc); - glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, - sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); - glGenBuffers(1, &frame_ibo); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindVertexArray(0); - - glGenTextures(2, textures); - - shader_program_->bind(); - shader_program_->setUniformValue("uTextureY", 0); - shader_program_->setUniformValue("uTextureUV", 1); - shader_program_->release(); } void CameraWidget::showEvent(QShowEvent *event) { - if (!vipc_thread) { + if (!vipc_thread.joinable()) { clearFrames(); - vipc_thread = new QThread(); - connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); - connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); - vipc_thread->start(); + vipc_exit = false; + vipc_thread = std::thread(&CameraWidget::vipcThread, this); } } void CameraWidget::stopVipcThread() { - makeCurrent(); - if (vipc_thread) { - vipc_thread->requestInterruption(); - vipc_thread->quit(); - vipc_thread->wait(); - vipc_thread = nullptr; + vipc_exit = true; + if (vipc_thread.joinable()) { + vipc_thread.join(); } } @@ -141,74 +42,29 @@ void CameraWidget::availableStreamsUpdated(std::set streams) { available_streams = streams; } -void CameraWidget::paintGL() { - glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); - glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); +void CameraWidget::paintEvent(QPaintEvent *event) { + QPainter p(this); + p.fillRect(rect(), bg); std::lock_guard lk(frame_lock); - if (!current_frame_) return; + if (rgb_frame.isNull()) return; // Scale for aspect ratio float widget_ratio = (float)width() / height(); - float frame_ratio = (float)stream_width / stream_height; - float scale_x = std::min(frame_ratio / widget_ratio, 1.0f); - float scale_y = std::min(widget_ratio / frame_ratio, 1.0f); + float frame_ratio = (float)rgb_frame.width() / rgb_frame.height(); + int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f)); + int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f)); + QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h); - glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()); - - shader_program_->bind(); - QMatrix4x4 transform; - transform.scale(scale_x, scale_y, 1.0f); - shader_program_->setUniformValue("uTransform", transform); - - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y); - - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv); - - glBindVertexArray(frame_vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr); - glBindVertexArray(0); - - // Reset both texture units - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, 0); - glPixelStorei(GL_UNPACK_ALIGNMENT, 4); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - - shader_program_->release(); -} - -void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { - makeCurrent(); - stream_width = vipc_client->buffers[0].width; - stream_height = vipc_client->buffers[0].height; - stream_stride = vipc_client->buffers[0].stride; - - glBindTexture(GL_TEXTURE_2D, textures[0]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); - - glBindTexture(GL_TEXTURE_2D, textures[1]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); - assert(glGetError() == GL_NO_ERROR); + p.setRenderHint(QPainter::SmoothPixmapTransform); + if (active_stream_type == VISION_STREAM_DRIVER) { + // mirror driver camera horizontally + const qreal cx = video_rect.x() + video_rect.width() / 2.0; + p.translate(cx, 0); + p.scale(-1, 1); + p.translate(-cx, 0); + } + p.drawImage(video_rect, rgb_frame); } void CameraWidget::vipcFrameReceived() { @@ -220,7 +76,7 @@ void CameraWidget::vipcThread() { std::unique_ptr vipc_client; VisionIpcBufExtra frame_meta = {}; - while (!QThread::currentThread()->isInterruptionRequested()) { + while (!vipc_exit) { if (!vipc_client || cur_stream != requested_stream_type) { clearFrames(); fprintf(stderr, "connecting to stream %d, was connected to %d\n", @@ -234,23 +90,27 @@ void CameraWidget::vipcThread() { clearFrames(); auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); if (streams.empty()) { - QThread::msleep(100); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } emit vipcAvailableStreamsUpdated(streams); if (!vipc_client->connect(false)) { - QThread::msleep(100); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } - emit vipcThreadConnected(vipc_client.get()); } if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { + // NV12 -> RGBA once per frame on the receive thread; paint just draws the image + if (rgb_back.width() != (int)buf->width || rgb_back.height() != (int)buf->height) { + rgb_back = QImage(buf->width, buf->height, QImage::Format_RGBA8888); + } + yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride, + rgb_back.bits(), rgb_back.bytesPerLine(), buf->width, buf->height); { std::lock_guard lk(frame_lock); - current_frame_ = buf; - frame_meta_ = frame_meta; + rgb_frame.swap(rgb_back); } emit vipcThreadFrameReceived(); } @@ -259,6 +119,7 @@ void CameraWidget::vipcThread() { void CameraWidget::clearFrames() { std::lock_guard lk(frame_lock); - current_frame_ = nullptr; + rgb_frame = QImage(); + rgb_back = QImage(); available_streams.clear(); } diff --git a/openpilot/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h index 930b13d82c..40d3776005 100644 --- a/openpilot/tools/cabana/cameraview.h +++ b/openpilot/tools/cabana/cameraview.h @@ -1,23 +1,21 @@ #pragma once -#include +#include #include #include #include +#include #include -#include -#include -#include -#include +#include +#include #include "msgq/visionipc/visionipc_client.h" -class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { +class CameraWidget : public QWidget { Q_OBJECT public: - using QOpenGLWidget::QOpenGLWidget; explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); ~CameraWidget(); void setStreamType(VisionStreamType type) { requested_stream_type = type; } @@ -26,37 +24,30 @@ public: signals: void clicked(); - void vipcThreadConnected(VisionIpcClient *); void vipcThreadFrameReceived(); void vipcAvailableStreamsUpdated(std::set); protected: - void paintGL() override; - void initializeGL() override; + void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override { stopVipcThread(); } void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } void vipcThread(); void clearFrames(); - GLuint frame_vao, frame_vbo, frame_ibo; - GLuint textures[2]; - std::unique_ptr shader_program_; QColor bg = Qt::black; + QImage rgb_frame; // written by vipc thread, drawn by GUI thread; guarded by frame_lock + QImage rgb_back; // vipc thread only std::string stream_name; - int stream_width = 0; - int stream_height = 0; - int stream_stride = 0; std::atomic active_stream_type; std::atomic requested_stream_type; std::set available_streams; - QThread *vipc_thread = nullptr; - std::recursive_mutex frame_lock; - VisionBuf* current_frame_ = nullptr; - VisionIpcBufExtra frame_meta_ = {}; + std::thread vipc_thread; + std::atomic vipc_exit = false; + std::mutex frame_lock; protected slots: - void vipcConnected(VisionIpcClient *vipc_client); void vipcFrameReceived(); void availableStreamsUpdated(std::set streams); }; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index 0ce5d051b3..8496c26994 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -6,50 +6,37 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include "tools/cabana/chart/chartswidget.h" -// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) const int AXIS_X_TOP_MARGIN = 4; -const double MIN_ZOOM_SECONDS = 0.01; // 10ms +const int X_TICK_COUNT = 5; +const double MIN_ZOOM_SECONDS = 0.01; // 10ms // Define a small value of epsilon to compare double values const float EPSILON = 0.000001; static inline bool xLessThan(const QPointF &p, float x) { return p.x() < (x - EPSILON); } +static QMargins layoutMargins(const QStyle *style) { + return { + style->pixelMetric(QStyle::PM_LayoutLeftMargin), + style->pixelMetric(QStyle::PM_LayoutTopMargin), + style->pixelMetric(QStyle::PM_LayoutRightMargin), + style->pixelMetric(QStyle::PM_LayoutBottomMargin), + }; +} + ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) - : charts_widget(parent), QChartView(parent) { + : x_min(x_range.first), x_max(x_range.second), charts_widget(parent), QWidget(parent) { series_type = (SeriesType)settings.chart_series_type; - chart()->setBackgroundVisible(false); - axis_x = new QValueAxis(this); - axis_y = new QValueAxis(this); - chart()->addAxis(axis_x, Qt::AlignBottom); - chart()->addAxis(axis_y, Qt::AlignLeft); - chart()->legend()->layout()->setContentsMargins(0, 0, 0, 0); - chart()->legend()->setShowToolTips(true); - chart()->setMargins({0, 0, 0, 0}); - - axis_x->setRange(x_range.first, x_range.second); - + align_to = 50; + setMouseTracking(true); tip_label = new TipLabel(this); createToolButtons(); - setRubberBand(QChartView::HorizontalRubberBand); - setMouseTracking(true); - setTheme(utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); signal_value_font.setPointSize(9); - QObject::connect(axis_y, &QValueAxis::rangeChanged, this, &ChartView::resetChartCache); - QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, this, &ChartView::resetChartCache); - QObject::connect(window()->windowHandle(), &QWindow::screenChanged, this, &ChartView::resetChartCache); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved); QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated); QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved); @@ -57,13 +44,7 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par } void ChartView::createToolButtons() { - move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart()); - move_icon->setToolTip(tr("Drag and drop to move chart")); - - QToolButton *remove_btn = new ToolButton("x", tr("Remove Chart")); - close_btn_proxy = new QGraphicsProxyWidget(chart()); - close_btn_proxy->setWidget(remove_btn); - close_btn_proxy->setZValue(chart()->zValue() + 11); + close_btn = new ToolButton("x", tr("Remove Chart"), this); menu = new QMenu(this); // series types @@ -81,17 +62,14 @@ void ChartView::createToolButtons() { menu->addAction(tr("Manage Signals"), this, &ChartView::manageSignals); split_chart_act = menu->addAction(tr("Split Chart"), [this]() { charts_widget->splitChart(this); }); - QToolButton *manage_btn = new ToolButton("list", ""); + manage_btn = new ToolButton("list", "", this); manage_btn->setMenu(menu); manage_btn->setPopupMode(QToolButton::InstantPopup); manage_btn->setStyleSheet("QToolButton::menu-indicator { image: none; }"); - manage_btn_proxy = new QGraphicsProxyWidget(chart()); - manage_btn_proxy->setWidget(manage_btn); - manage_btn_proxy->setZValue(chart()->zValue() + 11); close_act = new QAction(tr("Close"), this); QObject::connect(close_act, &QAction::triggered, [this] () { charts_widget->removeChart(this); }); - QObject::connect(remove_btn, &QToolButton::clicked, close_act, &QAction::triggered); + QObject::connect(close_btn, &QToolButton::clicked, close_act, &QAction::triggered); QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { setSeriesType((SeriesType)action->data().toInt()); }); @@ -101,29 +79,11 @@ QSize ChartView::sizeHint() const { return {CHART_MIN_WIDTH, settings.chart_height}; } -void ChartView::setTheme(QChart::ChartTheme theme) { - chart()->setTheme(theme); - if (theme == QChart::ChartThemeDark) { - axis_x->setTitleBrush(palette().text()); - axis_x->setLabelsBrush(palette().text()); - axis_y->setTitleBrush(palette().text()); - axis_y->setLabelsBrush(palette().text()); - chart()->legend()->setLabelColor(palette().color(QPalette::Text)); - } - axis_x->setLineVisible(false); - axis_y->setLineVisible(false); - for (auto &s : sigs) { - s.series->setColor(toQColor(s.sig->color)); - } -} - void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { if (hasSignal(msg_id, sig)) return; - QXYSeries *series = createSeries(series_type, toQColor(sig->color)); - sigs.push_back({.msg_id = msg_id, .sig = sig, .series = series}); + sigs.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(toQColor(sig->color))}); updateSeries(sig); - updateSeriesPoints(); updateTitle(); emit charts_widget->seriesChanged(); } @@ -134,29 +94,21 @@ bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) co void ChartView::removeIf(std::function predicate) { int prev_size = sigs.size(); - for (auto it = sigs.begin(); it != sigs.end(); /**/) { - if (predicate(*it)) { - chart()->removeSeries(it->series); - it->series->deleteLater(); - it = sigs.erase(it); - } else { - ++it; - } - } + sigs.erase(std::remove_if(sigs.begin(), sigs.end(), predicate), sigs.end()); if (sigs.empty()) { charts_widget->removeChart(this); } else if (sigs.size() != prev_size) { emit charts_widget->seriesChanged(); updateAxisY(); - resetChartCache(); + updateTitle(); } } void ChartView::signalUpdated(const cabana::Signal *sig) { auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); if (it != sigs.end()) { - if (it->series->color() != toQColor(sig->color)) { - setSeriesColor(it->series, toQColor(sig->color)); + if (it->color != toQColor(sig->color)) { + it->color = uniqueColor(toQColor(sig->color), sig); } updateTitle(); updateSeries(sig); @@ -186,98 +138,75 @@ void ChartView::manageSignals() { } void ChartView::resizeEvent(QResizeEvent *event) { - qreal left, top, right, bottom; - chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); - move_icon->setPos(left, top); - close_btn_proxy->setPos(rect().right() - right - close_btn_proxy->size().width(), top); - int x = close_btn_proxy->pos().x() - manage_btn_proxy->size().width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); - manage_btn_proxy->setPos(x, top); - if (align_to > 0) { - updatePlotArea(align_to, true); - } - QChartView::resizeEvent(event); + QWidget::resizeEvent(event); + const auto margins = layoutMargins(style()); + QPixmap grip = utils::icon("grip-horizontal"); + move_icon_rect = QRect(QPoint(margins.left(), margins.top()), grip.size() / grip.devicePixelRatio()); + close_btn->resize(close_btn->sizeHint()); + manage_btn->resize(manage_btn->sizeHint()); + close_btn->move(rect().right() - margins.right() - close_btn->width(), margins.top()); + manage_btn->move(close_btn->x() - manage_btn->width() - style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), margins.top()); + updatePlotArea(align_to, true); } void ChartView::updatePlotArea(int left_pos, bool force) { if (align_to != left_pos || force) { align_to = left_pos; - qreal left, top, right, bottom; - chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); - QSizeF legend_size = chart()->legend()->layout()->minimumSize(); - legend_size.setWidth(manage_btn_proxy->sceneBoundingRect().left() - move_icon->sceneBoundingRect().right()); - chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), legend_size}); + const auto margins = layoutMargins(style()); + QFont bold_font = font(); + bold_font.setBold(true); + QFontMetrics fm(font()), bfm(bold_font); + const int marker_size = fm.height() - 4; + const int row_height = std::max(marker_size, fm.height()) + QFontMetrics(signal_value_font).height() + 3; + const int legend_left = move_icon_rect.right() + margins.left(); + const int legend_right = std::max(manage_btn->x() - margins.right(), legend_left + 10); - // add top space for signal value - int adjust_top = chart()->legend()->geometry().height() + QFontMetrics(signal_value_font).height() + 3; - adjust_top = std::max(adjust_top, manage_btn_proxy->sceneBoundingRect().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); + // layout legend entries left-to-right, wrapping between the move icon and the buttons + legend_rects.clear(); + int x = legend_left, y = margins.top(); + for (auto &s : sigs) { + int w = marker_size + 5 + bfm.horizontalAdvance(QString::fromStdString(s.sig->name)) + + fm.horizontalAdvance(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString())); + w = std::min(w, legend_right - legend_left); // keep oversized entries clear of the header buttons + if (x + w > legend_right && x > legend_left) { + x = legend_left; + y += row_height; + } + legend_rects.emplace_back(x, y, w, std::max(marker_size, fm.height())); + x += w + 12; + } + + // add top space for the legend and signal values + int adjust_top = (y + row_height) - margins.top(); + adjust_top = std::max(adjust_top, manage_btn->geometry().bottom() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); // add right space for x-axis label - QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2)); - x_label_size += QSizeF{5, 5}; - chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom)); - chart()->layout()->invalidate(); + QSizeF x_label_size = fm.size(Qt::TextSingleLine, QString::number(x_max, 'f', xAxisPrecision())) + QSizeF{5, 5}; + plot_area = rect().adjusted(align_to + margins.left(), adjust_top + margins.top(), + -x_label_size.width() / 2 - margins.right(), + -x_label_size.height() - margins.bottom()); resetChartCache(); } } void ChartView::updateTitle() { - for (QLegendMarker *marker : chart()->legend()->markers()) { - QObject::connect(marker, &QLegendMarker::clicked, this, &ChartView::handleMarkerClicked, Qt::UniqueConnection); - } - - // Use CSS to draw titles in the WindowText color - auto tmp = palette().color(QPalette::WindowText); - auto titleColorCss = tmp.name(QColor::HexArgb); - // Draw message details in similar color, but slightly fade it to the background - tmp.setAlpha(180); - auto msgColorCss = tmp.name(QColor::HexArgb); - - for (auto &s : sigs) { - auto decoration = s.series->isVisible() ? "none" : "line-through"; - s.series->setName(QString("%3 %5 %6") - .arg(decoration, titleColorCss, QString::fromStdString(s.sig->name), - msgColorCss, QString::fromStdString(msgName(s.msg_id)), QString::fromStdString(s.msg_id.toString()))); - } split_chart_act->setEnabled(sigs.size() > 1); - resetChartCache(); + updatePlotArea(align_to, true); } void ChartView::updatePlot(double cur, double min, double max) { cur_sec = cur; - if (min != axis_x->min() || max != axis_x->max()) { - axis_x->setRange(min, max); + if (min != x_min || max != x_max) { + x_min = min; + x_max = max; updateAxisY(); - updateSeriesPoints(); // update tooltip if (tooltip_x >= 0) { - showTip(chart()->mapToValue({tooltip_x, 0}).x()); + showTip(secondsAtPoint({tooltip_x, 0})); } resetChartCache(); } - viewport()->update(); -} - -void ChartView::updateSeriesPoints() { - // Show points when zoomed in enough - for (auto &s : sigs) { - auto begin = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto end = std::lower_bound(begin, s.vals.cend(), axis_x->max(), xLessThan); - if (begin != end) { - int num_points = std::max((end - begin), 1); - QPointF right_pt = end == s.vals.cend() ? s.vals.back() : *end; - double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points; - - if (series_type == SeriesType::Scatter) { - qreal size = std::clamp(pixels_per_point / 2.0, 2.0, 8.0); - if (s.series->useOpenGL()) { - size *= devicePixelRatioF(); - } - ((QScatterSeries *)s.series)->setMarkerSize(size); - } else { - s.series->setPointsVisible(num_points == 1 || pixels_per_point > 20); - } - } - } + update(); } void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector &events, @@ -322,8 +251,6 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * if (!can->liveStreaming()) { s.segment_tree.build(s.vals); } - const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; - s.series->replace(QVector(points.cbegin(), points.cend())); } } updateAxisY(); @@ -340,15 +267,15 @@ void ChartView::updateAxisY() { QString unit = QString::fromStdString(sigs[0].sig->unit); for (auto &s : sigs) { - if (!s.series->isVisible()) continue; + if (!s.visible) continue; // Only show unit when all signals have the same unit if (unit != QString::fromStdString(s.sig->unit)) { unit.clear(); } - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); s.min = std::numeric_limits::max(); s.max = std::numeric_limits::lowest(); if (can->liveStreaming()) { @@ -365,28 +292,28 @@ void ChartView::updateAxisY() { if (min == std::numeric_limits::max()) min = 0; if (max == std::numeric_limits::lowest()) max = 0; - if (axis_y->titleText() != unit) { - axis_y->setTitleText(unit); + if (y_unit != unit) { + y_unit = unit; y_label_width = 0; // recalc width } double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, 3); - if (min_y != axis_y->min() || max_y != axis_y->max() || y_label_width == 0) { - axis_y->setRange(min_y, max_y); - axis_y->setTickCount(tick_count); + if (min_y != y_min || max_y != y_max || y_label_width == 0) { + y_min = min_y; + y_max = max_y; + y_tick_count = tick_count; + y_precision = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); - int n = std::max(int(-std::floor(std::log10((max_y - min_y) / (tick_count - 1)))), 0); + QFontMetrics fm(font()); int max_label_width = 0; - QFontMetrics fm(axis_y->labelsFont()); for (int i = 0; i < tick_count; i++) { qreal value = min_y + (i * (max_y - min_y) / (tick_count - 1)); - max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', n))); + max_label_width = std::max(max_label_width, fm.horizontalAdvance(QString::number(value, 'f', y_precision))); } - int title_spacing = unit.isEmpty() ? 0 : QFontMetrics(axis_y->titleFont()).size(Qt::TextSingleLine, unit).height(); + int title_spacing = y_unit.isEmpty() ? 0 : fm.size(Qt::TextSingleLine, y_unit).height(); y_label_width = title_spacing + max_label_width + 15; - axis_y->setLabelFormat(QString("%.%1f").arg(n)); emit axisYLabelWidthChanged(y_label_width); } } @@ -400,6 +327,10 @@ std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal m return {min * step, max * step, tick_count}; } +int ChartView::xAxisPrecision() const { + return std::max(int(-std::floor(std::log10((x_max - x_min) / (X_TICK_COUNT - 1)))), 2); +} + // nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n qreal ChartView::niceNumber(qreal x, bool ceiling) { qreal z = std::pow(10, std::floor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x @@ -430,344 +361,394 @@ void ChartView::contextMenuEvent(QContextMenuEvent *event) { } void ChartView::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { - QMimeData *mimeData = new QMimeData; - mimeData->setData(CHART_MIME_TYPE, QByteArray::number((qulonglong)this)); - QPixmap px = grab().scaledToWidth(CHART_MIN_WIDTH * viewport()->devicePixelRatio(), Qt::SmoothTransformation); - charts_widget->stopAutoScroll(); - QDrag *drag = new QDrag(this); - drag->setMimeData(mimeData); - drag->setPixmap(px); - drag->setHotSpot(-QPoint(5, 5)); - drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::MoveAction); - } else if (event->button() == Qt::LeftButton && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { + press_pos = event->pos(); + if (event->button() == Qt::LeftButton && move_icon_rect.contains(event->pos())) { + charts_widget->startChartDrag(this, event->globalPos()); + } else if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::ShiftModifier)) { // Save current playback state when scrubbing resume_after_scrub = !can->isPaused(); if (resume_after_scrub) { can->pause(true); } - is_scrubbing = true; + mouse_mode = MouseMode::Scrub; + } else if (event->button() == Qt::LeftButton && plot_area.contains(event->pos())) { + mouse_mode = MouseMode::Rubber; + rubber_rect = QRect(); } else { - QChartView::mousePressEvent(event); - } -} - -void ChartView::mouseReleaseEvent(QMouseEvent *event) { - auto rubber = findChild(); - if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) { - rubber->hide(); - auto rect = rubber->geometry().normalized(); - // Prevent zooming/seeking past the end of the route - double min = std::clamp(chart()->mapToValue(rect.topLeft()).x(), can->minSeconds(), can->maxSeconds()); - double max = std::clamp(chart()->mapToValue(rect.bottomRight()).x(), can->minSeconds(), can->maxSeconds()); - if (rubber->width() <= 0) { - // no rubber dragged, seek to mouse position - can->seekTo(min); - } else if (rubber->width() > 10 && (max - min) > MIN_ZOOM_SECONDS) { - charts_widget->zoom_undo_stack->push(new ZoomCommand({min, max})); - } else { - viewport()->update(); - } - event->accept(); - } else if (event->button() == Qt::RightButton) { - charts_widget->zoom_undo_stack->undo(); - event->accept(); - } else { - QGraphicsView::mouseReleaseEvent(event); - } - - // Resume playback if we were scrubbing - is_scrubbing = false; - if (resume_after_scrub) { - can->pause(false); - resume_after_scrub = false; + QWidget::mousePressEvent(event); } } void ChartView::mouseMoveEvent(QMouseEvent *ev) { - const auto plot_area = chart()->plotArea(); // Scrubbing - if (is_scrubbing && QApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) { + if (mouse_mode == MouseMode::Scrub && ev->modifiers().testFlag(Qt::ShiftModifier)) { if (plot_area.contains(ev->pos())) { - can->seekTo(std::clamp(chart()->mapToValue(ev->pos()).x(), can->minSeconds(), can->maxSeconds())); + can->seekTo(std::clamp(secondsAtPoint(ev->pos()), can->minSeconds(), can->maxSeconds())); } } - auto rubber = findChild(); - bool is_zooming = rubber && rubber->isVisible(); - clearTrackPoints(); + if (mouse_mode == MouseMode::Rubber) { + // horizontal selection, clamped to the plot area + int left = std::clamp(std::min(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); + int right = std::clamp(std::max(press_pos.x(), ev->pos().x()), plot_area.left(), plot_area.right()); + rubber_rect = QRect(left, plot_area.top(), right - left, plot_area.height()); + update(); + } - if (!is_zooming && plot_area.contains(ev->pos()) && isActiveWindow()) { + clearTrackPoints(); + if (mouse_mode != MouseMode::Rubber && plot_area.contains(ev->pos()) && isActiveWindow()) { charts_widget->showValueTip(secondsAtPoint(ev->pos())); } else if (tip_label->isVisible()) { charts_widget->showValueTip(-1); } + QWidget::mouseMoveEvent(ev); +} - QChartView::mouseMoveEvent(ev); - if (is_zooming) { - QRect rubber_rect = rubber->geometry(); - rubber_rect.setLeft(std::max(rubber_rect.left(), (int)plot_area.left())); - rubber_rect.setRight(std::min(rubber_rect.right(), (int)plot_area.right())); - if (rubber_rect != rubber->geometry()) { - rubber->setGeometry(rubber_rect); +void ChartView::mouseReleaseEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::Rubber) { + mouse_mode = MouseMode::None; + // Prevent zooming/seeking past the end of the route + double min = std::clamp(secondsAtPoint(rubber_rect.topLeft()), can->minSeconds(), can->maxSeconds()); + double max = std::clamp(secondsAtPoint(rubber_rect.bottomRight()), can->minSeconds(), can->maxSeconds()); + if (rubber_rect.width() <= 0) { + // no rubber dragged, seek to mouse position + can->seekTo(std::clamp(secondsAtPoint(press_pos), can->minSeconds(), can->maxSeconds())); + } else if (rubber_rect.width() > 10 && (max - min) > MIN_ZOOM_SECONDS) { + charts_widget->zoom_undo_stack.push(new ZoomCommand({min, max})); + } + rubber_rect = QRect(); + update(); + } else if (event->button() == Qt::LeftButton && mouse_mode == MouseMode::None && sigs.size() > 1) { + // toggle series visibility by clicking its legend entry + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + if (legend_rects[i].contains(press_pos) && legend_rects[i].contains(event->pos())) { + sigs[i].visible = !sigs[i].visible; + updateAxisY(); + updateTitle(); + break; + } + } + } else if (event->button() == Qt::RightButton) { + charts_widget->zoom_undo_stack.undo(); + } else { + QWidget::mouseReleaseEvent(event); + } + + // Resume playback if we were scrubbing + if (mouse_mode == MouseMode::Scrub) { + mouse_mode = MouseMode::None; + if (resume_after_scrub) { + can->pause(false); + resume_after_scrub = false; } - viewport()->update(); } } +void ChartView::takeSignalsFrom(ChartView *source) { + for (auto &s : source->sigs) { + sigs.push_back(std::move(s)); + sigs.back().color = uniqueColor(sigs.back().color, sigs.back().sig); + } + source->sigs.clear(); + updateAxisY(); + updateTitle(); + charts_widget->removeChart(source); +} + void ChartView::showTip(double sec) { - QRect tip_area(0, chart()->plotArea().top(), rect().width(), chart()->plotArea().height()); + QRect tip_area(0, plot_area.top(), rect().width(), plot_area.height()); QRect visible_rect = charts_widget->chartVisibleRect(this).intersected(tip_area); if (visible_rect.isEmpty()) { tip_label->hide(); return; } - tooltip_x = chart()->mapToPosition({sec, 0}).x(); + tooltip_x = xPos(sec); qreal x = -1; QStringList text_list; for (auto &s : sigs) { - if (s.series->isVisible()) { + if (s.visible) { QString value = "--"; // use reverse iterator to find last item <= sec. auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double v) { return p.x() > v; }); - if (it != s.vals.crend() && it->x() >= axis_x->min()) { + if (it != s.vals.crend() && it->x() >= x_min) { value = QString::fromStdString(s.sig->formatValue(it->y(), false)); s.track_pt = *it; - x = std::max(x, chart()->mapToPosition(*it).x()); + x = std::max(x, xPos(it->x())); } QString name = sigs.size() > 1 ? QString::fromStdString(s.sig->name) + ": " : ""; QString min = s.min == std::numeric_limits::max() ? "--" : QString::number(s.min); QString max = s.max == std::numeric_limits::lowest() ? "--" : QString::number(s.max); text_list << QString("■ %2%3 (%4, %5)") - .arg(s.series->color().name(), name, value, min, max); + .arg(s.color.name(), name, value, min, max); } } if (x < 0) { x = tooltip_x; } - QPoint pt(x, chart()->plotArea().top()); - text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); + QPoint pt(x, plot_area.top()); + text_list.push_front(QString::number(secondsAtPoint({x, 0}), 'f', 3)); QString text = "

" % text_list.join("
") % "

"; tip_label->showText(pt, text, this, visible_rect); - viewport()->update(); + update(); } void ChartView::hideTip() { clearTrackPoints(); tooltip_x = -1; tip_label->hide(); - viewport()->update(); -} - -void ChartView::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - drawDropIndicator(event->source() != this); - event->acceptProposedAction(); - } -} - -void ChartView::dragMoveEvent(QDragMoveEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction); - event->accept(); - } - charts_widget->startAutoScroll(); -} - -void ChartView::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - if (event->source() != this) { - ChartView *source_chart = (ChartView *)event->source(); - for (auto &s : source_chart->sigs) { - source_chart->chart()->removeSeries(s.series); - addSeries(s.series); - } - sigs.insert(sigs.end(), std::move_iterator(source_chart->sigs.begin()), std::move_iterator(source_chart->sigs.end())); - updateAxisY(); - updateTitle(); - - source_chart->sigs.clear(); - charts_widget->removeChart(source_chart); - event->acceptProposedAction(); - } - can_drop = false; - } + update(); } void ChartView::resetChartCache() { chart_pixmap = QPixmap(); - viewport()->update(); + update(); } void ChartView::paintEvent(QPaintEvent *event) { - if (!can->liveStreaming()) { - if (chart_pixmap.isNull()) { - const qreal dpr = viewport()->devicePixelRatioF(); - chart_pixmap = QPixmap(viewport()->size() * dpr); - chart_pixmap.setDevicePixelRatio(dpr); - QPainter p(&chart_pixmap); - p.setRenderHints(QPainter::Antialiasing); - drawBackground(&p, viewport()->rect()); - scene()->setSceneRect(viewport()->rect()); - scene()->render(&p, viewport()->rect()); - } + QPainter painter(this); + painter.setRenderHints(QPainter::Antialiasing); - QPainter painter(viewport()); - painter.setRenderHints(QPainter::Antialiasing); - painter.drawPixmap(QPoint(), chart_pixmap); - if (can_drop) { - painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); - painter.drawRect(viewport()->rect()); - } - QRectF exposed_rect = mapToScene(event->region().boundingRect()).boundingRect(); - drawForeground(&painter, exposed_rect); - } else { - QChartView::paintEvent(event); + // the static layer is invalidated on x-range change and data merge, so cache it in live mode too + const qreal dpr = devicePixelRatioF(); + if (chart_pixmap.isNull() || chart_pixmap.size() != size() * dpr) { + chart_pixmap = QPixmap(size() * dpr); + chart_pixmap.setDevicePixelRatio(dpr); + QPainter p(&chart_pixmap); + p.setRenderHints(QPainter::Antialiasing); + p.setFont(font()); + drawStaticLayer(&p); + } + painter.drawPixmap(QPoint(), chart_pixmap); + + if (can_drop) { + painter.setPen(QPen(palette().color(QPalette::Highlight), 4)); + painter.drawRect(rect()); + } + drawForeground(&painter); +} + +void ChartView::drawStaticLayer(QPainter *painter) { + painter->fillRect(rect(), palette().color(QPalette::Base)); + painter->drawPixmap(move_icon_rect.topLeft(), utils::icon("grip-horizontal")); + drawAxes(painter); + drawLegend(painter); + drawSeries(painter); +} + +void ChartView::drawAxes(QPainter *painter) { + const QColor text_color = palette().color(QPalette::Text); + QColor grid_color = text_color; + grid_color.setAlpha(50); + QFontMetrics fm(font()); + painter->setFont(font()); + + // y grid lines and tick labels + for (int i = 0; i < y_tick_count; ++i) { + double value = y_min + i * (y_max - y_min) / (y_tick_count - 1); + qreal y = yPos(value); + painter->setPen(grid_color); + painter->drawLine(QPointF(plot_area.left(), y), QPointF(plot_area.right(), y)); + painter->setPen(text_color); + QRectF label_rect(0, y - fm.height() / 2.0, plot_area.left() - 6, fm.height()); + painter->drawText(label_rect, Qt::AlignRight | Qt::AlignVCenter, QString::number(value, 'f', y_precision)); + } + + // rotated y axis title (unit) + if (!y_unit.isEmpty()) { + painter->save(); + painter->translate(plot_area.left() - y_label_width + fm.height() / 2.0, plot_area.center().y()); + painter->rotate(-90); + painter->drawText(QRectF(-plot_area.height() / 2.0, -fm.height() / 2.0, plot_area.height(), fm.height()), + Qt::AlignCenter, y_unit); + painter->restore(); + } + + // x grid lines and tick labels + const int x_precision = xAxisPrecision(); + for (int i = 0; i < X_TICK_COUNT; ++i) { + double sec = x_min + i * (x_max - x_min) / (X_TICK_COUNT - 1); + qreal x = xPos(sec); + painter->setPen(grid_color); + painter->drawLine(QPointF(x, plot_area.top()), QPointF(x, plot_area.bottom())); + painter->setPen(text_color); + QString label = QString::number(sec, 'f', x_precision); + QRectF label_rect(x - 100, plot_area.bottom() + AXIS_X_TOP_MARGIN, 200, fm.height()); + painter->drawText(label_rect, Qt::AlignHCenter | Qt::AlignTop, label); } } -void ChartView::drawBackground(QPainter *painter, const QRectF &rect) { - painter->fillRect(rect, palette().color(QPalette::Base)); +void ChartView::drawLegend(QPainter *painter) { + QColor title_color = palette().color(QPalette::WindowText); + // Draw message details in similar color, but slightly fade it to the background + QColor msg_color = title_color; + msg_color.setAlpha(180); + QFont bold_font = font(); + bold_font.setBold(true); + const int marker_size = QFontMetrics(font()).height() - 4; + + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + const auto &s = sigs[i]; + const QRect &r = legend_rects[i]; + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + QRectF marker_rect(r.left(), r.center().y() - marker_size / 2.0, marker_size, marker_size); + series_type == SeriesType::Scatter ? painter->drawEllipse(marker_rect) : painter->drawRect(marker_rect); + + bold_font.setStrikeOut(!s.visible); + QFont normal_font = font(); + normal_font.setStrikeOut(!s.visible); + + qreal x = r.left() + marker_size + 5; + painter->setFont(bold_font); + painter->setPen(title_color); + QString name = QFontMetrics(bold_font).elidedText(QString::fromStdString(s.sig->name), Qt::ElideRight, r.right() - x); + painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, name); + x += QFontMetrics(bold_font).horizontalAdvance(name); + painter->setFont(normal_font); + painter->setPen(msg_color); + QString msg = QFontMetrics(normal_font).elidedText(QString::fromStdString(" " + msgName(s.msg_id) + " " + s.msg_id.toString()), + Qt::ElideRight, r.right() - x); + painter->drawText(QRectF(x, r.top(), r.right() - x, r.height()), Qt::AlignLeft | Qt::AlignVCenter, msg); + } } -void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { +void ChartView::drawSeries(QPainter *painter) { + painter->save(); + painter->setClipRect(plot_area); + for (auto &s : sigs) { + if (!s.visible) continue; + + // visible points in vals to compute point density + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), x_min, xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), x_max, xLessThan); + int num_points = std::max(last - first, 1); + double pixels_per_point = 0; + if (first != last) { + const QPointF &right_pt = last == s.vals.cend() ? s.vals.back() : *last; + pixels_per_point = (xPos(right_pt.x()) - xPos(first->x())) / num_points; + } + + if (series_type == SeriesType::Scatter) { + qreal radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0; + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + for (auto it = first; it != last; ++it) { + painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), radius, radius); + } + } else { + const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; + auto begin = std::lower_bound(points.cbegin(), points.cend(), x_min, xLessThan); + if (begin != points.cbegin()) --begin; + auto end = std::lower_bound(begin, points.cend(), x_max, xLessThan); + if (end != points.cend()) ++end; + if (begin == end) continue; + + std::vector polyline; + polyline.reserve(end - begin); + for (auto it = begin; it != end; ++it) { + polyline.emplace_back(xPos(it->x()), yPos(it->y())); + } + painter->setPen(QPen(s.color, 2)); + painter->setBrush(Qt::NoBrush); + painter->drawPolyline(polyline.data(), polyline.size()); + + // show points when zoomed in enough + if (num_points == 1 || pixels_per_point > 20) { + painter->setPen(Qt::NoPen); + painter->setBrush(s.color); + for (auto it = first; it != last; ++it) { + painter->drawEllipse(QPointF(xPos(it->x()), yPos(it->y())), 4, 4); + } + } + } + } + painter->restore(); +} + +void ChartView::drawForeground(QPainter *painter) { drawTimeline(painter); drawSignalValue(painter); // draw track points painter->setPen(Qt::NoPen); qreal track_line_x = -1; for (auto &s : sigs) { - if (!s.track_pt.isNull() && s.series->isVisible()) { - painter->setBrush(s.series->color().darker(125)); - QPointF pos = chart()->mapToPosition(s.track_pt); + if (!s.track_pt.isNull() && s.visible) { + painter->setBrush(s.color.darker(125)); + QPointF pos(xPos(s.track_pt.x()), yPos(s.track_pt.y())); painter->drawEllipse(pos, 5.5, 5.5); track_line_x = std::max(track_line_x, pos.x()); } } if (track_line_x > 0) { - auto plot_area = chart()->plotArea(); painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{track_line_x, plot_area.top()}, QPointF{track_line_x, plot_area.bottom()}); - } - - // paint points. OpenGL mode lacks certain features (such as showing points) - painter->setPen(Qt::NoPen); - for (auto &s : sigs) { - if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { - auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); - painter->setBrush(s.series->color()); - for (auto it = first; it != last; ++it) { - painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); - } - } + painter->drawLine(QPointF{track_line_x, (qreal)plot_area.top()}, QPointF{track_line_x, (qreal)plot_area.bottom()}); } drawRubberBandTimeRange(painter); } void ChartView::drawRubberBandTimeRange(QPainter *painter) { - auto rubber = findChild(); - if (rubber && rubber->isVisible() && rubber->width() > 1) { - painter->setPen(Qt::white); - auto rubber_rect = rubber->geometry().normalized(); - for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { - QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 2); - auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); - pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); - painter->fillRect(r, Qt::gray); - painter->drawText(r, Qt::AlignCenter, sec); - } + if (rubber_rect.width() <= 1) return; + + // selection rect + QColor highlight = palette().color(QPalette::Highlight); + QColor fill = highlight; + fill.setAlpha(50); + painter->fillRect(rubber_rect, fill); + painter->setPen(highlight); + painter->setBrush(Qt::NoBrush); + painter->drawRect(rubber_rect); + + // time labels at the bottom corners + painter->setPen(Qt::white); + painter->setFont(font()); + for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { + QString sec = QString::number(secondsAtPoint(pt), 'f', 2); + auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); + pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); + painter->fillRect(r, Qt::gray); + painter->drawText(r, Qt::AlignCenter, sec); } } void ChartView::drawTimeline(QPainter *painter) { - const auto plot_area = chart()->plotArea(); // draw vertical time line - qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right()); - painter->setPen(QPen(chart()->titleBrush().color(), 1)); - painter->drawLine(QPointF{x, plot_area.top() - 1}, QPointF{x, plot_area.bottom() + 1}); + qreal x = std::clamp(xPos(cur_sec), (qreal)plot_area.left(), (qreal)plot_area.right()); + painter->setPen(QPen(palette().color(QPalette::Text), 1)); + painter->drawLine(QPointF{x, plot_area.top() - 1.0}, QPointF{x, plot_area.bottom() + 1.0}); // draw current time under the axis-x QString time_str = QString::number(cur_sec, 'f', 2); - QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); + QSize time_str_size = QFontMetrics(font()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); QPainterPath path; path.addRoundedRect(time_str_rect, 3, 3); painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); painter->setPen(palette().color(QPalette::BrightText)); - painter->setFont(axis_x->labelsFont()); + painter->setFont(font()); painter->drawText(time_str_rect, Qt::AlignCenter, time_str); } void ChartView::drawSignalValue(QPainter *painter) { - auto item_group = qgraphicsitem_cast(chart()->legend()->childItems()[0]); - assert(item_group != nullptr); - auto legend_markers = item_group->childItems(); - assert(legend_markers.size() == sigs.size()); - painter->setFont(signal_value_font); - painter->setPen(chart()->legend()->labelColor()); - int i = 0; - for (auto &s : sigs) { + painter->setPen(palette().color(QPalette::Text)); + for (int i = 0; i < sigs.size() && i < legend_rects.size(); ++i) { + const auto &s = sigs[i]; auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, [](auto &p, double x) { return p.x() > x + EPSILON; }); - QString value = (it != s.vals.crend() && it->x() >= axis_x->min()) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--"; - QRectF marker_rect = legend_markers[i++]->sceneBoundingRect(); - QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size()); + QString value = (it != s.vals.crend() && it->x() >= x_min) ? QString::fromStdString(s.sig->formatValue(it->y())) : "--"; + QRectF value_rect(legend_rects[i].bottomLeft() - QPoint(0, 1), legend_rects[i].size()); QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val); } } -QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { - QXYSeries *series = nullptr; - if (type == SeriesType::Line) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); - } else if (type == SeriesType::StepLine) { - series = new QLineSeries(this); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries); - } else { - series = new QScatterSeries(this); - static_cast(series)->setBorderColor(color); - chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); - } - series->setColor(color); - // TODO: Due to a bug in CameraWidget the camera frames - // are drawn instead of the graphs on MacOS. Re-enable OpenGL when fixed -#ifndef __APPLE__ - series->setUseOpenGL(true); - // Qt doesn't properly apply device pixel ratio in OpenGL mode - QPen pen = series->pen(); - pen.setWidthF(2.0 * devicePixelRatioF()); - series->setPen(pen); -#endif - addSeries(series); - return series; -} - -void ChartView::addSeries(QXYSeries *series) { - setSeriesColor(series, series->color()); - chart()->addSeries(series); - series->attachAxis(axis_x); - series->attachAxis(axis_y); - - // disables the delivery of mouse events to the opengl widget. - // this enables the user to select the zoom area when the mouse press on the data point. - auto glwidget = findChild(); - if (glwidget && !glwidget->testAttribute(Qt::WA_TransparentForMouseEvents)) { - glwidget->setAttribute(Qt::WA_TransparentForMouseEvents); - } -} - -void ChartView::setSeriesColor(QXYSeries *series, QColor color) { - auto existing_series = chart()->series(); - for (auto s : existing_series) { - if (s != series && std::abs(color.hueF() - qobject_cast(s)->color().hueF()) < 0.1) { +QColor ChartView::uniqueColor(QColor color, const cabana::Signal *exclude) const { + for (auto &s : sigs) { + if (s.sig != exclude && std::abs(color.hueF() - s.color.hueF()) < 0.1) { // use different color to distinguish it from others. - auto last_color = qobject_cast(existing_series.back())->color(); + auto last_color = sigs.back().color; static thread_local std::mt19937 rng{std::random_device{}()}; std::uniform_int_distribution sat(35, 99); std::uniform_int_distribution val(85, 99); @@ -777,36 +758,13 @@ void ChartView::setSeriesColor(QXYSeries *series, QColor color) { break; } } - series->setColor(color); + return color; } void ChartView::setSeriesType(SeriesType type) { if (type != series_type) { series_type = type; - for (auto &s : sigs) { - chart()->removeSeries(s.series); - s.series->deleteLater(); - } - for (auto &s : sigs) { - s.series = createSeries(series_type, toQColor(s.sig->color)); - const auto &points = series_type == SeriesType::StepLine ? s.step_vals : s.vals; - s.series->replace(QVector(points.cbegin(), points.cend())); - } - updateSeriesPoints(); - updateTitle(); - menu->actions()[(int)type]->setChecked(true); - } -} - -void ChartView::handleMarkerClicked() { - auto marker = qobject_cast(sender()); - Q_ASSERT(marker); - if (sigs.size() > 1) { - auto series = marker->series(); - series->setVisible(!series->isVisible()); - marker->setVisible(true); - updateAxisY(); updateTitle(); } } diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index ae050269bf..b03623475a 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -1,18 +1,11 @@ #pragma once +#include #include #include #include #include -#include -#include -#include -#include -#include -#include -#include -using namespace QtCharts; #include "tools/cabana/chart/tiplabel.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -25,7 +18,7 @@ enum class SeriesType { }; class ChartsWidget; -class ChartView : public QChartView { +class ChartView : public QWidget { Q_OBJECT public: @@ -38,12 +31,15 @@ public: void updatePlotArea(int left, bool force = false); void showTip(double sec); void hideTip(); - double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); } + double secondsAtPoint(const QPointF &pt) const { + return x_min + (pt.x() - plot_area.left()) * (x_max - x_min) / std::max(plot_area.width(), 1); + } struct SigItem { MessageId msg_id; const cabana::Signal *sig = nullptr; - QXYSeries *series = nullptr; + QColor color; + bool visible = true; std::vector vals; std::vector step_vals; QPointF track_pt{}; @@ -58,7 +54,6 @@ signals: private slots: void signalUpdated(const cabana::Signal *sig); void manageSignals(); - void handleMarkerClicked(); void msgUpdated(MessageId id); void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); } void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } @@ -67,52 +62,65 @@ private: void appendCanEvents(const cabana::Signal *sig, const std::vector &events, std::vector &vals, std::vector &step_vals); void createToolButtons(); - void addSeries(QXYSeries *series); void contextMenuEvent(QContextMenuEvent *event) override; void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *ev) override; - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); } - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; void resizeEvent(QResizeEvent *event) override; QSize sizeHint() const override; void updateAxisY(); void updateTitle(); void resetChartCache(); - void setTheme(QChart::ChartTheme theme); void paintEvent(QPaintEvent *event) override; - void drawForeground(QPainter *painter, const QRectF &rect) override; - void drawBackground(QPainter *painter, const QRectF &rect) override; - void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } + void drawStaticLayer(QPainter *painter); + void drawAxes(QPainter *painter); + void drawLegend(QPainter *painter); + void drawSeries(QPainter *painter); + void drawForeground(QPainter *painter); void drawSignalValue(QPainter *painter); void drawTimeline(QPainter *painter); void drawRubberBandTimeRange(QPainter *painter); + int xAxisPrecision() const; std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); - QXYSeries *createSeries(SeriesType type, QColor color); - void setSeriesColor(QXYSeries *, QColor color); - void updateSeriesPoints(); + QColor uniqueColor(QColor color, const cabana::Signal *exclude = nullptr) const; void removeIf(std::function predicate); + void takeSignalsFrom(ChartView *source); + void setDropHighlight(bool highlight) { if (std::exchange(can_drop, highlight) != highlight) update(); } inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } + inline qreal xPos(double sec) const { return plot_area.left() + (sec - x_min) / (x_max - x_min) * plot_area.width(); } + inline qreal yPos(double val) const { return plot_area.bottom() - (val - y_min) / (y_max - y_min) * plot_area.height(); } + // layout + QRect plot_area; + QRect move_icon_rect; + std::vector legend_rects; + // axes + double x_min; + double x_max; + double y_min = 0; + double y_max = 1; + int y_tick_count = 3; + int y_precision = 0; + QString y_unit; int y_label_width = 0; int align_to = 0; - QValueAxis *axis_x; - QValueAxis *axis_y; + // interaction + enum class MouseMode { None, Rubber, Scrub }; + MouseMode mouse_mode = MouseMode::None; + QPoint press_pos; + QRect rubber_rect; + bool resume_after_scrub = false; + QMenu *menu; QAction *split_chart_act; QAction *close_act; - QGraphicsPixmapItem *move_icon; - QGraphicsProxyWidget *close_btn_proxy; - QGraphicsProxyWidget *manage_btn_proxy; + ToolButton *manage_btn; + ToolButton *close_btn; TipLabel *tip_label; std::vector sigs; double cur_sec = 0; SeriesType series_type = SeriesType::Line; - bool is_scrubbing = false; - bool resume_after_scrub = false; QPixmap chart_pixmap; bool can_drop = false; double tooltip_x = -1; diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 93d8570996..3144c5132d 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include @@ -72,11 +72,14 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { range_slider_action = toolbar->addWidget(range_slider); // zoom controls - zoom_undo_stack = new QUndoStack(this); - toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this)); - undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); - toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this)); - redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + undo_zoom_action = toolbar->addAction(utils::icon("arrow-counterclockwise"), tr("Undo Zoom"), [this]() { zoom_undo_stack.undo(); }); + redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); }); + undo_zoom_action->setEnabled(false); + redo_zoom_action->setEnabled(false); + zoom_undo_stack.setCallbacks({.index_changed = [this]() { + undo_zoom_action->setEnabled(zoom_undo_stack.canUndo()); + redo_zoom_action->setEnabled(zoom_undo_stack.canRedo()); + }}); reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -89,8 +92,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { tabbar->setAutoHide(true); tabbar->setExpanding(false); tabbar->setDrawBase(true); - tabbar->setAcceptDrops(true); - tabbar->setChangeCurrentOnDrag(true); tabbar->setUsesScrollButtons(true); main_layout->addWidget(tabbar); @@ -105,6 +106,11 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); main_layout->addWidget(charts_scroll); + // chart drag preview + drag_preview = new QLabel(this); + drag_preview->setAttribute(Qt::WA_TransparentForMouseEvents); + drag_preview->hide(); + // init settings current_theme = settings.theme; column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); @@ -186,7 +192,7 @@ void ChartsWidget::timeRangeChanged(const std::optionalsetTimeRange(std::nullopt); - zoom_undo_stack->clear(); + zoom_undo_stack.clear(); } QRect ChartsWidget::chartVisibleRect(ChartView *chart) { @@ -256,10 +262,6 @@ void ChartsWidget::settingChanged() { if (std::exchange(current_theme, settings.theme) != current_theme) { undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; - for (auto c : charts) { - c->setTheme(theme); - } } if (range_slider->maximum() != settings.max_cached_minutes * 60) { range_slider->setRange(1, settings.max_cached_minutes * 60); @@ -307,12 +309,8 @@ void ChartsWidget::splitChart(ChartView *src_chart) { int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1; for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) { auto c = createChart(pos); - src_chart->chart()->removeSeries(it->series); - // Restore to the original color - it->series->setColor(toQColor(it->sig->color)); - - c->addSeries(it->series); + it->color = toQColor(it->sig->color); c->sigs.emplace_back(std::move(*it)); c->updateAxisY(); c->updateTitle(); @@ -320,6 +318,7 @@ void ChartsWidget::splitChart(ChartView *src_chart) { } src_chart->updateAxisY(); src_chart->updateTitle(); + updateState(); QTimer::singleShot(0, src_chart, &ChartView::resetChartCache); } } @@ -390,7 +389,87 @@ void ChartsWidget::updateLayout(bool force) { } } -void ChartsWidget::startAutoScroll() { +void ChartsWidget::startChartDrag(ChartView *chart, const QPoint &global_pos) { + stopAutoScroll(); + drag = {.source = chart, .press_pos = global_pos}; + QPixmap px = chart->grab().scaledToWidth(CHART_MIN_WIDTH * chart->devicePixelRatio(), Qt::SmoothTransformation); + drag_preview->setPixmap(px); + drag_preview->resize(px.size() / px.devicePixelRatio()); +} + +void ChartsWidget::dragChartMove(const QPoint &global_pos) { + if (!drag.active) { + if ((global_pos - drag.press_pos).manhattanLength() < QApplication::startDragDistance()) return; + drag.active = true; + drag_preview->show(); + drag_preview->raise(); + } + drag_preview->move(mapFromGlobal(global_pos) + QPoint(5, 5)); + + // hovering a tab switches to it so the chart can be dropped into another tab + int tab = tabbar->tabAt(tabbar->mapFromGlobal(global_pos)); + if (tab >= 0 && tab != tabbar->currentIndex()) { + tabbar->setCurrentIndex(tab); + } + + const QPoint container_pos = charts_container->mapFromGlobal(global_pos); + ChartView *target = nullptr; + for (auto c : currentCharts()) { + if (c != drag.source && c->isVisible() && c->geometry().contains(container_pos)) { + target = c; + break; + } + } + if (std::exchange(drop_target, target) != target) { + for (auto c : charts) c->setDropHighlight(c == target); + } + bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); + bool on_background = !target && in_viewport && !charts_container->childAt(container_pos); + charts_container->drawDropIndicator(on_background ? container_pos : QPoint()); + + if (in_viewport) { + startAutoScroll(global_pos); + } +} + +void ChartsWidget::cancelChartDrag() { + drag = {}; + stopAutoScroll(); + drag_preview->hide(); + charts_container->drawDropIndicator({}); + if (auto target = std::exchange(drop_target, nullptr)) target->setDropHighlight(false); +} + +void ChartsWidget::dragChartRelease(const QPoint &global_pos) { + ChartView *source = drag.source; + bool active = drag.active; + ChartView *target = drop_target; + cancelChartDrag(); + if (!active) return; + + const QPoint container_pos = charts_container->mapFromGlobal(global_pos); + bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos)); + if (target) { + // merge source into target + target->takeSignalsFrom(source); + } else if (in_viewport && !charts_container->childAt(container_pos)) { + // reorder within the current tab + auto w = charts_container->getDropAfter(container_pos); + if (w != source) { + for (auto &[_, list] : tab_charts) { + list.erase(std::remove(list.begin(), list.end(), source), list.end()); + } + auto &cur = currentCharts(); + int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0; + cur.insert(cur.begin() + to, source); + updateLayout(true); + updateTabBar(); + } + } +} + +void ChartsWidget::startAutoScroll(const QPoint &global_pos) { + auto_scroll_pos = global_pos; auto_scroll_timer->start(50); } @@ -406,7 +485,7 @@ void ChartsWidget::doAutoScroll() { } int value = scroll->value(); - QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos()); + QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos); QRect area = charts_scroll->viewport()->rect(); if (pos.y() - area.top() < settings.chart_height / 2) { @@ -414,16 +493,11 @@ void ChartsWidget::doAutoScroll() { } else if (area.bottom() - pos.y() < settings.chart_height / 2) { scroll->setValue(value + auto_scroll_count); } - bool vertical_unchanged = value == scroll->value(); - if (vertical_unchanged) { + if (value == scroll->value()) { stopAutoScroll(); - } else { - // mouseMoveEvent to updates the drag-selection rectangle - const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos); - const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos); - QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, - Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt); - QApplication::sendEvent(charts_scroll->viewport(), &mm); + } else if (chartDragActive()) { + // refresh the drop indicator/target at the new scroll position + dragChartMove(auto_scroll_pos); } } @@ -440,11 +514,14 @@ void ChartsWidget::newChart() { for (auto it : items) { c->addSignal(it->msg_id, it->sig); } + updateState(); } } } void ChartsWidget::removeChart(ChartView *chart) { + if (drag.source == chart) cancelChartDrag(); + if (drop_target == chart) drop_target = nullptr; charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end()); chart->deleteLater(); for (auto &[_, list] : tab_charts) { @@ -484,6 +561,19 @@ void ChartsWidget::alignCharts() { } bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { + // route all mouse events to the chart drag, even when the source chart is hidden by a tab switch + if (chartDragActive()) { + if (e->type() == QEvent::MouseMove) { + dragChartMove(static_cast(e)->globalPos()); + return true; + } else if (e->type() == QEvent::MouseButtonRelease && static_cast(e)->button() == Qt::LeftButton) { + dragChartRelease(static_cast(e)->globalPos()); + return false; // let the release through so Qt clears the implicit mouse grab + } else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) { + return true; // swallow other buttons during the drag + } + } + if (!value_tip_visible_) return false; if (e->type() == QEvent::MouseMove) { @@ -492,7 +582,7 @@ bool ChartsWidget::eventFilter(QObject *o, QEvent *e) { for (const auto &c : charts) { auto local_pos = c->mapFromGlobal(global_pos); - if (c->chart()->plotArea().contains(local_pos)) { + if (c->plot_area.contains(local_pos)) { if (on_tip) { showValueTip(c->secondsAtPoint(local_pos)); } @@ -524,13 +614,14 @@ bool ChartsWidget::event(QEvent *event) { break; case QEvent::WindowDeactivate: case QEvent::FocusOut: + if (chartDragActive()) cancelChartDrag(); showValueTip(-1); default: break; } if (back_button) { - zoom_undo_stack->undo(); + zoom_undo_stack.undo(); return true; // Return true since the event has been handled } return QFrame::event(event); @@ -539,7 +630,6 @@ bool ChartsWidget::event(QEvent *event) { // ChartsContainer ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { - setAcceptDrops(true); setBackgroundRole(QPalette::Window); QVBoxLayout *charts_main_layout = new QVBoxLayout(this); charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING); @@ -549,32 +639,6 @@ ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), charts_main_layout->addStretch(0); } -void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - event->acceptProposedAction(); - drawDropIndicator(event->pos()); - } -} - -void ChartsContainer::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { - auto w = getDropAfter(event->pos()); - auto chart = qobject_cast(event->source()); - if (w != chart) { - for (auto &[_, list] : charts_widget->tab_charts) { - list.erase(std::remove(list.begin(), list.end(), chart), list.end()); - } - auto &cur = charts_widget->currentCharts(); - int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0; - cur.insert(cur.begin() + to, chart); - charts_widget->updateLayout(true); - charts_widget->updateTabBar(); - event->acceptProposedAction(); - } - drawDropIndicator({}); - } -} - void ChartsContainer::paintEvent(QPaintEvent *ev) { if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { QRect r = geometry(); diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index ef3fbc471a..8b3003dcd6 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -8,15 +8,13 @@ #include #include #include -#include -#include #include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/commands.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" const int CHART_MIN_WIDTH = 300; -const QString CHART_MIME_TYPE = "application/x-cabanachartview"; class ChartView; class ChartsWidget; @@ -24,9 +22,6 @@ class ChartsWidget; class ChartsContainer : public QWidget { public: ChartsContainer(ChartsWidget *parent); - void dragEnterEvent(QDragEnterEvent *event) override; - void dropEvent(QDropEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); } void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } void paintEvent(QPaintEvent *ev) override; ChartView *getDropAfter(const QPoint &pos) const; @@ -69,7 +64,12 @@ private: void eventsMerged(const MessageEventsMap &new_events); void updateState(); void zoomReset(); - void startAutoScroll(); + void startChartDrag(ChartView *chart, const QPoint &global_pos); + void dragChartMove(const QPoint &global_pos); + void dragChartRelease(const QPoint &global_pos); + void cancelChartDrag(); + bool chartDragActive() const { return drag.source != nullptr; } + void startAutoScroll(const QPoint &global_pos); void stopAutoScroll(); void doAutoScroll(); void updateToolBar(); @@ -97,7 +97,7 @@ private: QAction *redo_zoom_action; QAction *reset_zoom_action; ToolButton *reset_zoom_btn; - QUndoStack *zoom_undo_stack; + UndoStack zoom_undo_stack; ToolButton *remove_all_btn; std::vector charts; @@ -110,7 +110,15 @@ private: QAction *columns_action; int column_count = 1; int current_column_count = 0; + struct ChartDrag { + ChartView *source = nullptr; + QPoint press_pos; // global + bool active = false; + } drag; + QLabel *drag_preview; + ChartView *drop_target = nullptr; int auto_scroll_count = 0; + QPoint auto_scroll_pos; QTimer *auto_scroll_timer; QTimer *align_timer; int current_theme = 0; @@ -119,11 +127,10 @@ private: friend class ChartsContainer; }; -class ZoomCommand : public QUndoCommand { +class ZoomCommand : public UndoCommand { public: - ZoomCommand(std::pair range) : range(range), QUndoCommand() { + ZoomCommand(std::pair range) : range(range) { prev_range = can->timeRange(); - setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2)); } void undo() override { can->setTimeRange(prev_range); } void redo() override { can->setTimeRange(range); } diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index f158528b51..b47bf90b0c 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -1,20 +1,81 @@ -#include - #include "tools/cabana/commands.h" +#include + +// UndoStack + +void UndoStack::push(UndoCommand *cmd) { + commands_.resize(index_); // drop any redoable commands + if (clean_index_ > index_) clean_index_ = -1; + commands_.emplace_back(cmd); + cmd->redo(); + setIndex(index_ + 1); +} + +void UndoStack::undo() { + if (!canUndo()) return; + commands_[index_ - 1]->undo(); + setIndex(index_ - 1); +} + +void UndoStack::redo() { + if (!canRedo()) return; + commands_[index_]->redo(); + setIndex(index_ + 1); +} + +void UndoStack::clear() { + bool was_clean = isClean(); + commands_.clear(); + index_ = clean_index_ = 0; + if (callbacks_.index_changed) callbacks_.index_changed(); + if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true); +} + +void UndoStack::setClean() { + if (!isClean()) { + clean_index_ = index_; + if (callbacks_.clean_changed) callbacks_.clean_changed(true); + } +} + +void UndoStack::setIndex(int index) { + bool was_clean = isClean(); + index_ = index; + if (callbacks_.index_changed) callbacks_.index_changed(); + if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean()); +} + +UndoStack *UndoStack::instance() { + static UndoStack undo_stack; + return &undo_stack; +} + +QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) { + UndoStack::instance()->setCallbacks({ + .index_changed = [this]() { emit indexChanged(); }, + .clean_changed = [this](bool clean) { emit cleanChanged(clean); }, + }); +} + +QtUndoNotifier *undoNotifier() { + static QtUndoNotifier notifier; + return ¬ifier; +} + // EditMsgCommand EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size, - const std::string &node, const std::string &comment, QUndoCommand *parent) - : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) { + const std::string &node, const std::string &comment) + : id(id), new_name(name), new_size(size), new_node(node), new_comment(comment) { if (auto msg = dbc()->msg(id)) { old_name = msg->name; old_size = msg->size; old_node = msg->transmitter; old_comment = msg->comment; - setText(QObject::tr("edit message %1:%2").arg(QString::fromStdString(name)).arg(id.address)); + text = "edit message " + name + ":" + std::to_string(id.address); } else { - setText(QObject::tr("new message %1:%2").arg(QString::fromStdString(name)).arg(id.address)); + text = "new message " + name + ":" + std::to_string(id.address); } } @@ -31,10 +92,10 @@ void EditMsgCommand::redo() { // RemoveMsgCommand -RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) { +RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) { if (auto msg = dbc()->msg(id)) { message = *msg; - setText(QObject::tr("remove message %1:%2").arg(QString::fromStdString(message.name)).arg(id.address)); + text = "remove message " + message.name + ":" + std::to_string(id.address); } } @@ -53,9 +114,9 @@ void RemoveMsgCommand::redo() { // AddSigCommand -AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent) - : id(id), signal(sig), QUndoCommand(parent) { - setText(QObject::tr("add signal %1 to %2:%3").arg(QString::fromStdString(sig.name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); +AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig) + : id(id), signal(sig) { + text = "add signal " + sig.name + " to " + msgName(id) + ":" + std::to_string(id.address); } void AddSigCommand::undo() { @@ -75,8 +136,7 @@ void AddSigCommand::redo() { // RemoveSigCommand -RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent) - : id(id), QUndoCommand(parent) { +RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) { sigs.push_back(*sig); if (sig->type == cabana::Signal::Type::Multiplexor) { for (const auto &s : dbc()->msg(id)->sigs) { @@ -85,7 +145,7 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si } } } - setText(QObject::tr("remove signal %1 from %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); + text = "remove signal " + sig->name + " from " + msgName(id) + ":" + std::to_string(id.address); } void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); } @@ -93,8 +153,8 @@ void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(i // EditSignalCommand -EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent) - : id(id), QUndoCommand(parent) { +EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig) + : id(id) { sigs.push_back({*sig, new_sig}); if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) { // convert all multiplexed signals to normal signals @@ -108,17 +168,8 @@ EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal * } } } - setText(QObject::tr("edit signal %1 in %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address)); + text = "edit signal " + sig->name + " in " + msgName(id) + ":" + std::to_string(id.address); } void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); } void EditSignalCommand::redo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.first.name, s.second); } - -namespace UndoStack { - -QUndoStack *instance() { - static QUndoStack *undo_stack = new QUndoStack(qApp); - return undo_stack; -} - -} // namespace UndoStack diff --git a/openpilot/tools/cabana/commands.h b/openpilot/tools/cabana/commands.h index 4081f86985..200a4f2f5b 100644 --- a/openpilot/tools/cabana/commands.h +++ b/openpilot/tools/cabana/commands.h @@ -1,19 +1,70 @@ #pragma once +#include +#include #include #include #include -#include -#include +#include #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" -class EditMsgCommand : public QUndoCommand { +class UndoCommand { +public: + virtual ~UndoCommand() = default; + virtual void undo() = 0; + virtual void redo() = 0; + std::string text; +}; + +class UndoStack { +public: + struct Callbacks { + std::function index_changed; + std::function clean_changed; + }; + + void push(UndoCommand *cmd); // takes ownership and calls redo() + void undo(); + void redo(); + void clear(); + void setClean(); + bool isClean() const { return clean_index_ == index_; } + bool canUndo() const { return index_ > 0; } + bool canRedo() const { return index_ < (int)commands_.size(); } + std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; } + std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; } + void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } + static UndoStack *instance(); + +private: + void setIndex(int index); + std::vector> commands_; + int index_ = 0; + int clean_index_ = 0; + Callbacks callbacks_; +}; + +// emits Qt signals for the global undo stack +class QtUndoNotifier : public QObject { + Q_OBJECT + +public: + explicit QtUndoNotifier(QObject *parent = nullptr); + +signals: + void indexChanged(); + void cleanChanged(bool clean); +}; + +QtUndoNotifier *undoNotifier(); + +class EditMsgCommand : public UndoCommand { public: EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node, - const std::string &comment, QUndoCommand *parent = nullptr); + const std::string &comment); void undo() override; void redo() override; @@ -23,9 +74,9 @@ private: int old_size = 0, new_size = 0; }; -class RemoveMsgCommand : public QUndoCommand { +class RemoveMsgCommand : public UndoCommand { public: - RemoveMsgCommand(const MessageId &id, QUndoCommand *parent = nullptr); + RemoveMsgCommand(const MessageId &id); void undo() override; void redo() override; @@ -34,9 +85,9 @@ private: cabana::Msg message; }; -class AddSigCommand : public QUndoCommand { +class AddSigCommand : public UndoCommand { public: - AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent = nullptr); + AddSigCommand(const MessageId &id, const cabana::Signal &sig); void undo() override; void redo() override; @@ -46,9 +97,9 @@ private: cabana::Signal signal = {}; }; -class RemoveSigCommand : public QUndoCommand { +class RemoveSigCommand : public UndoCommand { public: - RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent = nullptr); + RemoveSigCommand(const MessageId &id, const cabana::Signal *sig); void undo() override; void redo() override; @@ -57,9 +108,9 @@ private: std::vector sigs; }; -class EditSignalCommand : public QUndoCommand { +class EditSignalCommand : public UndoCommand { public: - EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent = nullptr); + EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig); void undo() override; void redo() override; @@ -67,8 +118,3 @@ private: const MessageId id; std::vector> sigs; // {old_sig, new_sig} }; - -namespace UndoStack { - QUndoStack *instance(); - inline void push(QUndoCommand *cmd) { instance()->push(cmd); } -}; diff --git a/openpilot/tools/cabana/deqt.md b/openpilot/tools/cabana/deqt.md index 6d2aed8264..3bef2d7ab0 100644 --- a/openpilot/tools/cabana/deqt.md +++ b/openpilot/tools/cabana/deqt.md @@ -20,13 +20,8 @@ some rules - `QObject`, `QMetaObject`, `QMetaType` - `QApplication`, `QCoreApplication`, `QGuiApplication` - `QString`, `QStringList`, `QStringBuilder`, `QChar`, `QLatin1Char` -- `QByteArray` - `QVariant` -- `QVector`, `QMap`, `QSet`, `QPointer` -- `QSettings` -- `QFile`, `QFileInfo`, `QDir`, `QIODevice`, `QStandardPaths` -- `QThread` -- `QTimer`, `QBasicTimer`, `QTimerEvent` +- `QTimer` - `QWidget`, `QMainWindow`, `QWindow` - `QDialog`, `QDialogButtonBox`, `QMessageBox`, `QProgressDialog` - `QFileDialog` @@ -36,7 +31,7 @@ some rules - `QComboBox`, `QLineEdit`, `QTextEdit`, `QSpinBox`, `QSlider` - `QLabel`, `QGroupBox`, `QFrame` - `QTabBar`, `QTabWidget`, `QSplitter`, `QScrollArea`, `QScrollBar` -- `QDockWidget`, `QStatusBar`, `QProgressBar`, `QRubberBand` +- `QDockWidget`, `QStatusBar`, `QProgressBar` - `QFormLayout`, `QGridLayout`, `QHBoxLayout`, `QVBoxLayout` - `QSizePolicy` - `QAbstractItemModel`, `QAbstractTableModel`, `QModelIndex` @@ -44,27 +39,15 @@ some rules - `QTableWidget`, `QTableWidgetItem`, `QListWidget`, `QListWidgetItem` - `QItemSelection`, `QItemSelectionModel`, `QItemSelectionRange` - `QHeaderView`, `QStyledItemDelegate`, `QStyleOptionViewItem` -- `QValidator`, `QIntValidator`, `QDoubleValidator` +- `QValidator`, `QIntValidator` - `QColor`, `QRgb`, `QPalette` - `QBrush`, `QPen` - `QPainter`, `QPainterPath`, `QStylePainter` -- `QPixmap`, `QPixmapCache`, `QIcon`, `QStaticText` +- `QImage`, `QPixmap`, `QPixmapCache`, `QStaticText` - `QFont`, `QFontDatabase`, `QFontMetrics`, `QTextDocument` - `QStyle`, `QStyleOption`, `QStyleOptionFrame`, `QStyleOptionSlider` - `QPoint`, `QPointF`, `QRect`, `QRectF`, `QRegion` - `QSize`, `QSizeF` -- `QCursor`, `QClipboard`, `QScreen`, `QDesktopWidget` - `QEvent`, `QPaintEvent`, `QResizeEvent`, `QShowEvent`, `QCloseEvent` - `QMouseEvent`, `QWheelEvent`, `QNativeGestureEvent`, `QContextMenuEvent` -- `QDrag`, `QMimeData` -- `QDragEnterEvent`, `QDragLeaveEvent`, `QDragMoveEvent`, `QDropEvent` - `QKeySequence`, `QShortcut`, `QToolTip` -- `QChart`, `QChartView`, `QAbstractAxis`, `QValueAxis` -- `QXYSeries`, `QLineSeries`, `QScatterSeries` -- `QLegend`, `QLegendMarker` -- `QGraphicsScene`, `QGraphicsView`, `QGraphicsItemGroup`, `QGraphicsLayout` -- `QGraphicsPixmapItem`, `QGraphicsProxyWidget` -- `QOpenGLWidget`, `QOpenGLFunctions` -- `QOpenGLShader`, `QOpenGLShaderProgram` -- `QMatrix4x4`, `QSurfaceFormat` -- `QUndoCommand`, `QUndoStack`, `QUndoView` diff --git a/openpilot/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc index 36a95ff96e..6b62f54959 100644 --- a/openpilot/tools/cabana/detailwidget.cc +++ b/openpilot/tools/cabana/detailwidget.cc @@ -58,7 +58,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState); QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { if (index != -1) { @@ -211,13 +211,13 @@ void DetailWidget::editMsg() { int size = msg ? msg->size : can->lastMessage(msg_id).dat.size(); EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this); if (dlg.exec()) { - UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(), + UndoStack::instance()->push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(), dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString())); } } void DetailWidget::removeMsg() { - UndoStack::push(new RemoveMsgCommand(msg_id)); + UndoStack::instance()->push(new RemoveMsgCommand(msg_id)); } // EditMessageDialog diff --git a/openpilot/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc index 51c2481f3b..3fd569c648 100644 --- a/openpilot/tools/cabana/historylog.cc +++ b/openpilot/tools/cabana/historylog.cc @@ -209,7 +209,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset); QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &HistoryLogModel::reset); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset); QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); } diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 6372ea9295..953f573a0b 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -2,23 +2,21 @@ #include "tools/cabana/dbc/dbcqt.h" #include +#include +#include #include +#include #include +#include -#include -#include -#include #include -#include #include #include #include #include #include #include -#include #include -#include #include "json11/json11.hpp" #include "tools/cabana/commands.h" @@ -37,14 +35,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW createShortcuts(); // save default window state to allow resetting it - default_state = saveState(); + default_state = utils::toBytes(saveState()); - // restore states - restoreGeometry(settings.geometry); - if (isMaximized()) { - setGeometry(QApplication::desktop()->availableGeometry(this)); - } - restoreState(settings.window_state); + // restore states; restoreGeometry() itself corrects stale off-screen geometry + restoreGeometry(utils::qbytes(settings.geometry)); + restoreState(utils::qbytes(settings.window_state)); // install handlers static auto static_main_win = this; @@ -65,7 +60,7 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged); - QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); + QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); @@ -73,10 +68,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW } void MainWindow::loadFingerprints() { - QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json"); - if (!json_file.open(QIODevice::ReadOnly)) return; + std::ifstream json_file((QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json").toStdString()); + if (!json_file) return; + const std::string contents{std::istreambuf_iterator(json_file), std::istreambuf_iterator()}; std::string err; - auto doc = json11::Json::parse(json_file.readAll().toStdString(), err); + auto doc = json11::Json::parse(contents, err); if (!err.empty() || !doc.is_object()) return; fingerprint_to_dbc.clear(); for (const auto &kv : doc.object_items()) { @@ -108,8 +104,17 @@ void MainWindow::createActions() { file_menu->addSeparator(); QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc")); // load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }"); - for (const auto &dbc_name : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) { - load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); }); + std::vector dbc_names; + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) { + if (entry.is_regular_file() && entry.path().extension() == ".dbc") { + dbc_names.push_back(entry.path().filename().string()); + } + } + std::sort(dbc_names.begin(), dbc_names.end()); + for (const auto &dbc_name : dbc_names) { + QString name = QString::fromStdString(dbc_name); + load_opendbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); }); } file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); }); @@ -127,18 +132,12 @@ void MainWindow::createActions() { // Edit Menu QMenu *edit_menu = menuBar()->addMenu(tr("&Edit")); - auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo")); + undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); }); undo_act->setShortcuts(QKeySequence::Undo); - edit_menu->addAction(undo_act); - auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Redo")); + redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); }); redo_act->setShortcuts(QKeySequence::Redo); - edit_menu->addAction(redo_act); - edit_menu->addSeparator(); - - QMenu *commands_menu = edit_menu->addMenu(tr("Command &List")); - QWidgetAction *commands_act = new QWidgetAction(this); - commands_act->setDefaultWidget(new QUndoView(UndoStack::instance())); - commands_menu->addAction(commands_act); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions); + updateUndoRedoActions(); // View Menu QMenu *view_menu = menuBar()->addMenu(tr("&View")); @@ -148,7 +147,7 @@ void MainWindow::createActions() { view_menu->addAction(messages_dock->toggleViewAction()); view_menu->addAction(video_dock->toggleViewAction()); view_menu->addSeparator(); - view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(default_state); }); + view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); }); // Tools Menu tools_menu = menuBar()->addMenu(tr("&Tools")); @@ -195,7 +194,7 @@ void MainWindow::createDockWidgets() { video_splitter->addWidget(charts_container); video_splitter->setStretchFactor(1, 1); - video_splitter->restoreState(settings.video_splitter_state); + video_splitter->restoreState(utils::qbytes(settings.video_splitter_state)); video_splitter->handle(1)->setEnabled(!can->liveStreaming()); video_dock->setWidget(video_splitter); QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking); @@ -226,6 +225,14 @@ void MainWindow::undoStackCleanChanged(bool clean) { setWindowModified(!clean); } +void MainWindow::updateUndoRedoActions() { + auto stack = UndoStack::instance(); + undo_act->setEnabled(stack->canUndo()); + undo_act->setText(stack->canUndo() ? tr("&Undo %1").arg(QString::fromStdString(stack->undoText())) : tr("&Undo")); + redo_act->setEnabled(stack->canRedo()); + redo_act->setText(stack->canRedo() ? tr("&Redo %1").arg(QString::fromStdString(stack->redoText())) : tr("&Redo")); +} + void MainWindow::DBCFileChanged() { UndoStack::instance()->clear(); @@ -306,11 +313,20 @@ void MainWindow::loadDBCFromOpendbc(const QString &name) { } void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { + std::string text; + if (!utils::getClipboardText(&text)) { + QMessageBox::warning(this, tr("Load From Clipboard"), tr("No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland).")); + return; + } + if (text.empty()) { + QMessageBox::warning(this, tr("Load From Clipboard"), tr("Clipboard is empty.")); + return; + } + closeFile(s); - QString dbc_str = QGuiApplication::clipboard()->text(); std::string error; - bool ret = dbc()->open(s, std::string(""), dbc_str.toStdString(), &error); + bool ret = dbc()->open(s, std::string(""), text, &error); if (ret && dbc()->nonEmptyDBCCount() > 0) { QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!")); } else { @@ -436,7 +452,7 @@ void MainWindow::saveFile(DBCFile *dbc_file) { void MainWindow::saveFileAs(DBCFile *dbc_file) { QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file)))); - QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(QString::fromStdString(settings.last_dir) + "/untitled.dbc"), tr("DBC (*.dbc)")); + QString fn = QFileDialog::getSaveFileName(this, title, QString::fromStdString((std::filesystem::path(settings.last_dir) / "untitled.dbc").string()), tr("DBC (*.dbc)")); if (!fn.isEmpty()) { dbc_file->saveAs(fn.toStdString()); UndoStack::instance()->setClean(); @@ -455,8 +471,11 @@ void MainWindow::saveToClipboard() { void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { assert(dbc_file != nullptr); - QGuiApplication::clipboard()->setText(QString::fromStdString(dbc_file->generateDBC())); - QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); + if (utils::setClipboardText(dbc_file->generateDBC())) { + QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!")); + } else { + QMessageBox::warning(this, tr("Copy To Clipboard"), tr("Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland).")); + } } void MainWindow::updateLoadSaveMenus() { @@ -496,7 +515,7 @@ void MainWindow::updateRecentFiles(const QString &fn) { while (settings.recent_files.size() > MAX_RECENT_FILES) { settings.recent_files.pop_back(); } - settings.last_dir = QFileInfo(fn).absolutePath().toStdString(); + settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); } void MainWindow::updateRecentFileMenu() { @@ -509,7 +528,7 @@ void MainWindow::updateRecentFileMenu() { } for (int i = 0; i < num_recent_files; ++i) { - QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(QString::fromStdString(settings.recent_files[i])).fileName()); + QString text = tr("&%1 %2").arg(i + 1).arg(QString::fromStdString(std::filesystem::path(settings.recent_files[i]).filename().string())); open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); }); } } @@ -576,16 +595,17 @@ void MainWindow::closeEvent(QCloseEvent *event) { floating_window->deleteLater(); // save states - settings.geometry = saveGeometry(); - settings.window_state = saveState(); + settings.geometry = utils::toBytes(saveGeometry()); + settings.window_state = utils::toBytes(saveState()); if (can && !can->liveStreaming()) { - settings.video_splitter_state = video_splitter->saveState(); + settings.video_splitter_state = utils::toBytes(video_splitter->saveState()); } if (messages_widget) { settings.message_header_state = messages_widget->saveHeaderState(); } saveSessionState(); + settings.save(); QWidget::closeEvent(event); } diff --git a/openpilot/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h index 3526a58202..279ea3b969 100644 --- a/openpilot/tools/cabana/mainwin.h +++ b/openpilot/tools/cabana/mainwin.h @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include #include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/dbc/dbcmanager.h" @@ -68,6 +70,7 @@ protected: void findSimilarBits(); void findSignal(); void undoStackCleanChanged(bool clean); + void updateUndoRedoActions(); void onlineHelp(); void toggleFullScreen(); void updateStatus(); @@ -97,8 +100,10 @@ protected: QAction *save_dbc = nullptr; QAction *save_dbc_as = nullptr; QAction *copy_dbc_to_clipboard = nullptr; + QAction *undo_act = nullptr; + QAction *redo_act = nullptr; QString car_fingerprint; - QByteArray default_state; + std::vector default_state; }; class HelpOverlay : public QWidget { diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index 22de1350ec..b07e3ea0bf 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -45,7 +45,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified); + QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -212,7 +212,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { return {}; } -void MessageListModel::setFilterStrings(const QMap &filters) { +void MessageListModel::setFilterStrings(const std::map &filters) { filters_ = filters; filterAndSort(); } @@ -265,14 +265,14 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) { } bool MessageListModel::match(const MessageListModel::Item &item) { - if (filters_.isEmpty()) + if (filters_.empty()) return true; bool match = true; const auto &data = can->lastMessage(item.id); for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) { - const QString &txt = it.value(); - switch (it.key()) { + const QString &txt = it->second; + switch (it->first) { case Column::NAME: { match = item.name.contains(txt, Qt::CaseInsensitive); if (!match) { @@ -388,10 +388,13 @@ void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, painter->setPen(oldPen); } -void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { +void MessageView::setModel(QAbstractItemModel *model) { + QTreeView::setModel(model); // Bypass the slow call to QTreeView::dataChanged. // QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView. - QAbstractItemView::dataChanged(topLeft, bottomRight, roles); + QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr); + QObject::connect(model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); } void MessageView::updateBytesSectionSize() { @@ -422,9 +425,9 @@ MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizont } void MessageViewHeader::updateFilters() { - QMap filters; - for (int i = 0; i < count(); i++) { - if (editors[i] && !editors[i]->text().isEmpty()) { + std::map filters; + for (int i = 0; i < (int)editors.size(); i++) { + if (!editors[i]->text().isEmpty()) { filters[i] = editors[i]->text(); } } @@ -433,27 +436,24 @@ void MessageViewHeader::updateFilters() { void MessageViewHeader::updateHeaderPositions() { QSize sz = QHeaderView::sizeHint(); - for (int i = 0; i < count(); i++) { - if (editors[i]) { - int h = editors[i]->sizeHint().height(); - editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); - editors[i]->setHidden(isSectionHidden(i)); - } + for (int i = 0; i < (int)editors.size(); i++) { + int h = editors[i]->sizeHint().height(); + editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h); + editors[i]->setHidden(isSectionHidden(i)); } } void MessageViewHeader::updateGeometries() { - for (int i = 0; i < count(); i++) { - if (!editors[i]) { - QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); - editors[i] = new QLineEdit(this); - editors[i]->setClearButtonEnabled(true); - editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name)); + for (int i = (int)editors.size(); i < count(); i++) { + QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); + auto edit = new QLineEdit(this); + edit->setClearButtonEnabled(true); + edit->setPlaceholderText(tr("Filter %1").arg(column_name)); - QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); - } + QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); + editors.push_back(edit); } - setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0); + setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0); QHeaderView::updateGeometries(); updateHeaderPositions(); @@ -461,5 +461,5 @@ void MessageViewHeader::updateGeometries() { QSize MessageViewHeader::sizeHint() const { QSize sz = QHeaderView::sizeHint(); - return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; + return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; } diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 9ffb156604..0a9cd256d8 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include #include @@ -35,7 +37,7 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void setFilterStrings(const QMap &filters); + void setFilterStrings(const std::map &filters); void showInactiveMessages(bool show); void msgsReceived(const std::set *new_msgs, bool has_new_ids); bool filterAndSort(); @@ -56,7 +58,7 @@ private: void sortItems(std::vector &items); bool match(const MessageListModel::Item &id); - QMap filters_; + std::map filters_; std::set dbc_messages_; int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; @@ -68,11 +70,11 @@ class MessageView : public QTreeView { public: MessageView(QWidget *parent) : QTreeView(parent) {} void updateBytesSectionSize(); + void setModel(QAbstractItemModel *model) override; protected: void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {} - void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override; void wheelEvent(QWheelEvent *event) override; }; @@ -86,7 +88,7 @@ public: QSize sizeHint() const override; void updateFilters(); - QMap editors; + std::vector editors; }; class MessagesWidget : public QWidget { @@ -95,8 +97,13 @@ class MessagesWidget : public QWidget { public: MessagesWidget(QWidget *parent); void selectMessage(const MessageId &message_id); - QByteArray saveHeaderState() const { return view->header()->saveState(); } - bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } + std::vector saveHeaderState() const { + const auto state = view->header()->saveState(); + return {state.begin(), state.end()}; + } + bool restoreHeaderState(const std::vector &state) const { + return view->header()->restoreState({(const char *)state.data(), (int)state.size()}); + } void suppressHighlighted(); signals: diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index e385d83eca..0c8136b84c 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -1,15 +1,37 @@ #include "tools/cabana/settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + #include #include -#include #include #include #include -#include -#include #include +#include "json11/json11.hpp" #include "tools/cabana/utils/util.h" const int MIN_CACHE_MINIUTES = 30; @@ -17,32 +39,442 @@ const int MAX_CACHE_MINIUTES = 120; Settings settings; +namespace { + +std::filesystem::path settingsFile() { + return utils::configPath() / "cabana.json"; +} + +struct LoadedSettings { + json11::Json::object values; + bool exists = false; + bool valid = true; +}; + +class FileLock { +public: + explicit FileLock(const std::filesystem::path &path) { + fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600); + if (fd < 0 || flock(fd, LOCK_EX) < 0) { + fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + if (fd >= 0) close(fd); + fd = -1; + } + } + ~FileLock() { + if (fd >= 0) close(fd); + } + bool isLocked() const { return fd >= 0; } + +private: + int fd = -1; +}; + +LoadedSettings loadSettings() { + std::ifstream input(settingsFile()); + if (!input) return {}; + + const std::string contents{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + std::string error; + auto settings_json = json11::Json::parse(contents, error); + if (!error.empty() || !settings_json.is_object()) { + fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str()); + return {.exists = true, .valid = false}; + } + return {.values = settings_json.object_items(), .exists = true}; +} + +bool ensureSettingsDirectory() { + const auto path = settingsFile(); + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + if (error) { + fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str()); + return false; + } + return true; +} + +bool writeAll(int fd, const std::string &data) { + size_t written = 0; + while (written < data.size()) { + ssize_t result = write(fd, data.data() + written, data.size() - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) return false; + written += result; + } + return true; +} + +bool saveSettings(const json11::Json::object &settings_json) { + const auto path = settingsFile(); + const std::string contents = json11::Json(settings_json).dump(); + std::string temporary_path = path.string() + ".tmp.XXXXXX"; + int fd = mkstemp(temporary_path.data()); + if (fd < 0) { + fprintf(stderr, "failed to create temporary Cabana settings %s: %s\n", temporary_path.c_str(), strerror(errno)); + return false; + } + + bool success = writeAll(fd, contents) && fsync(fd) == 0; + if (close(fd) < 0) success = false; + if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false; + + if (success) { + int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC); + success = dir_fd >= 0 && fsync(dir_fd) == 0; + if (dir_fd >= 0 && close(dir_fd) < 0) success = false; + } + + if (!success) { + const int saved_errno = errno; + unlink(temporary_path.c_str()); + fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno)); + } + return success; +} + +bool preserveCorruptSettings() { + const auto path = settingsFile(); + auto backup = path; + backup += ".corrupt"; + for (int i = 1; std::filesystem::exists(backup); ++i) { + backup = path; + backup += ".corrupt." + std::to_string(i); + } + if (rename(path.c_str(), backup.c_str()) < 0) { + fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + return false; + } + fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str()); + return true; +} + +// TODO: Remove the legacy QSettings migration after users have had time to migrate to cabana.json. +struct LegacyValue { + std::vector strings; + std::string bytes; + bool is_byte_array = false; +}; + +using LegacySettings = std::map; + +int hexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +#ifndef __APPLE__ + +void appendUtf8(std::string &result, uint32_t codepoint) { + if (codepoint <= 0x7f) { + result.push_back(codepoint); + } else if (codepoint <= 0x7ff) { + result.push_back(0xc0 | (codepoint >> 6)); + result.push_back(0x80 | (codepoint & 0x3f)); + } else if (codepoint <= 0xffff) { + result.push_back(0xe0 | (codepoint >> 12)); + result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); + result.push_back(0x80 | (codepoint & 0x3f)); + } else { + result.push_back(0xf0 | (codepoint >> 18)); + result.push_back(0x80 | ((codepoint >> 12) & 0x3f)); + result.push_back(0x80 | ((codepoint >> 6) & 0x3f)); + result.push_back(0x80 | (codepoint & 0x3f)); + } +} + +LegacyValue decodeIniValue(std::string_view encoded) { + std::vector> decoded(1); + std::vector quoted(1, false); + bool in_quotes = false; + + for (size_t i = 0; i < encoded.size();) { + char c = encoded[i++]; + if (c == '"') { + in_quotes = !in_quotes; + quoted.back() = true; + } else if (c == ',' && !in_quotes) { + decoded.emplace_back(); + quoted.push_back(false); + while (i < encoded.size() && (encoded[i] == ' ' || encoded[i] == '\t')) ++i; + } else if (c == '\\' && i < encoded.size()) { + c = encoded[i++]; + static const std::map escapes = { + {'a', '\a'}, {'b', '\b'}, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'}, + {'v', '\v'}, {'"', '"'}, {'?', '?'}, {'\'', '\''}, {'\\', '\\'}, + }; + if (auto it = escapes.find(c); it != escapes.end()) { + decoded.back().push_back(static_cast(it->second)); + } else if (c == 'x' && i < encoded.size() && hexDigit(encoded[i]) >= 0) { + uint32_t value = 0; + while (i < encoded.size() && hexDigit(encoded[i]) >= 0) value = (value << 4) + hexDigit(encoded[i++]); + decoded.back().push_back(value & 0xffff); + } else if (c >= '0' && c <= '7') { + uint32_t value = c - '0'; + while (i < encoded.size() && encoded[i] >= '0' && encoded[i] <= '7') value = (value << 3) + (encoded[i++] - '0'); + decoded.back().push_back(value & 0xffff); + } + } else { + decoded.back().push_back(static_cast(c)); + } + } + + LegacyValue result; + for (size_t i = 0; i < decoded.size(); ++i) { + auto &value = decoded[i]; + if (!quoted[i]) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin()); + while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back(); + } + + std::string string_value; + for (size_t j = 0; j < value.size(); ++j) { + uint32_t codepoint = value[j]; + if (codepoint >= 0xd800 && codepoint <= 0xdbff && j + 1 < value.size() && value[j + 1] >= 0xdc00 && value[j + 1] <= 0xdfff) { + codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (value[++j] - 0xdc00); + } + appendUtf8(string_value, codepoint); + } + result.strings.push_back(std::move(string_value)); + } + + if (result.strings.size() == 1 && result.strings[0] == "@Invalid()") { + result.strings.clear(); + } else if (decoded.size() == 1) { + static constexpr std::string_view prefix = "@ByteArray("; + const auto &value = decoded[0]; + if (value.size() >= prefix.size() + 1 && std::equal(prefix.begin(), prefix.end(), value.begin()) && value.back() == ')') { + result.is_byte_array = true; + result.bytes.reserve(value.size() - prefix.size() - 1); + for (size_t i = prefix.size(); i + 1 < value.size(); ++i) result.bytes.push_back(value[i] & 0xff); + } + } + if (!result.is_byte_array) { + for (auto &value : result.strings) { + if (value.compare(0, 2, "@@") == 0) value.erase(0, 1); + } + } + return result; +} + +LegacySettings loadLegacySettings() { + auto path = settingsFile(); + path.replace_filename("cabana.conf"); + std::ifstream input(path); + if (!input) return {}; + + LegacySettings settings; + bool in_general_section = false; + std::string line; + while (std::getline(input, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line == "[General]") { + in_general_section = true; + continue; + } + if (!line.empty() && line.front() == '[') { + in_general_section = false; + continue; + } + if (!in_general_section || line.empty() || line.front() == ';') continue; + if (auto separator = line.find('='); separator != std::string::npos) { + settings[line.substr(0, separator)] = decodeIniValue(std::string_view(line).substr(separator + 1)); + } + } + return settings; +} + +#else + +std::string cfStringToUtf8(CFStringRef value) { + CFIndex length = CFStringGetLength(value); + CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; + std::string result(size, '\0'); + if (!CFStringGetCString(value, result.data(), size, kCFStringEncodingUTF8)) return {}; + result.resize(strlen(result.c_str())); + return result; +} + +LegacyValue cfStringValue(CFStringRef string) { + LegacyValue value; + if (CFStringHasPrefix(string, CFSTR("@ByteArray(")) && CFStringHasSuffix(string, CFSTR(")"))) { + CFRange range{11, CFStringGetLength(string) - 12}; + std::vector data(range.length); + CFStringGetCharacters(string, range, data.data()); + value.is_byte_array = true; + value.bytes.reserve(data.size()); + for (UniChar c : data) value.bytes.push_back(c & 0xff); + } else { + std::string string_value = cfStringToUtf8(string); + if (string_value.compare(0, 2, "@@") == 0) string_value.erase(0, 1); + value.strings.push_back(std::move(string_value)); + } + return value; +} + +LegacySettings loadLegacySettings() { + LegacySettings settings; + CFDictionaryRef values = CFPreferencesCopyMultiple(nullptr, CFSTR("com.cabana"), + kCFPreferencesCurrentUser, kCFPreferencesAnyHost); + if (values == nullptr) return settings; + + CFIndex count = CFDictionaryGetCount(values); + std::vector keys(count); + std::vector objects(count); + CFDictionaryGetKeysAndValues(values, keys.data(), objects.data()); + for (CFIndex i = 0; i < count; ++i) { + if (CFGetTypeID(keys[i]) != CFStringGetTypeID()) continue; + std::string key = cfStringToUtf8(static_cast(keys[i])); + CFTypeRef object = objects[i]; + LegacyValue value; + if (CFGetTypeID(object) == CFBooleanGetTypeID()) { + value.strings.push_back(CFBooleanGetValue(static_cast(object)) ? "true" : "false"); + } else if (CFGetTypeID(object) == CFNumberGetTypeID()) { + int number = 0; + if (CFNumberGetValue(static_cast(object), kCFNumberIntType, &number)) value.strings.push_back(std::to_string(number)); + } else if (CFGetTypeID(object) == CFStringGetTypeID()) { + value = cfStringValue(static_cast(object)); + } else if (CFGetTypeID(object) == CFDataGetTypeID()) { + auto data = static_cast(object); + value.is_byte_array = true; + value.bytes.assign(reinterpret_cast(CFDataGetBytePtr(data)), CFDataGetLength(data)); + } else if (CFGetTypeID(object) == CFArrayGetTypeID()) { + auto array = static_cast(object); + for (CFIndex j = 0; j < CFArrayGetCount(array); ++j) { + CFTypeRef item = CFArrayGetValueAtIndex(array, j); + if (CFGetTypeID(item) != CFStringGetTypeID()) { + value.strings.clear(); + break; + } + auto item_value = cfStringValue(static_cast(item)); + if (item_value.strings.size() != 1) { + value.strings.clear(); + break; + } + value.strings.push_back(std::move(item_value.strings[0])); + } + } + if (!value.strings.empty() || value.is_byte_array || CFGetTypeID(object) == CFArrayGetTypeID()) { + settings.emplace(std::move(key), std::move(value)); + } + } + CFRelease(values); + return settings; +} + +#endif + template -void readSetting(QSettings &settings_store, const char *key, T &value) { - if (auto stored = settings_store.value(key); stored.canConvert()) value = stored.value(); +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, T &value) { + auto it = legacy_settings.find(key); + if (it == legacy_settings.end() || it->second.strings.size() != 1) return; + const auto &stored = it->second.strings[0]; + + if constexpr (std::is_same_v) { + if (stored == "true") value = true; + if (stored == "false") value = false; + } else if constexpr (std::is_integral_v || std::is_enum_v) { + int number = 0; + auto [end, error] = std::from_chars(stored.data(), stored.data() + stored.size(), number); + if (error == std::errc{} && end == stored.data() + stored.size()) value = static_cast(number); + } } -void readSetting(QSettings &settings_store, const char *key, std::string &value) { - value = settings_store.value(key, QString::fromStdString(value)).toString().toStdString(); +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::string &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && it->second.strings.size() == 1) value = it->second.strings[0]; } -void readSetting(QSettings &settings_store, const char *key, std::vector &value) { - value.clear(); - for (const auto &item : settings_store.value(key).toStringList()) value.push_back(item.toStdString()); +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && !it->second.is_byte_array) value = it->second.strings; +} + +void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector &value) { + auto it = legacy_settings.find(key); + if (it != legacy_settings.end() && it->second.is_byte_array) { + value.assign(it->second.bytes.begin(), it->second.bytes.end()); + } } template -void writeSetting(QSettings &settings_store, const char *key, const T &value) { settings_store.setValue(key, value); } -void writeSetting(QSettings &settings_store, const char *key, const std::string &value) { settings_store.setValue(key, QString::fromStdString(value)); } -void writeSetting(QSettings &settings_store, const char *key, const std::vector &value) { - QStringList items; - for (const auto &item : value) items.push_back(QString::fromStdString(item)); - settings_store.setValue(key, items); +void readSetting(const json11::Json::object &settings_json, const char *key, T &value) { + auto it = settings_json.find(key); + if (it == settings_json.end()) return; + + if constexpr (std::is_same_v) { + if (it->second.is_bool()) value = it->second.bool_value(); + } else if constexpr (std::is_integral_v) { + if (it->second.is_number()) value = it->second.int_value(); + } else if constexpr (std::is_enum_v) { + if (it->second.is_number()) value = static_cast(it->second.int_value()); + } } -template -void settingsOp(SettingOperation op) { - QSettings s("cabana"); +void readSetting(const json11::Json::object &settings_json, const char *key, std::string &value) { + auto it = settings_json.find(key); + if (it != settings_json.end() && it->second.is_string()) value = it->second.string_value(); +} + +void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { + auto it = settings_json.find(key); + if (it == settings_json.end() || !it->second.is_array()) return; + + std::vector stored; + for (const auto &item : it->second.array_items()) { + if (!item.is_string()) return; + stored.push_back(item.string_value()); + } + value = std::move(stored); +} + +void readSetting(const json11::Json::object &settings_json, const char *key, std::vector &value) { + auto it = settings_json.find(key); + if (it == settings_json.end() || !it->second.is_string()) return; + + const auto &hex = it->second.string_value(); + if (hex.size() % 2 == 0 && std::all_of(hex.begin(), hex.end(), [](unsigned char c) { return std::isxdigit(c); })) { + value.clear(); + value.reserve(hex.size() / 2); + for (size_t i = 0; i < hex.size(); i += 2) { + value.push_back((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 1])); + } + } +} + +template +void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) { + if constexpr (std::is_same_v) { + settings_json[key] = value; + } else if constexpr (std::is_integral_v || std::is_enum_v) { + settings_json[key] = static_cast(value); + } +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::string &value) { + settings_json[key] = value; +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { + settings_json[key] = value; +} + +void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector &value) { + static const char digits[] = "0123456789abcdef"; + std::string hex; + hex.reserve(value.size() * 2); + for (uint8_t b : value) { + hex.push_back(digits[b >> 4]); + hex.push_back(digits[b & 0xf]); + } + settings_json[key] = hex; +} + +template +void settingsOp(Store &s, SettingOperation op) { op(s, "absolute_time", settings.absolute_time); op(s, "fps", settings.fps); op(s, "max_cached_minutes", settings.max_cached_minutes); @@ -70,14 +502,38 @@ void settingsOp(SettingOperation op) { op(s, "active_charts", settings.active_charts); } +} // namespace + Settings::Settings() { - last_dir = last_route_dir = QDir::homePath().toStdString(); - log_path = (QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/").toStdString(); - settingsOp([](QSettings &s, const char *key, auto &value) { readSetting(s, key, value); }); + last_dir = last_route_dir = utils::homePath(); + log_path = utils::homePath() + "/cabana_live_stream/"; + const auto stored_settings = loadSettings(); + if (stored_settings.valid) { + if (stored_settings.exists) { + settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); }); + } else { + auto legacy_settings = loadLegacySettings(); + settingsOp(legacy_settings, [](const auto &s, const char *key, auto &value) { readLegacySetting(s, key, value); }); + } + } + fps = std::clamp(fps, 1, 100); } -Settings::~Settings() { - settingsOp([](QSettings &s, const char *key, const auto &value) { writeSetting(s, key, value); }); +// Must be called before main() returns: json11's internal statistics are constructed on first +// use at runtime, so they are destroyed before this pre-main global. Saving from ~Settings +// would use them after destruction and corrupt the heap. +void Settings::save() { + if (!ensureSettingsDirectory()) return; + + auto lock_path = settingsFile(); + lock_path += ".lock"; + FileLock lock(lock_path); + if (!lock.isLocked()) return; + + auto stored_settings = loadSettings(); + if (!stored_settings.valid && !preserveCorruptSettings()) return; + settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); }); + saveSettings(stored_settings.values); } // SettingsDlg @@ -121,6 +577,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { log_livestream = new QGroupBox(tr("Enable live stream logging"), this); log_livestream->setCheckable(true); + log_livestream->setChecked(settings.log_livestream); QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); log_path->setReadOnly(true); @@ -135,7 +592,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { QObject::connect(browse_btn, &QPushButton::clicked, [this]() { QString fn = QFileDialog::getExistingDirectory( this, tr("Log File Location"), - QStandardPaths::writableLocation(QStandardPaths::HomeLocation), + QString::fromStdString(utils::homePath()), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (!fn.isEmpty()) { log_path->setText(fn); diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 4254a8009d..7357ecf4fc 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -1,6 +1,8 @@ #pragma once -#include +#include +#include + #include #include #include @@ -14,13 +16,13 @@ class Settings : public QObject, public CabanaSettingsState { public: Settings(); - ~Settings(); + void save(); // Qt frontend layout state. This intentionally stays outside CabanaSettingsState. - QByteArray geometry; - QByteArray video_splitter_state; - QByteArray window_state; - QByteArray message_header_state; + std::vector geometry; + std::vector video_splitter_state; + std::vector window_state; + std::vector message_header_state; signals: void changed(); diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 87294b41f7..ebb5374140 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -198,7 +198,7 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) if (s.is_little_endian != origin_s->is_little_endian) { s.start_bit = flipBitPos(s.start_bit); } - UndoStack::push(new EditSignalCommand(msg_id, origin_s, s)); + UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s)); return true; } @@ -515,7 +515,7 @@ void SignalView::rowsChanged() { tree->setIndexWidget(index, w); auto sig = model->getItem(index)->sig; - QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::push(new RemoveSigCommand(model->msg_id, sig)); }); + QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, sig)); }); QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) { emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier); }); diff --git a/openpilot/tools/cabana/signalview.h b/openpilot/tools/cabana/signalview.h index 42db830df7..2e44e55cbf 100644 --- a/openpilot/tools/cabana/signalview.h +++ b/openpilot/tools/cabana/signalview.h @@ -129,9 +129,12 @@ private: // update widget geometries in QTreeView::rowsInserted QTreeView::rowsInserted(parent, start, end); } - void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles = QVector()) override { + void setModel(QAbstractItemModel *model) override { + QTreeView::setModel(model); // Bypass the slow call to QTreeView::dataChanged. - QAbstractItemView::dataChanged(topLeft, bottomRight, roles); + QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr); + QObject::connect(model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); }); } void leaveEvent(QEvent *event) override { emit static_cast(parentWidget())->highlight(nullptr); diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 26f05d9846..82cb899937 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -75,12 +75,12 @@ protected: const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); void waitForSeekFinshed(); + virtual void updateLastMessages(); std::vector all_events_; double current_sec_ = 0; std::optional> time_range_; private: - void updateLastMessages(); void updateLastMsgsTo(double sec); void updateMasks(); diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 1ce0dbb71f..91e890316e 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -1,22 +1,23 @@ #include "tools/cabana/streams/devicestream.h" #include +#include #include #include #include +#include #include #include +#include #include #include #include "openpilot/cereal/services.h" #include -#include #include #include #include -#include #include "tools/cabana/utils/util.h" @@ -26,6 +27,7 @@ DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(addre } DeviceStream::~DeviceStream() { + stop(); stopBridge(); } @@ -50,9 +52,8 @@ void DeviceStream::stopBridge() { void DeviceStream::start() { if (!zmq_address.isEmpty()) { stopBridge(); - QString bridge_path = QFileInfo(QCoreApplication::applicationDirPath() + - "/../../openpilot/cereal/messaging/bridge").absoluteFilePath(); - const std::string path = bridge_path.toStdString(); + const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) / + "../../openpilot/cereal/messaging/bridge").lexically_normal().string(); const std::string addr = zmq_address.toStdString(); const char *can_filter = "/\"can/\""; @@ -108,10 +109,10 @@ void DeviceStream::streamThread() { std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); assert(sock != NULL); // run as fast as messages come in - while (!QThread::currentThread()->isInterruptionRequested()) { + while (!exit_) { std::unique_ptr msg(sock->receive(true)); if (!msg) { - QThread::msleep(50); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); continue; } handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 5aa4628bc0..019a67e8f2 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -1,7 +1,7 @@ #include "tools/cabana/streams/livestream.h" -#include #include +#include #include #include #include @@ -42,37 +42,34 @@ LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { if (settings.log_livestream) { logger = std::make_unique(); } - stream_thread = new QThread(this); - - QObject::connect(&settings, &Settings::changed, this, &LiveStream::startUpdateTimer); - QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); }); - QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater); } LiveStream::~LiveStream() { stop(); } -void LiveStream::startUpdateTimer() { - update_timer.stop(); - update_timer.start(1000.0 / settings.fps, this); - timer_id = update_timer.timerId(); -} - void LiveStream::start() { - stream_thread->start(); - startUpdateTimer(); begin_date_time = std::chrono::system_clock::now(); + fps_ = settings.fps; + exit_ = false; + stream_thread = std::thread(&LiveStream::streamThread, this); + update_thread = std::thread(&LiveStream::updateThread, this); } void LiveStream::stop() { - if (!stream_thread) return; + exit_ = true; + if (stream_thread.joinable()) stream_thread.join(); + if (update_thread.joinable()) update_thread.join(); +} - update_timer.stop(); - stream_thread->requestInterruption(); - stream_thread->quit(); - stream_thread->wait(); - stream_thread = nullptr; +void LiveStream::updateThread() { + while (!exit_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_)); + // coalesce: skip the emit if the main thread hasn't processed the previous one yet. + if (!update_pending_.exchange(true)) { + emit privateUpdateLastMsgsSignal(); + } + } } // called in streamThread @@ -92,23 +89,22 @@ void LiveStream::handleEvent(kj::ArrayPtr data) { } } -void LiveStream::timerEvent(QTimerEvent *event) { - if (event->timerId() == timer_id) { - { - // merge events received from live stream thread. - std::lock_guard lk(lock); - mergeEvents(received_events_); - uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0; - lastest_event_ts = std::max(lastest_event_ts, last_received_ts); - received_events_.clear(); - } - if (!all_events_.empty()) { - begin_event_ts = all_events_.front()->mono_time; - updateEvents(); - return; - } +// called on the main thread by the queued privateUpdateLastMsgsSignal connection +void LiveStream::updateLastMessages() { + update_pending_ = false; + fps_ = settings.fps; + { + // merge events received from live stream thread. + std::lock_guard lk(lock); + mergeEvents(received_events_); + uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0; + lastest_event_ts = std::max(lastest_event_ts, last_received_ts); + received_events_.clear(); + } + if (!all_events_.empty()) { + begin_event_ts = all_events_.front()->mono_time; + updateEvents(); } - QObject::timerEvent(event); } void LiveStream::updateEvents() { @@ -138,7 +134,7 @@ void LiveStream::updateEvents() { updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size); current_event_ts = e->mono_time; } - emit privateUpdateLastMsgsSignal(); + AbstractStream::updateLastMessages(); } void LiveStream::seekTo(double sec) { diff --git a/openpilot/tools/cabana/streams/livestream.h b/openpilot/tools/cabana/streams/livestream.h index e88fbadd8e..5d65b1743f 100644 --- a/openpilot/tools/cabana/streams/livestream.h +++ b/openpilot/tools/cabana/streams/livestream.h @@ -1,11 +1,11 @@ #pragma once #include +#include #include +#include #include -#include - #include "tools/cabana/streams/abstractstream.h" class LiveStream : public AbstractStream { @@ -29,18 +29,19 @@ protected: virtual void streamThread() = 0; void handleEvent(kj::ArrayPtr event); + std::atomic exit_ = false; + private: - void startUpdateTimer(); - void timerEvent(QTimerEvent *event) override; + void updateThread(); + void updateLastMessages() override; void updateEvents(); std::mutex lock; - QThread *stream_thread; + std::thread stream_thread, update_thread; + std::atomic update_pending_ = false; + std::atomic fps_ = 10; std::vector received_events_; - int timer_id; - QBasicTimer update_timer; - std::chrono::system_clock::time_point begin_date_time; uint64_t begin_event_ts = 0; uint64_t lastest_event_ts = 0; diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index aa1e01c5a3..7ccb18a756 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -1,12 +1,13 @@ #include "tools/cabana/streams/pandastream.h" +#include #include +#include #include #include #include #include -#include #include PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { @@ -45,13 +46,13 @@ bool PandaStream::connect() { void PandaStream::streamThread() { std::vector raw_can_data; - while (!QThread::currentThread()->isInterruptionRequested()) { - QThread::msleep(1); + while (!exit_) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); if (!panda->connected()) { fprintf(stderr, "Connection to panda lost. Attempting reconnect.\n"); if (!connect()){ - QThread::msleep(1000); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); continue; } } diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index c74aa49821..c502ea061a 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,5 +1,7 @@ #include "tools/cabana/streams/replaystream.h" +#include + #include #include #include @@ -139,7 +141,7 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); if (!dir.isEmpty()) { route_edit->setText(dir); - settings.last_route_dir = QFileInfo(dir).absolutePath().toStdString(); + settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); } }); QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { diff --git a/openpilot/tools/cabana/streams/routes.cc b/openpilot/tools/cabana/streams/routes.cc index 9c19219ce2..b6f98da533 100644 --- a/openpilot/tools/cabana/streams/routes.cc +++ b/openpilot/tools/cabana/streams/routes.cc @@ -12,7 +12,6 @@ #include #include #include -#include #include "json11/json11.hpp" #include "tools/replay/py_downloader.h" @@ -112,11 +111,10 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); // Fetch devices - QPointer self = this; - std::thread([self]() { + std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result)]() { - if (self) self->parseDeviceList(r, response.first, response.second); + QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + if (!alive.expired()) parseDeviceList(r, response.first, response.second); }, Qt::QueuedConnection); }).detach(); } @@ -156,12 +154,10 @@ void RoutesDialog::fetchRoutes() { } int request_id = ++fetch_id_; - QPointer self = this; - std::thread([self, did, start_ms, end_ms, preserved, request_id]() { + std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - if (!self || self->fetch_id_ != request_id) return; - QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { - if (self && self->fetch_id_ == request_id) self->parseRouteList(r, response.first, response.second); + QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); }, Qt::QueuedConnection); }).detach(); } diff --git a/openpilot/tools/cabana/streams/routes.h b/openpilot/tools/cabana/streams/routes.h index 99fa67ef8c..6ed145603f 100644 --- a/openpilot/tools/cabana/streams/routes.h +++ b/openpilot/tools/cabana/streams/routes.h @@ -1,6 +1,8 @@ #pragma once #include +#include + #include #include @@ -21,4 +23,6 @@ protected: QComboBox *period_selector_; RouteListWidget *route_list_; std::atomic fetch_id_{0}; + // expires on destruction; guards main-thread callbacks from detached worker threads + std::shared_ptr alive_ = std::make_shared(true); }; diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index cedbddf99e..b616e7f242 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -8,13 +8,13 @@ #include #include +#include +#include -#include #include #include #include #include -#include SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { if (!available()) { @@ -82,7 +82,7 @@ bool SocketCanStream::connect() { void SocketCanStream::streamThread() { struct canfd_frame frame; - while (!QThread::currentThread()->isInterruptionRequested()) { + while (!exit_) { ssize_t nbytes = read(sock_fd, &frame, sizeof(frame)); if (nbytes <= 0) continue; @@ -128,14 +128,12 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi void OpenSocketCanWidget::refreshDevices() { device_edit->clear(); // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) - QDir net_dir("/sys/class/net"); - for (const auto &iface : net_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { - QFile type_file(net_dir.filePath(iface) + "/type"); - if (type_file.open(QIODevice::ReadOnly)) { - int type = type_file.readAll().trimmed().toInt(); - if (type == 280) { - device_edit->addItem(iface); - } + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { + std::ifstream type_file(entry.path() / "type"); + int type = 0; + if (type_file >> type && type == 280) { + device_edit->addItem(QString::fromStdString(entry.path().filename().string())); } } } diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 75fdf384b2..7e8adc568d 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -1,5 +1,7 @@ #include "tools/cabana/streamselector.h" +#include + #include #include #include @@ -55,7 +57,7 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)"); if (!fn.isEmpty()) { dbc_file->setText(fn); - settings.last_dir = QFileInfo(fn).absolutePath().toStdString(); + settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string(); } }); } diff --git a/openpilot/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc index d538d676b1..4511af7c01 100644 --- a/openpilot/tools/cabana/tools/findsignal.cc +++ b/openpilot/tools/cabana/tools/findsignal.cc @@ -1,5 +1,6 @@ #include "tools/cabana/tools/findsignal.h" +#include #include #include @@ -210,13 +211,13 @@ void FindSignalDlg::search() { } void FindSignalDlg::setInitialSignals() { - QSet buses; + std::set buses; for (auto bus : bus_edit->text().trimmed().split(",")) { bus = bus.trimmed(); if (!bus.isEmpty()) buses.insert(bus.toUShort()); } - QSet addresses; + std::set addresses; for (auto addr : address_edit->text().trimmed().split(",")) { addr = addr.trimmed(); if (!addr.isEmpty()) addresses.insert(addr.toULong(nullptr, 16)); @@ -239,7 +240,7 @@ void FindSignalDlg::setInitialSignals() { model->initial_signals.clear(); for (const auto &[id, m] : can->lastMessages()) { - if ((buses.isEmpty() || buses.contains(id.source)) && (addresses.isEmpty() || addresses.contains(id.address))) { + if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) { const auto &events = can->events(id); auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent()); if (e != events.cend()) { @@ -276,7 +277,7 @@ void FindSignalDlg::customMenuRequested(const QPoint &pos) { menu.addAction(tr("Create Signal")); if (menu.exec(view->mapToGlobal(pos))) { auto &s = model->filtered_signals[index.row()]; - UndoStack::push(new AddSigCommand(s.id, s.sig)); + UndoStack::instance()->push(new AddSigCommand(s.id, s.sig)); emit openMessage(s.id); } } diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 5c55bcf015..87a0f89427 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -4,21 +4,21 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include -#include #include #include -#include -#include #include #include #include "common/util.h" @@ -271,13 +271,13 @@ QValidator::State DoubleValidator::validate(QString &input, int &pos) const { if (input.isEmpty()) return QValidator::Intermediate; // Match QString::toDouble(): C locale, no hex floats / inf / nan. - const QByteArray bytes = input.toLatin1(); + const std::string bytes = input.toLatin1().toStdString(); // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. - if (bytes.contains('x') || bytes.contains('X') || bytes.contains('p') || bytes.contains('P')) { + if (bytes.find_first_of("xXpP") != std::string::npos) { return QValidator::Invalid; } - const char *start = bytes.constData(); + const char *start = bytes.c_str(); char *end = nullptr; const double value = std::strtod(start, &end); if (end == start) { @@ -304,6 +304,58 @@ QValidator::State DoubleValidator::validate(QString &input, int &pos) const { namespace utils { +std::string homePath() { + const char *home = ::getenv("HOME"); + return home ? home : ""; +} + +std::filesystem::path configPath() { +#ifdef __APPLE__ + return std::filesystem::path(homePath()) / "Library/Preferences"; +#else + const char *xdg = ::getenv("XDG_CONFIG_HOME"); + return (xdg && xdg[0]) ? std::filesystem::path(xdg) : std::filesystem::path(homePath()) / ".config"; +#endif +} + +#ifdef __APPLE__ +static const char *clipboard_read_cmds[] = {"pbpaste"}; +static const char *clipboard_write_cmds[] = {"pbcopy"}; +#else +static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"}; +static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"}; +#endif + +bool getClipboardText(std::string *text) { + text->clear(); + bool has_tool = false; + for (const char *cmd : clipboard_read_cmds) { + FILE *f = ::popen(cmd, "r"); + if (!f) continue; + std::string out; + char buf[4096]; + for (size_t n; (n = ::fread(buf, 1, sizeof(buf), f)) > 0;) out.append(buf, n); + int status = ::pclose(f); + if (status == 0) { + *text = std::move(out); + return true; + } + has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found + } + return has_tool; // tool present but clipboard empty +} + +bool setClipboardText(const std::string &text) { + std::signal(SIGPIPE, SIG_IGN); + for (const char *cmd : clipboard_write_cmds) { + FILE *f = ::popen(cmd, "w"); + if (!f) continue; + size_t written = ::fwrite(text.data(), 1, text.size(), f); + if (::pclose(f) == 0 && written == text.size()) return true; + } + return false; +} + bool isDarkTheme() { QColor windowColor = QApplication::palette().color(QPalette::Window); return windowColor.lightness() < 128; @@ -413,20 +465,6 @@ QString signalToolTip(const cabana::Signal *sig) { .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); } -void setSurfaceFormat() { - QSurfaceFormat fmt; -#ifdef __APPLE__ - fmt.setVersion(3, 2); - fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - fmt.setRenderableType(QSurfaceFormat::OpenGL); -#else - fmt.setRenderableType(QSurfaceFormat::OpenGLES); -#endif - fmt.setSamples(16); - fmt.setStencilBufferSize(1); - QSurfaceFormat::setDefaultFormat(fmt); -} - void sigTermHandler(int s) { std::signal(s, SIG_DFL); qApp->quit(); @@ -437,57 +475,57 @@ void initApp(int argc, char *argv[], bool disable_hidpi) { std::signal(SIGINT, sigTermHandler); std::signal(SIGTERM, sigTermHandler); - QString app_dir; + std::filesystem::path app_dir; #ifdef __APPLE__ // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering QApplication tmp(argc, argv); - app_dir = QCoreApplication::applicationDirPath(); + app_dir = QCoreApplication::applicationDirPath().toStdString(); if (disable_hidpi) { qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); } #else - app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path(); + app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif - qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150)); + qputenv("QT_DBL_CLICK_DIST", "150"); // ensure the current dir matches the exectuable's directory - QDir::setCurrent(app_dir); - - setSurfaceFormat(); + std::error_code ec; + std::filesystem::current_path(app_dir, ec); } +// embedded at build time from the bootstrap_icons package (see SConscript) +extern const unsigned char bootstrap_icons_svg[]; +extern const size_t bootstrap_icons_svg_len; + static std::unordered_map load_bootstrap_icons() { std::unordered_map icons; - QFile f(":/bootstrap-icons.svg"); - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - std::string content = f.readAll().toStdString(); - const std::string sym_open = "(bootstrap_icons_svg), bootstrap_icons_svg_len); + const std::string sym_open = " with - svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) - icons[id] = std::move(svg_str); - } + // extract id + size_t id_start = content.find(id_attr, pos); + if (id_start != std::string::npos && id_start < end) { + id_start += id_attr.size(); + size_t id_end = content.find('"', id_start); + if (id_end != std::string::npos && id_end < end) { + std::string id = content.substr(id_start, id_end - id_start); + std::string svg_str = content.substr(pos, end - pos); + // replace with + svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) + icons[id] = std::move(svg_str); } - pos = end; } + pos = end; } return icons; } diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index e0318f5c16..5a3c62d118 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -3,12 +3,13 @@ #include #include #include +#include +#include #include #include #include #include -#include #include #include #include @@ -132,6 +133,10 @@ public: namespace utils { QPixmap icon(const QString &id); +std::string homePath(); +std::filesystem::path configPath(); +bool getClipboardText(std::string *text); // false if no clipboard tool is available +bool setClipboardText(const std::string &text); bool isDarkTheme(); void setTheme(int theme); QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); @@ -140,7 +145,22 @@ inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); } inline QString toHex(const std::vector &dat, char separator = '\0') { - return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper(); + static const char digits[] = "0123456789ABCDEF"; + QString hex; + hex.reserve(dat.size() * (separator ? 3 : 2)); + for (size_t i = 0; i < dat.size(); ++i) { + if (separator && i) hex += QLatin1Char(separator); + hex += QLatin1Char(digits[dat[i] >> 4]); + hex += QLatin1Char(digits[dat[i] & 0xf]); + } + return hex; +} + +// boundary conversions for the remaining Qt byte-array based state APIs +template +std::vector toBytes(const T &dat) { return {dat.begin(), dat.end()}; } +inline auto qbytes(const std::vector &dat) { + return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); } } diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 60f7b4d2b8..67b89cb080 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -356,8 +356,8 @@ void StreamCameraView::parseQLog(std::shared_ptr qlog) { update(); } -void StreamCameraView::paintGL() { - CameraWidget::paintGL(); +void StreamCameraView::paintEvent(QPaintEvent *event) { + CameraWidget::paintEvent(event); QPainter p(this); bool scrubbing = false; diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index d45695904e..09f7a9931b 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -35,7 +35,7 @@ class StreamCameraView : public CameraWidget { public: StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr); - void paintGL() override; + void paintEvent(QPaintEvent *event) override; void parseQLog(std::shared_ptr qlog); private: diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 80f3ac4e45..b76fdc7dde 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -44,7 +44,7 @@ function install_linux_deps() { echo "[ ] system packages already installed t=$SECONDS" elif command -v apt-get > /dev/null 2>&1; then $SUDO apt-get update - $SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git + $SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git xclip wl-clipboard elif command -v dnf > /dev/null 2>&1; then $SUDO dnf install -y ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-langpack-en git elif command -v yum > /dev/null 2>&1; then From 3a55f31dc5ad69a732950531997e970a66374e14 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 17 Jul 2026 15:24:34 -0700 Subject: [PATCH 007/325] agnos 18.5 (#38302) --- launch_env.sh | 2 +- openpilot/common/hardware/tici/agnos.json | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index d71a5c2767..3e0ca602b4 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="18.4" + export AGNOS_VERSION="18.5" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/tici/agnos.json b/openpilot/common/hardware/tici/agnos.json index 07e2079ec9..f22a57db75 100644 --- a/openpilot/common/hardware/tici/agnos.json +++ b/openpilot/common/hardware/tici/agnos.json @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz", - "hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", - "hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8", + "url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz", + "hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", + "hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", "size": 17487872, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8" + "ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz", - "hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113", - "hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", + "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz", + "hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539", + "hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342", + "ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134", "alt": { - "hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img", + "hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", + "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img", "size": 4718592000 } } From c8786d930d3fbe936e184c917769306518eaedeb Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 17 Jul 2026 15:27:20 -0700 Subject: [PATCH 008/325] DM: reasonable lockout ramp up (#38358) --- openpilot/cereal/log.capnp | 3 +- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/monitoring/policy.py | 30 ++++++++++++------- .../selfdrive/monitoring/test_monitoring.py | 14 ++++----- openpilot/selfdrive/selfdrived/events.py | 6 ++-- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 8ce9f868af..e1725d1e44 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2160,7 +2160,8 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 { struct DriverMonitoringState { lockout @0 :Bool; - lockoutRecoveryPercent @11 :Int8; + lockoutCount @15 :Int8; + lockoutMinutesRemaining @11 :Int8; alert3Count @12 :Int8; noResponseCount @13 :Int8; noResponseForceDecel @14 :Bool; diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 7efa5721dc..0615c02700 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -37,6 +37,7 @@ inline static std::unordered_map keys = { {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, + {"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}}, {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY, BOOL}}, {"ExperimentalMode", {PERSISTENT, BOOL}}, {"ExperimentalModeConfirmed", {PERSISTENT, BOOL}}, diff --git a/openpilot/selfdrive/monitoring/policy.py b/openpilot/selfdrive/monitoring/policy.py index 03db1dc02e..1ce9f03b3e 100644 --- a/openpilot/selfdrive/monitoring/policy.py +++ b/openpilot/selfdrive/monitoring/policy.py @@ -41,7 +41,7 @@ class DRIVER_MONITOR_SETTINGS: # lockout specs self._MAX_ALERT_3 = 2 self._MAX_NO_RESPONSE = 1 - self._LOCKOUT_TIME = int(1800 / DT_DMON) + self._LOCKOUT_TIMES = [int(60 * n_min / DT_DMON) for n_min in [1, 5, 15, 30]] self._TIMEOUT_RECOVERY_FACTOR_MAX = 5. self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25 @@ -152,7 +152,10 @@ class DriverMonitoring: self.cnt_since_alert_3 = 0 self.no_response_timeout = int(self.settings._NO_RESPONSE_TIMEOUT / DT_DMON) self.no_response_cnt = 0 - self.lockout_time = 0 + self.lockout_active = Params().get_bool("DriverTooDistracted") + self.lockout_count = Params().get("DriverLockoutCount") or 0 + self.lockout_duration = self.settings._LOCKOUT_TIMES[min(max(self.lockout_count - 1, 0), len(self.settings._LOCKOUT_TIMES) - 1)] + self.lockout_time_elapsed = 0 self.step_change = 0. self.active_policy = MonitoringPolicy.vision self.driver_interacting = False @@ -163,7 +166,6 @@ class DriverMonitoring: self.threshold_alert_2 = 0. self.dcam_uncertain_cnt = 0 self.dcam_reset_cnt = 0 - self.too_distracted = Params().get_bool("DriverTooDistracted") self._reset_awareness() self._set_policy(MonitoringPolicy.vision) @@ -310,16 +312,20 @@ class DriverMonitoring: self.driver_interacting = driver_engaged if self.alert_3_cnt >= self.settings._MAX_ALERT_3 or self.no_response_cnt >= self.settings._MAX_NO_RESPONSE: - self.too_distracted = True + if not self.lockout_active: + self.lockout_count += 1 + self.lockout_duration = self.settings._LOCKOUT_TIMES[min(self.lockout_count - 1, len(self.settings._LOCKOUT_TIMES) - 1)] + Params().put("DriverLockoutCount", self.lockout_count) + self.lockout_active = True - if self.too_distracted: - self.lockout_time += 1 - if self.lockout_time > self.settings._LOCKOUT_TIME: - self.too_distracted = False + if self.lockout_active: + self.lockout_time_elapsed += 1 + if self.lockout_time_elapsed > self.lockout_duration: + self.lockout_active = False self.alert_3_cnt = 0 self.cnt_since_alert_3 = 0 self.no_response_cnt = 0 - self.lockout_time = 0 + self.lockout_time_elapsed = 0 always_on_valid = self.always_on and not wrong_gear if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \ @@ -379,8 +385,10 @@ class DriverMonitoring: dat = messaging.new_message('driverMonitoringState', valid=valid) dm = dat.driverMonitoringState - dm.lockout = self.too_distracted - dm.lockoutRecoveryPercent = to_percent(self.lockout_time / self.settings._LOCKOUT_TIME) + dm.lockout = self.lockout_active + dm.lockoutCount = self.lockout_count + if self.lockout_active: + dm.lockoutMinutesRemaining = max(1, round((self.lockout_duration - self.lockout_time_elapsed) * DT_DMON / 60.)) dm.alert3Count = self.alert_3_cnt dm.noResponseCount = self.no_response_cnt dm.noResponseForceDecel = self.alert_level == AlertLevel.three and self.cnt_since_alert_3 >= self.no_response_timeout diff --git a/openpilot/selfdrive/monitoring/test_monitoring.py b/openpilot/selfdrive/monitoring/test_monitoring.py index 48fffcc1c0..0368dc9b67 100644 --- a/openpilot/selfdrive/monitoring/test_monitoring.py +++ b/openpilot/selfdrive/monitoring/test_monitoring.py @@ -83,21 +83,17 @@ class TestMonitoring: # engaged, distracted past red and beyond the no-response window -> unavailability response + lockout def test_distracted_lockout(self): alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false) - s = d_status.settings assert alert_lvls[int(DISTRACTED_SECONDS_TO_RED / DT_DMON)] == 3 - assert d_status.alert_3_cnt == 1 - assert d_status.no_response_cnt == s._MAX_NO_RESPONSE - assert d_status.too_distracted - assert d_status.lockout_time > 0 + assert d_status.lockout_active + assert d_status.lockout_time_elapsed > 0 + assert d_status.lockout_count >= 1 # no face -> wheeltouch red, sustained past the no-response timeout -> unavailability response + lockout def test_invisible_lockout(self): _, d_status = self._run_seq(always_no_face, always_false, always_true, always_false) - s = d_status.settings assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch - assert d_status.alert_3_cnt == 1 - assert d_status.no_response_cnt == s._MAX_NO_RESPONSE - assert d_status.too_distracted + assert d_status.lockout_active + assert d_status.lockout_count >= 1 # engaged, no face detected the whole time, no action def test_fully_invisible_driver(self): diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 00d0334ada..367bc03d33 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -10,9 +10,8 @@ from opendbc.car.structs import car import openpilot.cereal.messaging as messaging from openpilot.common.constants import CV from openpilot.common.git import get_short_branch -from openpilot.common.realtime import DT_CTRL, DT_DMON +from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER -from openpilot.selfdrive.monitoring.policy import DRIVER_MONITOR_SETTINGS from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION from openpilot.common.hardware import HARDWARE @@ -23,7 +22,6 @@ VisualAlert = car.CarControl.HUDControl.VisualAlert AudibleAlert = log.SelfdriveState.AudibleAlert EventName = log.OnroadEvent.EventName -DMON_LOCKOUT_TIME = DRIVER_MONITOR_SETTINGS()._LOCKOUT_TIME # Alert priorities class Priority(IntEnum): @@ -269,7 +267,7 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: if sm['driverMonitoringState'].lockout: - mins_left = max(1, round((100 - sm['driverMonitoringState'].lockoutRecoveryPercent) / 100 * DMON_LOCKOUT_TIME * DT_DMON / 60.)) + mins_left = sm['driverMonitoringState'].lockoutMinutesRemaining return NoEntryAlert("Too Distracted", f"{mins_left} minute{'s' if mins_left != 1 else ''} Left", priority=Priority.HIGH) return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH) From b9f25f8a43bf6f47921ba8270810fb8315351e8f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 17 Jul 2026 15:28:39 -0700 Subject: [PATCH 009/325] webrtcd: move to libdatachannel (#38228) * try libdatachannel * fix uv lock * fix uv lock * remove datachannel abstraction and just use libdatachannel * clean * fix livestream bitrate controller * fix sample * fix import breaking ci * clean up and add catches * bump tele * move to teleop master * fix linter * . * remove libdatachannel explicit reference from pyproject.toml * add PyJWT crypto and catch in registration * spelling * add crypto back to lock --------- Co-authored-by: stefpi <19478336+stefpi@users.noreply.github.com> --- openpilot/system/athena/registration.py | 3 + openpilot/system/webrtc/device/video.py | 10 +- .../webrtc/tests/test_stream_session.py | 12 +- openpilot/system/webrtc/webrtcd.py | 109 +++--- pyproject.toml | 3 +- teleoprtc_repo | 2 +- uv.lock | 315 ++---------------- 7 files changed, 75 insertions(+), 379 deletions(-) diff --git a/openpilot/system/athena/registration.py b/openpilot/system/athena/registration.py index 0e8d4bc49e..35051c0659 100755 --- a/openpilot/system/athena/registration.py +++ b/openpilot/system/athena/registration.py @@ -82,6 +82,9 @@ def register(show_spinner=False) -> str | None: dongleauth = json.loads(resp.text) dongle_id = dongleauth["dongle_id"] break + except NotImplementedError: + # dependency issues with PyJWT will hang the registration test in backoff loop otherwise + raise except Exception: cloudlog.exception("failed to authenticate") backoff = min(backoff + 1, 15) diff --git a/openpilot/system/webrtc/device/video.py b/openpilot/system/webrtc/device/video.py index 3c8a7b93c2..a67247bc3c 100644 --- a/openpilot/system/webrtc/device/video.py +++ b/openpilot/system/webrtc/device/video.py @@ -4,7 +4,6 @@ import time import av from teleoprtc.tracks import TiciVideoStreamTrack -from aiortc import MediaStreamError from openpilot.cereal import messaging from openpilot.common.realtime import DT_MDL @@ -55,6 +54,9 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): if not enabled: self._seen_keyframe = False + def request_keyframe(self) -> None: + self.params.put("LivestreamRequestKeyframe", True, block=False) + def _build_frame_data(self, msg) -> bytes: encode_data = getattr(msg, msg.which()) if not self.timing_sei_enabled: @@ -71,9 +73,6 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): async def recv(self): while True: - if self.readyState != "live": - raise MediaStreamError - # while video is disabled, pause here without returning if not self.video_enabled: await asyncio.sleep(0.005) @@ -95,6 +94,3 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): self.log_debug("track sending frame %d", self._pts) return packet - - def codec_preference(self) -> str | None: - return "H264" diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index e03932f7b2..7e95097c87 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -1,15 +1,10 @@ import asyncio import json import time -# for aiortc and its dependencies -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) -warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel -from aiortc import RTCDataChannel -from aiortc.mediastreams import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE import capnp from openpilot.cereal import messaging, log +from teleoprtc.tracks import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack @@ -31,7 +26,8 @@ class TestStreamSession: expected_dict = {"type": "customReservedRawData0", "logMonoTime": 123, "valid": True, "data": "test"} expected_json = json.dumps(expected_dict).encode() - channel = mocker.Mock(spec=RTCDataChannel) + channel = mocker.Mock() + channel.is_open.return_value = True proxy = CerealOutgoingMessageProxy(["customReservedRawData0"]) def mocked_update(t): proxy.sm.update_msgs(0, [test_msg]) @@ -73,7 +69,6 @@ class TestStreamSession: track = LiveStreamVideoStreamTrack("driver") assert track.id.startswith("driver") - assert track.codec_preference() == "H264" for i in range(5): packet = self.loop.run_until_complete(track.recv()) @@ -83,4 +78,3 @@ class TestStreamSession: start_pts = packet.pts assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms assert packet.size == 0 - diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index bf96cd04ba..8276df98f2 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 from abc import abstractmethod +from collections.abc import Callable import os import socket import time +import capnp import argparse import asyncio import contextlib @@ -14,17 +16,7 @@ import signal import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import urlparse, parse_qs -from typing import Any, TYPE_CHECKING - -# aiortc and its dependencies have lots of internal warnings :( -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) -warnings.filterwarnings("ignore", category=RuntimeWarning) # TODO: remove this when google-crc32c publish a python3.12 wheel - -import capnp -if TYPE_CHECKING: - from aiortc.rtcdatachannel import RTCDataChannel -import aioice.ice +from typing import Any from openpilot.system.webrtc.helpers import StreamRequestBody from openpilot.system.webrtc.schema import generate_field @@ -44,20 +36,8 @@ def _default_route_ip() -> str | None: finally: s.close() -# aioice patch: gather ICE candidates only on the default-route interface -_get_host_addresses = aioice.ice.get_host_addresses -def _primary_host_addresses(use_ipv4: bool, use_ipv6: bool) -> list[str]: - addresses = _get_host_addresses(use_ipv4, use_ipv6) - primary = _default_route_ip() - if primary not in addresses: - return addresses - return [primary, ] -aioice.ice.get_host_addresses = _primary_host_addresses - - class AsyncTaskRunner: def __init__(self): - self.is_running = False self.task = None self.logger = logging.getLogger("webrtcd") @@ -86,10 +66,10 @@ class CerealOutgoingMessageProxy(AsyncTaskRunner): super().__init__() self.services = list(services) self.sm = messaging.SubMaster(self.services) - self.channels: list[RTCDataChannel] = [] + self.channels = [] self._enabled = enabled - def add_channel(self, channel: 'RTCDataChannel'): + def add_channel(self, channel): self.channels.append(channel) def enable(self, enable: bool): @@ -118,20 +98,17 @@ class CerealOutgoingMessageProxy(AsyncTaskRunner): outgoing_msg = {"type": service, "logMonoTime": mono_time, "valid": valid, "data": msg_dict} encoded_msg = json.dumps(outgoing_msg).encode() for channel in self.channels: + if not channel.is_open(): + continue channel.send(encoded_msg) async def run(self): - from aiortc.exceptions import InvalidStateError - while True: if not self._enabled: await asyncio.sleep(0.01) continue try: self.update() - except InvalidStateError: - self.logger.warning("Cereal outgoing proxy invalid state (connection closed)") - break except Exception: self.logger.exception("Cereal outgoing proxy failure") await asyncio.sleep(0.01) @@ -172,17 +149,17 @@ class LivestreamBitrateController(AsyncTaskRunner): high_level = 0.1 # drop immediately med_level = 0.05 # drop after # of samples low_level = 0 # raise after # of samples - down_samples = 5 # 1s + down_samples = 5 param_name = "LivestreamEncoderBitrate" - def __init__(self, peer_connection: Any, params: Params, enabled: bool = True): + def __init__(self, get_stats: Callable[[], dict[str, Any]], params: Params, enabled: bool = True): super().__init__() - self.pc = peer_connection + self.get_stats = get_stats self.params = params self.level = 2 self._publish(self.bitrates[self.level]) - self.prev_lost, self.prev_sent = None, None + self.prev_stats: tuple[Any, ...] | None = None self.counter = 0 self.up_samples = 5 # 1s self._auto = True @@ -199,7 +176,7 @@ class LivestreamBitrateController(AsyncTaskRunner): if not self._auto: continue - loss_rate = await self._sample() + loss_rate = self._sample() if loss_rate is None: continue if loss_rate >= self.med_level and self.level > 0: @@ -216,22 +193,18 @@ class LivestreamBitrateController(AsyncTaskRunner): self.counter = 0 self._publish(self.bitrates[self.level]) - async def _sample(self) -> float | None: - report = await self.pc.getStats() - packets_lost = packets_sent = 0 - for s in report.values(): - if s.type == "remote-inbound-rtp": - packets_lost += s.packetsLost - elif s.type == "outbound-rtp": - packets_sent += s.packetsSent - - if self.prev_lost is None: - self.prev_lost, self.prev_sent = packets_lost, packets_sent + def _sample(self) -> float | None: + report = next(iter(self.get_stats().values()), None) + if report is None: return None - lost_delta = max(0, packets_lost - self.prev_lost) - sent_delta = max(0, packets_sent - self.prev_sent) - self.prev_lost, self.prev_sent = packets_lost, packets_sent - return lost_delta / sent_delta if sent_delta else 0.0 + + current = (report.ssrc, report.fraction_lost, report.packets_lost, report.highest_seq_no, report.jitter, report.lsr, report.dlsr) + if self.prev_stats == current: + return None + self.prev_stats = current + + loss_rate = report.fraction_lost / 256 + return loss_rate def _publish(self, bitrate: float): self.params.put(self.param_name, bitrate) @@ -248,17 +221,15 @@ class StreamSession: shared_pub_master = DynamicPubMaster([]) def __init__(self, body: StreamRequestBody, debug_mode: bool = False): - if debug_mode: - from aiortc.mediastreams import VideoStreamTrack from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack from teleoprtc.builder import WebRTCAnswerBuilder self.identifier = str(uuid.uuid4()) self.params = Params() - builder = WebRTCAnswerBuilder(body.sdp) + builder = WebRTCAnswerBuilder(body.sdp, bind_address=_default_route_ip()) self.enabled = body.enabled - self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled) if not debug_mode else VideoStreamTrack() + self.video_track = LiveStreamVideoStreamTrack(body.init_camera, self.enabled) builder.add_video_stream(body.init_camera, self.video_track) self.stream = builder.stream() @@ -270,7 +241,7 @@ class StreamSession: self.incoming_bridge = CerealIncomingMessageProxy(self.shared_pub_master) if len(body.bridge_services_out) > 0: self.outgoing_bridge = CerealOutgoingMessageProxy(body.bridge_services_out, self.enabled) - self.bitrate_controller = LivestreamBitrateController(self.stream.peer_connection, self.params, self.enabled) + self.bitrate_controller = LivestreamBitrateController(self.stream.get_receiver_report_stats, self.params, self.enabled) self.run_task: asyncio.Task | None = None self._cleanup_lock = asyncio.Lock() @@ -305,13 +276,16 @@ class StreamSession: case "livestreamCameraSwitch": self.video_track.switch_camera(payload["data"]["camera"]) case "livestreamSettings": - self.bitrate_controller.set_quality(payload["data"]["quality"]) + if self.bitrate_controller is not None: + self.bitrate_controller.set_quality(payload["data"]["quality"]) case "livestreamVideoEnable": enabled = payload["data"]["enabled"] self.enabled = enabled self.video_track.enable(enabled) - self.outgoing_bridge.enable(enabled) - self.bitrate_controller.enable(enabled) + if self.outgoing_bridge is not None: + self.outgoing_bridge.enable(enabled) + if self.bitrate_controller is not None: + self.bitrate_controller.enable(enabled) if not enabled: self.params.put("LivestreamRequestKeyframe", True) case "clockSync": @@ -325,7 +299,8 @@ class StreamSession: case _: if payload.get("type") not in self.incoming_bridge_services: return - self.incoming_bridge.send(message) + if self.incoming_bridge is not None: + self.incoming_bridge.send(message) except Exception: self.logger.exception("Cereal incoming proxy failure") @@ -341,7 +316,8 @@ class StreamSession: channel = self.stream.get_messaging_channel() self.outgoing_bridge.add_channel(channel) self.outgoing_bridge.start() - self.bitrate_controller.start() + if self.bitrate_controller is not None: + self.bitrate_controller.start() self.logger.info("Stream session (%s) connected", self.identifier) await self.stream.wait_for_disconnection() @@ -357,7 +333,8 @@ class StreamSession: return self._cleanup_done = True self.params.put("LivestreamRequestKeyframe", False) - await self.bitrate_controller.stop() + if self.bitrate_controller is not None: + await self.bitrate_controller.stop() if self.outgoing_bridge is not None: await self.outgoing_bridge.stop() if self.video_track is not None: @@ -417,7 +394,12 @@ async def handle_get_stream(state: ServerState, raw_body: bytes) -> tuple[int, b session = StreamSession(body, debug_mode) stream_dict[session.identifier] = session try: - answer = await session.get_answer() + answer = await asyncio.wait_for(session.get_answer(), timeout=30) + except TimeoutError: + await session.stop() + stream_dict.pop(session.identifier, None) + logging.getLogger("webrtcd").exception("Timed out creating stream answer") + raise except Exception: await session.stop() stream_dict.pop(session.identifier, None) @@ -558,9 +540,6 @@ async def _shutdown(server: WebrtcdHTTPServer, state: ServerState, loop: asyncio def prewarm_stream_session_imports(debug_mode: bool = False) -> None: - if debug_mode: - from aiortc.mediastreams import VideoStreamTrack - assert VideoStreamTrack from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack from teleoprtc.builder import WebRTCAnswerBuilder assert LiveStreamVideoStreamTrack diff --git a/pyproject.toml b/pyproject.toml index 51f07d3870..de356e4b6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ # body / webrtcd "av", - "aiortc", # logging "pyzmq", @@ -47,7 +46,7 @@ dependencies = [ "xattr", # used in place of 'os.getxattr' for macOS compatibility # athena - "PyJWT", + "PyJWT[crypto]", "websocket_client", # joystickd diff --git a/teleoprtc_repo b/teleoprtc_repo index 22df577821..c0f813f1c4 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit 22df577821862e32a011fb0cf42577599f3a79c4 +Subproject commit c0f813f1c4f7e2d29bc0ed6a4d2a7b3268511b0f diff --git a/uv.lock b/uv.lock index bb6a1cb59c..d771c2aacb 100644 --- a/uv.lock +++ b/uv.lock @@ -8,95 +8,6 @@ overrides = [ { name = "opendbc", editable = "opendbc_repo" }, ] -[[package]] -name = "aiohappyeyeballs" -version = "2.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "typing-extensions" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, - { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, - { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, - { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, - { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, - { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, - { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, -] - -[[package]] -name = "aioice" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "ifaddr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, -] - -[[package]] -name = "aiortc" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aioice" }, - { name = "av" }, - { name = "cryptography" }, - { name = "google-crc32c" }, - { name = "pyee" }, - { name = "pylibsrtp" }, - { name = "pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -493,15 +404,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, ] -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -528,44 +430,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "google-crc32c" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, -] - [[package]] name = "hypothesis" version = "6.47.5" @@ -588,15 +452,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] -[[package]] -name = "ifaddr" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, -] - [[package]] name = "importlib-resources" version = "7.1.0" @@ -672,6 +527,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] +[[package]] +name = "libdatachannel-py" +version = "2026.1.0.dev2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2f/68e8306327ddef4b2133d2efb163cb05b319759ce8bd50b8b32dcd03dd95/libdatachannel_py-2026.1.0.dev2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6607fa1439e1b5bfceecd387c433470c9d45e439c3c06fa064f5c4669ad7e582", size = 1213155, upload-time = "2026-05-19T03:37:12.796Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e3/10aed36ffaf1744795322aae612db777991575b72a9f04e2c677c2c022bf/libdatachannel_py-2026.1.0.dev2-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:a060b1250f57d1fccb36e3a6b36ac8f4fd34926a6b51c564e787e8b7206458aa", size = 1224706, upload-time = "2026-05-19T03:37:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/09/a9/103fc647a8f9c721ab140fe8d2f8dbd90817e917ca763c4eb0f843fe247e/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:0b8aa3be2fa3654ea24f882756d6599e847de866a44f7a291c60994548a2debb", size = 1638879, upload-time = "2026-05-19T03:37:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/0dc7d3fe80fc247ec165dbd455bc2a1a307ef65702a43e24473202c2bf42/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_35_x86_64.whl", hash = "sha256:339a79fcbc8c6caf91c620f6e4a0f1b8ccb6a941a966d10a3135c980ae4651a6", size = 1718006, upload-time = "2026-05-19T03:37:11.891Z" }, + { url = "https://files.pythonhosted.org/packages/6d/86/30904a8753e9db60d8c3cf8efda09585fc68f2004d3d7aa2910c93a8eed5/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_38_aarch64.whl", hash = "sha256:b9f476cb065b50856ab2e53bf774ccca9c6a66454b7ce903aaf6dfcdef2a4482", size = 1643757, upload-time = "2026-05-19T03:37:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9d/1e10131396d28e84a8088a63c14978cc215f6677dc85acdd96b6068f0664/libdatachannel_py-2026.1.0.dev2-cp312-cp312-manylinux_2_38_x86_64.whl", hash = "sha256:1f31db7347549edcd69fcc1ecb8b31e7183894808ccf9387afc49a4d68debaae", size = 1751748, upload-time = "2026-05-19T03:37:06.98Z" }, +] + [[package]] name = "libusb-package" version = "1.0.30.0" @@ -785,33 +653,6 @@ requires-dist = [ ] provides-extras = ["dev"] -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, -] - [[package]] name = "numpy" version = "2.5.0" @@ -876,7 +717,6 @@ name = "openpilot" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "aiortc" }, { name = "av" }, { name = "cffi" }, { name = "comma-deps-acados" }, @@ -899,7 +739,7 @@ dependencies = [ { name = "numpy" }, { name = "pillow" }, { name = "pycapnp" }, - { name = "pyjwt" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "pyzmq" }, { name = "qrcode" }, { name = "requests" }, @@ -954,7 +794,6 @@ standalone = [ [package.metadata] requires-dist = [ - { name = "aiortc" }, { name = "av" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, @@ -987,7 +826,7 @@ requires-dist = [ { name = "pillow" }, { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "pycapnp", specifier = "==2.1.0" }, - { name = "pyjwt" }, + { name = "pyjwt", extras = ["crypto"] }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, @@ -1096,32 +935,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, ] -[[package]] -name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, -] - [[package]] name = "pycapnp" version = "2.1.0" @@ -1170,18 +983,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, ] -[[package]] -name = "pyee" -version = "13.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1200,26 +1001,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] -[[package]] -name = "pylibsrtp" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, - { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, - { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, - { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, - { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, ] [[package]] @@ -1235,19 +1019,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] -[[package]] -name = "pyopenssl" -version = "26.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, -] - [[package]] name = "pyparsing" version = "3.3.2" @@ -1566,18 +1337,12 @@ name = "teleoprtc" version = "1.0.1" source = { editable = "teleoprtc_repo" } dependencies = [ - { name = "aiohttp" }, - { name = "aiortc" }, - { name = "av" }, - { name = "numpy" }, + { name = "libdatachannel-py" }, ] [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.7.0" }, - { name = "aiortc", specifier = ">=1.6.0" }, - { name = "av", specifier = ">=11.0.0,<13.0.0" }, - { name = "numpy", specifier = ">=1.19.0" }, + { name = "libdatachannel-py", specifier = ">=2026.1.0.dev2" }, { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8" }, { name = "pre-commit", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -1700,15 +1465,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -1745,37 +1501,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, ] -[[package]] -name = "yarl" -version = "1.24.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, - { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, - { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, - { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, - { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, - { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, - { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, - { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, -] - [[package]] name = "zensical" version = "0.0.46" From c21b0821daffaa529f2a210ffce67e5166a07b60 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:23:44 -0700 Subject: [PATCH 010/325] fix(ui): label alignment and text with icon (#38365) fix align bottom and text positioning with icon --- openpilot/system/ui/widgets/label.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpilot/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py index 7fe25ab51d..7052b2c2a9 100644 --- a/openpilot/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -188,6 +188,9 @@ class Label(Widget): if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) + elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE + text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height) else: text_pos = rl.Vector2(self._rect.x, self._rect.y) @@ -196,11 +199,11 @@ class Label(Widget): if len(self._text_wrapped) > 0: if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: icon_x = self._rect.x + self._text_padding - text_pos.x = self._icon.width + ICON_PADDING + text_pos.x = self._rect.x + self._icon.width + ICON_PADDING elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: total_width = self._icon.width + ICON_PADDING + text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 - text_pos.x = self._icon.width + ICON_PADDING + text_pos.x = self._rect.x + self._icon.width + ICON_PADDING else: icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - ICON_PADDING - self._icon.width else: From 3f93b0012063987f722abd0f8f5a5061a07bfd99 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jul 2026 08:52:02 -0700 Subject: [PATCH 011/325] webrtc: remove av (#38369) --- openpilot/system/webrtc/device/video.py | 17 +++++++++++------ .../system/webrtc/tests/test_stream_session.py | 5 ++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpilot/system/webrtc/device/video.py b/openpilot/system/webrtc/device/video.py index a67247bc3c..c300f76485 100644 --- a/openpilot/system/webrtc/device/video.py +++ b/openpilot/system/webrtc/device/video.py @@ -1,8 +1,8 @@ import asyncio +from dataclasses import dataclass import struct import time -import av from teleoprtc.tracks import TiciVideoStreamTrack from openpilot.cereal import messaging @@ -21,6 +21,15 @@ TIMING_SEI_UUID = bytes([ _SEI_PREFIX = b'\x00\x00\x00\x01\x06\x05\x30' + TIMING_SEI_UUID +@dataclass(frozen=True) +class EncodedVideoFrame: + data: bytes + pts: int + + def __bytes__(self) -> bytes: + return self.data + + class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): camera_to_sock_mapping = { "driver": "livestreamDriverEncodeData", @@ -86,11 +95,7 @@ class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): break await asyncio.sleep(0.005) - packet = av.Packet(self._build_frame_data(msg)) - packet.time_base = self._time_base - self._pts = ((time.monotonic_ns() - self._t0_ns) * self._clock_rate) // 1_000_000_000 - packet.pts = self._pts self.log_debug("track sending frame %d", self._pts) - return packet + return EncodedVideoFrame(self._build_frame_data(msg), self._pts) diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index 7e95097c87..b83e1223d2 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -4,7 +4,7 @@ import time import capnp from openpilot.cereal import messaging, log -from teleoprtc.tracks import VIDEO_CLOCK_RATE, VIDEO_TIME_BASE +from teleoprtc.tracks import VIDEO_CLOCK_RATE from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack @@ -72,9 +72,8 @@ class TestStreamSession: for i in range(5): packet = self.loop.run_until_complete(track.recv()) - assert packet.time_base == VIDEO_TIME_BASE if i == 0: start_ns = time.monotonic_ns() start_pts = packet.pts assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms - assert packet.size == 0 + assert bytes(packet) == b"" From 3f49e2d33c40b20424aceafe1d5d2d1973e17acf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jul 2026 09:02:08 -0700 Subject: [PATCH 012/325] jp: add thumbnail source (#38363) --- openpilot/tools/jotpluggler/app.cc | 9 +- openpilot/tools/jotpluggler/app.h | 27 ++ openpilot/tools/jotpluggler/common.cc | 4 +- openpilot/tools/jotpluggler/common.h | 3 +- openpilot/tools/jotpluggler/layout_io.cc | 2 + openpilot/tools/jotpluggler/session.cc | 3 + openpilot/tools/jotpluggler/sketch_layout.cc | 45 ++++ openpilot/tools/jotpluggler/thumbnail.cc | 253 +++++++++++++++++++ openpilot/tools/jotpluggler/thumbnail.h | 5 + 9 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 openpilot/tools/jotpluggler/thumbnail.cc create mode 100644 openpilot/tools/jotpluggler/thumbnail.h diff --git a/openpilot/tools/jotpluggler/app.cc b/openpilot/tools/jotpluggler/app.cc index 01eec15367..e6ba696bae 100644 --- a/openpilot/tools/jotpluggler/app.cc +++ b/openpilot/tools/jotpluggler/app.cc @@ -3,6 +3,7 @@ #include "tools/jotpluggler/common.h" #include "tools/jotpluggler/internal.h" #include "tools/jotpluggler/map.h" +#include "tools/jotpluggler/thumbnail.h" #include "common/hardware/hw.h" #include "imgui_impl_glfw.h" @@ -1033,10 +1034,12 @@ bool apply_special_item_to_pane(WorkspaceTab *tab, TabUiState *tab_state, int pa if (pane.title == UNTITLED_PANE_TITLE || previous_kind != PaneKind::Plot) { pane.title = spec->label; } - } else { + } else if (spec->kind == PaneKind::Camera) { pane.title = spec->label; resize_tab_pane_state(tab_state, tab->panes.size()); tab_state->camera_panes[static_cast(pane_index)].fit_to_pane = true; + } else { + pane.title = spec->label; } tab_state->active_pane_index = pane_index; return true; @@ -1565,6 +1568,8 @@ void draw_pane_windows(AppSession *session, UiState *state) { } if (pane.kind == PaneKind::Map) { draw_map_pane(session, state, &pane, static_cast(i)); + } else if (pane.kind == PaneKind::Thumbnail) { + draw_thumbnail_pane(session, state); } else if (pane.kind == PaneKind::Camera) { draw_camera_pane(session, state, tab_state, static_cast(i), pane); } else { @@ -1847,6 +1852,7 @@ int run(const Options &options) { for (std::unique_ptr &feed : session.pane_camera_feeds) { feed = std::make_unique(); } + session.thumbnail_view = std::make_unique(); sync_camera_feeds(&session); if (session.async_route_loading) { @@ -1892,6 +1898,7 @@ int run(const Options &options) { for (std::unique_ptr &feed : session.pane_camera_feeds) { feed.reset(); } + session.thumbnail_view.reset(); return 0; } catch (const std::exception &err) { std::cerr << err.what() << "\n"; diff --git a/openpilot/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h index a38f889687..b7754f51b2 100644 --- a/openpilot/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -81,6 +81,7 @@ struct Curve { enum class PaneKind : uint8_t { Plot, Map, + Thumbnail, Camera, }; @@ -141,6 +142,12 @@ struct CameraFeedIndex { std::vector entries; }; +struct ThumbnailFrame { + double timestamp = 0.0; + int segment = -1; + std::vector jpeg; +}; + enum class LogOrigin : uint8_t { Log, OperatingSystem, @@ -318,6 +325,7 @@ struct RouteData { CameraFeedIndex driver_camera; CameraFeedIndex wide_road_camera; CameraFeedIndex qroad_camera; + std::vector thumbnails; GpsTrace gps_trace; std::vector logs; std::vector timeline; @@ -445,6 +453,7 @@ bool icon_menu_item(const char *glyph, class AsyncRouteLoader; class CameraFeedView; +class ThumbnailView; class StreamPoller; class MapDataManager; @@ -486,6 +495,7 @@ struct AppSession { std::unique_ptr route_loader; std::unique_ptr stream_poller; std::array, 4> pane_camera_feeds; + std::unique_ptr thumbnail_view; std::unique_ptr map_data; bool async_route_loading = false; double next_stream_custom_refresh_time = 0.0; @@ -885,3 +895,20 @@ private: struct Impl; std::unique_ptr impl_; }; + +class ThumbnailView { +public: + ThumbnailView(); + ~ThumbnailView(); + + ThumbnailView(const ThumbnailView &) = delete; + ThumbnailView &operator=(const ThumbnailView &) = delete; + + void setThumbnails(const std::vector &thumbnails); + void update(double tracker_time); + void drawSized(ImVec2 size, bool loading); + +private: + struct Impl; + std::unique_ptr impl_; +}; diff --git a/openpilot/tools/jotpluggler/common.cc b/openpilot/tools/jotpluggler/common.cc index 8f696657bd..50f5fc0b95 100644 --- a/openpilot/tools/jotpluggler/common.cc +++ b/openpilot/tools/jotpluggler/common.cc @@ -46,11 +46,11 @@ const char *special_item_label(std::string_view item_id) { } bool pane_kind_is_special(PaneKind kind) { - return kind == PaneKind::Map || kind == PaneKind::Camera; + return kind == PaneKind::Map || kind == PaneKind::Thumbnail || kind == PaneKind::Camera; } bool is_default_special_title(std::string_view title) { - if (title == "Map") return true; + if (title == "Map" || title == "Thumbnail") return true; return std::any_of(kCameraViewSpecs.begin(), kCameraViewSpecs.end(), [&](const CameraViewSpec &spec) { return title == spec.label; }); diff --git a/openpilot/tools/jotpluggler/common.h b/openpilot/tools/jotpluggler/common.h index 14db83fd33..5c68b0ed22 100644 --- a/openpilot/tools/jotpluggler/common.h +++ b/openpilot/tools/jotpluggler/common.h @@ -28,8 +28,9 @@ inline constexpr std::array kCameraViewSpecs = {{ {CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera}, }}; -inline constexpr std::array kSpecialItemSpecs = {{ +inline constexpr std::array kSpecialItemSpecs = {{ {"map", "Map", PaneKind::Map, CameraViewKind::Road}, + {"thumbnail", "Thumbnail", PaneKind::Thumbnail, CameraViewKind::Road}, {kCameraViewSpecs[0].special_item_id, kCameraViewSpecs[0].label, PaneKind::Camera, kCameraViewSpecs[0].view}, {kCameraViewSpecs[1].special_item_id, kCameraViewSpecs[1].label, PaneKind::Camera, kCameraViewSpecs[1].view}, {kCameraViewSpecs[2].special_item_id, kCameraViewSpecs[2].label, PaneKind::Camera, kCameraViewSpecs[2].view}, diff --git a/openpilot/tools/jotpluggler/layout_io.cc b/openpilot/tools/jotpluggler/layout_io.cc index 5c70f7a42a..24e3a4b51f 100644 --- a/openpilot/tools/jotpluggler/layout_io.cc +++ b/openpilot/tools/jotpluggler/layout_io.cc @@ -62,6 +62,8 @@ json11::Json workspace_node_to_json(const WorkspaceNode &node, const WorkspaceTa }; if (pane.kind == PaneKind::Map) { obj["kind"] = "map"; + } else if (pane.kind == PaneKind::Thumbnail) { + obj["kind"] = "thumbnail"; } else if (pane.kind == PaneKind::Camera) { obj["kind"] = "camera"; obj["camera_view"] = camera_view_spec(pane.camera_view).layout_name; diff --git a/openpilot/tools/jotpluggler/session.cc b/openpilot/tools/jotpluggler/session.cc index beb0a292be..4c694a8daa 100644 --- a/openpilot/tools/jotpluggler/session.cc +++ b/openpilot/tools/jotpluggler/session.cc @@ -19,6 +19,9 @@ void sync_camera_feeds(AppSession *session) { session->pane_camera_feeds[i]->setCameraIndex(session->route_data.*(kCameraViewSpecs[i].route_member), kCameraViewSpecs[i].view); } } + if (session->thumbnail_view) { + session->thumbnail_view->setThumbnails(session->route_data.thumbnails); + } } void apply_route_data(AppSession *session, UiState *state, RouteData route_data) { diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index ee307653f6..1f7df72fed 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -98,6 +98,7 @@ struct LoadedRouteArtifacts { std::vector can_messages; std::vector logs; std::vector timeline; + std::vector thumbnails; std::unordered_map enum_info; }; @@ -684,6 +685,25 @@ std::vector extract_segment_logs(const std::vector &events) { return logs; } +std::vector extract_segment_thumbnails(const std::vector &events, int segment) { + std::vector thumbnails; + for (const Event &event_record : events) { + if (event_record.which != cereal::Event::Which::THUMBNAIL) continue; + with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) { + const auto thumbnail = event.getThumbnail(); + const auto jpeg = thumbnail.getThumbnail(); + if (jpeg.size() == 0) return; + const uint64_t timestamp = thumbnail.getTimestampEof(); + ThumbnailFrame frame; + frame.timestamp = static_cast(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9; + frame.segment = segment; + frame.jpeg.assign(jpeg.begin(), jpeg.end()); + thumbnails.push_back(std::move(frame)); + }); + } + return thumbnails; +} + RouteMetadata extract_segment_metadata(const std::vector &events) { RouteMetadata metadata; for (const Event &event_record : events) { @@ -796,6 +816,8 @@ Pane parse_dock_area(const json11::Json &dock_area_node) { const std::string kind = dock_area_node["kind"].string_value(); if (kind == "map") { pane.kind = PaneKind::Map; + } else if (kind == "thumbnail") { + pane.kind = PaneKind::Thumbnail; } else if (kind == "camera") { pane.kind = PaneKind::Camera; const std::string camera_view = dock_area_node["camera_view"].string_value(); @@ -1167,6 +1189,7 @@ RouteData build_route_data(std::vector &&series_list, std::vector &&can_messages, std::vector &&logs, std::vector &&timeline, + std::vector &&thumbnails, std::unordered_map &&enum_info, std::string car_fingerprint, std::string dbc_name) { @@ -1233,6 +1256,14 @@ RouteData build_route_data(std::vector &&series_list, route_data.x_min = timeline.front().start_time; route_data.x_max = timeline.back().end_time; } + std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) { + return a.timestamp < b.timestamp; + }); + if (!route_data.has_time_range && !thumbnails.empty()) { + route_data.has_time_range = true; + route_data.x_min = thumbnails.front().timestamp; + route_data.x_max = thumbnails.back().timestamp; + } if (route_data.has_time_range) { const double time_offset = route_data.x_min; @@ -1254,6 +1285,9 @@ RouteData build_route_data(std::vector &&series_list, entry.start_time -= time_offset; entry.end_time -= time_offset; } + for (ThumbnailFrame &thumbnail : thumbnails) { + thumbnail.timestamp -= time_offset; + } route_data.x_max -= time_offset; route_data.x_min = 0.0; } @@ -1271,6 +1305,7 @@ RouteData build_route_data(std::vector &&series_list, merged_timeline.push_back(std::move(entry)); } route_data.timeline = std::move(merged_timeline); + route_data.thumbnails = std::move(thumbnails); std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) { return std::make_tuple(a.id.service, a.id.bus, a.id.address) < std::make_tuple(b.id.service, b.id.bus, b.id.address); @@ -1524,6 +1559,7 @@ LoadedRouteArtifacts load_route_series_parallel( SeriesAccumulator series; std::vector logs; std::vector timeline; + std::vector thumbnails; }; const std::vector> segment_list(segments.begin(), segments.end()); @@ -1586,6 +1622,7 @@ LoadedRouteArtifacts load_route_series_parallel( results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers); results[index].logs = extract_segment_logs(reader.events); results[index].timeline = extract_segment_timeline(reader.events); + results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number); segment_stats.extract_seconds = std::chrono::duration(LoadStats::Clock::now() - extract_start).count(); segment_stats.event_count = reader.events.size(); segment_stats.series_count = populated_series_count(results[index].series); @@ -1612,6 +1649,7 @@ LoadedRouteArtifacts load_route_series_parallel( } std::vector logs; std::vector timeline; + std::vector thumbnails; for (SegmentResult &result : results) { if (!result.logs.empty()) { logs.insert(logs.end(), @@ -1623,12 +1661,18 @@ LoadedRouteArtifacts load_route_series_parallel( std::make_move_iterator(result.timeline.begin()), std::make_move_iterator(result.timeline.end())); } + if (!result.thumbnails.empty()) { + thumbnails.insert(thumbnails.end(), + std::make_move_iterator(result.thumbnails.begin()), + std::make_move_iterator(result.thumbnails.end())); + } } LoadedRouteArtifacts artifacts; artifacts.series = collect_series(std::move(merged)); artifacts.can_messages = std::move(merged.can_messages); artifacts.logs = std::move(logs); artifacts.timeline = std::move(timeline); + artifacts.thumbnails = std::move(thumbnails); artifacts.enum_info = std::move(merged.enum_info); stats->merge_end = LoadStats::Clock::now(); return artifacts; @@ -1834,6 +1878,7 @@ RouteData load_route_data(const std::string &route_name, std::move(artifacts.can_messages), std::move(artifacts.logs), std::move(artifacts.timeline), + std::move(artifacts.thumbnails), std::move(artifacts.enum_info), metadata.car_fingerprint, resolved_dbc); diff --git a/openpilot/tools/jotpluggler/thumbnail.cc b/openpilot/tools/jotpluggler/thumbnail.cc new file mode 100644 index 0000000000..11e184323a --- /dev/null +++ b/openpilot/tools/jotpluggler/thumbnail.cc @@ -0,0 +1,253 @@ +#include "tools/jotpluggler/thumbnail.h" + +#include "imgui_impl_opengl3_loader.h" + +#include +#include +#include + +extern "C" { +#include +#include +} + +namespace { + +bool decode_jpeg(const std::vector &jpeg, int *width, int *height, std::vector *rgba) { + if (jpeg.empty()) return false; + + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG); + AVCodecContext *context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr; + AVFrame *frame = av_frame_alloc(); + AVPacket *packet = av_packet_alloc(); + if (context == nullptr || frame == nullptr || packet == nullptr) { + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + const bool packet_ready = jpeg.size() <= static_cast(std::numeric_limits::max()) + && av_new_packet(packet, static_cast(jpeg.size())) >= 0; + if (packet_ready) { + std::copy(jpeg.begin(), jpeg.end(), packet->data); + } + const bool decoded = packet_ready + && avcodec_open2(context, codec, nullptr) >= 0 + && avcodec_send_packet(context, packet) >= 0 + && avcodec_receive_frame(context, frame) >= 0; + if (!decoded || frame->width <= 0 || frame->height <= 0) { + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + int chroma_x_shift = 0; + int chroma_y_shift = 0; + switch (static_cast(frame->format)) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_YUVJ420P: + chroma_x_shift = 1; + chroma_y_shift = 1; + break; + case AV_PIX_FMT_YUV422P: + case AV_PIX_FMT_YUVJ422P: + chroma_x_shift = 1; + break; + case AV_PIX_FMT_YUV444P: + case AV_PIX_FMT_YUVJ444P: + break; + default: + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return false; + } + + *width = frame->width; + *height = frame->height; + rgba->resize(static_cast(*width) * static_cast(*height) * 4U); + const bool full_range = frame->color_range == AVCOL_RANGE_JPEG + || frame->format == AV_PIX_FMT_YUVJ420P + || frame->format == AV_PIX_FMT_YUVJ422P + || frame->format == AV_PIX_FMT_YUVJ444P; + for (int y = 0; y < *height; ++y) { + const uint8_t *y_row = frame->data[0] + y * frame->linesize[0]; + const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1]; + const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2]; + uint8_t *out = rgba->data() + static_cast(y) * static_cast(*width) * 4U; + for (int x = 0; x < *width; ++x) { + const double luma = full_range ? static_cast(y_row[x]) + : 1.164383 * (static_cast(y_row[x]) - 16.0); + const double u = static_cast(u_row[x >> chroma_x_shift]) - 128.0; + const double v = static_cast(v_row[x >> chroma_x_shift]) - 128.0; + const double red = luma + (full_range ? 1.402 : 1.596027) * v; + const double green = luma - (full_range ? 0.344136 : 0.391762) * u + - (full_range ? 0.714136 : 0.812968) * v; + const double blue = luma + (full_range ? 1.772 : 2.017232) * u; + out[x * 4 + 0] = static_cast(std::clamp(std::lround(red), 0L, 255L)); + out[x * 4 + 1] = static_cast(std::clamp(std::lround(green), 0L, 255L)); + out[x * 4 + 2] = static_cast(std::clamp(std::lround(blue), 0L, 255L)); + out[x * 4 + 3] = 255; + } + } + + av_packet_free(&packet); + av_frame_free(&frame); + avcodec_free_context(&context); + return true; +} + +std::string format_thumbnail_time(double seconds) { + const int rounded = std::max(0, static_cast(std::lround(seconds))); + const int hours = rounded / 3600; + const int minutes = (rounded % 3600) / 60; + const int secs = rounded % 60; + if (hours > 0) { + return util::string_format("%d:%02d:%02d", hours, minutes, secs); + } + return util::string_format("%02d:%02d", minutes, secs); +} + +} // namespace + +struct ThumbnailView::Impl { + ~Impl() { + destroy_texture(); + } + + void setThumbnails(const std::vector &next_thumbnails) { + destroy_texture(); + thumbnails = &next_thumbnails; + displayed_index = -1; + failed_index = -1; + } + + void update(double tracker_time) { + if (thumbnails == nullptr || thumbnails->empty()) return; + auto it = std::lower_bound(thumbnails->begin(), thumbnails->end(), tracker_time, + [](const ThumbnailFrame &frame, double time) { + return frame.timestamp < time; + }); + if (it == thumbnails->end()) { + it = std::prev(thumbnails->end()); + } else if (it != thumbnails->begin()) { + const auto previous = std::prev(it); + if (std::abs(previous->timestamp - tracker_time) <= std::abs(it->timestamp - tracker_time)) { + it = previous; + } + } + const int index = static_cast(std::distance(thumbnails->begin(), it)); + if (index == displayed_index || index == failed_index) return; + + int width = 0; + int height = 0; + std::vector rgba; + if (!decode_jpeg(it->jpeg, &width, &height, &rgba)) { + failed_index = index; + return; + } + + if (texture == 0) { + glGenTextures(1, &texture); + } + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data()); + glBindTexture(GL_TEXTURE_2D, 0); + texture_width = width; + texture_height = height; + displayed_index = index; + failed_index = -1; + } + + void drawSized(ImVec2 size, bool loading) const { + size.x = std::max(1.0f, size.x); + size.y = std::max(1.0f, size.y); + ImGui::InvisibleButton("##thumbnail_sized", size); + const ImVec2 pane_min = ImGui::GetItemRectMin(); + const ImVec2 pane_max = ImGui::GetItemRectMax(); + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(pane_min, pane_max, IM_COL32(24, 24, 24, 255)); + + if (texture != 0 && texture_width > 0 && texture_height > 0) { + const float scale = std::min(size.x / static_cast(texture_width), + size.y / static_cast(texture_height)); + const ImVec2 image_size(static_cast(texture_width) * scale, + static_cast(texture_height) * scale); + const ImVec2 image_min(pane_min.x + (size.x - image_size.x) * 0.5f, + pane_min.y + (size.y - image_size.y) * 0.5f); + const ImVec2 image_max(image_min.x + image_size.x, image_min.y + image_size.y); + draw_list->AddImage(static_cast(texture), image_min, image_max); + + if (thumbnails != nullptr && displayed_index >= 0 + && displayed_index < static_cast(thumbnails->size())) { + const ThumbnailFrame &frame = (*thumbnails)[static_cast(displayed_index)]; + const std::string label = util::string_format("%s · segment %d · %d/%zu", + format_thumbnail_time(frame.timestamp).c_str(), + frame.segment, + displayed_index + 1, + thumbnails->size()); + const ImVec2 text_size = ImGui::CalcTextSize(label.c_str()); + const ImVec2 label_min(image_min.x, std::max(image_min.y, image_max.y - text_size.y - 14.0f)); + draw_list->AddRectFilled(label_min, image_max, IM_COL32(0, 0, 0, 175)); + draw_list->AddText(ImVec2(label_min.x + 7.0f, label_min.y + 7.0f), IM_COL32_WHITE, label.c_str()); + } + return; + } + + const bool has_thumbnails = thumbnails != nullptr && !thumbnails->empty(); + const char *label = loading ? "loading" : (has_thumbnails ? "invalid thumbnail" : "no thumbnails"); + const ImVec2 text_size = ImGui::CalcTextSize(label); + draw_list->AddText(ImVec2(pane_min.x + (size.x - text_size.x) * 0.5f, + pane_min.y + (size.y - text_size.y) * 0.5f), + IM_COL32(187, 187, 187, 255), label); + } + + void destroy_texture() { + if (texture != 0) { + glDeleteTextures(1, &texture); + } + texture = 0; + texture_width = 0; + texture_height = 0; + } + + const std::vector *thumbnails = nullptr; + int displayed_index = -1; + int failed_index = -1; + GLuint texture = 0; + int texture_width = 0; + int texture_height = 0; +}; + +ThumbnailView::ThumbnailView() : impl_(std::make_unique()) {} +ThumbnailView::~ThumbnailView() = default; + +void ThumbnailView::setThumbnails(const std::vector &thumbnails) { + impl_->setThumbnails(thumbnails); +} + +void ThumbnailView::update(double tracker_time) { + impl_->update(tracker_time); +} + +void ThumbnailView::drawSized(ImVec2 size, bool loading) { + impl_->drawSized(size, loading); +} + +void draw_thumbnail_pane(AppSession *session, UiState *state) { + if (session->thumbnail_view == nullptr) { + ImGui::TextDisabled("Thumbnails unavailable"); + return; + } + if (state->has_tracker_time) { + session->thumbnail_view->update(state->tracker_time); + } + session->thumbnail_view->drawSized(ImGui::GetContentRegionAvail(), session->async_route_loading); +} diff --git a/openpilot/tools/jotpluggler/thumbnail.h b/openpilot/tools/jotpluggler/thumbnail.h new file mode 100644 index 0000000000..6970173a5c --- /dev/null +++ b/openpilot/tools/jotpluggler/thumbnail.h @@ -0,0 +1,5 @@ +#pragma once + +#include "tools/jotpluggler/app.h" + +void draw_thumbnail_pane(AppSession *session, UiState *state); From f0841b827a505db8e843188281fa8d6ceeda4d0b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jul 2026 09:04:41 -0700 Subject: [PATCH 013/325] remove av (#38366) --- openpilot/tools/camerastream/README.md | 12 +- .../tools/camerastream/compressed_vipc.py | 101 ++++--- .../tools/camerastream/ffmpeg_decoder.py | 261 ++++++++++++++++++ pyproject.toml | 3 - uv.lock | 17 -- 5 files changed, 315 insertions(+), 79 deletions(-) create mode 100644 openpilot/tools/camerastream/ffmpeg_decoder.py diff --git a/openpilot/tools/camerastream/README.md b/openpilot/tools/camerastream/README.md index 8b77fc5990..9671199297 100644 --- a/openpilot/tools/camerastream/README.md +++ b/openpilot/tools/camerastream/README.md @@ -44,18 +44,18 @@ To actually display the stream, run `watch3` in separate terminal: ## compressed_vipc.py usage ``` $ python3 compressed_vipc.py -h -usage: compressed_vipc.py [-h] [--nvidia] [--cams CAMS] [--silent] addr +usage: compressed_vipc.py [-h] [--cams CAMS] [--server SERVER] [--silent] addr Decode video streams and broadcast on VisionIPC positional arguments: - addr Address of comma three + addr Address of comma three options: - -h, --help show this help message and exit - --nvidia Use nvidia instead of ffmpeg - --cams CAMS Cameras to decode - --silent Suppress debug output + -h, --help show this help message and exit + --cams CAMS Cameras to decode + --server SERVER choose vipc server name + --silent Suppress debug output ``` diff --git a/openpilot/tools/camerastream/compressed_vipc.py b/openpilot/tools/camerastream/compressed_vipc.py index 35e9d3dab2..56ed12889f 100755 --- a/openpilot/tools/camerastream/compressed_vipc.py +++ b/openpilot/tools/camerastream/compressed_vipc.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 -import av -import av.video.format import os -import sys import argparse -import numpy as np import multiprocessing import time import signal +from collections import deque import openpilot.cereal.messaging as messaging from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError V4L2_BUF_FLAG_KEYFRAME = 8 @@ -25,23 +23,12 @@ ENCODE_SOCKETS = { VisionStreamType.VISION_STREAM_WIDE_ROAD: "wideRoadEncodeData", } -def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): +def decoder(addr, vipc_server, vst, W, H, debug=False): sock_name = ENCODE_SOCKETS[vst] if debug: print(f"start decoder for {sock_name}, {W}x{H}") - if nvidia: - os.environ["NV_LOW_LATENCY"] = "3" # both bLowLatency and CUVID_PKT_ENDOFPICTURE - sys.path += os.environ["LD_LIBRARY_PATH"].split(":") - import PyNvCodec as nvc - - nvDec = nvc.PyNvDecoder(W, H, nvc.PixelFormat.NV12, nvc.CudaVideoCodec.HEVC, 0) - cc1 = nvc.ColorspaceConversionContext(nvc.ColorSpace.BT_709, nvc.ColorRange.JPEG) - conv_yuv = nvc.PySurfaceConverter(W, H, nvc.PixelFormat.NV12, nvc.PixelFormat.YUV420, 0) - nvDwn_yuv = nvc.PySurfaceDownloader(W, H, nvc.PixelFormat.YUV420, 0) - img_yuv = np.ndarray((H*W//2*3), dtype=np.uint8) - else: - codec = av.CodecContext.create("hevc", "r") + codec = Decoder("hevc") os.environ["ZMQ"] = "1" messaging.reset_context() @@ -50,13 +37,22 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): last_idx = -1 seen_iframe = False - time_q = [] + time_q = deque() + + def resync(): + nonlocal seen_iframe + codec.reset() + seen_iframe = False + time_q.clear() + while 1: msgs = messaging.drain_sock(sock, wait_for_one=True) for evt in msgs: evta = getattr(evt, evt.which()) - if debug and evta.idx.encodeId != 0 and evta.idx.encodeId != (last_idx+1): - print("DROP PACKET!") + if last_idx != -1 and evta.idx.encodeId != (last_idx + 1): + if debug: + print("DROP PACKET!") + resync() last_idx = evta.idx.encodeId if not seen_iframe and not (evta.idx.flags & V4L2_BUF_FLAG_KEYFRAME): if debug: @@ -67,48 +63,48 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): frame_latency = ((evta.idx.timestampEof/1e9) - (evta.idx.timestampSof/1e9))*1000 process_latency = ((evt.logMonoTime/1e9) - (evta.idx.timestampEof/1e9))*1000 - # put in header (first) + # put in header (first) — VPS/SPS/PPS only, no frame expected if not seen_iframe: - if nvidia: - nvDec.DecodeSurfaceFromPacket(np.frombuffer(evta.header, dtype=np.uint8)) - else: - codec.decode(av.packet.Packet(evta.header)) + try: + codec.decode(evta.header) + except FFmpegError as e: + if debug: + print(f"HEADER ERROR: {e}") + resync() + continue seen_iframe = True - if nvidia: - rawSurface = nvDec.DecodeSurfaceFromPacket(np.frombuffer(evta.data, dtype=np.uint8)) - if rawSurface.Empty(): - if debug: - print("DROP SURFACE") - continue - convSurface = conv_yuv.Execute(rawSurface, cc1) - nvDwn_yuv.DownloadSingleSurface(convSurface, img_yuv) - else: - frames = codec.decode(av.packet.Packet(evta.data)) - if len(frames) == 0: - if debug: - print("DROP SURFACE") - continue - assert len(frames) == 1 - img_yuv = frames[0].to_ndarray(format=av.video.format.VideoFormat('yuv420p')).flatten() - uv_offset = H*W - y = img_yuv[:uv_offset] - uv = img_yuv[uv_offset:].reshape(2, -1).ravel('F') - img_yuv = np.hstack((y, uv)) + try: + img_yuv = codec.decode(evta.data) + except FFmpegError as e: + if debug: + print(f"DECODE ERROR: {e}") + resync() + continue - vipc_server.send(vst, img_yuv.data, cnt, int(time_q[0]*1e9), int(time.monotonic()*1e9)) + if img_yuv is None: + if debug: + print("DROP SURFACE") + continue + + if codec.width != W or codec.height != H: + if debug: + print(f"DECODE ERROR: decoded frame is {codec.width}x{codec.height}, expected {W}x{H}") + resync() + continue + + frame_start_time = time_q.popleft() + vipc_server.send(vst, img_yuv.data, cnt, int(frame_start_time*1e9), int(time.monotonic()*1e9)) cnt += 1 - pc_latency = (time.monotonic()-time_q[0])*1000 - time_q = time_q[1:] + pc_latency = (time.monotonic()-frame_start_time)*1000 if debug: print(f"{len(msgs):2d} {evta.idx.encodeId:4d} {evt.logMonoTime/1e9:.3f} {evta.idx.timestampEof/1e6:.3f} \ roll {frame_latency:6.2f} ms latency {process_latency:6.2f} ms + {network_latency:6.2f} ms + {pc_latency:6.2f} ms \ = {process_latency+network_latency+pc_latency:6.2f} ms", len(evta.data), sock_name) - class CompressedVipc: - def __init__(self, addr, vision_streams, server_name, nvidia=False, debug=False): + def __init__(self, addr, vision_streams, server_name, debug=False): print("getting frame sizes") os.environ["ZMQ"] = "1" messaging.reset_context() @@ -127,7 +123,7 @@ class CompressedVipc: self.procs = [] for vst in vision_streams: ed = sm[ENCODE_SOCKETS[vst]] - p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, nvidia, ed.width, ed.height, debug)) + p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, ed.width, ed.height, debug)) p.start() self.procs.append(p) @@ -143,7 +139,6 @@ class CompressedVipc: if __name__ == "__main__": parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC") parser.add_argument("addr", help="Address of comma three") - parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg") parser.add_argument("--cams", default="0,1,2", help="Cameras to decode") parser.add_argument("--server", default="camerad", help="choose vipc server name") parser.add_argument("--silent", action="store_true", help="Suppress debug output") @@ -156,7 +151,7 @@ if __name__ == "__main__": ] vsts = [vision_streams[int(x)] for x in args.cams.split(",")] - cvipc = CompressedVipc(args.addr, vsts, args.server, args.nvidia, debug=(not args.silent)) + cvipc = CompressedVipc(args.addr, vsts, args.server, debug=(not args.silent)) # register exit handler signal.signal(signal.SIGINT, lambda sig, frame: cvipc.kill()) diff --git a/openpilot/tools/camerastream/ffmpeg_decoder.py b/openpilot/tools/camerastream/ffmpeg_decoder.py new file mode 100644 index 0000000000..13cdc12df1 --- /dev/null +++ b/openpilot/tools/camerastream/ffmpeg_decoder.py @@ -0,0 +1,261 @@ +import ctypes +import errno +import os + +import ffmpeg +import numpy as np + + +AV_INPUT_BUFFER_PADDING_SIZE = 64 +AV_LOG_QUIET = -8 +SWS_FAST_BILINEAR = 1 + + +class FFmpegError(RuntimeError): + pass + + +class AVPacket(ctypes.Structure): + # Public prefix of AVPacket. Only data and size are modified here; the packet + # remains non-refcounted and points at Decoder._packet_buffer. + _fields_ = [ + ("buf", ctypes.c_void_p), + ("pts", ctypes.c_int64), + ("dts", ctypes.c_int64), + ("data", ctypes.POINTER(ctypes.c_uint8)), + ("size", ctypes.c_int), + ] + + +class AVFrame(ctypes.Structure): + # Public prefix of AVFrame through format. Stable within a libavutil major + _fields_ = [ + ("data", ctypes.POINTER(ctypes.c_uint8) * 8), + ("linesize", ctypes.c_int * 8), + ("extended_data", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8))), + ("width", ctypes.c_int), + ("height", ctypes.c_int), + ("nb_samples", ctypes.c_int), + ("format", ctypes.c_int), + ] + + +def _bind(fn, restype, *argtypes): + fn.restype = restype + fn.argtypes = list(argtypes) + return fn + + +def _load_libraries(): + avutil = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavutil.so.59"), mode=ctypes.RTLD_GLOBAL) + avcodec = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavcodec.so.61"), mode=ctypes.RTLD_GLOBAL) + swscale = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libswscale.so.8"), mode=ctypes.RTLD_GLOBAL) + + c_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t + c_uint8_p = ctypes.POINTER(ctypes.c_uint8) + c_void_p_p = ctypes.POINTER(c_void_p) + + _bind(avutil.av_log_set_level, None, c_int) + _bind(avutil.av_opt_set, c_int, c_void_p, c_char_p, c_char_p, c_int) + _bind(avutil.av_strerror, c_int, c_int, c_char_p, c_size_t) + _bind(avutil.av_get_pix_fmt, c_int, c_char_p) + + _bind(avcodec.avcodec_find_decoder_by_name, c_void_p, c_char_p) + _bind(avcodec.avcodec_alloc_context3, c_void_p, c_void_p) + _bind(avcodec.avcodec_open2, c_int, c_void_p, c_void_p, c_void_p) + _bind(avcodec.avcodec_free_context, None, c_void_p_p) + _bind(avcodec.avcodec_flush_buffers, None, c_void_p) + _bind(avcodec.avcodec_send_packet, c_int, c_void_p, ctypes.POINTER(AVPacket)) + _bind(avcodec.avcodec_receive_frame, c_int, c_void_p, ctypes.POINTER(AVFrame)) + _bind(avcodec.av_packet_alloc, ctypes.POINTER(AVPacket)) + _bind(avcodec.av_packet_free, None, ctypes.POINTER(ctypes.POINTER(AVPacket))) + _bind(avcodec.av_frame_alloc, ctypes.POINTER(AVFrame)) + _bind(avcodec.av_frame_free, None, ctypes.POINTER(ctypes.POINTER(AVFrame))) + _bind(avcodec.av_frame_unref, None, ctypes.POINTER(AVFrame)) + + _bind(swscale.sws_getCachedContext, c_void_p, + c_void_p, c_int, c_int, c_int, c_int, c_int, c_int, c_int, c_void_p, c_void_p, c_void_p) + _bind(swscale.sws_scale, c_int, + c_void_p, ctypes.POINTER(c_uint8_p), ctypes.POINTER(c_int), + c_int, c_int, ctypes.POINTER(c_uint8_p), ctypes.POINTER(c_int)) + _bind(swscale.sws_freeContext, None, c_void_p) + + avutil.av_log_set_level(AV_LOG_QUIET) + return avutil, avcodec, swscale +_avutil, _avcodec, _swscale = _load_libraries() + +_DataArray = ctypes.POINTER(ctypes.c_uint8) * 4 +_LinesizeArray = ctypes.c_int * 4 + +AV_PIX_FMT_NV12 = _avutil.av_get_pix_fmt(b"nv12") +assert AV_PIX_FMT_NV12 >= 0 + + +def _error_string(code: int) -> str: + buf = ctypes.create_string_buffer(256) + if _avutil.av_strerror(code, buf, len(buf)) == 0: + return buf.value.decode(errors="replace") + return f"FFmpeg error {code}" + + +def _check(code: int, operation: str) -> None: + if code < 0: + raise FFmpegError(f"{operation}: {_error_string(code)}") + + +class Decoder: + def __init__(self, codec_name: str = "hevc"): + self.closed = True + self._sws_context = ctypes.c_void_p() + self._packet_buffer = bytearray() + self._packet_address = 0 + self._packet_data = None + self._output = np.empty(0, dtype=np.uint8) + self._dst_data = _DataArray() + self._dst_linesize = _LinesizeArray() + self.width = 0 + self.height = 0 + self._src_format = -1 + + codec = _avcodec.avcodec_find_decoder_by_name(codec_name.encode()) + if not codec: + raise FFmpegError(f"decoder not found: {codec_name}") + + self._context = ctypes.c_void_p(_avcodec.avcodec_alloc_context3(codec)) + if not self._context: + raise MemoryError("avcodec_alloc_context3 failed") + + self._packet = _avcodec.av_packet_alloc() + if not self._packet: + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise MemoryError("av_packet_alloc failed") + + self._frame = _avcodec.av_frame_alloc() + if not self._frame: + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise MemoryError("av_frame_alloc failed") + + try: + # Frame threading holds decoded frames to populate worker pipelines. + # Slice threads can reduce decode time without adding that frame queue; + # four was the latency minimum on the replay camera workload. + _check(_avutil.av_opt_set(self._context, b"threads", b"4", 0), "set decoder threads") + _check(_avutil.av_opt_set(self._context, b"thread_type", b"slice", 0), "set decoder thread type") + _check(_avutil.av_opt_set(self._context, b"flags", b"+low_delay", 0), "set low-delay mode") + _check(_avcodec.avcodec_open2(self._context, codec, None), "open decoder") + except Exception: + _avcodec.av_frame_free(ctypes.byref(self._frame)) + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + raise + + self.closed = False + + def __enter__(self): + self._ensure_open() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def _ensure_open(self) -> None: + if self.closed: + raise RuntimeError("decoder is closed") + + def _prepare_packet(self, data) -> None: + size = len(data) + required = size + AV_INPUT_BUFFER_PADDING_SIZE + if len(self._packet_buffer) < required: + # Grow-only; the address stays valid until the next reallocation. + self._packet_buffer = bytearray(required) + self._packet_address = ctypes.addressof(ctypes.c_uint8.from_buffer(self._packet_buffer)) + self._packet_data = ctypes.cast(self._packet_address, ctypes.POINTER(ctypes.c_uint8)) + + self._packet_buffer[:size] = data + ctypes.memset(self._packet_address + size, 0, AV_INPUT_BUFFER_PADDING_SIZE) + self._packet.contents.data = self._packet_data + self._packet.contents.size = size + + def _prepare_output(self, frame: AVFrame) -> None: + width, height, src_format = frame.width, frame.height, frame.format + if width <= 0 or height <= 0 or width % 2 or height % 2: + raise FFmpegError(f"unsupported frame dimensions: {width}x{height}") + if (width, height, src_format) == (self.width, self.height, self._src_format): + return + + sws_context = _swscale.sws_getCachedContext( + self._sws_context, width, height, src_format, + width, height, AV_PIX_FMT_NV12, SWS_FAST_BILINEAR, + None, None, None, + ) + if not sws_context: + raise FFmpegError("sws_getCachedContext failed") + self._sws_context = ctypes.c_void_p(sws_context) + + self.width, self.height = width, height + self._src_format = src_format + self._output = np.empty(width * height * 3 // 2, dtype=np.uint8) + output_address = self._output.ctypes.data + self._dst_data = _DataArray( + ctypes.cast(output_address, ctypes.POINTER(ctypes.c_uint8)), + ctypes.cast(output_address + width * height, ctypes.POINTER(ctypes.c_uint8)), + None, + None, + ) + self._dst_linesize = _LinesizeArray(width, width, 0, 0) + + def _receive(self) -> np.ndarray | None: + """Return one NV12 frame, or None if the decoder needs more input. + + The returned buffer is reused on the next successful decode; callers must + use or copy it before calling decode again. + """ + result = _avcodec.avcodec_receive_frame(self._context, self._frame) + if result == -errno.EAGAIN: + return None + _check(result, "receive decoded frame") + + try: + frame = self._frame.contents + self._prepare_output(frame) + rows = _swscale.sws_scale( + self._sws_context, frame.data, frame.linesize, 0, frame.height, + self._dst_data, self._dst_linesize, + ) + if rows != frame.height: + raise FFmpegError(f"convert decoded frame: produced {rows} of {frame.height} rows") + return self._output + finally: + _avcodec.av_frame_unref(self._frame) + + def decode(self, data) -> np.ndarray | None: + self._ensure_open() + if len(data) == 0: + return None + + self._prepare_packet(data) + result = _avcodec.avcodec_send_packet(self._context, self._packet) + # The packet buffer is ours, not FFmpeg's. Clear the borrowed pointer so + # packet teardown can never attempt to release it. + self._packet.contents.data = None + self._packet.contents.size = 0 + _check(result, "send packet to decoder") + return self._receive() + + def reset(self) -> None: + """Discard decoder state after a stream discontinuity.""" + self._ensure_open() + _avcodec.avcodec_flush_buffers(self._context) + + def close(self) -> None: + if self.closed: + return + self.closed = True + _swscale.sws_freeContext(self._sws_context) + _avcodec.av_frame_free(ctypes.byref(self._frame)) + _avcodec.av_packet_free(ctypes.byref(self._packet)) + _avcodec.avcodec_free_context(ctypes.byref(self._context)) + + def __del__(self): + self.close() diff --git a/pyproject.toml b/pyproject.toml index de356e4b6f..3d90dfe167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,6 @@ dependencies = [ "comma-deps-git-lfs", "comma-deps-gcc-arm-none-eabi", - # body / webrtcd - "av", - # logging "pyzmq", "sentry-sdk", diff --git a/uv.lock b/uv.lock index d771c2aacb..6e14cbc02a 100644 --- a/uv.lock +++ b/uv.lock @@ -17,21 +17,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "av" -version = "16.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, - { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, - { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, -] - [[package]] name = "certifi" version = "2026.6.17" @@ -717,7 +702,6 @@ name = "openpilot" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "av" }, { name = "cffi" }, { name = "comma-deps-acados" }, { name = "comma-deps-bootstrap-icons" }, @@ -794,7 +778,6 @@ standalone = [ [package.metadata] requires-dist = [ - { name = "av" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, { name = "comma-deps-acados" }, From 1c07e0075914b09685683ebb2cd9cb8138ea822f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jul 2026 13:55:47 -0700 Subject: [PATCH 014/325] rm xattr (#38373) * rm xattr * doesn't add new coverage --- openpilot/system/loggerd/xattr_cache.py | 53 +++++++++++++++++++++++-- pyproject.toml | 1 - uv.lock | 20 ---------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/openpilot/system/loggerd/xattr_cache.py b/openpilot/system/loggerd/xattr_cache.py index 39bb172059..e0f6ba9588 100644 --- a/openpilot/system/loggerd/xattr_cache.py +++ b/openpilot/system/loggerd/xattr_cache.py @@ -1,6 +1,53 @@ +import ctypes import errno +import os +import sys -import xattr + +if sys.platform == "darwin": + _libc = ctypes.CDLL(None, use_errno=True) + _libc.getxattr.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_int] + _libc.getxattr.restype = ctypes.c_ssize_t + _libc.setxattr.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_int] + _libc.setxattr.restype = ctypes.c_int + + +def _raise_os_error(path: str) -> None: + error = ctypes.get_errno() + raise OSError(error, os.strerror(error), path) + + +def _getxattr(path: str, attr_name: str) -> bytes: + if sys.platform != "darwin": + return os.getxattr(path, attr_name) + + encoded_path = os.fsencode(path) + encoded_attr_name = os.fsencode(attr_name) + while True: + size = _libc.getxattr(encoded_path, encoded_attr_name, None, 0, 0, 0) + if size == -1: + _raise_os_error(path) + if size == 0: + return b"" + + value = ctypes.create_string_buffer(size) + result = _libc.getxattr(encoded_path, encoded_attr_name, value, size, 0, 0) + if result != -1: + return value.raw[:result] + if ctypes.get_errno() != errno.ERANGE: + _raise_os_error(path) + + +def _setxattr(path: str, attr_name: str, attr_value: bytes) -> None: + if sys.platform != "darwin": + os.setxattr(path, attr_name, attr_value) + return + + encoded_path = os.fsencode(path) + encoded_attr_name = os.fsencode(attr_name) + value = ctypes.create_string_buffer(attr_value) + if _libc.setxattr(encoded_path, encoded_attr_name, value, len(attr_value), 0, 0) == -1: + _raise_os_error(path) _cached_attributes: dict[tuple, bytes | None] = {} @@ -8,7 +55,7 @@ def getxattr(path: str, attr_name: str) -> bytes | None: key = (path, attr_name) if key not in _cached_attributes: try: - response = xattr.getxattr(path, attr_name) + response = _getxattr(path, attr_name) except OSError as e: # ENODATA (Linux) or ENOATTR (macOS) means attribute hasn't been set if e.errno == errno.ENODATA or (hasattr(errno, 'ENOATTR') and e.errno == errno.ENOATTR): @@ -20,4 +67,4 @@ def getxattr(path: str, attr_name: str) -> bytes | None: def setxattr(path: str, attr_name: str, attr_value: bytes) -> None: _cached_attributes.pop((path, attr_name), None) - xattr.setxattr(path, attr_name, attr_value) + _setxattr(path, attr_name, attr_value) diff --git a/pyproject.toml b/pyproject.toml index 3d90dfe167..c33b33c44c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ dependencies = [ # logging "pyzmq", "sentry-sdk", - "xattr", # used in place of 'os.getxattr' for macOS compatibility # athena "PyJWT[crypto]", diff --git a/uv.lock b/uv.lock index 6e14cbc02a..4ff0815783 100644 --- a/uv.lock +++ b/uv.lock @@ -734,7 +734,6 @@ dependencies = [ { name = "sounddevice" }, { name = "tqdm" }, { name = "websocket-client" }, - { name = "xattr" }, { name = "zstandard" }, ] @@ -830,7 +829,6 @@ requires-dist = [ { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, { name = "websocket-client" }, - { name = "xattr" }, { name = "zensical", marker = "extra == 'docs'" }, { name = "zstandard" }, ] @@ -1466,24 +1464,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] -[[package]] -name = "xattr" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/d5/25f7b19af3a2cb4000cac4f9e5525a40bec79f4f5d0ac9b517c0544586a0/xattr-1.3.0.tar.gz", hash = "sha256:30439fabd7de0787b27e9a6e1d569c5959854cb322f64ce7380fedbfa5035036", size = 17148, upload-time = "2025-10-13T22:16:47.353Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/78/00bdc9290066173e53e1e734d8d8e1a84a6faa9c66aee9df81e4d9aeec1c/xattr-1.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd4e63614722d183e81842cb237fd1cc978d43384166f9fe22368bfcb187ebe5", size = 23476, upload-time = "2025-10-13T22:16:06.942Z" }, - { url = "https://files.pythonhosted.org/packages/53/16/5243722294eb982514fa7b6b87a29dfb7b29b8e5e1486500c5babaf6e4b3/xattr-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:995843ef374af73e3370b0c107319611f3cdcdb6d151d629449efecad36be4c4", size = 18556, upload-time = "2025-10-13T22:16:08.209Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/d7ab0e547bea885b55f097206459bd612cefb652c5fc1f747130cbc0d42c/xattr-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa23a25220e29d956cedf75746e3df6cc824cc1553326d6516479967c540e386", size = 18869, upload-time = "2025-10-13T22:16:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/25/25cc7d64f07de644b7e9057842227adf61017e5bcfe59a79df79f768874c/xattr-1.3.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4345387087fffcd28f709eb45aae113d911e1a1f4f0f70d46b43ba81e69ccdd", size = 38797, upload-time = "2025-10-13T22:16:11.624Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/cc350bcdbed006dfcc6ade0ac817693b8b3d4b2787f20e427fd0697042e4/xattr-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe92bb05eb849ab468fe13e942be0f8d7123f15d074f3aba5223fad0c4b484de", size = 38956, upload-time = "2025-10-13T22:16:13.121Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b2/9416317ac89e2ed759a861857cda0d5e284c3691e6f460d36cc2bd5ce4d1/xattr-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c42ef5bdac3febbe28d3db14d3a8a159d84ba5daca2b13deae6f9f1fc0d4092", size = 38214, upload-time = "2025-10-13T22:16:14.389Z" }, - { url = "https://files.pythonhosted.org/packages/38/63/188f7cb41ab35d795558325d5cc8ab552171d5498cfb178fd14409651e18/xattr-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2aaa5d66af6523332189108f34e966ca120ff816dfa077ca34b31e6263f8a236", size = 37754, upload-time = "2025-10-13T22:16:15.306Z" }, -] - [[package]] name = "zensical" version = "0.0.46" From cefcf10ec31f76cbcd98663bf48d7f51bcf56b28 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 08:15:01 -0700 Subject: [PATCH 015/325] rm jinja2 --- pyproject.toml | 1 - uv.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c33b33c44c..d58991adf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,6 @@ dependencies = [ [project.optional-dependencies] docs = [ - "Jinja2", "zensical", ] diff --git a/uv.lock b/uv.lock index 4ff0815783..6d8fbba14d 100644 --- a/uv.lock +++ b/uv.lock @@ -742,7 +742,6 @@ dev = [ { name = "matplotlib" }, ] docs = [ - { name = "jinja2" }, { name = "zensical" }, ] submodules = [ @@ -799,7 +798,6 @@ requires-dist = [ { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, - { name = "jinja2", marker = "extra == 'docs'" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "msgq", marker = "extra == 'submodules'", editable = "msgq_repo" }, { name = "numpy", specifier = ">=2.0" }, From 7d74c3c99b440868490706d34442d1749c06416d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 08:25:31 -0700 Subject: [PATCH 016/325] rm setuptools (#38376) * rm setuptools * lock --- pyproject.toml | 1 - uv.lock | 2 -- 2 files changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d58991adf5..7ff0482b2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ dependencies = [ "scons", "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "Cython", - "setuptools", "numpy >=2.0", # vendored native dependencies diff --git a/uv.lock b/uv.lock index 6d8fbba14d..cb6f859b50 100644 --- a/uv.lock +++ b/uv.lock @@ -730,7 +730,6 @@ dependencies = [ { name = "scons" }, { name = "sentry-sdk" }, { name = "setproctitle" }, - { name = "setuptools" }, { name = "sounddevice" }, { name = "tqdm" }, { name = "websocket-client" }, @@ -820,7 +819,6 @@ requires-dist = [ { name = "scons" }, { name = "sentry-sdk" }, { name = "setproctitle" }, - { name = "setuptools" }, { name = "sounddevice" }, { name = "teleoprtc", marker = "extra == 'submodules'", editable = "teleoprtc_repo" }, { name = "tinygrad", marker = "extra == 'submodules'", editable = "tinygrad_repo" }, From e475d10adcc7aafc845f81cfb724e31cf5c3fb2b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 08:51:16 -0700 Subject: [PATCH 017/325] more ruff (#38377) * lil more ruff * no exclusions! * rm nb exception * and generated * not used * c408 * random * all * unittest is fine --- openpilot/cereal/messaging/__init__.py | 79 +++++++++++++------ .../messaging/tests/test_pub_sub_master.py | 3 +- openpilot/cereal/services.py | 6 +- openpilot/common/swaglog.py | 2 +- openpilot/selfdrive/car/tests/test_models.py | 2 +- .../tests/test_torqued_lat_accel_offset.py | 2 - openpilot/selfdrive/locationd/helpers.py | 3 +- .../selfdrive/locationd/test/test_lagd.py | 11 +-- openpilot/selfdrive/modeld/compile_modeld.py | 4 +- .../selfdrive/pandad/tests/test_pandad_spi.py | 4 +- openpilot/selfdrive/test/mem_usage.py | 2 +- .../selfdrive/ui/layouts/settings/settings.py | 4 +- .../selfdrive/ui/tests/profile_onroad.py | 2 +- openpilot/system/athena/tests/test_athenad.py | 2 +- .../system/loggerd/tests/test_uploader.py | 4 +- openpilot/system/ui/lib/shader_polygon.py | 4 +- openpilot/system/webrtc/webrtcd.py | 5 +- openpilot/tools/lib/url_file.py | 2 +- openpilot/tools/lib/vidindex.py | 2 +- openpilot/tools/replay/lib/ui_helpers.py | 4 +- .../sim/bridge/metadrive/metadrive_bridge.py | 60 +++++++------- pyproject.toml | 19 ++--- .../examples/find_segments_with_message.ipynb | 5 +- .../examples/ford_vin_fingerprint.ipynb | 9 ++- .../examples/hkg_canfd_gear_message.ipynb | 10 +-- tools/car_porting/test_car_model.py | 2 +- 26 files changed, 136 insertions(+), 116 deletions(-) diff --git a/openpilot/cereal/messaging/__init__.py b/openpilot/cereal/messaging/__init__.py index cb5f090095..dcc54baeb0 100644 --- a/openpilot/cereal/messaging/__init__.py +++ b/openpilot/cereal/messaging/__init__.py @@ -7,12 +7,43 @@ import os import capnp import time -from typing import Optional, List, Union, Dict +from typing import Union from openpilot.cereal import log from openpilot.cereal.services import SERVICE_LIST from openpilot.common.utils import MovingAverage +__all__ = ( + "NO_TRAVERSAL_LIMIT", + "Context", + "FrequencyTracker", + "IpcError", + "MultiplePublishersError", + "Poller", + "PubMaster", + "PubSocket", + "SocketEventHandle", + "SubMaster", + "SubSocket", + "delete_fake_prefix", + "drain_sock", + "drain_sock_raw", + "fake_event_handle", + "get_fake_prefix", + "log_from_bytes", + "new_message", + "pub_sock", + "recv_one", + "recv_one_or_none", + "recv_one_retry", + "recv_sock", + "reset_context", + "set_fake_prefix", + "sub_sock", + "toggle_fake_events", + "wait_for_one_event", +) + NO_TRAVERSAL_LIMIT = 2**64-1 @@ -22,8 +53,8 @@ def pub_sock(endpoint: str) -> PubSocket: return msgq.pub_sock(endpoint, segment_size) -def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1", - conflate: bool = False, timeout: Optional[int] = None) -> SubSocket: +def sub_sock(endpoint: str, poller: Poller | None = None, addr: str = "127.0.0.1", + conflate: bool = False, timeout: int | None = None) -> SubSocket: service = SERVICE_LIST.get(endpoint) segment_size = service.queue_size if service else 0 return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate, @@ -39,7 +70,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event return msg -def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder: +def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder: args = { 'valid': False, 'logMonoTime': int(time.monotonic() * 1e9), @@ -54,14 +85,14 @@ def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> return dat -def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> List[capnp.lib.capnp._DynamicStructReader]: +def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> list[capnp.lib.capnp._DynamicStructReader]: """Receive all message currently available on the queue""" msgs = drain_sock_raw(sock, wait_for_one=wait_for_one) return [log_from_bytes(m) for m in msgs] # TODO: print when we drop packets? -def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_sock(sock: SubSocket, wait: bool = False) -> capnp.lib.capnp._DynamicStructReader | None: """Same as drain sock, but only returns latest message. Consider using conflate instead.""" dat = None @@ -82,14 +113,14 @@ def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._ return dat -def recv_one(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_one(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None: dat = sock.receive() if dat is not None: dat = log_from_bytes(dat) return dat -def recv_one_or_none(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]: +def recv_one_or_none(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None: dat = sock.receive(non_blocking=True) if dat is not None: dat = log_from_bytes(dat) @@ -148,27 +179,27 @@ class FrequencyTracker: class SubMaster: - def __init__(self, services: List[str], poll: Optional[str] = None, - ignore_alive: Optional[List[str]] = None, ignore_avg_freq: Optional[List[str]] = None, - ignore_valid: Optional[List[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None): + def __init__(self, services: list[str], poll: str | None = None, + ignore_alive: list[str] | None = None, ignore_avg_freq: list[str] | None = None, + ignore_valid: list[str] | None = None, addr: str = "127.0.0.1", frequency: float | None = None): self.frame = -1 self.services = services - self.seen = {s: False for s in services} - self.updated = {s: False for s in services} - self.recv_time = {s: 0. for s in services} - self.recv_frame = {s: 0 for s in services} + self.seen = dict.fromkeys(services, False) + self.updated = dict.fromkeys(services, False) + self.recv_time = dict.fromkeys(services, 0.0) + self.recv_frame = dict.fromkeys(services, 0) self.sock = {} self.data = {} - self.logMonoTime = {s: 0 for s in services} + self.logMonoTime = dict.fromkeys(services, 0) # zero-frequency / on-demand services are always alive and presumed valid; all others must pass checks on_demand = {s: SERVICE_LIST[s].frequency <= 1e-5 for s in services} - self.static_freq_services = set(s for s in services if not on_demand[s]) + self.static_freq_services = {s for s in services if not on_demand[s]} self.alive = {s: on_demand[s] for s in services} self.freq_ok = {s: on_demand[s] for s in services} self.valid = {s: on_demand[s] for s in services} - self.freq_tracker: Dict[str, FrequencyTracker] = {} + self.freq_tracker: dict[str, FrequencyTracker] = {} self.poller = Poller() polled_services = set([poll, ] if poll is not None else services) self.non_polled_services = set(services) - polled_services @@ -211,7 +242,7 @@ class SubMaster: msgs.append(recv_one_or_none(self.sock[s])) self.update_msgs(time.monotonic(), msgs) - def update_msgs(self, cur_time: float, msgs: List[capnp.lib.capnp._DynamicStructReader]) -> None: + def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None: self.frame += 1 self.updated = dict.fromkeys(self.services, False) for msg in msgs: @@ -234,21 +265,21 @@ class SubMaster: self.alive[s] = (cur_time - self.recv_time[s]) < (10. / SERVICE_LIST[s].frequency) or (self.seen[s] and self.simulation) self.freq_ok[s] = self.freq_tracker[s].valid or self.simulation - def all_alive(self, service_list: Optional[List[str]] = None) -> bool: + def all_alive(self, service_list: list[str] | None = None) -> bool: return all(self.alive[s] for s in (service_list or self.services) if s not in self.ignore_alive) - def all_freq_ok(self, service_list: Optional[List[str]] = None) -> bool: + def all_freq_ok(self, service_list: list[str] | None = None) -> bool: return all(self.freq_ok[s] for s in (service_list or self.services) if self._check_avg_freq(s)) - def all_valid(self, service_list: Optional[List[str]] = None) -> bool: + def all_valid(self, service_list: list[str] | None = None) -> bool: return all(self.valid[s] for s in (service_list or self.services) if s not in self.ignore_valid) - def all_checks(self, service_list: Optional[List[str]] = None) -> bool: + def all_checks(self, service_list: list[str] | None = None) -> bool: return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list) class PubMaster: - def __init__(self, services: List[str]): + def __init__(self, services: list[str]): self.sock = {} for s in services: self.sock[s] = pub_sock(s) diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index eb8f62140b..02f944b69f 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -1,6 +1,7 @@ import random import time -from typing import Sized, cast +from typing import cast +from collections.abc import Sized import openpilot.cereal.messaging as messaging from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \ diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index 0f5e50f31f..db1731a986 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 from enum import IntEnum -from typing import Optional # TODO: this should be automatically determined using the capnp schema @@ -11,7 +10,7 @@ class QueueSize(IntEnum): class Service: - def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None, + def __init__(self, should_log: bool, frequency: float, decimation: int | None = None, queue_size: QueueSize = QueueSize.SMALL): self.should_log = should_log self.frequency = frequency @@ -112,8 +111,7 @@ def build_header(): for k, v in SERVICE_LIST.items(): should_log = "true" if v.should_log else "false" decimation = -1 if v.decimation is None else v.decimation - h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \ - (k, k, should_log, v.frequency, decimation, v.queue_size) + h += f' {{ "{k}", {{"{k}", {should_log}, {v.frequency:f}, {decimation:d}, {v.queue_size:d}}}}},\n' h += "};\n" h += "#endif\n" diff --git a/openpilot/common/swaglog.py b/openpilot/common/swaglog.py index ea72766fcb..ac64230eed 100644 --- a/openpilot/common/swaglog.py +++ b/openpilot/common/swaglog.py @@ -39,7 +39,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler): return stream def get_existing_logfiles(self): - log_files = list() + log_files = [] base_dir = os.path.dirname(self.base_filename) for fn in os.listdir(base_dir): fp = os.path.join(base_dir, fn) diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index ac2cd3ec7e..436de261e6 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -2,7 +2,7 @@ import time import os import pytest import random -import unittest # noqa: TID251 +import unittest from collections import defaultdict, Counter import hypothesis.strategies as st from hypothesis import Phase, given, settings diff --git a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py index 93a57d7f60..7a63da23a5 100644 --- a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -7,8 +7,6 @@ from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS -np.random.seed(0) - LA_ERR_STD = 1.0 INPUT_NOISE_STD = 0.08 V_EGO = 30.0 diff --git a/openpilot/selfdrive/locationd/helpers.py b/openpilot/selfdrive/locationd/helpers.py index fe7930c509..849b950de2 100644 --- a/openpilot/selfdrive/locationd/helpers.py +++ b/openpilot/selfdrive/locationd/helpers.py @@ -69,6 +69,7 @@ class NPQueue: class PointBuckets: def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + self._rng = np.random.default_rng() self.x_bounds = x_bounds self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds} self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True)) @@ -98,7 +99,7 @@ class PointBuckets: points = np.vstack([x.arr for x in self.buckets.values()]) if num_points is None: return points - return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] + return points[self._rng.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] def load_points(self, points: list[list[float]]) -> None: for point in points: diff --git a/openpilot/selfdrive/locationd/test/test_lagd.py b/openpilot/selfdrive/locationd/test/test_lagd.py index 128c19332f..efaba6ead9 100644 --- a/openpilot/selfdrive/locationd/test/test_lagd.py +++ b/openpilot/selfdrive/locationd/test/test_lagd.py @@ -81,6 +81,7 @@ class TestLagd: assert retrieve_initial_lag(params, CP) is None def test_ncc(self): + rng = np.random.default_rng() lag_frames = random.randint(1, 19) desired_sig = np.sin(np.arange(0.0, 10.0, 0.1)) @@ -91,15 +92,15 @@ class TestLagd: assert np.argmax(corr) == lag_frames # add some noise - desired_sig += np.random.normal(0, 0.05, len(desired_sig)) - actual_sig += np.random.normal(0, 0.05, len(actual_sig)) + desired_sig += rng.normal(0, 0.05, len(desired_sig)) + actual_sig += rng.normal(0, 0.05, len(actual_sig)) corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20] assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1) # mask out 40% of the values, and make them noise - mask = np.random.choice([True, False], size=len(desired_sig), p=[0.6, 0.4]) - desired_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask)) - actual_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask)) + mask = rng.choice([True, False], size=len(desired_sig), p=[0.6, 0.4]) + desired_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask)) + actual_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask)) corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20] assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1) diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index 769fb69eb3..f74ab27b8c 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -242,7 +242,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues): SEED = 42 def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): input_queues, npy = make_queues(Device.DEFAULT) - np.random.seed(seed) + rng = np.random.default_rng(seed) Tensor.manual_seed(seed) testing = test_val is not None or test_buffers is not None @@ -250,7 +250,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues): for i in range(n_runs): for v in npy.values(): - v[:] = np.random.randn(*v.shape).astype(v.dtype) + v[:] = rng.standard_normal(v.shape).astype(v.dtype) Device.default.synchronize() random_inputs = make_random_inputs() st = time.perf_counter() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py index 02f5accfd6..2bc302b47c 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -38,10 +38,10 @@ class TestBoarddSpi: total_recv_count = 0 total_sent_count = 0 - sent_msgs = {bus: list() for bus in range(3)} + sent_msgs = {bus: [] for bus in range(3)} st = time.monotonic() - ts = {s: list() for s in socks.keys()} + ts = {s: [] for s in socks.keys()} for _ in range(int(os.getenv("TEST_TIME", "20"))): # send some CAN messages if not JUNGLE_SPAM: diff --git a/openpilot/selfdrive/test/mem_usage.py b/openpilot/selfdrive/test/mem_usage.py index 1446d0b4dc..02b7934466 100644 --- a/openpilot/selfdrive/test/mem_usage.py +++ b/openpilot/selfdrive/test/mem_usage.py @@ -7,7 +7,7 @@ from openpilot.common.utils import tabulate DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496" MB = 1024 * 1024 -TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center") +TABULATE_OPTS = {"tablefmt": "simple_grid", "stralign": "center", "numalign": "center"} def _get_procs(): diff --git a/openpilot/selfdrive/ui/layouts/settings/settings.py b/openpilot/selfdrive/ui/layouts/settings/settings.py index 68f45df77d..48b75e5dbd 100644 --- a/openpilot/selfdrive/ui/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/layouts/settings/settings.py @@ -1,5 +1,5 @@ import pyray as rl -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import IntEnum from collections.abc import Callable from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout @@ -43,7 +43,7 @@ class PanelType(IntEnum): class PanelInfo: name: str instance: Widget - button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + button_rect: rl.Rectangle = field(default_factory=lambda: rl.Rectangle(0, 0, 0, 0)) class SettingsLayout(Widget): diff --git a/openpilot/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py index 18194d7363..29e44d6260 100755 --- a/openpilot/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -92,7 +92,7 @@ if __name__ == "__main__": 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() + yuv_data = np.random.default_rng().integers(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() with cProfile.Profile() as pr: for _ in gui_app.render(): if ui_state.sm.frame >= len(message_chunks): diff --git a/openpilot/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py index 253a057b91..accf0784ab 100644 --- a/openpilot/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -437,7 +437,7 @@ class TestAthenadMethods: thread.join() def test_get_logs_to_send_sorted(self): - fl = list() + fl = [] for i in range(10): file = f'swaglog.{i:010}' self._create_file(file, Paths.swaglog_root()) diff --git a/openpilot/system/loggerd/tests/test_uploader.py b/openpilot/system/loggerd/tests/test_uploader.py index eec85eda2e..3af78e2c37 100644 --- a/openpilot/system/loggerd/tests/test_uploader.py +++ b/openpilot/system/loggerd/tests/test_uploader.py @@ -18,8 +18,8 @@ class FakeLogHandler(logging.Handler): self.reset() def reset(self): - self.upload_order = list() - self.upload_ignored = list() + self.upload_order = [] + self.upload_ignored = [] def emit(self, record): try: diff --git a/openpilot/system/ui/lib/shader_polygon.py b/openpilot/system/ui/lib/shader_polygon.py index 94af35e157..de729d6aae 100644 --- a/openpilot/system/ui/lib/shader_polygon.py +++ b/openpilot/system/ui/lib/shader_polygon.py @@ -152,7 +152,7 @@ class ShaderState: self.initialized = False -def _configure_shader_color(state: ShaderState, color: Optional[rl.Color], +def _configure_shader_color(state: ShaderState, color: Optional[rl.Color], # noqa: UP045 # rl.Color is a function, so `rl.Color | None` fails gradient: Gradient | None, origin_rect: rl.Rectangle): assert (color is not None) != (gradient is not None), "Either color or gradient must be provided" @@ -204,7 +204,7 @@ def triangulate(pts: np.ndarray) -> list[tuple[float, float]]: def draw_polygon(origin_rect: rl.Rectangle, points: np.ndarray, - color: Optional[rl.Color] = None, gradient: Gradient | None = None): + color: Optional[rl.Color] = None, gradient: Gradient | None = None): # noqa: UP045 # rl.Color is a function, so `rl.Color | None` fails """ Draw a ribbon polygon (two chains) with a triangle strip and gradient. diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 8276df98f2..82ebb7b018 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -566,13 +566,14 @@ def webrtcd_thread(host: str, port: int, debug: bool): http_thread.start() shutting_down = False + shutdown_task = None def request_shutdown() -> None: - nonlocal shutting_down + nonlocal shutting_down, shutdown_task if shutting_down: return shutting_down = True - loop.create_task(_shutdown(server, state, loop)) + shutdown_task = loop.create_task(_shutdown(server, state, loop)) for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, request_shutdown) diff --git a/openpilot/tools/lib/url_file.py b/openpilot/tools/lib/url_file.py index 3f72429b94..ec0a3d5815 100644 --- a/openpilot/tools/lib/url_file.py +++ b/openpilot/tools/lib/url_file.py @@ -21,7 +21,7 @@ logging.getLogger("urllib3").setLevel(logging.WARNING) def hash_url(link: str) -> str: - return md5((link.split("?")[0]).encode('utf-8')).hexdigest() + return md5(link.split("?", maxsplit=1)[0].encode('utf-8')).hexdigest() def prune_cache(new_entry: str | None = None) -> None: diff --git a/openpilot/tools/lib/vidindex.py b/openpilot/tools/lib/vidindex.py index 4aef6fb4d5..b200700887 100755 --- a/openpilot/tools/lib/vidindex.py +++ b/openpilot/tools/lib/vidindex.py @@ -269,7 +269,7 @@ def hevc_index(hevc_file_name: str, allow_corrupt: bool=False) -> tuple[list, in raise VideoFileInvalid("first byte must be 0x00") prefix_dat = b"" - frame_types = list() + frame_types = [] i = 1 # skip past first byte 0x00 try: diff --git a/openpilot/tools/replay/lib/ui_helpers.py b/openpilot/tools/replay/lib/ui_helpers.py index 6a3e8c20ed..8f0c14983f 100644 --- a/openpilot/tools/replay/lib/ui_helpers.py +++ b/openpilot/tools/replay/lib/ui_helpers.py @@ -122,8 +122,8 @@ def init_plots(arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_co title_texts = [] for j2, (nm, cl) in enumerate(zip(pl_list, plot_colors[i], strict=False)): if j2 > 0: - title_texts.append(TextArea(", ", textprops=dict(color="white", fontsize=10))) - title_texts.append(TextArea(nm, textprops=dict(color=label_palette[cl], fontsize=10))) + title_texts.append(TextArea(", ", textprops={"color": "white", "fontsize": 10})) + title_texts.append(TextArea(nm, textprops={"color": label_palette[cl], "fontsize": 10})) packed = HPacker(children=title_texts, pad=0, sep=0) ab = AnchoredOffsetbox(loc='lower center', child=packed, bbox_to_anchor=(0.5, 1.0), bbox_transform=axs[i].transAxes, frameon=False, pad=0) diff --git a/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py b/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py index b8dc94cf86..4222ba45a7 100644 --- a/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py +++ b/openpilot/tools/sim/bridge/metadrive/metadrive_bridge.py @@ -29,11 +29,11 @@ def curve_block(length, angle=45, direction=0): def create_map(track_size=60): curve_len = track_size * 2 - return dict( - type=MapGenerateMethod.PG_MAP_FILE, - lane_num=2, - lane_width=4.5, - config=[ + return { + "type": MapGenerateMethod.PG_MAP_FILE, + "lane_num": 2, + "lane_width": 4.5, + "config": [ None, straight_block(track_size), curve_block(curve_len, 90), @@ -44,7 +44,7 @@ def create_map(track_size=60): straight_block(track_size), curve_block(curve_len, 90), ] - ) + } class MetaDriveBridge(SimulatorBridge): @@ -65,29 +65,29 @@ class MetaDriveBridge(SimulatorBridge): if self.dual_camera: sensors["rgb_wide"] = (RGBCameraWide, W, H) - config = dict( - use_render=self.should_render, - vehicle_config=dict( - enable_reverse=False, - render_vehicle=False, - image_source="rgb_road", - ), - sensors=sensors, - image_on_cuda=_cuda_enable, - image_observation=True, - interface_panel=[], - out_of_route_done=False, - on_continuous_line_done=False, - crash_vehicle_done=False, - crash_object_done=False, - arrive_dest_done=False, - traffic_density=0.0, # traffic is incredibly expensive - map_config=create_map(), - decision_repeat=1, - physics_world_step_size=self.TICKS_PER_FRAME/100, - preload_models=False, - show_logo=False, - anisotropic_filtering=False - ) + config = { + "use_render": self.should_render, + "vehicle_config": { + "enable_reverse": False, + "render_vehicle": False, + "image_source": "rgb_road", + }, + "sensors": sensors, + "image_on_cuda": _cuda_enable, + "image_observation": True, + "interface_panel": [], + "out_of_route_done": False, + "on_continuous_line_done": False, + "crash_vehicle_done": False, + "crash_object_done": False, + "arrive_dest_done": False, + "traffic_density": 0.0, # traffic is incredibly expensive + "map_config": create_map(), + "decision_repeat": 1, + "physics_world_step_size": self.TICKS_PER_FRAME/100, + "preload_models": False, + "show_logo": False, + "anisotropic_filtering": False + } return MetaDriveWorld(queue, config, self.test_duration, self.test_run, self.dual_camera) diff --git a/pyproject.toml b/pyproject.toml index 7ff0482b2f..597f7a4489 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,33 +152,26 @@ lint.select = [ "E", "F", "W", "PIE", "C4", "ISC", "A", "B", "NPY", # numpy "UP", # pyupgrade + "ASYNC", + "B904", "B905", + "PLC0207", "TRY203", "TRY400", "TRY401", # try/excepts - "RUF008", "RUF100", + "RUF006", "RUF008", "RUF009", "RUF061", "RUF064", "RUF100", "RUF102", "RUF103", "RUF104", "TID251", "PLE", "PLR1704", ] lint.ignore = [ "E741", "E402", - "C408", - "ISC003", "B027", - "B024", - "NPY002", # new numpy random syntax is worse - "UP045", "UP007", # these don't play nice with raylib atm + "UP007", # this doesn't play nice with raylib atm ] line-length = 160 -exclude = [ - "openpilot/cereal", - "*.ipynb", - "generated", -] lint.flake8-implicit-str-concat.allow-multiline = false [tool.ruff.lint.flake8-tidy-imports.banned-api] "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" -"unittest".msg = "Use pytest" -"time.time".msg = "Use time.monotonic" +"time.time".msg = "Use time.monotonic" # time.time can skip due to its reference clock, you probably want a monotonic clock # raylib banned APIs "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" diff --git a/tools/car_porting/examples/find_segments_with_message.ipynb b/tools/car_porting/examples/find_segments_with_message.ipynb index af17bde52b..f91827f7e5 100644 --- a/tools/car_porting/examples/find_segments_with_message.ipynb +++ b/tools/car_porting/examples/find_segments_with_message.ipynb @@ -9,7 +9,6 @@ "source": [ "# Import all cars from opendbc\n", "\n", - "from opendbc.car import structs\n", "from opendbc.car.values import PLATFORMS as TEST_PLATFORMS\n", "\n", "# Example: add additional platforms/segments to test outside of commaCarSegments\n", @@ -147,8 +146,8 @@ } ], "source": [ - "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", - "from tqdm.notebook import tqdm, tnrange\n", + "from openpilot.tools.lib.logreader import comma_car_segments_source\n", + "from tqdm.notebook import tnrange\n", "\n", "# Example search for CAN ignition messages\n", "# Be careful when filtering by bus, account for odd harness arrangements on Honda/HKG\n", diff --git a/tools/car_porting/examples/ford_vin_fingerprint.ipynb b/tools/car_porting/examples/ford_vin_fingerprint.ipynb index 6b806d22d2..cb5fd8f820 100644 --- a/tools/car_porting/examples/ford_vin_fingerprint.ipynb +++ b/tools/car_porting/examples/ford_vin_fingerprint.ipynb @@ -53,12 +53,12 @@ " if vin.startswith('1FT'):\n", " if vin_positions_567 in F150_CODES:\n", " if vin[7] in LIGHTNING_CODES:\n", - " return f\"FORD F-150 LIGHTNING 1ST GEN\"\n", + " return \"FORD F-150 LIGHTNING 1ST GEN\"\n", " else:\n", - " return f\"FORD F-150 14TH GEN\"\n", + " return \"FORD F-150 14TH GEN\"\n", " elif vin.startswith('3FM'):\n", " if vin_positions_567 in MACHE_CODES:\n", - " return f\"FORD MUSTANG MACH-E 1ST GEN\"\n", + " return \"FORD MUSTANG MACH-E 1ST GEN\"\n", " elif vin.startswith('5LM'):\n", " pass\n", "\n", @@ -147,7 +147,8 @@ "source": [ "for vin, real_fingerprint in VINS_TO_CHECK:\n", " determined_fingerprint = ford_vin_fingerprint(vin)\n", - " print(f\"vin: {vin} real platform: {real_fingerprint: <30} determined platform: {determined_fingerprint: <30} correct: {real_fingerprint == determined_fingerprint}\")" + " print(f\"vin: {vin} real platform: {real_fingerprint: <30} \" +\n", + " f\"determined platform: {determined_fingerprint: <30} correct: {real_fingerprint == determined_fingerprint}\")" ] } ], diff --git a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb index f0bca8decc..ec902b7d10 100644 --- a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb +++ b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb @@ -21,9 +21,7 @@ } ], "source": [ - "from opendbc.car import structs\n", "from opendbc.car.hyundai.values import CAR, HyundaiFlags\n", - "from opendbc.car.hyundai.fingerprints import FW_VERSIONS\n", "\n", "TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) & set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD electric vehicles only\n", "#TEST_PLATFORMS = set(CAR.with_flags(HyundaiFlags.CANFD)) - set(CAR.with_flags(HyundaiFlags.EV)) # CAN-FD hybrid and ICE vehicles only\n", @@ -190,15 +188,13 @@ ], "source": [ "import copy\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "\n", "from opendbc.can.parser import CANParser\n", "from opendbc.car.hyundai.values import DBC\n", "from opendbc.car.hyundai.hyundaicanfd import CanBus\n", "\n", "from openpilot.selfdrive.pandad import can_capnp_to_list\n", - "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", + "from openpilot.tools.lib.logreader import comma_car_segments_source\n", "\n", "message_names = [\"GEAR_SHIFTER\", \"ACCELERATOR\", \"GEAR\", \"GEAR_ALT\", \"GEAR_ALT_2\"]\n", "\n", @@ -229,11 +225,11 @@ " for i, parsed_messages in enumerate(parsed_message_history):\n", " gear = parsed_messages[name][\"GEAR\"]\n", " if gear != gear_prev:\n", - " print(f\" *** Signal transition found! ***\")\n", + " print(\" *** Signal transition found! ***\")\n", " examples.append(i)\n", " gear_prev = gear\n", "\n", - "print(f\"Analysis finished\")\n" + "print(\"Analysis finished\")\n" ] }, { diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py index 20e7d136ea..61ec2e15ed 100755 --- a/tools/car_porting/test_car_model.py +++ b/tools/car_porting/test_car_model.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse import sys -import unittest # noqa: TID251 +import unittest from opendbc.car.tests.routes import CarTestRoute from openpilot.selfdrive.car.tests.test_models import TestCarModel from openpilot.tools.lib.route import SegmentRange From ecac2d386b7f8cb2f17e75dc046c78a2f70f2fcd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 09:02:02 -0700 Subject: [PATCH 018/325] these go in tools --- pyproject.toml | 9 ++++----- uv.lock | 21 +++++++++------------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 597f7a4489..da04c77f1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,12 @@ dependencies = [ "numpy >=2.0", # vendored native dependencies - "comma-deps-bzip2", - "comma-deps-bootstrap-icons", "comma-deps-capnproto", "comma-deps-catch2", "comma-deps-acados", "comma-deps-ffmpeg", "comma-deps-zstd", - "comma-deps-ncurses", "comma-deps-zeromq", - "comma-deps-libusb", "comma-deps-json11", "comma-deps-git-lfs", "comma-deps-gcc-arm-none-eabi", @@ -86,6 +82,10 @@ dev = [ tools = [ "comma-deps-imgui", + "comma-deps-bzip2", + "comma-deps-bootstrap-icons", + "comma-deps-libusb", + "comma-deps-ncurses", # this can be added back once it's stripped down some more #"metadrive-simulator @ git+https://github.com/commaai/metadrive.git@minimal ; (platform_machine != 'aarch64')", @@ -200,7 +200,6 @@ python-preference = "only-managed" default-groups = ["standalone"] override-dependencies = [ "opendbc", # panda pins opendbc from git for standalone use; always use our submodule - "av", # teleoprtc's av<13 pin is stale ] [tool.uv.sources] diff --git a/uv.lock b/uv.lock index cb6f859b50..9aef9c9a01 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,7 @@ revision = 3 requires-python = ">=3.12.3, <3.13" [manifest] -overrides = [ - { name = "av" }, - { name = "opendbc", editable = "opendbc_repo" }, -] +overrides = [{ name = "opendbc", editable = "opendbc_repo" }] [[package]] name = "attrs" @@ -704,16 +701,12 @@ source = { editable = "." } dependencies = [ { name = "cffi" }, { name = "comma-deps-acados" }, - { name = "comma-deps-bootstrap-icons" }, - { name = "comma-deps-bzip2" }, { name = "comma-deps-capnproto" }, { name = "comma-deps-catch2" }, { name = "comma-deps-ffmpeg" }, { name = "comma-deps-gcc-arm-none-eabi" }, { name = "comma-deps-git-lfs" }, { name = "comma-deps-json11" }, - { name = "comma-deps-libusb" }, - { name = "comma-deps-ncurses" }, { name = "comma-deps-raylib" }, { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, @@ -765,7 +758,11 @@ testing = [ { name = "ty" }, ] tools = [ + { name = "comma-deps-bootstrap-icons" }, + { name = "comma-deps-bzip2" }, { name = "comma-deps-imgui" }, + { name = "comma-deps-libusb" }, + { name = "comma-deps-ncurses" }, ] [package.dev-dependencies] @@ -778,8 +775,8 @@ requires-dist = [ { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, { name = "comma-deps-acados" }, - { name = "comma-deps-bootstrap-icons" }, - { name = "comma-deps-bzip2" }, + { name = "comma-deps-bootstrap-icons", marker = "extra == 'tools'" }, + { name = "comma-deps-bzip2", marker = "extra == 'tools'" }, { name = "comma-deps-capnproto" }, { name = "comma-deps-catch2" }, { name = "comma-deps-ffmpeg" }, @@ -787,8 +784,8 @@ requires-dist = [ { name = "comma-deps-git-lfs" }, { name = "comma-deps-imgui", marker = "extra == 'tools'" }, { name = "comma-deps-json11" }, - { name = "comma-deps-libusb" }, - { name = "comma-deps-ncurses" }, + { name = "comma-deps-libusb", marker = "extra == 'tools'" }, + { name = "comma-deps-ncurses", marker = "extra == 'tools'" }, { name = "comma-deps-raylib" }, { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, From 19ecc37de83f44dd2d5e352beb701369430a8915 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 09:45:46 -0700 Subject: [PATCH 019/325] more ty (#38378) * enable no matching overload * enable call non callable * enable unsupported-operator * enable not subscriptable * refactor pass --- .../cereal/messaging/tests/test_pub_sub_master.py | 1 + openpilot/common/swaglog.py | 2 +- openpilot/selfdrive/car/tests/test_models.py | 2 +- .../locationd/test/test_locationd_scenarios.py | 4 ++-- .../selfdrive/ui/mici/layouts/offroad_alerts.py | 13 +++++++------ openpilot/selfdrive/ui/mici/widgets/button.py | 3 +++ .../ui/translations/update_translations.py | 8 ++++---- openpilot/system/loggerd/tests/test_loggerd.py | 4 +++- openpilot/system/qcomgpsd/nmeaport.py | 8 +++++--- openpilot/system/ui/lib/application.py | 1 + openpilot/system/ui/widgets/network.py | 1 + openpilot/tools/clip/run.py | 11 ++++++----- openpilot/tools/lib/comma_car_segments.py | 2 +- .../tools/sim/bridge/metadrive/metadrive_world.py | 2 +- pyproject.toml | 4 ---- 15 files changed, 37 insertions(+), 29 deletions(-) diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index 02f944b69f..20ff855fda 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -92,6 +92,7 @@ class TestSubMaster: for service, (max_freq, min_freq) in checks.items(): if max_freq is not None: + assert min_freq is not None assert sm._check_avg_freq(service) assert sm.freq_tracker[service].max_freq == max_freq*1.2 assert sm.freq_tracker[service].min_freq == min_freq*0.8 diff --git a/openpilot/common/swaglog.py b/openpilot/common/swaglog.py index ac64230eed..8b629b3fba 100644 --- a/openpilot/common/swaglog.py +++ b/openpilot/common/swaglog.py @@ -27,7 +27,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler): self.log_files = self.get_existing_logfiles() log_indexes = [f.split(".")[-1] for f in self.log_files] self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1]) - self.last_rollover = None + self.last_rollover = 0.0 self.doRollover() def _open(self): diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index 436de261e6..cda94b3158 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -250,7 +250,7 @@ class TestCarModelBase(unittest.TestCase): # Don't check relay malfunction on disabled routes (relay closed), # or before fingerprinting is done (elm327 and noOutput) - if self.openpilot_enabled and t / 1e4 > self.car_safety_mode_frame: + if self.car_safety_mode_frame is not None and t / 1e4 > self.car_safety_mode_frame: self.assertFalse(self.safety.get_relay_malfunction()) else: self.safety.set_relay_malfunction(False) diff --git a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py index 69f2ca2821..04d2d6b55b 100644 --- a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py +++ b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py @@ -37,9 +37,9 @@ def get_select_fields_data(logs): def sig_smooth(signal): return masked_symmetric_moving_average(signal, np.ones_like(signal), 5, 1.0) def get_nested_keys(msg, keys): - val = None + val = msg for key in keys: - val = getattr(msg if val is None else val, key) if isinstance(key, str) else val[key] + val = getattr(val, key) if isinstance(key, str) else val[key] return val lp = [x.livePose for x in logs if x.which() == 'livePose'] data = defaultdict(list) diff --git a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py index 47a2ddde95..c27e05f752 100644 --- a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -250,12 +250,12 @@ class MiciOffroadAlerts(Scroller): {alert_data.key: self.params.get(alert_data.key) for alert_data in self.sorted_alerts}) time.sleep(REFRESH_INTERVAL) - def _refresh(self) -> int: + def _refresh(self, pending_params: dict) -> int: """Refresh alerts from params and return active count.""" active_count = 0 # Handle UpdateAvailable alert specially - update_available = self._pending_params["UpdateAvailable"] + update_available = pending_params["UpdateAvailable"] update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None) if update_alert_data: @@ -263,7 +263,7 @@ class MiciOffroadAlerts(Scroller): version_string = "" # Get new version description and parse version and date - new_desc = self._pending_params["UpdaterNewDescription"] or "" + new_desc = pending_params["UpdaterNewDescription"] or "" if new_desc: # format: "version / branch / commit / date" parts = new_desc.split(" / ") @@ -284,7 +284,7 @@ class MiciOffroadAlerts(Scroller): continue # Skip, already handled above text = "" - alert_json = self._pending_params[alert_data.key] + alert_json = pending_params[alert_data.key] if alert_json: text = alert_json.get("text", "").replace("%1", alert_json.get("extra", "")) @@ -311,8 +311,9 @@ class MiciOffroadAlerts(Scroller): def _update_state(self): """Periodically refresh alerts.""" # Refresh alerts when thread updates params - if self._pending_params is not None: - self._refresh() + pending_params = self._pending_params + if pending_params is not None: + self._refresh(pending_params) self._pending_params = None def _render(self, rect: rl.Rectangle): diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index 1dceb79691..3dd7ad9a8a 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -375,6 +375,7 @@ class GreyBigButton(BigButton): class BigMultiParamToggle(BigMultiToggle): def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None, select_callback: Callable | None = None): + assert Params is not None super().__init__(text, options, toggle_callback, select_callback) self._param = param @@ -392,6 +393,7 @@ class BigMultiParamToggle(BigMultiToggle): class BigParamControl(BigToggle): def __init__(self, text: str, param: str, toggle_callback: Callable | None = None): + assert Params is not None super().__init__(text, "", toggle_callback=toggle_callback) self.param = param self.params = Params() @@ -409,6 +411,7 @@ class BigParamControl(BigToggle): class BigCircleParamControl(BigCircleToggle): def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None, icon_offset: tuple[int, int] = (0, 0)): + assert Params is not None super().__init__(icon, toggle_callback, icon_offset=icon_offset) self._param = param self.params = Params() diff --git a/openpilot/selfdrive/ui/translations/update_translations.py b/openpilot/selfdrive/ui/translations/update_translations.py index 6ff3667d8a..9b0d63ee12 100755 --- a/openpilot/selfdrive/ui/translations/update_translations.py +++ b/openpilot/selfdrive/ui/translations/update_translations.py @@ -12,9 +12,9 @@ POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot") def update_translations(): files = [] for root, _, filenames in chain(os.walk(SYSTEM_UI_DIR), - os.walk(os.path.join(UI_DIR, "widgets")), - os.walk(os.path.join(UI_DIR, "layouts")), - os.walk(os.path.join(UI_DIR, "onroad"))): + os.walk(os.path.join(str(UI_DIR), "widgets")), + os.walk(os.path.join(str(UI_DIR), "layouts")), + os.walk(os.path.join(str(UI_DIR), "onroad"))): for filename in filenames: if filename.endswith(".py"): files.append(os.path.relpath(os.path.join(root, filename), BASEDIR)) @@ -25,7 +25,7 @@ def update_translations(): # Generate/update translation files for each language for name in multilang.languages.values(): - po_file = os.path.join(TRANSLATIONS_DIR, f"app_{name}.po") + po_file = os.path.join(str(TRANSLATIONS_DIR), f"app_{name}.po") if os.path.exists(po_file): merge_po(po_file, POT_FILE) else: diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 5d8f635d96..666b56dc03 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -277,7 +277,9 @@ class TestLoggerd: assert recv_cnt == 0, f"got {recv_cnt} {s} msgs in qlog" else: # check logged message count matches decimation - expected_cnt = (len(msgs) - 1) // SERVICE_LIST[s].decimation + 1 + decimation = SERVICE_LIST[s].decimation + assert decimation is not None + expected_cnt = (len(msgs) - 1) // decimation + 1 assert recv_cnt == expected_cnt, f"expected {expected_cnt} msgs for {s}, got {recv_cnt}" def test_rlog(self): diff --git a/openpilot/system/qcomgpsd/nmeaport.py b/openpilot/system/qcomgpsd/nmeaport.py index 0e695b65e5..ba7cf117b1 100644 --- a/openpilot/system/qcomgpsd/nmeaport.py +++ b/openpilot/system/qcomgpsd/nmeaport.py @@ -3,7 +3,7 @@ import sys from dataclasses import dataclass, fields from subprocess import check_output, CalledProcessError from time import sleep -from typing import NoReturn +from typing import NoReturn, cast DEBUG = int(os.environ.get("DEBUG", "0")) @@ -30,7 +30,8 @@ class GnssClockNmeaPort: def __post_init__(self): for field in fields(self): val = getattr(self, field.name) - setattr(self, field.name, field.type(val) if val else None) + field_type = cast(type, field.type) + setattr(self, field.name, field_type(val) if val else None) @dataclass class GnssMeasNmeaPort: @@ -73,7 +74,8 @@ class GnssMeasNmeaPort: def __post_init__(self): for field in fields(self): val = getattr(self, field.name) - setattr(self, field.name, field.type(val) if val else None) + field_type = cast(type, field.type) + setattr(self, field.name, field_type(val) if val else None) def nmea_checksum_ok(s): checksum = 0 diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index bd2c9dfcd3..3fa47ebd20 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -823,6 +823,7 @@ class GuiApplication: import pstats self._render_profiler.disable() + assert self._render_profile_start_time is not None elapsed_ms = (time.monotonic() - self._render_profile_start_time) * 1e3 avg_frame_time = elapsed_ms / self._frame if self._frame > 0 else 0 diff --git a/openpilot/system/ui/widgets/network.py b/openpilot/system/ui/widgets/network.py index f104ba6add..710202c54f 100644 --- a/openpilot/system/ui/widgets/network.py +++ b/openpilot/system/ui/widgets/network.py @@ -105,6 +105,7 @@ class NetworkUI(Widget): class AdvancedNetworkSettings(Widget): def __init__(self, wifi_manager: WifiManager): + assert Params is not None super().__init__() self._wifi_manager = wifi_manager self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) diff --git a/openpilot/tools/clip/run.py b/openpilot/tools/clip/run.py index fa4b203862..594383f7fd 100755 --- a/openpilot/tools/clip/run.py +++ b/openpilot/tools/clip/run.py @@ -11,6 +11,7 @@ import itertools import numpy as np import tqdm from argparse import ArgumentParser +from collections.abc import Callable from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed @@ -138,7 +139,7 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal frames_per_seg = fps * 60 start_frame, end_frame = int(start_time * fps), int(end_time * fps) current_seg: int = -1 - seg_frames: FrameReader | np.ndarray | None = None + get_frame: Callable[[int], np.ndarray] | None = None for global_idx in range(start_frame, end_frame): seg_idx, local_idx = global_idx // frames_per_seg, global_idx % frames_per_seg @@ -157,12 +158,12 @@ def iter_segment_frames(camera_paths, start_time, end_time, fps=20, use_qcam=Fal if result.returncode != 0: raise RuntimeError(f"ffmpeg failed: {result.stderr.decode()}") seg_frames = np.frombuffer(result.stdout, dtype=np.uint8).reshape(-1, w * h * 3 // 2) + get_frame = seg_frames.__getitem__ else: - seg_frames = FrameReader(path, pix_fmt="nv12") + get_frame = FrameReader(path, pix_fmt="nv12").get - assert seg_frames is not None - frame = seg_frames[local_idx] if use_qcam else seg_frames.get(local_idx) - yield global_idx, frame + assert get_frame is not None + yield global_idx, get_frame(local_idx) class FrameQueue: diff --git a/openpilot/tools/lib/comma_car_segments.py b/openpilot/tools/lib/comma_car_segments.py index cd19356d66..b27887ed29 100644 --- a/openpilot/tools/lib/comma_car_segments.py +++ b/openpilot/tools/lib/comma_car_segments.py @@ -74,7 +74,7 @@ def get_repo_url(path): response = requests.head(get_repo_raw_url(path)) - if "text/plain" in response.headers.get("content-type"): + if "text/plain" in response.headers.get("content-type", ""): # This is an LFS pointer, so download the raw data from lfs response = requests.get(get_repo_raw_url(path)) assert response.status_code == 200 diff --git a/openpilot/tools/sim/bridge/metadrive/metadrive_world.py b/openpilot/tools/sim/bridge/metadrive/metadrive_world.py index c5111289d0..54b461a46c 100644 --- a/openpilot/tools/sim/bridge/metadrive/metadrive_world.py +++ b/openpilot/tools/sim/bridge/metadrive/metadrive_world.py @@ -97,7 +97,7 @@ class MetaDriveWorld(World): self.op_engaged.set() # check moving 5 seconds after engaged, doesn't move right away - after_engaged_check = is_engaged and time.monotonic() - self.first_engage >= 5 and self.test_run + after_engaged_check = is_engaged and self.first_engage is not None and time.monotonic() - self.first_engage >= 5 and self.test_run x_dist = abs(curr_pos[0] - self.vehicle_last_pos[0]) y_dist = abs(curr_pos[1] - self.vehicle_last_pos[1]) diff --git a/pyproject.toml b/pyproject.toml index da04c77f1e..5c9f16d33a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -189,11 +189,7 @@ unresolved-attribute = "ignore" # many from capnp and Cython modules invalid-method-override = "ignore" # signature variance issues possibly-missing-attribute = "ignore" # too many false positives invalid-assignment = "ignore" # often intentional monkey-patching -no-matching-overload = "ignore" # numpy/ctypes overload matching issues invalid-argument-type = "ignore" # many false positives from raylib, ctypes, numpy -call-non-callable = "ignore" # false positives from dynamic types -unsupported-operator = "ignore" # false positives from dynamic types -not-subscriptable = "ignore" # false positives from dynamic types [tool.uv] python-preference = "only-managed" From f0d93eb32db9683986cfcafebec6d0f74a2a95da Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 11:02:00 -0700 Subject: [PATCH 020/325] more ty, part 2 (#38379) --- openpilot/common/api.py | 2 + openpilot/common/hardware/tici/agnos.py | 2 +- openpilot/common/hardware/tici/hardware.py | 16 ++--- openpilot/common/hardware/tici/modem.py | 2 +- openpilot/common/pid.py | 12 ++-- openpilot/common/transformations/camera.py | 3 +- openpilot/selfdrive/car/tests/test_models.py | 3 +- openpilot/selfdrive/locationd/helpers.py | 5 +- openpilot/selfdrive/locationd/locationd.py | 2 +- openpilot/selfdrive/locationd/torqued.py | 9 +-- openpilot/selfdrive/modeld/modeld.py | 2 +- .../selfdrive/modeld/parse_model_outputs.py | 2 +- .../selfdrive/pandad/tests/test_pandad.py | 2 +- .../selfdrived/tests/test_state_machine.py | 8 +-- .../test/process_replay/process_replay.py | 8 +-- openpilot/selfdrive/ui/body/animations.py | 11 +-- openpilot/selfdrive/ui/layouts/main.py | 6 +- openpilot/selfdrive/ui/layouts/sidebar.py | 2 +- openpilot/selfdrive/ui/mici/layouts/home.py | 2 +- openpilot/selfdrive/ui/mici/layouts/main.py | 2 +- .../ui/mici/layouts/settings/device.py | 4 +- .../selfdrive/ui/mici/onroad/cameraview.py | 2 +- .../ui/mici/onroad/model_renderer.py | 4 +- .../selfdrive/ui/onroad/model_renderer.py | 4 +- .../selfdrive/ui/tests/diff/replay_script.py | 2 +- .../selfdrive/ui/tests/profile_onroad.py | 2 +- .../selfdrive/ui/translations/potools.py | 22 +++--- openpilot/system/athena/athenad.py | 5 +- openpilot/system/athena/tests/test_athenad.py | 4 +- openpilot/system/hardware/power_monitoring.py | 2 +- .../hardware/tests/test_power_monitoring.py | 2 +- .../loggerd/tests/loggerd_tests_common.py | 4 +- .../system/loggerd/tests/test_deleter.py | 2 +- .../system/loggerd/tests/test_loggerd.py | 5 +- openpilot/system/qcomgpsd/nmeaport.py | 72 +++++++++---------- .../system/sensord/tests/test_sensord.py | 2 +- openpilot/system/ubloxd/binary_struct.py | 2 +- openpilot/system/ui/lib/utils.py | 3 +- openpilot/system/ui/lib/wifi_manager.py | 2 +- openpilot/system/ui/tici_reset.py | 6 +- openpilot/system/ui/widgets/__init__.py | 29 +++++--- openpilot/system/ui/widgets/label.py | 3 +- openpilot/system/ui/widgets/nav_widget.py | 2 +- openpilot/system/ui/widgets/network.py | 19 ++--- openpilot/system/ui/widgets/scroller.py | 12 ++-- openpilot/system/ui/widgets/scroller_tici.py | 3 +- openpilot/system/updated/updated.py | 2 +- .../webrtc/tests/test_stream_session.py | 6 +- openpilot/system/webrtc/webrtcd.py | 2 +- openpilot/tools/lib/auth.py | 2 +- openpilot/tools/lib/file_sources.py | 8 +-- openpilot/tools/lib/tests/test_caching.py | 2 +- .../longitudinal_maneuvers/maneuversd.py | 2 +- openpilot/tools/replay/lib/ui_helpers.py | 3 +- openpilot/tools/sim/bridge/common.py | 2 +- openpilot/tools/sim/lib/common.py | 6 +- pyproject.toml | 4 -- 57 files changed, 194 insertions(+), 165 deletions(-) diff --git a/openpilot/common/api.py b/openpilot/common/api.py index c97f56c4b5..3e3733c3ad 100644 --- a/openpilot/common/api.py +++ b/openpilot/common/api.py @@ -27,6 +27,8 @@ class Api: return api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params) def get_token(self, payload_extra=None, expiry_hours=1): + if self.private_key is None: + raise RuntimeError("private key is not configured") now = datetime.now(UTC).replace(tzinfo=None) payload = { 'identity': self.dongle_id, diff --git a/openpilot/common/hardware/tici/agnos.py b/openpilot/common/hardware/tici/agnos.py index e1f62c841c..b3b5e05176 100755 --- a/openpilot/common/hardware/tici/agnos.py +++ b/openpilot/common/hardware/tici/agnos.py @@ -19,7 +19,7 @@ class StreamingDecompressor: def __init__(self, url: str) -> None: self.buf = b"" - self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60) + self.req = requests.get(url, stream=True, headers={'Accept-Encoding': 'identity'}, timeout=60) self.it = self.req.iter_content(chunk_size=1024 * 1024) self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) self.eof = False diff --git a/openpilot/common/hardware/tici/hardware.py b/openpilot/common/hardware/tici/hardware.py index 773cbd4bed..aa125ed97d 100644 --- a/openpilot/common/hardware/tici/hardware.py +++ b/openpilot/common/hardware/tici/hardware.py @@ -339,7 +339,7 @@ class Tici(HardwareBase): # Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL gpio_init(GPIO.SOM_ST_IO, True) - gpio_set(GPIO.SOM_ST_IO, 1) + gpio_set(GPIO.SOM_ST_IO, True) # *** IRQ config *** @@ -389,21 +389,21 @@ class Tici(HardwareBase): gpio_init(GPIO.STM_RST_N, True) gpio_init(GPIO.STM_BOOT0, True) - gpio_set(GPIO.STM_RST_N, 1) - gpio_set(GPIO.STM_BOOT0, 0) + gpio_set(GPIO.STM_RST_N, True) + gpio_set(GPIO.STM_BOOT0, False) time.sleep(0.01) - gpio_set(GPIO.STM_RST_N, 0) + gpio_set(GPIO.STM_RST_N, False) def recover_internal_panda(self): gpio_init(GPIO.STM_RST_N, True) gpio_init(GPIO.STM_BOOT0, True) - gpio_set(GPIO.STM_RST_N, 1) - gpio_set(GPIO.STM_BOOT0, 1) + gpio_set(GPIO.STM_RST_N, True) + gpio_set(GPIO.STM_BOOT0, True) time.sleep(0.01) - gpio_set(GPIO.STM_RST_N, 0) + gpio_set(GPIO.STM_RST_N, False) time.sleep(0.01) - gpio_set(GPIO.STM_BOOT0, 0) + gpio_set(GPIO.STM_BOOT0, False) def booted(self): # this normally boots within 8s, but on rare occasions takes 30+s diff --git a/openpilot/common/hardware/tici/modem.py b/openpilot/common/hardware/tici/modem.py index 0d30c49328..54251d4815 100755 --- a/openpilot/common/hardware/tici/modem.py +++ b/openpilot/common/hardware/tici/modem.py @@ -52,7 +52,7 @@ PPPD_CMD = [ "novj", "novjccomp", "ipcp-accept-local", "ipcp-accept-remote", "nomagic", "user", '""', "password", '""', ] -INITIAL_STATE = { +INITIAL_STATE: dict[str, object] = { "seconds_since_boot": 0, "state": "INITIALIZING", "connected": False, "ip_address": "", diff --git a/openpilot/common/pid.py b/openpilot/common/pid.py index b3d64d6fcd..f541baf05c 100644 --- a/openpilot/common/pid.py +++ b/openpilot/common/pid.py @@ -1,11 +1,13 @@ import numpy as np -from numbers import Number +from collections.abc import Sequence + +Gain = int | float | tuple[Sequence[float], Sequence[float]] | list[list[float]] class PIDController: - def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100): - self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p - self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i - self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d + def __init__(self, k_p: Gain, k_i: Gain, k_d: Gain = 0., pos_limit=1e308, neg_limit=-1e308, rate=100): + self._k_p = ([0], [k_p]) if isinstance(k_p, (int, float)) else k_p + self._k_i = ([0], [k_i]) if isinstance(k_i, (int, float)) else k_i + self._k_d = ([0], [k_d]) if isinstance(k_d, (int, float)) else k_d self.set_limits(pos_limit, neg_limit) diff --git a/openpilot/common/transformations/camera.py b/openpilot/common/transformations/camera.py index 2e68b5e37c..ada9c5b398 100644 --- a/openpilot/common/transformations/camera.py +++ b/openpilot/common/transformations/camera.py @@ -52,7 +52,7 @@ _ar_ox_config = DeviceCameraConfig(CameraConfig(1928, 1208, 2648.0), _ar_ox_fish _os_config = DeviceCameraConfig(CameraConfig(2688 // 2, 1520 // 2, 1522.0 * 3 / 4), _os_fisheye, _os_fisheye) _neo_config = DeviceCameraConfig(CameraConfig(1164, 874, 910.0), CameraConfig(816, 612, 650.0), _NoneCameraConfig()) -DEVICE_CAMERAS = { +DEVICE_CAMERAS: dict[tuple[str, str], DeviceCameraConfig] = { # A "device camera" is defined by a device type and sensor # sensor type was never set on eon/neo/two @@ -176,4 +176,3 @@ def img_from_device(pt_device): pt_img = pt_view/pt_view[:, 2:3] return pt_img.reshape(input_shape)[:, :2] - diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index cda94b3158..5ad31f93ab 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -21,6 +21,7 @@ from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.selfdrive.test.helpers import read_segment_list from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source +from openpilot.tools.lib.file_sources import Source from openpilot.tools.lib.route import SegmentName SafetyModel = car.CarParams.SafetyModel @@ -130,7 +131,7 @@ class TestCarModelBase(unittest.TestCase): segment_range = f"{cls.test_route.route}/{seg}" try: - sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] + sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] lr = LogReader(segment_range, sources=sources, sort_by_time=True) return cls.get_testing_data_from_logreader(lr) except (LogsUnavailable, AssertionError): diff --git a/openpilot/selfdrive/locationd/helpers.py b/openpilot/selfdrive/locationd/helpers.py index 849b950de2..5e3fa16027 100644 --- a/openpilot/selfdrive/locationd/helpers.py +++ b/openpilot/selfdrive/locationd/helpers.py @@ -1,4 +1,5 @@ import numpy as np +from collections.abc import Sequence from typing import Any from functools import cache @@ -68,7 +69,7 @@ class NPQueue: class PointBuckets: - def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: + def __init__(self, x_bounds: list[tuple[float, float]], min_points: Sequence[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None: self._rng = np.random.default_rng() self.x_bounds = x_bounds self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds} @@ -101,7 +102,7 @@ class PointBuckets: return points return points[self._rng.choice(np.arange(len(points)), min(len(points), num_points), replace=False)] - def load_points(self, points: list[list[float]]) -> None: + def load_points(self, points: Sequence[Sequence[float]]) -> None: for point in points: self.add_point(*point) diff --git a/openpilot/selfdrive/locationd/locationd.py b/openpilot/selfdrive/locationd/locationd.py index 8e03995d13..eb3c42fce2 100755 --- a/openpilot/selfdrive/locationd/locationd.py +++ b/openpilot/selfdrive/locationd/locationd.py @@ -66,7 +66,7 @@ class LocationEstimator: self.observations = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} self.observation_errors = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds} - def reset(self, t: float, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P): + def reset(self, t: float | None, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P): self.kf.init_state(x_initial, covs=P_initial, filter_time=t) def _validate_sensor_source(self, source: log.SensorEventData.SensorSource): diff --git a/openpilot/selfdrive/locationd/torqued.py b/openpilot/selfdrive/locationd/torqued.py index 6321461e8b..d36684563c 100755 --- a/openpilot/selfdrive/locationd/torqued.py +++ b/openpilot/selfdrive/locationd/torqued.py @@ -57,14 +57,14 @@ class TorqueEstimator(ParameterEstimator): self.lag = 0.0 self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters if decimated: - self.min_bucket_points = MIN_BUCKET_POINTS / 10 + self.min_bucket_points: list[float] = (MIN_BUCKET_POINTS / 10).tolist() self.min_points_total = MIN_POINTS_TOTAL_QLOG self.fit_points = FIT_POINTS_TOTAL_QLOG self.factor_sanity = FACTOR_SANITY_QLOG self.friction_sanity = FRICTION_SANITY_QLOG else: - self.min_bucket_points = MIN_BUCKET_POINTS + self.min_bucket_points = MIN_BUCKET_POINTS.tolist() self.min_points_total = MIN_POINTS_TOTAL self.fit_points = FIT_POINTS_TOTAL self.factor_sanity = FACTOR_SANITY @@ -112,9 +112,10 @@ class TorqueEstimator(ParameterEstimator): 'latAccelOffset': cache_ltp.latAccelOffsetFiltered, 'frictionCoefficient': cache_ltp.frictionCoefficientFiltered } - initial_params['points'] = cache_ltp.points + cached_points: list[list[float]] = [list(point) for point in cache_ltp.points] + initial_params['points'] = cached_points self.decay = cache_ltp.decay - self.filtered_points.load_points(initial_params['points']) + self.filtered_points.load_points(cached_points) cloudlog.info("restored torque params from cache") except Exception: cloudlog.exception("failed to restore cached torque params") diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 7c70cdb14d..39fcc0725c 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -89,7 +89,7 @@ class ModelState: self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.full_frames: dict[str, Tensor] = {} - self._blob_cache: dict[int, Tensor] = {} + self._blob_cache: dict[tuple[str, int], Tensor] = {} self.parser = Parser() self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')} self.run_policy = jits['run_policy'] diff --git a/openpilot/selfdrive/modeld/parse_model_outputs.py b/openpilot/selfdrive/modeld/parse_model_outputs.py index 26c138b8ec..839c20f7cc 100644 --- a/openpilot/selfdrive/modeld/parse_model_outputs.py +++ b/openpilot/selfdrive/modeld/parse_model_outputs.py @@ -41,7 +41,7 @@ class Parser: raw = outs[name] outs[name] = sigmoid(raw) - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): + def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=()): if self.check_missing(outs, name): return raw = outs[name] diff --git a/openpilot/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py index e7a7107dcd..0f8fd9fc1a 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -60,7 +60,7 @@ class TestPandad: def test_in_reset(self): gpio_init(GPIO.STM_RST_N, True) - gpio_set(GPIO.STM_RST_N, 1) + gpio_set(GPIO.STM_RST_N, True) assert not Panda.list() self._run_test() diff --git a/openpilot/selfdrive/selfdrived/tests/test_state_machine.py b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py index 8139e53d2e..ec8f068039 100644 --- a/openpilot/selfdrive/selfdrived/tests/test_state_machine.py +++ b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py @@ -13,11 +13,11 @@ ALL_STATES = tuple(State.schema.enumerants.values()) ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE, ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL) -def make_event(event_types): - event = {} +def make_event(event_types: list[str | None]): + EVENTS[0] = {} for ev in event_types: - event[ev] = NormalPermanentAlert("alert") - EVENTS[0] = event + if ev is not None: + EVENTS[0][ev] = NormalPermanentAlert("alert") return 0 diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index b34535e96b..90eb397996 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -215,7 +215,7 @@ class ProcessContainer: def _start_process(self): if self.capture is not None: - self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) + self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) # ty: ignore[invalid-assignment] # intentional wrapper self.process.prepare() self.process.start() @@ -631,10 +631,10 @@ def replay_process( fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None, captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False ) -> list[capnp._DynamicStructReader]: - if isinstance(cfg, Iterable): - cfgs = list(cfg) - else: + if isinstance(cfg, ProcessConfig): cfgs = [cfg] + else: + cfgs = list(cfg) all_msgs = migrate_all(lr, manager_states=True, diff --git a/openpilot/selfdrive/ui/body/animations.py b/openpilot/selfdrive/ui/body/animations.py index c40f7ecdef..302f8989bf 100644 --- a/openpilot/selfdrive/ui/body/animations.py +++ b/openpilot/selfdrive/ui/body/animations.py @@ -204,7 +204,10 @@ class FaceAnimator: frames_back = round(rewind_elapsed / self._animation.frame_duration) frame_index = self._rewind_from - frames_back if frame_index <= 0: - return self._switch_to_next(now) + if self._next is None: + self._rewinding = False + return self._animation.frames[0] + return self._switch_to_next(now, self._next) return self._animation.frames[frame_index] # Play starting frames first (once) @@ -223,7 +226,7 @@ class FaceAnimator: if self._next is not None: if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero): - return self._switch_to_next(now) + return self._switch_to_next(now, self._next) # No natural return to frame 0 — start rewinding if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD): self._rewinding = True @@ -232,8 +235,8 @@ class FaceAnimator: return self._animation.frames[frame_index] - def _switch_to_next(self, now: float) -> list[tuple[int, int]]: - self._animation = self._next + def _switch_to_next(self, now: float, animation: Animation) -> list[tuple[int, int]]: + self._animation = animation self._next = None self._rewinding = False self._seen_nonzero = False diff --git a/openpilot/selfdrive/ui/layouts/main.py b/openpilot/selfdrive/ui/layouts/main.py index 47a52280f2..277b6f1404 100644 --- a/openpilot/selfdrive/ui/layouts/main.py +++ b/openpilot/selfdrive/ui/layouts/main.py @@ -31,7 +31,11 @@ class MainLayout(Widget): # Initialize layouts self._home_layout = HomeLayout() self._home_body_layout = BodyLayout() - self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()} + self._layouts: dict[MainState, Widget] = { + MainState.HOME: self._home_layout, + MainState.SETTINGS: SettingsLayout(), + MainState.ONROAD: AugmentedRoadView(), + } self._sidebar_rect = rl.Rectangle(0, 0, 0, 0) self._content_rect = rl.Rectangle(0, 0, 0, 0) diff --git a/openpilot/selfdrive/ui/layouts/sidebar.py b/openpilot/selfdrive/ui/layouts/sidebar.py index eabf5cc008..4a7c2856bb 100644 --- a/openpilot/selfdrive/ui/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/layouts/sidebar.py @@ -65,7 +65,7 @@ class MetricData: class Sidebar(Widget): def __init__(self): super().__init__() - self._net_type = NETWORK_TYPES.get(NetworkType.none) + self._net_type = NETWORK_TYPES[NetworkType.none] self._net_strength = 0 self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index adb02fb731..e7e99b38ba 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -251,7 +251,7 @@ class MiciHomeLayout(Widget): self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled) self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled) self._mic_icon.set_visible(ui_state.recording_audio) - self._body_icon.set_visible(ui_state.is_body) + self._body_icon.set_visible(bool(ui_state.is_body)) footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48) self._status_bar_layout.render(footer_rect) diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index 26ec5555be..e592253544 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -146,4 +146,4 @@ class MiciMainLayout(Scroller): def _on_body_changed(self): self._car_onroad_layout.set_visible(not ui_state.is_body) - self._body_onroad_layout.set_visible(ui_state.is_body) + self._body_onroad_layout.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index 0adcf53752..cd85ef3add 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -16,7 +16,7 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer +from openpilot.system.ui.widgets.html_render import HtmlRenderer from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -160,7 +160,7 @@ class DeviceLayoutMici(NavScroller): def __init__(self): super().__init__() - self._fcc_dialog: HtmlModal | None = None + self._fcc_dialog: MiciFccModal | None = None def power_off_callback(): ui_state.params.put_bool("DoShutdown", True, block=True) diff --git a/openpilot/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py index 991349dbf0..82e4865c76 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -219,7 +219,7 @@ class CameraView(Widget): [0.0, 0.0, 1.0] ]) - def _render(self, rect: rl.Rectangle): + def _render(self, rect: rl.Rectangle, /): if self._switching: self._handle_switch() diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py index adf9814364..4d19850769 100644 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -44,8 +44,8 @@ class ModelPoints: @dataclass class LeadVehicle: - glow: list[float] = field(default_factory=list) - chevron: list[float] = field(default_factory=list) + glow: list[tuple[float, float]] = field(default_factory=list) + chevron: list[tuple[float, float]] = field(default_factory=list) fill_alpha: int = 0 diff --git a/openpilot/selfdrive/ui/onroad/model_renderer.py b/openpilot/selfdrive/ui/onroad/model_renderer.py index 50ff3b1c53..8a40c90025 100644 --- a/openpilot/selfdrive/ui/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/onroad/model_renderer.py @@ -37,8 +37,8 @@ class ModelPoints: @dataclass class LeadVehicle: - glow: list[float] = field(default_factory=list) - chevron: list[float] = field(default_factory=list) + glow: list[tuple[float, float]] = field(default_factory=list) + chevron: list[tuple[float, float]] = field(default_factory=list) fill_alpha: int = 0 diff --git a/openpilot/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py index 8517f0dece..109f32e47a 100644 --- a/openpilot/selfdrive/ui/tests/diff/replay_script.py +++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py @@ -357,7 +357,7 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None: params = Params() main_layout._alerts_layout._pending_params = ({"UpdaterNewDescription": params.get("UpdaterNewDescription")} | {alert_data.key: params.get(alert_data.key) for alert_data in main_layout._alerts_layout.sorted_alerts}) - main_layout._alerts_layout._refresh() + main_layout._alerts_layout._update_state() swipe_right(width, wait_after=WAIT_SHORT) # open alerts script.setup(setup_offroad_alerts_and_refresh) # show alerts diff --git a/openpilot/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py index 29e44d6260..ec15e71c35 100755 --- a/openpilot/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -49,7 +49,7 @@ def patch_submaster(message_chunks): sm.recv_frame[service] = sm.frame sm.valid[service] = True sm.frame += 1 - ui_state.sm.update = mock_update + ui_state.sm.update = mock_update # ty: ignore[invalid-assignment] # profiling hook if __name__ == "__main__": diff --git a/openpilot/selfdrive/ui/translations/potools.py b/openpilot/selfdrive/ui/translations/potools.py index ac4dafb988..15da4c586c 100644 --- a/openpilot/selfdrive/ui/translations/potools.py +++ b/openpilot/selfdrive/ui/translations/potools.py @@ -67,22 +67,22 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: cur_field: str | None = None plural_idx = 0 - def finish(): - nonlocal cur, header - if cur is None: + def finish(entry: POEntry | None): + nonlocal header + if entry is None: return - if cur.msgid == "" and cur.msgstr: - header = cur - elif cur.msgid != "" or cur.is_plural: - entries.append(cur) - cur = None + if entry.msgid == "" and entry.msgstr: + header = entry + elif entry.msgid != "" or entry.is_plural: + entries.append(entry) for raw in lines: line = raw.rstrip('\n') stripped = line.strip() if not stripped: - finish() + finish(cur) + cur = None cur_field = None continue @@ -123,6 +123,8 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: continue if stripped.startswith('msgstr '): + if cur is None: + cur = POEntry() cur.msgstr = _parse_quoted(stripped[len('msgstr '):]) cur_field = 'msgstr' continue @@ -138,7 +140,7 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]: elif cur_field == 'msgstr_plural': cur.msgstr_plural[plural_idx] += val - finish() + finish(cur) return header, entries diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index cf590afb40..91b12fc0a7 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -669,7 +669,10 @@ def log_handler(end_event: threading.Event) -> None: def ws_proxy_recv(ws: WebSocket, local_sock: socket.socket, ssock: socket.socket, end_event: threading.Event, global_end_event: threading.Event) -> None: while not (end_event.is_set() or global_end_event.is_set()): try: - r = select.select((ws.sock,), (), (), 30) + sock = ws.sock + if sock is None: + return + r = select.select((sock,), (), (), 30) if r[0]: data = ws.recv() if isinstance(data, str): diff --git a/openpilot/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py index accf0784ab..8c090b93af 100644 --- a/openpilot/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -60,7 +60,7 @@ class TestAthenadMethods: @classmethod def setup_class(cls): cls.SOCKET_PORT = 45454 - athenad.Api = MockApi + athenad.Api = MockApi # ty: ignore[invalid-assignment] # test double athenad.LOCAL_PORT_WHITELIST = {cls.SOCKET_PORT} def setup_method(self): @@ -351,6 +351,7 @@ class TestAthenadMethods: assert items[0] == asdict(item) assert not items[0]['current'] + assert item.id is not None athenad.cancelled_uploads.add(item.id) items = dispatcher["listUploadQueue"]() assert len(items) == 0 @@ -363,6 +364,7 @@ class TestAthenadMethods: athenad.upload_queue.put_nowait(item2) # Ensure canceled items are not persisted + assert item2.id is not None athenad.cancelled_uploads.add(item2.id) # serialize item diff --git a/openpilot/system/hardware/power_monitoring.py b/openpilot/system/hardware/power_monitoring.py index 72a8c6848c..816d5e37dd 100644 --- a/openpilot/system/hardware/power_monitoring.py +++ b/openpilot/system/hardware/power_monitoring.py @@ -34,7 +34,7 @@ class PowerMonitoring: self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh) # Calculation tick - def calculate(self, voltage: int | None, ignition: bool): + def calculate(self, voltage: float | None, ignition: bool): try: now = time.monotonic() diff --git a/openpilot/system/hardware/tests/test_power_monitoring.py b/openpilot/system/hardware/tests/test_power_monitoring.py index 5e1b751a0f..f3ffb98e09 100644 --- a/openpilot/system/hardware/tests/test_power_monitoring.py +++ b/openpilot/system/hardware/tests/test_power_monitoring.py @@ -35,7 +35,7 @@ class TestPowerMonitoring: def test_panda_state_present(self): pm = PowerMonitoring() for _ in range(10): - pm.calculate(None, None) + pm.calculate(None, False) assert pm.get_power_used() == 0 assert pm.get_car_battery_capacity() == (CAR_BATTERY_CAPACITY_uWh / 10) diff --git a/openpilot/system/loggerd/tests/loggerd_tests_common.py b/openpilot/system/loggerd/tests/loggerd_tests_common.py index 757b61d17e..55f5fbb816 100644 --- a/openpilot/system/loggerd/tests/loggerd_tests_common.py +++ b/openpilot/system/loggerd/tests/loggerd_tests_common.py @@ -63,10 +63,10 @@ class UploaderTestCase: seg_dir: str def set_ignore(self): - uploader.Api = MockApiIgnore + uploader.Api = MockApiIgnore # ty: ignore[invalid-assignment] # test double def setup_method(self): - uploader.Api = MockApi + uploader.Api = MockApi # ty: ignore[invalid-assignment] # test double uploader.fake_upload = True uploader.force_wifi = True uploader.allow_sleep = False diff --git a/openpilot/system/loggerd/tests/test_deleter.py b/openpilot/system/loggerd/tests/test_deleter.py index 6222ea253b..8f18c8ceee 100644 --- a/openpilot/system/loggerd/tests/test_deleter.py +++ b/openpilot/system/loggerd/tests/test_deleter.py @@ -19,7 +19,7 @@ class TestDeleter(UploaderTestCase): self.f_type = "fcamera.hevc" super().setup_method() self.fake_stats = Stats(f_bavail=0, f_blocks=10, f_frsize=4096) - deleter.os.statvfs = self.fake_statvfs + deleter.os.statvfs = self.fake_statvfs # ty: ignore[invalid-assignment] # test double def start_thread(self): self.end_event = threading.Event() diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 666b56dc03..3678ca95be 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -5,6 +5,7 @@ import random import string import subprocess import time +from collections.abc import Collection from collections import defaultdict from pathlib import Path import pytest @@ -74,8 +75,8 @@ class TestLoggerd: end_type = SentinelType.endOfRoute if route else SentinelType.endOfSegment assert msgs[-1].sentinel.type == end_type - def _publish_random_messages(self, services: list[str]) -> dict[str, list]: - pm = messaging.PubMaster(services) + def _publish_random_messages(self, services: Collection[str]) -> dict[str, list]: + pm = messaging.PubMaster(list(services)) managed_processes["loggerd"].start() for s in services: diff --git a/openpilot/system/qcomgpsd/nmeaport.py b/openpilot/system/qcomgpsd/nmeaport.py index ba7cf117b1..db1fe5552d 100644 --- a/openpilot/system/qcomgpsd/nmeaport.py +++ b/openpilot/system/qcomgpsd/nmeaport.py @@ -1,9 +1,9 @@ import os import sys -from dataclasses import dataclass, fields +from dataclasses import dataclass from subprocess import check_output, CalledProcessError from time import sleep -from typing import NoReturn, cast +from typing import NoReturn DEBUG = int(os.environ.get("DEBUG", "0")) @@ -17,27 +17,27 @@ class GnssClockNmeaPort: # 0x10 = bias_uncertainty_ns valid # 0x20 = drift_nsps valid # 0x40 = drift_uncertainty_nsps valid - flags: int - leap_seconds: int - time_ns: int - time_uncertainty_ns: int # 1-sigma - full_bias_ns: int - bias_ns: float - bias_uncertainty_ns: float # 1-sigma - drift_nsps: float - drift_uncertainty_nsps: float # 1-sigma + flags: int | None + leap_seconds: int | None + time_ns: int | None + time_uncertainty_ns: int | None # 1-sigma + full_bias_ns: int | None + bias_ns: float | None + bias_uncertainty_ns: float | None # 1-sigma + drift_nsps: float | None + drift_uncertainty_nsps: float | None # 1-sigma - def __post_init__(self): - for field in fields(self): - val = getattr(self, field.name) - field_type = cast(type, field.type) - setattr(self, field.name, field_type(val) if val else None) + @classmethod + def from_fields(cls, values: list[str]) -> 'GnssClockNmeaPort': + ints = [int(value) if value else None for value in values[:5]] + floats = [float(value) if value else None for value in values[5:9]] + return cls(*ints, *floats) @dataclass class GnssMeasNmeaPort: - messageCount: int - messageNum: int - svCount: int + messageCount: int | None + messageNum: int | None + svCount: int | None # constellation enum: # 1 = GPS # 2 = SBAS @@ -45,10 +45,10 @@ class GnssMeasNmeaPort: # 4 = QZSS # 5 = BEIDOU # 6 = GALILEO - constellation: int - svId: int - flags: int # always zero - time_offset_ns: int + constellation: int | None + svId: int | None + flags: int | None # always zero + time_offset_ns: int | None # state bit mask: # 0x0001 = CODE LOCK # 0x0002 = BIT SYNC @@ -64,18 +64,18 @@ class GnssMeasNmeaPort: # 0x0800 = GALILEO E1C 2ND CODE LOCK # 0x1000 = GALILEO E1B PAGE SYNC # 0x2000 = GALILEO E1B PAGE SYNC - state: int - time_of_week_ns: int - time_of_week_uncertainty_ns: int # 1-sigma - carrier_to_noise_ratio: float - pseudorange_rate: float - pseudorange_rate_uncertainty: float # 1-sigma + state: int | None + time_of_week_ns: int | None + time_of_week_uncertainty_ns: int | None # 1-sigma + carrier_to_noise_ratio: float | None + pseudorange_rate: float | None + pseudorange_rate_uncertainty: float | None # 1-sigma - def __post_init__(self): - for field in fields(self): - val = getattr(self, field.name) - field_type = cast(type, field.type) - setattr(self, field.name, field_type(val) if val else None) + @classmethod + def from_fields(cls, values: list[str]) -> 'GnssMeasNmeaPort': + ints = [int(value) if value else None for value in values[:10]] + floats = [float(value) if value else None for value in values[10:13]] + return cls(*ints, *floats) def nmea_checksum_ok(s): checksum = 0 @@ -109,11 +109,11 @@ def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: match fields[0]: case "$GNCLK": # fields at end are reserved (not used) - gnss_clock = GnssClockNmeaPort(*fields[1:10]) + gnss_clock = GnssClockNmeaPort.from_fields(fields[1:10]) print(gnss_clock) case "$GNMEAS": # fields at end are reserved (not used) - gnss_meas = GnssMeasNmeaPort(*fields[1:14]) + gnss_meas = GnssMeasNmeaPort.from_fields(fields[1:14]) print(gnss_meas) except Exception as e: print(e) diff --git a/openpilot/system/sensord/tests/test_sensord.py b/openpilot/system/sensord/tests/test_sensord.py index fc4e3061bc..dc96886e4a 100644 --- a/openpilot/system/sensord/tests/test_sensord.py +++ b/openpilot/system/sensord/tests/test_sensord.py @@ -20,7 +20,7 @@ SENSOR_CONFIGS = ( ) SENSOR_CONFIGS_BY_MEASUREMENT = {config.measurement: config for config in SENSOR_CONFIGS} -def get_irq_count(irq: int): +def get_irq_count(irq: str): with open(f"/sys/kernel/irq/{irq}/per_cpu_count") as f: per_cpu = map(int, f.read().split(",")) return sum(per_cpu) diff --git a/openpilot/system/ubloxd/binary_struct.py b/openpilot/system/ubloxd/binary_struct.py index c144bd5696..5bc05094f3 100644 --- a/openpilot/system/ubloxd/binary_struct.py +++ b/openpilot/system/ubloxd/binary_struct.py @@ -184,7 +184,7 @@ class BinaryStruct: setattr(obj, name, value) return obj - cls._read = _read + cls._read = _read # ty: ignore[invalid-assignment] # installed dynamically for each subclass @classmethod def from_bytes(cls: type[T], data: bytes) -> T: diff --git a/openpilot/system/ui/lib/utils.py b/openpilot/system/ui/lib/utils.py index 77035d0da0..e97b3ba9d9 100644 --- a/openpilot/system/ui/lib/utils.py +++ b/openpilot/system/ui/lib/utils.py @@ -1,8 +1,9 @@ import pyray as rl +from collections.abc import Sequence class GuiStyleContext: - def __init__(self, styles: list[tuple[int, int, int]]): + def __init__(self, styles: Sequence[tuple[int, int, int]]): """styles is a list of tuples (control, prop, new_value)""" self.styles = styles self.prev_styles: list[tuple[int, int, int]] = [] diff --git a/openpilot/system/ui/lib/wifi_manager.py b/openpilot/system/ui/lib/wifi_manager.py index 147666bf85..c52e5a2ddf 100644 --- a/openpilot/system/ui/lib/wifi_manager.py +++ b/openpilot/system/ui/lib/wifi_manager.py @@ -257,7 +257,7 @@ class WifiManager: def add_callbacks(self, need_auth: Callable[[str], None] | None = None, activated: Callable[[], None] | None = None, - forgotten: Callable[[str], None] | None = None, + forgotten: Callable[[str | None], None] | None = None, networks_updated: Callable[[list[Network]], None] | None = None, disconnected: Callable[[], None] | None = None): if need_auth is not None: diff --git a/openpilot/system/ui/tici_reset.py b/openpilot/system/ui/tici_reset.py index 569faf45fe..5c175dbfa6 100755 --- a/openpilot/system/ui/tici_reset.py +++ b/openpilot/system/ui/tici_reset.py @@ -37,7 +37,11 @@ class Reset(Widget): self._reset_state = ResetState.NONE self._cancel_button = Button("Cancel", gui_app.request_close) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) - self._reboot_button = Button("Reboot", lambda: subprocess.run("sudo reboot", shell=True)) + self._reboot_button = Button("Reboot", self._reboot) + + @staticmethod + def _reboot() -> None: + subprocess.run("sudo reboot", shell=True) def _do_erase(self): if PC: diff --git a/openpilot/system/ui/widgets/__init__.py b/openpilot/system/ui/widgets/__init__.py index 4ce1c1b694..f8cb04d8a7 100644 --- a/openpilot/system/ui/widgets/__init__.py +++ b/openpilot/system/ui/widgets/__init__.py @@ -3,16 +3,25 @@ from __future__ import annotations import abc import pyray as rl from enum import IntEnum -from typing import TypeVar +from typing import Protocol, TypeVar from collections.abc import Callable from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS, MouseEvent -try: - from openpilot.selfdrive.ui.ui_state import device -except ImportError: - class Device: - awake = True - device = Device() +class DeviceLike(Protocol): + awake: bool + + +def _get_device() -> DeviceLike: + try: + from openpilot.selfdrive.ui.ui_state import device + return device + except ImportError: + class Device: + awake = True + return Device() + + +device = _get_device() W = TypeVar('W', bound='Widget') @@ -185,16 +194,16 @@ class Widget(abc.ABC): """Optionally update the widget's non-layout state. This is called before rendering.""" @abc.abstractmethod - def _render(self, rect: rl.Rectangle) -> bool | int | None: + def _render(self, rect: rl.Rectangle, /) -> bool | int | None: """Render the widget within the given rectangle.""" def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" - def _handle_mouse_press(self, mouse_pos: MousePos) -> None: + def _handle_mouse_press(self, mouse_pos: MousePos, /) -> None: """Optionally handle mouse press events.""" - def _handle_mouse_release(self, mouse_pos: MousePos) -> None: + def _handle_mouse_release(self, mouse_pos: MousePos, /) -> None: """Optionally handle mouse release events.""" if self._click_delay is not None: self._click_release_time = rl.get_time() + self._click_delay diff --git a/openpilot/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py index 7052b2c2a9..fdaf6f3148 100644 --- a/openpilot/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -1,7 +1,6 @@ import math from enum import IntEnum from collections.abc import Callable -from itertools import zip_longest from typing import Union import pyray as rl @@ -210,7 +209,7 @@ class Label(Widget): icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) - for text, text_size, emojis in zip_longest(self._text_wrapped, self._text_size, self._emojis, fillvalue=[]): + for text, text_size, emojis in zip(self._text_wrapped, self._text_size, self._emojis, strict=True): line_pos = rl.Vector2(text_pos.x, text_pos.y) if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: line_pos.x += self._text_padding diff --git a/openpilot/system/ui/widgets/nav_widget.py b/openpilot/system/ui/widgets/nav_widget.py index 11770bbe5d..58ff8123bb 100644 --- a/openpilot/system/ui/widgets/nav_widget.py +++ b/openpilot/system/ui/widgets/nav_widget.py @@ -78,7 +78,7 @@ class NavWidget(Widget, abc.ABC): # the top of a vertical scroll panel to prevent erroneous swipes return True - def set_back_callback(self, callback: Callable[[], None]) -> None: + def set_back_callback(self, callback: Callable[[], None] | None) -> None: self._back_callback = callback def set_shown_callback(self, callback: Callable[[], None] | None) -> None: diff --git a/openpilot/system/ui/widgets/network.py b/openpilot/system/ui/widgets/network.py index 710202c54f..4068d552b9 100644 --- a/openpilot/system/ui/widgets/network.py +++ b/openpilot/system/ui/widgets/network.py @@ -15,16 +15,6 @@ from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item -# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI -try: - from openpilot.common.params import Params - from openpilot.selfdrive.ui.ui_state import ui_state - from openpilot.selfdrive.ui.lib.prime_state import PrimeType -except Exception: - Params = None - ui_state = None - PrimeType = None - NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 MAX_PASSWORD_LENGTH = 64 @@ -105,11 +95,16 @@ class NetworkUI(Widget): class AdvancedNetworkSettings(Widget): def __init__(self, wifi_manager: WifiManager): - assert Params is not None + # AdvancedNetworkSettings needs the full openpilot environment, standalone apps just use WifiManagerUI + from openpilot.common.params import Params + from openpilot.selfdrive.ui.ui_state import ui_state + from openpilot.selfdrive.ui.lib.prime_state import PrimeType super().__init__() self._wifi_manager = wifi_manager self._wifi_manager.add_callbacks(networks_updated=self._on_network_updated) self._params = Params() + self._prime_state = ui_state.prime_state + self._cell_prime_types = (PrimeType.NONE, PrimeType.LITE) self._keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) @@ -254,7 +249,7 @@ class AdvancedNetworkSettings(Widget): self._wifi_manager.process_callbacks() # If not using prime SIM, show GSM settings and enable IPv4 forwarding - show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE) + show_cell_settings = self._prime_state.get_type() in self._cell_prime_types self._wifi_manager.set_ipv4_forward(show_cell_settings) self._roaming_btn.set_visible(show_cell_settings) self._apn_btn.set_visible(show_cell_settings) diff --git a/openpilot/system/ui/widgets/scroller.py b/openpilot/system/ui/widgets/scroller.py index b7b6bf5932..55195b30fb 100644 --- a/openpilot/system/ui/widgets/scroller.py +++ b/openpilot/system/ui/widgets/scroller.py @@ -1,6 +1,6 @@ import pyray as rl import numpy as np -from collections.abc import Callable +from collections.abc import Callable, Sequence from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter from openpilot.common.swaglog import cloudlog @@ -40,7 +40,7 @@ class ScrollIndicator(Widget): self._content_size = content_size self._viewport = viewport - def _render(self, _): + def _render(self, _, /): # scale indicator width based on content size indicator_w = float(np.interp(self._content_size, [1000, 3000], [300, 100])) @@ -69,7 +69,7 @@ class ScrollIndicator(Widget): class _Scroller(Widget): """Should use wrapper below to reduce boilerplate""" - def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, + def __init__(self, items: Sequence[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING, pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True): super().__init__() self._items: list[Widget] = [] @@ -150,7 +150,7 @@ class _Scroller(Widget): and not self.moving_items and (original_touch_valid_callback() if original_touch_valid_callback else True)) - def add_widgets(self, items: list[Widget]) -> None: + def add_widgets(self, items: Sequence[Widget]) -> None: for item in items: self.add_widget(item) @@ -332,7 +332,7 @@ class _Scroller(Widget): else: item.render() - def _render(self, _): + def _render(self, _, /): rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) @@ -397,7 +397,7 @@ class Scroller(Widget): # pass down enabled to child widget for nav stack self._scroller.set_enabled(lambda: self.enabled) - def _render(self, _): + def _render(self, _, /): self._scroller.render(self._rect) diff --git a/openpilot/system/ui/widgets/scroller_tici.py b/openpilot/system/ui/widgets/scroller_tici.py index a843010d56..fc81e1b079 100644 --- a/openpilot/system/ui/widgets/scroller_tici.py +++ b/openpilot/system/ui/widgets/scroller_tici.py @@ -1,4 +1,5 @@ import pyray as rl +from collections.abc import Sequence from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.widgets import Widget @@ -23,7 +24,7 @@ class LineSeparator(Widget): class Scroller(Widget): - def __init__(self, items: list[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True): + def __init__(self, items: Sequence[Widget], spacing: int = ITEM_SPACING, line_separator: bool = False, pad_end: bool = True): super().__init__() self._items: list[Widget] = [] self._spacing = spacing diff --git a/openpilot/system/updated/updated.py b/openpilot/system/updated/updated.py index ac8111d5c5..9610785861 100755 --- a/openpilot/system/updated/updated.py +++ b/openpilot/system/updated/updated.py @@ -357,7 +357,7 @@ class Updater: setup_git_options(OVERLAY_MERGED) output = run(["git", "ls-remote", "--heads"], OVERLAY_MERGED) - self.branches = defaultdict(lambda: None) + self.branches.clear() for line in output.split('\n'): ls_remotes_re = r'(?P\b[0-9a-f]{5,40}\b)(\s+)(refs\/heads\/)(?P.*$)' x = re.fullmatch(ls_remotes_re, line.strip()) diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index b83e1223d2..1f7bd5747a 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -55,9 +55,11 @@ class TestStreamSession: mocked_pubmaster.send.assert_called_once() mt, md = mocked_pubmaster.send.call_args.args - assert mt == msg["type"] + msg_type = msg["type"] + assert isinstance(msg_type, str) + assert mt == msg_type assert isinstance(md, capnp._DynamicStructBuilder) - assert hasattr(md, msg["type"]) + assert hasattr(md, msg_type) mocked_pubmaster.reset_mock() diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 82ebb7b018..7901f9847f 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -519,7 +519,7 @@ class WebrtcdHandler(BaseHTTPRequestHandler): def do_OPTIONS(self) -> None: self._dispatch_request() - def log_message(self, fmt, *args) -> None: + def log_message(self, format: str, *args: object) -> None: # noqa: A002 # stdlib override # silence default access logging; errors are logged explicitly in _dispatch_request pass diff --git a/openpilot/tools/lib/auth.py b/openpilot/tools/lib/auth.py index 5988397d0a..9139d8b42d 100755 --- a/openpilot/tools/lib/auth.py +++ b/openpilot/tools/lib/auth.py @@ -54,7 +54,7 @@ class ClientRedirectHandler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(b'Return to the CLI to continue') - def log_message(self, *args): + def log_message(self, format: str, *args: object) -> None: # noqa: A002 # stdlib override pass # this prevent http server from dumping messages to stdout diff --git a/openpilot/tools/lib/file_sources.py b/openpilot/tools/lib/file_sources.py index cb7bf15114..2d2bfccfd7 100755 --- a/openpilot/tools/lib/file_sources.py +++ b/openpilot/tools/lib/file_sources.py @@ -12,7 +12,7 @@ Source = Callable[[SegmentRange, list[int], FileNames], dict[int, str]] InternalUnavailableException = Exception("Internal source not available") -def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: route = Route(sr.route_name) # comma api will have already checked if the file exists @@ -22,7 +22,7 @@ def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> d return {seg: route.qlog_paths()[seg] for seg in seg_idxs if route.qlog_paths()[seg] is not None} -def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, endpoint_url: str = DATA_ENDPOINT) -> dict[int, str]: +def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /, endpoint_url: str = DATA_ENDPOINT) -> dict[int, str]: if not internal_source_available(endpoint_url): raise InternalUnavailableException @@ -32,11 +32,11 @@ def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, endpo return eval_source({seg: [get_internal_url(sr, seg, fn) for fn in fns] for seg in seg_idxs}) -def openpilotci_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def openpilotci_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs}) -def comma_car_segments_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: +def comma_car_segments_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, /) -> dict[int, str]: return eval_source({seg: get_comma_segments_url(sr.route_name, seg) for seg in seg_idxs}) diff --git a/openpilot/tools/lib/tests/test_caching.py b/openpilot/tools/lib/tests/test_caching.py index 0753ef1d3c..c70d7fc81b 100644 --- a/openpilot/tools/lib/tests/test_caching.py +++ b/openpilot/tools/lib/tests/test_caching.py @@ -16,7 +16,7 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.FILE_EXISTS: - self.send_response(206 if "Range" in self.headers else 200, b'1234') + self.send_response(206 if "Range" in self.headers else 200, '1234') else: self.send_response(404) self.end_headers() diff --git a/openpilot/tools/longitudinal_maneuvers/maneuversd.py b/openpilot/tools/longitudinal_maneuvers/maneuversd.py index a9a5ba6304..8cdc2f26cf 100755 --- a/openpilot/tools/longitudinal_maneuvers/maneuversd.py +++ b/openpilot/tools/longitudinal_maneuvers/maneuversd.py @@ -59,7 +59,7 @@ class Maneuver: return float(action_accel) - def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool) -> float: + def get_accel(self, v_ego: float, long_active: bool, standstill: bool, cruise_standstill: bool, /) -> float: ready = abs(v_ego - self.initial_speed) < 0.3 and long_active and not cruise_standstill if self.initial_speed < 0.01: ready = ready and standstill diff --git a/openpilot/tools/replay/lib/ui_helpers.py b/openpilot/tools/replay/lib/ui_helpers.py index 8f0c14983f..b9da0d225f 100644 --- a/openpilot/tools/replay/lib/ui_helpers.py +++ b/openpilot/tools/replay/lib/ui_helpers.py @@ -5,6 +5,7 @@ import numpy as np import pyray as rl from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.artist import Artist from matplotlib.offsetbox import AnchoredOffsetbox, HPacker, TextArea from openpilot.common.transformations.camera import get_view_frame_from_calib_frame @@ -119,7 +120,7 @@ def init_plots(arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_co idxs.append(name_to_arr_idx[item]) plot_select.append(i) # Build colored title: each label colored to match its plot line - title_texts = [] + title_texts: list[Artist] = [] for j2, (nm, cl) in enumerate(zip(pl_list, plot_colors[i], strict=False)): if j2 > 0: title_texts.append(TextArea(", ", textprops={"color": "white", "fontsize": 10})) diff --git a/openpilot/tools/sim/bridge/common.py b/openpilot/tools/sim/bridge/common.py index 048bf6cb11..1d00e3b0f9 100644 --- a/openpilot/tools/sim/bridge/common.py +++ b/openpilot/tools/sim/bridge/common.py @@ -94,7 +94,7 @@ Ignition: {self.simulator_state.ignition} Engaged: {self.simulator_state.is_enga """) @abstractmethod - def spawn_world(self, q: Queue) -> World: + def spawn_world(self, q: Queue, /) -> World: pass def _run(self, q: Queue): diff --git a/openpilot/tools/sim/lib/common.py b/openpilot/tools/sim/lib/common.py index 1324932131..eb29119fb4 100644 --- a/openpilot/tools/sim/lib/common.py +++ b/openpilot/tools/sim/lib/common.py @@ -40,7 +40,7 @@ class SimulatorState: self.is_engaged = False self.ignition = True - self.velocity: vec3 = None + self.velocity = vec3(0, 0, 0) self.bearing: float = 0 self.gps = GPSState() self.imu = IMUState() @@ -72,7 +72,7 @@ class World(ABC): self.exit_event = multiprocessing.Event() @abstractmethod - def apply_controls(self, steer_sim, throttle_out, brake_out): + def apply_controls(self, steer_sim, throttle_out, brake_out, /): pass @abstractmethod @@ -84,7 +84,7 @@ class World(ABC): pass @abstractmethod - def read_sensors(self, simulator_state: SimulatorState): + def read_sensors(self, simulator_state: SimulatorState, /): pass @abstractmethod diff --git a/pyproject.toml b/pyproject.toml index 5c9f16d33a..5895a2a095 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,10 +186,6 @@ quote-style = "preserve" [tool.ty.rules] unresolved-import = "ignore" # Cython-compiled modules (.pyx) unresolved-attribute = "ignore" # many from capnp and Cython modules -invalid-method-override = "ignore" # signature variance issues -possibly-missing-attribute = "ignore" # too many false positives -invalid-assignment = "ignore" # often intentional monkey-patching -invalid-argument-type = "ignore" # many false positives from raylib, ctypes, numpy [tool.uv] python-preference = "only-managed" From 39e12c8bb1ab6e4ff47f1a4e3660eb3a97077c10 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 11:14:35 -0700 Subject: [PATCH 021/325] rm cffi, it's a raylib transitive dep --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5895a2a095..6d9f3923a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ "tqdm", # cars (fw_versions.py) on start + many one-off uses # core - "cffi", "scons", "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "Cython", From 24893ebadbc77b9fe0451c7600acf0c4fc7bdfe5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 12:10:48 -0700 Subject: [PATCH 022/325] rm pre-commit-hooks (#38382) * rm pre-commit-hooks * rm test for the tests * lil more --- pyproject.toml | 1 - scripts/lint/check_added_large_files.py | 47 +++++++++++++++++ .../check_shebang_scripts_are_executable.py | 50 +++++++++++++++++++ scripts/lint/lint.sh | 4 +- uv.lock | 23 --------- 5 files changed, 99 insertions(+), 26 deletions(-) create mode 100755 scripts/lint/check_added_large_files.py create mode 100755 scripts/lint/check_shebang_scripts_are_executable.py diff --git a/pyproject.toml b/pyproject.toml index 6d9f3923a1..c3769f0401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,6 @@ testing = [ "pytest-mock", "ruff", "codespell", - "pre-commit-hooks", ] dev = [ diff --git a/scripts/lint/check_added_large_files.py b/scripts/lint/check_added_large_files.py new file mode 100755 index 0000000000..c1aa820181 --- /dev/null +++ b/scripts/lint/check_added_large_files.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import argparse +import math +import os +import subprocess + + +def lfs_files(filenames: list[str]) -> set[str]: + if not filenames: + return set() + + result = subprocess.run( + ("git", "check-attr", "filter", "-z", "--stdin"), + input="\0".join(filenames), + check=True, + capture_output=True, + text=True, + ) + fields = result.stdout.rstrip("\0").split("\0") if result.stdout else [] + return {fields[i] for i in range(0, len(fields), 3) if fields[i + 2] == "lfs"} + + +def check_added_large_files(filenames: list[str], max_kb: int) -> int: + failed = False + ignored = lfs_files(filenames) + for filename in filenames: + if filename in ignored: + continue + + size_kb = math.ceil(os.stat(filename).st_size / 1024) + if size_kb > max_kb: + print(f"{filename} ({size_kb} KB) exceeds {max_kb} KB.") + failed = True + + return int(failed) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check that tracked files do not exceed a size limit.") + parser.add_argument("filenames", nargs="*") + parser.add_argument("--maxkb", type=int, default=500, help="maximum allowable size in KiB") + args = parser.parse_args() + return check_added_large_files(args.filenames, args.maxkb) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lint/check_shebang_scripts_are_executable.py b/scripts/lint/check_shebang_scripts_are_executable.py new file mode 100755 index 0000000000..7288640091 --- /dev/null +++ b/scripts/lint/check_shebang_scripts_are_executable.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import argparse +import shlex +import subprocess +import sys + + +def staged_modes(filenames: list[str]) -> list[tuple[str, str]]: + if not filenames: + return [] + + result = subprocess.run( + ("git", "ls-files", "-z", "--stage", "--", *filenames), + check=True, + capture_output=True, + text=True, + ) + entries = result.stdout.rstrip("\0").split("\0") if result.stdout else [] + return [(entry.split(" ", 1)[0], entry.split("\t", 1)[1]) for entry in entries] + + +def has_shebang(filename: str) -> bool: + with open(filename, "rb") as f: + return f.read(2) == b"#!" + + +def check_shebang_scripts_are_executable(filenames: list[str]) -> int: + failed = False + for mode, filename in staged_modes(filenames): + if mode != "100755" and has_shebang(filename): + quoted = shlex.quote(filename) + print("\n".join(( + f"{filename}: has a shebang but is not marked executable!", + f" If it is supposed to be executable, try: `chmod +x {quoted}`", + " If it is not supposed to be executable, double-check its shebang is wanted.\n", + )), file=sys.stderr) + failed = True + + return int(failed) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check that tracked files with shebangs are executable.") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args() + return check_shebang_scripts_are_executable(args.filenames) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index e01e7b37c2..c678379b42 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -46,8 +46,8 @@ function run_tests() { PYTHON_FILES=$2 run "ruff" ruff check openpilot --quiet - run "check_added_large_files" python3 -m pre_commit_hooks.check_added_large_files --enforce-all $ALL_FILES --maxkb=120 - run "check_shebang_scripts_are_executable" python3 -m pre_commit_hooks.check_shebang_scripts_are_executable $ALL_FILES + run "check_added_large_files" $DIR/check_added_large_files.py --maxkb=120 $ALL_FILES + run "check_shebang_scripts_are_executable" $DIR/check_shebang_scripts_are_executable.py $ALL_FILES run "check_shebang_format" $DIR/check_shebang_format.sh $ALL_FILES run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES diff --git a/uv.lock b/uv.lock index 9aef9c9a01..d03a44160e 100644 --- a/uv.lock +++ b/uv.lock @@ -748,7 +748,6 @@ testing = [ { name = "codespell" }, { name = "coverage" }, { name = "hypothesis" }, - { name = "pre-commit-hooks" }, { name = "pytest" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, @@ -800,7 +799,6 @@ requires-dist = [ { name = "opendbc", marker = "extra == 'submodules'", editable = "opendbc_repo" }, { name = "pandacan", marker = "extra == 'submodules'", editable = "panda" }, { name = "pillow" }, - { name = "pre-commit-hooks", marker = "extra == 'testing'" }, { name = "pycapnp", specifier = "==2.1.0" }, { name = "pyjwt", extras = ["crypto"] }, { name = "pytest", marker = "extra == 'testing'" }, @@ -897,18 +895,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pre-commit-hooks" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/36/4d/93e63e48f8fd16d6c1e4cef5dabadcade4d1325c7fd6f29f075a4d2284f3/pre_commit_hooks-6.0.0.tar.gz", hash = "sha256:76d8370c006f5026cdd638a397a678d26dda735a3c88137e05885a020f824034", size = 28293, upload-time = "2025-08-09T19:25:04.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, -] - [[package]] name = "pycapnp" version = "2.1.0" @@ -1171,15 +1157,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruff" version = "0.15.20" From caa9e770cc359775621a2638c73ee7a7d13393f5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 12:20:25 -0700 Subject: [PATCH 023/325] manager: remove preimport stage (#38381) --- .../test/process_replay/process_replay.py | 1 - openpilot/system/manager/manager.py | 5 ----- openpilot/system/manager/process.py | 15 --------------- 3 files changed, 21 deletions(-) diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index 90eb397996..2a90dfc295 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -216,7 +216,6 @@ class ProcessContainer: def _start_process(self): if self.capture is not None: self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) # ty: ignore[invalid-assignment] # intentional wrapper - self.process.prepare() self.process.start() def start( diff --git a/openpilot/system/manager/manager.py b/openpilot/system/manager/manager.py index 2d090b37f0..86fca2723f 100755 --- a/openpilot/system/manager/manager.py +++ b/openpilot/system/manager/manager.py @@ -87,11 +87,6 @@ def manager_init() -> None: dirty=build_metadata.openpilot.is_dirty, device=HARDWARE.get_device_type()) - # preimport all processes - for p in managed_processes.values(): - p.prepare() - - def manager_cleanup() -> None: # send signals to kill all procs for p in managed_processes.values(): diff --git a/openpilot/system/manager/process.py b/openpilot/system/manager/process.py index a0b878d0f2..7f7eabd7be 100644 --- a/openpilot/system/manager/process.py +++ b/openpilot/system/manager/process.py @@ -70,10 +70,6 @@ class ManagerProcess(ABC): shutting_down = False restart_if_crash = False - @abstractmethod - def prepare(self) -> None: - pass - @abstractmethod def start(self) -> None: pass @@ -150,9 +146,6 @@ class NativeProcess(ManagerProcess): self.sigkill = sigkill self.launcher = nativelauncher - def prepare(self) -> None: - pass - def start(self) -> None: # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: @@ -178,11 +171,6 @@ class PythonProcess(ManagerProcess): self.launcher = launcher self.restart_if_crash = restart_if_crash - def prepare(self) -> None: - if self.enabled: - cloudlog.info(f"preimporting {self.module}") - importlib.import_module(self.module) - def start(self) -> None: # In case we only tried a non blocking stop we need to stop it before restarting if self.shutting_down: @@ -211,9 +199,6 @@ class DaemonProcess(ManagerProcess): def should_run(started, params, CP): return True - def prepare(self) -> None: - pass - def start(self) -> None: if self.params is None: self.params = Params() From 157c7080ce4771407bacea59620e11011e50b75c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 12:21:04 -0700 Subject: [PATCH 024/325] bump opendbc (#38370) --- opendbc_repo | 2 +- openpilot/selfdrive/test/process_replay/migration.py | 8 ++++---- openpilot/tools/replay/ui.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index f95568e03c..78a1c9e73d 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit f95568e03cc5de650b7935c18ac56ee347caaae2 +Subproject commit 78a1c9e73de2e0a081cb2bb41f7f10281b9eeabf diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index a6e466bef5..87064f5017 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -295,7 +295,7 @@ def migrate_carOutput(msgs): co = messaging.new_message('carOutput') co.valid = msg.valid co.logMonoTime = msg.logMonoTime - co.carOutput.actuatorsOutput = msg.carControl.actuatorsOutputDEPRECATED + co.carOutput.actuatorsOutput = msg.carControl.deprecated.actuatorsOutput add_ops.append(as_reader(co)) return [], add_ops, [] @@ -321,10 +321,10 @@ def migrate_pandaStates(msgs): safety_param = safety_param_migration[fingerprint].value elif len(CP.safetyConfigs): safety_param = CP.safetyConfigs[0].safetyParam - if CP.safetyConfigs[0].safetyParamDEPRECATED != 0: - safety_param = CP.safetyConfigs[0].safetyParamDEPRECATED + if CP.safetyConfigs[0].deprecated.safetyParam != 0: + safety_param = CP.safetyConfigs[0].deprecated.safetyParam else: - safety_param = CP.safetyParamDEPRECATED + safety_param = CP.deprecated.safetyParam ops = [] for index, msg in msgs: diff --git a/openpilot/tools/replay/ui.py b/openpilot/tools/replay/ui.py index f5bd3f1d78..4fbede2be9 100755 --- a/openpilot/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -173,7 +173,7 @@ def ui_thread(addr): plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k - plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED + plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].deprecated.gas # TODO gas is deprecated plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel / 4.0, 0.0, 1.0) plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brakePressed From fef29ad22534aea01851bd3706182f582fbb4e02 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 14:33:26 -0700 Subject: [PATCH 025/325] start porting tests to unittest style (#38384) --- .../controls/tests/test_longcontrol.py | 52 ++++---- .../tests/test_torqued_lat_accel_offset.py | 25 ++-- .../selfdrive/locationd/test/test_torqued.py | 35 ++--- .../ui/mici/tests/test_widget_leaks.py | 125 +++++++++--------- .../selfdrive/ui/tests/test_raylib_ui.py | 9 +- .../selfdrive/ui/tests/test_translations.py | 84 ++++++------ .../tools/jotpluggler/test_jotpluggler.py | 9 +- 7 files changed, 171 insertions(+), 168 deletions(-) diff --git a/openpilot/selfdrive/controls/tests/test_longcontrol.py b/openpilot/selfdrive/controls/tests/test_longcontrol.py index da916b156f..f69c54a3cd 100644 --- a/openpilot/selfdrive/controls/tests/test_longcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_longcontrol.py @@ -27,30 +27,30 @@ class TestLongControlStateTransition: should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.off -def test_engage(): - CP = car.CarParams.new_message() - active = True - current_state = LongCtrlState.off - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=True, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=True, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=True) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid + def test_engage(self): + CP = car.CarParams.new_message() + active = True + current_state = LongCtrlState.off + next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + should_stop=True, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + should_stop=False, brake_pressed=True, cruise_standstill=False) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + should_stop=False, brake_pressed=False, cruise_standstill=True) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + should_stop=False, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.pid -def test_starting(): - CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5) - active = True - current_state = LongCtrlState.starting - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.starting - next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid + def test_starting(self): + CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5) + active = True + current_state = LongCtrlState.starting + next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + should_stop=False, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.starting + next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0, + should_stop=False, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.pid diff --git a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py index 7a63da23a5..7599eb1e7b 100644 --- a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -56,16 +56,17 @@ def simulate_straight_road_msgs(est): for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): est.handle_log(t, which, msg) -def test_estimated_offset(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - msg = est.get_msg() - # TODO add lataccelfactor and friction check when we have more accurate estimates - assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 +class TestTorquedLatAccelOffset: + def test_estimated_offset(self): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + msg = est.get_msg() + # TODO add lataccelfactor and friction check when we have more accurate estimates + assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 -def test_straight_road_roll_bias(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - simulate_straight_road_msgs(est) - msg = est.get_msg() - assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) + def test_straight_road_roll_bias(self): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + simulate_straight_road_msgs(est) + msg = est.get_msg() + assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) diff --git a/openpilot/selfdrive/locationd/test/test_torqued.py b/openpilot/selfdrive/locationd/test/test_torqued.py index ac8e40fc30..4d337178a0 100644 --- a/openpilot/selfdrive/locationd/test/test_torqued.py +++ b/openpilot/selfdrive/locationd/test/test_torqued.py @@ -2,24 +2,25 @@ from opendbc.car.structs import car from openpilot.selfdrive.locationd.torqued import TorqueEstimator -def test_cal_percent(): - est = TorqueEstimator(car.CarParams()) - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 0 +class TestTorqued: + def test_cal_percent(self): + est = TorqueEstimator(car.CarParams()) + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 0 - for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), - est.filtered_points.buckets_min_points.values(), strict=True): - for _ in range(int(min_pts)): - est.filtered_points.add_point((low + high) / 2.0, 0.0) + for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), + est.filtered_points.buckets_min_points.values(), strict=True): + for _ in range(int(min_pts)): + est.filtered_points.add_point((low + high) / 2.0, 0.0) - # enough bucket points, but not enough total points - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 + # enough bucket points, but not enough total points + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 - # add enough points to bucket with most capacity - key = list(est.filtered_points.buckets)[0] - for _ in range(est.min_points_total - len(est.filtered_points)): - est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) + # add enough points to bucket with most capacity + key = list(est.filtered_points.buckets)[0] + for _ in range(est.min_points_total - len(est.filtered_points)): + est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) - msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 100 + msg = est.get_msg() + assert msg.liveTorqueParameters.calPerc == 100 diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py index 09142a8e11..68c61f4ff3 100755 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -41,80 +41,81 @@ def get_child_widgets(widget) -> list: return children -@pytest.mark.skip(reason="segfaults") -def test_dialogs_do_not_leak(): - import pyray as rl - rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) - from openpilot.system.ui.lib.application import gui_app +class TestWidgetLeaks: + @pytest.mark.skip(reason="segfaults") + def test_dialogs_do_not_leak(self): + import pyray as rl + rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) + from openpilot.system.ui.lib.application import gui_app - # mici dialogs - from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow - from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog - from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog - from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog - from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal + # mici dialogs + from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow + from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog + from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog + from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog + from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal - # tici dialogs - from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog - from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow - from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog - from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog - from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog - from openpilot.system.ui.widgets.html_render import HtmlModal - from openpilot.system.ui.widgets.keyboard import Keyboard + # tici dialogs + from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog + from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow + from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog + from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog + from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog + from openpilot.system.ui.widgets.html_render import HtmlModal + from openpilot.system.ui.widgets.keyboard import Keyboard - gui_app.init_window("ref-test") + gui_app.init_window("ref-test") - leaked_widgets = set() + leaked_widgets = set() - for ctor in ( - # mici - MiciDriverCameraDialog, MiciPairingDialog, - lambda: MiciTrainingGuide(lambda: None), - lambda: MiciOnboardingWindow(lambda: None), - lambda: BigDialog("test", "test"), - lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), - lambda: BigInputDialog("test"), - lambda: MiciFccModal(text="test"), - # tici - TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, - lambda: ConfirmDialog("test", "ok"), - lambda: MultiOptionDialog("test", ["a", "b"]), - lambda: HtmlModal(text="test"), - ): - widget = ctor() - all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] + for ctor in ( + # mici + MiciDriverCameraDialog, MiciPairingDialog, + lambda: MiciTrainingGuide(lambda: None), + lambda: MiciOnboardingWindow(lambda: None), + lambda: BigDialog("test", "test"), + lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), + lambda: BigInputDialog("test"), + lambda: MiciFccModal(text="test"), + # tici + TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, + lambda: ConfirmDialog("test", "ok"), + lambda: MultiOptionDialog("test", ["a", "b"]), + lambda: HtmlModal(text="test"), + ): + widget = ctor() + all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] - del widget + del widget - for ref in all_refs: - if ref() is not None: - obj = ref() - name = f"{type(obj).__module__}.{type(obj).__qualname__}" - leaked_widgets.add(name) + for ref in all_refs: + if ref() is not None: + obj = ref() + name = f"{type(obj).__module__}.{type(obj).__qualname__}" + leaked_widgets.add(name) - print(f"\n=== Widget {name} alive after del") - print(" Referrers:") - for r in gc.get_referrers(obj): - if r is obj: - continue + print(f"\n=== Widget {name} alive after del") + print(" Referrers:") + for r in gc.get_referrers(obj): + if r is obj: + continue - if hasattr(r, '__self__') and r.__self__ is not obj: - print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") - elif hasattr(r, '__func__'): - print(f" method: {r.__name__}") - else: - print(f" {type(r).__module__}.{type(r).__qualname__}") - del obj + if hasattr(r, '__self__') and r.__self__ is not obj: + print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") + elif hasattr(r, '__func__'): + print(f" method: {r.__name__}") + else: + print(f" {type(r).__module__}.{type(r).__qualname__}") + del obj - gui_app.close() + gui_app.close() - unexpected = leaked_widgets - KNOWN_LEAKS - assert not unexpected, f"New leaked widgets: {unexpected}" + unexpected = leaked_widgets - KNOWN_LEAKS + assert not unexpected, f"New leaked widgets: {unexpected}" - fixed = KNOWN_LEAKS - leaked_widgets - assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" + fixed = KNOWN_LEAKS - leaked_widgets + assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" if __name__ == "__main__": - test_dialogs_do_not_leak() + TestWidgetLeaks().test_dialogs_do_not_leak() diff --git a/openpilot/selfdrive/ui/tests/test_raylib_ui.py b/openpilot/selfdrive/ui/tests/test_raylib_ui.py index 69ba946dcd..d04c940cde 100644 --- a/openpilot/selfdrive/ui/tests/test_raylib_ui.py +++ b/openpilot/selfdrive/ui/tests/test_raylib_ui.py @@ -2,7 +2,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) +class TestRaylibUi: + @with_processes(["ui"]) + def test_raylib_ui(self): + """Test initialization of the UI widgets is successful.""" + time.sleep(1) diff --git a/openpilot/selfdrive/ui/tests/test_translations.py b/openpilot/selfdrive/ui/tests/test_translations.py index fba595acad..9eae072f21 100644 --- a/openpilot/selfdrive/ui/tests/test_translations.py +++ b/openpilot/selfdrive/ui/tests/test_translations.py @@ -46,61 +46,59 @@ def load_po_text(po_path: Path) -> str: return po_path.read_text(encoding='utf-8') -@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values())) -def test_translation_file_exists(language_code: str): - po_path = PO_DIR / f"app_{language_code}.po" - assert po_path.exists(), f"missing translation file: {po_path}" +class TestTranslations: + @pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values())) + def test_translation_file_exists(self, language_code: str): + po_path = PO_DIR / f"app_{language_code}.po" + assert po_path.exists(), f"missing translation file: {po_path}" + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_placeholders_are_preserved(self, po_path: Path): + _, entries = parse_po(po_path) + language = po_path.stem.removeprefix("app_") -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_placeholders_are_preserved(po_path: Path): - _, entries = parse_po(po_path) - language = po_path.stem.removeprefix("app_") + for entry in entries: + source_placeholders = extract_placeholders(entry.msgid) - for entry in entries: - source_placeholders = extract_placeholders(entry.msgid) + if entry.is_plural: + plural_placeholders = extract_placeholders(entry.msgid_plural) + message = ( + f"{language}: source plural placeholders do not match singular for " + + f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}" + ) + assert plural_placeholders == source_placeholders, message - if entry.is_plural: - plural_placeholders = extract_placeholders(entry.msgid_plural) - message = ( - f"{language}: source plural placeholders do not match singular for " - + f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}" - ) - assert plural_placeholders == source_placeholders, message + for idx, msgstr in sorted(entry.msgstr_plural.items()): + if not msgstr: + continue - for idx, msgstr in sorted(entry.msgstr_plural.items()): - if not msgstr: + translated_placeholders = extract_placeholders(msgstr) + message = ( + f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: " + + f"expected {source_placeholders}, got {translated_placeholders}" + ) + assert translated_placeholders == source_placeholders, message + else: + if not entry.msgstr: continue - translated_placeholders = extract_placeholders(msgstr) + translated_placeholders = extract_placeholders(entry.msgstr) message = ( - f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: " + f"{language}: translation changes placeholders for {entry.msgid!r}: " + f"expected {source_placeholders}, got {translated_placeholders}" ) assert translated_placeholders == source_placeholders, message - else: - if not entry.msgstr: - continue - translated_placeholders = extract_placeholders(entry.msgstr) - message = ( - f"{language}: translation changes placeholders for {entry.msgid!r}: " - + f"expected {source_placeholders}, got {translated_placeholders}" + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_refs_do_not_include_line_numbers(self, po_path: Path): + for line in load_po_text(po_path).splitlines(): + assert not LINE_NUMBER_REF_RE.match(line), ( + f"{po_path.name}: line-number source reference found: {line}" ) - assert translated_placeholders == source_placeholders, message - -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_refs_do_not_include_line_numbers(po_path: Path): - for line in load_po_text(po_path).splitlines(): - assert not LINE_NUMBER_REF_RE.match(line), ( - f"{po_path.name}: line-number source reference found: {line}" + @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + def test_translation_entities_are_valid(self, po_path: Path): + matches = BAD_ENTITY_RE.findall(load_po_text(po_path)) + assert not matches, ( + f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}" ) - - -@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) -def test_translation_entities_are_valid(po_path: Path): - matches = BAD_ENTITY_RE.findall(load_po_text(po_path)) - assert not matches, ( - f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}" - ) diff --git a/openpilot/tools/jotpluggler/test_jotpluggler.py b/openpilot/tools/jotpluggler/test_jotpluggler.py index b9b751c4a5..0729e3b2e3 100644 --- a/openpilot/tools/jotpluggler/test_jotpluggler.py +++ b/openpilot/tools/jotpluggler/test_jotpluggler.py @@ -5,7 +5,8 @@ from pathlib import Path JOTPLUGGLER_DIR = Path(__file__).parent -def test_help(): - result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) - assert result.returncode == 0, result.stderr - assert "Usage:" in result.stderr +class TestJotpluggler: + def test_help(self): + result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + assert "Usage:" in result.stderr From 3dbf02f803235312639be15c078ea6c795af5100 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 14:45:26 -0700 Subject: [PATCH 026/325] cereal: gc dead ZMQ branches in tests --- .../cereal/messaging/tests/test_messaging.py | 36 ++++--------------- .../messaging/tests/test_pub_sub_master.py | 16 +-------- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/openpilot/cereal/messaging/tests/test_messaging.py b/openpilot/cereal/messaging/tests/test_messaging.py index c2ac1578d4..92d77f1b35 100644 --- a/openpilot/cereal/messaging/tests/test_messaging.py +++ b/openpilot/cereal/messaging/tests/test_messaging.py @@ -1,4 +1,3 @@ -import os import capnp import multiprocessing import numbers @@ -6,7 +5,6 @@ import random import threading import time from openpilot.common.parameterized import parameterized -import pytest from openpilot.cereal import log from opendbc.car.structs import car @@ -24,10 +22,6 @@ def random_socks(num_socks=10): def random_bytes(length=1000): return bytes([random.randrange(0xFF) for _ in range(length)]) -def zmq_sleep(t=1): - if "ZMQ" in os.environ: - time.sleep(t) - # TODO: this should take any capnp struct and returrn a msg with random populated data def random_carstate(): @@ -53,16 +47,6 @@ def delayed_send(delay, sock, dat): class TestMessaging: - def setUp(self): - # TODO: ZMQ tests are too slow; all sleeps will need to be - # replaced with logic to block on the necessary condition - if "ZMQ" in os.environ: - pytest.skip() - - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep() - @parameterized.expand(events) def test_new_message(self, evt): try: @@ -89,7 +73,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=1000) - zmq_sleep() # no wait and no msgs in queue msgs = func(sub_sock) @@ -110,7 +93,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=100) - zmq_sleep() # no wait and no msg in queue, socket should timeout recvd = messaging.recv_sock(sub_sock) @@ -129,7 +111,6 @@ class TestMessaging: sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=1000) - zmq_sleep() # no msg in queue, socket should timeout recvd = messaging.recv_one(sub_sock) @@ -142,12 +123,10 @@ class TestMessaging: assert isinstance(recvd, capnp._DynamicStructReader) assert_carstate(msg.carState, recvd.carState) - @pytest.mark.xfail(condition="ZMQ" in os.environ, reason='ZMQ detected') def test_recv_one_or_none(self): sock = "carState" pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock) - zmq_sleep() # no msg in queue, socket shouldn't block recvd = messaging.recv_one_or_none(sub_sock) @@ -165,16 +144,13 @@ class TestMessaging: sock_timeout = 0.1 pub_sock = messaging.pub_sock(sock) sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000)) - zmq_sleep() - # this test doesn't work with ZMQ since multiprocessing interrupts it - if "ZMQ" not in os.environ: - # wait 5 socket timeouts and make sure it's still retrying - p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,)) - p.start() - time.sleep(sock_timeout*5) - assert p.is_alive() - p.terminate() + # wait 5 socket timeouts and make sure it's still retrying + p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,)) + p.start() + time.sleep(sock_timeout*5) + assert p.is_alive() + p.terminate() # wait 5 socket timeouts before sending msg = random_carstate() diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index 20ff855fda..90e78ef649 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -5,18 +5,12 @@ from collections.abc import Sized import openpilot.cereal.messaging as messaging from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \ - random_bytes, random_carstate, assert_carstate, \ - zmq_sleep + random_bytes, random_carstate, assert_carstate from openpilot.cereal.services import SERVICE_LIST class TestSubMaster: - def setup_method(self): - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep(3) - def test_init(self): sm = messaging.SubMaster(events) for p in [sm.updated, sm.recv_time, sm.recv_frame, sm.alive, @@ -43,7 +37,6 @@ class TestSubMaster: sock = "carState" pub_sock = messaging.pub_sock(sock) sm = messaging.SubMaster([sock,]) - zmq_sleep() msg = random_carstate() pub_sock.send(msg.to_bytes()) @@ -55,7 +48,6 @@ class TestSubMaster: sock = "carState" pub_sock = messaging.pub_sock(sock) sm = messaging.SubMaster([sock,]) - zmq_sleep() for i in range(10): msg = messaging.new_message(sock) @@ -126,11 +118,6 @@ class TestSubMaster: class TestPubMaster: - def setup_method(self): - # ZMQ pub socket takes too long to die - # sleep to prevent multiple publishers error between tests - zmq_sleep(3) - def test_init(self): messaging.PubMaster(events) @@ -138,7 +125,6 @@ class TestPubMaster: socks = random_socks() pm = messaging.PubMaster(socks) sub_socks = {s: messaging.sub_sock(s, conflate=True, timeout=1000) for s in socks} - zmq_sleep() # PubMaster accepts either a capnp msg builder or bytes for capnp in [True, False]: From e124d6df9bae1fff3da702ecda564afa72239216 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 14:52:04 -0700 Subject: [PATCH 027/325] more dead code gc --- .../messaging/tests/test_pub_sub_master.py | 9 ------ openpilot/common/params_keys.h | 1 - openpilot/common/tests/test_params.py | 6 ++-- openpilot/selfdrive/locationd/paramsd.py | 21 ------------- .../selfdrive/locationd/test/test_paramsd.py | 31 +------------------ .../selfdrive/ui/layouts/settings/device.py | 1 - .../ui/mici/layouts/settings/device.py | 1 - .../tools/cabana/streams/replaystream.cc | 3 -- 8 files changed, 4 insertions(+), 69 deletions(-) diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index 90e78ef649..7353764aea 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -91,15 +91,6 @@ class TestSubMaster: else: assert not sm._check_avg_freq(service) - def test_alive(self): - pass - - def test_ignore_alive(self): - pass - - def test_valid(self): - pass - # SubMaster should always conflate def test_conflate(self): sock = "carState" diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 0615c02700..6441fc91df 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -78,7 +78,6 @@ inline static std::unordered_map keys = { {"LastUpdateTime", {PERSISTENT, TIME}}, {"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"LiveDelay", {PERSISTENT, BYTES}}, - {"LiveParameters", {PERSISTENT, JSON}}, {"LiveParametersV2", {PERSISTENT, BYTES}}, {"LivestreamEncoderBitrate", {CLEAR_ON_MANAGER_START | DONT_LOG, INT}}, {"LivestreamRequestKeyframe", {CLEAR_ON_MANAGER_START | DONT_LOG, BOOL}}, diff --git a/openpilot/common/tests/test_params.py b/openpilot/common/tests/test_params.py index fcb8e5a185..bbd411bc37 100644 --- a/openpilot/common/tests/test_params.py +++ b/openpilot/common/tests/test_params.py @@ -112,14 +112,14 @@ class TestParams: def test_params_default_value(self): self.params.remove("LanguageSetting") self.params.remove("LongitudinalPersonality") - self.params.remove("LiveParameters") + self.params.remove("LiveParametersV2") assert self.params.get("LanguageSetting") is None assert self.params.get("LanguageSetting", return_default=False) is None assert isinstance(self.params.get("LanguageSetting", return_default=True), str) assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int) - assert self.params.get("LiveParameters") is None - assert self.params.get("LiveParameters", return_default=True) is None + assert self.params.get("LiveParametersV2") is None + assert self.params.get("LiveParametersV2", return_default=True) is None def test_params_get_type(self): # json diff --git a/openpilot/selfdrive/locationd/paramsd.py b/openpilot/selfdrive/locationd/paramsd.py index a32773bf0c..c1088a8180 100755 --- a/openpilot/selfdrive/locationd/paramsd.py +++ b/openpilot/selfdrive/locationd/paramsd.py @@ -200,25 +200,6 @@ def check_valid_with_hysteresis(current_valid: bool, val: float, threshold: floa return current_valid -# TODO: Remove this function after few releases (added in 0.9.9) -def migrate_cached_vehicle_params_if_needed(params: Params): - last_parameters_data_old = params.get("LiveParameters") - last_parameters_data = params.get("LiveParametersV2") - if last_parameters_data_old is None or last_parameters_data is not None: - return - - try: - last_parameters_msg = messaging.new_message('liveParameters') - last_parameters_msg.liveParameters.valid = True - last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio'] - last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor'] - last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg'] - params.put("LiveParametersV2", last_parameters_msg.to_bytes(), block=True) - except Exception as e: - cloudlog.error(f"Failed to perform parameter migration: {e}") - params.remove("LiveParameters") - - def retrieve_initial_vehicle_params(params: Params, CP: car.CarParams, replay: bool, debug: bool): last_parameters_data = params.get("LiveParametersV2") last_carparams_data = params.get("CarParamsPrevRoute") @@ -273,8 +254,6 @@ def main(): params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - migrate_cached_vehicle_params_if_needed(params) - steer_ratio, stiffness_factor, angle_offset_deg, pInitial = retrieve_initial_vehicle_params(params, CP, REPLAY, DEBUG) learner = VehicleParamsLearner(CP, steer_ratio, stiffness_factor, np.radians(angle_offset_deg), pInitial) diff --git a/openpilot/selfdrive/locationd/test/test_paramsd.py b/openpilot/selfdrive/locationd/test/test_paramsd.py index 28c3f4acf7..74135cb875 100644 --- a/openpilot/selfdrive/locationd/test/test_paramsd.py +++ b/openpilot/selfdrive/locationd/test/test_paramsd.py @@ -2,7 +2,7 @@ import random import numpy as np from openpilot.cereal import messaging -from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed +from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params from openpilot.selfdrive.locationd.models.car_kf import CarKalman from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams @@ -30,38 +30,9 @@ class TestParamsd: params.put("LiveParametersV2", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) - migrate_cached_vehicle_params_if_needed(params) # this is not tested here but should not mess anything up or throw an error sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True) np.testing.assert_allclose(sr, msg.liveParameters.steerRatio) np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor) np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg) np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape) np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std) - - # TODO Remove this test after the support for old format is removed - def test_read_saved_params_old_format(self): - params = Params() - - lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams]) - CP = next(m for m in lr if m.which() == "carParams").carParams - - msg = get_random_live_parameters(CP) - params.put("LiveParameters", msg.liveParameters.to_dict(), block=True) - params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) - params.remove("LiveParametersV2") - - migrate_cached_vehicle_params_if_needed(params) - sr, sf, offset, _ = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True) - np.testing.assert_allclose(sr, msg.liveParameters.steerRatio) - np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor) - np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg) - assert params.get("LiveParametersV2") is not None - - def test_read_saved_params_corrupted_old_format(self): - params = Params() - params.put("LiveParameters", {}, block=True) - params.remove("LiveParametersV2") - - migrate_cached_vehicle_params_if_needed(params) - assert params.get("LiveParameters") is None - assert params.get("LiveParametersV2") is None diff --git a/openpilot/selfdrive/ui/layouts/settings/device.py b/openpilot/selfdrive/ui/layouts/settings/device.py index aa15899ac2..22853b0bd9 100644 --- a/openpilot/selfdrive/ui/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/layouts/settings/device.py @@ -102,7 +102,6 @@ class DeviceLayout(Widget): self._params.remove("CalibrationParams") self._params.remove("LiveTorqueParameters") - self._params.remove("LiveParameters") self._params.remove("LiveParametersV2") self._params.remove("LiveDelay") self._params.put_bool("OnroadCycleRequested", True, block=True) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index cd85ef3add..7f4a9ab0b8 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -172,7 +172,6 @@ class DeviceLayoutMici(NavScroller): params = ui_state.params params.remove("CalibrationParams") params.remove("LiveTorqueParameters") - params.remove("LiveParameters") params.remove("LiveParametersV2") params.remove("LiveDelay") params.put_bool("OnroadCycleRequested", True, block=True) diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index c502ea061a..dbd44b54e3 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -16,10 +16,7 @@ ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { unsetenv("ZMQ"); setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ op_prefix = std::make_unique(); -#endif QObject::connect(&settings, &Settings::changed, this, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); From d9596fa9985edc3e516174fc0b2de1f35021734e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jul 2026 14:55:42 -0700 Subject: [PATCH 028/325] gc old athena and uploader migration code --- openpilot/system/athena/athenad.py | 4 ---- openpilot/system/loggerd/uploader.py | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index 91b12fc0a7..ef0561b122 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -488,10 +488,6 @@ def setRouteViewed(route: str) -> dict[str, int | str]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: try: - # migration, can be removed once 0.9.8 is out for a while - if local_port == 8022: - local_port = 22 - if local_port not in LOCAL_PORT_WHITELIST: raise Exception("Requested local port not whitelisted") diff --git a/openpilot/system/loggerd/uploader.py b/openpilot/system/loggerd/uploader.py index e36b12ed6a..81f8ed1ba3 100755 --- a/openpilot/system/loggerd/uploader.py +++ b/openpilot/system/loggerd/uploader.py @@ -46,9 +46,7 @@ class FakeResponse: def get_directory_sort(d: str) -> list[str]: - # ensure old format is sorted sooner - o = ["0", ] if d.startswith("2024-") else ["1", ] - return o + [s.rjust(10, '0') for s in d.rsplit('--', 1)] + return [s.rjust(10, '0') for s in d.rsplit('--', 1)] def listdir_by_creation(d: str) -> list[str]: if not os.path.isdir(d): From 24a9b6dae54ea2b63d326a0972f70312e6f2e971 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 20 Jul 2026 10:36:09 -0700 Subject: [PATCH 029/325] profile usb gpu link stability (#38368) * profile usb gpu link before using * ci --- openpilot/selfdrive/modeld/compile_modeld.py | 4 +++ openpilot/selfdrive/modeld/modeld.py | 3 ++ openpilot/selfdrive/modeld/usbgpu_link.py | 34 ++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpilot/selfdrive/modeld/usbgpu_link.py diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index f74ab27b8c..ebc3f21a13 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -13,6 +13,7 @@ from collections import namedtuple import numpy as np from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob +from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link def _patch_tinygrad_fetch_fw(): import hashlib @@ -311,6 +312,9 @@ if __name__ == "__main__": p.add_argument('--frame-skip', type=int, required=True) args = p.parse_args() + if 'USB+AMD' in os.environ.get('DEV', ''): + wait_usbgpu_link() + model_path = read_file_chunked_to_disk(args.onnx) model_w, model_h = args.model_size diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 39fcc0725c..41a1f44e5f 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -25,6 +25,7 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_drivi from openpilot.common.file_chunker import open_file_chunked, get_manifest_path from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -168,6 +169,8 @@ def main(demo=False): if use_extra_client: cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") + if USBGPU: + wait_usbgpu_link() st = time.monotonic() cloudlog.warning("loading model") model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU) diff --git a/openpilot/selfdrive/modeld/usbgpu_link.py b/openpilot/selfdrive/modeld/usbgpu_link.py new file mode 100644 index 0000000000..ef12cba939 --- /dev/null +++ b/openpilot/selfdrive/modeld/usbgpu_link.py @@ -0,0 +1,34 @@ +import time +from pathlib import Path + +from openpilot.common.swaglog import cloudlog +from openpilot.common.hardware.usb import CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID, usb_devices, controller, read_int + +STABLE_SECONDS = 2.0 +STABLE_THRESHOLD = 5.0 # link errors per second + + +def _chestnut_portli() -> Path | None: + for device in usb_devices(): + if read_int(device / "idVendor", 16) == CHESTNUT_VENDOR_ID and \ + read_int(device / "idProduct", 16) == CHESTNUT_PRODUCT_ID: + ctrl = controller(device) + if ctrl is not None and (ctrl / "portli").exists(): + return ctrl / "portli" + return None + + +def wait_usbgpu_link(timeout: float = 30.0) -> None: + portli = _chestnut_portli() + if portli is None: + return + + t0 = time.monotonic() + while time.monotonic() - t0 < timeout: + start = read_int(portli, 0) + time.sleep(STABLE_SECONDS) + rate = (read_int(portli, 0) - start) / STABLE_SECONDS + if rate <= STABLE_THRESHOLD: + return + cloudlog.warning(f"usbgpu link not stable: {rate:.0f} errors/s") + cloudlog.error("usbgpu link never stabilized") From 78909dac7309efd52132def3a7038891667194d2 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 20 Jul 2026 10:50:18 -0700 Subject: [PATCH 030/325] soundd: add complete sound (#38390) --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/assets/sounds/complete.wav | 3 +++ openpilot/selfdrive/ui/soundd.py | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 openpilot/selfdrive/assets/sounds/complete.wav diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index e1725d1e44..2148e81d3b 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -812,6 +812,7 @@ struct SelfdriveState { promptDistracted @8; preAlert @9; + complete @10; } enum OpenpilotState @0xdbe58b96d2d1ac61 { diff --git a/openpilot/selfdrive/assets/sounds/complete.wav b/openpilot/selfdrive/assets/sounds/complete.wav new file mode 100644 index 0000000000..f85d48f315 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/complete.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3027da9834adf9c71177bc3086f8de3018ce22e4725b694d635723c5b3c860cd +size 97120 diff --git a/openpilot/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py index d84c98710b..151c49b0ec 100644 --- a/openpilot/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -45,6 +45,8 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME), + AudibleAlert.complete: ("complete.wav", 1, MAX_VOLUME), + AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME), AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME), } From 5472e69e351c4c4968ad1c80db88f842ef270f48 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 20 Jul 2026 14:31:55 -0700 Subject: [PATCH 031/325] New sounds (#38154) * new sounds * soundd: fix sound cutoff * update sounds * add max warning_immediate variant * mici: play sounds button in developer settings * rename sounds * add complete sound * update sounds * raise soundfloor by 5% * update sounds * play unused sounds * update sounds * bump opendbc * Revert "play unused sounds" This reverts commit 47e538da2200e700804714576baa5f8c0f28cdb3. * Revert "mici: play sounds button in developer settings" This reverts commit 149141bb8a21bec7a04dc8f231d0b6b9120cc7c2. * remove unused sounds * raise soundfloor by 5% * opendbc * add space --- .../selfdrive/assets/sounds/critical.wav | 3 +++ .../selfdrive/assets/sounds/disengage.wav | 4 +-- .../selfdrive/assets/sounds/dm_critical.wav | 3 +++ .../selfdrive/assets/sounds/dm_warning.wav | 3 +++ openpilot/selfdrive/assets/sounds/engage.wav | 4 +-- .../selfdrive/assets/sounds/pre_alert.wav | 4 +-- openpilot/selfdrive/assets/sounds/prompt.wav | 3 --- .../assets/sounds/prompt_distracted.wav | 3 --- openpilot/selfdrive/assets/sounds/refuse.wav | 4 +-- openpilot/selfdrive/assets/sounds/warning.wav | 3 +++ .../assets/sounds/warning_immediate.wav | 3 --- .../selfdrive/assets/sounds/warning_soft.wav | 3 --- openpilot/selfdrive/ui/soundd.py | 27 ++++++++++++++----- 13 files changed, 40 insertions(+), 27 deletions(-) create mode 100644 openpilot/selfdrive/assets/sounds/critical.wav create mode 100644 openpilot/selfdrive/assets/sounds/dm_critical.wav create mode 100644 openpilot/selfdrive/assets/sounds/dm_warning.wav delete mode 100644 openpilot/selfdrive/assets/sounds/prompt.wav delete mode 100644 openpilot/selfdrive/assets/sounds/prompt_distracted.wav create mode 100644 openpilot/selfdrive/assets/sounds/warning.wav delete mode 100644 openpilot/selfdrive/assets/sounds/warning_immediate.wav delete mode 100644 openpilot/selfdrive/assets/sounds/warning_soft.wav diff --git a/openpilot/selfdrive/assets/sounds/critical.wav b/openpilot/selfdrive/assets/sounds/critical.wav new file mode 100644 index 0000000000..f36aa1bacd --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/critical.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca29ff46fbd6d00cc02596de8c9abfbf1cacdc6a7b2f98b27a74d24c790da004 +size 52374 diff --git a/openpilot/selfdrive/assets/sounds/disengage.wav b/openpilot/selfdrive/assets/sounds/disengage.wav index 7bfd97ad71..7db1d31911 100644 --- a/openpilot/selfdrive/assets/sounds/disengage.wav +++ b/openpilot/selfdrive/assets/sounds/disengage.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42bd04a57b527c787a0555503e02a203f7d672c12d448769a3f41f17befbf013 -size 48044 +oid sha256:6f47633b5082b911e79dd13fc2d1f4c2a9d44d2fe0860f3d390ffc4e3d772f7d +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/dm_critical.wav b/openpilot/selfdrive/assets/sounds/dm_critical.wav new file mode 100644 index 0000000000..9a4171444f --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/dm_critical.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:538271820d618046009bb456e930598f2736dd1a6174c11925ec3912581b05c1 +size 52374 diff --git a/openpilot/selfdrive/assets/sounds/dm_warning.wav b/openpilot/selfdrive/assets/sounds/dm_warning.wav new file mode 100644 index 0000000000..52124b0801 --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/dm_warning.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:634fad6590295ef2e73d5f7e5aecbb5dd245a0f7692f1e300c2ebac95aabb8e3 +size 73026 diff --git a/openpilot/selfdrive/assets/sounds/engage.wav b/openpilot/selfdrive/assets/sounds/engage.wav index 8633b5ac2d..a984e2f3da 100644 --- a/openpilot/selfdrive/assets/sounds/engage.wav +++ b/openpilot/selfdrive/assets/sounds/engage.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1e177499d9439367179cc57a6301b6162393972e3a136cc35c5fdac026bf10a -size 48044 +oid sha256:a380fe0a022856b302841a0dd19b71eab318c6b08ec851f128942758a7f1631a +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/pre_alert.wav b/openpilot/selfdrive/assets/sounds/pre_alert.wav index 4443bf7d23..d8240a1717 100644 --- a/openpilot/selfdrive/assets/sounds/pre_alert.wav +++ b/openpilot/selfdrive/assets/sounds/pre_alert.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c0af7f5fe57bb36ab96fae868e20feca763541c97a61b3a3a84a0e7fcb81163 -size 83350 +oid sha256:cc9e67cfaba77e8e4f4049f0add9238660e4cf8f886c6915600d8f47a30c98db +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/prompt.wav b/openpilot/selfdrive/assets/sounds/prompt.wav deleted file mode 100644 index e482c85a62..0000000000 --- a/openpilot/selfdrive/assets/sounds/prompt.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad19268e4aaaeac8dd21f6b26c16a121e7b3f50bba867748e7226727643ae682 -size 144642 diff --git a/openpilot/selfdrive/assets/sounds/prompt_distracted.wav b/openpilot/selfdrive/assets/sounds/prompt_distracted.wav deleted file mode 100644 index c11993f20c..0000000000 --- a/openpilot/selfdrive/assets/sounds/prompt_distracted.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:412ef25d2fb103c1ebd55c667313a5921493305fb4e1f4e1dafc08d3b95d86ab -size 73026 diff --git a/openpilot/selfdrive/assets/sounds/refuse.wav b/openpilot/selfdrive/assets/sounds/refuse.wav index 1e0c47697d..10026d7be9 100644 --- a/openpilot/selfdrive/assets/sounds/refuse.wav +++ b/openpilot/selfdrive/assets/sounds/refuse.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4af81cbf1d96a42cc351878b015298aee82874b46baaf1a615ca91ec36c0ced6 -size 83228 +oid sha256:081d64ca28a84a59ff5e6aea4732865b8687585938b8a287bc7e6a49d0646508 +size 97120 diff --git a/openpilot/selfdrive/assets/sounds/warning.wav b/openpilot/selfdrive/assets/sounds/warning.wav new file mode 100644 index 0000000000..bb71a0176a --- /dev/null +++ b/openpilot/selfdrive/assets/sounds/warning.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601 +size 73026 diff --git a/openpilot/selfdrive/assets/sounds/warning_immediate.wav b/openpilot/selfdrive/assets/sounds/warning_immediate.wav deleted file mode 100644 index fcbfed79ed..0000000000 --- a/openpilot/selfdrive/assets/sounds/warning_immediate.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a390831afca3bfc6ea3c2739b872ebf866e70df8ae30653f8587e5cd3993959 -size 68306 diff --git a/openpilot/selfdrive/assets/sounds/warning_soft.wav b/openpilot/selfdrive/assets/sounds/warning_soft.wav deleted file mode 100644 index 7db30303d6..0000000000 --- a/openpilot/selfdrive/assets/sounds/warning_soft.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67e636d072703e6b1233a12344c0a6304fd43d64dbb31c66b71c2c8870a339c1 -size 153764 diff --git a/openpilot/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py index 151c49b0ec..5cbb1298f9 100644 --- a/openpilot/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -22,7 +22,7 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES) -AMBIENT_DB = 24 # DB where MIN_VOLUME is applied +AMBIENT_DB = 26 # DB where MIN_VOLUME is applied DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied VOLUME_BASE = 20 @@ -39,16 +39,15 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME), AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME), - AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME), - AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME), - AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME), + AudibleAlert.prompt: ("warning.wav", 1, MAX_VOLUME), + AudibleAlert.promptRepeat: ("warning.wav", None, MAX_VOLUME), + AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME), AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME), - AudibleAlert.complete: ("complete.wav", 1, MAX_VOLUME), - AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME), - AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME), + AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME), + AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME), } if HARDWARE.get_device_type() == "tizi": sound_list.update({ @@ -78,6 +77,7 @@ class Soundd: self.ramp_start_time = 0. self.selfdrive_timeout_alert = False + self.pending_stop = False self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False) @@ -116,6 +116,10 @@ class Soundd: self.current_sound_frame += frames_to_write current_sound_frame = self.current_sound_frame % len(sound_data) loops = self.current_sound_frame // len(sound_data) + if self.pending_stop and current_sound_frame == 0: + self.current_alert = AudibleAlert.none + self.pending_stop = False + break return ret * self.current_volume @@ -126,6 +130,15 @@ class Soundd: def update_alert(self, new_alert): current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert]) + # let looping sounds finish the current loop instead of cutting off mid tone + if new_alert == AudibleAlert.none and self.current_alert != AudibleAlert.none and sound_list[self.current_alert][1] is None: + if current_alert_played_once: + self.pending_stop = True + else: + self.current_alert = AudibleAlert.none + self.current_sound_frame = 0 + return + self.pending_stop = False if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once): if new_alert == AudibleAlert.warningImmediate: self.ramp_start_volume = self.current_volume From c20263d9851daa7c0142b20f095697e8e1910263 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 20 Jul 2026 14:46:33 -0700 Subject: [PATCH 032/325] speedup chunk reading (#38389) read chunk directly into caller buffer --- openpilot/common/file_chunker.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpilot/common/file_chunker.py b/openpilot/common/file_chunker.py index 74f3013f71..2d080c3fff 100755 --- a/openpilot/common/file_chunker.py +++ b/openpilot/common/file_chunker.py @@ -42,25 +42,26 @@ def get_existing_chunks(path): class ChunkStream(io.RawIOBase): def __init__(self, paths): self._paths = iter(paths) - self._buf = memoryview(b'') + self._f = None def readable(self): return True def readinto(self, b): n = 0 + view = memoryview(b) while n < len(b): - if not self._buf: + if self._f is None: p = next(self._paths, None) if p is None: break - with open(p, 'rb') as f: - self._buf = memoryview(f.read()) + self._f = open(p, 'rb') + count = self._f.readinto(view[n:]) + if not count: + self._f.close() + self._f = None continue - take = min(len(b) - n, len(self._buf)) - b[n:n + take] = self._buf[:take] - self._buf = self._buf[take:] - n += take + n += count return n def open_file_chunked(path): From b1f2e638de2d5ec81c88637c0a3236fae8c4d20c Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 20 Jul 2026 16:00:20 -0700 Subject: [PATCH 033/325] fix branch cleanup burning GH API requests (#38392) * ci: stop branch cleanup from burning API rate limit * simpler --- .github/workflows/repo-maintenance.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 0fb1027a8d..0421efe6e8 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -89,6 +89,14 @@ jobs: const { owner, repo } = context.repo; const upstream = `${owner}/${repo}`; + const closed = context.payload.pull_request; + if (closed) { + if (closed.head.repo?.full_name === upstream) { + await github.rest.git.deleteRef({ owner, repo, ref: `heads/${closed.head.ref}` }).catch(console.log); + } + return; + } + for await (const response of github.paginate.iterator(github.rest.pulls.list, { owner, repo, From 031b1ad0a3d4e59e7b4cfb53fb1dbc01fa004cd9 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 16:30:42 -0700 Subject: [PATCH 034/325] longcontrol: remove starting state (#38340) --- opendbc_repo | 2 +- .../selfdrive/controls/lib/longcontrol.py | 23 ++------ .../controls/tests/test_longcontrol.py | 56 +++++++------------ 3 files changed, 27 insertions(+), 54 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 78a1c9e73d..938043e24a 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 78a1c9e73de2e0a081cb2bb41f7f10281b9eeabf +Subproject commit 938043e24a0ff1a3a6f81a4c793822d4733f09ef diff --git a/openpilot/selfdrive/controls/lib/longcontrol.py b/openpilot/selfdrive/controls/lib/longcontrol.py index 977add1285..07abd0d5d3 100644 --- a/openpilot/selfdrive/controls/lib/longcontrol.py +++ b/openpilot/selfdrive/controls/lib/longcontrol.py @@ -10,8 +10,7 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] LongCtrlState = car.CarControl.Actuators.LongControlState -def long_control_state_trans(CP, active, long_control_state, v_ego, - should_stop, brake_pressed, cruise_standstill): +def long_control_state_trans(active, long_control_state, should_stop, brake_pressed, cruise_standstill): starting_condition = (not should_stop and not cruise_standstill and not brake_pressed) @@ -23,22 +22,17 @@ def long_control_state_trans(CP, active, long_control_state, v_ego, if long_control_state == LongCtrlState.off: if not starting_condition: long_control_state = LongCtrlState.stopping - elif CP.startingState: - long_control_state = LongCtrlState.starting else: long_control_state = LongCtrlState.pid elif long_control_state == LongCtrlState.stopping: - if starting_condition and CP.startingState: - long_control_state = LongCtrlState.starting - elif starting_condition: + if starting_condition: long_control_state = LongCtrlState.pid - elif long_control_state in [LongCtrlState.starting, LongCtrlState.pid]: + elif long_control_state == LongCtrlState.pid: if should_stop: long_control_state = LongCtrlState.stopping - elif v_ego > CP.vEgoStarting: - long_control_state = LongCtrlState.pid + return long_control_state class LongControl: @@ -58,9 +52,8 @@ class LongControl: self.pid.neg_limit = accel_limits[0] self.pid.pos_limit = accel_limits[1] - self.long_control_state = long_control_state_trans(self.CP, active, self.long_control_state, CS.vEgo, - should_stop, CS.brakePressed, - CS.cruiseState.standstill) + self.long_control_state = long_control_state_trans(active, self.long_control_state, should_stop, + CS.brakePressed, CS.cruiseState.standstill) if self.long_control_state == LongCtrlState.off: self.reset() output_accel = 0. @@ -72,10 +65,6 @@ class LongControl: output_accel -= self.CP.stoppingDecelRate * DT_CTRL self.reset() - elif self.long_control_state == LongCtrlState.starting: - output_accel = self.CP.startAccel - self.reset() - else: # LongCtrlState.pid error = a_target - CS.aEgo output_accel = self.pid.update(error, speed=CS.vEgo, diff --git a/openpilot/selfdrive/controls/tests/test_longcontrol.py b/openpilot/selfdrive/controls/tests/test_longcontrol.py index f69c54a3cd..2c52132068 100644 --- a/openpilot/selfdrive/controls/tests/test_longcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_longcontrol.py @@ -1,56 +1,40 @@ -from opendbc.car.structs import car from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans - - class TestLongControlStateTransition: def test_stay_stopped(self): - CP = car.CarParams.new_message() active = True current_state = LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(active, current_state, should_stop=True, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=True, cruise_standstill=False) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=True) assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0, + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.pid active = False - next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0, + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.off - def test_engage(self): - CP = car.CarParams.new_message() - active = True - current_state = LongCtrlState.off - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=True, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=True, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=True) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid - - def test_starting(self): - CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5) - active = True - current_state = LongCtrlState.starting - next_state = long_control_state_trans(CP, active, current_state, v_ego=0.1, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.starting - next_state = long_control_state_trans(CP, active, current_state, v_ego=1.0, - should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid +def test_engage(): + active = True + current_state = LongCtrlState.off + next_state = long_control_state_trans(active, current_state, + should_stop=True, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, + should_stop=False, brake_pressed=True, cruise_standstill=False) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, + should_stop=False, brake_pressed=False, cruise_standstill=True) + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, + should_stop=False, brake_pressed=False, cruise_standstill=False) + assert next_state == LongCtrlState.pid From fdd1df79fbf80e83e8f7168719c067d527443e72 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jul 2026 19:52:23 -0700 Subject: [PATCH 035/325] longitudinal: remove per-car stopping tunes (#38394) * remove per-car longitudinal stopping tunes * bump opendbc * lil more * lil more * lil more * lil more * revert modeld for now --- opendbc_repo | 2 +- openpilot/selfdrive/controls/lib/drive_helpers.py | 8 +++++--- openpilot/selfdrive/controls/lib/longcontrol.py | 3 ++- openpilot/selfdrive/controls/lib/longitudinal_planner.py | 2 +- openpilot/tools/joystick/joystickd.py | 2 +- openpilot/tools/longitudinal_maneuvers/maneuversd.py | 8 ++++---- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 938043e24a..d4c6f68c39 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 938043e24a0ff1a3a6f81a4c793822d4733f09ef +Subproject commit d4c6f68c39dbab1c2663bd3c72a0087f93858213 diff --git a/openpilot/selfdrive/controls/lib/drive_helpers.py b/openpilot/selfdrive/controls/lib/drive_helpers.py index 5392ff8875..497f8d1bdf 100644 --- a/openpilot/selfdrive/controls/lib/drive_helpers.py +++ b/openpilot/selfdrive/controls/lib/drive_helpers.py @@ -15,6 +15,9 @@ MAX_LATERAL_JERK = 5.0 # m/s^3 MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2 +def should_stop(v_ego: float, a_target: float) -> bool: + return bool(v_ego < 0.25 and a_target < 0.1) + def clamp(val, min_val, max_val): clamped_val = float(np.clip(val, min_val, max_val)) return clamped_val, clamped_val != val @@ -40,7 +43,7 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, b return float(new_curvature), limited_accel or limited_max_curv -def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.3): +def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL): if len(speeds) == len(t_idxs): v_now = speeds[0] a_now = accels[0] @@ -53,8 +56,7 @@ def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0. v_now = 0.0 v_target = 0.0 a_target = 0.0 - should_stop = (v_now < vEgoStopping and a_target < 0.1) - return a_target, should_stop + return a_target, should_stop(v_now, a_target) def curv_from_psis(psi_target, psi_rate, vego, action_t): vego = np.clip(vego, MIN_SPEED, np.inf) diff --git a/openpilot/selfdrive/controls/lib/longcontrol.py b/openpilot/selfdrive/controls/lib/longcontrol.py index 07abd0d5d3..437bcef777 100644 --- a/openpilot/selfdrive/controls/lib/longcontrol.py +++ b/openpilot/selfdrive/controls/lib/longcontrol.py @@ -62,7 +62,8 @@ class LongControl: output_accel = self.last_output_accel if output_accel > self.CP.stopAccel: output_accel = min(output_accel, 0.0) - output_accel -= self.CP.stoppingDecelRate * DT_CTRL + # TODO: can we just go straight to stopAccel? + output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop self.reset() else: # LongCtrlState.pid diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 884b4d00bc..116de14e0e 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -129,7 +129,7 @@ class LongitudinalPlanner: action_t = self.CP.longitudinalActuatorDelay + DT_MDL output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, - action_t=action_t, vEgoStopping=self.CP.vEgoStopping) + action_t=action_t) output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop diff --git a/openpilot/tools/joystick/joystickd.py b/openpilot/tools/joystick/joystickd.py index a1c00746b6..ac2f99f67e 100755 --- a/openpilot/tools/joystick/joystickd.py +++ b/openpilot/tools/joystick/joystickd.py @@ -48,7 +48,7 @@ def joystickd_thread(): if CC.longActive: actuators.accel = 4.0 * float(np.clip(joystick_axes[0], -1, 1)) - actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > CP.vEgoStopping else LongCtrlState.stopping + actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > 0.1 else LongCtrlState.stopping CC.cruiseControl.resume = actuators.accel > 0.0 if CC.latActive: diff --git a/openpilot/tools/longitudinal_maneuvers/maneuversd.py b/openpilot/tools/longitudinal_maneuvers/maneuversd.py index 8cdc2f26cf..0e20840274 100755 --- a/openpilot/tools/longitudinal_maneuvers/maneuversd.py +++ b/openpilot/tools/longitudinal_maneuvers/maneuversd.py @@ -3,11 +3,11 @@ import numpy as np from dataclasses import dataclass from openpilot.cereal import messaging -from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.controls.lib.drive_helpers import should_stop @dataclass @@ -139,8 +139,8 @@ MANEUVERS = [ def main(): params = Params() - cloudlog.info("joystickd is waiting for CarParams") - CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) + cloudlog.info("maneuversd is waiting for CarParams") + params.get("CarParams", block=True) sm = messaging.SubMaster(['carState', 'carControl', 'controlsState', 'selfdriveState', 'modelV2'], poll='modelV2') pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'alertDebug']) @@ -178,7 +178,7 @@ def main(): pm.send('alertDebug', alert_msg) longitudinalPlan.aTarget = accel - longitudinalPlan.shouldStop = v_ego < CP.vEgoStopping and accel < 1e-2 + longitudinalPlan.shouldStop = should_stop(v_ego, accel) longitudinalPlan.allowBrake = True longitudinalPlan.allowThrottle = True From ebb5cc12f5d8ac46002e3ac35575c9f7c611f6f6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 10:55:26 -0700 Subject: [PATCH 036/325] ci: don't pay the big model tax yet (#38371) * ci: don't pay the big model tax yet * skip big in release --- .github/workflows/docs.yaml | 5 +++++ .github/workflows/model_review.yaml | 7 ++++++- .github/workflows/release.yaml | 5 +++++ .github/workflows/repo-maintenance.yaml | 3 +++ .github/workflows/tests.yaml | 3 +++ tools/release/release_files.py | 3 +++ 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2d7da672e7..4be4fac916 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -15,6 +15,11 @@ concurrency: group: docs-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true +env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + jobs: docs: name: build docs diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index 82c732dc3b..d491b6397a 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -7,6 +7,11 @@ on: - 'openpilot/selfdrive/modeld/models/*.onnx' workflow_dispatch: +env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + jobs: comment: permissions: @@ -39,4 +44,4 @@ jobs: uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 with: header: model-review - message: ${{ steps.report.outputs.content }} \ No newline at end of file + message: ${{ steps.report.outputs.content }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6916fd5ce8..26b28ab330 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,11 @@ on: - cron: '0 9 * * *' workflow_dispatch: +env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + jobs: build_masterci: name: build master-ci diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 0421efe6e8..e4ca9b2129 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -9,6 +9,9 @@ on: env: PYTHONPATH: ${{ github.workspace }} + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx jobs: package_updates: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 08206ee1e2..d0de0fb680 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -21,6 +21,9 @@ env: CI: 1 PYTHONPATH: ${{ github.workspace }} PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: lfs.fetchexclude + GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx jobs: build_release: diff --git a/tools/release/release_files.py b/tools/release/release_files.py index dd6b16253c..223e3f2c77 100755 --- a/tools/release/release_files.py +++ b/tools/release/release_files.py @@ -12,6 +12,9 @@ blacklist = [ "matlab.*.md", + # skip big model for now + "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx", + # no LFS or submodules in release ".lfsconfig", ".gitattributes", From 911f07ee8763eef8e509cedd847e20a0d494c6d9 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 11:02:34 -0700 Subject: [PATCH 037/325] convert tests to unittest (#38387) --- .github/workflows/tests.yaml | 8 +- conftest.py | 92 +--------- .../cereal/messaging/tests/test_messaging.py | 3 +- .../messaging/tests/test_pub_sub_master.py | 5 +- .../cereal/messaging/tests/test_services.py | 3 +- .../hardware/tici/tests/test_agnos_updater.py | 3 +- .../hardware/tici/tests/test_amplifier.py | 14 +- openpilot/common/parameterized.py | 72 ++++++-- openpilot/common/test.py | 172 ++++++++++++++++++ openpilot/common/tests/test_file_helpers.py | 3 +- openpilot/common/tests/test_markdown.py | 3 +- openpilot/common/tests/test_params.py | 12 +- openpilot/common/tests/test_simple_kalman.py | 3 +- .../transformations/tests/test_coordinates.py | 3 +- .../transformations/tests/test_orientation.py | 10 +- .../selfdrive/car/tests/big_cars_test.sh | 2 +- .../car/tests/test_car_interfaces.py | 3 +- .../selfdrive/car/tests/test_cruise_speed.py | 8 +- openpilot/selfdrive/car/tests/test_docs.py | 3 +- openpilot/selfdrive/car/tests/test_models.py | 12 +- .../controls/tests/test_following_distance.py | 7 +- .../controls/tests/test_latcontrol.py | 3 +- .../tests/test_latcontrol_torque_buffer.py | 3 +- .../selfdrive/controls/tests/test_leads.py | 3 +- .../controls/tests/test_longcontrol.py | 25 +-- .../tests/test_torqued_lat_accel_offset.py | 3 +- .../locationd/test/test_calibrationd.py | 3 +- .../selfdrive/locationd/test/test_lagd.py | 7 +- .../test/test_locationd_scenarios.py | 21 ++- .../selfdrive/locationd/test/test_paramsd.py | 3 +- .../selfdrive/locationd/test/test_torqued.py | 3 +- .../selfdrive/monitoring/test_monitoring.py | 3 +- .../selfdrive/pandad/tests/test_pandad.py | 6 +- .../pandad/tests/test_pandad_loopback.py | 6 +- .../selfdrive/pandad/tests/test_pandad_spi.py | 6 +- .../selfdrived/tests/test_alertmanager.py | 3 +- .../selfdrive/selfdrived/tests/test_alerts.py | 3 +- .../selfdrived/tests/test_state_machine.py | 3 +- openpilot/selfdrive/test/cpp_harness.py | 10 - openpilot/selfdrive/test/helpers.py | 3 +- .../test_longitudinal.py | 3 +- .../test/process_replay/test_fuzzy.py | 3 +- .../test/process_replay/test_regen.py | 5 +- openpilot/selfdrive/test/test_onroad.py | 8 +- openpilot/selfdrive/test/test_power_draw.py | 6 +- .../ui/mici/tests/test_widget_leaks.py | 7 +- .../selfdrive/ui/tests/test_feedbackd.py | 10 +- .../selfdrive/ui/tests/test_raylib_ui.py | 3 +- openpilot/selfdrive/ui/tests/test_soundd.py | 4 +- .../selfdrive/ui/tests/test_translations.py | 13 +- openpilot/system/athena/tests/test_athenad.py | 15 +- .../system/athena/tests/test_athenad_ping.py | 9 +- .../system/athena/tests/test_registration.py | 3 +- openpilot/system/camerad/test/test_camerad.py | 44 +++-- .../hardware/tests/test_fan_controller.py | 15 +- .../hardware/tests/test_power_monitoring.py | 10 +- .../loggerd/tests/loggerd_tests_common.py | 3 +- .../system/loggerd/tests/test_deleter.py | 2 +- .../system/loggerd/tests/test_encoder.py | 6 +- .../system/loggerd/tests/test_loggerd.py | 12 +- .../system/loggerd/tests/test_uploader.py | 2 +- openpilot/system/manager/test/test_manager.py | 7 +- .../system/sensord/tests/test_sensord.py | 6 +- openpilot/system/tests/test_logmessaged.py | 4 +- openpilot/system/ubloxd/tests/test_pigeond.py | 6 +- .../ui/lib/tests/test_handle_state_change.py | 37 ++-- .../webrtc/tests/test_stream_session.py | 3 +- openpilot/test_native.py | 25 +++ .../tools/jotpluggler/test_jotpluggler.py | 3 +- openpilot/tools/lib/tests/test_caching.py | 12 +- .../lib/tests/test_comma_car_segments.py | 7 +- openpilot/tools/lib/tests/test_logreader.py | 39 ++-- .../tools/lib/tests/test_route_library.py | 3 +- .../tools/plotjuggler/test_plotjuggler.py | 7 +- openpilot/tools/sim/tests/conftest.py | 8 - .../tools/sim/tests/test_metadrive_bridge.py | 15 +- openpilot/tools/sim/tests/test_sim_bridge.py | 8 +- pyproject.toml | 19 +- tools/op.sh | 4 +- uv.lock | 53 +----- 80 files changed, 573 insertions(+), 434 deletions(-) create mode 100644 openpilot/common/test.py delete mode 100755 openpilot/selfdrive/test/cpp_harness.py create mode 100644 openpilot/test_native.py delete mode 100644 openpilot/tools/sim/tests/conftest.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d0de0fb680..2a9d1ad35d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,7 +20,7 @@ concurrency: env: CI: 1 PYTHONPATH: ${{ github.workspace }} - PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical + PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical --dist worksteal GIT_CONFIG_COUNT: 1 GIT_CONFIG_KEY_0: lfs.fetchexclude GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -110,9 +110,9 @@ jobs: env: RAYLIB_BACKEND: headless run: | - # Pre-compile Python bytecode so each pytest worker doesn't need to - $PYTEST --collect-only -m 'not slow' -qq - MAX_EXAMPLES=1 $PYTEST -m 'not slow' + # Pre-compile Python bytecode so each worker doesn't need to. + python -m compileall -q -j 0 openpilot + MAX_EXAMPLES=1 SKIP_SLOW=1 $PYTEST process_replay: name: process replay diff --git a/conftest.py b/conftest.py index 48d42aed86..402540b6bf 100644 --- a/conftest.py +++ b/conftest.py @@ -1,95 +1,15 @@ -import contextlib -import gc +"""Pytest runner configuration for the unittest suite.""" + import os -import pytest -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.system.manager import manager -from openpilot.common.hardware import TICI, HARDWARE - -# these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml +# Heavy CI-only tests are invoked explicitly by their dedicated jobs. collect_ignore = [ "openpilot/selfdrive/test/process_replay/test_processes.py", "openpilot/selfdrive/test/process_replay/test_regen.py", - "openpilot/tools/sim/", ] -def pytest_sessionstart(session): - # TODO: fix tests and enable test order randomization - if session.config.pluginmanager.hasplugin('randomly'): - session.config.option.randomly_reorganize = False - - -@pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_runtest_call(item): - # ensure we run as a hook after capturemanager's - if item.get_closest_marker("nocapture") is not None: - capmanager = item.config.pluginmanager.getplugin("capturemanager") - with capmanager.global_and_fixture_disabled(): - yield - else: - yield - - -@contextlib.contextmanager -def clean_env(): - starting_env = dict(os.environ) - yield - os.environ.clear() - os.environ.update(starting_env) - - -@pytest.fixture(scope="function", autouse=True) -def openpilot_function_fixture(request): - with clean_env(): - # setup a clean environment for each test - with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: - prefix = os.environ["OPENPILOT_PREFIX"] - - yield - - # ensure the test doesn't change the prefix - assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"] - - # cleanup any started processes - manager.manager_cleanup() - - # some processes disable gc for performance, re-enable here - if not gc.isenabled(): - gc.enable() - gc.collect() - -# If you use setUpClass, the environment variables won't be cleared properly, -# so we need to hook both the function and class pytest fixtures -@pytest.fixture(scope="class", autouse=True) -def openpilot_class_fixture(): - with clean_env(): - yield - - -@pytest.fixture(scope="function") -def tici_setup_fixture(request, openpilot_function_fixture): - """Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first.""" - if 'skip_tici_setup' in request.keywords: - return - HARDWARE.initialize_hardware() - HARDWARE.set_power_save(False) - os.system("pkill -9 -f athena") - - -@pytest.hookimpl(tryfirst=True) -def pytest_collection_modifyitems(config, items): - skipper = pytest.mark.skip(reason="Skipping tici test on PC") - for item in items: - if "tici" in item.keywords: - if not TICI: - item.add_marker(skipper) - else: - item.fixturenames.append('tici_setup_fixture') - - if "xdist_group_class_property" in item.keywords: - class_property_name = item.get_closest_marker('xdist_group_class_property').args[0] - class_property_value = getattr(item.cls, class_property_name) - item.add_marker(pytest.mark.xdist_group(class_property_value)) +def pytest_collection_modifyitems(items): + if os.environ.get("SKIP_SLOW"): + items[:] = [item for item in items if not getattr(getattr(item, "cls", None), "SLOW_TEST", False)] diff --git a/openpilot/cereal/messaging/tests/test_messaging.py b/openpilot/cereal/messaging/tests/test_messaging.py index 92d77f1b35..5e92cc7d04 100644 --- a/openpilot/cereal/messaging/tests/test_messaging.py +++ b/openpilot/cereal/messaging/tests/test_messaging.py @@ -4,6 +4,7 @@ import numbers import random import threading import time +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.cereal import log @@ -46,7 +47,7 @@ def delayed_send(delay, sock, dat): threading.Timer(delay, send_func).start() -class TestMessaging: +class TestMessaging(OpenpilotTestCase): @parameterized.expand(events) def test_new_message(self, evt): try: diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index 7353764aea..4ac7fdbd75 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -3,13 +3,14 @@ import time from typing import cast from collections.abc import Sized +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \ random_bytes, random_carstate, assert_carstate from openpilot.cereal.services import SERVICE_LIST -class TestSubMaster: +class TestSubMaster(OpenpilotTestCase): def test_init(self): sm = messaging.SubMaster(events) @@ -107,7 +108,7 @@ class TestSubMaster: assert sm[sock].vEgo == n -class TestPubMaster: +class TestPubMaster(OpenpilotTestCase): def test_init(self): messaging.PubMaster(events) diff --git a/openpilot/cereal/messaging/tests/test_services.py b/openpilot/cereal/messaging/tests/test_services.py index 9ae7c3d840..f4c1b81e4f 100644 --- a/openpilot/cereal/messaging/tests/test_services.py +++ b/openpilot/cereal/messaging/tests/test_services.py @@ -1,13 +1,14 @@ import subprocess import tempfile +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized import openpilot.cereal.services as services from openpilot.cereal.services import SERVICE_LIST -class TestServices: +class TestServices(OpenpilotTestCase): @parameterized.expand(SERVICE_LIST.keys()) def test_services(self, s): diff --git a/openpilot/common/hardware/tici/tests/test_agnos_updater.py b/openpilot/common/hardware/tici/tests/test_agnos_updater.py index a1bbd363fd..f096426707 100644 --- a/openpilot/common/hardware/tici/tests/test_agnos_updater.py +++ b/openpilot/common/hardware/tici/tests/test_agnos_updater.py @@ -6,7 +6,8 @@ TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) MANIFEST = os.path.join(TEST_DIR, "../agnos.json") -class TestAgnosUpdater: +from openpilot.common.test import OpenpilotTestCase +class TestAgnosUpdater(OpenpilotTestCase): def test_manifest(self): with open(MANIFEST) as f: diff --git a/openpilot/common/hardware/tici/tests/test_amplifier.py b/openpilot/common/hardware/tici/tests/test_amplifier.py index 3e36ff3a3d..08b8c9be80 100644 --- a/openpilot/common/hardware/tici/tests/test_amplifier.py +++ b/openpilot/common/hardware/tici/tests/test_amplifier.py @@ -1,18 +1,14 @@ -import pytest import time import subprocess from panda import Panda -from openpilot.common.hardware import TICI, HARDWARE +from openpilot.common.test import OpenpilotTestCase +from openpilot.common.hardware import HARDWARE from openpilot.common.hardware.tici.amplifier import Amplifier -class TestAmplifier: - - @classmethod - def setup_class(cls): - if not TICI: - pytest.skip() +class TestAmplifier(OpenpilotTestCase): + TICI_TEST = True def setup_method(self): # clear dmesg @@ -65,4 +61,4 @@ class TestAmplifier: if self._check_for_i2c_errors(True): break else: - pytest.fail("didn't hit any i2c errors") + self.fail("didn't hit any i2c errors") diff --git a/openpilot/common/parameterized.py b/openpilot/common/parameterized.py index f36d649dad..2782fb9a57 100644 --- a/openpilot/common/parameterized.py +++ b/openpilot/common/parameterized.py @@ -1,7 +1,7 @@ import re import sys -import pytest import inspect +import unittest def _to_safe_name(s): @@ -10,22 +10,60 @@ def _to_safe_name(s): class parameterized: @staticmethod - def expand(cases): + def expand(cases, names=None, ids=None, serial=False): cases = list(cases) if not cases: - return lambda func: pytest.mark.skip("no parameterized cases")(func) + return lambda func: unittest.skip("no parameterized cases")(func) - def decorator(func): - params = [p for p in inspect.signature(func).parameters if p != 'self'] - normalized = [c if isinstance(c, tuple) else (c,) for c in cases] - # Infer arg count from first case so extra params (e.g. from @given) are left untouched - expand_params = params[: len(normalized[0])] - if len(expand_params) == 1: - return pytest.mark.parametrize(expand_params[0], [c[0] for c in normalized])(func) - return pytest.mark.parametrize(', '.join(expand_params), normalized)(func) + if serial: + def decorator(func): + normalized = [case if isinstance(case, tuple) else (case,) for case in cases] - return decorator + def wrapper(self): + for case in normalized: + with self.subTest(): + func(self, *case) + + wrapper.__name__ = func.__name__ + wrapper.__doc__ = func.__doc__ + return wrapper + + return decorator + + return lambda func: _Expanded(func, cases, names, ids) + + +class _Expanded: + """Descriptor that turns every parameter case into a real unittest method.""" + + def __init__(self, func, cases, names, ids): + self.func = func + self.cases = [c if isinstance(c, tuple) else (c,) for c in cases] + self.names = names + self.ids = ids + + def __set_name__(self, owner, name): + params = [p for p in inspect.signature(self.func).parameters if p != "self"] + + for index, case in enumerate(self.cases): + label = self.ids(*case) if self.ids is not None else None + method_name = f"{name}_{index}" + (f"_{_to_safe_name(label)}" if label is not None else "") + + def test_method(test_case, current_case=case): + if self.names is None: + self.func(test_case, *current_case) + else: + values = dict(zip(self.names, current_case, strict=True)) + values.update({param: test_case._fixture(param) for param in params if param not in values}) + self.func(test_case, **values) + + test_method.__name__ = method_name + test_method.__doc__ = self.func.__doc__ + setattr(owner, method_name, test_method) + + # The descriptor itself is only a method factory, not a test. + setattr(owner, name, None) def parameterized_class(attrs, input_list=None): @@ -39,16 +77,16 @@ def parameterized_class(attrs, input_list=None): def decorator(cls): globs = sys._getframe(1).f_globals for i, params in enumerate(params_list): - # append sanitized string param values so pytest -k can filter by them + # Append sanitized values so unittest's -k can filter by them. suffix = "_".join(filter(None, (_to_safe_name(v) for v in params.values() if isinstance(v, str)))) name = f"{cls.__name__}_{i}" + (f"_{suffix}" if suffix else "") new_cls = type(name, (cls,), dict(params)) new_cls.__module__ = cls.__module__ - new_cls.__test__ = True # override inherited False so pytest collects this subclass + new_cls.__unittest_skip__ = False globs[name] = new_cls - # Don't collect the un-parametrised base, but return it so outer decorators - # (e.g. @pytest.mark.skip) land on it and propagate to subclasses via MRO. - cls.__test__ = False + # Don't collect the un-parametrised base. + cls.__unittest_skip__ = True + cls.__unittest_skip_why__ = "parameterized base class" return cls return decorator diff --git a/openpilot/common/test.py b/openpilot/common/test.py new file mode 100644 index 0000000000..32d06759c4 --- /dev/null +++ b/openpilot/common/test.py @@ -0,0 +1,172 @@ +import contextlib +import gc +import inspect +import os +import subprocess +import unittest +from unittest import mock + +from openpilot.common.hardware import HARDWARE, TICI +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.system.manager import manager + + +@contextlib.contextmanager +def clean_env(): + starting_env = dict(os.environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(starting_env) + + +class OpenpilotTestCase(unittest.TestCase): + """TestCase with openpilot's per-test isolation and legacy hook support.""" + + TICI_TEST = False + SKIP_TICI_SETUP = False + SHARED_DOWNLOAD_CACHE = False + SLOW_TEST = False + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + # Hide legacy pytest xunit hook names from pytest. unittest invokes the + # preserved hooks inside the OpenpilotPrefix boundary below. + for name in ("setup_method", "teardown_method"): + hook = cls.__dict__.get(name) + if hook is not None: + setattr(cls, f"openpilot_{name}", hook) + setattr(cls, name, None) + + def _fixture(self, name): + if name == "mocker": + return Mocker(self.addCleanup) + if name == "monkeypatch": + return MonkeyPatch(self.addCleanup) + if name == "subtests": + return SubTests(self) + + fixture = getattr(inspect.getmodule(type(self)), name) + kwargs = {p: self._fixture(p) for p in inspect.signature(fixture).parameters} + value = fixture(**kwargs) + if inspect.isgenerator(value): + generator = value + value = next(generator) + self.addCleanup(lambda: next(generator, None)) + return value + + def _callTestMethod(self, method): + params = [name for name, param in inspect.signature(method).parameters.items() + if param.default is inspect.Parameter.empty] + return method(**{name: self._fixture(name) for name in params}) + + def run(self, result=None): + # This boundary cannot live in setUp/tearDown: existing unittest classes + # are allowed to override those hooks without calling super(). + if ((self.SLOW_TEST and os.environ.get("SKIP_SLOW")) or + (self.TICI_TEST and not TICI) or getattr(type(self), "__unittest_skip__", False)): + return super().run(result) + test_env = clean_env() + test_env.__enter__() + prefix = OpenpilotPrefix(shared_download_cache=self.SHARED_DOWNLOAD_CACHE) + prefix.__enter__() + try: + return super().run(result) + finally: + prefix.__exit__(None, None, None) + manager.manager_cleanup() + if not gc.isenabled(): + gc.enable() + gc.collect() + test_env.__exit__(None, None, None) + + @classmethod + def setUpClass(cls): + super().setUpClass() + if cls.SLOW_TEST and os.environ.get("SKIP_SLOW"): + raise unittest.SkipTest("slow test") + if cls.TICI_TEST and not TICI: + raise unittest.SkipTest("Skipping tici test on PC") + cls._class_env = clean_env() + cls._class_env.__enter__() + setup_class = getattr(cls, "setup_class", None) + if setup_class is not None: + setup_class() + + @classmethod + def tearDownClass(cls): + try: + teardown_class = getattr(cls, "teardown_class", None) + if teardown_class is not None: + teardown_class() + finally: + cls._class_env.__exit__(None, None, None) + super().tearDownClass() + + def setUp(self): + super().setUp() + if self.TICI_TEST and not TICI: + self.skipTest("Skipping tici test on PC") + + if self.TICI_TEST and not self.SKIP_TICI_SETUP: + HARDWARE.initialize_hardware() + HARDWARE.set_power_save(False) + subprocess.run(["pkill", "-9", "-f", "athena"], check=False) + + setup_method = getattr(self, "openpilot_setup_method", None) + if setup_method is not None: + setup_method() + + def tearDown(self): + try: + teardown_method = getattr(self, "openpilot_teardown_method", None) + if teardown_method is not None: + teardown_method() + finally: + super().tearDown() + + +class Mocker: + Mock = mock.Mock + MagicMock = mock.MagicMock + call = mock.call + ANY = mock.ANY + + def __init__(self, add_cleanup): + self._add_cleanup = add_cleanup + self.patch = Patch(self._start) + + def _start(self, patcher): + value = patcher.start() + self._add_cleanup(patcher.stop) + return value + + +class Patch: + def __init__(self, start): + self._start = start + + def __call__(self, *args, **kwargs): + return self._start(mock.patch(*args, **kwargs)) + + def object(self, *args, **kwargs): + return self._start(mock.patch.object(*args, **kwargs)) + + +class MonkeyPatch: + def __init__(self, add_cleanup): + self._add_cleanup = add_cleanup + + def setattr(self, target, name, value): + patcher = mock.patch.object(target, name, value) + patcher.start() + self._add_cleanup(patcher.stop) + + +class SubTests: + def __init__(self, test_case): + self._test_case = test_case + + def test(self, label=None, **kwargs): + return self._test_case.subTest(**kwargs) if label is None else self._test_case.subTest(label, **kwargs) diff --git a/openpilot/common/tests/test_file_helpers.py b/openpilot/common/tests/test_file_helpers.py index c2b880f873..09b6990ed6 100644 --- a/openpilot/common/tests/test_file_helpers.py +++ b/openpilot/common/tests/test_file_helpers.py @@ -1,10 +1,11 @@ import os from uuid import uuid4 +from openpilot.common.test import OpenpilotTestCase from openpilot.common.utils import atomic_write -class TestFileHelpers: +class TestFileHelpers(OpenpilotTestCase): def run_atomic_write_func(self, atomic_write_func): path = f"/tmp/tmp{uuid4()}" with atomic_write_func(path) as f: diff --git a/openpilot/common/tests/test_markdown.py b/openpilot/common/tests/test_markdown.py index d3c7e02c69..a0ddc90094 100644 --- a/openpilot/common/tests/test_markdown.py +++ b/openpilot/common/tests/test_markdown.py @@ -1,10 +1,11 @@ import os +from openpilot.common.test import OpenpilotTestCase from openpilot.common.basedir import BASEDIR from openpilot.common.markdown import parse_markdown -class TestMarkdown: +class TestMarkdown(OpenpilotTestCase): def test_all_release_notes(self): with open(os.path.join(BASEDIR, "RELEASES.md")) as f: release_notes = f.read().split("\n\n") diff --git a/openpilot/common/tests/test_params.py b/openpilot/common/tests/test_params.py index bbd411bc37..4fcc5c7f58 100644 --- a/openpilot/common/tests/test_params.py +++ b/openpilot/common/tests/test_params.py @@ -1,13 +1,13 @@ -import pytest import datetime import os import threading import time import uuid +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName -class TestParams: +class TestParams(OpenpilotTestCase): def setup_method(self): self.params = Params() @@ -50,16 +50,16 @@ class TestParams: assert self.params.get("CarParams", block=True) == b"test" def test_params_unknown_key_fails(self): - with pytest.raises(UnknownKeyName): + with self.assertRaises(UnknownKeyName): self.params.get("swag") - with pytest.raises(UnknownKeyName): + with self.assertRaises(UnknownKeyName): self.params.get_bool("swag") - with pytest.raises(UnknownKeyName): + with self.assertRaises(UnknownKeyName): self.params.put("swag", "abc", block=True) - with pytest.raises(UnknownKeyName): + with self.assertRaises(UnknownKeyName): self.params.put_bool("swag", True, block=True) def test_remove_not_there(self): diff --git a/openpilot/common/tests/test_simple_kalman.py b/openpilot/common/tests/test_simple_kalman.py index e44ac2cc57..ca0f7ece7d 100644 --- a/openpilot/common/tests/test_simple_kalman.py +++ b/openpilot/common/tests/test_simple_kalman.py @@ -1,7 +1,8 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.common.simple_kalman import KF1D -class TestSimpleKalman: +class TestSimpleKalman(OpenpilotTestCase): def setup_method(self): dt = 0.01 x0_0 = 0.0 diff --git a/openpilot/common/transformations/tests/test_coordinates.py b/openpilot/common/transformations/tests/test_coordinates.py index 0b5d1c36df..febc566a53 100644 --- a/openpilot/common/transformations/tests/test_coordinates.py +++ b/openpilot/common/transformations/tests/test_coordinates.py @@ -1,5 +1,6 @@ import numpy as np +from openpilot.common.test import OpenpilotTestCase import openpilot.common.transformations.coordinates as coord geodetic_positions = np.array([[37.7610403, -122.4778699, 115], @@ -41,7 +42,7 @@ ned_offsets_batch = np.array([[ 53.88103168, 43.83445935, -46.27488057], [ 78.56272609, 18.53100158, -43.25290759]]) -class TestNED: +class TestNED(OpenpilotTestCase): def test_small_distances(self): start_geodetic = np.array([33.8042184, -117.888593, 0.0]) local_coord = coord.LocalCoord.from_geodetic(start_geodetic) diff --git a/openpilot/common/transformations/tests/test_orientation.py b/openpilot/common/transformations/tests/test_orientation.py index 1bf94115c8..7acf16dadf 100644 --- a/openpilot/common/transformations/tests/test_orientation.py +++ b/openpilot/common/transformations/tests/test_orientation.py @@ -1,6 +1,6 @@ import numpy as np -import pytest +from openpilot.common.test import OpenpilotTestCase from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \ rot2quat, quat2rot, \ ned_euler_from_ecef @@ -30,7 +30,7 @@ ned_eulers = np.array([[ 0.46806039, -0.4881889 , 1.65697808], [ 2.50450101, 0.36304151, 0.33136365]]) -class TestOrientation: +class TestOrientation(OpenpilotTestCase): def test_quat_euler(self): for i, eul in enumerate(eulers): np.testing.assert_allclose(quats[i], euler2quat(eul), rtol=1e-7) @@ -62,13 +62,13 @@ class TestOrientation: # np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7) def test_inputs(self): - with pytest.raises(ValueError): + with self.assertRaises(ValueError): euler2quat([1, 2]) - with pytest.raises(ValueError): + with self.assertRaises(ValueError): quat2rot([1, 2, 3]) - with pytest.raises(IndexError): + with self.assertRaises(IndexError): rot2quat(np.zeros((2, 2))) def test_euler_rot_consistency(self): diff --git a/openpilot/selfdrive/car/tests/big_cars_test.sh b/openpilot/selfdrive/car/tests/big_cars_test.sh index 456fc698c5..8c0ecee41e 100755 --- a/openpilot/selfdrive/car/tests/big_cars_test.sh +++ b/openpilot/selfdrive/car/tests/big_cars_test.sh @@ -8,4 +8,4 @@ export MAX_EXAMPLES=300 export INTERNAL_SEG_CNT=300 export INTERNAL_SEG_LIST=openpilot/selfdrive/car/tests/test_models_segs.txt -cd openpilot/selfdrive/car/tests && pytest test_models.py test_car_interfaces.py +pytest -n logical --dist worksteal openpilot/selfdrive/car/tests/test_models.py openpilot/selfdrive/car/tests/test_car_interfaces.py diff --git a/openpilot/selfdrive/car/tests/test_car_interfaces.py b/openpilot/selfdrive/car/tests/test_car_interfaces.py index 7ddd73c2d8..1990b25981 100644 --- a/openpilot/selfdrive/car/tests/test_car_interfaces.py +++ b/openpilot/selfdrive/car/tests/test_car_interfaces.py @@ -1,6 +1,7 @@ import os import hypothesis.strategies as st from hypothesis import Phase, given, settings +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from opendbc.car.structs import car @@ -18,7 +19,7 @@ from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60')) -class TestCarInterfaces: +class TestCarInterfaces(OpenpilotTestCase): # FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause # many generated examples to overrun when max_examples > ~20, don't use it @parameterized.expand([(car,) for car in sorted(PLATFORMS)] + [MOCK.MOCK]) diff --git a/openpilot/selfdrive/car/tests/test_cruise_speed.py b/openpilot/selfdrive/car/tests/test_cruise_speed.py index 57e89a1117..bf53747df8 100644 --- a/openpilot/selfdrive/car/tests/test_cruise_speed.py +++ b/openpilot/selfdrive/car/tests/test_cruise_speed.py @@ -1,7 +1,7 @@ -import pytest import itertools import numpy as np +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized_class from openpilot.cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT @@ -35,18 +35,18 @@ def run_cruise_simulation(cruise, e2e, personality, t_end=20.): [True, False], # e2e log.LongitudinalPersonality.schema.enumerants, # personality [5,35])) # speed -class TestCruiseSpeed: +class TestCruiseSpeed(OpenpilotTestCase): def test_cruise_speed(self): print(f'Testing {self.speed} m/s') cruise_speed = float(self.speed) simulation_steady_state = run_cruise_simulation(cruise_speed, self.e2e, self.personality) - assert simulation_steady_state == pytest.approx(cruise_speed, abs=.01), f'Did not reach {self.speed} m/s' + self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {self.speed} m/s') # TODO: test pcmCruise @parameterized_class(('pcm_cruise',), [(False,)]) -class TestVCruiseHelper: +class TestVCruiseHelper(OpenpilotTestCase): def setup_method(self): self.CP = car.CarParams(pcmCruise=self.pcm_cruise) self.v_cruise_helper = VCruiseHelper(self.CP) diff --git a/openpilot/selfdrive/car/tests/test_docs.py b/openpilot/selfdrive/car/tests/test_docs.py index 8ccbfb5a79..9f5abba336 100644 --- a/openpilot/selfdrive/car/tests/test_docs.py +++ b/openpilot/selfdrive/car/tests/test_docs.py @@ -1,8 +1,9 @@ from opendbc.car.docs import generate_cars_md, get_all_car_docs +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE -class TestCarDocs: +class TestCarDocs(OpenpilotTestCase): @classmethod def setup_class(cls): cls.all_cars = get_all_car_docs() diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index 5ad31f93ab..e8d9c6cb42 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -1,12 +1,12 @@ import time import os -import pytest import random import unittest from collections import defaultdict, Counter import hypothesis.strategies as st from hypothesis import Phase, given, settings from openpilot.common.parameterized import parameterized_class +from openpilot.common.test import OpenpilotTestCase from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces @@ -66,9 +66,9 @@ def get_test_cases() -> list[tuple[str, CarTestRoute | None]]: return test_cases -@pytest.mark.slow -@pytest.mark.shared_download_cache -class TestCarModelBase(unittest.TestCase): +class TestCarModelBase(OpenpilotTestCase): + SLOW_TEST = True + SHARED_DOWNLOAD_CACHE = True platform: Platform | None = None test_route: CarTestRoute | None = None @@ -302,8 +302,7 @@ class TestCarModelBase(unittest.TestCase): CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(resume=True)) test_car_controller(CC.as_reader()) - # Skip stdout/stderr capture with pytest, causes elevated memory usage - @pytest.mark.nocapture + # Capturing stdout/stderr here causes elevated memory usage. @settings(max_examples=MAX_EXAMPLES, deadline=None, phases=(Phase.reuse, Phase.generate, Phase.shrink)) @given(data=st.data()) @@ -471,7 +470,6 @@ class TestCarModelBase(unittest.TestCase): @parameterized_class(('platform', 'test_route'), get_test_cases()) -@pytest.mark.xdist_group_class_property('test_route') class TestCarModel(TestCarModelBase): pass diff --git a/openpilot/selfdrive/controls/tests/test_following_distance.py b/openpilot/selfdrive/controls/tests/test_following_distance.py index fcabce0387..1914769282 100644 --- a/openpilot/selfdrive/controls/tests/test_following_distance.py +++ b/openpilot/selfdrive/controls/tests/test_following_distance.py @@ -1,5 +1,5 @@ -import pytest import itertools +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized_class from openpilot.cereal import log @@ -36,11 +36,12 @@ def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personalit log.LongitudinalPersonality.standard, log.LongitudinalPersonality.aggressive], [0,10,35])) # speed -class TestFollowingDistance: +class TestFollowingDistance(OpenpilotTestCase): def test_following_distance(self): v_lead = float(self.speed) simulation_steady_state = run_following_distance_simulation(v_lead, e2e=self.e2e, personality=self.personality) correct_steady_state = desired_follow_distance(v_lead, v_lead, get_T_FOLLOW(self.personality)) err_ratio = 0.2 if self.e2e else 0.1 abs_err_margin = 0.5 if v_lead > 0.0 else 1.15 - assert simulation_steady_state == pytest.approx(correct_steady_state, abs=err_ratio * correct_steady_state + abs_err_margin) + self.assertAlmostEqual(simulation_steady_state, correct_steady_state, + delta=err_ratio * correct_steady_state + abs_err_margin) diff --git a/openpilot/selfdrive/controls/tests/test_latcontrol.py b/openpilot/selfdrive/controls/tests/test_latcontrol.py index 8389a1e31a..7f2ab8d6b7 100644 --- a/openpilot/selfdrive/controls/tests/test_latcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.cereal import log @@ -14,7 +15,7 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle -class TestLatControl: +class TestLatControl(OpenpilotTestCase): @parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)]) diff --git a/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py index 9e04316798..65befdec0f 100644 --- a/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.cereal import log @@ -16,7 +17,7 @@ def get_controller(car_name): controller = LatControlTorque(CP.as_reader(), CI, DT_CTRL) return controller, VM -class TestLatControlTorqueBuffer: +class TestLatControlTorqueBuffer(OpenpilotTestCase): @parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)]) def test_request_buffer_consistency(self, car_name): diff --git a/openpilot/selfdrive/controls/tests/test_leads.py b/openpilot/selfdrive/controls/tests/test_leads.py index 1956bb34ec..a3e226cb4b 100644 --- a/openpilot/selfdrive/controls/tests/test_leads.py +++ b/openpilot/selfdrive/controls/tests/test_leads.py @@ -1,10 +1,11 @@ +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.selfdrive.test.process_replay import replay_process_with_name -class TestLeads: +class TestLeads(OpenpilotTestCase): def test_radar_fault(self): # if there's no radar-related can traffic, radard should either not respond or respond with an error # this is tightly coupled with underlying car radar_interface implementation, but it's a good sanity check diff --git a/openpilot/selfdrive/controls/tests/test_longcontrol.py b/openpilot/selfdrive/controls/tests/test_longcontrol.py index 2c52132068..7d670d23df 100644 --- a/openpilot/selfdrive/controls/tests/test_longcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_longcontrol.py @@ -1,7 +1,8 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans -class TestLongControlStateTransition: +class TestLongControlStateTransition(OpenpilotTestCase): def test_stay_stopped(self): active = True @@ -23,18 +24,18 @@ class TestLongControlStateTransition: should_stop=False, brake_pressed=False, cruise_standstill=False) assert next_state == LongCtrlState.off -def test_engage(): - active = True - current_state = LongCtrlState.off - next_state = long_control_state_trans(active, current_state, + def test_engage(self): + active = True + current_state = LongCtrlState.off + next_state = long_control_state_trans(active, current_state, should_stop=True, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(active, current_state, + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=True, cruise_standstill=False) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(active, current_state, + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=True) - assert next_state == LongCtrlState.stopping - next_state = long_control_state_trans(active, current_state, + assert next_state == LongCtrlState.stopping + next_state = long_control_state_trans(active, current_state, should_stop=False, brake_pressed=False, cruise_standstill=False) - assert next_state == LongCtrlState.pid + assert next_state == LongCtrlState.pid diff --git a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py index 7599eb1e7b..4fe855544e 100644 --- a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -1,4 +1,5 @@ import numpy as np +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging from opendbc.car.structs import car from opendbc.car import ACCELERATION_DUE_TO_GRAVITY @@ -56,7 +57,7 @@ def simulate_straight_road_msgs(est): for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): est.handle_log(t, which, msg) -class TestTorquedLatAccelOffset: +class TestTorquedLatAccelOffset(OpenpilotTestCase): def test_estimated_offset(self): steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) est = get_warmed_up_estimator(steer_torques, lat_accels) diff --git a/openpilot/selfdrive/locationd/test/test_calibrationd.py b/openpilot/selfdrive/locationd/test/test_calibrationd.py index f862b369fe..b3722e344c 100644 --- a/openpilot/selfdrive/locationd/test/test_calibrationd.py +++ b/openpilot/selfdrive/locationd/test/test_calibrationd.py @@ -2,6 +2,7 @@ import random import numpy as np +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal import log from openpilot.common.params import Params @@ -29,7 +30,7 @@ def process_messages(c, cam_odo_calib, cycles, [0.0, 0.0, HEIGHT_INIT.item()], [cam_odo_height_std, cam_odo_height_std, cam_odo_height_std]) -class TestCalibrationd: +class TestCalibrationd(OpenpilotTestCase): def test_read_saved_params(self): msg = messaging.new_message('liveCalibration') diff --git a/openpilot/selfdrive/locationd/test/test_lagd.py b/openpilot/selfdrive/locationd/test/test_lagd.py index efaba6ead9..83edc00d1e 100644 --- a/openpilot/selfdrive/locationd/test/test_lagd.py +++ b/openpilot/selfdrive/locationd/test/test_lagd.py @@ -1,8 +1,9 @@ import random import numpy as np import time -import pytest +import unittest +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging, log from opendbc.car.structs import car from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \ @@ -45,7 +46,7 @@ def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_thres estimator.update_estimate() -class TestLagd: +class TestLagd(OpenpilotTestCase): def test_read_saved_params(self): params = Params() @@ -137,7 +138,7 @@ class TestLagd: assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) assert msg.liveDelay.calPerc == 100 - @pytest.mark.skipif(PC, reason="only on device") + @unittest.skipIf(PC, "only on device") def test_estimator_performance(self): mocked_CP = car.CarParams(steerActuatorDelay=0.5) estimator = LateralLagEstimator(mocked_CP, DT) diff --git a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py index 04d2d6b55b..c5fe1908bc 100644 --- a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py +++ b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py @@ -1,7 +1,11 @@ +import fcntl import numpy as np +import os +import tempfile from collections import defaultdict from enum import Enum +from openpilot.common.test import OpenpilotTestCase from openpilot.tools.lib.logreader import LogReader from openpilot.selfdrive.locationd.lagd import masked_symmetric_moving_average from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -96,7 +100,7 @@ def run_scenarios(scenario, logs): return get_select_fields_data(logs), get_select_fields_data(replayed_logs) -class TestLocationdScenarios: +class TestLocationdScenarios(OpenpilotTestCase): """ Test locationd with different scenarios. In all these scenarios, we expect the following: - locationd kalman filter should never go unstable (we care mostly about yaw_rate, roll, gpsOK, inputsOK, sensorsOK) @@ -105,7 +109,20 @@ class TestLocationdScenarios: @classmethod def setup_class(cls): - cls.logs = migrate_all(LogReader(TEST_ROUTE)) + # xdist can initialize this class in several workers at once. URLFile's + # cache writes are atomic, but cache misses are not locked, so every worker + # otherwise downloads the same route concurrently. + lock_path = os.path.join(tempfile.gettempdir(), "openpilot-locationd-scenarios.lock") + ready_path = f"{lock_path}.ready" + logs = None + with open(lock_path, "w") as lock: + fcntl.flock(lock, fcntl.LOCK_EX) + if not os.path.exists(ready_path): + logs = list(LogReader(TEST_ROUTE)) + open(ready_path, "w").close() + if logs is None: + logs = list(LogReader(TEST_ROUTE)) + cls.logs = migrate_all(logs) def test_base(self): """ diff --git a/openpilot/selfdrive/locationd/test/test_paramsd.py b/openpilot/selfdrive/locationd/test/test_paramsd.py index 74135cb875..09a067f4cf 100644 --- a/openpilot/selfdrive/locationd/test/test_paramsd.py +++ b/openpilot/selfdrive/locationd/test/test_paramsd.py @@ -1,6 +1,7 @@ import random import numpy as np +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params from openpilot.selfdrive.locationd.models.car_kf import CarKalman @@ -19,7 +20,7 @@ def get_random_live_parameters(CP): return msg -class TestParamsd: +class TestParamsd(OpenpilotTestCase): def test_read_saved_params(self): params = Params() diff --git a/openpilot/selfdrive/locationd/test/test_torqued.py b/openpilot/selfdrive/locationd/test/test_torqued.py index 4d337178a0..af3aeb95d6 100644 --- a/openpilot/selfdrive/locationd/test/test_torqued.py +++ b/openpilot/selfdrive/locationd/test/test_torqued.py @@ -1,8 +1,9 @@ from opendbc.car.structs import car +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.locationd.torqued import TorqueEstimator -class TestTorqued: +class TestTorqued(OpenpilotTestCase): def test_cal_percent(self): est = TorqueEstimator(car.CarParams()) msg = est.get_msg() diff --git a/openpilot/selfdrive/monitoring/test_monitoring.py b/openpilot/selfdrive/monitoring/test_monitoring.py index 0368dc9b67..53e40f7019 100644 --- a/openpilot/selfdrive/monitoring/test_monitoring.py +++ b/openpilot/selfdrive/monitoring/test_monitoring.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log from openpilot.common.realtime import DT_DMON from openpilot.selfdrive.monitoring.policy import DriverMonitoring, DRIVER_MONITOR_SETTINGS @@ -46,7 +47,7 @@ always_distracted = [msg_DISTRACTED] * int(TEST_TIMESPAN / DT_DMON) always_true = [True] * int(TEST_TIMESPAN / DT_DMON) always_false = [False] * int(TEST_TIMESPAN / DT_DMON) -class TestMonitoring: +class TestMonitoring(OpenpilotTestCase): def _run_seq(self, msgs, interaction, engaged, lowspeed): DM = DriverMonitoring() alert_lvls = [] diff --git a/openpilot/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py index 0f8fd9fc1a..820001bbc7 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -1,7 +1,7 @@ import os -import pytest import time +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal import log from openpilot.common.gpio import gpio_set, gpio_init @@ -13,8 +13,8 @@ from openpilot.common.hardware.tici.pins import GPIO HERE = os.path.dirname(os.path.realpath(__file__)) -@pytest.mark.tici -class TestPandad: +class TestPandad(OpenpilotTestCase): + TICI_TEST = True def teardown_method(self): managed_processes['pandad'].stop() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py index 0d0aa80046..8dd064bd73 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py @@ -2,10 +2,10 @@ import os import copy import random import time -import pytest from collections import defaultdict from pprint import pprint +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal import log from opendbc.car.structs import car @@ -69,8 +69,8 @@ def send_random_can_messages(sendcan, count): return sent_msgs -@pytest.mark.tici -class TestBoarddLoopback: +class TestBoarddLoopback(OpenpilotTestCase): + TICI_TEST = True @classmethod def setup_class(cls): os.environ['STARTED'] = '1' diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py index 2bc302b47c..79eb2dd77e 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -1,9 +1,9 @@ import os import time import numpy as np -import pytest import random +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal.services import SERVICE_LIST from openpilot.common.timeout import Timeout @@ -12,8 +12,8 @@ from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ -@pytest.mark.tici -class TestBoarddSpi: +class TestBoarddSpi(OpenpilotTestCase): + TICI_TEST = True @classmethod def setup_class(cls): os.environ['STARTED'] = '1' diff --git a/openpilot/selfdrive/selfdrived/tests/test_alertmanager.py b/openpilot/selfdrive/selfdrived/tests/test_alertmanager.py index 030b7d4515..23a01d184c 100644 --- a/openpilot/selfdrive/selfdrived/tests/test_alertmanager.py +++ b/openpilot/selfdrive/selfdrived/tests/test_alertmanager.py @@ -1,10 +1,11 @@ import random +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.selfdrived.events import Alert, EmptyAlert, EVENTS from openpilot.selfdrive.selfdrived.alertmanager import AlertManager -class TestAlertManager: +class TestAlertManager(OpenpilotTestCase): def test_duration(self): """ diff --git a/openpilot/selfdrive/selfdrived/tests/test_alerts.py b/openpilot/selfdrive/selfdrived/tests/test_alerts.py index 276de2a6a9..d19b31333a 100644 --- a/openpilot/selfdrive/selfdrived/tests/test_alerts.py +++ b/openpilot/selfdrive/selfdrived/tests/test_alerts.py @@ -4,6 +4,7 @@ import os import random from PIL import Image, ImageDraw, ImageFont +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log from opendbc.car.structs import car from openpilot.cereal.messaging import SubMaster @@ -24,7 +25,7 @@ for event_types in EVENTS.values(): ALERTS.append(alert) -class TestAlerts: +class TestAlerts(OpenpilotTestCase): @classmethod def setup_class(cls): diff --git a/openpilot/selfdrive/selfdrived/tests/test_state_machine.py b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py index ec8f068039..22f664718e 100644 --- a/openpilot/selfdrive/selfdrived/tests/test_state_machine.py +++ b/openpilot/selfdrive/selfdrived/tests/test_state_machine.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.selfdrived.state import StateMachine, SOFT_DISABLE_TIME @@ -21,7 +22,7 @@ def make_event(event_types: list[str | None]): return 0 -class TestStateMachine: +class TestStateMachine(OpenpilotTestCase): def setup_method(self): self.events = Events() self.state_machine = StateMachine() diff --git a/openpilot/selfdrive/test/cpp_harness.py b/openpilot/selfdrive/test/cpp_harness.py deleted file mode 100755 index f9f425102b..0000000000 --- a/openpilot/selfdrive/test/cpp_harness.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -import subprocess -import sys - -from openpilot.common.prefix import OpenpilotPrefix - -with OpenpilotPrefix(): - ret = subprocess.call(sys.argv[1:]) - -sys.exit(ret) diff --git a/openpilot/selfdrive/test/helpers.py b/openpilot/selfdrive/test/helpers.py index 9d954216b8..2bcb6d8409 100644 --- a/openpilot/selfdrive/test/helpers.py +++ b/openpilot/selfdrive/test/helpers.py @@ -3,7 +3,6 @@ import http.server import os import threading import time -import pytest from functools import wraps @@ -32,7 +31,7 @@ def release_only(f): @wraps(f) def wrap(self, *args, **kwargs): if "RELEASE" not in os.environ: - pytest.skip("This test is only for release branches") + self.skipTest("This test is only for release branches") f(self, *args, **kwargs) return wrap diff --git a/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py index b8f97c5041..9bd22902d3 100644 --- a/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py @@ -1,4 +1,5 @@ import itertools +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized_class from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE @@ -179,7 +180,7 @@ def create_maneuvers(kwargs): @parameterized_class(("e2e", "force_decel"), itertools.product([True, False], repeat=2)) -class TestLongitudinalControl: +class TestLongitudinalControl(OpenpilotTestCase): e2e: bool force_decel: bool diff --git a/openpilot/selfdrive/test/process_replay/test_fuzzy.py b/openpilot/selfdrive/test/process_replay/test_fuzzy.py index 372b368608..9a5bb41d48 100644 --- a/openpilot/selfdrive/test/process_replay/test_fuzzy.py +++ b/openpilot/selfdrive/test/process_replay/test_fuzzy.py @@ -2,6 +2,7 @@ import copy import os from hypothesis import given, HealthCheck, Phase, settings import hypothesis.strategies as st +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.cereal import log @@ -17,7 +18,7 @@ NOT_TESTED = ['selfdrived', 'controlsd', 'card', 'plannerd', 'calibrationd', 'dm TEST_CASES = [(cfg.proc_name, copy.deepcopy(cfg)) for cfg in pr.CONFIGS if cfg.proc_name not in NOT_TESTED] MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "10")) -class TestFuzzProcesses: +class TestFuzzProcesses(OpenpilotTestCase): # TODO: make this faster and increase examples @parameterized.expand(TEST_CASES) diff --git a/openpilot/selfdrive/test/process_replay/test_regen.py b/openpilot/selfdrive/test/process_replay/test_regen.py index f4942e486c..1a3dc8442a 100644 --- a/openpilot/selfdrive/test/process_replay/test_regen.py +++ b/openpilot/selfdrive/test/process_replay/test_regen.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.selfdrive.test.process_replay.regen import regen_segment @@ -26,7 +27,9 @@ def ci_setup_data_readers(route, sidx): return lr, frs -class TestRegen: +class TestRegen(OpenpilotTestCase): + SLOW_TEST = True + @parameterized.expand(TESTED_SEGMENTS) def test_engaged(self, case_name, segment): route, sidx = segment.rsplit("--", 1) diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index b15d890452..b710fce919 100644 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -1,13 +1,13 @@ import math import json import os -import pytest import shutil import subprocess import time import numpy as np from collections import Counter, defaultdict from pathlib import Path +from openpilot.common.test import OpenpilotTestCase from openpilot.common.utils import tabulate from openpilot.cereal import log @@ -102,9 +102,9 @@ def cputime_total(ct): return ct.cpuUser + ct.cpuSystem + ct.cpuChildrenUser + ct.cpuChildrenSystem -@pytest.mark.tici -@pytest.mark.skip_tici_setup -class TestOnroad: +class TestOnroad(OpenpilotTestCase): + TICI_TEST = True + SKIP_TICI_SETUP = True @classmethod def setup_class(cls): diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index 9bc012ceaa..530ce86f31 100644 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -1,8 +1,8 @@ from collections import defaultdict, deque -import pytest import time import numpy as np from dataclasses import dataclass +from openpilot.common.test import OpenpilotTestCase from openpilot.common.utils import tabulate import openpilot.cereal.messaging as messaging @@ -38,8 +38,8 @@ PROCS = [ ] -@pytest.mark.tici -class TestPowerDraw: +class TestPowerDraw(OpenpilotTestCase): + TICI_TEST = True def setup_method(self): Params().put("CarParams", get_demo_car_params().to_bytes(), block=True) diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py index 68c61f4ff3..271cb2704f 100755 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -1,6 +1,6 @@ import gc import weakref -import pytest +import unittest # FIXME: known small leaks not worth worrying about at the moment KNOWN_LEAKS = { @@ -41,8 +41,9 @@ def get_child_widgets(widget) -> list: return children -class TestWidgetLeaks: - @pytest.mark.skip(reason="segfaults") +from openpilot.common.test import OpenpilotTestCase +class TestWidgetLeaks(OpenpilotTestCase): + @unittest.skip("segfaults") def test_dialogs_do_not_leak(self): import pyray as rl rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) diff --git a/openpilot/selfdrive/ui/tests/test_feedbackd.py b/openpilot/selfdrive/ui/tests/test_feedbackd.py index 72bd499081..71803c3452 100644 --- a/openpilot/selfdrive/ui/tests/test_feedbackd.py +++ b/openpilot/selfdrive/ui/tests/test_feedbackd.py @@ -1,12 +1,14 @@ -import pytest +import unittest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.system.manager.process_config import managed_processes -@pytest.mark.skip("tmp disabled") -class TestFeedbackd: +@unittest.skip("tmp disabled") +class TestFeedbackd(OpenpilotTestCase): def setup_method(self): self.pm = messaging.PubMaster(['carState', 'rawAudioData']) self.sm = messaging.SubMaster(['audioFeedback']) @@ -25,7 +27,7 @@ class TestFeedbackd: self.pm.send('rawAudioData', audio_msg) self.sm.update(timeout=100) - @pytest.mark.parametrize("record_feedback", [False, True]) + @parameterized.expand([False, True]) def test_audio_feedback(self, record_feedback): Params().put_bool("RecordAudioFeedback", record_feedback, block=True) diff --git a/openpilot/selfdrive/ui/tests/test_raylib_ui.py b/openpilot/selfdrive/ui/tests/test_raylib_ui.py index d04c940cde..88f40ae6d9 100644 --- a/openpilot/selfdrive/ui/tests/test_raylib_ui.py +++ b/openpilot/selfdrive/ui/tests/test_raylib_ui.py @@ -1,8 +1,9 @@ import time +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.test.helpers import with_processes -class TestRaylibUi: +class TestRaylibUi(OpenpilotTestCase): @with_processes(["ui"]) def test_raylib_ui(self): """Test initialization of the UI widgets is successful.""" diff --git a/openpilot/selfdrive/ui/tests/test_soundd.py b/openpilot/selfdrive/ui/tests/test_soundd.py index da7921a559..3f2753f9fb 100644 --- a/openpilot/selfdrive/ui/tests/test_soundd.py +++ b/openpilot/selfdrive/ui/tests/test_soundd.py @@ -1,3 +1,4 @@ +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log, messaging from openpilot.cereal.messaging import SubMaster, PubMaster from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert @@ -7,7 +8,7 @@ import time AudibleAlert = log.SelfdriveState.AudibleAlert -class TestSoundd: +class TestSoundd(OpenpilotTestCase): def test_check_selfdrive_timeout_alert(self): sm = SubMaster(['selfdriveState']) pm = PubMaster(['selfdriveState']) @@ -31,4 +32,3 @@ class TestSoundd: assert check_selfdrive_timeout_alert(sm) # TODO: add test with micd for checking that soundd actually outputs sounds - diff --git a/openpilot/selfdrive/ui/tests/test_translations.py b/openpilot/selfdrive/ui/tests/test_translations.py index 9eae072f21..0d636b61c8 100644 --- a/openpilot/selfdrive/ui/tests/test_translations.py +++ b/openpilot/selfdrive/ui/tests/test_translations.py @@ -3,8 +3,9 @@ import re import string from pathlib import Path -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.ui.translations.potools import parse_po from openpilot.system.ui.lib.multilang import LANGUAGES_FILE, TRANSLATIONS_DIR @@ -46,13 +47,13 @@ def load_po_text(po_path: Path) -> str: return po_path.read_text(encoding='utf-8') -class TestTranslations: - @pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values())) +class TestTranslations(OpenpilotTestCase): + @parameterized.expand(sorted(TRANSLATION_LANGUAGES.values())) def test_translation_file_exists(self, language_code: str): po_path = PO_DIR / f"app_{language_code}.po" assert po_path.exists(), f"missing translation file: {po_path}" - @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) def test_translation_placeholders_are_preserved(self, po_path: Path): _, entries = parse_po(po_path) language = po_path.stem.removeprefix("app_") @@ -89,14 +90,14 @@ class TestTranslations: ) assert translated_placeholders == source_placeholders, message - @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) def test_translation_refs_do_not_include_line_numbers(self, po_path: Path): for line in load_po_text(po_path).splitlines(): assert not LINE_NUMBER_REF_RE.match(line), ( f"{po_path.name}: line-number source reference found: {line}" ) - @pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) + @parameterized.expand(sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name) def test_translation_entities_are_valid(self, po_path: Path): matches = BAD_ENTITY_RE.findall(load_po_text(po_path)) assert not matches, ( diff --git a/openpilot/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py index 8c090b93af..dbc1050db8 100644 --- a/openpilot/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -1,4 +1,3 @@ -import pytest from functools import wraps import json import multiprocessing @@ -14,6 +13,8 @@ from datetime import datetime, timedelta from websocket import ABNF from websocket._exceptions import WebSocketConnectionClosedException +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging from openpilot.common.params import Params @@ -47,16 +48,14 @@ def with_upload_handler(func): thread.join() return wrapper -@pytest.fixture def mock_create_connection(mocker): return mocker.patch('openpilot.system.athena.athenad.create_connection') -@pytest.fixture def host(): with http_server_context(handler=HTTPRequestHandler, setup=seed_athena_server) as (host, port): yield f"http://{host}:{port}" -class TestAthenadMethods: +class TestAthenadMethods(OpenpilotTestCase): @classmethod def setup_class(cls): cls.SOCKET_PORT = 45454 @@ -111,7 +110,7 @@ class TestAthenadMethods: assert dispatcher["echo"]("bob") == "bob" def test_get_message(self): - with pytest.raises(TimeoutError) as _: + with self.assertRaises(TimeoutError) as _: dispatcher["getMessage"]("controlsState") end_event = multiprocessing.Event() @@ -184,14 +183,14 @@ class TestAthenadMethods: if fn.endswith('.zst'): assert athenad.strip_zst_extension(fn) == fn[:-4] - @pytest.mark.parametrize("compress", [True, False]) + @parameterized.expand([True, False], names=("compress",)) def test_do_upload(self, host, compress): # random bytes to ensure rather large object post-compression fn = self._create_file('qlog', data=os.urandom(10000 * 1024)) upload_fn = fn + ('.zst' if compress else '') item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 - with pytest.raises(requests.exceptions.ConnectionError): + with self.assertRaises(requests.exceptions.ConnectionError): athenad._do_upload(item) item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 @@ -236,7 +235,7 @@ class TestAthenadMethods: # TODO: also check that end_event and metered network raises AbortTransferException assert athenad.upload_queue.qsize() == 0 - @pytest.mark.parametrize("status,retry", [(500,True), (412,False)]) + @parameterized.expand([(500,True), (412,False)], names=("status", "retry")) @with_upload_handler def test_upload_handler_retry(self, mocker, host, status, retry): mock_put = mocker.patch('openpilot.system.athena.athenad.UPLOAD_SESS.put') diff --git a/openpilot/system/athena/tests/test_athenad_ping.py b/openpilot/system/athena/tests/test_athenad_ping.py index 44a6b8a56b..d5592c40f1 100644 --- a/openpilot/system/athena/tests/test_athenad_ping.py +++ b/openpilot/system/athena/tests/test_athenad_ping.py @@ -1,9 +1,10 @@ -import pytest +import unittest import subprocess import threading import time from typing import cast +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad @@ -19,7 +20,7 @@ def wifi_radio(on: bool) -> None: subprocess.run(["nmcli", "radio", "wifi", "on" if on else "off"], check=True) -class TestAthenadPing: +class TestAthenadPing(OpenpilotTestCase): params: Params dongle_id: str @@ -90,12 +91,12 @@ class TestAthenadPing: time.sleep(0.1) print("ping received") - @pytest.mark.skipif(not TICI, reason="only run on desk") + @unittest.skipIf(not TICI, "only run on desk") def test_offroad(self, subtests, mocker) -> None: self.params.put_bool("IsOffroad", True, block=True) self.assertTimeout(60 + TIMEOUT_TOLERANCE, subtests, mocker) # based using TCP keepalive settings - @pytest.mark.skipif(not TICI, reason="only run on desk") + @unittest.skipIf(not TICI, "only run on desk") def test_onroad(self, subtests, mocker) -> None: self.params.put_bool("IsOffroad", False, block=True) self.assertTimeout(21 + TIMEOUT_TOLERANCE, subtests, mocker) diff --git a/openpilot/system/athena/tests/test_registration.py b/openpilot/system/athena/tests/test_registration.py index bb1523de80..3c75255c61 100644 --- a/openpilot/system/athena/tests/test_registration.py +++ b/openpilot/system/athena/tests/test_registration.py @@ -2,13 +2,14 @@ import json from Crypto.PublicKey import RSA from pathlib import Path +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_ID from openpilot.system.athena.tests.helpers import MockResponse from openpilot.common.hardware.hw import Paths -class TestRegistration: +class TestRegistration(OpenpilotTestCase): def setup_method(self): # clear params and setup key paths diff --git a/openpilot/system/camerad/test/test_camerad.py b/openpilot/system/camerad/test/test_camerad.py index afc49c02bf..ad98a22581 100644 --- a/openpilot/system/camerad/test/test_camerad.py +++ b/openpilot/system/camerad/test/test_camerad.py @@ -1,8 +1,9 @@ import os import time -import pytest import numpy as np +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal.services import SERVICE_LIST from openpilot.tools.lib.log_time_series import msgs_to_time_series from openpilot.system.camerad.snapshot import get_snapshots @@ -38,7 +39,6 @@ def run_and_log(procs, services, duration): with processes_context(procs): return collect_logs(services, duration) -@pytest.fixture(scope="module") def _camera_session(): """Single camerad session that collects logs and exposure data. Runs until exposure stabilizes (min TEST_TIMESPAN seconds for enough log data).""" @@ -69,20 +69,18 @@ def _camera_session(): return ts, exposure -@pytest.fixture(scope="module") -def logs(_camera_session): - return _camera_session[0] +class TestCamerad(OpenpilotTestCase): + TICI_TEST = True -@pytest.fixture(scope="module") -def exposure_data(_camera_session): - return _camera_session[1] + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.logs, cls.exposure_data = _camera_session() -@pytest.mark.tici -class TestCamerad: - @pytest.mark.parametrize("cam", CAMERAS) - def test_camera_exposure(self, exposure_data, cam): + @parameterized.expand(CAMERAS, names=("cam",)) + def test_camera_exposure(self, cam): lo, hi = EXPOSURE_RANGE - checks = exposure_data[cam] + checks = self.exposure_data[cam] assert len(checks) >= EXPOSURE_STABLE_COUNT, f"{cam}: only got {len(checks)} samples" # check that exposure converges into the valid range @@ -96,34 +94,34 @@ class TestCamerad: for i, (median, mean) in enumerate(checks): ok = _in_range(median, mean) if in_range and not ok: - pytest.fail(f"{cam}: exposure regressed on sample {i+1} " + + self.fail(f"{cam}: exposure regressed on sample {i+1} " + f"(median={median:.4f}, mean={mean:.4f}, expected: ({lo}, {hi}))") in_range = ok - def test_frame_skips(self, logs): + def test_frame_skips(self): for c in CAMERAS: - assert set(np.diff(logs[c]['frameId'])) == {1, }, f"{c} has frame skips" + assert set(np.diff(self.logs[c]['frameId'])) == {1, }, f"{c} has frame skips" - def test_frame_sync(self, logs): + def test_frame_sync(self): SYNCED_CAMS = ('roadCameraState', 'wideRoadCameraState') - n = range(len(logs['roadCameraState']['t'][:-10])) + n = range(len(self.logs['roadCameraState']['t'][:-10])) - frame_ids = {i: [logs[cam]['frameId'][i] for cam in CAMERAS] for i in n} + frame_ids = {i: [self.logs[cam]['frameId'][i] for cam in CAMERAS] for i in n} assert all(len(set(v)) == 1 for v in frame_ids.values()), "frame IDs not aligned" # road and wide cameras should be synced within 1.1ms - synced_times = {i: [logs[cam]['timestampSof'][i] for cam in SYNCED_CAMS] for i in n} + synced_times = {i: [self.logs[cam]['timestampSof'][i] for cam in SYNCED_CAMS] for i in n} diffs = {i: (max(ts) - min(ts))/1e6 for i, ts in synced_times.items()} laggy_frames = {k: v for k, v in diffs.items() if v > 1.1} assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}" # driver camera should be staggered ~25ms from road camera for i in n: - offset_ms = abs(logs['driverCameraState']['timestampSof'][i] - logs['roadCameraState']['timestampSof'][i]) / 1e6 + offset_ms = abs(self.logs['driverCameraState']['timestampSof'][i] - self.logs['roadCameraState']['timestampSof'][i]) / 1e6 assert 20 < offset_ms < 30, f"driver camera stagger out of range at frame {i}: {offset_ms:.1f}ms (expected ~25ms)" - def test_sanity_checks(self, logs): - self._sanity_checks(logs) + def test_sanity_checks(self): + self._sanity_checks(self.logs) def _sanity_checks(self, ts): for c in CAMERAS: diff --git a/openpilot/system/hardware/tests/test_fan_controller.py b/openpilot/system/hardware/tests/test_fan_controller.py index e1aceeb081..63f9967101 100644 --- a/openpilot/system/hardware/tests/test_fan_controller.py +++ b/openpilot/system/hardware/tests/test_fan_controller.py @@ -1,10 +1,11 @@ -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.system.hardware.fan_controller import FanController ALL_CONTROLLERS = [FanController] -class TestFanController: +class TestFanController(OpenpilotTestCase): def wind_up(self, controller, ignition=True): for _ in range(1000): controller.update(100, ignition) @@ -13,31 +14,31 @@ class TestFanController: for _ in range(1000): controller.update(10, ignition) - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) + @parameterized.expand(ALL_CONTROLLERS) def test_hot_onroad(self, controller_class): controller = controller_class(2) self.wind_up(controller) assert controller.update(100, True) >= 70 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) + @parameterized.expand(ALL_CONTROLLERS) def test_offroad_limits(self, controller_class): controller = controller_class(2) self.wind_up(controller) assert controller.update(100, False) <= 30 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) + @parameterized.expand(ALL_CONTROLLERS) def test_no_fan_wear(self, controller_class): controller = controller_class(2) self.wind_down(controller) assert controller.update(10, False) == 0 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) + @parameterized.expand(ALL_CONTROLLERS) def test_limited(self, controller_class): controller = controller_class(2) self.wind_up(controller, True) assert controller.update(100, True) == 100 - @pytest.mark.parametrize("controller_class", ALL_CONTROLLERS) + @parameterized.expand(ALL_CONTROLLERS) def test_windup_speed(self, controller_class): controller = controller_class(2) self.wind_down(controller, True) diff --git a/openpilot/system/hardware/tests/test_power_monitoring.py b/openpilot/system/hardware/tests/test_power_monitoring.py index f3ffb98e09..7e0a260412 100644 --- a/openpilot/system/hardware/tests/test_power_monitoring.py +++ b/openpilot/system/hardware/tests/test_power_monitoring.py @@ -1,5 +1,5 @@ -import pytest +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.system.hardware.power_monitoring import PowerMonitoring, CAR_BATTERY_CAPACITY_uWh, \ CAR_CHARGING_RATE_W, VBATT_PAUSE_CHARGING, DELAY_SHUTDOWN_TIME_S @@ -22,13 +22,9 @@ def pm_patch(mocker, name, value, constant=False): mocker.patch(f"openpilot.system.hardware.power_monitoring.{name}", return_value=value) -@pytest.fixture(autouse=True) -def mock_time(mocker): - mocker.patch("time.monotonic", mock_time_monotonic) - - -class TestPowerMonitoring: +class TestPowerMonitoring(OpenpilotTestCase): def setup_method(self): + self._fixture("mocker").patch("time.monotonic", mock_time_monotonic) self.params = Params() # Test to see that it doesn't do anything when pandaState is None diff --git a/openpilot/system/loggerd/tests/loggerd_tests_common.py b/openpilot/system/loggerd/tests/loggerd_tests_common.py index 55f5fbb816..689d2c27c0 100644 --- a/openpilot/system/loggerd/tests/loggerd_tests_common.py +++ b/openpilot/system/loggerd/tests/loggerd_tests_common.py @@ -3,6 +3,7 @@ import random from pathlib import Path +from openpilot.common.test import OpenpilotTestCase import openpilot.system.loggerd.deleter as deleter import openpilot.system.loggerd.uploader as uploader from openpilot.common.params import Params @@ -53,7 +54,7 @@ class MockApiIgnore: def get_token(self): return "fake-token" -class UploaderTestCase: +class UploaderTestCase(OpenpilotTestCase): f_type = "UNKNOWN" root: Path diff --git a/openpilot/system/loggerd/tests/test_deleter.py b/openpilot/system/loggerd/tests/test_deleter.py index 8f18c8ceee..6a0696c699 100644 --- a/openpilot/system/loggerd/tests/test_deleter.py +++ b/openpilot/system/loggerd/tests/test_deleter.py @@ -17,7 +17,7 @@ class TestDeleter(UploaderTestCase): def setup_method(self): self.f_type = "fcamera.hevc" - super().setup_method() + super().openpilot_setup_method() self.fake_stats = Stats(f_bavail=0, f_blocks=10, f_frsize=4096) deleter.os.statvfs = self.fake_statvfs # ty: ignore[invalid-assignment] # test double diff --git a/openpilot/system/loggerd/tests/test_encoder.py b/openpilot/system/loggerd/tests/test_encoder.py index 05bc211cbe..4d5a998583 100644 --- a/openpilot/system/loggerd/tests/test_encoder.py +++ b/openpilot/system/loggerd/tests/test_encoder.py @@ -1,6 +1,5 @@ import math import os -import pytest import shutil import subprocess import time @@ -8,6 +7,7 @@ from pathlib import Path from tqdm import trange +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.common.hardware import TICI @@ -29,8 +29,8 @@ CAMERAS = [ FILE_SIZE_TOLERANCE = 0.7 -@pytest.mark.tici # TODO: all of loggerd should work on PC -class TestEncoder: +class TestEncoder(OpenpilotTestCase): + TICI_TEST = True def setup_method(self): self._clear_logs() diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 3678ca95be..6bb7e9bcf7 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -8,8 +8,9 @@ import time from collections.abc import Collection from collections import defaultdict from pathlib import Path -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal import log from openpilot.cereal.services import SERVICE_LIST @@ -32,7 +33,7 @@ CEREAL_SERVICES = [f for f in log.Event.schema.union_fields if f in SERVICE_LIST and SERVICE_LIST[f].should_log and "encode" not in f.lower()] -class TestLoggerd: +class TestLoggerd(OpenpilotTestCase): def _get_latest_log_dir(self): log_dirs = sorted(Path(Paths.log_root()).iterdir(), key=lambda f: f.stat().st_mtime) return log_dirs[-1] @@ -193,7 +194,6 @@ class TestLoggerd: assert getattr(initData, initData_key) == v assert logged_params[param_key].decode() == v - @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing def test_rotation(self): Params().put("RecordFront", True, block=True) @@ -314,8 +314,7 @@ class TestLoggerd: segment_dir = self._get_latest_log_dir() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None - @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing - @pytest.mark.parametrize("record_front", [True, False]) + @parameterized.expand([True, False]) def test_record_front(self, record_front): params = Params() params.put_bool("RecordFront", record_front, block=True) @@ -325,8 +324,7 @@ class TestLoggerd: dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc')) assert dcamera_hevc_exists == record_front - @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing - @pytest.mark.parametrize("record_audio", [True, False]) + @parameterized.expand([True, False]) def test_record_audio(self, record_audio): params = Params() params.put_bool("RecordAudio", record_audio, block=True) diff --git a/openpilot/system/loggerd/tests/test_uploader.py b/openpilot/system/loggerd/tests/test_uploader.py index 3af78e2c37..8b076ef0f4 100644 --- a/openpilot/system/loggerd/tests/test_uploader.py +++ b/openpilot/system/loggerd/tests/test_uploader.py @@ -37,7 +37,7 @@ cloudlog.addHandler(log_handler) class TestUploader(UploaderTestCase): def setup_method(self): - super().setup_method() + super().openpilot_setup_method() log_handler.reset() def start_thread(self): diff --git a/openpilot/system/manager/test/test_manager.py b/openpilot/system/manager/test/test_manager.py index a3808bf29f..d5945a4401 100644 --- a/openpilot/system/manager/test/test_manager.py +++ b/openpilot/system/manager/test/test_manager.py @@ -1,9 +1,10 @@ import os -import pytest +import unittest import signal import time from opendbc.car.structs import car +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params import openpilot.system.manager.manager as manager from openpilot.system.manager.process import ensure_running @@ -16,7 +17,7 @@ MAX_STARTUP_TIME = 3 BLACKLIST_PROCS = ['manage_athenad', 'pandad', 'pigeond'] -class TestManager: +class TestManager(OpenpilotTestCase): def setup_method(self): HARDWARE.set_power_save(False) @@ -47,7 +48,7 @@ class TestManager: assert params.get("OpenpilotEnabledToggle") assert params.get("RouteCount") == 0 - @pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad") + @unittest.skip("this test is flaky the way it's currently written, should be moved to test_onroad") def test_clean_exit(self, subtests): """ Ensure all processes exit cleanly when stopped. diff --git a/openpilot/system/sensord/tests/test_sensord.py b/openpilot/system/sensord/tests/test_sensord.py index dc96886e4a..868d37c793 100644 --- a/openpilot/system/sensord/tests/test_sensord.py +++ b/openpilot/system/sensord/tests/test_sensord.py @@ -1,10 +1,10 @@ import os import subprocess -import pytest import time import numpy as np from collections import namedtuple, defaultdict +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import get_irqs_for_action @@ -54,8 +54,8 @@ def iter_measurements(events): for measurement in msgs: yield measurement, getattr(measurement, measurement.which()) -@pytest.mark.tici -class TestSensord: +class TestSensord(OpenpilotTestCase): + TICI_TEST = True @classmethod def setup_class(cls): # enable LSM self test diff --git a/openpilot/system/tests/test_logmessaged.py b/openpilot/system/tests/test_logmessaged.py index e2637fb0e6..247bfd8a42 100644 --- a/openpilot/system/tests/test_logmessaged.py +++ b/openpilot/system/tests/test_logmessaged.py @@ -2,13 +2,14 @@ import glob import os import time +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.system.manager.process_config import managed_processes from openpilot.common.hardware.hw import Paths from openpilot.common.swaglog import cloudlog, ipchandler -class TestLogmessaged: +class TestLogmessaged(OpenpilotTestCase): def setup_method(self): # clear the IPC buffer in case some other tests used cloudlog and filled it ipchandler.close() @@ -52,4 +53,3 @@ class TestLogmessaged: logsize = sum([os.path.getsize(f) for f in self._get_log_files()]) assert (n*len(msg)) < logsize < (n*(len(msg)+1024)) - diff --git a/openpilot/system/ubloxd/tests/test_pigeond.py b/openpilot/system/ubloxd/tests/test_pigeond.py index b894ed718b..b7a0cd216d 100644 --- a/openpilot/system/ubloxd/tests/test_pigeond.py +++ b/openpilot/system/ubloxd/tests/test_pigeond.py @@ -1,6 +1,6 @@ -import pytest import time +from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import gpio_read @@ -10,8 +10,8 @@ from openpilot.common.hardware.tici.pins import GPIO # TODO: test TTFF when we have good A-GNSS -@pytest.mark.tici -class TestPigeond: +class TestPigeond(OpenpilotTestCase): + TICI_TEST = True def teardown_method(self): managed_processes['pigeond'].stop() diff --git a/openpilot/system/ui/lib/tests/test_handle_state_change.py b/openpilot/system/ui/lib/tests/test_handle_state_change.py index 69aae6fdf3..a7a33834cd 100644 --- a/openpilot/system/ui/lib/tests/test_handle_state_change.py +++ b/openpilot/system/ui/lib/tests/test_handle_state_change.py @@ -3,15 +3,16 @@ Tests the state machine in isolation by constructing a WifiManager with mocked DBus, then calling _handle_state_change directly with NM state transitions. """ -import pytest +import unittest from jeepney.low_level import MessageType -from pytest_mock import MockerFixture +from openpilot.common.parameterized import parameterized +from openpilot.common.test import Mocker, OpenpilotTestCase from openpilot.system.ui.lib.networkmanager import NMDeviceState, NMDeviceStateReason from openpilot.system.ui.lib.wifi_manager import WifiManager, WifiState, ConnectStatus -def _make_wm(mocker: MockerFixture, connections=None): +def _make_wm(mocker: Mocker, connections=None): """Create a WifiManager with only the fields _handle_state_change touches.""" mocker.patch.object(WifiManager, '_initialize') wm = WifiManager.__new__(WifiManager) @@ -50,7 +51,7 @@ def fire_wpa_connect(wm: WifiManager) -> None: # Basic transitions # --------------------------------------------------------------------------- -class TestDisconnected: +class TestDisconnected(OpenpilotTestCase): def test_generic_disconnect_clears_state(self, mocker): wm = _make_wm(mocker) wm._wifi_state = WifiState(ssid="Net", status=ConnectStatus.CONNECTED) @@ -92,7 +93,7 @@ class TestDisconnected: assert wm._wifi_state.status == ConnectStatus.DISCONNECTED -class TestDeactivating: +class TestDeactivating(OpenpilotTestCase): def test_deactivating_noop_for_non_connection_removed(self, mocker): """DEACTIVATING with non-CONNECTION_REMOVED reason is a no-op.""" wm = _make_wm(mocker) @@ -103,10 +104,10 @@ class TestDeactivating: assert wm._wifi_state.ssid == "Net" assert wm._wifi_state.status == ConnectStatus.CONNECTED - @pytest.mark.parametrize("status, expected_clears", [ + @parameterized.expand([ (ConnectStatus.CONNECTED, True), (ConnectStatus.CONNECTING, False), - ]) + ], names=("status", "expected_clears")) def test_deactivating_connection_removed(self, mocker, status, expected_clears): """DEACTIVATING(CONNECTION_REMOVED) clears CONNECTED but preserves CONNECTING. @@ -130,7 +131,7 @@ class TestDeactivating: assert wm._wifi_state.status == ConnectStatus.CONNECTING -class TestPrepareConfig: +class TestPrepareConfig(OpenpilotTestCase): def test_user_initiated_skips_dbus_lookup(self, mocker): """User called _set_connecting('B') — PREPARE must not overwrite via DBus. @@ -148,7 +149,7 @@ class TestPrepareConfig: assert wm._wifi_state.status == ConnectStatus.CONNECTING wm._get_active_wifi_connection.assert_not_called() - @pytest.mark.parametrize("state", [NMDeviceState.PREPARE, NMDeviceState.CONFIG]) + @parameterized.expand([NMDeviceState.PREPARE, NMDeviceState.CONFIG], names=("state",)) def test_auto_connect_looks_up_ssid(self, mocker, state): """Auto-connection (ssid=None): PREPARE and CONFIG must look up ssid from NM.""" wm = _make_wm(mocker, connections={"AutoNet": "/path/auto"}) @@ -179,7 +180,7 @@ class TestPrepareConfig: assert wm._wifi_state.status == ConnectStatus.CONNECTING -class TestNeedAuth: +class TestNeedAuth(OpenpilotTestCase): def test_wrong_password_fires_callback(self, mocker): """NEED_AUTH+SUPPLICANT_DISCONNECT from CONFIG = real wrong password.""" wm = _make_wm(mocker) @@ -272,16 +273,16 @@ class TestNeedAuth: assert len(wm._callback_queue) == 0 -class TestPassthroughStates: +class TestPassthroughStates(OpenpilotTestCase): """NEED_AUTH (generic), IP_CONFIG, IP_CHECK, SECONDARIES, FAILED (generic) are no-ops.""" - @pytest.mark.parametrize("state", [ + @parameterized.expand([ NMDeviceState.NEED_AUTH, NMDeviceState.IP_CONFIG, NMDeviceState.IP_CHECK, NMDeviceState.SECONDARIES, NMDeviceState.FAILED, - ]) + ], names=("state",)) def test_passthrough_is_noop(self, mocker, state): wm = _make_wm(mocker) wm._set_connecting("Net") @@ -293,7 +294,7 @@ class TestPassthroughStates: assert len(wm._callback_queue) == 0 -class TestActivated: +class TestActivated(OpenpilotTestCase): def test_sets_connected(self, mocker): """ACTIVATED sets status to CONNECTED and fires callback.""" wm = _make_wm(mocker, connections={"MyNet": "/path/mynet"}) @@ -344,7 +345,7 @@ class TestActivated: # guard) shrink these race windows significantly. The epoch counter closes the # remaining gaps. -class TestThreadRaces: +class TestThreadRaces(OpenpilotTestCase): def test_prepare_race_user_tap_during_dbus(self, mocker): """User taps B while PREPARE's DBus call is in flight for auto-connect. @@ -416,7 +417,7 @@ class TestThreadRaces: # Full sequences (NM signal order from real devices) # --------------------------------------------------------------------------- -class TestFullSequences: +class TestFullSequences(OpenpilotTestCase): def test_normal_connect(self, mocker): """User connects to saved network: full happy path. @@ -771,7 +772,7 @@ class TestFullSequences: wm.process_callbacks() cb.assert_called_once_with("Hotspot") - @pytest.mark.xfail(reason="TODO: FAILED(SSID_NOT_FOUND) should emit error for UI") + @unittest.expectedFailure # "TODO: FAILED(SSID_NOT_FOUND) should emit error for UI" def test_ssid_not_found(self, mocker): """Network drops off while connected — hotspot turned off. @@ -843,7 +844,7 @@ class TestFullSequences: # Verified on device: when ActivateConnection returns UnknownConnection error, # NM emits no state signals. The worker error path is the only recovery point. -class TestWorkerErrorRecovery: +class TestWorkerErrorRecovery(OpenpilotTestCase): """Worker threads re-sync with NM via _init_wifi_state on DBus errors, preserving actual NM state instead of blindly clearing to DISCONNECTED.""" diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index 1f7bd5747a..5a9dc8772c 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -3,6 +3,7 @@ import json import time import capnp +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging, log from teleoprtc.tracks import VIDEO_CLOCK_RATE @@ -10,7 +11,7 @@ from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIn from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack -class TestStreamSession: +class TestStreamSession(OpenpilotTestCase): def setup_method(self): self.loop = asyncio.new_event_loop() diff --git a/openpilot/test_native.py b/openpilot/test_native.py new file mode 100644 index 0000000000..a69771f675 --- /dev/null +++ b/openpilot/test_native.py @@ -0,0 +1,25 @@ +import os +import subprocess + +from openpilot.common.basedir import BASEDIR +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase + + +NATIVE_TESTS = ( + "openpilot/common/tests/test_common", + "openpilot/selfdrive/pandad/tests/test_pandad_canprotocol", + "openpilot/system/loggerd/tests/test_logger", + "openpilot/tools/cabana/tests/test_cabana", + "openpilot/tools/cabana/tests/test_dbc_core", + "openpilot/tools/replay/tests/test_replay", +) + + +class TestNative(OpenpilotTestCase): + @parameterized.expand(NATIVE_TESTS) + def test_native(self, executable): + path = os.path.join(BASEDIR, executable) + if not os.path.exists(path): + self.skipTest(f"optional native test was not built: {executable}") + subprocess.run([path], check=True) diff --git a/openpilot/tools/jotpluggler/test_jotpluggler.py b/openpilot/tools/jotpluggler/test_jotpluggler.py index 0729e3b2e3..cbcc0a8168 100644 --- a/openpilot/tools/jotpluggler/test_jotpluggler.py +++ b/openpilot/tools/jotpluggler/test_jotpluggler.py @@ -5,7 +5,8 @@ from pathlib import Path JOTPLUGGLER_DIR = Path(__file__).parent -class TestJotpluggler: +from openpilot.common.test import OpenpilotTestCase +class TestJotpluggler(OpenpilotTestCase): def test_help(self): result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) assert result.returncode == 0, result.stderr diff --git a/openpilot/tools/lib/tests/test_caching.py b/openpilot/tools/lib/tests/test_caching.py index c70d7fc81b..565bac40dd 100644 --- a/openpilot/tools/lib/tests/test_caching.py +++ b/openpilot/tools/lib/tests/test_caching.py @@ -3,8 +3,9 @@ import os import shutil import socket import tempfile -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.test.helpers import http_server_context from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.url_file import URLFile, prune_cache @@ -30,12 +31,11 @@ class CachingTestRequestHandler(http.server.BaseHTTPRequestHandler): self.end_headers() -@pytest.fixture def host(): with http_server_context(handler=CachingTestRequestHandler) as (host, port): yield f"http://{host}:{port}" -class TestFileDownload: +class TestFileDownload(OpenpilotTestCase): def test_pipeline_defaults(self, host): # TODO: parameterize the defaults so we don't rely on hard-coded values in xx @@ -59,7 +59,7 @@ class TestFileDownload: # ensure caching on by default and cache dir gets created os.environ.pop("DISABLE_FILEREADER_CACHE", None) if os.path.exists(Paths.download_cache_root()): - shutil.rmtree(Paths.download_cache_root()) + shutil.rmtree(Paths.download_cache_root(), ignore_errors=True) URLFile(f"{host}/test.txt").get_length() URLFile(f"{host}/test.txt").read() assert os.path.exists(Paths.download_cache_root()) @@ -117,7 +117,7 @@ class TestFileDownload: self.compare_loads(large_file_url, length - 100, 100) self.compare_loads(large_file_url) - @pytest.mark.parametrize("cache_enabled", [True, False]) + @parameterized.expand([True, False], names=("cache_enabled",)) def test_recover_from_missing_file(self, host, cache_enabled): if cache_enabled: os.environ.pop("DISABLE_FILEREADER_CACHE", None) @@ -135,7 +135,7 @@ class TestFileDownload: assert length == 4 -class TestCache: +class TestCache(OpenpilotTestCase): def test_prune_cache(self, monkeypatch): with tempfile.TemporaryDirectory() as tmpdir: monkeypatch.setattr(Paths, 'download_cache_root', staticmethod(lambda: tmpdir + "/")) diff --git a/openpilot/tools/lib/tests/test_comma_car_segments.py b/openpilot/tools/lib/tests/test_comma_car_segments.py index 1b0f07ee63..a678aad51d 100644 --- a/openpilot/tools/lib/tests/test_comma_car_segments.py +++ b/openpilot/tools/lib/tests/test_comma_car_segments.py @@ -1,13 +1,14 @@ -import pytest +import unittest import requests from opendbc.car.fingerprints import MIGRATION +from openpilot.common.test import OpenpilotTestCase from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database, get_url from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.route import SegmentRange -@pytest.mark.skip(reason="huggingface is flaky, run this test manually to check for issues") -class TestCommaCarSegments: +@unittest.skip("huggingface is flaky, run this test manually to check for issues") +class TestCommaCarSegments(OpenpilotTestCase): def test_database(self): database = get_comma_car_segments_database() diff --git a/openpilot/tools/lib/tests/test_logreader.py b/openpilot/tools/lib/tests/test_logreader.py index a27a348d9e..edf2314ee1 100644 --- a/openpilot/tools/lib/tests/test_logreader.py +++ b/openpilot/tools/lib/tests/test_logreader.py @@ -4,9 +4,10 @@ import io import shutil import tempfile import os -import pytest +import unittest import requests +from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized from openpilot.cereal import log as capnp_log @@ -47,7 +48,7 @@ def setup_source_scenario(mocker, is_internal=False): yield -class TestLogReader: +class TestLogReader(OpenpilotTestCase): @parameterized.expand([ (f"{TEST_ROUTE}", ALL_SEGS), (f"{TEST_ROUTE.replace('/', '|')}", ALL_SEGS), @@ -72,7 +73,7 @@ class TestLogReader: (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '|')}", ALL_SEGS), (f"https://useradmin.comma.ai/?onebox={TEST_ROUTE.replace('/', '%7C')}", ALL_SEGS), ]) - @pytest.mark.skip("this got flaky. internet tests are stupid.") + @unittest.skip("this got flaky. internet tests are stupid.") def test_indirect_parsing(self, identifier, expected): parsed = parse_indirect(identifier) sr = SegmentRange(parsed) @@ -90,7 +91,7 @@ class TestLogReader: sr = SegmentRange(identifier) assert str(sr) == expected - @pytest.mark.parametrize("cache_enabled", [True, False]) + @parameterized.expand([True, False], names=("cache_enabled",)) def test_direct_parsing(self, mocker, cache_enabled): file_exists_mock = mocker.patch("openpilot.tools.lib.filereader.file_exists") if cache_enabled: @@ -107,7 +108,7 @@ class TestLogReader: l = len(list(LogReader(f))) assert l > 100 - with pytest.raises(URLFileException) if not cache_enabled else pytest.raises(AssertionError): + with self.assertRaises(URLFileException if not cache_enabled else AssertionError): l = len(list(LogReader(QLOG_FILE.replace("/3/", "/200/")))) # file_exists should not be called for direct files @@ -126,44 +127,44 @@ class TestLogReader: (f"{TEST_ROUTE}--3a",), ]) def test_bad_ranges(self, segment_range): - with pytest.raises(AssertionError): + with self.assertRaises(AssertionError): _ = SegmentRange(segment_range).seg_idxs - @pytest.mark.parametrize("segment_range, api_call", [ + @parameterized.expand([ (f"{TEST_ROUTE}/0", False), (f"{TEST_ROUTE}/:2", False), (f"{TEST_ROUTE}/0:", True), (f"{TEST_ROUTE}/-1", True), (f"{TEST_ROUTE}", True), - ]) + ], names=("segment_range", "api_call")) def test_slicing_api_call(self, mocker, segment_range, api_call): max_seg_mock = mocker.patch("openpilot.tools.lib.route.get_max_seg_number_cached") max_seg_mock.return_value = NUM_SEGS _ = SegmentRange(segment_range).seg_idxs assert api_call == max_seg_mock.called - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_modes(self): qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.QLOG))) rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0", ReadMode.RLOG))) assert qlog_len * 6 < rlog_len - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_modes_from_name(self): qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q"))) rlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/r"))) assert qlog_len * 6 < rlog_len - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_list(self): qlog_len = len(list(LogReader(f"{TEST_ROUTE}/0/q"))) qlog_len_2 = len(list(LogReader([f"{TEST_ROUTE}/0/q", f"{TEST_ROUTE}/0/q"]))) assert qlog_len * 2 == qlog_len_2 - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_multiple_iterations(self, mocker): init_mock = mocker.patch("openpilot.tools.lib.logreader._LogFileReader") lr = LogReader(f"{TEST_ROUTE}/0/q") @@ -175,14 +176,14 @@ class TestLogReader: assert qlog_len1 == qlog_len2 - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_helpers(self): lr = LogReader(f"{TEST_ROUTE}/0/q") assert lr.first("carParams").carFingerprint == "SUBARU OUTBACK 6TH GEN" assert 0 < len(list(lr.filter("carParams"))) < len(list(lr)) @parameterized.expand([(True,), (False,)]) - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_run_across_segments(self, cache_enabled): if cache_enabled: os.environ.pop("DISABLE_FILEREADER_CACHE", None) @@ -191,7 +192,7 @@ class TestLogReader: lr = LogReader(f"{TEST_ROUTE}/0:4") assert len(lr.run_across_segments(4, noop)) == len(list(lr)) - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_auto_mode(self, subtests, mocker): lr = LogReader(f"{TEST_ROUTE}/0/q") qlog_len = len(list(lr)) @@ -207,7 +208,7 @@ class TestLogReader: with subtests.test("interactive_no"): mocker.patch("sys.stdin", new=io.StringIO("n\n")) - with pytest.raises(LogsUnavailable): + with self.assertRaises(LogsUnavailable): lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source]) with subtests.test("non_interactive"): @@ -215,7 +216,7 @@ class TestLogReader: log_len = len(list(lr)) assert qlog_len == log_len - @pytest.mark.parametrize("is_internal", [True, False]) + @parameterized.expand([True, False], names=("is_internal",)) def test_auto_source_scenarios(self, mocker, is_internal): lr = LogReader(QLOG_FILE) qlog_len = len(list(lr)) @@ -225,7 +226,7 @@ class TestLogReader: log_len = len(list(lr)) assert qlog_len == log_len - @pytest.mark.slow + @unittest.skipIf(os.environ.get("SKIP_SLOW"), "slow test") def test_sort_by_time(self): msgs = list(LogReader(f"{TEST_ROUTE}/0/q")) assert msgs != sorted(msgs, key=lambda m: m.logMonoTime) @@ -254,7 +255,7 @@ class TestLogReader: # ensure new message is added, but is not a union type msgs = list(LogReader(qlog.name)) assert len(msgs) == num_msgs + 1 - with pytest.raises(capnp.KjException): + with self.assertRaises(capnp.KjException): [m.which() for m in msgs] # should not be added when only_union_types=True diff --git a/openpilot/tools/lib/tests/test_route_library.py b/openpilot/tools/lib/tests/test_route_library.py index 491bb81327..392484c794 100644 --- a/openpilot/tools/lib/tests/test_route_library.py +++ b/openpilot/tools/lib/tests/test_route_library.py @@ -1,8 +1,9 @@ from collections import namedtuple +from openpilot.common.test import OpenpilotTestCase from openpilot.tools.lib.route import SegmentName -class TestRouteLibrary: +class TestRouteLibrary(OpenpilotTestCase): def test_segment_name_formats(self): Case = namedtuple('Case', ['input', 'expected_route', 'expected_segment_num', 'expected_data_dir']) diff --git a/openpilot/tools/plotjuggler/test_plotjuggler.py b/openpilot/tools/plotjuggler/test_plotjuggler.py index d55aafe9be..10cd342dc8 100644 --- a/openpilot/tools/plotjuggler/test_plotjuggler.py +++ b/openpilot/tools/plotjuggler/test_plotjuggler.py @@ -5,17 +5,18 @@ import signal import subprocess import time -import pytest +import unittest +from openpilot.common.test import OpenpilotTestCase from openpilot.common.basedir import BASEDIR from openpilot.common.timeout import Timeout from openpilot.tools.plotjuggler.juggle import DEMO_ROUTE, install PJ_DIR = os.path.join(BASEDIR, "openpilot/tools/plotjuggler") -class TestPlotJuggler: +class TestPlotJuggler(OpenpilotTestCase): - @pytest.mark.skipif(not shutil.which('qmake'), reason="Qt not installed") + @unittest.skipIf(not shutil.which('qmake'), "Qt not installed") def test_demo(self): install() diff --git a/openpilot/tools/sim/tests/conftest.py b/openpilot/tools/sim/tests/conftest.py deleted file mode 100644 index ddf6635276..0000000000 --- a/openpilot/tools/sim/tests/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import pytest - -def pytest_addoption(parser): - parser.addoption("--test_duration", action="store", default=60, type=int, help="Seconds to run metadrive drive") - -@pytest.fixture -def test_duration(request): - return request.config.getoption("--test_duration") diff --git a/openpilot/tools/sim/tests/test_metadrive_bridge.py b/openpilot/tools/sim/tests/test_metadrive_bridge.py index 9be640d736..4e7560907b 100644 --- a/openpilot/tools/sim/tests/test_metadrive_bridge.py +++ b/openpilot/tools/sim/tests/test_metadrive_bridge.py @@ -1,17 +1,22 @@ -import pytest import warnings +import unittest +import importlib # Since metadrive depends on pkg_resources, and pkg_resources is deprecated as an API warnings.filterwarnings("ignore", category=DeprecationWarning) -from openpilot.tools.sim.bridge.metadrive.metadrive_bridge import MetaDriveBridge +try: + MetaDriveBridge = importlib.import_module("openpilot.tools.sim.bridge.metadrive.metadrive_bridge").MetaDriveBridge +except ModuleNotFoundError: + MetaDriveBridge = None from openpilot.tools.sim.tests.test_sim_bridge import TestSimBridgeBase -@pytest.mark.slow +@unittest.skipIf(MetaDriveBridge is None, "metadrive is not installed") class TestMetaDriveBridge(TestSimBridgeBase): - @pytest.fixture(autouse=True) - def setup_create_bridge(self, test_duration): + def setup_method(self): + super().openpilot_setup_method() self.test_duration = 30 def create_bridge(self): + assert MetaDriveBridge is not None return MetaDriveBridge(False, False, self.test_duration, True) diff --git a/openpilot/tools/sim/tests/test_sim_bridge.py b/openpilot/tools/sim/tests/test_sim_bridge.py index f93cc2ef50..a2baddad60 100644 --- a/openpilot/tools/sim/tests/test_sim_bridge.py +++ b/openpilot/tools/sim/tests/test_sim_bridge.py @@ -1,21 +1,23 @@ import os import subprocess import time -import pytest +import unittest from multiprocessing import Queue +from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging from openpilot.common.basedir import BASEDIR from openpilot.tools.sim.bridge.common import QueueMessageType SIM_DIR = os.path.join(BASEDIR, "openpilot/tools/sim") -class TestSimBridgeBase: +class TestSimBridgeBase(OpenpilotTestCase): + SLOW_TEST = True @classmethod def setup_class(cls): if cls is TestSimBridgeBase: - raise pytest.skip("Don't run this base class, run test_metadrive_bridge.py instead") + raise unittest.SkipTest("Don't run this base class, run test_metadrive_bridge.py instead") def setup_method(self): self.processes = [] diff --git a/pyproject.toml b/pyproject.toml index c3769f0401..68c5dd7fe7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,11 +65,7 @@ testing = [ "hypothesis ==6.47.*", "ty", "pytest", - "pytest-cpp", - "pytest-subtests", - # https://github.com/pytest-dev/pytest-xdist/pull/1229 - "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", - "pytest-mock", + "pytest-xdist", "ruff", "codespell", ] @@ -120,18 +116,8 @@ allow-direct-references = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" -cpp_files = "test_*" -cpp_harness = "openpilot/selfdrive/test/cpp_harness.py" +addopts = "-Werror --strict-config --durations=10" python_files = "test_*.py" -markers = [ - "slow: tests that take awhile to run and can be skipped with -m 'not slow'", - "tici: tests that are only meant to run on the C3/C3X", - "skip_tici_setup: mark test to skip tici setup fixture", - "nocapture: don't capture test output", - "shared_download_cache: share download cache between tests", - "xdist_group_class_property: group tests by a property of the class that contains them", -] testpaths = [ "openpilot", ] @@ -168,6 +154,7 @@ line-length = 160 lint.flake8-implicit-str-concat.allow-multiline = false [tool.ruff.lint.flake8-tidy-imports.banned-api] +"pytest".msg = "Use unittest" "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "time.time".msg = "Use time.monotonic" # time.time can skip due to its reference clock, you probably want a monotonic clock diff --git a/tools/op.sh b/tools/op.sh index 1ee7b232b0..6d70310668 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -327,7 +327,7 @@ function op_lint() { function op_test() { op_before_cmd - op_run_command pytest "$@" + op_run_command pytest -n logical --dist worksteal "$@" } function op_replay() { @@ -431,7 +431,7 @@ function op_default() { echo -e " ${BOLD}sim${NC} Run openpilot in a simulator" echo -e " ${BOLD}lint${NC} Run the linter" echo -e " ${BOLD}post-commit${NC} Install the linter as a post-commit hook" - echo -e " ${BOLD}test${NC} Run all unit tests from pytest" + echo -e " ${BOLD}test${NC} Run all unit tests" echo "" echo -e "${BOLD}${UNDERLINE}Options:${NC}" echo -e " ${BOLD}-d, --dir${NC}" diff --git a/uv.lock b/uv.lock index d03a44160e..ff155ab3bc 100644 --- a/uv.lock +++ b/uv.lock @@ -749,9 +749,6 @@ testing = [ { name = "coverage" }, { name = "hypothesis" }, { name = "pytest" }, - { name = "pytest-cpp" }, - { name = "pytest-mock" }, - { name = "pytest-subtests" }, { name = "pytest-xdist" }, { name = "ruff" }, { name = "ty" }, @@ -802,10 +799,7 @@ requires-dist = [ { name = "pycapnp", specifier = "==2.1.0" }, { name = "pyjwt", extras = ["crypto"] }, { name = "pytest", marker = "extra == 'testing'" }, - { name = "pytest-cpp", marker = "extra == 'testing'" }, - { name = "pytest-mock", marker = "extra == 'testing'" }, - { name = "pytest-subtests", marker = "extra == 'testing'" }, - { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, + { name = "pytest-xdist", marker = "extra == 'testing'" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, @@ -1004,51 +998,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] -[[package]] -name = "pytest-cpp" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/a1/c2679d7ff2da20a0f89c7820ae2739cde739eac9b43c192531117b31b5f4/pytest_cpp-2.6.0.tar.gz", hash = "sha256:c2f49d3c038539ac84786a94d852e4f4619c34c95979c2bc69c20b3bdf051d85", size = 465490, upload-time = "2024-09-18T00:08:08.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/44/dc2f5d53165264ae5831f361fe7723c45da05718a97015b2eddc452cf503/pytest_cpp-2.6.0-py3-none-any.whl", hash = "sha256:b33de94609450feea2fba9efff3558b8ac8f1fdf40a99e263b395d4798b911bb", size = 15074, upload-time = "2024-09-18T00:08:06.415Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, -] - -[[package]] -name = "pytest-subtests" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/d9/20097971a8d315e011e055d512fa120fd6be3bdb8f4b3aa3e3c6bf77bebc/pytest_subtests-0.15.0.tar.gz", hash = "sha256:cb495bde05551b784b8f0b8adfaa27edb4131469a27c339b80fd8d6ba33f887c", size = 18525, upload-time = "2025-10-20T16:26:18.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/64/bba465299b37448b4c1b84c7a04178399ac22d47b3dc5db1874fe55a2bd3/pytest_subtests-0.15.0-py3-none-any.whl", hash = "sha256:da2d0ce348e1f8d831d5a40d81e3aeac439fec50bd5251cbb7791402696a9493", size = 9185, upload-time = "2025-10-20T16:26:17.239Z" }, -] - [[package]] name = "pytest-xdist" -version = "3.7.1.dev24+g2b4372bd6" -source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] [[package]] name = "python-dateutil" From 1262e26899cf8439f514a6e4cabedeb1d02401c6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 12:45:56 -0700 Subject: [PATCH 038/325] athena: bring back port migration This reverts commit d9596fa9985edc3e516174fc0b2de1f35021734e. --- openpilot/system/athena/athenad.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index ef0561b122..91b12fc0a7 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -488,6 +488,10 @@ def setRouteViewed(route: str) -> dict[str, int | str]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: try: + # migration, can be removed once 0.9.8 is out for a while + if local_port == 8022: + local_port = 22 + if local_port not in LOCAL_PORT_WHITELIST: raise Exception("Requested local port not whitelisted") From eb6c9c7f0dbe4fa99129e706af9a755c1a54d772 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 12:52:48 -0700 Subject: [PATCH 039/325] Run device tests directly (#38397) --- Jenkinsfile | 24 +++++++++---------- .../hardware/tici/tests/test_amplifier.py | 7 ++++++ .../selfdrive/pandad/tests/test_pandad.py | 7 ++++++ .../pandad/tests/test_pandad_loopback.py | 7 ++++++ .../selfdrive/pandad/tests/test_pandad_spi.py | 7 ++++++ openpilot/selfdrive/test/test_onroad.py | 7 ++++++ openpilot/selfdrive/test/test_power_draw.py | 7 ++++++ openpilot/system/camerad/test/test_camerad.py | 7 ++++++ .../system/loggerd/tests/test_encoder.py | 7 ++++++ openpilot/system/manager/test/test_manager.py | 6 +++++ .../system/sensord/tests/test_sensord.py | 7 ++++++ tools/release/build_release.sh | 2 +- 12 files changed, 82 insertions(+), 13 deletions(-) mode change 100644 => 100755 openpilot/common/hardware/tici/tests/test_amplifier.py mode change 100644 => 100755 openpilot/selfdrive/pandad/tests/test_pandad.py mode change 100644 => 100755 openpilot/selfdrive/pandad/tests/test_pandad_loopback.py mode change 100644 => 100755 openpilot/selfdrive/pandad/tests/test_pandad_spi.py mode change 100644 => 100755 openpilot/selfdrive/test/test_onroad.py mode change 100644 => 100755 openpilot/selfdrive/test/test_power_draw.py mode change 100644 => 100755 openpilot/system/camerad/test/test_camerad.py mode change 100644 => 100755 openpilot/system/loggerd/tests/test_encoder.py mode change 100644 => 100755 openpilot/system/manager/test/test_manager.py mode change 100644 => 100755 openpilot/system/sensord/tests/test_sensord.py diff --git a/Jenkinsfile b/Jenkinsfile index d57e9502aa..7c8d1c060e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -206,35 +206,35 @@ node { deviceStage("onroad", "tizi-needs-can", ["UNSAFE=1"], [ step("build openpilot", "cd openpilot/system/manager && ./build.py"), step("check dirty", "tools/release/check-dirty.sh"), - step("onroad tests", "pytest openpilot/selfdrive/test/test_onroad.py -s", [timeout: 60]), + step("onroad tests", "./openpilot/selfdrive/test/test_onroad.py", [timeout: 60]), ]) }, 'HW + Unit Tests': { deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [ step("build", "cd openpilot/system/manager && ./build.py"), - step("test power draw", "pytest -s openpilot/selfdrive/test//test_power_draw.py"), - step("test encoder", "pytest openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]), - step("test manager", "pytest openpilot/system/manager/test/test_manager.py"), + step("test power draw", "./openpilot/selfdrive/test/test_power_draw.py"), + step("test encoder", "./openpilot/system/loggerd/tests/test_encoder.py", [diffPaths: ["openpilot/system/loggerd/"]]), + step("test manager", "./openpilot/system/manager/test/test_manager.py"), ]) }, 'camerad OX03C10': { deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ step("build", "cd openpilot/system/manager && ./build.py"), - step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"), - step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), + step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"), + step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'camerad OS04C10': { deviceStage("OS04C10", "tici-os04c10", ["UNSAFE=1"], [ step("build", "cd openpilot/system/manager && ./build.py"), - step("test pandad", "pytest openpilot/selfdrive/pandad/tests/test_pandad.py"), - step("test camerad", "pytest openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), + step("test pandad", "./openpilot/selfdrive/pandad/tests/test_pandad.py"), + step("test camerad", "./openpilot/system/camerad/test/test_camerad.py", [timeout: 90]), ]) }, 'sensord': { deviceStage("LSM + MMC", "tizi-lsmc", ["UNSAFE=1"], [ step("build", "cd openpilot/system/manager && ./build.py"), - step("test sensord", "pytest openpilot/system/sensord/tests/test_sensord.py"), + step("test sensord", "./openpilot/system/sensord/tests/test_sensord.py"), ]) }, 'replay': { @@ -246,9 +246,9 @@ node { 'tizi': { deviceStage("tizi", "tizi", ["UNSAFE=1"], [ step("build openpilot", "cd openpilot/system/manager && ./build.py"), - step("test pandad loopback", "pytest openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"), - step("test pandad spi", "pytest openpilot/selfdrive/pandad/tests/test_pandad_spi.py"), - step("test amp", "pytest openpilot/common/hardware/tici/tests/test_amplifier.py"), + step("test pandad loopback", "./openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"), + step("test pandad spi", "./openpilot/selfdrive/pandad/tests/test_pandad_spi.py"), + step("test amp", "./openpilot/common/hardware/tici/tests/test_amplifier.py"), ]) }, diff --git a/openpilot/common/hardware/tici/tests/test_amplifier.py b/openpilot/common/hardware/tici/tests/test_amplifier.py old mode 100644 new mode 100755 index 08b8c9be80..2845805860 --- a/openpilot/common/hardware/tici/tests/test_amplifier.py +++ b/openpilot/common/hardware/tici/tests/test_amplifier.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 + import time import subprocess +import unittest from panda import Panda from openpilot.common.test import OpenpilotTestCase @@ -62,3 +65,7 @@ class TestAmplifier(OpenpilotTestCase): break else: self.fail("didn't hit any i2c errors") + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py old mode 100644 new mode 100755 index 820001bbc7..bf70d5af79 --- a/openpilot/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 + import os import time +import unittest from openpilot.common.test import OpenpilotTestCase import openpilot.cereal.messaging as messaging @@ -79,3 +82,7 @@ class TestPandad(OpenpilotTestCase): assert not PandaDFU.list() self._run_test() + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py old mode 100644 new mode 100755 index 8dd064bd73..5895a32060 --- a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py @@ -1,7 +1,10 @@ +#!/usr/bin/env python3 + import os import copy import random import time +import unittest from collections import defaultdict from pprint import pprint @@ -114,3 +117,7 @@ class TestBoarddLoopback(OpenpilotTestCase): pprint(sm['pandaStates']) # may drop messages due to RX buffer overflow for bus in sent_loopback.keys(): assert not len(sent_loopback[bus]), f"loop {i}: bus {bus} missing {len(sent_loopback[bus])} out of {sent_total[bus]} messages" + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py old mode 100644 new mode 100755 index 79eb2dd77e..9cb0ac30c8 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 + import os import time +import unittest import numpy as np import random @@ -105,3 +108,7 @@ class TestBoarddSpi(OpenpilotTestCase): with subtests.test(msg="CAN traffic"): print(f"Sent {total_sent_count} CAN messages, got {total_recv_count} back. {total_recv_count/(total_sent_count+1e-4):.2%} received") assert total_recv_count > 20 + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py old mode 100644 new mode 100755 index b710fce919..2fa7c75fb1 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -1,9 +1,12 @@ +#!/usr/bin/env python3 + import math import json import os import shutil import subprocess import time +import unittest import numpy as np from collections import Counter, defaultdict from pathlib import Path @@ -442,3 +445,7 @@ class TestOnroad(OpenpilotTestCase): eng = [m.selfdriveState.engageable for m in self.msgs['selfdriveState'][offset:]] assert all(eng), \ f"Not engageable for whole segment:\n- selfdriveState.engageable: {Counter(eng)}\n- No entry events: {no_entries}" + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py old mode 100644 new mode 100755 index 530ce86f31..4c2a15a41f --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 + from collections import defaultdict, deque import time +import unittest import numpy as np from dataclasses import dataclass from openpilot.common.test import OpenpilotTestCase @@ -123,3 +126,7 @@ class TestPowerDraw(OpenpilotTestCase): assert self.valid_power_draw(proc, cur), f"expected {expected:.2f}W, got {cur:.2f}W" print(tabulate(tab)) print(f"Baseline {baseline:.2f}W\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/system/camerad/test/test_camerad.py b/openpilot/system/camerad/test/test_camerad.py old mode 100644 new mode 100755 index ad98a22581..0d66f0e16b --- a/openpilot/system/camerad/test/test_camerad.py +++ b/openpilot/system/camerad/test/test_camerad.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python3 + import os import time +import unittest import numpy as np from openpilot.common.parameterized import parameterized @@ -158,3 +161,7 @@ class TestCamerad(OpenpilotTestCase): assert np.max([ np.max(np.diff(ts[c]['requestId'])) for c in CAMERAS ]) > 1 self._sanity_checks(ts) + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/system/loggerd/tests/test_encoder.py b/openpilot/system/loggerd/tests/test_encoder.py old mode 100644 new mode 100755 index 4d5a998583..62c040cc19 --- a/openpilot/system/loggerd/tests/test_encoder.py +++ b/openpilot/system/loggerd/tests/test_encoder.py @@ -1,8 +1,11 @@ +#!/usr/bin/env python3 + import math import os import shutil import subprocess import time +import unittest from pathlib import Path from tqdm import trange @@ -144,3 +147,7 @@ class TestEncoder(OpenpilotTestCase): managed_processes['encoderd'].stop() managed_processes['camerad'].stop() managed_processes['sensord'].stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/system/manager/test/test_manager.py b/openpilot/system/manager/test/test_manager.py old mode 100644 new mode 100755 index d5945a4401..54bdaec6fe --- a/openpilot/system/manager/test/test_manager.py +++ b/openpilot/system/manager/test/test_manager.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import os import unittest import signal @@ -76,3 +78,7 @@ class TestManager(OpenpilotTestCase): if p.sigkill: exit_codes = [-signal.SIGKILL] assert exit_code in exit_codes, f"{p.name} died with {exit_code}" + + +if __name__ == "__main__": + unittest.main() diff --git a/openpilot/system/sensord/tests/test_sensord.py b/openpilot/system/sensord/tests/test_sensord.py old mode 100644 new mode 100755 index 868d37c793..e1580af9bb --- a/openpilot/system/sensord/tests/test_sensord.py +++ b/openpilot/system/sensord/tests/test_sensord.py @@ -1,6 +1,9 @@ +#!/usr/bin/env python3 + import os import subprocess import time +import unittest import numpy as np from collections import namedtuple, defaultdict @@ -183,3 +186,7 @@ class TestSensord(OpenpilotTestCase): time.sleep(1) state_two = get_irq_count(self.sensord_irq) assert state_one == state_two, "Interrupts received after sensord stop!" + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index cf03c8b393..0b6ed16ba9 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -85,7 +85,7 @@ git commit --amend -m "openpilot v$VERSION" # Run tests cd $BUILD_DIR -RELEASE=1 pytest -n0 -s openpilot/selfdrive/test/test_onroad.py +RELEASE=1 ./openpilot/selfdrive/test/test_onroad.py #pytest openpilot/selfdrive/car/tests/test_car_interfaces.py echo "[-] pushing release T=$SECONDS" From f1977eaa9300db5a4607acb698362574bc260cdf Mon Sep 17 00:00:00 2001 From: Armand du Parc Locmaria Date: Tue, 21 Jul 2026 16:08:19 -0700 Subject: [PATCH 040/325] bump tg (#38398) * bump tg * slightly nicer --- openpilot/selfdrive/modeld/compile_modeld.py | 17 ----------------- openpilot/selfdrive/modeld/helpers.py | 10 +++------- tinygrad_repo | 2 +- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index ebc3f21a13..34e3095557 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -3,7 +3,6 @@ import argparse import atexit import math import os -import pickle import tempfile import time import shutil @@ -31,22 +30,6 @@ def _patch_tinygrad_fetch_fw(): helpers.fetch_fw = fetch_fw _patch_tinygrad_fetch_fw() -def _patch_tinygrad_buffer_reduce(): - from tinygrad.device import Buffer - def __reduce_ex__(self, protocol): - buf = None - if self._base is not None: - return self.__class__, (self.device, self.size, self.dtype, None, None, None, 0, self.base, self.offset, self.is_allocated()) - if self.device == "NPY": - return self.__class__, (self.device, self.size, self.dtype, self._buf, self.options, None, self.uop_refcount) - if self.is_allocated(): - buf = bytearray(self.nbytes) - self.copyout(memoryview(buf)) - if protocol >= 5: - buf = pickle.PickleBuffer(buf) - return self.__class__, (self.device, self.size, self.dtype, None, self.options, buf, self.uop_refcount) - Buffer.__reduce_ex__ = __reduce_ex__ -_patch_tinygrad_buffer_reduce() from tinygrad.tensor import Tensor from tinygrad.helpers import Context diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 608bc6aa64..7c322c5993 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -38,14 +38,10 @@ def dump_oob(obj, f): def load_oob(f): opcodes = f.read(struct.unpack(' bool: diff --git a/tinygrad_repo b/tinygrad_repo index e6fbede157..ef37830d13 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit e6fbede1576f0a201e0b2c112499552a61280abd +Subproject commit ef37830d138838933d6d70d0024de9f130cb2308 From 9e0293671764290991462ff2af84ead16eb9bae5 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 21 Jul 2026 16:23:27 -0700 Subject: [PATCH 041/325] AGNOS 18.6 (#38393) * version * agnos 18.6 * fix --- launch_env.sh | 2 +- openpilot/common/hardware/tici/agnos.json | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 3e0ca602b4..696d0fe78d 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="18.5" + export AGNOS_VERSION="18.6" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/tici/agnos.json b/openpilot/common/hardware/tici/agnos.json index f22a57db75..a1202ef7a4 100644 --- a/openpilot/common/hardware/tici/agnos.json +++ b/openpilot/common/hardware/tici/agnos.json @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz", - "hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", - "hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39", + "url": "https://commadist.azureedge.net/agnosupdate/boot-e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190.img.xz", + "hash": "e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190", + "hash_raw": "e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190", "size": 17487872, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626" + "ondevice_hash": "34b160f05881d6b4cd7d9a85ad84ac8289d60d9854ccfc00fa31a4534a2c63fc" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz", - "hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539", - "hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95.img.xz", + "hash": "e34ea7ee73ce85ac357ca2a0ade022fc56c11e9c5af3989cd0c83896a4f9266b", + "hash_raw": "ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134", + "ondevice_hash": "7a2bc0374a2719a48b5c27012b16ae9654a473eb3a37c96d5d669d2a1cf45184", "alt": { - "hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724", - "url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img", + "hash": "ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95", + "url": "https://commadist.azureedge.net/agnosupdate/system-ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95.img", "size": 4718592000 } } From 765d6fff9b3029e19b9c475f7d70545e0544478f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 18:30:20 -0700 Subject: [PATCH 042/325] remove audio feedback (#38401) * remove audio feedback * rm feedbackd * backwards --- openpilot/cereal/deprecated.capnp | 10 +++ openpilot/cereal/log.capnp | 9 +-- openpilot/cereal/services.py | 1 - openpilot/common/params_keys.h | 1 - openpilot/selfdrive/selfdrived/events.py | 14 ---- openpilot/selfdrive/selfdrived/selfdrived.py | 7 +- .../test/process_replay/process_replay.py | 2 +- openpilot/selfdrive/test/test_onroad.py | 1 - openpilot/selfdrive/ui/feedback/feedbackd.py | 71 ------------------- openpilot/selfdrive/ui/layouts/main.py | 8 +-- openpilot/selfdrive/ui/mici/layouts/main.py | 8 +-- .../selfdrive/ui/tests/test_feedbackd.py | 55 -------------- openpilot/system/loggerd/loggerd.cc | 2 +- .../system/loggerd/tests/test_loggerd.py | 2 +- openpilot/system/manager/process_config.py | 1 - 15 files changed, 25 insertions(+), 167 deletions(-) delete mode 100755 openpilot/selfdrive/ui/feedback/feedbackd.py delete mode 100644 openpilot/selfdrive/ui/tests/test_feedbackd.py diff --git a/openpilot/cereal/deprecated.capnp b/openpilot/cereal/deprecated.capnp index 37153e4d62..b119e7091f 100644 --- a/openpilot/cereal/deprecated.capnp +++ b/openpilot/cereal/deprecated.capnp @@ -775,3 +775,13 @@ struct GpsTrajectory @0x8cfeb072f5301000 { x @0 :List(Float32); y @1 :List(Float32); } + +struct AudioFeedbackDEPRECATED @0xed47e3c075be372a { + audio @0 :AudioData; + blockNum @1 :UInt16; + + struct AudioData { + data @0 :Data; + sampleRate @1 :UInt32; + } +} diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 2148e81d3b..60e0bb9010 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -131,9 +131,9 @@ struct OnroadEvent @0xc4fa6047f024e718 { aeb @92; userBookmark @95; excessiveActuation @96; - audioFeedback @97; soundsUnavailableDEPRECATED @47; + audioFeedbackDEPRECATED @97; } } @@ -2485,11 +2485,6 @@ struct AudioData { sampleRate @1 :UInt32; } -struct AudioFeedback { - audio @0 :AudioData; - blockNum @1 :UInt16; -} - struct Touch { sec @0 :Int64; usec @1 :Int64; @@ -2583,7 +2578,6 @@ struct Event { # driving feedback userBookmark @93 :UserBookmark; bookmarkButton @148 :UserBookmark; - audioFeedback @149 :AudioFeedback; lateralManeuverPlan @150 :LateralManeuverPlan; @@ -2633,6 +2627,7 @@ struct Event { # *********** legacy + deprecated *********** model @9 :Deprecated.ModelData; # TODO: rename modelV2 and mark this as deprecated + audioFeedbackDEPRECATED @149 :Deprecated.AudioFeedbackDEPRECATED; liveMpcDEPRECATED @36 :Deprecated.LiveMpcData; liveLongitudinalMpcDEPRECATED @37 :Deprecated.LiveLongitudinalMpcData; liveLocationKalmanDeprecatedDEPRECATED @51 :Deprecated.LiveLocationData; diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index db1731a986..42669688b0 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -75,7 +75,6 @@ _services: dict[str, tuple] = { "soundPressure": (True, 10., 10), "rawAudioData": (False, 20.), "bookmarkButton": (True, 0., 1), - "audioFeedback": (True, 0., 1), "roadEncodeData": (False, 20., None, QueueSize.BIG), "driverEncodeData": (False, 20., None, QueueSize.BIG), "wideRoadEncodeData": (False, 20., None, QueueSize.BIG), diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 6441fc91df..7ecac1c729 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -105,7 +105,6 @@ inline static std::unordered_map keys = { {"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"PrimeType", {PERSISTENT, INT}}, {"RecordAudio", {PERSISTENT, BOOL}}, - {"RecordAudioFeedback", {PERSISTENT, BOOL, "0"}}, {"RecordFront", {PERSISTENT, BOOL}}, {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet {"SecOCKey", {PERSISTENT | DONT_LOG, STRING}}, diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 367bc03d33..ee9191fd1e 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -12,8 +12,6 @@ from openpilot.common.constants import CV from openpilot.common.git import get_short_branch from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER -from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER -from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION from openpilot.common.hardware import HARDWARE AlertSize = log.SelfdriveState.AlertSize @@ -272,14 +270,6 @@ def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubM return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH) -def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) - return NormalPermanentAlert( - "Recording Audio Feedback", - f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", - priority=Priority.LOW) - - # *** debug alerts *** def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -1032,10 +1022,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.userBookmark: { ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), }, - - EventName.audioFeedback: { - ET.PERMANENT: audio_feedback_alert, - }, } diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index d89c564925..9fed889be3 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -86,7 +86,7 @@ class SelfdriveD: self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', - 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback', + 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'lateralManeuverPlan'] + \ self.camera_packets + self.sensor_packets + self.gps_packets, ignore_alive=ignore, ignore_avg_freq=ignore, @@ -171,13 +171,10 @@ class SelfdriveD: self.events.add(EventName.selfdriveInitializing) return - # Check for user bookmark press (bookmark button or end of LKAS button feedback) + # Check for user bookmark press if self.sm.updated['userBookmark']: self.events.add(EventName.userBookmark) - if self.sm.updated['audioFeedback']: - self.events.add(EventName.audioFeedback) - # Don't add any more events while in dashcam mode if self.CP.passive: return diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index 2a90dfc295..5d2e5a22fa 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -436,7 +436,7 @@ CONFIGS = [ "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", - "carControl", "driverAssistance", "alertDebug", "audioFeedback", + "carControl", "driverAssistance", "alertDebug", ], subs=["selfdriveState", "onroadEvents"], ignore=["logMonoTime"], diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 2fa7c75fb1..2504afb7cd 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -57,7 +57,6 @@ PROCS = { "openpilot.selfdrive.locationd.paramsd": 9.0, "openpilot.selfdrive.locationd.lagd": 11.0, "openpilot.selfdrive.ui.soundd": 3.0, - "openpilot.selfdrive.ui.feedback.feedbackd": 1.0, "openpilot.selfdrive.monitoring.dmonitoringd": 4.0, "openpilot.system.proclogd": 7.0, "openpilot.system.logmessaged": 1.0, diff --git a/openpilot/selfdrive/ui/feedback/feedbackd.py b/openpilot/selfdrive/ui/feedback/feedbackd.py deleted file mode 100755 index 57f6b881e4..0000000000 --- a/openpilot/selfdrive/ui/feedback/feedbackd.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -import openpilot.cereal.messaging as messaging -from openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog -from opendbc.car.structs import car -from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER - -FEEDBACK_MAX_DURATION = 10.0 -ButtonType = car.CarState.ButtonEvent.Type - - -def main(): - params = Params() - pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) - sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton', 'carState']) - should_record_audio = False - block_num = 0 - waiting_for_release = False - early_stop_triggered = False - - while True: - sm.update() - should_send_bookmark = False - - # TODO: https://github.com/commaai/openpilot/issues/36015 - if False and sm.updated['carState'] and sm['carState'].canValid: - for be in sm['carState'].buttonEvents: - if be.type == ButtonType.lkas: - if be.pressed: - if not should_record_audio: - if params.get_bool("RecordAudioFeedback"): # Start recording on first press if toggle set - should_record_audio = True - block_num = 0 - waiting_for_release = False - early_stop_triggered = False - cloudlog.info("LKAS button pressed - starting 10-second audio feedback") - else: - should_send_bookmark = True # immediately send bookmark if toggle false - cloudlog.info("LKAS button pressed - bookmarking") - elif should_record_audio and not waiting_for_release: # Wait for release of second press to stop recording early - waiting_for_release = True - elif waiting_for_release: # Second press released - waiting_for_release = False - early_stop_triggered = True - cloudlog.info("LKAS button released - ending recording early") - - if should_record_audio and sm.updated['rawAudioData']: - raw_audio = sm['rawAudioData'] - msg = messaging.new_message('audioFeedback', valid=True) - msg.audioFeedback.audio.data = raw_audio.data - msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate - msg.audioFeedback.blockNum = block_num - block_num += 1 - if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: # Check for timeout or early stop - should_send_bookmark = True # send bookmark at end of audio segment - should_record_audio = False - early_stop_triggered = False - cloudlog.info("10-second recording completed or second button press - stopping audio feedback") - pm.send('audioFeedback', msg) - - if sm.updated['bookmarkButton']: - cloudlog.info("Bookmark button pressed!") - should_send_bookmark = True - - if should_send_bookmark: - msg = messaging.new_message('userBookmark', valid=True) - pm.send('userBookmark', msg) - - -if __name__ == '__main__': - main() diff --git a/openpilot/selfdrive/ui/layouts/main.py b/openpilot/selfdrive/ui/layouts/main.py index 277b6f1404..9ce43b9e48 100644 --- a/openpilot/selfdrive/ui/layouts/main.py +++ b/openpilot/selfdrive/ui/layouts/main.py @@ -22,7 +22,7 @@ class MainLayout(Widget): def __init__(self): super().__init__() - self._pm = messaging.PubMaster(['bookmarkButton']) + self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark']) self._sidebar = Sidebar() self._current_mode = MainState.HOME @@ -111,9 +111,9 @@ class MainLayout(Widget): self.open_settings(PanelType.DEVICE) def _on_bookmark_clicked(self): - user_bookmark = messaging.new_message('bookmarkButton') - user_bookmark.valid = True - self._pm.send('bookmarkButton', user_bookmark) + for service in ('bookmarkButton', 'userBookmark'): + msg = messaging.new_message(service, valid=True) + self._pm.send(service, msg) def _on_onroad_clicked(self): self._sidebar.set_visible(not self._sidebar.is_visible) diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index e592253544..20925ab63c 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -19,7 +19,7 @@ class MiciMainLayout(Scroller): def __init__(self): super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) - self._pm = messaging.PubMaster(['bookmarkButton']) + self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark']) self._prev_onroad = False self._prev_standstill = False @@ -140,9 +140,9 @@ class MiciMainLayout(Scroller): self._scroll_to(self._home_layout) def _on_bookmark_clicked(self): - user_bookmark = messaging.new_message('bookmarkButton') - user_bookmark.valid = True - self._pm.send('bookmarkButton', user_bookmark) + for service in ('bookmarkButton', 'userBookmark'): + msg = messaging.new_message(service, valid=True) + self._pm.send(service, msg) def _on_body_changed(self): self._car_onroad_layout.set_visible(not ui_state.is_body) diff --git a/openpilot/selfdrive/ui/tests/test_feedbackd.py b/openpilot/selfdrive/ui/tests/test_feedbackd.py deleted file mode 100644 index 71803c3452..0000000000 --- a/openpilot/selfdrive/ui/tests/test_feedbackd.py +++ /dev/null @@ -1,55 +0,0 @@ -import unittest -from openpilot.common.parameterized import parameterized -from openpilot.common.test import OpenpilotTestCase -import openpilot.cereal.messaging as messaging -from opendbc.car.structs import car -from openpilot.common.params import Params -from openpilot.system.manager.process_config import managed_processes - - -@unittest.skip("tmp disabled") -class TestFeedbackd(OpenpilotTestCase): - 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) - - @parameterized.expand([False, True]) - def test_audio_feedback(self, record_feedback): - Params().put_bool("RecordAudioFeedback", record_feedback, block=True) - - 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() diff --git a/openpilot/system/loggerd/loggerd.cc b/openpilot/system/loggerd/loggerd.cc index 3755929619..9f848608ac 100644 --- a/openpilot/system/loggerd/loggerd.cc +++ b/openpilot/system/loggerd/loggerd.cc @@ -246,7 +246,7 @@ void loggerd_thread() { .counter = 0, .freq = it.decimation, .encoder = encoder, - .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"), + .preserve_segment = it.name == "userBookmark", .record_audio = record_audio, }; } diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 6bb7e9bcf7..618e3d0016 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -308,7 +308,7 @@ class TestLoggerd(OpenpilotTestCase): assert getxattr(segment_dir, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE def test_not_preserving_nonbookmarked_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark", "audioFeedback"} + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index 655d1c4e29..e60598a7b6 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -116,7 +116,6 @@ procs = [ PythonProcess("tombstoned", "openpilot.system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "openpilot.system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "openpilot.system.loggerd.uploader", always_run), - PythonProcess("feedbackd", "openpilot.selfdrive.ui.feedback.feedbackd", only_onroad), # debug procs NativeProcess("bridge", "openpilot/cereal/messaging", ["./bridge"], notcar), From c9b9fd56ea3c55f7e439b155e8e292f628f4ceae Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 18:49:47 -0700 Subject: [PATCH 043/325] gc dead events (#38402) --- openpilot/cereal/log.capnp | 6 ++--- openpilot/selfdrive/selfdrived/events.py | 24 +------------------- openpilot/selfdrive/selfdrived/selfdrived.py | 2 -- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 60e0bb9010..9f0baf3594 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -75,7 +75,6 @@ struct OnroadEvent @0xc4fa6047f024e718 { driverUnresponsive2 @37; driverUnresponsive3 @38; belowSteerSpeed @39; - lowBattery @40; accFaulted @41; sensorDataInvalid @42; commIssue @43; @@ -107,14 +106,12 @@ struct OnroadEvent @0xc4fa6047f024e718 { noGps @68; wrongCruiseMode @69; modeldLagging @70; - deviceFalling @71; fanMalfunction @72; cameraMalfunction @73; cameraFrameRate @74; processNotRunning @75; dashcamMode @76; selfdriveInitializing @77; - usbError @78; cruiseMismatch @79; canBusMissing @80; selfdrivedLagging @81; @@ -132,7 +129,10 @@ struct OnroadEvent @0xc4fa6047f024e718 { userBookmark @95; excessiveActuation @96; + lowBatteryDEPRECATED @40; soundsUnavailableDEPRECATED @47; + deviceFallingDEPRECATED @71; + usbErrorDEPRECATED @78; audioFeedbackDEPRECATED @97; } } diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index ee9191fd1e..772b35f7cb 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -392,6 +392,7 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** + EventName.noGps: {}, EventName.stockFcw: {}, EventName.actuatorsApiUnavailable: {}, @@ -779,9 +780,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"), }, - EventName.noGps: { - }, - EventName.tooDistracted: { ET.NO_ENTRY: too_distracted_alert, }, @@ -840,11 +838,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: NoEntryAlert("Electronic Stability Control Disabled"), }, - EventName.lowBattery: { - ET.SOFT_DISABLE: soft_disable_alert("Low Battery"), - ET.NO_ENTRY: NoEntryAlert("Low Battery"), - }, - # Different openpilot services communicate between each other at a certain # interval. If communication does not follow the regular schedule this alert # is thrown. This can mean a service crashed, did not broadcast a message for @@ -898,13 +891,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: posenet_invalid_alert, }, - # When the localizer detects an acceleration of more than 40 m/s^2 (~4G) we - # alert the driver the device might have fallen from the windshield. - EventName.deviceFalling: { - ET.SOFT_DISABLE: soft_disable_alert("Device Fell Off Mount"), - ET.NO_ENTRY: NoEntryAlert("Device Fell Off Mount"), - }, - EventName.lowMemory: { ET.SOFT_DISABLE: soft_disable_alert("Low Memory: Reboot Your Device"), ET.PERMANENT: low_memory_alert, @@ -927,14 +913,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: NoEntryAlert("Controls Mismatch"), }, - # Sometimes the USB stack on the device can get into a bad state - # causing the connection to the panda to be lost - EventName.usbError: { - ET.SOFT_DISABLE: soft_disable_alert("USB Error: Reboot Your Device"), - ET.PERMANENT: NormalPermanentAlert("USB Error: Reboot Your Device"), - ET.NO_ENTRY: NoEntryAlert("USB Error: Reboot Your Device"), - }, - # This alert can be thrown for the following reasons: # - No CAN data received at all # - CAN data is received, but some message are not received at the right frequency diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 9fed889be3..dd94439b95 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -339,8 +339,6 @@ class SelfdriveD: self.events.add(EventName.radarTempUnavailable) elif any(self.sm['radarState'].radarErrors.to_dict().values()): self.events.add(EventName.radarFault) - if not self.sm.valid['pandaStates']: - self.events.add(EventName.usbError) if CS.canTimeout: self.events.add(EventName.canBusMissing) elif not CS.canValid: From 86753ea93db7638994a4f5a1b77cf9ce51221f2a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 21:01:19 -0700 Subject: [PATCH 044/325] better alerts (#38403) * better alerts * just rm that * lil more --- openpilot/selfdrive/selfdrived/events.py | 6 +++--- openpilot/selfdrive/ui/mici/onroad/alert_renderer.py | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 772b35f7cb..220efd9c18 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -622,7 +622,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # Thrown when the fan is driven at >50% but is not rotating EventName.fanMalfunction: { - ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Likely Hardware Issue"), + ET.PERMANENT: NormalPermanentAlert("Fan Malfunction", "Contact comma.ai/support"), }, # Camera is not outputting frames @@ -773,7 +773,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.sensorDataInvalid: { ET.PERMANENT: Alert( "Sensor Data Invalid", - "Possible Hardware Issue", + "Contact comma.ai/support", AlertStatus.normal, AlertSize.mid, Priority.LOWER, VisualAlert.none, AudibleAlert.none, .2, creation_delay=1.), ET.NO_ENTRY: NoEntryAlert("Sensor Data Invalid"), @@ -965,7 +965,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # and this alert is thrown. EventName.relayMalfunction: { ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Harness Relay Malfunction"), - ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Check Hardware"), + ET.PERMANENT: NormalPermanentAlert("Harness Relay Malfunction", "Contact comma.ai/support"), ET.NO_ENTRY: NoEntryAlert("Harness Relay Malfunction"), }, diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index e2e398a4e1..12b62924c4 100644 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -296,13 +296,10 @@ class AlertRenderer(Widget): # TODO: hack alert_text1 = alert.text1.lower().replace('calibrating: ', 'calibrating:\n') - can_draw_second_line = False # TODO: there should be a common way to determine font size based on text length to maximize rect if len(alert_text1) <= 12: - can_draw_second_line = True font_size = 92 - 10 elif len(alert_text1) <= 16: - can_draw_second_line = True font_size = 70 else: font_size = 64 - 10 @@ -334,13 +331,13 @@ class AlertRenderer(Widget): self._text_gen_time = time.monotonic() alert_text2 = self._alert_text2_gen or alert_text2 - if can_draw_second_line and alert_text2: + if alert_text2: last_line_h = self._alert_text1_label.rect.y + self._alert_text1_label.get_content_height(int(alert_layout.text_rect.width)) last_line_h -= 4 - if len(alert_text2) > 18: - small_font_size = 36 - elif len(alert_text2) > 24: + if len(alert_text2) > 24: small_font_size = 32 + elif len(alert_text2) > 18: + small_font_size = 36 else: small_font_size = 40 text_rect2 = rl.Rectangle( From 576de9c7ed8906631450be127543139f3f50e3bc Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 21:28:21 -0700 Subject: [PATCH 045/325] speed up jotpluggler build (#38406) --- .../jotpluggler/generate_event_extractors.py | 123 ++++++++++++++---- openpilot/tools/jotpluggler/sketch_layout.cc | 4 +- 2 files changed, 103 insertions(+), 24 deletions(-) diff --git a/openpilot/tools/jotpluggler/generate_event_extractors.py b/openpilot/tools/jotpluggler/generate_event_extractors.py index be9bd49003..a424ebc237 100644 --- a/openpilot/tools/jotpluggler/generate_event_extractors.py +++ b/openpilot/tools/jotpluggler/generate_event_extractors.py @@ -62,6 +62,8 @@ class Generator: def __init__(self, event_schema): self.event_schema = event_schema self.fixed_paths = [] + self.event_base_slots = {} + self.static_enums = [] self.tmp_index = 0 self.lines = [] self.emits_memo = {} @@ -103,9 +105,13 @@ class Generator: self.emit(indent, f"append_dynamic_scalar_point({path_expr}, tm, {double_expr}, series);") else: slot = self.add_fixed_path(path) - if kind == "Enum": - self.emit_enum_capture(indent, cxx_string(path), enum_names(schema)) - self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});") + names = enum_names(schema) if kind == "Enum" else [] + if names: + enum_index = len(self.static_enums) + self.static_enums.append(names) + self.emit(indent, f"append_fixed_enum_point({slot}, {enum_index}, tm, {double_expr}, series);") + else: + self.emit(indent, f"append_fixed_scalar_point(&series->fixed_series[{slot}], tm, {double_expr});") return if type_kind == "struct": @@ -144,9 +150,13 @@ class Generator: self.emit(indent, f"if ({' && '.join(conditions)}) {{") indent += 2 - value_var = self.tmp("value") - self.emit(indent, f"const auto {value_var} = {get_call};") - self.emit_node(indent, type_kind, type_proto, value_schema, value_var, field_path, field_path_expr, dynamic_path) + # Scalar getters are only consumed once. Emitting them directly avoids + # thousands of single-use locals in the generated extractor. + value_expr = get_call + if kind is None: + value_expr = self.tmp("value") + self.emit(indent, f"const auto {value_expr} = {get_call};") + self.emit_node(indent, type_kind, type_proto, value_schema, value_expr, field_path, field_path_expr, dynamic_path) if conditions: indent -= 2 @@ -247,31 +257,43 @@ class Generator: self.emit(indent + 2, "}") self.emit(indent, "}") self.emit(indent, "if (skip_raw_can) {") - self.emit(indent + 2, "return true;") + self.emit(indent + 2, "return;") self.emit(indent, "}") - def emit_event_case(self, field_name): + def emit_event_reader(self, field_name): field = self.event_schema.fields[field_name] proto = field.proto type_kind = field_type(field) type_proto = field_type_proto(field) kind = scalar_kind(type_proto) schema = field.schema if kind == "Enum" or type_kind in NESTED_TYPE_KINDS else None - self.emit(4, f"case static_cast({proto.discriminantValue}): {{") valid_slot = self.add_fixed_path(f"/{field_name}/valid") - mono_slot = self.add_fixed_path(f"/{field_name}/logMonoTime") - seconds_slot = self.add_fixed_path(f"/{field_name}/t") - self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{valid_slot}], tm, event.getValid() ? 1.0 : 0.0);") - self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{mono_slot}], tm, static_cast(event.getLogMonoTime()));") - self.emit(6, f"append_fixed_scalar_point(&series->fixed_series[{seconds_slot}], tm, tm);") - if field_name in {"can", "sendcan"}: - self.emit_can_special(6, field_name) - if self.node_emits(type_kind, type_proto, schema): + self.add_fixed_path(f"/{field_name}/logMonoTime") + self.add_fixed_path(f"/{field_name}/t") + self.event_base_slots[proto.discriminantValue] = valid_slot + + emits_payload = self.node_emits(type_kind, type_proto, schema) + if field_name not in {"can", "sendcan"} and not emits_payload: + return None + + reader_name = f"append_event_{proto.discriminantValue}" + needs_can = field_name in {"can", "sendcan"} + header_index = len(self.lines) + self.emit(0, "") + if needs_can: + self.emit_can_special(2, field_name) + if emits_payload: payload = self.tmp("payload") - self.emit(6, f"const auto {payload} = event.{accessor('get', field_name)}();") - self.emit_node(6, type_kind, type_proto, schema, payload, f"/{field_name}", None, False) - self.emit(6, "return true;") - self.emit(4, "}") + self.emit(2, f"const auto {payload} = event.{accessor('get', field_name)}();") + self.emit_node(2, type_kind, type_proto, schema, payload, f"/{field_name}", None, False) + self.emit(0, "}") + self.emit(0, "") + if needs_can: + signature = "const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double tm, SeriesAccumulator *series" + else: + signature = "const cereal::Event::Reader &event, double tm, SeriesAccumulator *series" + self.lines[header_index] = f"__attribute__((noinline)) void {reader_name}({signature}) {{" + return reader_name, needs_can def generate(self): self.lines = [] @@ -298,11 +320,66 @@ class Generator: self.emit(2, "}") self.emit(0, "}") self.emit(0, "") + self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series);") # noqa: E501 + self.emit(0, "") + + self.emit(0, "// Keep each event payload behind its own optimizer boundary. Combining the") + self.emit(0, "// whole schema into one function creates much more code and runs slower.") + event_readers = {} + for field_name in self.event_schema.union_fields: + event_readers[field_name] = self.emit_event_reader(field_name) + + self.emit(0, "static const std::initializer_list static_event_enum_names[] = {") + for names in self.static_enums: + names_expr = "{" + ", ".join(cxx_string(name) for name in names) + "}" + self.emit(2, f"{names_expr},") + self.emit(0, "};") + self.emit(0, "") + self.emit(0, "__attribute__((noinline)) void append_fixed_enum_point(size_t series_slot, size_t enum_index, double tm, double value, SeriesAccumulator *series) {") # noqa: E501 + self.emit(2, "RouteSeries *fixed_series = &series->fixed_series[series_slot];") + self.emit(2, "capture_static_enum_info(fixed_series->path, static_event_enum_names[enum_index], series);") + self.emit(2, "fixed_series->times.push_back(tm);") + self.emit(2, "fixed_series->values.push_back(value);") + self.emit(0, "}") + self.emit(0, "") + self.emit(0, "bool append_event_static_reader(cereal::Event::Which which, const cereal::Event::Reader &event, const dbc::Database *can_dbc, bool skip_raw_can, double time_offset, SeriesAccumulator *series) {") # noqa: E501 - self.emit(2, "const double tm = static_cast(event.getLogMonoTime()) / 1.0e9 - time_offset;") + self.emit(2, "const auto log_mono_time = event.getLogMonoTime();") + self.emit(2, "const double tm = static_cast(log_mono_time) / 1.0e9 - time_offset;") + + invalid_slot = "static_cast(-1)" + max_discriminant = max(self.event_base_slots) + base_slots = [self.event_base_slots.get(i, invalid_slot) for i in range(max_discriminant + 1)] + self.emit(2, "static constexpr size_t event_base_slots[] = {") + for slot in base_slots: + self.emit(4, f"{slot},") + self.emit(2, "};") + self.emit(2, "const size_t event_index = static_cast(which);") + self.emit(2, "if (event_index >= sizeof(event_base_slots) / sizeof(event_base_slots[0])) {") + self.emit(4, "return false;") + self.emit(2, "}") + self.emit(2, "const size_t base_slot = event_base_slots[event_index];") + self.emit(2, f"if (base_slot == {invalid_slot}) {{") + self.emit(4, "return false;") + self.emit(2, "}") + self.emit(2, "RouteSeries *base_series = &series->fixed_series[base_slot];") + self.emit(2, "base_series[0].times.push_back(tm);") + self.emit(2, "base_series[0].values.push_back(event.getValid() ? 1.0 : 0.0);") + self.emit(2, "base_series[1].times.push_back(tm);") + self.emit(2, "base_series[1].values.push_back(static_cast(log_mono_time));") + self.emit(2, "base_series[2].times.push_back(tm);") + self.emit(2, "base_series[2].values.push_back(tm);") self.emit(2, "switch (which) {") for field_name in self.event_schema.union_fields: - self.emit_event_case(field_name) + field = self.event_schema.fields[field_name] + self.emit(4, f"case static_cast({field.proto.discriminantValue}):") + reader = event_readers[field_name] + if reader is not None: + if reader[1]: + self.emit(6, f"{reader[0]}(event, can_dbc, skip_raw_can, tm, series);") + else: + self.emit(6, f"{reader[0]}(event, tm, series);") + self.emit(6, "return true;") self.emit(4, "default:") self.emit(6, "return false;") self.emit(2, "}") diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index 1f7df72fed..a489160b22 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -926,7 +926,9 @@ void append_scalar_point(RouteSeries *series, series->values.push_back(value); } -void append_fixed_scalar_point(RouteSeries *series, double tm, double value) { +// This has thousands of generated call sites. Inlining it duplicates vector +// growth logic throughout the extractor and is slower both to compile and run. +__attribute__((noinline)) void append_fixed_scalar_point(RouteSeries *series, double tm, double value) { series->times.push_back(tm); series->values.push_back(value); } From 75d590fb909b8c93dfe6aec966ae8c1bf55379de Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 22:20:09 -0700 Subject: [PATCH 046/325] replace catch2 tests for 20% faster builds (#38408) * replace catch2 tests * lil more * lil more * rm dep! --- SConstruct | 2 +- msgq_repo | 2 +- openpilot/common/SConscript | 4 +- openpilot/common/tests/native_test.h | 25 +++ openpilot/common/tests/test_runner.cc | 2 - openpilot/common/tests/test_swaglog.cc | 117 +++++--------- openpilot/common/tests/test_util.cc | 147 ------------------ .../pandad/tests/test_pandad_canprotocol.cc | 76 ++++----- openpilot/system/camerad/SConscript | 3 - openpilot/system/camerad/test/test_ae_gray.cc | 84 ---------- openpilot/system/loggerd/SConscript | 3 - openpilot/system/loggerd/tests/test_logger.cc | 75 --------- openpilot/system/loggerd/tests/test_runner.cc | 2 - .../system/loggerd/tests/test_zstd_writer.cc | 44 ------ openpilot/test_native.py | 5 +- openpilot/tools/cabana/SConscript | 3 - openpilot/tools/cabana/tests/test_cabana.cc | 62 +++----- openpilot/tools/cabana/tests/test_runner.cc | 7 - openpilot/tools/replay/SConscript | 3 - openpilot/tools/replay/tests/test_replay.cc | 18 --- pyproject.toml | 1 - uv.lock | 13 -- 22 files changed, 114 insertions(+), 584 deletions(-) create mode 100644 openpilot/common/tests/native_test.h delete mode 100644 openpilot/common/tests/test_runner.cc delete mode 100644 openpilot/common/tests/test_util.cc delete mode 100644 openpilot/system/camerad/test/test_ae_gray.cc delete mode 100644 openpilot/system/loggerd/tests/test_logger.cc delete mode 100644 openpilot/system/loggerd/tests/test_runner.cc delete mode 100644 openpilot/system/loggerd/tests/test_zstd_writer.cc delete mode 100644 openpilot/tools/cabana/tests/test_runner.cc delete mode 100644 openpilot/tools/replay/tests/test_replay.cc diff --git a/SConstruct b/SConstruct index 2a69cd2e38..4538bee69c 100644 --- a/SConstruct +++ b/SConstruct @@ -55,7 +55,7 @@ assert arch in [ "Darwin", # macOS arm64 (x86 not supported) ] -pkg_names = ['acados', 'bzip2', 'capnproto', 'catch2', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] +pkg_names = ['acados', 'bzip2', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] pkgs = [importlib.import_module(name) for name in pkg_names] acados = pkgs[pkg_names.index('acados')] ffmpeg = pkgs[pkg_names.index('ffmpeg')] diff --git a/msgq_repo b/msgq_repo index a771d031ec..6a8d2e8f06 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit a771d031ec58716824f365a89f0607fd786a1161 +Subproject commit 6a8d2e8f06f7864976d148f3cf423e8b3a418e8a diff --git a/openpilot/common/SConscript b/openpilot/common/SConscript index 13935b80d5..600cbec2e5 100644 --- a/openpilot/common/SConscript +++ b/openpilot/common/SConscript @@ -12,9 +12,7 @@ _common = env.Library('common', common_libs, LIBS="json11") Export('_common') if GetOption('extras'): - env.Program('tests/test_common', - ['tests/test_runner.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'], - LIBS=[_common, 'json11', 'zmq', 'pthread']) + env.Program('tests/test_swaglog', 'tests/test_swaglog.cc', LIBS=[_common, 'json11', 'zmq', 'pthread']) # Cython bindings params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11']) diff --git a/openpilot/common/tests/native_test.h b/openpilot/common/tests/native_test.h new file mode 100644 index 0000000000..d3dfbb9ce1 --- /dev/null +++ b/openpilot/common/tests/native_test.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +inline void native_test_check(bool condition, const char *expression, const char *file, int line) { + if (!condition) { + throw std::runtime_error(std::string(file) + ":" + std::to_string(line) + ": check failed: " + expression); + } +} + +#define CHECK(condition) native_test_check(static_cast(condition), #condition, __FILE__, __LINE__) +#define REQUIRE(...) CHECK((__VA_ARGS__)) + +template +int run_native_test(Function &&function) { + try { + function(); + return 0; + } catch (const std::exception &error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/openpilot/common/tests/test_runner.cc b/openpilot/common/tests/test_runner.cc deleted file mode 100644 index 62bf7476a1..0000000000 --- a/openpilot/common/tests/test_runner.cc +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" diff --git a/openpilot/common/tests/test_swaglog.cc b/openpilot/common/tests/test_swaglog.cc index 0c9cfcc1e6..07e2b16bc1 100644 --- a/openpilot/common/tests/test_swaglog.cc +++ b/openpilot/common/tests/test_swaglog.cc @@ -1,88 +1,47 @@ +#include +#include + #include -#include - -#include "catch2/catch.hpp" -#include "common/swaglog.h" -#include "common/util.h" -#include "common/version.h" #include "common/hardware/hw.h" +#include "common/swaglog.h" +#include "common/tests/native_test.h" #include "json11/json11.hpp" -std::string daemon_name = "testy"; -std::string dongle_id = "test_dongle_id"; -int LINE_NO = 0; +void test_swaglog() { + setenv("MANAGER_DAEMON", "swaglog_test", 1); + setenv("DONGLE_ID", "test_dongle_id", 1); + setenv("CLEAN", "1", 1); -void log_thread(int thread_id, int msg_cnt) { - for (int i = 0; i < msg_cnt; ++i) { - LOGD("%d", thread_id); - LINE_NO = __LINE__ - 1; - usleep(1); - } + void *context = zmq_ctx_new(); + CHECK(context != nullptr); + void *socket = zmq_socket(context, ZMQ_PULL); + CHECK(socket != nullptr); + int timeout = 5000; + CHECK(zmq_setsockopt(socket, ZMQ_RCVTIMEO, &timeout, sizeof(timeout)) == 0); + CHECK(zmq_bind(socket, Path::swaglog_ipc().c_str()) == 0); + + LOGD("native-cpp-log"); + + char buffer[4096] = {}; + const int size = zmq_recv(socket, buffer, sizeof(buffer), 0); + CHECK(size > 1); + CHECK(buffer[0] == CLOUDLOG_DEBUG); + std::string error; + const auto message = json11::Json::parse(std::string(buffer + 1, size - 1), error); + CHECK(error.empty()); + CHECK(message["levelnum"].int_value() == CLOUDLOG_DEBUG); + CHECK(message["msg"].string_value() == "native-cpp-log"); + CHECK(message["funcname"].string_value() == "test_swaglog"); + CHECK(message["filename"].string_value().find("test_swaglog.cc") != std::string::npos); + CHECK(message["ctx"]["daemon"].string_value() == "swaglog_test"); + CHECK(message["ctx"]["dongle_id"].string_value() == "test_dongle_id"); + CHECK(message["ctx"]["dirty"].bool_value() == false); + + CHECK(zmq_close(socket) == 0); + CHECK(zmq_ctx_destroy(context) == 0); } -void recv_log(int thread_cnt, int thread_msg_cnt) { - void *zctx = zmq_ctx_new(); - void *sock = zmq_socket(zctx, ZMQ_PULL); - zmq_bind(sock, Path::swaglog_ipc().c_str()); - std::vector thread_msgs(thread_cnt); - int total_count = 0; - - for (auto start = std::chrono::steady_clock::now(), now = start; - now < start + std::chrono::seconds{1} && total_count < (thread_cnt * thread_msg_cnt); - now = std::chrono::steady_clock::now()) { - char buf[4096] = {}; - if (zmq_recv(sock, buf, sizeof(buf), ZMQ_DONTWAIT) <= 0) { - if (errno == EAGAIN || errno == EINTR || errno == EFSM) continue; - break; - } - - REQUIRE(buf[0] == CLOUDLOG_DEBUG); - std::string err; - auto msg = json11::Json::parse(buf + 1, err); - REQUIRE(!msg.is_null()); - - REQUIRE(msg["levelnum"].int_value() == CLOUDLOG_DEBUG); - REQUIRE_THAT(msg["filename"].string_value(), Catch::Contains("test_swaglog.cc")); - REQUIRE(msg["funcname"].string_value() == "log_thread"); - REQUIRE(msg["lineno"].int_value() == LINE_NO); - - auto ctx = msg["ctx"]; - - REQUIRE(ctx["daemon"].string_value() == daemon_name); - REQUIRE(ctx["dongle_id"].string_value() == dongle_id); - REQUIRE(ctx["dirty"].bool_value() == true); - - REQUIRE(ctx["version"].string_value() == COMMA_VERSION); - - std::string device = Hardware::get_name(); - REQUIRE(ctx["device"].string_value() == device); - - int thread_id = atoi(msg["msg"].string_value().c_str()); - REQUIRE((thread_id >= 0 && thread_id < thread_cnt)); - thread_msgs[thread_id]++; - total_count++; - } - for (int i = 0; i < thread_cnt; ++i) { - INFO("thread :" << i); - REQUIRE(thread_msgs[i] == thread_msg_cnt); - } - zmq_close(sock); - zmq_ctx_destroy(zctx); -} - -TEST_CASE("swaglog") { - setenv("MANAGER_DAEMON", daemon_name.c_str(), 1); - setenv("DONGLE_ID", dongle_id.c_str(), 1); - setenv("dirty", "1", 1); - const int thread_cnt = 5; - const int thread_msg_cnt = 100; - - std::vector log_threads; - for (int i = 0; i < thread_cnt; ++i) { - log_threads.push_back(std::thread(log_thread, i, thread_msg_cnt)); - } - for (auto &t : log_threads) t.join(); - - recv_log(thread_cnt, thread_msg_cnt); +int main() { + return run_native_test(test_swaglog); } diff --git a/openpilot/common/tests/test_util.cc b/openpilot/common/tests/test_util.cc deleted file mode 100644 index d927b98a4d..0000000000 --- a/openpilot/common/tests/test_util.cc +++ /dev/null @@ -1,147 +0,0 @@ - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "catch2/catch.hpp" -#include "common/util.h" - -std::string random_bytes(int size) { - std::random_device rd; - std::independent_bits_engine rbe(rd()); - std::string bytes(size + 1, '\0'); - std::generate(bytes.begin(), bytes.end(), std::ref(rbe)); - return bytes; -} - -TEST_CASE("util::read_file") { - SECTION("read /proc/version") { - std::string ret = util::read_file("/proc/version"); - REQUIRE(ret.find("Linux version") != std::string::npos); - } - SECTION("read from sysfs") { - std::string ret = util::read_file("/sys/power/wakeup_count"); - REQUIRE(!ret.empty()); - } - SECTION("read file") { - char filename[] = "/tmp/test_read_XXXXXX"; - int fd = mkstemp(filename); - - REQUIRE(util::read_file(filename).empty()); - - std::string content = random_bytes(64 * 1024); - REQUIRE(write(fd, content.c_str(), content.size()) == (ssize_t)content.size()); - std::string ret = util::read_file(filename); - bool equal = (ret == content); - REQUIRE(equal); - close(fd); - } - SECTION("read directory") { - REQUIRE(util::read_file(".").empty()); - } - SECTION("read non-existent file") { - std::string ret = util::read_file("does_not_exist"); - REQUIRE(ret.empty()); - } - SECTION("read non-permission") { - REQUIRE(util::read_file("/proc/kmsg").empty()); - } -} - -TEST_CASE("util::file_exists") { - char filename[] = "/tmp/test_file_exists_XXXXXX"; - int fd = mkstemp(filename); - REQUIRE(fd != -1); - close(fd); - - SECTION("existent file") { - REQUIRE(util::file_exists(filename)); - REQUIRE(util::file_exists("/tmp")); - } - SECTION("nonexistent file") { - std::string fn = filename; - REQUIRE(!util::file_exists(fn + "/nonexistent")); - } - SECTION("file has no access permissions") { - std::string fn = "/proc/kmsg"; - std::ifstream f(fn); - REQUIRE(f.good() == false); - REQUIRE(util::file_exists(fn)); - } - ::remove(filename); -} - -TEST_CASE("util::read_files_in_dir") { - char tmp_path[] = "/tmp/test_XXXXXX"; - const std::string test_path = mkdtemp(tmp_path); - const std::string files[] = {".test1", "'test2'", "test3"}; - for (auto fn : files) { - std::ofstream{test_path + "/" + fn} << fn; - } - mkdir((test_path + "/dir").c_str(), 0777); - - std::map result = util::read_files_in_dir(test_path); - REQUIRE(result.find("dir") == result.end()); - REQUIRE(result.size() == std::size(files)); - for (auto& [k, v] : result) { - REQUIRE(k == v); - } -} - - -TEST_CASE("util::safe_fwrite") { - char filename[] = "/tmp/XXXXXX"; - int fd = mkstemp(filename); - close(fd); - std::string dat = random_bytes(1024 * 1024); - - FILE *f = util::safe_fopen(filename, "wb"); - REQUIRE(f != nullptr); - size_t size = util::safe_fwrite(dat.data(), 1, dat.size(), f); - REQUIRE(size == dat.size()); - int ret = util::safe_fflush(f); - REQUIRE(ret == 0); - ret = fclose(f); - REQUIRE(ret == 0); - bool equal = (dat == util::read_file(filename)); - REQUIRE(equal); -} - -TEST_CASE("util::create_directories") { - REQUIRE(system("rm /tmp/test_create_directories -rf") == 0); - std::string dir = "/tmp/test_create_directories/a/b/c/d/e/f"; - - auto check_dir_permissions = [](const std::string &path, mode_t mode) -> bool { - struct stat st = {}; - return stat(path.c_str(), &st) == 0 && (st.st_mode & S_IFMT) == S_IFDIR && (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) == mode; - }; - - SECTION("create_directories") { - REQUIRE(util::create_directories(dir, 0755)); - REQUIRE(check_dir_permissions(dir, 0755)); - } - SECTION("dir already exists") { - REQUIRE(util::create_directories(dir, 0755)); - REQUIRE(util::create_directories(dir, 0755)); - } - SECTION("a file exists with the same name") { - REQUIRE(util::create_directories(dir, 0755)); - int f = open((dir + "/file").c_str(), O_RDWR | O_CREAT, 0644); - REQUIRE(f != -1); - close(f); - REQUIRE(util::create_directories(dir + "/file", 0755) == false); - REQUIRE(util::create_directories(dir + "/file/1/2/3", 0755) == false); - } - SECTION("end with slashes") { - REQUIRE(util::create_directories(dir + "/", 0755)); - } - SECTION("empty") { - REQUIRE(util::create_directories("", 0755) == false); - } -} diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc b/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc index 83339a4c1c..50bbc51f49 100644 --- a/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_pandad_canprotocol.cc @@ -1,11 +1,7 @@ -#define CATCH_CONFIG_MAIN -#define CATCH_CONFIG_ENABLE_BENCHMARKING - #include -#include "catch2/catch.hpp" +#include "common/tests/native_test.h" #include "openpilot/cereal/messaging/messaging.h" -#include "common/util.h" #include "selfdrive/pandad/panda.h" struct PandaTest : public Panda { @@ -26,12 +22,9 @@ PandaTest::PandaTest(int can_list_size_, cereal::PandaState::PandaType hw_type_) int data_limit = ((hw_type == cereal::PandaState::PandaType::RED_PANDA) ? std::size(dlc_to_len) : 8); // prepare test data for (int i = 0; i < data_limit; ++i) { - std::random_device rd; - std::independent_bits_engine rbe(rd()); - int data_len = dlc_to_len[i]; std::string bytes(data_len, '\0'); - std::generate(bytes.begin(), bytes.end(), std::ref(rbe)); + for (int j = 0; j < data_len; ++j) bytes[j] = static_cast((i * 31 + j) & 0xff); test_data[data_len] = bytes; } @@ -39,16 +32,15 @@ PandaTest::PandaTest(int can_list_size_, cereal::PandaState::PandaType hw_type_) auto can_list = msg.initEvent().initSendcan(can_list_size); for (uint8_t i = 0; i < can_list_size; ++i) { auto can = can_list[i]; - uint32_t id = util::random_int(0, std::size(dlc_to_len) - 1); + uint32_t id = i % data_limit; const std::string &dat = test_data[dlc_to_len[id]]; can.setAddress(i); - can.setSrc(util::random_int(0, 2)); + can.setSrc(i % 3); can.setDat(kj::ArrayPtr((uint8_t *)dat.data(), dat.size())); total_pakets_size += sizeof(can_header) + dat.size(); } can_data_list = can_list.asReader(); - INFO("test " << can_list_size << " packets, total size " << total_pakets_size); } void PandaTest::test_can_send() { @@ -56,30 +48,29 @@ void PandaTest::test_can_send() { this->pack_can_buffer(can_data_list, [&](uint8_t *chunk, size_t size) { unpacked_data.insert(unpacked_data.end(), chunk, &chunk[size]); }); - REQUIRE(unpacked_data.size() == total_pakets_size); + CHECK(unpacked_data.size() == total_pakets_size); int cnt = 0; - INFO("test can message integrity"); for (int pos = 0, pckt_len = 0; pos < unpacked_data.size(); pos += pckt_len) { can_header header; memcpy(&header, &unpacked_data[pos], sizeof(can_header)); const uint8_t data_len = dlc_to_len[header.data_len_code]; pckt_len = sizeof(can_header) + data_len; - REQUIRE(header.addr == cnt); - REQUIRE(test_data.find(data_len) != test_data.end()); + CHECK(header.addr == cnt); + CHECK(test_data.find(data_len) != test_data.end()); const std::string &dat = test_data[data_len]; - REQUIRE(memcmp(dat.data(), &unpacked_data[pos + sizeof(can_header)], dat.size()) == 0); + CHECK(memcmp(dat.data(), &unpacked_data[pos + sizeof(can_header)], dat.size()) == 0); ++cnt; } - REQUIRE(cnt == can_list_size); + CHECK(cnt == can_list_size); } void PandaTest::test_can_recv(uint32_t rx_chunk_size) { std::vector frames; this->pack_can_buffer(can_data_list, [&](uint8_t *data, uint32_t size) { if (rx_chunk_size == 0) { - REQUIRE(this->unpack_can_buffer(data, size, frames)); + CHECK(this->unpack_can_buffer(data, size, frames)); } else { this->receive_buffer_size = 0; uint32_t pos = 0; @@ -90,46 +81,35 @@ void PandaTest::test_can_recv(uint32_t rx_chunk_size) { this->receive_buffer_size += chunk_size; pos += chunk_size; - REQUIRE(this->unpack_can_buffer(this->receive_buffer, this->receive_buffer_size, frames)); + CHECK(this->unpack_can_buffer(this->receive_buffer, this->receive_buffer_size, frames)); } } }); - REQUIRE(frames.size() == can_list_size); + CHECK(frames.size() == can_list_size); for (int i = 0; i < frames.size(); ++i) { - REQUIRE(frames[i].address == i); - REQUIRE(test_data.find(frames[i].dat.size()) != test_data.end()); + CHECK(frames[i].address == i); + CHECK(test_data.find(frames[i].dat.size()) != test_data.end()); const std::string &dat = test_data[frames[i].dat.size()]; - REQUIRE(memcmp(dat.data(), frames[i].dat.data(), dat.size()) == 0); + CHECK(memcmp(dat.data(), frames[i].dat.data(), dat.size()) == 0); } } -TEST_CASE("send/recv CAN 2.0 packets") { - auto can_list_size = GENERATE(1, 3, 5, 10, 30, 60, 100, 200); - PandaTest test(can_list_size, cereal::PandaState::PandaType::DOS); +void test_can_protocol() { + for (auto hw_type : {cereal::PandaState::PandaType::DOS, cereal::PandaState::PandaType::RED_PANDA}) { + for (int can_list_size : {1, 3, 5, 10, 30, 60, 100, 200}) { + PandaTest send_test(can_list_size, hw_type); + send_test.test_can_send(); - SECTION("can_send") { - test.test_can_send(); - } - SECTION("can_receive") { - test.test_can_recv(); - } - SECTION("chunked_can_receive") { - test.test_can_recv(0x40); + PandaTest receive_test(can_list_size, hw_type); + receive_test.test_can_recv(); + + PandaTest chunked_receive_test(can_list_size, hw_type); + chunked_receive_test.test_can_recv(0x40); + } } } -TEST_CASE("send/recv CAN FD packets") { - auto can_list_size = GENERATE(1, 3, 5, 10, 30, 60, 100, 200); - PandaTest test(can_list_size, cereal::PandaState::PandaType::RED_PANDA); - - SECTION("can_send") { - test.test_can_send(); - } - SECTION("can_receive") { - test.test_can_recv(); - } - SECTION("chunked_can_receive") { - test.test_can_recv(0x40); - } +int main() { + return run_native_test(test_can_protocol); } diff --git a/openpilot/system/camerad/SConscript b/openpilot/system/camerad/SConscript index c28330b32c..e6bc3f2bfb 100644 --- a/openpilot/system/camerad/SConscript +++ b/openpilot/system/camerad/SConscript @@ -6,6 +6,3 @@ if arch != "Darwin": camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc', 'cameras/cdm.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc']) env.Program('camerad', ['main.cc', camera_obj], LIBS=libs) - -if GetOption("extras") and arch == "x86_64": - env.Program('test/test_ae_gray', ['test/test_ae_gray.cc', camera_obj], LIBS=libs) diff --git a/openpilot/system/camerad/test/test_ae_gray.cc b/openpilot/system/camerad/test/test_ae_gray.cc deleted file mode 100644 index 39c3d9c4e5..0000000000 --- a/openpilot/system/camerad/test/test_ae_gray.cc +++ /dev/null @@ -1,84 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" - -#include - -#include -#include - -#include "common/util.h" -#include "system/camerad/cameras/camera_common.h" - -#define W 240 -#define H 160 - - -#define TONE_SPLITS 3 - -float gts[TONE_SPLITS * TONE_SPLITS * TONE_SPLITS * TONE_SPLITS] = { - 0.917969, 0.917969, 0.375000, 0.917969, 0.375000, 0.375000, 0.187500, 0.187500, 0.187500, 0.917969, - 0.375000, 0.375000, 0.187500, 0.187500, 0.187500, 0.187500, 0.187500, 0.187500, 0.093750, 0.093750, - 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.917969, 0.375000, 0.375000, - 0.187500, 0.187500, 0.187500, 0.187500, 0.187500, 0.187500, 0.093750, 0.093750, 0.093750, 0.093750, - 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, 0.093750, - 0.093750, 0.093750, 0.093750, 0.093750, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, - 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, - 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, - 0.000000}; - - -TEST_CASE("camera.test_calculate_exposure_value") { - // set up fake camerabuf - CameraBuf cb = {}; - VisionBuf vb = {}; - uint8_t * fb_y = new uint8_t[W*H]; - vb.y = fb_y; - cb.cur_yuv_buf = &vb; - cb.out_img_width = W; - cb.out_img_height = H; - Rect rect = {0, 0, W-1, H-1}; - - printf("AE test patterns %dx%d\n", cb.out_img_width, cb.out_img_height); - - // mix of 5 tones - uint8_t l[5] = {0, 24, 48, 96, 235}; // 235 is yuv max - - bool passed = true; - float rtol = 0.05; - // generate pattern and calculate EV - int cnt = 0; - for (int i_0=0; i_0 rtol*evgt) { - passed = false; - } - - // report - printf("%d/%d/%d/%d/%d: ev %f, gt %f, err %f\n", h_0, h_1, h_2, h_3, h_4, ev, evgt, fabs(ev - evgt) / (evgt != 0 ? evgt : 0.00001f)); - cnt++; - } - } - } - } - assert(passed); - - delete[] fb_y; -} diff --git a/openpilot/system/loggerd/SConscript b/openpilot/system/loggerd/SConscript index 7f6d4faf0c..0222803f1d 100644 --- a/openpilot/system/loggerd/SConscript +++ b/openpilot/system/loggerd/SConscript @@ -17,6 +17,3 @@ libs.insert(0, logger_lib) env.Program('loggerd', ['loggerd.cc'], LIBS=libs, FRAMEWORKS=frameworks) env.Program('encoderd', ['encoderd.cc'], LIBS=libs, FRAMEWORKS=frameworks) env.Program('bootlog.cc', LIBS=libs, FRAMEWORKS=frameworks) - -if GetOption('extras'): - env.Program('tests/test_logger', ['tests/test_runner.cc', 'tests/test_logger.cc', 'tests/test_zstd_writer.cc'], LIBS=libs) diff --git a/openpilot/system/loggerd/tests/test_logger.cc b/openpilot/system/loggerd/tests/test_logger.cc deleted file mode 100644 index 61509c256c..0000000000 --- a/openpilot/system/loggerd/tests/test_logger.cc +++ /dev/null @@ -1,75 +0,0 @@ -#include "catch2/catch.hpp" -#include "system/loggerd/logger.h" - -typedef cereal::Sentinel::SentinelType SentinelType; - -void verify_segment(const std::string &route_path, int segment, int max_segment, int required_event_cnt) { - const std::string segment_path = route_path + "--" + std::to_string(segment); - SentinelType begin_sentinel = segment == 0 ? SentinelType::START_OF_ROUTE : SentinelType::START_OF_SEGMENT; - SentinelType end_sentinel = segment == max_segment - 1 ? SentinelType::END_OF_ROUTE : SentinelType::END_OF_SEGMENT; - - REQUIRE(!util::file_exists(segment_path + "/rlog.lock")); - for (const char *fn : {"/rlog.zst", "/qlog.zst"}) { - const std::string log_file = segment_path + fn; - std::string log = util::read_file(log_file); - REQUIRE(!log.empty()); - std::string decompressed_log = zstd_decompress(log); - int event_cnt = 0, i = 0; - kj::ArrayPtr words((capnp::word *)decompressed_log.data(), decompressed_log.size() / sizeof(capnp::word)); - while (words.size() > 0) { - try { - capnp::FlatArrayMessageReader reader(words); - auto event = reader.getRoot(); - words = kj::arrayPtr(reader.getEnd(), words.end()); - if (i == 0) { - REQUIRE(event.which() == cereal::Event::INIT_DATA); - } else if (i == 1) { - REQUIRE(event.which() == cereal::Event::SENTINEL); - REQUIRE(event.getSentinel().getType() == begin_sentinel); - REQUIRE(event.getSentinel().getSignal() == 0); - } else if (words.size() > 0) { - REQUIRE(event.which() == cereal::Event::CLOCKS); - ++event_cnt; - } else { - // the last event must be SENTINEL - REQUIRE(event.which() == cereal::Event::SENTINEL); - REQUIRE(event.getSentinel().getType() == end_sentinel); - REQUIRE(event.getSentinel().getSignal() == (end_sentinel == SentinelType::END_OF_ROUTE ? 1 : 0)); - } - ++i; - } catch (const kj::Exception &ex) { - INFO("failed parse " << i << " exception :" << ex.getDescription()); - REQUIRE(0); - break; - } - } - REQUIRE(event_cnt == required_event_cnt); - } -} - -void write_msg(LoggerState *logger) { - MessageBuilder msg; - msg.initEvent().initClocks(); - logger->write(msg.toBytes(), true); -} - -TEST_CASE("logger") { - const int segment_cnt = 100; - const std::string log_root = "/tmp/test_logger"; - REQUIRE(system(("rm " + log_root + " -rf").c_str()) == 0); - std::string route_name; - { - LoggerState logger(log_root); - route_name = logger.routeName(); - for (int i = 0; i < segment_cnt; ++i) { - REQUIRE(logger.next()); - REQUIRE(util::file_exists(logger.segmentPath() + "/rlog.lock")); - REQUIRE(logger.segment() == i); - write_msg(&logger); - } - logger.setExitSignal(1); - } - for (int i = 0; i < segment_cnt; ++i) { - verify_segment(log_root + "/" + route_name, i, segment_cnt, 1); - } -} diff --git a/openpilot/system/loggerd/tests/test_runner.cc b/openpilot/system/loggerd/tests/test_runner.cc deleted file mode 100644 index 62bf7476a1..0000000000 --- a/openpilot/system/loggerd/tests/test_runner.cc +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" diff --git a/openpilot/system/loggerd/tests/test_zstd_writer.cc b/openpilot/system/loggerd/tests/test_zstd_writer.cc deleted file mode 100644 index 479e866a14..0000000000 --- a/openpilot/system/loggerd/tests/test_zstd_writer.cc +++ /dev/null @@ -1,44 +0,0 @@ -#include - -#include -#include -#include - -#include "common/util.h" -#include "system/loggerd/logger.h" -#include "system/loggerd/zstd_writer.h" - -TEST_CASE("ZstdFileWriter writes and compresses data correctly in loops", "[ZstdFileWriter]") { - const std::string filename = "test_zstd_file.zst"; - const int iterations = 100; - const size_t dataSize = 1024; - - std::string totalTestData; - - // Step 1: Write compressed data to file in a loop - { - ZstdFileWriter writer(filename, LOG_COMPRESSION_LEVEL); - // Write various data sizes including edge cases - std::vector testSizes = {dataSize, 1, 0, dataSize * 2}; // Normal, minimal, empty, large - for (int i = 0; i < iterations; ++i) { - size_t currentSize = testSizes[i % testSizes.size()]; - std::string testData = util::random_string(currentSize); - totalTestData.append(testData); - - writer.write((void *)testData.c_str(), testData.size()); - } - } - - // Step 2: Decompress the file and verify the data - auto compressedContent = util::read_file(filename); - REQUIRE(compressedContent.size() > 0); - REQUIRE(compressedContent.size() < totalTestData.size()); - std::string decompressedData = zstd_decompress(compressedContent); - - // Step 3: Verify that the decompressed data matches the original accumulated data - REQUIRE(decompressedData.size() == totalTestData.size()); - REQUIRE(std::memcmp(decompressedData.data(), totalTestData.c_str(), totalTestData.size()) == 0); - - // Clean up the test file - std::remove(filename.c_str()); -} diff --git a/openpilot/test_native.py b/openpilot/test_native.py index a69771f675..eed549f4e4 100644 --- a/openpilot/test_native.py +++ b/openpilot/test_native.py @@ -7,12 +7,9 @@ from openpilot.common.test import OpenpilotTestCase NATIVE_TESTS = ( - "openpilot/common/tests/test_common", + "openpilot/common/tests/test_swaglog", "openpilot/selfdrive/pandad/tests/test_pandad_canprotocol", - "openpilot/system/loggerd/tests/test_logger", - "openpilot/tools/cabana/tests/test_cabana", "openpilot/tools/cabana/tests/test_dbc_core", - "openpilot/tools/replay/tests/test_replay", ) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index fcaa3b7930..805bc41876 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -110,14 +110,11 @@ cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs + [bootstrap_icons_src cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('extras'): - cabana_env.Program('tests/test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) - # This target deliberately uses the base environment and links no Qt libraries. # It prevents Qt dependencies from creeping back into the DBC core. dbc_core_test_env = env.Clone() dbc_core_test_env['CXXFLAGS'] += [opendbc_path] dbc_core_test_objects = [ - dbc_core_test_env.Object('tests/dbc_core_test_runner', 'tests/test_runner.cc'), dbc_core_test_env.Object('tests/dbc_core_tests', 'tests/test_cabana.cc'), dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index c66f57d593..53be1b0afa 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -1,20 +1,14 @@ -#undef INFO #include #include -#include "catch2/catch.hpp" +#include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/core/settings.h" - -#ifdef QT_CORE_LIB -#include -#endif const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; -TEST_CASE("DBCFile::generateDBC") { +void test_generate_dbc() { std::string fn = std::string(OPENDBC_FILE_PATH) + "/tesla_can.dbc"; DBCFile dbc_origin(fn); DBCFile dbc_from_generated("", dbc_origin.generateDBC()); @@ -35,7 +29,7 @@ TEST_CASE("DBCFile::generateDBC") { } } -TEST_CASE("DBCFile::generateDBC - comment order") { +void test_comment_order() { // Ensure that message comments are followed by signal comments and in the correct order std::string content = R"(BO_ 160 message_1: 8 EON SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX @@ -52,7 +46,7 @@ CM_ SG_ 162 signal_2 "signal comment"; REQUIRE(dbc.generateDBC() == content); } -TEST_CASE("DBCFile::generateDBC -- preserve original header") { +void test_preserve_original_header() { std::string content = R"(VERSION "1.0" NS_ : @@ -72,7 +66,7 @@ CM_ SG_ 160 signal_1 "signal comment"; REQUIRE(dbc.generateDBC() == content); } -TEST_CASE("DBCFile::generateDBC - escaped quotes") { +void test_escaped_quotes() { std::string content = R"(BO_ 160 message_1: 8 EON SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX @@ -83,7 +77,7 @@ CM_ SG_ 160 signal_1 "signal comment with \"escaped quotes\""; REQUIRE(dbc.generateDBC() == content); } -TEST_CASE("parse_dbc") { +void test_parse_dbc() { std::string content = R"( BO_ 160 message_1: 8 EON SG_ signal_1 : 0|12@1+ (1,0) [0|4095] "unit" XXX @@ -149,7 +143,7 @@ CM_ SG_ 162 signal_1 "signal comment with \"escaped quotes\""; REQUIRE(msg->sigs[0]->comment == "signal comment with \"escaped quotes\""); } -TEST_CASE("parse_opendbc") { +void test_parse_opendbc() { std::vector errors; for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH)) { if (!entry.is_regular_file() || entry.path().extension() != ".dbc") continue; @@ -161,11 +155,11 @@ TEST_CASE("parse_opendbc") { } std::ostringstream details; for (const auto &error : errors) details << error << '\n'; - INFO(details.str()); + if (!errors.empty()) std::cerr << details.str(); REQUIRE(errors.empty()); } -TEST_CASE("DBCManager core callbacks") { +void test_dbc_manager() { DBCManager manager; int files_changed = 0; int signals_added = 0; @@ -192,34 +186,16 @@ TEST_CASE("DBCManager core callbacks") { REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr); } -TEST_CASE("Cabana settings core defaults") { - CabanaSettingsState state; - REQUIRE(state.fps == 10); - REQUIRE(state.chart_range == 180); - REQUIRE(state.drag_direction == CabanaSettingsState::MsbFirst); - REQUIRE(state.recent_files.empty()); +void test_cabana_core() { + test_generate_dbc(); + test_comment_order(); + test_preserve_original_header(); + test_escaped_quotes(); + test_parse_dbc(); + test_parse_opendbc(); + test_dbc_manager(); } -#ifdef QT_CORE_LIB -TEST_CASE("CabanaColor preserves QColor transformations") { - const std::vector colors = { - QColor(102, 86, 169, 64), QColor(0, 187, 255, 128), QColor(255, 0, 0, 128), QColor(45, 120, 75, 255), - }; - for (const auto &qt_color : colors) { - CabanaColor color(qt_color.red(), qt_color.green(), qt_color.blue(), qt_color.alpha()); - for (int factor : {75, 100, 135, 150, 200}) { - const auto lighter = color.lighter(factor); - const auto qt_lighter = qt_color.lighter(factor); - CHECK(std::abs(lighter.red() - qt_lighter.red()) <= 1); - CHECK(std::abs(lighter.green() - qt_lighter.green()) <= 1); - CHECK(std::abs(lighter.blue() - qt_lighter.blue()) <= 1); - - const auto darker = color.darker(factor); - const auto qt_darker = qt_color.darker(factor); - CHECK(std::abs(darker.red() - qt_darker.red()) <= 1); - CHECK(std::abs(darker.green() - qt_darker.green()) <= 1); - CHECK(std::abs(darker.blue() - qt_darker.blue()) <= 1); - } - } +int main() { + return run_native_test(test_cabana_core); } -#endif diff --git a/openpilot/tools/cabana/tests/test_runner.cc b/openpilot/tools/cabana/tests/test_runner.cc deleted file mode 100644 index a76c8e16b9..0000000000 --- a/openpilot/tools/cabana/tests/test_runner.cc +++ /dev/null @@ -1,7 +0,0 @@ -#define CATCH_CONFIG_RUNNER -#include "catch2/catch.hpp" - -int main(int argc, char **argv) { - const int res = Catch::Session().run(argc, argv); - return (res < 0xff ? res : 0xff); -} diff --git a/openpilot/tools/replay/SConscript b/openpilot/tools/replay/SConscript index 643de97bb9..c5abae502c 100644 --- a/openpilot/tools/replay/SConscript +++ b/openpilot/tools/replay/SConscript @@ -14,6 +14,3 @@ replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEW Export('replay_lib') replay_libs = [replay_lib] + ffmpeg_libs + ['bz2', 'zstd', 'ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) - -if GetOption('extras'): - replay_env.Program('tests/test_replay', ['tests/test_replay.cc'], LIBS=replay_libs) diff --git a/openpilot/tools/replay/tests/test_replay.cc b/openpilot/tools/replay/tests/test_replay.cc deleted file mode 100644 index 45fcc98191..0000000000 --- a/openpilot/tools/replay/tests/test_replay.cc +++ /dev/null @@ -1,18 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" -#include "tools/replay/filereader.h" -#include "tools/replay/replay.h" - -const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; - -TEST_CASE("LogReader") { - SECTION("corrupt log") { - FileReader reader(true); - std::string corrupt_content = reader.read(TEST_RLOG_URL); - corrupt_content.resize(corrupt_content.length() / 2); - corrupt_content = decompressBZ2(corrupt_content); - LogReader log; - REQUIRE(log.load(corrupt_content.data(), corrupt_content.size())); - REQUIRE(log.events.size() > 0); - } -} diff --git a/pyproject.toml b/pyproject.toml index 68c5dd7fe7..678720aa04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dependencies = [ # vendored native dependencies "comma-deps-capnproto", - "comma-deps-catch2", "comma-deps-acados", "comma-deps-ffmpeg", "comma-deps-zstd", diff --git a/uv.lock b/uv.lock index ff155ab3bc..de03b28288 100644 --- a/uv.lock +++ b/uv.lock @@ -142,14 +142,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/a9/f61fe62045c4ea867f51f2d74b4edfabd6c272c62a3281a9f1f33825a725/comma_deps_capnproto-1.0.1.post93-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f98cdba8f8c7f08a7a0c0a4b7cc0bfcf515e29ad8f1b5cb8eda4e253bef5e6e3", size = 2590769, upload-time = "2026-07-08T19:30:49.728Z" }, ] -[[package]] -name = "comma-deps-catch2" -version = "2.13.10.post93" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ee/b4ef7758d04a024775d49558ca930fec19aa37cd691ad9182b7141f4d4d7/comma_deps_catch2-2.13.10.post93-py3-none-any.whl", hash = "sha256:8f23293251b5db48c08885d8816ca252b3fb11b0c1c26775bae36e8b217e865e", size = 137085, upload-time = "2026-07-08T19:30:53.44Z" }, -] - [[package]] name = "comma-deps-eigen" version = "3.4.0.post93" @@ -621,7 +613,6 @@ source = { editable = "msgq_repo" } [package.metadata] requires-dist = [ - { name = "catch2", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2" }, { name = "codespell", marker = "extra == 'dev'" }, { name = "cppcheck", marker = "extra == 'dev'" }, { name = "cpplint", marker = "extra == 'dev'" }, @@ -699,10 +690,8 @@ name = "openpilot" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "cffi" }, { name = "comma-deps-acados" }, { name = "comma-deps-capnproto" }, - { name = "comma-deps-catch2" }, { name = "comma-deps-ffmpeg" }, { name = "comma-deps-gcc-arm-none-eabi" }, { name = "comma-deps-git-lfs" }, @@ -768,13 +757,11 @@ standalone = [ [package.metadata] requires-dist = [ - { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, { name = "comma-deps-acados" }, { name = "comma-deps-bootstrap-icons", marker = "extra == 'tools'" }, { name = "comma-deps-bzip2", marker = "extra == 'tools'" }, { name = "comma-deps-capnproto" }, - { name = "comma-deps-catch2" }, { name = "comma-deps-ffmpeg" }, { name = "comma-deps-gcc-arm-none-eabi" }, { name = "comma-deps-git-lfs" }, From 08fd571272513ce792b4882857b21b1731f1252b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jul 2026 22:35:27 -0700 Subject: [PATCH 047/325] add -pipe to build (#38410) --- SConstruct | 1 + 1 file changed, 1 insertion(+) diff --git a/SConstruct b/SConstruct index 4538bee69c..7b28ff39fb 100644 --- a/SConstruct +++ b/SConstruct @@ -129,6 +129,7 @@ env = Environment( CCFLAGS=[ "-g", "-fPIC", + "-pipe", "-O2", "-Wunused", "-Werror", From aac9d9ecf427afcfe727d569e561a29ee3d9a1f3 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 22 Jul 2026 10:25:58 -0700 Subject: [PATCH 048/325] ui: show eGPU icon when enumerated (#38407) * ui: react to egpu hotplug instead of waiting for modeld params * use chestnutPresent --- openpilot/common/params_keys.h | 2 -- openpilot/selfdrive/modeld/helpers.py | 5 +++++ openpilot/selfdrive/modeld/modeld.py | 10 +++------- openpilot/selfdrive/ui/ui_state.py | 10 ++++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 7ecac1c729..4565f3d5b2 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -126,7 +126,5 @@ inline static std::unordered_map keys = { {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, - {"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, - {"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 7c322c5993..8516325140 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -6,6 +6,8 @@ import struct import tempfile from pathlib import Path +from openpilot.common.file_chunker import get_manifest_path + MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' USBGPU_VID = 0xADD1 @@ -53,3 +55,6 @@ def usbgpu_present() -> bool: except Exception: pass return False + +def usbgpu_compiled() -> bool: + return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file() diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 41a1f44e5f..cd8740b462 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -22,9 +22,9 @@ from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState -from openpilot.common.file_chunker import open_file_chunked, get_manifest_path +from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" @@ -137,12 +137,8 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - _present = usbgpu_present() - _compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True))) - USBGPU = _present and _compiled + USBGPU = usbgpu_present() and usbgpu_compiled() params = Params() - params.put_bool("UsbGpuPresent", _present) - params.put_bool("UsbGpuCompiled", _compiled) config_realtime_process(7, 54) diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 78388593c8..7efcf2a7c9 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,6 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC +from openpilot.selfdrive.modeld.helpers import usbgpu_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 PARAM_UPDATE_TIME = 1 / 5.0 @@ -75,8 +76,8 @@ class UIState: self.is_release = self.params.get_bool("IsReleaseBranch") self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") - self.usbgpu: bool = self.params.get_bool("UsbGpuPresent") - self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled") + self.usbgpu: bool = False + self.usbgpu_compiled: bool = usbgpu_compiled() self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -203,8 +204,9 @@ class UIState: self.is_metric = self.params.get_bool("IsMetric") self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.usbgpu = self.params.get_bool("UsbGpuPresent") - self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled") + self.usbgpu = self.sm["deviceState"].chestnutPresent + if not self.usbgpu_compiled: + self.usbgpu_compiled = usbgpu_compiled() class Device: From 74ac5ef9a01362bb09512b4a522f10a31199d4eb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 22 Jul 2026 13:26:28 -0700 Subject: [PATCH 049/325] try ctypes params (#38411) * try cffi params * ctypes * lil more * rm cython * lil more * that was fine * lil more * Drop unrelated Params concurrency changes * Clean up ctypes Params build integration * just c * Clarify Params exception translation * Expand Params C wrappers * lil more --- openpilot/common/SConscript | 13 +- openpilot/common/params.py | 208 +++++++++++++++++- openpilot/common/params_c.cc | 163 ++++++++++++++ openpilot/common/params_pyx.pyx | 191 ---------------- openpilot/common/tests/test_params.py | 5 + openpilot/selfdrive/ui/mici/widgets/button.py | 11 +- openpilot/system/manager/process.py | 2 +- openpilot/system/ui/lib/multilang.py | 10 +- openpilot/system/ui/lib/wifi_manager.py | 11 +- 9 files changed, 396 insertions(+), 218 deletions(-) create mode 100644 openpilot/common/params_c.cc delete mode 100644 openpilot/common/params_pyx.pyx diff --git a/openpilot/common/SConscript b/openpilot/common/SConscript index 600cbec2e5..4e06ae88a1 100644 --- a/openpilot/common/SConscript +++ b/openpilot/common/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython') +Import('env') common_libs = [ 'params.cc', @@ -11,12 +11,9 @@ common_libs = [ _common = env.Library('common', common_libs, LIBS="json11") Export('_common') +params_python = env.SharedLibrary('params_c', 'params_c.cc', LIBS=[_common, 'zmq', 'json11']) +common_python = [params_python] +Export('common_python') + if GetOption('extras'): env.Program('tests/test_swaglog', 'tests/test_swaglog.cc', LIBS=[_common, 'json11', 'zmq', 'pthread']) - -# Cython bindings -params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11']) - -common_python = [params_python] - -Export('common_python') diff --git a/openpilot/common/params.py b/openpilot/common/params.py index 8af80c04bb..fc292b56dd 100644 --- a/openpilot/common/params.py +++ b/openpilot/common/params.py @@ -1,16 +1,210 @@ -from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName -assert Params -assert ParamKeyFlag -assert ParamKeyType -assert UnknownKeyName +import sys +import json +import ctypes +import weakref +import builtins +import datetime +from pathlib import Path +from enum import IntEnum, IntFlag + +from openpilot.common.swaglog import cloudlog + + +class ParamKeyFlag(IntFlag): + PERSISTENT = 0x02 + CLEAR_ON_MANAGER_START = 0x04 + CLEAR_ON_ONROAD_TRANSITION = 0x08 + CLEAR_ON_OFFROAD_TRANSITION = 0x10 + DEVELOPMENT_ONLY = 0x40 + CLEAR_ON_IGNITION_ON = 0x80 + ALL = 0xFFFFFFFF + + +class ParamKeyType(IntEnum): + STRING = 0 + BOOL = 1 + INT = 2 + FLOAT = 3 + TIME = 4 + JSON = 5 + BYTES = 6 + + +_suffix = ".dylib" if sys.platform == "darwin" else ".so" +lib = ctypes.CDLL(Path(__file__).with_name(f"libparams_c{_suffix}")) + +ParamsHandle = ctypes.c_void_p + + +class ParamsBuffer(ctypes.Structure): + _fields_ = [("data", ctypes.c_void_p), ("size", ctypes.c_size_t)] + + +def _bind_raw(name, args, result=None): + function = getattr(lib, name) + function.argtypes = args + function.restype = result + return function + + +params_last_error = _bind_raw("params_last_error", [], ctypes.c_char_p) + + +def _bind(name, args, result=None): + function = _bind_raw(name, args, result) + + def checked(*call_args): + value = function(*call_args) + if error := params_last_error(): + raise RuntimeError(error.decode()) + return value + + return checked + + +params_create = _bind("params_create", [ctypes.c_char_p, ctypes.c_size_t], ParamsHandle) +params_destroy = _bind("params_destroy", [ParamsHandle]) +params_clear_all = _bind("params_clear_all", [ParamsHandle, ctypes.c_uint]) +params_check_key = _bind("params_check_key", [ParamsHandle, ctypes.c_char_p], ctypes.c_bool) +params_get_key_type = _bind("params_get_key_type", [ParamsHandle, ctypes.c_char_p], ctypes.c_int) +params_get_default = _bind("params_get_default", [ParamsHandle, ctypes.c_char_p], ParamsBuffer) +params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ParamsBuffer) +params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool) +params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int) +params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int) +params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int) +params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer) +params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t) +params_key_at = _bind("params_key_at", [ParamsHandle, ctypes.c_size_t], ParamsBuffer) + +PYTHON_2_CPP = { + (str, ParamKeyType.STRING): lambda v: v, + (builtins.bool, ParamKeyType.BOOL): lambda v: "1" if v else "0", + (int, ParamKeyType.INT): str, + (float, ParamKeyType.FLOAT): str, + (datetime.datetime, ParamKeyType.TIME): lambda v: v.isoformat(), + (dict, ParamKeyType.JSON): json.dumps, + (list, ParamKeyType.JSON): json.dumps, + (bytes, ParamKeyType.BYTES): lambda v: v, +} +CPP_2_PYTHON = { + ParamKeyType.STRING: lambda v: v.decode("utf-8"), + ParamKeyType.BOOL: lambda v: v == b"1", + ParamKeyType.INT: int, + ParamKeyType.FLOAT: float, + ParamKeyType.TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")), + ParamKeyType.JSON: json.loads, + ParamKeyType.BYTES: lambda v: v, +} + + +def ensure_bytes(v): + return v.encode() if isinstance(v, str) else v + + +def _copy_string(value): + if value.data is None: + return None + return ctypes.string_at(value.data, value.size) + + +class UnknownKeyName(Exception): + pass + + +class Params: + def __init__(self, d=""): + path = ensure_bytes(d) + self.p = params_create(path, len(path)) + self._finalizer = weakref.finalize(self, params_destroy, self.p) + self.d = d + + def __reduce__(self): + return (type(self), (self.d,)) + + def clear_all(self, tx_flag=ParamKeyFlag.ALL): + params_clear_all(self.p, int(tx_flag)) + + def check_key(self, key): + key = ensure_bytes(key) + if b"\0" in key or not params_check_key(self.p, key): + raise UnknownKeyName(key) + return key + + def python2cpp(self, proposed_type, expected_type, value, key): + cast = PYTHON_2_CPP.get((proposed_type, expected_type)) + if cast: + return cast(value) + raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}") + + def _cpp2python(self, t, value, default, key): + if value is None: + return None + try: + return CPP_2_PYTHON[t](value) + except (KeyError, TypeError, ValueError): + cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") + return self._cpp2python(t, default, None, key) + + def _default(self, key): + return _copy_string(params_get_default(self.p, key)) + + def get(self, key, block=False, return_default=False): + k = self.check_key(key) + t = self.get_type(k) + default = self._default(k) if return_default else None + value = _copy_string(params_get(self.p, k, block)) + if value == b"": + if block: + raise KeyboardInterrupt + return self._cpp2python(t, default, None, key) + return self._cpp2python(t, value, default, key) + + def get_bool(self, key, block=False): + return bool(params_get_bool(self.p, self.check_key(key), block)) + + def _put_cast(self, key, dat): + return ensure_bytes(self.python2cpp(type(dat), self.get_type(key), dat, key)) + + def put(self, key, dat, block=False): + """Write a parameter. block=True waits until it is persisted to disk.""" + k = self.check_key(key) + value = self._put_cast(k, dat) + params_put(self.p, k, value, len(value), block) + + def put_bool(self, key, val, block=False): + params_put_bool(self.p, self.check_key(key), val, block) + + def remove(self, key): + params_remove(self.p, self.check_key(key)) + + def get_param_path(self, key=""): + key = ensure_bytes(key) + return _copy_string(params_get_path(self.p, key, len(key))).decode() + + def get_type(self, key): + return ParamKeyType(params_get_key_type(self.p, self.check_key(key))) + + def all_keys(self): + keys = [] + for i in range(params_keys_size(self.p)): + keys.append(_copy_string(params_key_at(self.p, i))) + return keys + + def get_default_value(self, key): + k = self.check_key(key) + return self._cpp2python(self.get_type(k), self._default(k), None, key) + + def cpp2python(self, key, value): + return self._cpp2python(self.get_type(key), value, None, key) + if __name__ == "__main__": import sys params = Params() key = sys.argv[1] - assert params.check_key(key), f"unknown param: {key}" - + params.check_key(key) if len(sys.argv) == 3: val = sys.argv[2] print(f"SET: {key} = {val}") diff --git a/openpilot/common/params_c.cc b/openpilot/common/params_c.cc new file mode 100644 index 0000000000..ddc9d893a9 --- /dev/null +++ b/openpilot/common/params_c.cc @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#include + +#include "common/params.h" + +typedef struct { + const char *data; + size_t size; +} ParamsBuffer; + +struct ParamsHandle { + ParamsHandle(const char *path, size_t path_size) : params(std::string(path, path_size)), keys(params.allKeys()) { + } + + Params params; + const std::vector keys; +}; + +namespace { +thread_local char last_error[512] = {}; +thread_local std::string result; + +void set_error(const char *error) { + snprintf(last_error, sizeof(last_error), "%s", error); +} + +ParamsBuffer return_string(std::string value) { + result = std::move(value); + return {result.data(), result.size()}; +} + +template +Result translate_exceptions(Result failure, Callable &&callable) noexcept { + last_error[0] = '\0'; + try { + return callable(); + } catch (const std::exception &e) { + set_error(e.what()); + } catch (...) { + set_error("unknown C++ exception"); + } + return failure; +} + +template +void translate_exceptions(Callable &&callable) noexcept { + translate_exceptions(false, [&]() { + callable(); + return true; + }); +} +} // namespace + +extern "C" { + +ParamsHandle *params_create(const char *path, size_t path_size) noexcept { + return translate_exceptions(static_cast(nullptr), [&]() { + return new ParamsHandle(path, path_size); + }); +} + +void params_destroy(ParamsHandle *handle) noexcept { + translate_exceptions([&]() { + delete handle; + }); +} + +const char *params_last_error() noexcept { + return last_error; +} + +void params_clear_all(ParamsHandle *handle, unsigned int flag) noexcept { + translate_exceptions([&]() { + handle->params.clearAll(static_cast(flag)); + }); +} + +bool params_check_key(ParamsHandle *handle, const char *key) noexcept { + return translate_exceptions(false, [&]() { + return handle->params.checkKey(key); + }); +} + +int params_get_key_type(ParamsHandle *handle, const char *key) noexcept { + return translate_exceptions(-1, [&]() { + return static_cast(handle->params.getKeyType(key)); + }); +} + +ParamsBuffer params_get_default(ParamsHandle *handle, const char *key) noexcept { + return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() { + auto value = handle->params.getKeyDefaultValue(key); + if (!value.has_value()) { + return ParamsBuffer{nullptr, 0}; + } + return return_string(*value); + }); +} + +ParamsBuffer params_get(ParamsHandle *handle, const char *key, bool block) noexcept { + return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() { + return return_string(handle->params.get(key, block)); + }); +} + +bool params_get_bool(ParamsHandle *handle, const char *key, bool block) noexcept { + return translate_exceptions(false, [&]() { + return handle->params.getBool(key, block); + }); +} + +int params_put(ParamsHandle *handle, const char *key, const char *value, size_t size, bool block) noexcept { + return translate_exceptions(-1, [&]() { + if (block) { + return handle->params.put(key, value, size); + } + handle->params.putNonBlocking(key, std::string(value, size)); + return 0; + }); +} + +int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool block) noexcept { + return translate_exceptions(-1, [&]() { + if (block) { + return handle->params.putBool(key, value); + } + handle->params.putBoolNonBlocking(key, value); + return 0; + }); +} + +int params_remove(ParamsHandle *handle, const char *key) noexcept { + return translate_exceptions(-1, [&]() { + return handle->params.remove(key); + }); +} + +ParamsBuffer params_get_path(ParamsHandle *handle, const char *key, size_t key_size) noexcept { + return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() { + return return_string(handle->params.getParamPath(std::string(key, key_size))); + }); +} + +size_t params_keys_size(ParamsHandle *handle) noexcept { + return translate_exceptions(size_t{0}, [&]() { + return handle->keys.size(); + }); +} + +ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept { + return translate_exceptions(ParamsBuffer{nullptr, 0}, [&]() { + if (index >= handle->keys.size()) { + return ParamsBuffer{nullptr, 0}; + } + return return_string(handle->keys[index]); + }); +} + +} // extern "C" diff --git a/openpilot/common/params_pyx.pyx b/openpilot/common/params_pyx.pyx deleted file mode 100644 index c919c85f0c..0000000000 --- a/openpilot/common/params_pyx.pyx +++ /dev/null @@ -1,191 +0,0 @@ -# distutils: language = c++ -# cython: language_level = 3 -import builtins -import datetime -import json -from libcpp cimport bool -from libcpp.string cimport string -from libcpp.vector cimport vector -from libcpp.optional cimport optional - -from openpilot.common.swaglog import cloudlog - -cdef extern from "common/params.h": - cpdef enum ParamKeyFlag: - PERSISTENT - CLEAR_ON_MANAGER_START - CLEAR_ON_ONROAD_TRANSITION - CLEAR_ON_OFFROAD_TRANSITION - DEVELOPMENT_ONLY - CLEAR_ON_IGNITION_ON - ALL - - cpdef enum ParamKeyType: - STRING - BOOL - INT - FLOAT - TIME - JSON - BYTES - - cdef cppclass c_Params "Params": - c_Params(string) except + nogil - string get(string, bool) nogil - bool getBool(string, bool) nogil - int remove(string) nogil - int put(string, string) nogil - void putNonBlocking(string, string) nogil - void putBoolNonBlocking(string, bool) nogil - int putBool(string, bool) nogil - bool checkKey(string) nogil - ParamKeyType getKeyType(string) nogil - optional[string] getKeyDefaultValue(string) nogil - string getParamPath(string) nogil - void clearAll(ParamKeyFlag) - vector[string] allKeys() - -PYTHON_2_CPP = { - (str, STRING): lambda v: v, - (builtins.bool, BOOL): lambda v: "1" if v else "0", - (int, INT): str, - (float, FLOAT): str, - (datetime.datetime, TIME): lambda v: v.isoformat(), - (dict, JSON): json.dumps, - (list, JSON): json.dumps, - (bytes, BYTES): lambda v: v, -} -CPP_2_PYTHON = { - STRING: lambda v: v.decode("utf-8"), - BOOL: lambda v: v == b"1", - INT: int, - FLOAT: float, - TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")), - JSON: json.loads, - BYTES: lambda v: v, -} - -def ensure_bytes(v): - return v.encode() if isinstance(v, str) else v - -class UnknownKeyName(Exception): - pass - -cdef class Params: - cdef c_Params* p - cdef str d - - def __cinit__(self, d=""): - cdef string path = d.encode() - with nogil: - self.p = new c_Params(path) - self.d = d - - def __reduce__(self): - return (type(self), (self.d,)) - - def __dealloc__(self): - del self.p - - def clear_all(self, tx_flag=ParamKeyFlag.ALL): - self.p.clearAll(tx_flag) - - def check_key(self, key): - key = ensure_bytes(key) - if not self.p.checkKey(key): - raise UnknownKeyName(key) - return key - - def python2cpp(self, proposed_type, expected_type, value, key): - cast = PYTHON_2_CPP.get((proposed_type, expected_type)) - if cast: - return cast(value) - raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}") - - def _cpp2python(self, t, value, default, key): - if value is None: - return None - try: - return CPP_2_PYTHON[t](value) - except (KeyError, TypeError, ValueError): - cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") - return self._cpp2python(t, default, None, key) - - def get(self, key, bool block=False, bool return_default=False): - cdef string k = self.check_key(key) - cdef ParamKeyType t = self.p.getKeyType(k) - cdef optional[string] default = self.p.getKeyDefaultValue(k) - cdef string val - with nogil: - val = self.p.get(k, block) - - default_val = (default.value() if default.has_value() else None) if return_default else None - if val == b"": - if block: - # If we got no value while running in blocked mode - # it means we got an interrupt while waiting - raise KeyboardInterrupt - else: - return self._cpp2python(t, default_val, None, key) - return self._cpp2python(t, val, default_val, key) - - def get_bool(self, key, bool block=False): - cdef string k = self.check_key(key) - cdef bool r - with nogil: - r = self.p.getBool(k, block) - return r - - def _put_cast(self, key, dat): - cdef string k = self.check_key(key) - cdef ParamKeyType t = self.p.getKeyType(k) - return ensure_bytes(self.python2cpp(type(dat), t, dat, key)) - - def put(self, key, dat, bool block = False): - """ - Warning: block=True blocks until the param is written to disk! - In very rare cases this can take over a second, and your code will hang. - Use block=False in time sensitive code, but in general try to avoid - writing params as much as possible. - """ - cdef string k = self.check_key(key) - cdef string dat_bytes = self._put_cast(key, dat) - with nogil: - if block: - self.p.put(k, dat_bytes) - else: - self.p.putNonBlocking(k, dat_bytes) - - def put_bool(self, key, bool val, bool block = False): - cdef string k = self.check_key(key) - with nogil: - if block: - self.p.putBool(k, val) - else: - self.p.putBoolNonBlocking(k, val) - - def remove(self, key): - cdef string k = self.check_key(key) - with nogil: - self.p.remove(k) - - def get_param_path(self, key=""): - cdef string key_bytes = ensure_bytes(key) - return self.p.getParamPath(key_bytes).decode("utf-8") - - def get_type(self, key): - return self.p.getKeyType(self.check_key(key)) - - def all_keys(self): - return self.p.allKeys() - - def get_default_value(self, key): - cdef string k = self.check_key(key) - cdef ParamKeyType t = self.p.getKeyType(k) - cdef optional[string] default = self.p.getKeyDefaultValue(k) - return self._cpp2python(t, default.value(), None, key) if default.has_value() else None - - def cpp2python(self, key, value): - cdef string k = self.check_key(key) - cdef ParamKeyType t = self.p.getKeyType(k) - return self._cpp2python(t, value, None, key) diff --git a/openpilot/common/tests/test_params.py b/openpilot/common/tests/test_params.py index 4fcc5c7f58..a81d346b06 100644 --- a/openpilot/common/tests/test_params.py +++ b/openpilot/common/tests/test_params.py @@ -62,6 +62,11 @@ class TestParams(OpenpilotTestCase): with self.assertRaises(UnknownKeyName): self.params.put_bool("swag", True, block=True) + with self.assertRaises(UnknownKeyName): + self.params.put(b"DongleId\0suffix", "abc", block=True) + + assert self.params.get_param_path(b"key\0suffix").endswith("/key\0suffix") + def test_remove_not_there(self): assert self.params.get("CarParams") is None self.params.remove("CarParams") diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index 3dd7ad9a8a..dcb1dc2fd0 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -1,6 +1,6 @@ import math import pyray as rl -from typing import Union +from typing import TYPE_CHECKING, Union from enum import Enum from collections.abc import Callable from openpilot.system.ui.widgets import Widget @@ -9,10 +9,13 @@ from openpilot.system.ui.widgets.scroller import DO_ZOOM from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.common.filter_simple import BounceFilter -try: +if TYPE_CHECKING: from openpilot.common.params import Params -except ImportError: - Params = None +else: + try: + from openpilot.common.params import Params + except (ImportError, OSError): + Params = None SCROLLING_SPEED_PX_S = 50 COMPLICATION_SIZE = 36 diff --git a/openpilot/system/manager/process.py b/openpilot/system/manager/process.py index 7f7eabd7be..418e16fa8f 100644 --- a/openpilot/system/manager/process.py +++ b/openpilot/system/manager/process.py @@ -228,7 +228,7 @@ class DaemonProcess(ManagerProcess): pass -def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None, CP: car.CarParams=None, +def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params: Params, CP: car.CarParams, not_run: list[str] | None=None) -> list[ManagerProcess]: if not_run is None: not_run = [] diff --git a/openpilot/system/ui/lib/multilang.py b/openpilot/system/ui/lib/multilang.py index 2305404679..3d786a7248 100644 --- a/openpilot/system/ui/lib/multilang.py +++ b/openpilot/system/ui/lib/multilang.py @@ -2,13 +2,17 @@ from importlib.resources import files import json import os import re +from typing import TYPE_CHECKING from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog -try: +if TYPE_CHECKING: from openpilot.common.params import Params -except ImportError: - Params = None +else: + try: + from openpilot.common.params import Params + except (ImportError, OSError): + Params = None SYSTEM_UI_DIR = os.path.join(BASEDIR, "openpilot/system", "ui") UI_DIR = files("openpilot.selfdrive.ui") diff --git a/openpilot/system/ui/lib/wifi_manager.py b/openpilot/system/ui/lib/wifi_manager.py index c52e5a2ddf..edf91ae2ea 100644 --- a/openpilot/system/ui/lib/wifi_manager.py +++ b/openpilot/system/ui/lib/wifi_manager.py @@ -6,7 +6,7 @@ import subprocess from collections.abc import Callable from dataclasses import dataclass, replace from enum import IntEnum -from typing import Any +from typing import TYPE_CHECKING, Any from jeepney import DBusAddress, new_method_call from jeepney.bus_messages import MatchRule, message_bus @@ -26,10 +26,13 @@ from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_80 NM_DEVICE_TYPE_WIFI, NM_ACTIVE_CONNECTION_IFACE, NM_IP4_CONFIG_IFACE, NM_PROPERTIES_IFACE, NMDeviceState, NMDeviceStateReason) -try: +if TYPE_CHECKING: from openpilot.common.params import Params -except Exception: - Params = None +else: + try: + from openpilot.common.params import Params + except (ImportError, OSError): + Params = None TETHERING_IP_ADDRESS = "192.168.43.1" DEFAULT_TETHERING_PASSWORD = "swagswagcomma" From 2a1c44c904862f8d65c0f1605037754903d38381 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 22 Jul 2026 13:43:23 -0700 Subject: [PATCH 050/325] rm scons cython tool (#38417) --- SConstruct | 2 +- site_scons/site_tools/cython.py | 75 --------------------------------- 2 files changed, 1 insertion(+), 76 deletions(-) delete mode 100644 site_scons/site_tools/cython.py diff --git a/SConstruct b/SConstruct index 7b28ff39fb..630cfe1c0d 100644 --- a/SConstruct +++ b/SConstruct @@ -165,7 +165,7 @@ env = Environment( COMPILATIONDB_USE_ABSPATH=True, REDNOSE_ROOT="#rednose_repo", tools=["default", "cython", "compilation_db", "rednose_filter"], - toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], + toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], ) # SCons' Darwin linker tool doesn't define the variables used to expand RPATH. if arch == "Darwin": diff --git a/site_scons/site_tools/cython.py b/site_scons/site_tools/cython.py deleted file mode 100644 index f11db1d71b..0000000000 --- a/site_scons/site_tools/cython.py +++ /dev/null @@ -1,75 +0,0 @@ -import re -import SCons -from SCons.Action import Action -from SCons.Scanner import Scanner -import numpy as np - -pyx_from_import_re = re.compile(r'^from\s+(\S+)\s+cimport', re.M) -pyx_import_re = re.compile(r'^cimport\s+(\S+)', re.M) -cdef_import_re = re.compile(r'^cdef extern from\s+.(\S+).:', re.M) - -np_version = SCons.Script.Value(np.__version__) - -def pyx_scan(node, env, path, arg=None): - contents = node.get_text_contents() - env.Depends(str(node).split('.')[0] + env['CYTHONCFILESUFFIX'], np_version) - - # from cimport ... - matches = pyx_from_import_re.findall(contents) - # cimport - matches += pyx_import_re.findall(contents) - - # Modules can be either .pxd or .pyx files - files = [m.replace('.', '/') + '.pxd' for m in matches] - files += [m.replace('.', '/') + '.pyx' for m in matches] - - # cdef extern from - files += cdef_import_re.findall(contents) - - # Handle relative imports - cur_dir = str(node.get_dir()) - files = [cur_dir + f if f.startswith('/') else f for f in files] - - # Filter out non-existing files (probably system imports) - files = [f for f in files if env.File(f).exists()] - return env.File(files) - - -pyxscanner = Scanner(function=pyx_scan, skeys=['.pyx', '.pxd'], recursive=True) -cythonAction = Action("$CYTHONCOM") - - -def create_builder(env): - try: - cython = env['BUILDERS']['Cython'] - except KeyError: - cython = SCons.Builder.Builder( - action=cythonAction, - emitter={}, - suffix=cython_suffix_emitter, - single_source=1 - ) - env.Append(SCANNERS=pyxscanner) - env['BUILDERS']['Cython'] = cython - return cython - -def cython_suffix_emitter(env, source): - return "$CYTHONCFILESUFFIX" - -def generate(env): - env["CYTHON"] = "cythonize" - env["CYTHONCOM"] = "$CYTHON $CYTHONFLAGS $SOURCE" - env["CYTHONCFILESUFFIX"] = ".cpp" - - c_file, _ = SCons.Tool.createCFileBuilders(env) - - c_file.suffix['.pyx'] = cython_suffix_emitter - c_file.add_action('.pyx', cythonAction) - - c_file.suffix['.py'] = cython_suffix_emitter - c_file.add_action('.py', cythonAction) - - create_builder(env) - -def exists(env): - return True From 736f3b1a00c23d286465eb14976c25068c82d025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 22 Jul 2026 14:53:56 -0700 Subject: [PATCH 051/325] longitudinal: move cruise speed limit out of the MPC (#38367) * --- .../lib/longitudinal_mpc_lib/long_mpc.py | 17 +-- .../controls/lib/longitudinal_planner.py | 108 +++++++++--------- .../test/longitudinal_maneuvers/maneuver.py | 10 +- .../test/longitudinal_maneuvers/plant.py | 2 +- .../test_longitudinal.py | 6 +- 5 files changed, 70 insertions(+), 73 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index de1ba0c278..725491eed2 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -23,7 +23,7 @@ EXPORT_DIR = os.path.join(LONG_MPC_DIR, "c_generated_code") JSON_FILE = os.path.join(LONG_MPC_DIR, "acados_ocp_long.json") LongitudinalPlanSource = log.LongitudinalPlan.LongitudinalPlanSource -MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1, LongitudinalPlanSource.cruise) +MPC_SOURCES = (LongitudinalPlanSource.lead0, LongitudinalPlanSource.lead1) X_DIM = 3 U_DIM = 1 @@ -55,8 +55,6 @@ FCW_IDXS = T_IDXS < 5.0 T_DIFFS = np.diff(T_IDXS, prepend=[0.]) COMFORT_BRAKE = 2.5 STOP_DISTANCE = 6.0 -CRUISE_MIN_ACCEL = -1.2 -CRUISE_MAX_ACCEL = 1.6 MIN_X_LEAD_FACTOR = 0.5 def get_jerk_factor(personality=log.LongitudinalPersonality.standard): @@ -309,9 +307,8 @@ class LongitudinalMpc: lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau) return lead_xv - def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard): + def update(self, radarstate, personality=log.LongitudinalPersonality.standard): t_follow = get_T_FOLLOW(personality) - v_ego = self.x0[1] lead_xv_0 = self.process_lead(radarstate.leadOne) lead_xv_1 = self.process_lead(radarstate.leadTwo) @@ -322,15 +319,7 @@ class LongitudinalMpc: lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) - # Fake an obstacle for cruise, this ensures smooth acceleration to set speed - # when the leads are no factor. - v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05) - # TODO does this make sense when max_a is negative? - v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05) - v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper) - cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow) - - x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle]) + x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle]) self.source = MPC_SOURCES[np.argmin(x_obstacles[0])] self.yref[:,:] = 0.0 diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 116de14e0e..0b1fee329d 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -17,6 +17,8 @@ from openpilot.common.swaglog import cloudlog A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] +J_CRUISE_VALS = [1.6, 1.2, 0.8, 0.6] +A_CRUISE_MIN = -1.2 CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] ALLOW_THROTTLE_THRESHOLD = 0.4 MIN_ALLOW_THROTTLE_SPEED = 2.5 @@ -31,18 +33,26 @@ def get_max_accel(v_ego): def get_coast_accel(pitch): return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py -def limit_accel_in_turns(v_ego, angle_steers, a_target, CP): - """ - This function returns a limited long acceleration allowed, depending on the existing lateral acceleration - this should avoid accelerating when losing the target in turns - """ - # FIXME: This function to calculate lateral accel is incorrect and should use the VehicleModel - # The lookup table for turns should also be updated if we do this - a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V) - a_y = v_ego ** 2 * angle_steers * CV.DEG_TO_RAD / (CP.steerRatio * CP.wheelbase) - a_x_allowed = math.sqrt(max(a_total_max ** 2 - a_y ** 2, 0.)) +def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, accel_coast, allow_throttle): + max_accel = ACCEL_MAX if e2e else get_max_accel(v_ego) - return [a_target[0], min(a_target[1], a_x_allowed)] + if not e2e: + a_total_max = np.interp(v_ego, _A_TOTAL_MAX_BP, _A_TOTAL_MAX_V) + a_y = v_ego ** 2 * angle_steers * CV.DEG_TO_RAD / (CP.steerRatio * CP.wheelbase) + a_x_allowed = math.sqrt(max(a_total_max ** 2 - a_y ** 2, 0.)) + max_accel = min(max_accel, a_x_allowed) + if not allow_throttle: + clipped_accel_coast = max(accel_coast, ACCEL_MIN) + coast_limit = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [max_accel, clipped_accel_coast]) + max_accel = min(max_accel, coast_limit) + + target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel) + if not e2e: + j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS) + target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt)) + + cruise_should_stop = v_cruise == 0.0 + return target_accel, cruise_should_stop class LongitudinalPlanner: @@ -55,7 +65,7 @@ class LongitudinalPlanner: self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) - self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX] + self.a_cruise = 0.0 self.output_a_target = 0.0 self.output_should_stop = False @@ -72,46 +82,36 @@ class LongitudinalPlanner: v_ego = sm['carState'].vEgo v_cruise_kph = min(sm['carState'].vCruise, V_CRUISE_MAX) v_cruise = v_cruise_kph * CV.KPH_TO_MS - v_cruise_initialized = sm['carState'].vCruise != V_CRUISE_UNSET + if sm['controlsState'].forceDecel: + v_cruise = 0.0 long_control_off = sm['controlsState'].longControlState == LongCtrlState.off - force_slow_decel = sm['controlsState'].forceDecel # Reset current state when not engaged, or user is controlling the speed reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['selfdriveState'].enabled # PCM cruise speed may be updated a few cycles later, check if initialized + v_cruise_initialized = sm['carState'].vCruise != V_CRUISE_UNSET reset_state = reset_state or not v_cruise_initialized + throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs + throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0 + self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED + + steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg + + if reset_state: + self.v_desired_filter.x = v_ego + self.a_desired = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX) + + # Prevent divergence, smooth in current v_ego + self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) + # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] - steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg - accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) - - if reset_state: - self.v_desired_filter.x = v_ego - # Clip aEgo to cruise limits to prevent large accelerations when becoming active - self.a_desired = np.clip(sm['carState'].aEgo, accel_clip[0], accel_clip[1]) - - # Prevent divergence, smooth in current v_ego - self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) - throttle_probs = sm['modelV2'].meta.disengagePredictions.gasPressProbs - throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0 - # Don't clip at low speeds since throttle_prob doesn't account for creep - self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED - - if not self.allow_throttle: - clipped_accel_coast = max(accel_coast, accel_clip[0]) - clipped_accel_coast_interp = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [accel_clip[1], clipped_accel_coast]) - accel_clip[1] = min(accel_clip[1], clipped_accel_coast_interp) - - if force_slow_decel: - v_cruise = 0.0 - self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) - self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality) + self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution) @@ -122,10 +122,8 @@ class LongitudinalPlanner: if self.fcw: cloudlog.info("FCW triggered") - # Interpolate 0.05 seconds and save as starting point for next iteration + # Save starting point for next iteration a_prev = self.a_desired - self.a_desired = float(np.interp(self.dt, CONTROL_N_T_IDX, self.a_desired_trajectory)) - self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0 action_t = self.CP.longitudinalActuatorDelay + DT_MDL output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, @@ -133,19 +131,21 @@ class LongitudinalPlanner: output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if sm['selfdriveState'].experimentalMode: - output_a_target = min(output_a_target_e2e, output_a_target_mpc) - self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc - if output_a_target < output_a_target_mpc: - self.mpc.source = LongitudinalPlanSource.e2e - else: - output_a_target = output_a_target_mpc - self.output_should_stop = output_should_stop_mpc + self.a_cruise, cruise_should_stop = get_cruise_accel(sm['selfdriveState'].experimentalMode, v_cruise, v_ego, + self.a_cruise, steer_angle_without_offset, self.CP, self.dt, + accel_coast, self.allow_throttle) - for idx in range(2): - accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) - self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1]) - self.prev_accel_clip = accel_clip + candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc), + (self.a_cruise, LongitudinalPlanSource.cruise, cruise_should_stop)] + if sm['selfdriveState'].experimentalMode: + candidates.append((output_a_target_e2e, LongitudinalPlanSource.e2e, output_should_stop_e2e)) + + output_a_target, self.mpc.source, _ = min(candidates, key=lambda c: c[0]) + self.output_should_stop = any(should_stop for _, _, should_stop in candidates) + self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX) + + self.a_desired = float(self.output_a_target) + self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0 def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') diff --git a/openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py b/openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py index ba0379f2d7..99a7d8d690 100644 --- a/openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/maneuver.py @@ -43,6 +43,7 @@ class Maneuver: valid = True logs = [] + not_starting_t = 0.0 while plant.current_time < self.duration: speed_lead = np.interp(plant.current_time, self.breakpoints, self.speed_lead_values) prob_lead = np.interp(plant.current_time, self.breakpoints, self.prob_lead_values) @@ -68,8 +69,13 @@ class Maneuver: valid = False if self.ensure_start and log['v_rel'] > 0 and log['acceleration'] < 1e-3: - print('LongitudinalPlanner not starting!') - valid = False + if not_starting_t == 0.0: + not_starting_t = plant.current_time + elif plant.current_time - not_starting_t > 0.5: + print('LongitudinalPlanner not starting!') + valid = False + else: + not_starting_t = 0.0 if self.ensure_slowdown and log['speed'] > 5.5: print('LongitudinalPlanner not slowing down!') diff --git a/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py index 23ccbdb85d..bdd5c51ee4 100755 --- a/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py @@ -107,7 +107,7 @@ class Plant: position = log.XYZTData.new_message() position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)] model.modelV2.position = position - model.modelV2.action.desiredAcceleration = float(self.acceleration + 0.1) + model.modelV2.action.desiredAcceleration = float(self.acceleration + 0.5) velocity = log.XYZTData.new_message() velocity.x = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)] velocity.x[0] = float(self.speed) # always start at current speed diff --git a/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py index 9bd22902d3..c8694b6ac2 100644 --- a/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py @@ -151,7 +151,9 @@ def create_maneuvers(kwargs): enabled=False, **kwargs, ), - Maneuver( + ] + if not kwargs['e2e']: + maneuvers.append(Maneuver( "slow to 5m/s with allow_throttle = False and pitch = +0.1", duration=30., initial_speed=20., @@ -162,7 +164,7 @@ def create_maneuvers(kwargs): breakpoints=[0.0, 2., 2.01], ensure_slowdown=True, **kwargs, - )] + )) if not kwargs['force_decel']: # controls relies on planner commanding to move for stock-ACC resume spamming maneuvers.append(Maneuver( From 1791057369d85385420379ef026fc7ec21d285fd Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 22 Jul 2026 15:13:46 -0700 Subject: [PATCH 052/325] add big model fallback (#38303) * big model fallback * reorder --- openpilot/cereal/log.capnp | 1 + openpilot/common/params_keys.h | 2 + openpilot/selfdrive/modeld/modeld.py | 51 ++++++++++++++++++-- openpilot/selfdrive/selfdrived/events.py | 4 ++ openpilot/selfdrive/selfdrived/selfdrived.py | 25 +++++++++- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 9f0baf3594..00fccd43a6 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -128,6 +128,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { aeb @92; userBookmark @95; excessiveActuation @96; + bigModelLoading @100; lowBatteryDEPRECATED @40; soundsUnavailableDEPRECATED @47; diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 4565f3d5b2..c0c5bebb2c 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -126,5 +126,7 @@ inline static std::unordered_map keys = { {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, + {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index cd8740b462..9fc72eb74b 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -2,6 +2,7 @@ import os os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom from tinygrad.tensor import Tensor +import threading import time import numpy as np import openpilot.cereal.messaging as messaging @@ -33,6 +34,7 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') LAT_SMOOTH_SECONDS = 0.0 LONG_SMOOTH_SECONDS = 0.3 MIN_LAT_CONTROL_SPEED = 0.3 +BIG_MODEL_TIMEOUT = 60 def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, @@ -133,12 +135,24 @@ class ModelState: outputs_dict['raw_pred'] = model_output.copy() return outputs_dict + def warmup(self) -> None: + dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names} + eye = np.eye(3, dtype=np.float32) + dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2} + self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()}) + self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.prev_desire[:] = 0 + self.full_frames.clear() + self._blob_cache.clear() + def main(demo=False): cloudlog.warning("modeld init") USBGPU = usbgpu_present() and usbgpu_compiled() params = Params() + params.put_bool("UsbGpuLoading", USBGPU) + params.put_bool("UsbGpuActive", False) config_realtime_process(7, 54) @@ -165,11 +179,30 @@ def main(demo=False): if use_extra_client: cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") - if USBGPU: - wait_usbgpu_link() st = time.monotonic() cloudlog.warning("loading model") - model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU) + model = None + if USBGPU: + big_model = None + def load_big(): + nonlocal big_model + try: + wait_usbgpu_link() + m = ModelState(vipc_client_main.width, vipc_client_main.height, True) + m.warmup() + big_model = m + except Exception: + cloudlog.exception("big model load failed") + loader = threading.Thread(target=load_big, daemon=True) + loader.start() + loader.join(BIG_MODEL_TIMEOUT) + model = big_model + params.put_bool("UsbGpuActive", model is not None) + + small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None + if model is None: + model = small_model + params.put_bool("UsbGpuLoading", False) cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging @@ -282,7 +315,17 @@ def main(demo=False): } mt1 = time.perf_counter() - model_output = model.run(bufs, transforms, inputs) + try: + model_output = model.run(bufs, transforms, inputs) + except Exception: + if not params.get_bool("UsbGpuActive"): + raise + # fallback to small model + cloudlog.exception("big model failed, fall back to small") + params.put_bool("UsbGpuActive", False) + model = small_model + run_count = 0 + model_output = None mt2 = time.perf_counter() model_execution_time = mt2 - mt1 diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 220efd9c18..96a316118b 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -409,6 +409,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { "Ensure road ahead is clear"), }, + EventName.bigModelLoading: { + ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), + }, + EventName.lateralManeuver: { ET.WARNING: longitudinal_maneuver_alert, ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"), diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index dd94439b95..04b5de25ea 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -65,6 +65,9 @@ class SelfdriveD: self.calibrated_pose: Pose | None = None self.excessive_actuation_check = ExcessiveActuationCheck() self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None + self.big_model_loading = False + self.big_model_active = False + self.big_model_ready_t = 0. # Setup sockets self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents']) @@ -154,6 +157,22 @@ class SelfdriveD: self.events.add(EventName.joystickDebug) self.startup_event = None + loading = self.params.get_bool("UsbGpuLoading") + if self.big_model_loading and not loading: + self.big_model_ready_t = time.monotonic() + self.big_model_loading = loading + if self.big_model_loading: + self.events.add(EventName.bigModelLoading) + + # soft disable if the big model fails + big_active = self.params.get_bool("UsbGpuActive") + if big_active: + self.big_model_active = True + if self.enabled and self.big_model_active and not big_active: + self.events.add(EventName.modeldLagging) + if not self.enabled: + self.big_model_active = False + if self.sm.recv_frame['lateralManeuverPlan'] > 0: self.events.add(EventName.lateralManeuver) self.startup_event = None @@ -347,7 +366,9 @@ class SelfdriveD: # generic catch-all. ideally, a more specific event should be added above instead has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE)) no_system_errors = (not has_disable_events) or (len(self.events) == num_events) - if not self.sm.all_checks() and no_system_errors: + warmup_sec = 5. + big_model_settling = self.big_model_loading or time.monotonic() < self.big_model_ready_t + warmup_sec + if not self.sm.all_checks() and no_system_errors and not big_model_settling: # the load holds modelV2 and friends back on purpose if not self.sm.all_alive(): self.events.add(EventName.commIssue) elif not self.sm.all_freq_ok(): @@ -366,7 +387,7 @@ class SelfdriveD: else: self.logged_comm_issue = None - if not self.CP.notCar: + if not self.CP.notCar and not big_model_settling: # localization has nothing to work with during the load if not self.sm['livePose'].posenetOK: self.events.add(EventName.posenetInvalid) if not self.sm['livePose'].inputsOK: From 05f42f752bb9b3f86f096c5ef0b560dbd926cfef Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 22 Jul 2026 16:07:43 -0700 Subject: [PATCH 053/325] joystickd: use should_stop (#38421) * param * not really needed now --- openpilot/tools/joystick/joystickd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/tools/joystick/joystickd.py b/openpilot/tools/joystick/joystickd.py index ac2f99f67e..ce8b581848 100755 --- a/openpilot/tools/joystick/joystickd.py +++ b/openpilot/tools/joystick/joystickd.py @@ -9,6 +9,7 @@ from opendbc.car.vehicle_model import VehicleModel from openpilot.common.realtime import DT_CTRL, Ratekeeper from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.controls.lib.drive_helpers import should_stop LongCtrlState = car.CarControl.Actuators.LongControlState MAX_LAT_ACCEL = 3.0 @@ -48,7 +49,7 @@ def joystickd_thread(): if CC.longActive: actuators.accel = 4.0 * float(np.clip(joystick_axes[0], -1, 1)) - actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > 0.1 else LongCtrlState.stopping + actuators.longControlState = LongCtrlState.stopping if should_stop(sm['carState'].vEgo, actuators.accel) else LongCtrlState.pid CC.cruiseControl.resume = actuators.accel > 0.0 if CC.latActive: From 6335db69bd08201478e6857282eb6a96a8938dd0 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 22 Jul 2026 16:23:32 -0700 Subject: [PATCH 054/325] eGPU UI (#38419) --- .../selfdrive/ui/mici/onroad/hud_renderer.py | 16 ++++++++++++++++ openpilot/selfdrive/ui/ui_state.py | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index 27bb815590..0225bc8d57 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -177,8 +177,24 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) + if ui_state.usbgpu and ui_state.usbgpu_compiled: + self._draw_model_source(rect) + self._draw_steering_wheel(rect) + def _draw_model_source(self, rect: rl.Rectangle) -> None: + if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: + return + small_drives = not ui_state.usbgpu_loading and not ui_state.usbgpu_active and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame + big_color = rl.GREEN if ui_state.usbgpu_active else rl.RED if small_drives else rl.GRAY + small_color = rl.GREEN if small_drives else rl.WHITE if ui_state.usbgpu_active else rl.GRAY + big_size = measure_text_cached(self._font_semi_bold, "BIG", FONT_SIZES.max_speed) + small_size = measure_text_cached(self._font_semi_bold, "SM", FONT_SIZES.max_speed) + big_pos = rl.Vector2(rect.x + rect.width - 12 - big_size.x, rect.y + rect.height - 14 - FONT_SIZES.max_speed) + small_pos = rl.Vector2(big_pos.x + (big_size.x - small_size.x) / 2, big_pos.y - FONT_SIZES.max_speed - 2) + rl.draw_text_ex(self._font_semi_bold, "BIG", big_pos, FONT_SIZES.max_speed, 0, big_color) + rl.draw_text_ex(self._font_semi_bold, "SM", small_pos, FONT_SIZES.max_speed, 0, small_color) + def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 7efcf2a7c9..a12df6906d 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -78,6 +78,8 @@ class UIState: self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.usbgpu: bool = False self.usbgpu_compiled: bool = usbgpu_compiled() + self.usbgpu_active: bool = self.params.get_bool("UsbGpuActive") + self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -204,9 +206,12 @@ class UIState: self.is_metric = self.params.get_bool("IsMetric") self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.usbgpu = self.sm["deviceState"].chestnutPresent + # keep usbgpu UI active until offroad transition when gpu disappears + self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started) if not self.usbgpu_compiled: self.usbgpu_compiled = usbgpu_compiled() + self.usbgpu_active = self.params.get_bool("UsbGpuActive") + self.usbgpu_loading = self.params.get_bool("UsbGpuLoading") class Device: From 0bac3c903909f9a33e8bd8c4cee1ff4c1564cbe0 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 22 Jul 2026 16:44:16 -0700 Subject: [PATCH 055/325] big model loaded chime (#38420) play chime when big model is loaded --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/selfdrived/events.py | 4 ++++ openpilot/selfdrive/selfdrived/selfdrived.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 00fccd43a6..342e4a03d4 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -129,6 +129,7 @@ struct OnroadEvent @0xc4fa6047f024e718 { userBookmark @95; excessiveActuation @96; bigModelLoading @100; + bigModelReady @101; lowBatteryDEPRECATED @40; soundsUnavailableDEPRECATED @47; diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 96a316118b..76ffea1111 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -413,6 +413,10 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), }, + EventName.bigModelReady: { + ET.PERMANENT: EngagementAlert(AudibleAlert.complete), + }, + EventName.lateralManeuver: { ET.WARNING: longitudinal_maneuver_alert, ET.PERMANENT: NormalPermanentAlert("Lateral Maneuver Mode"), diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 04b5de25ea..3f84708eeb 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -160,6 +160,8 @@ class SelfdriveD: loading = self.params.get_bool("UsbGpuLoading") if self.big_model_loading and not loading: self.big_model_ready_t = time.monotonic() + if self.params.get_bool("UsbGpuActive"): + self.events.add(EventName.bigModelReady) self.big_model_loading = loading if self.big_model_loading: self.events.add(EventName.bigModelLoading) From 49c3b8fc341a7fb1968bd00ba86a7559feed17bf Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 22 Jul 2026 17:24:38 -0700 Subject: [PATCH 056/325] joystick: add override state (#38423) fix js joverride --- openpilot/tools/joystick/joystickd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpilot/tools/joystick/joystickd.py b/openpilot/tools/joystick/joystickd.py index ce8b581848..2b84683863 100755 --- a/openpilot/tools/joystick/joystickd.py +++ b/openpilot/tools/joystick/joystickd.py @@ -34,6 +34,7 @@ def joystickd_thread(): CC.enabled = sm['selfdriveState'].enabled CC.latActive = sm['selfdriveState'].active and not sm['carState'].steerFaultTemporary and not sm['carState'].steerFaultPermanent CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in sm['onroadEvents']) and CP.openpilotLongitudinalControl + CC.cruiseControl.override = CC.enabled and not CC.longActive and CP.openpilotLongitudinalControl CC.cruiseControl.cancel = sm['carState'].cruiseState.enabled and (not CC.enabled or not CP.pcmCruise) CC.hudControl.leadDistanceBars = 2 From 2895346746634d7eec0ee749f946c87039948a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 22 Jul 2026 18:12:13 -0700 Subject: [PATCH 057/325] Rebel Legion model (#38164) * 3e5b7d32-e114-4517-96b4-b3a2448fd939/400 * 9ffd502c-01d0-40f3-93c2-07a4d3e4445c/400 * 1afc2042-8761-4663-ae6a-d593b13defd0/400 * d4b183da-3b8c-4ff7-8927-e3e7216ddb86/400 * 60a46520-f02f-49c5-8eae-e0f8de92af07/400 * ca63f597-de00-478f-970c-302069a59130/400 * 0322768e-5d7e-46e6-9712-d52de312e1da/400 * d25f38c8-8689-42a7-8326-f0c7ce00625f * f14f0be4-bf1e-4dc3-914d-3d9a23b5a05a/400 * lil smooth * 03dfde49-54e6-4129-b05f-24b0b0c7e5f0/400 * 6d9d6f8a-5c82-41f6-92aa-4c1a11eb5645/400 * ci: fix model review tinygrad import * ci: skip unfetched big model in review --------- Co-authored-by: Toby Penner --- .github/workflows/model_review.yaml | 2 +- openpilot/selfdrive/modeld/modeld.py | 4 ++-- openpilot/selfdrive/modeld/models/driving_supercombo.onnx | 4 ++-- scripts/reporter.py | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml index d491b6397a..bdcc2e8977 100644 --- a/.github/workflows/model_review.yaml +++ b/.github/workflows/model_review.yaml @@ -37,7 +37,7 @@ jobs: run: | echo "content<> $GITHUB_OUTPUT echo "## Model Review" >> $GITHUB_OUTPUT - PYTHONPATH=${{ github.workspace }} MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT + PYTHONPATH=${{ github.workspace }}:${{ github.workspace }}/tinygrad_repo MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Post model report comment diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 9fc72eb74b..3ba4dfa71a 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -31,8 +31,8 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') -LAT_SMOOTH_SECONDS = 0.0 -LONG_SMOOTH_SECONDS = 0.3 +LAT_SMOOTH_SECONDS = 0.1 +LONG_SMOOTH_SECONDS = 0.1 MIN_LAT_CONTROL_SPEED = 0.3 BIG_MODEL_TIMEOUT = 60 diff --git a/openpilot/selfdrive/modeld/models/driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx index f0672eab48..944eebb017 100644 --- a/openpilot/selfdrive/modeld/models/driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b -size 60881999 +oid sha256:15e563da34889318e41321a2559682d417416602de9b6f5093c3be0e6766419d +size 96599486 diff --git a/scripts/reporter.py b/scripts/reporter.py index 5de5521835..175a06fa67 100755 --- a/scripts/reporter.py +++ b/scripts/reporter.py @@ -36,6 +36,8 @@ if __name__ == "__main__": for f in glob.glob(BASEDIR + MODEL_PATH + "/*.onnx"): fn = os.path.basename(f) + if fn == "big_driving_supercombo.onnx": + continue master_path = MASTER_PATH + MODEL_PATH + fn if os.path.exists(master_path): master = get_checkpoint(master_path) From 66300306f40ee7a907d3ae911c47e45f62ab19a8 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:03:25 -0700 Subject: [PATCH 058/325] ui(body): hide body layout if no car fingerprint (#38427) * hide body layout if car params is none * reposition * refactor --- openpilot/selfdrive/ui/mici/layouts/main.py | 3 +++ openpilot/selfdrive/ui/ui_state.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py index 20925ab63c..7b37747ef7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ b/openpilot/selfdrive/ui/mici/layouts/main.py @@ -61,6 +61,9 @@ class MiciMainLayout(Scroller): if not self._onboarding_window.completed: gui_app.push_widget(self._onboarding_window) + # initialize correct onroad layout + self._on_body_changed() + @property def _onroad_layout(self) -> Widget: # For scroll_to diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index a12df6906d..9425133e3e 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -86,7 +86,7 @@ class UIState: self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard self.has_longitudinal_control: bool = False - self.is_body: bool | None = None + self.is_body: bool | None = False self.CP: car.CarParams | None = None self.light_sensor: float = -1.0 From 87fde8a058346fb7d35626fb7fdc37f51f6c54b4 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 08:31:54 -0700 Subject: [PATCH 059/325] pyproject cleanup --- pyproject.toml | 27 +++++++++++++-------------- uv.lock | 2 -- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 678720aa04..70ccee02d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ dependencies = [ # core "scons", "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references - "Cython", "numpy >=2.0", # vendored native dependencies @@ -30,10 +29,6 @@ dependencies = [ "comma-deps-git-lfs", "comma-deps-gcc-arm-none-eabi", - # logging - "pyzmq", - "sentry-sdk", - # athena "PyJWT[crypto]", "websocket_client", @@ -42,16 +37,16 @@ dependencies = [ "inputs", # these should be removed + "pyzmq", + "sentry-sdk", "setproctitle", - - # logreader - "zstandard", + "jeepney", + "pillow", + "zstandard", # this can go once we're on Python 3.14+ # ui "comma-deps-raylib", "qrcode", - "jeepney", - "pillow", ] [project.optional-dependencies] @@ -60,13 +55,17 @@ docs = [ ] testing = [ - "coverage", + "coverage", # line coverage + "ty", # type checking + "ruff", # linting + "codespell", # spellcheck + + # TODO: replace this with our own implementation "hypothesis ==6.47.*", - "ty", + + # TODO: replace these with our own nice simple test runner "pytest", "pytest-xdist", - "ruff", - "codespell", ] dev = [ diff --git a/uv.lock b/uv.lock index de03b28288..8b6c7c2f57 100644 --- a/uv.lock +++ b/uv.lock @@ -699,7 +699,6 @@ dependencies = [ { name = "comma-deps-raylib" }, { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, - { name = "cython" }, { name = "inputs" }, { name = "jeepney" }, { name = "numpy" }, @@ -773,7 +772,6 @@ requires-dist = [ { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, { name = "coverage", marker = "extra == 'testing'" }, - { name = "cython" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, From a8bb6b5aeedc74ad5929a0af52749f5103825a03 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 09:17:18 -0700 Subject: [PATCH 060/325] rm hypothesis (#38428) --- openpilot/common/fuzzy.py | 240 ++++++++++++++++++ .../car/tests/test_car_interfaces.py | 39 +-- openpilot/selfdrive/car/tests/test_models.py | 15 +- openpilot/selfdrive/test/fuzzy_generation.py | 81 ------ .../test/process_replay/test_fuzzy.py | 16 +- pyproject.toml | 3 - uv.lock | 33 --- 7 files changed, 274 insertions(+), 153 deletions(-) create mode 100644 openpilot/common/fuzzy.py delete mode 100644 openpilot/selfdrive/test/fuzzy_generation.py diff --git a/openpilot/common/fuzzy.py b/openpilot/common/fuzzy.py new file mode 100644 index 0000000000..b355115191 --- /dev/null +++ b/openpilot/common/fuzzy.py @@ -0,0 +1,240 @@ +import math +import os +import random +import secrets +import struct +from collections.abc import Callable, Sequence +from functools import wraps +from typing import Any, TypeVar + +import capnp + + +T = TypeVar("T") + +_EDGE_SLOTS = 16 +_MINIMAL_EXAMPLES = 10 +_INTEGER_RANGES = { + "int8": (-2**7, 2**7 - 1), + "int16": (-2**15, 2**15 - 1), + "int32": (-2**31, 2**31 - 1), + "int64": (-2**63, 2**63 - 1), + "uint8": (0, 2**8 - 1), + "uint16": (0, 2**16 - 1), + "uint32": (0, 2**32 - 1), + "uint64": (0, 2**64 - 1), +} + +# One seed is shared by the whole test process. Individual tests derive their seed +# from their unittest ID, so FUZZ_SEED is reproducible under pytest-xdist too. +FUZZ_SEED = int(os.environ.get("FUZZ_SEED", secrets.randbits(64))) + + +class Fuzzy: + """Fast, deterministic data generator with systematic boundary coverage.""" + + def __init__(self, seed: int | str, example_index: int): + self.example_index = example_index + self._random = random.Random(seed) + self._draw_index = 0 + + def _draw(self, edges: Sequence[T], random_value: Callable[[], T]) -> T: + draw_index = self._draw_index + self._draw_index += 1 + + # Preserve the cheap minimal prefix Hypothesis produced, then interleave + # systematic boundaries and random values at every draw site. + if self.example_index < _MINIMAL_EXAMPLES: + return edges[0] + search_example = self.example_index - _MINIMAL_EXAMPLES + if search_example < _EDGE_SLOTS * 2 and search_example % 2 == 0: + return edges[(search_example // 2 + draw_index) % len(edges)] + if self._random.randrange(4) == 0: + return self._random.choice(edges) + return random_value() + + def boolean(self) -> bool: + return self._draw((False, True), lambda: bool(self._random.getrandbits(1))) + + def choice(self, values: Sequence[T]) -> T: + if not values: + raise ValueError("cannot choose from an empty sequence") + return self._draw(values, lambda: self._random.choice(values)) + + def integer(self, min_value: int, max_value: int) -> int: + if min_value > max_value: + raise ValueError(f"{min_value=} must not exceed {max_value=}") + + edges = [ + 0, 1, -1, min_value, max_value, + min_value + 1, max_value - 1, + ] + edges.extend(1 << bit for bit in range(max_value.bit_length())) + edges.extend(-(1 << bit) for bit in range((-min_value).bit_length())) + valid_edges = tuple(dict.fromkeys(v for v in edges if min_value <= v <= max_value)) + return self._draw(valid_edges, lambda: self._random.randint(min_value, max_value)) + + def floating(self, width: int = 64, *, allow_nan: bool = True, allow_infinity: bool = True) -> float: + if width not in (32, 64): + raise ValueError("float width must be 32 or 64") + + if width == 32: + unpack_format = "!f" + finite_edges = ( + 0.0, -0.0, 1.0, -1.0, + struct.unpack(unpack_format, b"\x00\x00\x00\x01")[0], + struct.unpack(unpack_format, b"\x80\x00\x00\x01")[0], + struct.unpack(unpack_format, b"\x7f\x7f\xff\xff")[0], + struct.unpack(unpack_format, b"\xff\x7f\xff\xff")[0], + struct.unpack(unpack_format, b"\x00\x80\x00\x00")[0], + struct.unpack(unpack_format, b"\x80\x80\x00\x00")[0], + ) + else: + unpack_format = "!d" + finite_edges = ( + 0.0, -0.0, 1.0, -1.0, + math.ulp(0.0), -math.ulp(0.0), + float.fromhex("0x1.fffffffffffffp+1023"), -float.fromhex("0x1.fffffffffffffp+1023"), + float.fromhex("0x1p-1022"), -float.fromhex("0x1p-1022"), + ) + + edges = list(finite_edges) + if allow_infinity: + edges.extend((math.inf, -math.inf)) + if allow_nan: + edges.append(math.nan) + + def random_float() -> float: + while True: + value = struct.unpack(unpack_format, self._random.randbytes(width // 8))[0] + if (allow_nan or not math.isnan(value)) and (allow_infinity or not math.isinf(value)): + return value + + return self._draw(tuple(edges), random_float) + + def _length(self, min_length: int, max_length: int | None) -> int: + if min_length < 0: + raise ValueError("minimum length must be non-negative") + if max_length is not None and min_length > max_length: + raise ValueError(f"{min_length=} must not exceed {max_length=}") + if max_length == min_length: + return min_length + + offsets = (0, 1, 2, 4, 8, 16, 32) + edges = tuple(min_length + offset for offset in offsets if max_length is None or min_length + offset <= max_length) + + def random_length() -> int: + # A geometric tail keeps ordinary examples small without placing an + # artificial ceiling on an unbounded list. + length = min_length + while max_length is None or length < max_length: + if self._random.randrange(8) == 0: + break + length += 1 + return length + + return self._draw(edges, random_length) + + def binary(self, min_size: int = 0, max_size: int | None = None) -> bytes: + size = self._length(min_size, max_size) + patterns = ( + bytes(size), + b"\xff" * size, + (b"\xaa\x55" * ((size + 1) // 2))[:size], + bytes(i & 0xff for i in range(size)), + ) + return self._draw(patterns, lambda: self._random.randbytes(size)) + + def text(self, min_size: int = 0, max_size: int | None = None) -> str: + size = self._length(min_size, max_size) + + def scalar() -> str: + value = self._random.randrange(0x110000 - 0x800) + if value >= 0xd800: + value += 0x800 + return chr(value) + + patterns = ( + "", + "a" * size, + "\0" * size, + "\U0010ffff" * size, + ) + valid_patterns = tuple(value for value in patterns if len(value) == size) + return self._draw(valid_patterns, lambda: "".join(scalar() for _ in range(size))) + + def list(self, generate: Callable[[], T], min_size: int = 0, max_size: int | None = None) -> list[T]: + return [generate() for _ in range(self._length(min_size, max_size))] + + +def fuzzy_test(max_examples: int) -> Callable[[Callable[..., None]], Callable[..., None]]: + """Run a unittest method repeatedly with independent, reproducible fuzzy data.""" + max_examples = int(os.environ.get("MAX_EXAMPLES", max_examples)) + assert max_examples >= 1 + + def decorator(fn: Callable[..., None]) -> Callable[..., None]: + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> None: + test_seed = f"{FUZZ_SEED}:{args[0].id()}" + selected_example = os.environ.get("FUZZ_EXAMPLE") + examples = [int(selected_example, 0)] if selected_example is not None else range(max_examples) + + for example_index in examples: + if not 0 <= example_index < max_examples: + raise ValueError(f"FUZZ_EXAMPLE={example_index} is outside [0, {max_examples})") + try: + fn(*args, **kwargs, fuzzy=Fuzzy(f"{test_seed}:{example_index}", example_index)) + except Exception as exc: + exc.add_note(f"reproduce with FUZZ_SEED={FUZZ_SEED} FUZZ_EXAMPLE={example_index}") + raise + + return wrapper + return decorator + + +def capnp_random_dict(fuzzy: Fuzzy, schema: Any, event: str | None = None, *, real_floats: bool = False) -> dict[str, Any]: + """Generate a dictionary accepted by a pycapnp struct constructor.""" + + def native(type_name: str) -> bool | int | float | str | bytes: + if type_name == "bool": + return fuzzy.boolean() + if type_name in _INTEGER_RANGES: + return fuzzy.integer(*_INTEGER_RANGES[type_name]) + if type_name in ("float32", "float64"): + return fuzzy.floating(width=int(type_name[-2:]), allow_nan=not real_floats, allow_infinity=not real_floats) + if type_name == "text": + return fuzzy.text(max_size=1000) + if type_name == "anyPointer": + return fuzzy.text() + if type_name == "data": + return fuzzy.binary(max_size=1000) + raise NotImplementedError(f"invalid Cap'n Proto type: {type_name}") + + def generate_field(field: Any) -> Any: + def rec(field_type: Any, base_type: str) -> Any: + type_name = field_type.which() + if type_name == "struct": + struct_schema = field.schema.elementType if base_type == "list" else field.schema + return capnp_random_dict(fuzzy, struct_schema, real_floats=real_floats) + if type_name == "list": + return fuzzy.list(lambda: rec(field_type.list.elementType, "list")) + if type_name == "enum": + enum_schema = field.schema.elementType if base_type == "list" else field.schema + return fuzzy.choice(tuple(enum_schema.enumerants)) + return native(type_name) + + try: + if hasattr(field.proto, "slot"): + slot_type = field.proto.slot.type + return rec(slot_type, slot_type.which()) + return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats) + except capnp.lib.capnp.KjException: + return capnp_random_dict(fuzzy, field.schema, real_floats=real_floats) + + union_field = event or (fuzzy.choice(tuple(schema.union_fields)) if schema.union_fields else None) + fields = schema.non_union_fields + ((union_field,) if union_field else ()) + return { + field_name: generate_field(schema.fields[field_name]) + for field_name in fields + if not field_name.endswith("DEPRECATED") and field_name != "deprecated" + } diff --git a/openpilot/selfdrive/car/tests/test_car_interfaces.py b/openpilot/selfdrive/car/tests/test_car_interfaces.py index 1990b25981..f006d00722 100644 --- a/openpilot/selfdrive/car/tests/test_car_interfaces.py +++ b/openpilot/selfdrive/car/tests/test_car_interfaces.py @@ -1,36 +1,44 @@ -import os -import hypothesis.strategies as st -from hypothesis import Phase, given, settings from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized +from openpilot.common.fuzzy import capnp_random_dict, fuzzy_test from opendbc.car.structs import car from opendbc.car import DT_CTRL +from opendbc.car.car_helpers import interfaces +from opendbc.car.fingerprints import FW_VERSIONS +from opendbc.car.fw_versions import FW_QUERY_CONFIGS from opendbc.car.structs import CarParams -from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface from opendbc.car.mock.values import CAR as MOCK from opendbc.car.values import PLATFORMS from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl -from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator -MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60')) +ALL_ECUS = tuple(sorted({ecu for ecus in FW_VERSIONS.values() for ecu in ecus} | + {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus})) +ALL_REQUESTS = tuple(sorted({tuple(request.request) for config in FW_QUERY_CONFIGS.values() for request in config.requests})) +DLC_TO_LEN = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64) class TestCarInterfaces(OpenpilotTestCase): - # FIXME: Due to the lists used in carParams, Phase.target is very slow and will cause - # many generated examples to overrun when max_examples > ~20, don't use it @parameterized.expand([(car,) for car in sorted(PLATFORMS)] + [MOCK.MOCK]) - @settings(max_examples=MAX_EXAMPLES, deadline=None, - phases=(Phase.reuse, Phase.generate, Phase.shrink)) - @given(data=st.data()) - def test_car_interfaces(self, car_name, data): - car_interface = get_fuzzy_car_interface(car_name, data.draw) + @fuzzy_test(max_examples=60) + def test_car_interfaces(self, car_name, fuzzy): + fingerprint = dict(fuzzy.list(lambda: (fuzzy.integer(0, 0x800), fuzzy.choice(DLC_TO_LEN)))) + fingerprints = dict.fromkeys(range(7), fingerprint) + + def generate_car_fw(): + ecu, address, sub_address = fuzzy.choice(ALL_ECUS) + return CarParams.CarFw(ecu=ecu, address=address, subAddress=sub_address or 0, request=fuzzy.choice(ALL_REQUESTS)) + + CarInterface = interfaces[car_name] + car_params = CarInterface.get_params(car_name, fingerprints, fuzzy.list(generate_car_fw), + alpha_long=fuzzy.boolean(), is_release=False, docs=False) + car_interface = CarInterface(car_params) car_params = car_interface.CP.as_reader() - cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True) + cc_msg = capnp_random_dict(fuzzy, car.CarControl.schema, real_floats=True) # Run car interface now_nanos = 0 CC = car.CarControl.new_message(**cc_msg) @@ -51,8 +59,7 @@ class TestCarInterfaces(OpenpilotTestCase): now_nanos += DT_CTRL * 1e9 # 10ms # Test controller initialization - # TODO: wait until card refactor is merged to run controller a few times, - # hypothesis also slows down significantly with just one more message draw + # TODO: wait until card refactor is merged to run controller a few times LongControl(car_params) if car_params.steerControlType == CarParams.SteerControlType.angle: LatControlAngle(car_params, car_interface, DT_CTRL) diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index e8d9c6cb42..29d4eabcc0 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -3,8 +3,7 @@ import os import random import unittest from collections import defaultdict, Counter -import hypothesis.strategies as st -from hypothesis import Phase, given, settings +from openpilot.common.fuzzy import fuzzy_test from openpilot.common.parameterized import parameterized_class from openpilot.common.test import OpenpilotTestCase from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs @@ -39,7 +38,6 @@ NUM_JOBS = int(os.environ.get("NUM_JOBS", "1")) JOB_ID = int(os.environ.get("JOB_ID", "0")) INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "") INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0")) -MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "300")) CI = os.environ.get("CI", None) is not None @@ -303,10 +301,8 @@ class TestCarModelBase(OpenpilotTestCase): test_car_controller(CC.as_reader()) # Capturing stdout/stderr here causes elevated memory usage. - @settings(max_examples=MAX_EXAMPLES, deadline=None, - phases=(Phase.reuse, Phase.generate, Phase.shrink)) - @given(data=st.data()) - def test_panda_safety_carstate_fuzzy(self, data): + @fuzzy_test(max_examples=300) + def test_panda_safety_carstate_fuzzy(self, fuzzy): """ For each example, pick a random CAN message on the bus and fuzz its data, checking for panda state mismatches. @@ -316,10 +312,9 @@ class TestCarModelBase(OpenpilotTestCase): self.skipTest("no need to check panda safety for dashcamOnly") valid_addrs = [(addr, bus, size) for bus, addrs in self.fingerprint.items() for addr, size in addrs.items()] - address, bus, size = data.draw(st.sampled_from(valid_addrs)) + address, bus, size = fuzzy.choice(valid_addrs) - msg_strategy = st.binary(min_size=size, max_size=size) - msgs = data.draw(st.lists(msg_strategy, min_size=20)) + msgs = fuzzy.list(lambda: fuzzy.binary(min_size=size, max_size=size), min_size=20) vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar diff --git a/openpilot/selfdrive/test/fuzzy_generation.py b/openpilot/selfdrive/test/fuzzy_generation.py deleted file mode 100644 index c97221ae9e..0000000000 --- a/openpilot/selfdrive/test/fuzzy_generation.py +++ /dev/null @@ -1,81 +0,0 @@ -import capnp -import hypothesis.strategies as st -from typing import Any -from collections.abc import Callable -from functools import cache - -from openpilot.cereal import log - -DrawType = Callable[[st.SearchStrategy], Any] - - -class FuzzyGenerator: - def __init__(self, draw: DrawType, real_floats: bool): - self.draw = draw - self.native_type_map = FuzzyGenerator._get_native_type_map(real_floats) - - def generate_native_type(self, field: str) -> st.SearchStrategy[bool | int | float | str | bytes]: - value_func = self.native_type_map.get(field) - if value_func is not None: - return value_func - else: - raise NotImplementedError(f'Invalid type: {field}') - - def generate_field(self, field: capnp.lib.capnp._StructSchemaField) -> st.SearchStrategy: - def rec(field_type: capnp.lib.capnp._DynamicStructReader) -> st.SearchStrategy: - type_which = field_type.which() - if type_which == 'struct': - return self.generate_struct(field.schema.elementType if base_type == 'list' else field.schema) - elif type_which == 'list': - return st.lists(rec(field_type.list.elementType)) - elif type_which == 'enum': - schema = field.schema.elementType if base_type == 'list' else field.schema - return st.sampled_from(list(schema.enumerants.keys())) - else: - return self.generate_native_type(type_which) - - try: - if hasattr(field.proto, 'slot'): - slot_type = field.proto.slot.type - base_type = slot_type.which() - return rec(slot_type) - else: - return self.generate_struct(field.schema) - except capnp.lib.capnp.KjException: - return self.generate_struct(field.schema) - - def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]: - single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else () - fields_to_generate = [f for f in schema.non_union_fields + single_fill if not f.endswith('DEPRECATED') and f != 'deprecated'] - return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate}) - - @staticmethod - @cache - def _get_native_type_map(real_floats: bool) -> dict[str, st.SearchStrategy]: - return { - 'bool': st.booleans(), - 'int8': st.integers(min_value=-2**7, max_value=2**7-1), - 'int16': st.integers(min_value=-2**15, max_value=2**15-1), - 'int32': st.integers(min_value=-2**31, max_value=2**31-1), - 'int64': st.integers(min_value=-2**63, max_value=2**63-1), - 'uint8': st.integers(min_value=0, max_value=2**8-1), - 'uint16': st.integers(min_value=0, max_value=2**16-1), - 'uint32': st.integers(min_value=0, max_value=2**32-1), - 'uint64': st.integers(min_value=0, max_value=2**64-1), - 'float32': st.floats(width=32, allow_nan=not real_floats, allow_infinity=not real_floats), - 'float64': st.floats(width=64, allow_nan=not real_floats, allow_infinity=not real_floats), - 'text': st.text(max_size=1000), - 'data': st.binary(max_size=1000), - 'anyPointer': st.text(), # Note: No need to define a separate function for anyPointer - } - - @classmethod - def get_random_msg(cls, draw: DrawType, struct: capnp.lib.capnp._StructModule, real_floats: bool = False) -> dict[str, Any]: - fg = cls(draw, real_floats=real_floats) - data: dict[str, Any] = draw(fg.generate_struct(struct.schema)) - return data - - @classmethod - def get_random_event_msg(cls, draw: DrawType, events: list[str], real_floats: bool = False) -> list[dict[str, Any]]: - fg = cls(draw, real_floats=real_floats) - return [draw(fg.generate_struct(log.Event.schema, e)) for e in sorted(events)] diff --git a/openpilot/selfdrive/test/process_replay/test_fuzzy.py b/openpilot/selfdrive/test/process_replay/test_fuzzy.py index 9a5bb41d48..3a87f3a7f8 100644 --- a/openpilot/selfdrive/test/process_replay/test_fuzzy.py +++ b/openpilot/selfdrive/test/process_replay/test_fuzzy.py @@ -1,13 +1,10 @@ import copy -import os -from hypothesis import given, HealthCheck, Phase, settings -import hypothesis.strategies as st from openpilot.common.test import OpenpilotTestCase from openpilot.common.parameterized import parameterized +from openpilot.common.fuzzy import capnp_random_dict, fuzzy_test from openpilot.cereal import log from opendbc.car.toyota.values import CAR as TOYOTA -from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator import openpilot.selfdrive.test.process_replay.process_replay as pr # These processes currently fail because of unrealistic data breaking assumptions @@ -16,17 +13,16 @@ import openpilot.selfdrive.test.process_replay.process_replay as pr NOT_TESTED = ['selfdrived', 'controlsd', 'card', 'plannerd', 'calibrationd', 'dmonitoringd', 'paramsd', 'dmonitoringmodeld', 'modeld'] TEST_CASES = [(cfg.proc_name, copy.deepcopy(cfg)) for cfg in pr.CONFIGS if cfg.proc_name not in NOT_TESTED] -MAX_EXAMPLES = int(os.environ.get("MAX_EXAMPLES", "10")) class TestFuzzProcesses(OpenpilotTestCase): # TODO: make this faster and increase examples @parameterized.expand(TEST_CASES) - @given(st.data()) - @settings(phases=[Phase.generate, Phase.target], max_examples=MAX_EXAMPLES, deadline=1000, - suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large]) - def test_fuzz_process(self, proc_name, cfg, data): - msgs = FuzzyGenerator.get_random_event_msg(data.draw, events=cfg.pubs, real_floats=True) + @fuzzy_test(max_examples=10) + def test_fuzz_process(self, proc_name, cfg, fuzzy): + msgs = [capnp_random_dict(fuzzy, log.Event.schema, event, real_floats=True) for event in sorted(cfg.pubs)] + for i, msg in enumerate(msgs): + msg["logMonoTime"] = i * 1_000_000_000 lr = [log.Event.new_message(**m).as_reader() for m in msgs] cfg.timeout = 5 pr.replay_process(cfg, lr, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2, disable_progress=True) diff --git a/pyproject.toml b/pyproject.toml index 70ccee02d2..4b63b38005 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,6 @@ testing = [ "ruff", # linting "codespell", # spellcheck - # TODO: replace this with our own implementation - "hypothesis ==6.47.*", - # TODO: replace these with our own nice simple test runner "pytest", "pytest-xdist", diff --git a/uv.lock b/uv.lock index 8b6c7c2f57..a98fcc3af5 100644 --- a/uv.lock +++ b/uv.lock @@ -5,15 +5,6 @@ requires-python = ">=3.12.3, <3.13" [manifest] overrides = [{ name = "opendbc", editable = "opendbc_repo" }] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - [[package]] name = "certifi" version = "2026.6.17" @@ -404,19 +395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] -[[package]] -name = "hypothesis" -version = "6.47.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/f2/f77da8271b1abb630cb2090ead2f5aa4acc9639d632e8e68187f52527e4b/hypothesis-6.47.5.tar.gz", hash = "sha256:e0c1e253fc97e7ecdb9e2bbff2cf815d8739e0d1d3d093d67c3af5bb6a7211b0", size = 326641, upload-time = "2022-06-25T20:58:48.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/a7/389bbaade2cbbb2534cb2715986041ed01c6d792152c527e71f7f68e93b5/hypothesis-6.47.5-py3-none-any.whl", hash = "sha256:87049b781ee11ec1c7948565b889ab02e428a1e32d427ab4de8fdb3649242d06", size = 387311, upload-time = "2022-06-25T20:58:45.281Z" }, -] - [[package]] name = "idna" version = "3.18" @@ -735,7 +713,6 @@ submodules = [ testing = [ { name = "codespell" }, { name = "coverage" }, - { name = "hypothesis" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, @@ -772,7 +749,6 @@ requires-dist = [ { name = "comma-deps-zeromq" }, { name = "comma-deps-zstd" }, { name = "coverage", marker = "extra == 'testing'" }, - { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, { name = "matplotlib", marker = "extra == 'dev'" }, @@ -1186,15 +1162,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "sounddevice" version = "0.5.5" From e1c69719c6714b1c1f479dba1e6a4bf5d137ec07 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 09:35:05 -0700 Subject: [PATCH 061/325] bump panda (#38429) --- panda | 2 +- uv.lock | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/panda b/panda index 9da8467a7c..d9ed70b850 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 9da8467a7c3789733ba2afe751e558f6a0ea750c +Subproject commit d9ed70b850513f65e2a69707835f818409fe6052 diff --git a/uv.lock b/uv.lock index a98fcc3af5..2617d11f7a 100644 --- a/uv.lock +++ b/uv.lock @@ -812,7 +812,6 @@ requires-dist = [ { name = "libusb-package" }, { name = "libusb1" }, { name = "opendbc", git = "https://github.com/commaai/opendbc.git?rev=master" }, - { name = "pycryptodome", marker = "extra == 'dev'", specifier = ">=3.9.8" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-mock", marker = "extra == 'dev'" }, { name = "pytest-timeout", marker = "extra == 'dev'" }, From cfa8e5bf030b75c4de5fd5419a1358461960e4c5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 09:42:17 -0700 Subject: [PATCH 062/325] gitignore test_swaglog --- openpilot/common/tests/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/openpilot/common/tests/.gitignore b/openpilot/common/tests/.gitignore index 6cddfc7bdf..74a3bbc977 100644 --- a/openpilot/common/tests/.gitignore +++ b/openpilot/common/tests/.gitignore @@ -1 +1,2 @@ test_common +test_swaglog From 3601b850c2cbf8addc244b019d5f8854b4a4bba9 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 23 Jul 2026 09:57:47 -0700 Subject: [PATCH 063/325] new sounds for 3x (#38404) rm tizi sounds --- openpilot/selfdrive/assets/sounds/disengage_tizi.wav | 3 --- openpilot/selfdrive/assets/sounds/engage_tizi.wav | 3 --- openpilot/selfdrive/ui/soundd.py | 5 ----- 3 files changed, 11 deletions(-) delete mode 100644 openpilot/selfdrive/assets/sounds/disengage_tizi.wav delete mode 100644 openpilot/selfdrive/assets/sounds/engage_tizi.wav diff --git a/openpilot/selfdrive/assets/sounds/disengage_tizi.wav b/openpilot/selfdrive/assets/sounds/disengage_tizi.wav deleted file mode 100644 index f3b5f21a27..0000000000 --- a/openpilot/selfdrive/assets/sounds/disengage_tizi.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f061777d66d8d856a5fcb17378416f959088885ed770181355195ad15de881b -size 68628 diff --git a/openpilot/selfdrive/assets/sounds/engage_tizi.wav b/openpilot/selfdrive/assets/sounds/engage_tizi.wav deleted file mode 100644 index fc24a23c2f..0000000000 --- a/openpilot/selfdrive/assets/sounds/engage_tizi.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b54a85cc09b8ee79fce23d48209205d2e70510eed4c5a86fa400fe3f7caebfd -size 63120 diff --git a/openpilot/selfdrive/ui/soundd.py b/openpilot/selfdrive/ui/soundd.py index 5cbb1298f9..b87585a041 100644 --- a/openpilot/selfdrive/ui/soundd.py +++ b/openpilot/selfdrive/ui/soundd.py @@ -49,11 +49,6 @@ sound_list: dict[int, tuple[str, int | None, float]] = { AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME), AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME), } -if HARDWARE.get_device_type() == "tizi": - sound_list.update({ - AudibleAlert.engage: ("engage_tizi.wav", 1, MAX_VOLUME), - AudibleAlert.disengage: ("disengage_tizi.wav", 1, MAX_VOLUME), - }) def check_selfdrive_timeout_alert(sm): ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] From 6b47a5b6b770e1b532ff8dbc465a69b02971af62 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 10:06:44 -0700 Subject: [PATCH 064/325] tools: decompress in the python downloader (#38430) * tools: decompress in the python downloader * and zstd --- SConstruct | 2 +- openpilot/tools/cabana/SConscript | 2 +- openpilot/tools/jotpluggler/SConscript | 2 +- openpilot/tools/lib/file_downloader.py | 112 ++++++++++++++++++++++-- openpilot/tools/replay/SConscript | 2 +- openpilot/tools/replay/filereader.cc | 15 ++++ openpilot/tools/replay/logreader.cc | 10 --- openpilot/tools/replay/py_downloader.cc | 4 + openpilot/tools/replay/py_downloader.h | 3 + openpilot/tools/replay/util.cc | 88 ------------------- openpilot/tools/replay/util.h | 4 - pyproject.toml | 1 - uv.lock | 12 --- 13 files changed, 133 insertions(+), 124 deletions(-) diff --git a/SConstruct b/SConstruct index 630cfe1c0d..18efb9f8a1 100644 --- a/SConstruct +++ b/SConstruct @@ -55,7 +55,7 @@ assert arch in [ "Darwin", # macOS arm64 (x86 not supported) ] -pkg_names = ['acados', 'bzip2', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] +pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] pkgs = [importlib.import_module(name) for name in pkg_names] acados = pkgs[pkg_names.index('acados')] ffmpeg = pkgs[pkg_names.index('ffmpeg')] diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 805bc41876..387129709e 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -73,7 +73,7 @@ cabana_env = qt_env.Clone() cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR] cabana_env['LIBPATH'] += [libusb.LIB_DIR] -cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['bz2', 'zstd', 'usb-1.0'] + base_libs +cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['usb-1.0'] + base_libs opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../../opendbc_repo/opendbc/dbc").abspath) cabana_env['CXXFLAGS'] += [opendbc_path] diff --git a/openpilot/tools/jotpluggler/SConscript b/openpilot/tools/jotpluggler/SConscript index d5ebaffb98..bfe6e90742 100644 --- a/openpilot/tools/jotpluggler/SConscript +++ b/openpilot/tools/jotpluggler/SConscript @@ -102,7 +102,7 @@ event_extractors = jot_env.Command("generated_event_extractors.h", [ ) libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR}/libimgui.a"), File(f"{imgui.LIB_DIR}/libglfw3.a")] + \ - ffmpeg_libs + ["bz2", "zstd", "m", "pthread", "usb-1.0"] + ffmpeg_libs + ["zstd", "m", "pthread", "usb-1.0"] if arch == "Darwin": jot_env["FRAMEWORKS"] = ["OpenGL", "Cocoa", "IOKit", "CoreFoundation", "CoreVideo", "CoreMedia", "VideoToolbox"] else: diff --git a/openpilot/tools/lib/file_downloader.py b/openpilot/tools/lib/file_downloader.py index 68061b201e..efb06095be 100755 --- a/openpilot/tools/lib/file_downloader.py +++ b/openpilot/tools/lib/file_downloader.py @@ -5,17 +5,21 @@ Called by C++ replay/cabana via subprocess. Subcommands: route-files - Get route file URLs as JSON - download - Download URL to local cache, print local path + download - Download/decompress URL to local cache, print local path + decompress - Decompress a local log file, print temporary path devices - List user's devices as JSON device-routes - List routes for a device as JSON """ import argparse +import bz2 import hashlib import json import os +import shutil import sys import tempfile -import shutil + +import zstandard as zstd from openpilot.common.hardware.hw import Paths from openpilot.tools.lib.api import CommaApi, UnauthorizedError, APIError @@ -39,11 +43,60 @@ def api_call(func): sys.stdout.flush() -def cache_file_path(url): +def cache_file_path(url, compression=None): url_without_query = url.split("?")[0] + if compression: + url_without_query = f"decompressed-{compression}:{url_without_query}" return os.path.join(Paths.download_cache_root(), hashlib.sha256(url_without_query.encode()).hexdigest()) +def compression_type(data): + if data.startswith(b'BZh'): + return 'bz2' + if data.startswith(b'\x28\xb5\x2f\xfd'): + return 'zst' + return None + + +def make_decompressor(compression): + if compression == 'bz2': + return bz2.BZ2Decompressor() + if compression == 'zst': + return zstd.ZstdDecompressor().decompressobj() + raise ValueError(f"Unsupported compression type: {compression}") + + +def decompress_file(source, destination, compression=None): + with open(source, 'rb') as src, open(destination, 'wb') as dst: + header = src.read(4) + compression = compression or compression_type(header) + decompressor = make_decompressor(compression) + dst.write(decompressor.decompress(header)) + while data := src.read(1024 * 1024): + dst.write(decompressor.decompress(data)) + if not decompressor.eof: + raise EOFError(f"Compressed {compression} file ended before the end-of-stream marker") + + +def materialize_cached_file(source, url, compression): + local_path = cache_file_path(url, compression) + if os.path.exists(local_path): + return local_path + + tmp_fd, tmp_path = tempfile.mkstemp(dir=Paths.download_cache_root()) + os.close(tmp_fd) + try: + decompress_file(source, tmp_path, compression) + shutil.move(tmp_path, local_path) + except Exception: + try: + os.unlink(tmp_path) + except OSError: + pass + raise + return local_path + + def cmd_route_files(args): api_call(lambda api: api.get(f"v1/route/{args.route}/files")) @@ -53,8 +106,19 @@ def cmd_download(args): use_cache = not args.no_cache if use_cache: + for compression in ('bz2', 'zst'): + decompressed_path = cache_file_path(url, compression) + if os.path.exists(decompressed_path): + sys.stdout.write(decompressed_path + "\n") + sys.stdout.flush() + return + local_path = cache_file_path(url) if os.path.exists(local_path): + with open(local_path, 'rb') as f: + compression = compression_type(f.read(4)) + if compression: + local_path = materialize_cached_file(local_path, url, compression) sys.stdout.write(local_path + "\n") sys.stdout.flush() return @@ -80,14 +144,30 @@ def cmd_download(args): try: downloaded = 0 chunk_size = 1024 * 1024 + compression = None + decompressor = None with os.fdopen(tmp_fd, 'wb') as f: for data in r.stream(chunk_size): - f.write(data) + if downloaded == 0: + compression = compression_type(data) + if compression: + decompressor = make_decompressor(compression) + f.write(decompressor.decompress(data) if decompressor else data) downloaded += len(data) sys.stderr.write(f"PROGRESS:{downloaded}:{total}\n") sys.stderr.flush() - if use_cache: + if decompressor and not decompressor.eof: + raise EOFError(f"Compressed {compression} file ended before the end-of-stream marker") + + if decompressor: + if use_cache: + output_path = cache_file_path(url, compression) + shutil.move(tmp_path, output_path) + else: + output_path = tmp_path + sys.stdout.write(output_path + "\n") + elif use_cache: shutil.move(tmp_path, local_path) sys.stdout.write(local_path + "\n") else: @@ -109,6 +189,24 @@ def cmd_download(args): sys.stdout.flush() +def cmd_decompress(args): + os.makedirs(Paths.download_cache_root(), exist_ok=True) + output_fd, output_path = tempfile.mkstemp(dir=Paths.download_cache_root()) + os.close(output_fd) + try: + decompress_file(args.path, output_path) + except Exception as e: + try: + os.unlink(output_path) + except OSError: + pass + sys.stderr.write(f"ERROR:{e}\n") + sys.stderr.flush() + sys.exit(1) + sys.stdout.write(output_path + "\n") + sys.stdout.flush() + + def cmd_devices(args): api_call(lambda api: api.get("v1/me/devices/")) @@ -139,6 +237,10 @@ def main(): p_dl.add_argument("--no-cache", action="store_true") p_dl.set_defaults(func=cmd_download) + p_dc = subparsers.add_parser("decompress") + p_dc.add_argument("path") + p_dc.set_defaults(func=cmd_decompress) + p_dev = subparsers.add_parser("devices") p_dev.set_defaults(func=cmd_devices) diff --git a/openpilot/tools/replay/SConscript b/openpilot/tools/replay/SConscript index c5abae502c..bd48ecb4d9 100644 --- a/openpilot/tools/replay/SConscript +++ b/openpilot/tools/replay/SConscript @@ -12,5 +12,5 @@ if arch != "Darwin": replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') -replay_libs = [replay_lib] + ffmpeg_libs + ['bz2', 'zstd', 'ncurses'] + base_libs +replay_libs = [replay_lib] + ffmpeg_libs + ['ncurses'] + base_libs replay_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) diff --git a/openpilot/tools/replay/filereader.cc b/openpilot/tools/replay/filereader.cc index 93a6a1193f..6ad56bbfc6 100644 --- a/openpilot/tools/replay/filereader.cc +++ b/openpilot/tools/replay/filereader.cc @@ -1,5 +1,8 @@ #include "tools/replay/filereader.h" +#include +#include + #include "common/util.h" #include "tools/replay/py_downloader.h" @@ -10,5 +13,17 @@ std::string FileReader::read(const std::string &file, std::atomic *abort) if (local_path.empty()) return {}; return util::read_file(local_path); } + char header[4] = {}; + std::ifstream stream(file, std::ios::binary); + stream.read(header, sizeof(header)); + const std::string magic(header, stream.gcount()); + if (util::ends_with(file, ".bz2") || util::ends_with(file, ".zst") || + util::starts_with(magic, "BZh") || magic == "\x28\xB5\x2F\xFD") { + std::string local_path = PyDownloader::decompress(file, abort); + if (local_path.empty()) return {}; + std::string data = util::read_file(local_path); + unlink(local_path.c_str()); + return data; + } return util::read_file(file); } diff --git a/openpilot/tools/replay/logreader.cc b/openpilot/tools/replay/logreader.cc index 54b69dc168..0416413b4c 100644 --- a/openpilot/tools/replay/logreader.cc +++ b/openpilot/tools/replay/logreader.cc @@ -32,16 +32,6 @@ bool LogReader::load(const std::string &url, std::atomic *abort, bool loca } compressed_size_ = data.size(); download_seconds_ = std::chrono::duration(download_end - download_start).count(); - if (!data.empty()) { - const auto decompress_start = Clock::now(); - if (url.find(".bz2") != std::string::npos || util::starts_with(data, "BZh9")) { - data = decompressBZ2(data, abort); - } else if (url.find(".zst") != std::string::npos || util::starts_with(data, "\x28\xB5\x2F\xFD")) { - data = decompressZST(data, abort); - } - const auto decompress_end = Clock::now(); - decompress_seconds_ = std::chrono::duration(decompress_end - decompress_start).count(); - } decompressed_size_ = data.size(); bool success = !data.empty() && load(data.data(), data.size(), abort, progress); diff --git a/openpilot/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc index d27a77e6ee..a265dfd6a3 100644 --- a/openpilot/tools/replay/py_downloader.cc +++ b/openpilot/tools/replay/py_downloader.cc @@ -152,6 +152,10 @@ std::string download(const std::string &url, bool use_cache, std::atomic * return runPython(args, abort); } +std::string decompress(const std::string &path, std::atomic *abort) { + return runPython({"decompress", path}, abort); +} + std::string getRouteFiles(const std::string &route) { return runPython({"route-files", route}); } diff --git a/openpilot/tools/replay/py_downloader.h b/openpilot/tools/replay/py_downloader.h index 535189784c..80fab6ab00 100644 --- a/openpilot/tools/replay/py_downloader.h +++ b/openpilot/tools/replay/py_downloader.h @@ -12,6 +12,9 @@ namespace PyDownloader { // Downloads url to local cache, returns local file path. Reports progress via installDownloadProgressHandler. std::string download(const std::string &url, bool use_cache = true, std::atomic *abort = nullptr); +// Decompresses a local log file and returns the temporary output path. +std::string decompress(const std::string &path, std::atomic *abort = nullptr); + // Returns JSON string of route files (same format as /v1/route/.../files API) std::string getRouteFiles(const std::string &route); diff --git a/openpilot/tools/replay/util.cc b/openpilot/tools/replay/util.cc index 7b308b5c3d..44e144443a 100644 --- a/openpilot/tools/replay/util.cc +++ b/openpilot/tools/replay/util.cc @@ -1,14 +1,10 @@ #include "tools/replay/util.h" -#include - #include #include #include #include #include -#include - #include "common/timing.h" #include "common/util.h" @@ -58,90 +54,6 @@ std::string getUrlWithoutQuery(const std::string &url) { return (idx == std::string::npos ? url : url.substr(0, idx)); } -std::string decompressBZ2(const std::string &in, std::atomic *abort) { - return decompressBZ2((std::byte *)in.data(), in.size(), abort); -} - -std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort) { - if (in_size == 0) return {}; - - bz_stream strm = {}; - int bzerror = BZ2_bzDecompressInit(&strm, 0, 0); - assert(bzerror == BZ_OK); - - strm.next_in = (char *)in; - strm.avail_in = in_size; - std::string out(in_size * 5, '\0'); - do { - strm.next_out = (char *)(&out[strm.total_out_lo32]); - strm.avail_out = out.size() - strm.total_out_lo32; - - const char *prev_write_pos = strm.next_out; - bzerror = BZ2_bzDecompress(&strm); - if (bzerror == BZ_OK && prev_write_pos == strm.next_out) { - // content is corrupt - bzerror = BZ_STREAM_END; - rWarning("decompressBZ2 error: content is corrupt"); - break; - } - - if (bzerror == BZ_OK && strm.avail_in > 0 && strm.avail_out == 0) { - out.resize(out.size() * 2); - } - } while (bzerror == BZ_OK && !(abort && *abort)); - - BZ2_bzDecompressEnd(&strm); - if (bzerror == BZ_STREAM_END && !(abort && *abort)) { - out.resize(strm.total_out_lo32); - out.shrink_to_fit(); - return out; - } - return {}; -} - -std::string decompressZST(const std::string &in, std::atomic *abort) { - return decompressZST((std::byte *)in.data(), in.size(), abort); -} - -std::string decompressZST(const std::byte *in, size_t in_size, std::atomic *abort) { - ZSTD_DCtx *dctx = ZSTD_createDCtx(); - assert(dctx != nullptr); - - // Initialize input and output buffers - ZSTD_inBuffer input = {in, in_size, 0}; - - // Estimate and reserve memory for decompressed data - size_t estimatedDecompressedSize = ZSTD_getFrameContentSize(in, in_size); - if (estimatedDecompressedSize == ZSTD_CONTENTSIZE_ERROR || estimatedDecompressedSize == ZSTD_CONTENTSIZE_UNKNOWN) { - estimatedDecompressedSize = in_size * 2; // Use a fallback size - } - - std::string decompressedData; - decompressedData.reserve(estimatedDecompressedSize); - - const size_t bufferSize = ZSTD_DStreamOutSize(); // Recommended output buffer size - std::string outputBuffer(bufferSize, '\0'); - - while (input.pos < input.size && !(abort && *abort)) { - ZSTD_outBuffer output = {outputBuffer.data(), bufferSize, 0}; - - size_t result = ZSTD_decompressStream(dctx, &output, &input); - if (ZSTD_isError(result)) { - rWarning("decompressZST error: content is corrupt"); - break; - } - - decompressedData.append(outputBuffer.data(), output.pos); - } - - ZSTD_freeDCtx(dctx); - if (!(abort && *abort)) { - decompressedData.shrink_to_fit(); - return decompressedData; - } - return {}; -} - void precise_nano_sleep(int64_t nanoseconds, std::atomic &interrupt_requested) { struct timespec req, rem; req.tv_sec = nanoseconds / 1000000000; diff --git a/openpilot/tools/replay/util.h b/openpilot/tools/replay/util.h index 03a15787b2..45d6f7cd0b 100644 --- a/openpilot/tools/replay/util.h +++ b/openpilot/tools/replay/util.h @@ -47,10 +47,6 @@ private: }; void precise_nano_sleep(int64_t nanoseconds, std::atomic &interrupt_requested); -std::string decompressBZ2(const std::string &in, std::atomic *abort = nullptr); -std::string decompressBZ2(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); -std::string decompressZST(const std::string &in, std::atomic *abort = nullptr); -std::string decompressZST(const std::byte *in, size_t in_size, std::atomic *abort = nullptr); std::string getUrlWithoutQuery(const std::string &url); std::string formattedDataSize(size_t size); std::string extractFileName(const std::string& file); diff --git a/pyproject.toml b/pyproject.toml index 4b63b38005..c310d7d871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ dev = [ tools = [ "comma-deps-imgui", - "comma-deps-bzip2", "comma-deps-bootstrap-icons", "comma-deps-libusb", "comma-deps-ncurses", diff --git a/uv.lock b/uv.lock index 2617d11f7a..8fe08f2ed4 100644 --- a/uv.lock +++ b/uv.lock @@ -113,16 +113,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/0b/bb713dd4bed94b0b2b21657b7337e264953bd785a2b2f5c1b0706cdcde29/comma_deps_bootstrap_icons-1.10.5.0.post95-py3-none-any.whl", hash = "sha256:d59fc8d3e642e00f83d7a4854164f1dee21c3d17c726d5537b1b72b149f5788b", size = 386001, upload-time = "2026-06-24T23:58:37.84Z" }, ] -[[package]] -name = "comma-deps-bzip2" -version = "1.0.8.post95" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ac/539d76e9bcebbc53d62274a19c47522b118dce3261fc6baea045f93c98b6/comma_deps_bzip2-1.0.8.post95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be466743b7fc56fa18c2c389a46bdeb0fc885cf3cf39b779d748ce540609ebd5", size = 42832, upload-time = "2026-06-24T23:58:39.592Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fe/5206bd6d716022cea259c4878c6a1927ab3555f4caa48a74efaa83d8f418/comma_deps_bzip2-1.0.8.post95-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7c015d720cc5462f87062b4482c54684de6a237b4edb60612c2f58721413682f", size = 38629, upload-time = "2026-06-24T23:58:40.496Z" }, - { url = "https://files.pythonhosted.org/packages/39/0e/78218f9a645ad9d27000153795d6819b3b52ca62d882aa46a413e40887be/comma_deps_bzip2-1.0.8.post95-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3188a197aaf3efbfac975193887c59600b91a3b4eb7f62cc975377133cd48834", size = 36271, upload-time = "2026-06-24T23:58:41.2Z" }, -] - [[package]] name = "comma-deps-capnproto" version = "1.0.1.post93" @@ -720,7 +710,6 @@ testing = [ ] tools = [ { name = "comma-deps-bootstrap-icons" }, - { name = "comma-deps-bzip2" }, { name = "comma-deps-imgui" }, { name = "comma-deps-libusb" }, { name = "comma-deps-ncurses" }, @@ -736,7 +725,6 @@ requires-dist = [ { name = "codespell", marker = "extra == 'testing'" }, { name = "comma-deps-acados" }, { name = "comma-deps-bootstrap-icons", marker = "extra == 'tools'" }, - { name = "comma-deps-bzip2", marker = "extra == 'tools'" }, { name = "comma-deps-capnproto" }, { name = "comma-deps-ffmpeg" }, { name = "comma-deps-gcc-arm-none-eabi" }, From e04fd3c7247c721e76aae2d02884ffa6c1790788 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 10:23:08 -0700 Subject: [PATCH 065/325] rm private reporter comment (#38431) --- .github/workflows/model_review.yaml | 47 ---------------------------- scripts/reporter.py | 48 ----------------------------- 2 files changed, 95 deletions(-) delete mode 100644 .github/workflows/model_review.yaml delete mode 100755 scripts/reporter.py diff --git a/.github/workflows/model_review.yaml b/.github/workflows/model_review.yaml deleted file mode 100644 index bdcc2e8977..0000000000 --- a/.github/workflows/model_review.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: "model review" - -on: - pull_request: - types: [opened, reopened, synchronize] - paths: - - 'openpilot/selfdrive/modeld/models/*.onnx' - workflow_dispatch: - -env: - GIT_CONFIG_COUNT: 1 - GIT_CONFIG_KEY_0: lfs.fetchexclude - GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx - -jobs: - comment: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - submodules: true - - name: Checkout master - uses: actions/checkout@v7 - with: - ref: master - path: base - - run: git lfs pull - - run: cd base && git lfs pull - - - name: scripts/reporter.py - id: report - run: | - echo "content<> $GITHUB_OUTPUT - echo "## Model Review" >> $GITHUB_OUTPUT - PYTHONPATH=${{ github.workspace }}:${{ github.workspace }}/tinygrad_repo MASTER_PATH=${{ github.workspace }}/base python scripts/reporter.py >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Post model report comment - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 - with: - header: model-review - message: ${{ steps.report.outputs.content }} diff --git a/scripts/reporter.py b/scripts/reporter.py deleted file mode 100755 index 175a06fa67..0000000000 --- a/scripts/reporter.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -import os -import glob - -from tinygrad.nn.onnx import OnnxPBParser - -BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) - -MASTER_PATH = os.getenv("MASTER_PATH", BASEDIR) -MODEL_PATH = "/openpilot/selfdrive/modeld/models/" - - -class MetadataOnnxPBParser(OnnxPBParser): - def _parse_ModelProto(self) -> dict: - obj = {"metadata_props": []} - for fid, wire_type in self._parse_message(self.reader.len): - match fid: - case 14: - obj["metadata_props"].append(self._parse_StringStringEntryProto()) - case _: - self.reader.skip_field(wire_type) - return obj - - -def get_checkpoint(f): - model = MetadataOnnxPBParser(f).parse() - metadata = {prop["key"]: prop["value"] for prop in model["metadata_props"]} - # "" or "...//"; combined models list vision then policy - parts = metadata['model_checkpoint'].split('/') - return parts[-2] if len(parts) > 1 else parts[0] - - -if __name__ == "__main__": - print("| | master | PR branch |") - print("|-| ----- | --------- |") - - for f in glob.glob(BASEDIR + MODEL_PATH + "/*.onnx"): - fn = os.path.basename(f) - if fn == "big_driving_supercombo.onnx": - continue - master_path = MASTER_PATH + MODEL_PATH + fn - if os.path.exists(master_path): - master = get_checkpoint(master_path) - master_col = f"[{master}](https://reporter.comma.life/{master})" - else: - master_col = "N/A (new model)" - pr = get_checkpoint(BASEDIR + MODEL_PATH + fn) - print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/{pr})", "|") From ddf782f08a119d585066fab0bbca6994d129d58f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 13:36:12 -0700 Subject: [PATCH 066/325] big release --- RELEASES.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 770a7c40fe..87b1a3c54f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,15 @@ -Version 0.11.2 (2026-06-15) -======================== - +Version 0.11.2 (2026-07-24) +======================= +* New driving model + * Big model with 880M parameters +* New driver monitoring model + * Reduced false positives + * Improved face detection on comma four +* Support for big models running on an external GPU +* Live stream cameras from comma connect +* Remote control for comma body +* New alert sounds +* Volkswagen ID.4 2021-2025 support thanks to DaHansi! Version 0.11.1 (2026-05-18) ======================== From b392328c35dfbe231d8d7c0c43f359f46d0f8281 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 23 Jul 2026 15:00:25 -0700 Subject: [PATCH 067/325] big model loading permanent alert (#38436) show big model loading as permanent alert --- openpilot/selfdrive/selfdrived/events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 76ffea1111..c5b2a87089 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -411,6 +411,7 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { EventName.bigModelLoading: { ET.NO_ENTRY: NoEntryAlert("Big Model Loading"), + ET.PERMANENT: NormalPermanentAlert("Big Model Loading"), }, EventName.bigModelReady: { From 554d8d21143b92a8b584c13863af7bba025e60cd Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:11:22 -0700 Subject: [PATCH 068/325] webrtcd: timeout after 5 minutes (#38437) * webrtc timeout after 5 mins * omit timeout for body --- openpilot/system/webrtc/webrtcd.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 7901f9847f..7979fbe8e3 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -23,6 +23,7 @@ from openpilot.system.webrtc.schema import generate_field from openpilot.common.params import Params from openpilot.cereal import messaging, log +SESSION_TIMEOUT_SECONDS = 300 # socket trick: route lookup for 8.8.8.8 (nothing is sent or actually connected to) # return the source interfaces IP which is the default interface of the device @@ -233,6 +234,8 @@ class StreamSession: builder.add_video_stream(body.init_camera, self.video_track) self.stream = builder.stream() + self.is_body = "testJoystick" in body.bridge_services_in + self.incoming_bridge: CerealIncomingMessageProxy | None = None self.incoming_bridge_services = body.bridge_services_in self.outgoing_bridge: CerealOutgoingMessageProxy | None = None @@ -297,13 +300,26 @@ class StreamSession: if hasattr(self.video_track, 'timing_sei_enabled'): self.video_track.timing_sei_enabled = bool(payload["data"]["enabled"]) case _: - if payload.get("type") not in self.incoming_bridge_services: + if msg_type not in self.incoming_bridge_services: return if self.incoming_bridge is not None: self.incoming_bridge.send(message) except Exception: self.logger.exception("Cereal incoming proxy failure") + async def run_normal_session(self): + try: + await asyncio.wait_for(self.stream.wait_for_disconnection(), timeout=SESSION_TIMEOUT_SECONDS) + except TimeoutError: + self.logger.warning("Stream session (%s) timed out after %d s", self.identifier, SESSION_TIMEOUT_SECONDS) + try: + self.stream.get_messaging_channel().send(json.dumps({"type": "disconnect", "data": "Session timed out"})) + except Exception: + pass + + async def run_body_session(self): + await self.stream.wait_for_disconnection() + async def run(self): try: self.params.put("LivestreamRequestKeyframe", True) @@ -320,7 +336,10 @@ class StreamSession: self.bitrate_controller.start() self.logger.info("Stream session (%s) connected", self.identifier) - await self.stream.wait_for_disconnection() + if self.is_body: + await self.run_body_session() + else: + await self.run_normal_session() self.logger.info("Stream session (%s) ended", self.identifier) except Exception: self.logger.exception("Stream session failure") @@ -547,7 +566,7 @@ def prewarm_stream_session_imports(debug_mode: bool = False) -> None: def webrtcd_thread(host: str, port: int, debug: bool): - logging.basicConfig(level=logging.CRITICAL, handlers=[logging.StreamHandler()]) + logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) prewarm_start = time.monotonic() prewarm_stream_session_imports(debug) prewarm_end = time.monotonic() From ab7284fc8a09dfd94884efdcb75707330c1f16d5 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 23 Jul 2026 15:51:34 -0700 Subject: [PATCH 069/325] AGNOS 18.7 (#38416) * bump version * update AGNOS 18.7 * update to production --- launch_env.sh | 2 +- openpilot/common/hardware/tici/agnos.json | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 696d0fe78d..d348a6e2ac 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="18.6" + export AGNOS_VERSION="18.7" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/tici/agnos.json b/openpilot/common/hardware/tici/agnos.json index a1202ef7a4..7456b6cbfa 100644 --- a/openpilot/common/hardware/tici/agnos.json +++ b/openpilot/common/hardware/tici/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190.img.xz", - "hash": "e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190", - "hash_raw": "e230f456b4849fc7c54ead79cb5ea1c8be91b7ea3431403797e23614834a6190", + "url": "https://commadist.azureedge.net/agnosupdate/boot-4305a9e2061b695eeb955e76a178977d3aaccb220b2474248d3e986b386a788d.img.xz", + "hash": "4305a9e2061b695eeb955e76a178977d3aaccb220b2474248d3e986b386a788d", + "hash_raw": "4305a9e2061b695eeb955e76a178977d3aaccb220b2474248d3e986b386a788d", "size": 17487872, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "34b160f05881d6b4cd7d9a85ad84ac8289d60d9854ccfc00fa31a4534a2c63fc" + "ondevice_hash": "c8c772c7e40923a4f9c47a66a5e9659927e6084fcf2db6bb51a7a73d4d2e1561" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95.img.xz", - "hash": "e34ea7ee73ce85ac357ca2a0ade022fc56c11e9c5af3989cd0c83896a4f9266b", - "hash_raw": "ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95", + "url": "https://commadist.azureedge.net/agnosupdate/system-27882a3e9ca2c5340bef5a51cc995d480bb854ee7b7d8f9de711f79811ac8310.img.xz", + "hash": "d32eeb4c7652163ca77e484ec22ebbd366a7e4954f57dde8c2b7b1e728fa67bd", + "hash_raw": "27882a3e9ca2c5340bef5a51cc995d480bb854ee7b7d8f9de711f79811ac8310", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "7a2bc0374a2719a48b5c27012b16ae9654a473eb3a37c96d5d669d2a1cf45184", + "ondevice_hash": "7fb7931e0edae32ec17e97657f8dfc8bb85aa5370b0180c7965ff2bbe5900692", "alt": { - "hash": "ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95", - "url": "https://commadist.azureedge.net/agnosupdate/system-ef79d27902c9a432609c028e2f17c0609bab554a8644f0ecfdcfe4f3c936ea95.img", + "hash": "27882a3e9ca2c5340bef5a51cc995d480bb854ee7b7d8f9de711f79811ac8310", + "url": "https://commadist.azureedge.net/agnosupdate/system-27882a3e9ca2c5340bef5a51cc995d480bb854ee7b7d8f9de711f79811ac8310.img", "size": 4718592000 } } -] \ No newline at end of file +] From 9c21b92c4dc17b1ac6fe4b9536252856da02d716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 23 Jul 2026 18:36:31 -0700 Subject: [PATCH 070/325] should_stop: use shared helper (#38438) --- .../selfdrive/controls/lib/drive_helpers.py | 6 ++---- .../controls/lib/longitudinal_planner.py | 17 +++++++++-------- openpilot/selfdrive/modeld/modeld.py | 14 +++++++------- .../selfdrive/test/process_replay/migration.py | 8 +++++--- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/drive_helpers.py b/openpilot/selfdrive/controls/lib/drive_helpers.py index 497f8d1bdf..7ac2f50de0 100644 --- a/openpilot/selfdrive/controls/lib/drive_helpers.py +++ b/openpilot/selfdrive/controls/lib/drive_helpers.py @@ -16,7 +16,7 @@ MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2 def should_stop(v_ego: float, a_target: float) -> bool: - return bool(v_ego < 0.25 and a_target < 0.1) + return bool(v_ego < 0.3 and a_target < 0.1) def clamp(val, min_val, max_val): clamped_val = float(np.clip(val, min_val, max_val)) @@ -53,10 +53,8 @@ def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL): v_target = np.interp(action_t, t_idxs, speeds) a_target = 2 * (v_target - v_now) / (action_t) - a_now else: - v_now = 0.0 - v_target = 0.0 a_target = 0.0 - return a_target, should_stop(v_now, a_target) + return a_target def curv_from_psis(psi_target, psi_rate, vego, action_t): vego = np.clip(vego, MIN_SPEED, np.inf) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 0b1fee329d..8855d85af4 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -11,7 +11,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, LongitudinalPlanSource from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC -from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan, should_stop from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog @@ -51,8 +51,7 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS) target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt)) - cruise_should_stop = v_cruise == 0.0 - return target_accel, cruise_should_stop + return target_accel class LongitudinalPlanner: @@ -126,14 +125,16 @@ class LongitudinalPlanner: a_prev = self.a_desired action_t = self.CP.longitudinalActuatorDelay + DT_MDL - output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, - action_t=action_t) + output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, + action_t=action_t) + output_should_stop_mpc = should_stop(v_ego, output_a_target_mpc) output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - self.a_cruise, cruise_should_stop = get_cruise_accel(sm['selfdriveState'].experimentalMode, v_cruise, v_ego, - self.a_cruise, steer_angle_without_offset, self.CP, self.dt, - accel_coast, self.allow_throttle) + self.a_cruise = get_cruise_accel(sm['selfdriveState'].experimentalMode, v_cruise, v_ego, + self.a_cruise, steer_angle_without_offset, self.CP, self.dt, + accel_coast, self.allow_throttle) + cruise_should_stop = should_stop(v_ego, self.a_cruise) candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc), (self.a_cruise, LongitudinalPlanSource.cruise, cruise_should_stop)] diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 3ba4dfa71a..d7d6d38c75 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -19,7 +19,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.common.transformations.model import get_warp_matrix from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan +from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState @@ -41,10 +41,10 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: if 'action' not in model_output: plan = model_output['plan'][0] - desired_accel, should_stop = get_accel_from_plan(plan[:,Plan.VELOCITY][:,0], - plan[:,Plan.ACCELERATION][:,0], - ModelConstants.T_IDXS, - action_t=long_action_t) + desired_accel = get_accel_from_plan(plan[:,Plan.VELOCITY][:,0], + plan[:,Plan.ACCELERATION][:,0], + ModelConstants.T_IDXS, + action_t=long_action_t) desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2], plan[:,Plan.ORIENTATION_RATE][:,2], ModelConstants.T_IDXS, @@ -53,7 +53,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. else: desired_accel = model_output['action'][0,1] desired_curvature = model_output['action'][0,0] / (max(1.0, v_ego))**2 - should_stop = (v_ego < 0.3 and desired_accel < 0.1) + stop = should_stop(v_ego, desired_accel) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) if v_ego > MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS) @@ -62,7 +62,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature), desiredAcceleration=float(desired_accel), - shouldStop=bool(should_stop)) + shouldStop=bool(stop)) class FrameMeta: diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index 87064f5017..e38d1e1042 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -15,7 +15,7 @@ from opendbc.car.gm.values import GMSafetyFlags from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.fill_model_msg import fill_xyz_poly, fill_lane_line_meta from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_encode_index -from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan, should_stop from openpilot.system.manager.process_config import managed_processes from openpilot.tools.lib.logreader import LogIterable @@ -127,8 +127,10 @@ def migrate_longitudinalPlan(msgs): if msg.which() != 'longitudinalPlan': continue new_msg = msg.as_builder() - a_target, should_stop = get_accel_from_plan(msg.longitudinalPlan.speeds, msg.longitudinalPlan.accels, ModelConstants.T_IDXS[:CONTROL_N]) - new_msg.longitudinalPlan.aTarget, new_msg.longitudinalPlan.shouldStop = float(a_target), bool(should_stop) + a_target = get_accel_from_plan(msg.longitudinalPlan.speeds, msg.longitudinalPlan.accels, ModelConstants.T_IDXS[:CONTROL_N]) + v_now = msg.longitudinalPlan.speeds[0] if len(msg.longitudinalPlan.speeds) == CONTROL_N else 0.0 + stop = should_stop(v_now, a_target) + new_msg.longitudinalPlan.aTarget, new_msg.longitudinalPlan.shouldStop = float(a_target), bool(stop) ops.append((index, as_reader(new_msg))) return ops, [], [] From 4d4d6803e59a2c01eb9096dcd447d1afd0d832ea Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 19:11:23 -0700 Subject: [PATCH 071/325] modeld: remove tinygrad probe on build (#38439) --- openpilot/selfdrive/modeld/SConscript | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 5f6281556f..d9af91af4a 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -26,18 +26,7 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " def estimate_pickle_max_size(onnx_size): return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty -# get fastest TG config -# probe in subprocess so usbgpu locks gets released on process exit -def probe_devices(): - return set(subprocess.run( - [sys.executable, '-c', 'from tinygrad import Device\nprint("\\n".join(Device.get_available_devices()))'], - capture_output=True, text=True, check=True).stdout.strip().splitlines()) - -available = probe_devices() -if 'CUDA' in available: - tg_backend = 'CUDA' - tg_flags = f'DEV={tg_backend}' -elif 'QCOM' in available: +if arch == 'larch64': tg_backend = 'QCOM' tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1' else: @@ -54,7 +43,7 @@ tg_devices = { # which device to put jit inputs to at runtime }, } -USBGPU = usbgpu_present() # or release # TODO always build big model on release +USBGPU = usbgpu_present() if USBGPU: usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it From 9c312e91e7440321d1c9d1f85e90d456931b14b6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 19:49:31 -0700 Subject: [PATCH 072/325] release: include nested packages in PYTHONPATH (#38442) --- tools/release/build_release.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 0b6ed16ba9..3ece925a05 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -46,8 +46,9 @@ echo "[-] committing version $VERSION T=$SECONDS" git add -f . git commit -a -m "openpilot v$VERSION release" -# Build -export PYTHONPATH="$BUILD_DIR" +# Build and test before launch_chffrplus.sh creates the on-device package +# symlinks. SConstruct uses the same package roots for build subprocesses. +export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" scons if [ -z "$PANDA_DEBUG_BUILD" ]; then From 1a753ad9ae43aed77617272aea6929dd7454ac8d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jul 2026 19:50:08 -0700 Subject: [PATCH 073/325] test_models moved to opendbc (#38441) * remove test_models * rm that * update test_models docs * restore car model test tool * leave dependencies unchanged --- opendbc_repo | 2 +- .../selfdrive/car/tests/big_cars_test.sh | 11 - openpilot/selfdrive/car/tests/test_models.py | 473 ------------------ .../selfdrive/car/tests/test_models_segs.txt | 3 - tools/car_porting/README.md | 6 +- tools/car_porting/test_car_model.py | 6 +- 6 files changed, 7 insertions(+), 494 deletions(-) delete mode 100755 openpilot/selfdrive/car/tests/big_cars_test.sh delete mode 100644 openpilot/selfdrive/car/tests/test_models.py delete mode 100644 openpilot/selfdrive/car/tests/test_models_segs.txt diff --git a/opendbc_repo b/opendbc_repo index d4c6f68c39..58e07d4aaa 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit d4c6f68c39dbab1c2663bd3c72a0087f93858213 +Subproject commit 58e07d4aaa82147a631d2f38e23013780442dd4f diff --git a/openpilot/selfdrive/car/tests/big_cars_test.sh b/openpilot/selfdrive/car/tests/big_cars_test.sh deleted file mode 100755 index 8c0ecee41e..0000000000 --- a/openpilot/selfdrive/car/tests/big_cars_test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$(dirname "$0") -BASEDIR=$(realpath "$SCRIPT_DIR/../../../../") -cd $BASEDIR - -export MAX_EXAMPLES=300 -export INTERNAL_SEG_CNT=300 -export INTERNAL_SEG_LIST=openpilot/selfdrive/car/tests/test_models_segs.txt - -pytest -n logical --dist worksteal openpilot/selfdrive/car/tests/test_models.py openpilot/selfdrive/car/tests/test_car_interfaces.py diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py deleted file mode 100644 index 29d4eabcc0..0000000000 --- a/openpilot/selfdrive/car/tests/test_models.py +++ /dev/null @@ -1,473 +0,0 @@ -import time -import os -import random -import unittest -from collections import defaultdict, Counter -from openpilot.common.fuzzy import fuzzy_test -from openpilot.common.parameterized import parameterized_class -from openpilot.common.test import OpenpilotTestCase -from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs -from opendbc.car.can_definitions import CanData -from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces -from opendbc.car.fingerprints import MIGRATION -from opendbc.car.honda.values import HondaFlags -from opendbc.car.structs import car -from opendbc.car.tests.routes import non_tested_cars, routes, CarTestRoute -from opendbc.car.values import Platform, PLATFORMS -from opendbc.safety.tests.libsafety import libsafety_py -from openpilot.common.basedir import BASEDIR -from openpilot.selfdrive.pandad import can_capnp_to_list -from openpilot.selfdrive.test.helpers import read_segment_list -from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT -from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source -from openpilot.tools.lib.file_sources import Source -from openpilot.tools.lib.route import SegmentName - -SafetyModel = car.CarParams.SafetyModel -SteerControlType = structs.CarParams.SteerControlType - -# panda safety stores angle_meas in brand-specific CAN units (angle_deg_to_can in opendbc/safety/modes/*.h). -ANGLE_DEG_TO_CAN = { - "tesla": -10, - "toyota": 17.452007, - "nissan": 100, - "psa": 10, -} - -NUM_JOBS = int(os.environ.get("NUM_JOBS", "1")) -JOB_ID = int(os.environ.get("JOB_ID", "0")) -INTERNAL_SEG_LIST = os.environ.get("INTERNAL_SEG_LIST", "") -INTERNAL_SEG_CNT = int(os.environ.get("INTERNAL_SEG_CNT", "0")) -CI = os.environ.get("CI", None) is not None - - -def get_test_cases() -> list[tuple[str, CarTestRoute | None]]: - # build list of test cases - test_cases = [] - if not len(INTERNAL_SEG_LIST): - routes_by_car = defaultdict(set) - for r in routes: - routes_by_car[str(r.car_model)].add(r) - - for i, c in enumerate(sorted(PLATFORMS)): - if i % NUM_JOBS == JOB_ID: - test_cases.extend(sorted((c, r) for r in routes_by_car.get(c, (None,)))) - - else: - segment_list = read_segment_list(os.path.join(BASEDIR, INTERNAL_SEG_LIST)) - segment_list = random.sample(segment_list, INTERNAL_SEG_CNT or len(segment_list)) - for platform, segment in segment_list: - platform = MIGRATION.get(platform, platform) - segment_name = SegmentName(segment) - test_cases.append((platform, CarTestRoute(segment_name.route_name.canonical_name, platform, - segment=segment_name.segment_num))) - return test_cases - - -class TestCarModelBase(OpenpilotTestCase): - SLOW_TEST = True - SHARED_DOWNLOAD_CACHE = True - platform: Platform | None = None - test_route: CarTestRoute | None = None - - can_msgs: list[tuple[int, list[CanData]]] - fingerprint: dict[int, dict[int, int]] - elm_frame: int | None - car_safety_mode_frame: int | None - - @classmethod - def get_testing_data_from_logreader(cls, lr): - car_fw = [] - can_msgs = [] - cls.elm_frame = None - cls.car_safety_mode_frame = None - cls.fingerprint = gen_empty_fingerprint() - alpha_long = False - for msg in lr: - if msg.which() == "can": - can = can_capnp_to_list((msg.as_builder().to_bytes(),))[0] - can_msgs.append((can[0], [CanData(*can) for can in can[1]])) - if len(can_msgs) <= FRAME_FINGERPRINT: - for m in msg.can: - if m.src < 64: - cls.fingerprint[m.src][m.address] = len(m.dat) - - elif msg.which() == "carParams": - car_fw = msg.carParams.carFw - if msg.carParams.openpilotLongitudinalControl: - alpha_long = True - if cls.platform is None: - live_fingerprint = msg.carParams.carFingerprint - cls.platform = MIGRATION.get(live_fingerprint, live_fingerprint) - - # Log which can frame the panda safety mode left ELM327, for CAN validity checks - elif msg.which() == 'pandaStates': - for ps in msg.pandaStates: - if cls.elm_frame is None and ps.safetyModel != SafetyModel.elm327: - cls.elm_frame = len(can_msgs) - if cls.car_safety_mode_frame is None and ps.safetyModel not in \ - (SafetyModel.elm327, SafetyModel.noOutput): - cls.car_safety_mode_frame = len(can_msgs) - - elif msg.which() == 'pandaStateDEPRECATED': - if cls.elm_frame is None and msg.pandaStateDEPRECATED.safetyModel != SafetyModel.elm327: - cls.elm_frame = len(can_msgs) - if cls.car_safety_mode_frame is None and msg.pandaStateDEPRECATED.safetyModel not in \ - (SafetyModel.elm327, SafetyModel.noOutput): - cls.car_safety_mode_frame = len(can_msgs) - - assert len(can_msgs) > int(50 / DT_CTRL), "no can data found" - return car_fw, can_msgs, alpha_long - - @classmethod - def get_testing_data(cls): - test_segs = (2, 1, 0) - if cls.test_route.segment is not None: - test_segs = (cls.test_route.segment,) - - for seg in test_segs: - segment_range = f"{cls.test_route.route}/{seg}" - - try: - sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] - lr = LogReader(segment_range, sources=sources, sort_by_time=True) - return cls.get_testing_data_from_logreader(lr) - except (LogsUnavailable, AssertionError): - pass - - raise Exception(f"Route: {repr(cls.test_route.route)} with segments: {test_segs} not found or no CAN msgs found. Is it uploaded and public?") - - - @classmethod - def setUpClass(cls): - if cls.__name__ == 'TestCarModel' or cls.__name__.endswith('Base'): - raise unittest.SkipTest - - if cls.test_route is None: - if cls.platform in non_tested_cars: - print(f"Skipping tests for {cls.platform}: missing route") - raise unittest.SkipTest - raise Exception(f"missing test route for {cls.platform}") - - car_fw, cls.can_msgs, alpha_long = cls.get_testing_data() - - # if relay is expected to be open in the route - cls.openpilot_enabled = cls.car_safety_mode_frame is not None - - cls.CarInterface = interfaces[cls.platform] - cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, False, docs=False) - assert cls.CP - assert cls.CP.carFingerprint == cls.platform - - os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT - - @classmethod - def tearDownClass(cls): - del cls.can_msgs - - def setUp(self): - self.CI = self.CarInterface(self.CP.copy()) - assert self.CI - - # TODO: check safetyModel is in release panda build - self.safety = libsafety_py.libsafety - - cfg = self.CP.safetyConfigs[-1] - set_status = self.safety.set_safety_hooks(cfg.safetyModel.raw, cfg.safetyParam) - self.assertEqual(0, set_status, f"failed to set safetyModel {cfg}") - self.safety.init_tests() - - def test_car_params(self): - if self.CP.dashcamOnly: - self.skipTest("no need to check carParams for dashcamOnly") - - # make sure car params are within a valid range - self.assertGreater(self.CP.mass, 1) - - if self.CP.steerControlType not in (SteerControlType.angle, SteerControlType.curvature): - tuning = self.CP.lateralTuning.which() - if tuning == 'pid': - self.assertTrue(len(self.CP.lateralTuning.pid.kpV)) - elif tuning == 'torque': - self.assertTrue(self.CP.lateralTuning.torque.latAccelFactor > 0) - else: - raise Exception("unknown tuning") - - def test_car_interface(self): - # TODO: also check for checksum violations from can parser - can_invalid_cnt = 0 - CC = structs.CarControl().as_reader() - - for i, msg in enumerate(self.can_msgs): - CS = self.CI.update(msg) - self.CI.apply(CC, msg[0]) - - # wait max of 2s for low frequency msgs to be seen - if i > 250: - can_invalid_cnt += not CS.canValid - - self.assertEqual(can_invalid_cnt, 0) - - def test_radar_interface(self): - RI = self.CarInterface.RadarInterface(self.CP) - assert RI - - # Since OBD port is multiplexed to bus 1 (commonly radar bus) while fingerprinting, - # start parsing CAN messages after we've left ELM mode and can expect CAN traffic - error_cnt = 0 - for i, msg in enumerate(self.can_msgs[self.elm_frame:]): - rr: structs.RadarData | None = RI.update(msg) - if rr is not None and i > 50: - error_cnt += rr.errors.canError - self.assertEqual(error_cnt, 0) - - def test_panda_safety_rx_checks(self): - if self.CP.dashcamOnly: - self.skipTest("no need to check panda safety for dashcamOnly") - - start_ts = self.can_msgs[0][0] - - failed_addrs = Counter() - for can in self.can_msgs: - # update panda timer - t = (can[0] - start_ts) / 1e3 - self.safety.set_timer(int(t)) - - # run all msgs through the safety RX hook - for msg in can[1]: - if msg.src >= 64: - continue - - to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat) - if self.safety.safety_rx_hook(to_send) != 1: - failed_addrs[hex(msg.address)] += 1 - - # ensure all msgs defined in the addr checks are valid - self.safety.safety_tick_current_safety_config() - if t > 1e6: - self.assertTrue(self.safety.safety_config_valid()) - - # Don't check relay malfunction on disabled routes (relay closed), - # or before fingerprinting is done (elm327 and noOutput) - if self.car_safety_mode_frame is not None and t / 1e4 > self.car_safety_mode_frame: - self.assertFalse(self.safety.get_relay_malfunction()) - else: - self.safety.set_relay_malfunction(False) - - self.assertFalse(len(failed_addrs), f"panda safety RX check failed: {failed_addrs}") - - # ensure RX checks go invalid after small time with no traffic - self.safety.set_timer(int(t + (2*1e6))) - self.safety.safety_tick_current_safety_config() - self.assertFalse(self.safety.safety_config_valid()) - - def test_panda_safety_tx_cases(self, data=None): - """Asserts we can tx common messages""" - if self.CP.dashcamOnly: - self.skipTest("no need to check panda safety for dashcamOnly") - - if self.CP.notCar: - self.skipTest("Skipping test for notCar") - - def test_car_controller(car_control): - now_nanos = 0 - msgs_sent = 0 - CI = self.CarInterface(self.CP) - for _ in range(round(10.0 / DT_CTRL)): # make sure we hit the slowest messages - CI.update([]) - _, sendcan = CI.apply(car_control, now_nanos) - - now_nanos += DT_CTRL * 1e9 - msgs_sent += len(sendcan) - for addr, dat, bus in sendcan: - to_send = libsafety_py.make_CANPacket(addr, bus % 4, dat) - self.assertTrue(self.safety.safety_tx_hook(to_send), (addr, dat, bus)) - - # Make sure we attempted to send messages - self.assertGreater(msgs_sent, 50) - - # Make sure we can send all messages while inactive - CC = structs.CarControl() - test_car_controller(CC.as_reader()) - - # Test cancel + general messages (controls_allowed=False & cruise_engaged=True) - self.safety.set_cruise_engaged_prev(True) - CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(cancel=True)) - test_car_controller(CC.as_reader()) - - # Test resume + general messages (controls_allowed=True & cruise_engaged=True) - self.safety.set_controls_allowed(True) - CC = structs.CarControl(cruiseControl=structs.CarControl.CruiseControl(resume=True)) - test_car_controller(CC.as_reader()) - - # Capturing stdout/stderr here causes elevated memory usage. - @fuzzy_test(max_examples=300) - def test_panda_safety_carstate_fuzzy(self, fuzzy): - """ - For each example, pick a random CAN message on the bus and fuzz its data, - checking for panda state mismatches. - """ - - if self.CP.dashcamOnly: - self.skipTest("no need to check panda safety for dashcamOnly") - - valid_addrs = [(addr, bus, size) for bus, addrs in self.fingerprint.items() for addr, size in addrs.items()] - address, bus, size = fuzzy.choice(valid_addrs) - - msgs = fuzzy.list(lambda: fuzzy.binary(min_size=size, max_size=size), min_size=20) - - vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar - - for n, dat in enumerate(msgs): - # due to panda updating state selectively, only edges are expected to match - # TODO: warm up CarState with real CAN messages to check edge of both sources - # (eg. toyota's gasPressed is the inverse of a signal being set) - prev_panda_gas = self.safety.get_gas_pressed_prev() - prev_panda_brake = self.safety.get_brake_pressed_prev() - prev_panda_regen_braking = self.safety.get_regen_braking_prev() - prev_panda_steering_disengage = self.safety.get_steering_disengage_prev() - prev_panda_vehicle_moving = self.safety.get_vehicle_moving() - prev_panda_vehicle_speed_min = self.safety.get_vehicle_speed_min() - prev_panda_vehicle_speed_max = self.safety.get_vehicle_speed_max() - prev_panda_cruise_engaged = self.safety.get_cruise_engaged_prev() - prev_panda_acc_main_on = self.safety.get_acc_main_on() - - to_send = libsafety_py.make_CANPacket(address, bus, dat) - self.safety.safety_rx_hook(to_send) - - can = [(int(time.monotonic() * 1e9), [CanData(address=address, dat=dat, src=bus)])] - CS = self.CI.update(can) - if n < 5: # CANParser warmup time - continue - - if self.safety.get_gas_pressed_prev() != prev_panda_gas: - self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) - - if self.safety.get_brake_pressed_prev() != prev_panda_brake: - self.assertEqual(CS.brakePressed, self.safety.get_brake_pressed_prev()) - - if self.safety.get_regen_braking_prev() != prev_panda_regen_braking: - self.assertEqual(CS.regenBraking, self.safety.get_regen_braking_prev()) - - if self.safety.get_steering_disengage_prev() != prev_panda_steering_disengage: - self.assertEqual(CS.steeringDisengage, self.safety.get_steering_disengage_prev()) - - if self.safety.get_vehicle_moving() != prev_panda_vehicle_moving and not self.CP.notCar: - self.assertEqual(not CS.standstill, self.safety.get_vehicle_moving()) - - # check vehicle speed if angle control car or available - if self.safety.get_vehicle_speed_min() > 0 or self.safety.get_vehicle_speed_max() > 0: - vehicle_speed_seen = True - - if vehicle_speed_seen and (self.safety.get_vehicle_speed_min() != prev_panda_vehicle_speed_min or - self.safety.get_vehicle_speed_max() != prev_panda_vehicle_speed_max): - v_ego_raw = CS.vEgoRaw / self.CP.wheelSpeedFactor - self.assertFalse(v_ego_raw > (self.safety.get_vehicle_speed_max() + 1e-3) or - v_ego_raw < (self.safety.get_vehicle_speed_min() - 1e-3)) - - if not (self.CP.brand == "honda" and not (self.CP.flags & HondaFlags.BOSCH)): - if self.safety.get_cruise_engaged_prev() != prev_panda_cruise_engaged: - self.assertEqual(CS.cruiseState.enabled, self.safety.get_cruise_engaged_prev()) - - if self.CP.brand == "honda": - if self.safety.get_acc_main_on() != prev_panda_acc_main_on: - self.assertEqual(CS.cruiseState.available, self.safety.get_acc_main_on()) - - def test_panda_safety_carstate(self): - """ - Assert that panda safety matches openpilot's carState - """ - if self.CP.dashcamOnly: - self.skipTest("no need to check panda safety for dashcamOnly") - - # warm up pass, as initial states may be different - for can in self.can_msgs[:300]: - self.CI.update(can) - for msg in filter(lambda m: m.src < 64, can[1]): - to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat) - self.safety.safety_rx_hook(to_send) - - controls_allowed_prev = False - CS_prev = car.CarState.new_message() - checks = defaultdict(int) - vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar - for idx, can in enumerate(self.can_msgs): - CS = self.CI.update(can).as_reader() - for msg in filter(lambda m: m.src < 64, can[1]): - to_send = libsafety_py.make_CANPacket(msg.address, msg.src % 4, msg.dat) - ret = self.safety.safety_rx_hook(to_send) - self.assertEqual(1, ret, f"safety rx failed ({ret=}): {(msg.address, msg.src % 4)}") - - # Skip first frame so CS_prev is properly initialized - if idx == 0: - CS_prev = CS - # Button may be left pressed in warm up period - if not self.CP.pcmCruise: - self.safety.set_controls_allowed(0) - continue - - # TODO: check rest of panda's carstate (steering, ACC main on, etc.) - - checks['gasPressed'] += CS.gasPressed != self.safety.get_gas_pressed_prev() - checks['standstill'] += (CS.standstill == self.safety.get_vehicle_moving()) and not self.CP.notCar - - # check vehicle speed if angle control car or available - if self.safety.get_vehicle_speed_min() > 0 or self.safety.get_vehicle_speed_max() > 0: - vehicle_speed_seen = True - - if vehicle_speed_seen: - v_ego_raw = CS.vEgoRaw / self.CP.wheelSpeedFactor - checks['vEgoRaw'] += (v_ego_raw > (self.safety.get_vehicle_speed_max() + 1e-3) or - v_ego_raw < (self.safety.get_vehicle_speed_min() - 1e-3)) - - # check steering angle for angle control cars (panda stores angle_meas in CAN units) - # ford and VW MEB excluded since they track curvature, not steering angle - # TODO: add curvature check, standardize CAN units to rm brand specific ANGLE_DEG_TO_CAN - if self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar and self.CP.brand not in ("ford", "volkswagen"): - angle_can = (CS.steeringAngleDeg + CS.steeringAngleOffsetDeg) * ANGLE_DEG_TO_CAN[self.CP.brand] - checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or - angle_can < (self.safety.get_angle_meas_min() - 1)) - - checks['brakePressed'] += CS.brakePressed != self.safety.get_brake_pressed_prev() - checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev() - checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev() - - if self.CP.pcmCruise: - # On most pcmCruise cars, openpilot's state is always tied to the PCM's cruise state. - # On Honda Nidec, we always engage on the rising edge of the PCM cruise state, but - # openpilot brakes to zero even if the min ACC speed is non-zero (i.e. the PCM disengages). - if self.CP.brand == "honda" and not (self.CP.flags & HondaFlags.BOSCH): - # only the rising edges are expected to match - if CS.cruiseState.enabled and not CS_prev.cruiseState.enabled: - checks['controlsAllowed'] += not self.safety.get_controls_allowed() - else: - checks['controlsAllowed'] += not CS.cruiseState.enabled and self.safety.get_controls_allowed() - - # TODO: fix notCar mismatch - if not self.CP.notCar: - checks['cruiseState'] += CS.cruiseState.enabled != self.safety.get_cruise_engaged_prev() - else: - # Check for user button enable on rising edge of controls allowed - button_enable = CS.buttonEnable and (not CS.brakePressed or CS.standstill) - mismatch = button_enable != (self.safety.get_controls_allowed() and not controls_allowed_prev) - checks['controlsAllowed'] += mismatch - controls_allowed_prev = self.safety.get_controls_allowed() - if button_enable and not mismatch: - self.safety.set_controls_allowed(False) - - if self.CP.brand == "honda": - checks['mainOn'] += CS.cruiseState.available != self.safety.get_acc_main_on() - - CS_prev = CS - - failed_checks = {k: v for k, v in checks.items() if v > 0} - self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}") - - -@parameterized_class(('platform', 'test_route'), get_test_cases()) -class TestCarModel(TestCarModelBase): - pass - - -if __name__ == "__main__": - unittest.main() diff --git a/openpilot/selfdrive/car/tests/test_models_segs.txt b/openpilot/selfdrive/car/tests/test_models_segs.txt deleted file mode 100644 index c983fb08e7..0000000000 --- a/openpilot/selfdrive/car/tests/test_models_segs.txt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0810a361ec5b5f5f9a2ee73b89ffb2df62ef40e8feff7e97ecb62f80fa53f6f5 -size 124950 diff --git a/tools/car_porting/README.md b/tools/car_porting/README.md index 07ca2d0eba..c4ea80e6a7 100644 --- a/tools/car_porting/README.md +++ b/tools/car_porting/README.md @@ -52,9 +52,9 @@ FAIL: test_panda_safety_carstate (__main__.CarModelTestCase.test_panda_safety_ca Assert that panda safety matches openpilot's carState ---------------------------------------------------------------------- Traceback (most recent call last): - File "/home/batman/xx/openpilot/openpilot/selfdrive/car/tests/test_models.py", line 380, in test_panda_safety_carstate - self.assertFalse(len(failed_checks), f"panda safety doesn't agree with openpilot: {failed_checks}") -AssertionError: 1 is not false : panda safety doesn't agree with openpilot: {'gasPressed': 116} + File "/home/batman/openpilot/opendbc_repo/opendbc/car/tests/test_models.py", line 440, in test_panda_safety_carstate + self.assertFalse(failed_checks, f"panda safety doesn't agree with CarState: {failed_checks}") +AssertionError: {'gasPressed': 116} is not false : panda safety doesn't agree with CarState: {'gasPressed': 116} ``` ## Jupyter notebooks diff --git a/tools/car_porting/test_car_model.py b/tools/car_porting/test_car_model.py index 61ec2e15ed..3fb7ec9ae6 100755 --- a/tools/car_porting/test_car_model.py +++ b/tools/car_porting/test_car_model.py @@ -3,7 +3,7 @@ import argparse import sys import unittest from opendbc.car.tests.routes import CarTestRoute -from openpilot.selfdrive.car.tests.test_models import TestCarModel +from opendbc.car.tests.test_models import TestCarModelBase from openpilot.tools.lib.route import SegmentRange @@ -12,14 +12,14 @@ def create_test_models_suite(routes: list[CarTestRoute]) -> unittest.TestSuite: for test_route in routes: # create new test case and discover tests test_case_args = {"platform": test_route.car_model, "test_route": test_route} - CarModelTestCase = type("CarModelTestCase", (TestCarModel,), test_case_args) + CarModelTestCase = type("CarModelTestCase", (TestCarModelBase,), test_case_args) test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(CarModelTestCase)) return test_suite if __name__ == "__main__": parser = argparse.ArgumentParser(description="Test any route against common issues with a new car port. " + - "Uses openpilot/selfdrive/car/tests/test_models.py") + "Uses opendbc_repo/opendbc/car/tests/test_models.py") parser.add_argument("route_or_segment_name", help="Specify route to run tests on") parser.add_argument("--car", help="Specify car model for test route") args = parser.parse_args() From 171f271d11cedc40ac14561af41e58858c289e0b Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:59:21 -0700 Subject: [PATCH 074/325] bump teleoprtc (#38443) bump --- teleoprtc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teleoprtc_repo b/teleoprtc_repo index c0f813f1c4..6118703a4c 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit c0f813f1c4f7e2d29bc0ed6a4d2a7b3268511b0f +Subproject commit 6118703a4ceb473c957fcf63c3fcf68d45c9d2a7 From 71e29583cbd290a986178460b87db3bd1e03898d Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:39:22 -0700 Subject: [PATCH 075/325] Revert "bump teleoprtc" (#38444) Revert "bump teleoprtc (#38443)" This reverts commit 171f271d11cedc40ac14561af41e58858c289e0b. --- teleoprtc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teleoprtc_repo b/teleoprtc_repo index 6118703a4c..c0f813f1c4 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit 6118703a4ceb473c957fcf63c3fcf68d45c9d2a7 +Subproject commit c0f813f1c4f7e2d29bc0ed6a4d2a7b3268511b0f From 0fdfaf737ff43306423b30de8e3fe0558921b29e Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:42:51 -0700 Subject: [PATCH 076/325] ui(body): flip eye direction (#38445) flip eye --- openpilot/selfdrive/ui/body/layouts/onroad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/body/layouts/onroad.py b/openpilot/selfdrive/ui/body/layouts/onroad.py index d7e9f419cc..a48e525628 100644 --- a/openpilot/selfdrive/ui/body/layouts/onroad.py +++ b/openpilot/selfdrive/ui/body/layouts/onroad.py @@ -67,8 +67,8 @@ class BodyLayout(Widget): self._animator.set_animation(ASLEEP) steer = sm['testJoystick'].axes[1] if len(sm['testJoystick'].axes) > 1 else 0 - self._turning_left = steer <= -0.05 - self._turning_right = steer >= 0.05 + self._turning_left = steer >= 0.05 + self._turning_right = steer <= -0.05 # play animation on screen tap def _handle_mouse_release(self, mouse_pos): From ee6f374621b2ddda303d08bc03be1c40b386bc08 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:45:49 -0700 Subject: [PATCH 077/325] bump teleoprtc (#38446) bump --- teleoprtc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teleoprtc_repo b/teleoprtc_repo index c0f813f1c4..3ac144dc9a 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit c0f813f1c4f7e2d29bc0ed6a4d2a7b3268511b0f +Subproject commit 3ac144dc9a57ace70172d338690b121c42c5add2 From a0cc313fdc362210b59bf1e191a691db4a2a699c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 24 Jul 2026 11:36:33 -0400 Subject: [PATCH 078/325] version: bump to 2026.003.000 --- openpilot/sunnypilot/common/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/sunnypilot/common/version.h b/openpilot/sunnypilot/common/version.h index fccd807e64..26151e206f 100644 --- a/openpilot/sunnypilot/common/version.h +++ b/openpilot/sunnypilot/common/version.h @@ -1 +1 @@ -#define SUNNYPILOT_VERSION "2026.002.000" +#define SUNNYPILOT_VERSION "2026.003.000" From 9225dac4333215f048a86f29d950deb6d52b7ee4 Mon Sep 17 00:00:00 2001 From: Toby Penner Date: Fri, 24 Jul 2026 10:21:01 -0700 Subject: [PATCH 079/325] Revert "Rebel Legion model (#38164)" (#38449) This reverts commit 2895346746634d7eec0ee749f946c87039948a25. --- openpilot/selfdrive/modeld/modeld.py | 4 ++-- openpilot/selfdrive/modeld/models/driving_supercombo.onnx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index d7d6d38c75..7f459abef8 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -31,8 +31,8 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') -LAT_SMOOTH_SECONDS = 0.1 -LONG_SMOOTH_SECONDS = 0.1 +LAT_SMOOTH_SECONDS = 0.0 +LONG_SMOOTH_SECONDS = 0.3 MIN_LAT_CONTROL_SPEED = 0.3 BIG_MODEL_TIMEOUT = 60 diff --git a/openpilot/selfdrive/modeld/models/driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx index 944eebb017..f0672eab48 100644 --- a/openpilot/selfdrive/modeld/models/driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15e563da34889318e41321a2559682d417416602de9b6f5093c3be0e6766419d -size 96599486 +oid sha256:659727c4d4839adc4992a254409a54259a8756a743f2d567bf5fdc6579f8009b +size 60881999 From c37855d113494f58ca51e9995adf03c17ba17deb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 24 Jul 2026 11:03:01 -0700 Subject: [PATCH 080/325] op: merge setup and install (#38450) --- tools/op.sh | 28 ++++++++++++---------------- tools/setup.sh | 1 - 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tools/op.sh b/tools/op.sh index 6d70310668..f06b543bd2 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -19,18 +19,6 @@ RC_FILE="${HOME}/.$(basename ${SHELL})rc" if [ "$(uname)" == "Darwin" ] && [ $SHELL == "/bin/bash" ]; then RC_FILE="$HOME/.bash_profile" fi -function op_install() { - echo "Installing op system-wide..." - OP_SH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )/op.sh" - CMD=$(cat </dev/null || printf '\n%s\n' "$CMD" >> "$RC_FILE" - echo -e " ↳ [${GREEN}✔${NC}] op installed successfully. Open a new shell to use it." -} function retry() { local attempts=$1 @@ -99,7 +87,6 @@ function op_check_openpilot_dir() { echo -e " ↳ [${GREEN}✔${NC}] openpilot found." return 0 fi - echo -e " ↳ [${RED}✗${NC}] openpilot directory not found! Make sure that you are" echo " inside the openpilot directory or specify one with the" echo " --dir option!" @@ -193,6 +180,17 @@ function op_before_cmd() { } function op_setup() { + echo "Installing op system-wide..." + OP_SH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )/op.sh" + CMD=$(cat </dev/null || printf '\n%s\n' "$CMD" >> "$RC_FILE" + echo -e " ↳ [${GREEN}✔${NC}] op installed successfully. Open a new shell to use it." + op_get_openpilot_dir cd $OPENPILOT_ROOT @@ -409,9 +407,8 @@ function op_default() { echo -e " ${BOLD}check${NC} Check the development environment (git, os) to start using openpilot" echo -e " ${BOLD}esim${NC} Manage eSIM profiles on your comma device" echo -e " ${BOLD}venv${NC} Activate the python virtual environment" - echo -e " ${BOLD}setup${NC} Install openpilot dependencies" + echo -e " ${BOLD}setup${NC} Install the 'op' tool and openpilot dependencies" echo -e " ${BOLD}build${NC} Run the openpilot build system in the current working directory" - echo -e " ${BOLD}install${NC} Install the 'op' tool system wide" echo -e " ${BOLD}switch${NC} Switch to a different git branch with a clean slate (nukes any changes)" echo -e " ${BOLD}start${NC} Starts (or restarts) openpilot" echo -e " ${BOLD}stop${NC} Stops openpilot" @@ -477,7 +474,6 @@ function _op() { replay ) shift 1; op_replay "$@" ;; clip ) shift 1; op_clip "$@" ;; sim ) shift 1; op_sim "$@" ;; - install ) shift 1; op_install "$@" ;; switch ) shift 1; op_switch "$@" ;; start ) shift 1; op_start "$@" ;; stop ) shift 1; op_stop "$@" ;; diff --git a/tools/setup.sh b/tools/setup.sh index dafd466ef9..d4b68d15cb 100755 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -121,7 +121,6 @@ function git_clone() { function install_with_op() { cd $OPENPILOT_ROOT - $OPENPILOT_ROOT/tools/op.sh install $OPENPILOT_ROOT/tools/op.sh post-commit if ! $OPENPILOT_ROOT/tools/op.sh setup; then From 9163d1cb74dfc747028da751beaec11b3408fb6d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 25 Jul 2026 01:27:11 -0400 Subject: [PATCH 081/325] Revert "mapd: ignore in plannerd health check (#1880)" This reverts commit 2c334ede443d7391d27575af8c854a095ba702a8. --- openpilot/selfdrive/controls/plannerd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index 5908e141f7..66b17d60a7 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -23,14 +23,13 @@ def main(): cloudlog.info("plannerd got CarParamsSP") gps_location_service = get_gps_location_service(params) - ignore_services = ["liveMapDataSP", gps_location_service] ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP, CP_SP) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', 'liveMapDataSP', 'carStateSP', gps_location_service], - poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) + poll='carState') while True: sm.update() From 0265ae5f7629f464daecdba562962a3f33c29bb9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 25 Jul 2026 01:27:25 -0400 Subject: [PATCH 082/325] Revert "plannerd: check all services for validity (#38341)" This reverts commit a9ffebe96e88a664ce71e76a86f7b3b6719eb3f4. --- openpilot/selfdrive/controls/lib/longitudinal_planner.py | 2 +- openpilot/selfdrive/controls/plannerd.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 7e3446a768..6eaed7370d 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -158,7 +158,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') - plan_send.valid = sm.all_checks() + plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index 66b17d60a7..05e44c173e 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -40,7 +40,7 @@ def main(): ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl']) msg = messaging.new_message('driverAssistance') - msg.valid = sm.all_checks() + msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters']) msg.driverAssistance.leftLaneDeparture = ldw.left msg.driverAssistance.rightLaneDeparture = ldw.right pm.send('driverAssistance', msg) From 7801bdf0cc2e2373ed3d78cdd29f8f903595db79 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 25 Jul 2026 01:36:05 -0400 Subject: [PATCH 083/325] Reapply "mapd: ignore in plannerd health check (#1880)" This reverts commit 9163d1cb74dfc747028da751beaec11b3408fb6d. --- openpilot/selfdrive/controls/plannerd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index 05e44c173e..3a05ea174b 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -23,13 +23,14 @@ def main(): cloudlog.info("plannerd got CarParamsSP") gps_location_service = get_gps_location_service(params) + ignore_services = ["liveMapDataSP", gps_location_service] ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP, CP_SP) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP']) sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState', 'liveMapDataSP', 'carStateSP', gps_location_service], - poll='carState') + poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services) while True: sm.update() From ee3583df3383f815640636887591fd09874da4ae Mon Sep 17 00:00:00 2001 From: Christopher Haucke <132518562+CHaucke89@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:15:05 -0400 Subject: [PATCH 084/325] [tizi/tici] ui: Camera offset controls (#1813) * Camera offset controls * final --------- Co-authored-by: Jason Wen --- .../ui/sunnypilot/layouts/settings/models.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index e2ea81d1f5..be2883d718 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -43,7 +43,7 @@ class ModelsLayout(Widget): self._initialize_items() self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") - for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]: + for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]: ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100)) self._scroller = Scroller(self.items, line_separator=True, spacing=0) @@ -93,9 +93,14 @@ class ModelsLayout(Widget): self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle") + self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35, + tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"), + 1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, + lambda v: f"{v / 100:.2f} m") + self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, - self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle, - self.lane_turn_value_control, self.lagd_toggle, self.delay_control] + self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, + self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset] def _update_lagd_description(self, lagd_toggle: bool): desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " + @@ -232,6 +237,7 @@ class ModelsLayout(Widget): advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") live_delay: bool = ui_state.params.get_bool("LagdToggle") + camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None self.lane_turn_desire_toggle.action_item.set_state(turn_desire) self.lane_turn_value_control.set_visible(turn_desire and advanced_controls) @@ -240,6 +246,7 @@ class ModelsLayout(Widget): new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100 if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step: self.lane_turn_value_control.action_item.value_change_step = new_step + self.camera_offset.set_visible(camera_offset) self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] From fd22de1c9aa88e0ad02549ff27d62d05394417c9 Mon Sep 17 00:00:00 2001 From: Eitan <20345138+0x3k@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:37:18 -0700 Subject: [PATCH 085/325] SCC-M: fix operator precedence in quadratic roots (#1816) * controls/scc: fix operator precedence in map controller quadratic roots * not async --------- Co-authored-by: Jason Wen --- .../smart_cruise_control/map_controller.py | 4 ++-- .../tests/test_map_controller.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py index 65e157bdbc..f3ed0fc07b 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/map_controller.py @@ -151,8 +151,8 @@ class SmartCruiseControlMap: a = 0.5 * TARGET_JERK b = self.a_ego c = self.v_ego - tv - t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / 2 * a - t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / 2 * a + t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / (2 * a) + t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / (2 * a) if not isinstance(t_a, complex) and t_a > 0: t = t_a else: diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py index 3b16c59bb5..dc27447c0b 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py @@ -4,13 +4,17 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import json +import math import platform +import pytest + from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET -from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState @@ -55,4 +59,17 @@ class TestSmartCruiseControlMap: self.scc_m.update(True, False, 0., 0., 0.) assert self.scc_m.state == VisionState.enabled + def test_moderate_curve(self): + # Regression: `... / 2 * a` parsed as `(.../2)*a` instead of `.../(2*a)`, + # making max_d ~11x too small so the moderate-curve branch never tripped. + # v_ego=25, a_ego=0, tv=24: fixed max_d≈45m vs buggy ≈4m at a 40m waypoint. + waypoint_lon_deg = (40.0 / R) * (180.0 / math.pi) + self.mem_params.put("LastGPSPosition", json.dumps({"latitude": 0.0, "longitude": 0.0}), block=True) + self.mem_params.put("MapTargetVelocities", + json.dumps([{"latitude": 0.0, "longitude": waypoint_lon_deg, "velocity": 24.0}]), block=True) + + self.scc_m.update(True, False, 25.0, 0.0, 30.0) + + assert self.scc_m.v_target == pytest.approx(24.0) + # TODO-SP: mock data from modelV2 to test other states From 27122bbd281395fc74c796868532a8f0359aaeaa Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 25 Jul 2026 10:29:37 -0700 Subject: [PATCH 086/325] lint: check indentation (#38454) * lint: check indentation * cleanup * happy * that was too much * happy --- .../selfdrive/assets/sounds/make_beeps.py | 10 ++-- openpilot/selfdrive/locationd/helpers.py | 46 +++++++++---------- .../selfdrive/ui/mici/onroad/cameraview.py | 12 ++--- openpilot/system/athena/athenad.py | 2 +- openpilot/system/athena/tests/test_athenad.py | 2 +- openpilot/tools/lib/framereader.py | 2 +- .../sim/bridge/metadrive/metadrive_process.py | 4 +- scripts/lint/check_indentation.py | 38 +++++++++++++++ scripts/lint/lint.sh | 2 + 9 files changed, 79 insertions(+), 39 deletions(-) create mode 100755 scripts/lint/check_indentation.py diff --git a/openpilot/selfdrive/assets/sounds/make_beeps.py b/openpilot/selfdrive/assets/sounds/make_beeps.py index 6161e80e74..e4c10c8d19 100644 --- a/openpilot/selfdrive/assets/sounds/make_beeps.py +++ b/openpilot/selfdrive/assets/sounds/make_beeps.py @@ -6,12 +6,12 @@ sr = 48000 max_int16 = 2**15 - 1 def harmonic_beep(freq, duration_seconds): - n_total = int(sr * duration_seconds) + n_total = int(sr * duration_seconds) - signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr) - x = np.arange(n_total) - exp_scale = np.exp(-x/5.5e3) - return max_int16 * signal * exp_scale + signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr) + x = np.arange(n_total) + exp_scale = np.exp(-x/5.5e3) + return max_int16 * signal * exp_scale engage_beep = harmonic_beep(1661.219, 0.5) wavfile.write("engage.wav", sr, engage_beep.astype(np.int16)) diff --git a/openpilot/selfdrive/locationd/helpers.py b/openpilot/selfdrive/locationd/helpers.py index 5e3fa16027..9ea9b6bf4f 100644 --- a/openpilot/selfdrive/locationd/helpers.py +++ b/openpilot/selfdrive/locationd/helpers.py @@ -9,29 +9,29 @@ from openpilot.common.transformations.orientation import rot_from_euler, euler_f @cache def fft_next_good_size(n: int) -> int: - """ - smallest composite of 2, 3, 5, 7, 11 that is >= n - inspired by pocketfft - """ - if n <= 6: - return n - best, f2 = 2 * n, 1 - while f2 < best: - f23 = f2 - while f23 < best: - f235 = f23 - while f235 < best: - f2357 = f235 - while f2357 < best: - f235711 = f2357 - while f235711 < best: - best = f235711 if f235711 >= n else best - f235711 *= 11 - f2357 *= 7 - f235 *= 5 - f23 *= 3 - f2 *= 2 - return best + """ + smallest composite of 2, 3, 5, 7, 11 that is >= n + inspired by pocketfft + """ + if n <= 6: + return n + best, f2 = 2 * n, 1 + while f2 < best: + f23 = f2 + while f23 < best: + f235 = f23 + while f235 < best: + f2357 = f235 + while f2357 < best: + f235711 = f2357 + while f235711 < best: + best = f235711 if f235711 >= n else best + f235711 *= 11 + f2357 *= 7 + f235 *= 5 + f23 *= 3 + f2 *= 2 + return best def parabolic_peak_interp(R, max_index): diff --git a/openpilot/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py index 82e4865c76..4a31a9e04e 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -383,12 +383,12 @@ class CameraView(Widget): self._initialize_textures() def _initialize_textures(self): - self._clear_textures() - if not TICI: - self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), - int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) - self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), - int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) + self._clear_textures() + if not TICI: + self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), + int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) + self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), + int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA)) def _clear_textures(self): if self.texture_y and self.texture_y.id: diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index 91b12fc0a7..e7c318499a 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -580,7 +580,7 @@ def startStream(sdp: str, enabled: bool) -> dict: if CP.notCar: bridge_services_in.append("testJoystick") else: - raise Exception("failed to get CarParamsPersistent") + raise Exception("failed to get CarParamsPersistent") if params.get_bool("IsOffroad"): # manager owns camerad/stream_encoderd/webrtcd; flip the param and let it bring them up. diff --git a/openpilot/system/athena/tests/test_athenad.py b/openpilot/system/athena/tests/test_athenad.py index dbc1050db8..c4cd93558c 100644 --- a/openpilot/system/athena/tests/test_athenad.py +++ b/openpilot/system/athena/tests/test_athenad.py @@ -49,7 +49,7 @@ def with_upload_handler(func): return wrapper def mock_create_connection(mocker): - return mocker.patch('openpilot.system.athena.athenad.create_connection') + return mocker.patch('openpilot.system.athena.athenad.create_connection') def host(): with http_server_context(handler=HTTPRequestHandler, setup=seed_athena_server) as (host, port): diff --git a/openpilot/tools/lib/framereader.py b/openpilot/tools/lib/framereader.py index c923651563..62c75b95f7 100644 --- a/openpilot/tools/lib/framereader.py +++ b/openpilot/tools/lib/framereader.py @@ -28,7 +28,7 @@ class LRUCache: def __setitem__(self, key, value): self._cache[key] = value if len(self._cache) > self.capacity: - self._cache.popitem(last=False) + self._cache.popitem(last=False) def __contains__(self, key): return key in self._cache diff --git a/openpilot/tools/sim/bridge/metadrive/metadrive_process.py b/openpilot/tools/sim/bridge/metadrive/metadrive_process.py index 2486d87ff9..01d0473755 100644 --- a/openpilot/tools/sim/bridge/metadrive/metadrive_process.py +++ b/openpilot/tools/sim/bridge/metadrive/metadrive_process.py @@ -27,9 +27,9 @@ def apply_metadrive_patches(arrive_dest_done=True): # By default, metadrive won't try to use cuda images unless it's used as a sensor for vehicles, so patch that in def add_image_sensor_patched(self, name: str, cls, args): if self.global_config["image_on_cuda"]:# and name == self.global_config["vehicle_config"]["image_source"]: - sensor = cls(*args, self, cuda=True) + sensor = cls(*args, self, cuda=True) else: - sensor = cls(*args, self, cuda=False) + sensor = cls(*args, self, cuda=False) assert isinstance(sensor, ImageBuffer), "This API is for adding image sensor" self.sensors[name] = sensor diff --git a/scripts/lint/check_indentation.py b/scripts/lint/check_indentation.py new file mode 100755 index 0000000000..533dd2efa9 --- /dev/null +++ b/scripts/lint/check_indentation.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import argparse +import tokenize + + +# TODO: remove this once https://github.com/astral-sh/ruff/issues/8705 is closed +def check_indentation(filename: str, indent_width: int = 2) -> bool: + failed = False + indent_stack = [0] + + with tokenize.open(filename) as f: + tokens = tokenize.generate_tokens(f.readline) + for token in tokens: + if token.type == tokenize.INDENT: + indentation = token.string + width = len(indentation) + expected = indent_stack[-1] + indent_width + + if indentation != " " * expected: + found = "indentation containing tabs" if "\t" in indentation else f"{width} spaces" + print(f"{filename}:{token.start[0]}:1: expected {expected} spaces, found {found}") + failed = True + indent_stack.append(width) + elif token.type == tokenize.DEDENT: + indent_stack.pop() + + return failed + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Check Python block indentation.") + parser.add_argument("filenames", nargs="+") + args = parser.parse_args() + + failed = False + for filename in args.filenames: + failed |= check_indentation(filename) + raise SystemExit(failed) diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index c678379b42..bfd93f23a9 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -46,6 +46,7 @@ function run_tests() { PYTHON_FILES=$2 run "ruff" ruff check openpilot --quiet + run "check_indentation" $DIR/check_indentation.py $PYTHON_FILES run "check_added_large_files" $DIR/check_added_large_files.py --maxkb=120 $ALL_FILES run "check_shebang_scripts_are_executable" $DIR/check_shebang_scripts_are_executable.py $ALL_FILES run "check_shebang_format" $DIR/check_shebang_format.sh $ALL_FILES @@ -66,6 +67,7 @@ function help() { echo "" echo -e "${BOLD}${UNDERLINE}Tests:${NC}" echo -e " ${BOLD}ruff${NC}" + echo -e " ${BOLD}check_indentation${NC}" echo -e " ${BOLD}ty${NC}" echo -e " ${BOLD}codespell${NC}" echo -e " ${BOLD}check_added_large_files${NC}" From 3a05c03079d796f533f342489b3f681cfd21f98d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 25 Jul 2026 15:34:36 -0400 Subject: [PATCH 087/325] ci: no more docker (#1886) --- .github/workflows/prebuilt.yaml | 39 --------------------------------- release/ci/docker_build_sp.sh | 30 ------------------------- 2 files changed, 69 deletions(-) delete mode 100644 .github/workflows/prebuilt.yaml delete mode 100755 release/ci/docker_build_sp.sh diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml deleted file mode 100644 index aeb0f11d84..0000000000 --- a/.github/workflows/prebuilt.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: prebuilt -on: - schedule: - - cron: '0 * * * *' - workflow_dispatch: - -env: - DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: release/ci/docker_build_sp.sh - -jobs: - build_prebuilt: - name: build prebuilt - runs-on: ubuntu-latest - if: github.repository == 'sunnypilot/sunnypilot' - env: - PUSH_IMAGE: true - permissions: - checks: read - contents: read - packages: write - steps: - - name: Wait for green check mark - if: ${{ github.event_name != 'workflow_dispatch' }} - uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc - with: - ref: master - wait-interval: 30 - running-workflow-name: 'build prebuilt' - repo-token: ${{ secrets.GITHUB_TOKEN }} - check-regexp: ^((?!.*(build master-ci|create badges).*).)*$ - - uses: actions/checkout@v6 - with: - submodules: true - - run: git lfs pull - - name: Build and Push docker image - run: | - $DOCKER_LOGIN - eval "$BUILD" diff --git a/release/ci/docker_build_sp.sh b/release/ci/docker_build_sp.sh deleted file mode 100755 index 369daf5233..0000000000 --- a/release/ci/docker_build_sp.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -e - -SCRIPT_DIR=$(dirname "$0") -OPENPILOT_DIR=$SCRIPT_DIR/../../ - -DOCKER_IMAGE=sunnypilot -DOCKER_FILE=Dockerfile.openpilot -DOCKER_REGISTRY=ghcr.io/sunnypilot -COMMIT_SHA=$(git rev-parse HEAD) - -if [ -n "$TARGET_ARCHITECTURE" ]; then - PLATFORM="linux/$TARGET_ARCHITECTURE" - TAG_SUFFIX="-$TARGET_ARCHITECTURE" -else - PLATFORM="linux/$(uname -m)" - TAG_SUFFIX="" -fi - -LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX -REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG -REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA - -DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR - -if [ -n "$PUSH_IMAGE" ]; then - docker push $REMOTE_TAG - docker tag $REMOTE_TAG $REMOTE_SHA_TAG - docker push $REMOTE_SHA_TAG -fi From 9a39ccc66f92d3e17df1d5e51120434a065e2889 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 26 Jul 2026 08:52:49 -0700 Subject: [PATCH 088/325] tools/auth: use any open port (#38458) --- openpilot/tools/lib/auth.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/openpilot/tools/lib/auth.py b/openpilot/tools/lib/auth.py index 9139d8b42d..6a685e38d9 100755 --- a/openpilot/tools/lib/auth.py +++ b/openpilot/tools/lib/auth.py @@ -32,9 +32,6 @@ from urllib.parse import parse_qs, urlencode from openpilot.tools.lib.api import APIError, CommaApi, UnauthorizedError from openpilot.tools.lib.auth_config import set_token, get_token -PORT = 3000 - - class ClientRedirectServer(HTTPServer): query_params: dict[str, Any] = {} @@ -58,7 +55,7 @@ class ClientRedirectHandler(BaseHTTPRequestHandler): pass # this prevent http server from dumping messages to stdout -def auth_redirect_link(method): +def auth_redirect_link(method, port): provider_id = { 'google': 'g', 'apple': 'a', @@ -67,7 +64,7 @@ def auth_redirect_link(method): params = { 'redirect_uri': f"https://api.comma.ai/v2/auth/{provider_id}/redirect/", - 'state': f'service,localhost:{PORT}', + 'state': f'service,localhost:{port}', } if method == 'google': @@ -98,9 +95,9 @@ def auth_redirect_link(method): def login(method): - oauth_uri = auth_redirect_link(method) - - web_server = ClientRedirectServer(('localhost', PORT), ClientRedirectHandler) + # Let the OS select an available port to avoid colliding with other services. + web_server = ClientRedirectServer(('localhost', 0), ClientRedirectHandler) + oauth_uri = auth_redirect_link(method, web_server.server_port) print(f'To sign in, use your browser and navigate to {oauth_uri}') webbrowser.open(oauth_uri, new=2) From 5bcff3f87d2c0c819acf3686591e84365cff97b0 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 26 Jul 2026 12:18:11 -0700 Subject: [PATCH 089/325] bump opendbc: rm jinja (#38463) --- docs/CARS.md | 4 +- opendbc_repo | 2 +- openpilot/selfdrive/car/CARS_template.md | 26 ++-------- openpilot/selfdrive/car/docs.py | 57 +++++++++++++++++++++- openpilot/selfdrive/car/tests/test_docs.py | 4 +- 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 31abdf433c..d0ce526e7f 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -232,8 +232,8 @@ A supported vehicle is one that just works when you install a comma device. All |Nissan[6](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Nissan[6](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |SEAT[12](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |SEAT[12](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Subaru|Ascent 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| diff --git a/opendbc_repo b/opendbc_repo index 58e07d4aaa..d7c9aff771 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 58e07d4aaa82147a631d2f38e23013780442dd4f +Subproject commit d7c9aff771a847f066e49573eaf69a458e7f2e14 diff --git a/openpilot/selfdrive/car/CARS_template.md b/openpilot/selfdrive/car/CARS_template.md index 35804ff1fa..a67fa971a4 100644 --- a/openpilot/selfdrive/car/CARS_template.md +++ b/openpilot/selfdrive/car/CARS_template.md @@ -1,31 +1,16 @@ -{% set footnote_tag = '[{}](#footnotes)' %} -{% set star_icon = '[![star](assets/icon-star-{}.svg)](##)' %} -{% set video_icon = '' %} -{# Force hardware column wider by using a blank image with max width. #} -{% set width_tag = '%s
 ' %} -{% set hardware_col_name = 'Hardware Needed' %} -{% set wide_hardware_col_name = width_tag|format(hardware_col_name) -%} - # Supported Cars A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# {{all_car_docs | selectattr('support_type', 'eq', SupportType.UPSTREAM) | list | length}} Supported Cars - -|{{Column | map(attribute='value') | join('|') | replace(hardware_col_name, wide_hardware_col_name)}}| -|---|---|---|{% for _ in range((Column | length) - 3) %}{{':---:|'}}{% endfor +%} -{% for car_docs in all_car_docs | selectattr('support_type', 'eq', SupportType.UPSTREAM) %} -|{% for column in Column %}{{car_docs.get_column(column, star_icon, video_icon, footnote_tag)}}|{% endfor %} - -{% endfor %} +# $supported_count Supported Cars +$table_header +$table_separator +$table_rows ### Footnotes -{% for footnote in footnotes %} -{{loop.index}}{{footnote | replace('
', '')}}
-{% endfor %} - +$footnotes ## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). @@ -71,4 +56,3 @@ openpilot does not yet support these Toyota models due to a new message authenti * Lexus NX 2022+ * Toyota bZ4x 2023+ * Subaru Solterra 2023+ - diff --git a/openpilot/selfdrive/car/docs.py b/openpilot/selfdrive/car/docs.py index ea7a70688e..8d135d82ea 100755 --- a/openpilot/selfdrive/car/docs.py +++ b/openpilot/selfdrive/car/docs.py @@ -1,13 +1,68 @@ #!/usr/bin/env python3 import argparse import os +from string import Template from openpilot.common.basedir import BASEDIR -from opendbc.car.docs import get_all_car_docs, generate_cars_md +from opendbc.car.docs import get_all_car_docs, get_all_footnotes +from opendbc.car.docs_definitions import Column, SupportType CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "openpilot/selfdrive", "car", "CARS_template.md") +FOOTNOTE_TAG = '[{}](#footnotes)' +STAR_ICON = '[![star](assets/icon-star-{}.svg)](##)' +VIDEO_ICON = '' +# Force hardware column wider by using a blank image with max width. +HARDWARE_COL_NAME = 'Hardware Needed' +WIDE_HARDWARE_COL_NAME = f'{HARDWARE_COL_NAME}
 ' + + +def _build_cars_table(upstream_cars) -> tuple[str, str, str]: + columns = list(Column) + header_cells = [ + WIDE_HARDWARE_COL_NAME if col.value == HARDWARE_COL_NAME else col.value + for col in columns + ] + table_header = "|" + "|".join(header_cells) + "|" + + # First three columns left-aligned (---), remaining centered (:---:) + sep_parts = ["---"] * min(3, len(columns)) + [":---:"] * max(0, len(columns) - 3) + table_separator = "|" + "|".join(sep_parts) + "|" + + rows = [] + for car_docs in upstream_cars: + cells = [car_docs.get_column(column, STAR_ICON, VIDEO_ICON, FOOTNOTE_TAG) for column in columns] + rows.append("|" + "|".join(cells) + "|") + table_rows = "\n".join(rows) + ("\n" if rows else "") + + return table_header, table_separator, table_rows + + +def generate_cars_md(all_car_docs, template_fn: str, **kwargs) -> str: + del kwargs # kept for call-site compatibility + + upstream_cars = [c for c in all_car_docs if c.support_type == SupportType.UPSTREAM] + table_header, table_separator, table_rows = _build_cars_table(upstream_cars) + + footnotes = [fn.value.text.replace('
', '') for fn in get_all_footnotes()] + footnotes_md = "\n".join( + f"{i}{text}
" + for i, text in enumerate(footnotes, start=1) + ) + ("\n" if footnotes else "") + + with open(template_fn) as f: + template = Template(f.read()) + + return template.substitute( + supported_count=len(upstream_cars), + table_header=table_header, + table_separator=table_separator, + table_rows=table_rows, + footnotes=footnotes_md, + ) + + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Auto generates supported cars documentation", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/openpilot/selfdrive/car/tests/test_docs.py b/openpilot/selfdrive/car/tests/test_docs.py index 9f5abba336..99438b4720 100644 --- a/openpilot/selfdrive/car/tests/test_docs.py +++ b/openpilot/selfdrive/car/tests/test_docs.py @@ -1,6 +1,6 @@ -from opendbc.car.docs import generate_cars_md, get_all_car_docs from openpilot.common.test import OpenpilotTestCase -from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE +from openpilot.selfdrive.car.docs import CARS_MD_TEMPLATE, generate_cars_md +from opendbc.car.docs import get_all_car_docs class TestCarDocs(OpenpilotTestCase): From e1db0f59f1f11a379942ac3e18c147d1e12d01bd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 26 Jul 2026 13:56:32 -0700 Subject: [PATCH 090/325] docs: replace zensical with dumb html template (#38462) --- .github/workflows/docs.yaml | 5 +- .gitignore | 2 +- docs/DEVELOPMENT.md | 24 -- docs/README.md | 14 ++ docs/assets/comma-logo.png | 2 +- docs/assets/favicon.svg | 3 + docs/concepts/glossary.md | 3 - docs/ext/glossary.py | 216 ------------------ docs/ext/glossary.toml | 8 - docs/serve.py | 444 ++++++++++++++++++++++++++++++++++++ docs/stylesheets/extra.css | 42 ---- docs/template.html | 209 +++++++++++++++++ pyproject.toml | 4 - scripts/docs.py | 63 ----- uv.lock | 135 +---------- zensical.toml | 81 ------- 16 files changed, 676 insertions(+), 579 deletions(-) delete mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/README.md create mode 100644 docs/assets/favicon.svg delete mode 100644 docs/concepts/glossary.md delete mode 100644 docs/ext/glossary.py delete mode 100644 docs/ext/glossary.toml create mode 100644 docs/serve.py delete mode 100644 docs/stylesheets/extra.css create mode 100644 docs/template.html delete mode 100644 scripts/docs.py delete mode 100644 zensical.toml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 4be4fac916..f4a8e7cb89 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -35,8 +35,7 @@ jobs: - name: Build docs run: | git lfs pull - pip install zensical - python scripts/docs.py build + python docs/serve.py --build # Push to docs.comma.ai - uses: actions/checkout@v7 @@ -57,7 +56,7 @@ jobs: git rm -rf . # copy over docs - cp -r ../docs_site/ docs/ + cp -r ../docs/_site/ docs/ # GitHub pages config touch docs/.nojekyll diff --git a/.gitignore b/.gitignore index 5c99567581..48ce8f0414 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ compare_runtime*.html openpilot/selfdrive/modeld/models/tg_input_devices.json # build artifacts -docs_site/ +docs/_site/ openpilot/selfdrive/pandad/pandad openpilot/cereal/services.h openpilot/cereal/gen diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md deleted file mode 100644 index e803a3fb8a..0000000000 --- a/docs/DEVELOPMENT.md +++ /dev/null @@ -1,24 +0,0 @@ -# Docs development - -The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai). -The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml). - -Those commands must be run in the root directory of openpilot, **not /docs** - -**1. Install the docs dependencies** -``` bash -uv pip install .[docs] -``` - -**2. Build the new site** -``` bash -docs build -``` - -**3. Run the new site locally** -``` bash -docs serve -``` - -References: -* https://zensical.org/docs/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..d6a0126b39 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +# Docs development + +The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai). +The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml). + +**1. Build the site** +``` bash +python docs/serve.py --build +``` + +**2. Run the site locally** (rebuilds on change) +``` bash +python docs/serve.py +``` diff --git a/docs/assets/comma-logo.png b/docs/assets/comma-logo.png index 2838d92bfb..19b67d0739 120000 --- a/docs/assets/comma-logo.png +++ b/docs/assets/comma-logo.png @@ -1 +1 @@ -../../selfdrive/assets/icons_mici/settings/comma_icon.png \ No newline at end of file +../../openpilot/selfdrive/assets/icons_mici/settings/comma_icon.png \ No newline at end of file diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg new file mode 100644 index 0000000000..304454837a --- /dev/null +++ b/docs/assets/favicon.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e019ed5af500e8820d05934d47e0380728e1b30e01a179f786dc4edb1eccbd7 +size 349 diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md deleted file mode 100644 index 4f4dd54756..0000000000 --- a/docs/concepts/glossary.md +++ /dev/null @@ -1,3 +0,0 @@ -# openpilot glossary - -{{GLOSSARY_DEFINITIONS}} diff --git a/docs/ext/glossary.py b/docs/ext/glossary.py deleted file mode 100644 index 9bbf3c78d7..0000000000 --- a/docs/ext/glossary.py +++ /dev/null @@ -1,216 +0,0 @@ -import posixpath -import re -import tomllib -import xml.etree.ElementTree as ET -from pathlib import Path - -from markdown.extensions import Extension -from markdown.preprocessors import Preprocessor -from markdown.treeprocessors import Treeprocessor - -from zensical.extensions.links import LinksTreeprocessor - -GlossaryTerm = tuple[str, re.Pattern[str], str] - -GLOSSARY_FILE = Path(__file__).with_name("glossary.toml") -GLOSSARY_PAGE = "concepts/glossary.md" -GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}" - -SKIP_TAGS = { - "a", - "code", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "kbd", - "pre", - "script", - "style", -} - -def clean_tooltip(description: str) -> str: - text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description) - text = re.sub(r"`([^`]+)`", r"\1", text) - text = re.sub(r"[*_~]", "", text) - return re.sub(r"\s+", " ", text).strip() - - -def load_glossary() -> tuple[list[GlossaryTerm], str]: - with GLOSSARY_FILE.open("rb") as f: - glossary_data = tomllib.load(f).get("glossary", {}) - - glossary: list[GlossaryTerm] = [] - rendered = [] - for key, value in glossary_data.items(): - label = str(key).strip().replace("_", " ") - description = str(value).strip() - if not description: - continue - - slug = label.replace(" ", "-").replace("_", "-").lower() - glossary.append((slug, re.compile(rf"(?**{label}**: {description}') - - return glossary, "\n".join(rendered) - - -class GlossaryPreprocessor(Preprocessor): - def __init__(self, md, glossary: str): - super().__init__(md) - self.glossary = glossary - - def run(self, lines: list[str]) -> list[str]: - markdown = "\n".join(lines) - if GLOSSARY_PLACEHOLDER not in markdown: - return lines - return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines() - - -class GlossaryTreeprocessor(Treeprocessor): - def __init__(self, md, glossary: list[GlossaryTerm]): - super().__init__(md) - self.glossary = glossary - self.seen: set[str] = set() - - def run(self, root: ET.Element) -> None: - at = self.md.treeprocessors.get_index_for_name("zrelpath") - processor = self.md.treeprocessors[at] - if not isinstance(processor, LinksTreeprocessor): - raise TypeError("Links processor not registered") - if processor.path == GLOSSARY_PAGE: - return - - self.seen.clear() - glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#" - self._walk(root, glossary_href) - - def _walk(self, element: ET.Element, glossary_href: str) -> None: - if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None: - return - - self._replace(element, glossary_href) - - idx = 0 - while idx < len(element): - child = element[idx] - self._walk(child, glossary_href) - idx = self._replace(element, glossary_href, idx) + 1 - - def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int: - child = None if index is None else parent[index] - text = parent.text if child is None else child.tail - pieces = self._pieces(text or "", glossary_href) - if not pieces: - return -1 if index is None else index - - if child is None: - parent.text = pieces[0] if isinstance(pieces[0], str) else "" - # Insert replacements for parent.text before the first existing child. - insert_at = -1 - else: - assert index is not None - child.tail = pieces[0] if isinstance(pieces[0], str) else "" - insert_at = index - - start = 1 if isinstance(pieces[0], str) else 0 - previous = child - - for piece in pieces[start:]: - if isinstance(piece, str): - previous.tail = (previous.tail or "") + piece - continue - - insert_at += 1 - parent.insert(insert_at, piece) - previous = piece - - return insert_at - - def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]: - if not text.strip(): - return [] - - pieces: list[str | ET.Element] = [] - cursor = 0 - - while True: - best = None - for slug, pattern, tooltip in self.glossary: - if slug in self.seen: - continue - - found = pattern.search(text, cursor) - if found is None: - continue - - candidate = (slug, tooltip, found.start(), found.end()) - if best is None: - best = candidate - continue - - _, _, best_start, best_end = best - _, _, current_start, current_end = candidate - if current_start < best_start: - best = candidate - continue - - if current_start == best_start and current_end - current_start > best_end - best_start: - best = candidate - - if best is None: - break - - slug, tooltip, start, end = best - if start > cursor: - pieces.append(text[cursor:start]) - - link = ET.Element( - "a", - { - "class": "glossary-term", - "data-glossary-term": "", - "href": f"{glossary_href}{slug}", - }, - ) - ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end] - ET.SubElement( - link, - "span", - { - "class": "glossary-term__tooltip", - "data-search-exclude": "", - }, - ).text = tooltip - pieces.append(link) - self.seen.add(slug) - cursor = end - - if not pieces: - return [] - if cursor < len(text): - pieces.append(text[cursor:]) - return pieces - - -class GlossaryExtension(Extension): - def extendMarkdown(self, md) -> None: - md.registerExtension(self) - glossary, rendered = load_glossary() - - md.preprocessors.register( - GlossaryPreprocessor(md, rendered), - "docs-ext-glossary-preprocessor", - 27, - ) - md.treeprocessors.register( - GlossaryTreeprocessor(md, glossary), - "docs-ext-glossary-treeprocessor", - 0, - ) - - -def makeExtension(**kwargs) -> GlossaryExtension: - return GlossaryExtension(**kwargs) diff --git a/docs/ext/glossary.toml b/docs/ext/glossary.toml deleted file mode 100644 index 62408d9ddd..0000000000 --- a/docs/ext/glossary.toml +++ /dev/null @@ -1,8 +0,0 @@ -[glossary] -onroad = "openpilot's system state while ignition is on." -offroad = "openpilot's system state while ignition is off." -route = "A route is a recording of an onroad session." -segment = "Routes are split into one minute chunks called segments." -"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)." -panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)." -"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)." diff --git a/docs/serve.py b/docs/serve.py new file mode 100644 index 0000000000..dcc78f7b0e --- /dev/null +++ b/docs/serve.py @@ -0,0 +1,444 @@ +import argparse +import functools +import html +import http.server +import json +import posixpath +import re +import shutil +import threading +import time +import urllib.parse +from pathlib import Path + +DOCS_DIR = Path(__file__).resolve().parent +SITE_DIR = DOCS_DIR / "_site" +TEMPLATE_FILE = DOCS_DIR / "template.html" +EXCLUDE_DIRS = {"_site"} + +REPO_URL = "https://github.com/commaai/openpilot/" + +# (title, target) pairs. target is a page path or an absolute URL. +# A None target marks a section header. +NAV: list[tuple[str, str | None]] = [ + ("What is openpilot?", "index.md"), + ("How-to", None), + ("Turn the speed blue", "how-to/turn-the-speed-blue.md"), + ("Connect to a comma 3X or four", "how-to/connect-to-comma.md"), + ("Add support for a car", "how-to/car-port.md"), + ("Concepts", None), + ("Logs", "concepts/logs.md"), + ("Safety", "concepts/safety.md"), + ("Glossary", "concepts/glossary.md"), + ("Contributing", None), + ("Feedback", "contributing/feedback.md"), + ("Roadmap", "contributing/roadmap.md"), + ("Contributing Guide →", "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md"), + ("Links", None), + ("Blog →", "https://blog.comma.ai"), + ("Bounties →", "https://comma.ai/bounties"), + ("GitHub →", "https://github.com/commaai"), + ("Discord →", "https://discord.comma.ai"), + ("X →", "https://x.com/comma_ai"), +] + +GLOSSARY_DESCRIPTIONS = { + "onroad": "openpilot's system state while ignition is on.", + "offroad": "openpilot's system state while ignition is off.", + "route": "A route is a recording of an onroad session.", + "segment": "Routes are split into one minute chunks called segments.", + "comma connect": "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).", + "panda": "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).", + "comma four": "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).", +} +GLOSSARY_PAGE = "concepts/glossary.md" +GLOSSARY_ROUTE = GLOSSARY_PAGE.removesuffix(".md") +GLOSSARY_SKIP = frozenset("a code h1 h2 h3 h4 h5 h6 kbd pre script style".split()) + +_ENTITY = re.compile(r"&(?:#x?[0-9a-fA-F]+|[a-zA-Z]+);") +_LIST = re.compile(r"^(\s*)([*+-]|\d+\.)\s+(.*)$") +_HEADING = re.compile(r"^(#{1,6})\s+(.*)$") +_HR = re.compile(r"^(-{3,}|\*{3,}|_{3,})$") +_ATTR_URL = re.compile(r"""(?P
\b(?:href|src)=(?P["']))(?P.*?)(?P=q)""")
+_VOID = frozenset("br img hr meta link input".split())
+
+def page_route(path: str) -> str:
+  path = path.removesuffix(".md")
+  return posixpath.dirname(path) or "." if posixpath.basename(path) == "index" else path
+
+def page_href(current: str, target: str) -> str:
+  route = posixpath.relpath(page_route(target), page_route(current))
+  return ("." if route == "." else route) + "/"
+
+def rewrite_relative_url(value: str, page: str) -> str | None:
+  url = urllib.parse.urlparse(value)
+  if value.startswith(("#", "/")) or url.scheme or url.netloc or not url.path:
+    return None
+  target = posixpath.normpath(posixpath.join(posixpath.dirname(page), url.path))
+  if target == ".." or target.startswith("../"):
+    return None
+  path = page_href(page, target) if target.endswith(".md") else posixpath.relpath(target, page_route(page))
+  return url._replace(path=path).geturl()
+
+def rewrite_html_urls(fragment: str, page: str) -> str:
+  def repl(m: re.Match[str]) -> str:
+    r = rewrite_relative_url(m.group("url"), page)
+    return m.group(0) if r is None else f'{m.group("pre")}{r}{m.group("q")}'
+  return _ATTR_URL.sub(repl, fragment)
+
+def esc(text: str, attr: bool = False) -> str:
+  held: list[str] = []
+  def hold(m: re.Match[str]) -> str:
+    held.append(m.group(0))
+    return f"\0{len(held) - 1}\0"
+  return re.sub(r"\0(\d+)\0", lambda m: held[int(m.group(1))], html.escape(_ENTITY.sub(hold, text), quote=attr))
+
+def clean_tooltip(description: str) -> str:
+  text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
+  return re.sub(r"\s+", " ", re.sub(r"[*_~]", "", re.sub(r"`([^`]+)`", r"\1", text))).strip()
+
+def glossary_slug(label: str) -> str:
+  return label.replace(" ", "-").replace("_", "-").lower()
+
+GLOSSARY_TERMS = [
+  (glossary_slug(l), re.compile(rf"(?**{l}**: {d}' for l, d in GLOSSARY_DESCRIPTIONS.items()
+)
+
+def inject_glossary(body: str, page: str) -> str:
+  if page == GLOSSARY_PAGE:
+    return body
+  route = "." if page == "index.md" else page.removesuffix(".md")
+  base, seen, out, skip, depth = f"{posixpath.relpath(GLOSSARY_ROUTE, route)}/#", set(), [], None, 0
+  for part in re.split(r"(<[^>]+>)", body):
+    if not part:
+      continue
+    if part.startswith("<"):
+      out.append(part)
+      if part.startswith("") or tag in _VOID
+      if closing and skip == tag and depth:
+        depth -= 1
+        skip = None if not depth else skip
+      elif not closing and not void:
+        skip, depth = (tag, 1) if skip is None else (skip, depth + (skip == tag))
+      continue
+    if depth:
+      out.append(part); continue
+    cur, text = 0, part
+    while True:
+      best = None
+      for order, (slug, pat, tip) in enumerate(GLOSSARY_TERMS):
+        if slug in seen or (found := pat.search(text, cur)) is None:
+          continue
+        cand = (found.start(), found.start() - found.end(), order, slug, tip, found.end(), found.group(0))
+        if best is None or cand[:3] < best[:3]:
+          best = cand
+      if best is None:
+        out.append(text[cur:]); break
+      start, _, _, slug, tip, end, matched = best
+      out.append(text[cur:start])
+      out.append(
+        f''
+        f'{matched}'
+        f'{esc(tip)}'
+      )
+      seen.add(slug); cur = end
+  return "".join(out)
+
+def slugify(text: str) -> str:
+  text = html.unescape(re.sub(r"<[^>]+>", "", text)).lower()
+  return re.sub(r"[-\s]+", "-", re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE)).strip("-")
+
+def _parse_link(text: str, start: int) -> tuple[str, str, int] | None:
+  if start >= len(text) or text[start] != "[":
+    return None
+  depth, i = 0, start
+  while i < len(text):
+    depth += (text[i] == "[") - (text[i] == "]")
+    if text[i] == "]" and depth == 0:
+      label = text[start + 1:i]
+      if i + 1 >= len(text) or text[i + 1] != "(":
+        return None
+      j, dp = i + 2, 1
+      while j < len(text) and dp:
+        dp += (text[j] == "(") - (text[j] == ")"); j += 1
+      return None if dp else (label, text[i + 2:j - 1], j)
+    i += 1
+  return None
+
+def render_inline(text: str, page: str) -> str:
+  out, i, n = [], 0, len(text)
+  while i < n:
+    if text[i] == "\n" and i >= 2 and text[i-2:i] == "  " and out and out[-1].endswith("  "):
+      out[-1] = out[-1][:-2]; out.append("
\n"); i += 1; continue + if text[i] == "`" and (end := text.find("`", i + 1)) != -1: + out.append(f"{esc(text[i+1:end])}"); i = end + 1; continue + if text[i] == "!" and i + 1 < n and text[i+1] == "[" and (p := _parse_link(text, i + 1)): + label, url, end = p + src = rewrite_relative_url(url, page) or url + out.append(f'{esc(label, True)}'); i = end; continue + if text[i] == "[" and (p := _parse_link(text, i)): + label, url, end = p + href = rewrite_relative_url(url, page) or url + out.append(f'{render_inline(label, page)}'); i = end; continue + if text[i] == "<": + if text.startswith("", i + 4); end = n if end < 0 else end + 3 + out.append(rewrite_html_urls(text[i:end], page)); i = end; continue + if m := re.match(r"<[^>]+>", text[i:]): + out.append(rewrite_html_urls(m.group(0), page)); i += len(m.group(0)); continue + if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i:i+2], i+2)) != -1: + out.append(f"{render_inline(text[i+2:end], page)}"); i = end + 2; continue + if text[i] in "*_" and i + 1 < n and text[i+1] not in " \t\n" and (end := text.find(text[i], i+1)) > i + 1: + out.append(f"{render_inline(text[i+1:end], page)}"); i = end + 1; continue + j = i + 1 + while j < n and text[j] not in "`[ list[str]: + return [c.strip() for c in line.strip().removeprefix("|").removesuffix("|").split("|")] + +def _is_sep(line: str) -> bool: + return "|" in line and all(re.fullmatch(r":?-{3,}:?", c.strip()) for c in _trow(line)) + +def _align(sep: str) -> str: + s = sep.strip() + if s.startswith(":") and s.endswith(":"): return ' style="text-align: center;"' + if s.endswith(":"): return ' style="text-align: right;"' + if s.startswith(":"): return ' style="text-align: left;"' + return "" + +def _list_info(line: str) -> tuple[int, str, str] | None: + m = _LIST.match(line) + return None if not m else (len(m.group(1)) // 4, "ol" if m.group(2)[-1] == "." else "ul", m.group(3)) + +def render_markdown(text: str, page: str) -> str: + lines, out, i, n = text.splitlines(), [], 0, 0 + n = len(lines) + while i < n: + line, s = lines[i], lines[i].strip() + if not s: + i += 1; continue + + if s.startswith("```"): + lang, body = s[3:].strip(), [] + i += 1 + while i < n and not lines[i].strip().startswith("```"): + body.append(lines[i]); i += 1 + if i < n: i += 1 + code, cls = html.escape("\n".join(body) + ("\n" if body else "")), (f' class="language-{html.escape(lang)}"' if lang else "") + out.append(f"
{code}
"); continue + + if m := _HEADING.match(s): + content, level = m.group(2).rstrip("#").strip(), len(m.group(1)) + sid = slugify(content) + out.append(f'{render_inline(content, page)}#') + i += 1; continue + + if _HR.fullmatch(s): + out.append("
"); i += 1; continue + + if "|" in line and i + 1 < n and _is_sep(lines[i + 1]): + headers, aligns = _trow(line), [_align(c) for c in _trow(lines[i + 1])] + i += 2; rows = [] + while i < n and "|" in lines[i] and lines[i].strip(): + rows.append(_trow(lines[i])); i += 1 + parts = ["", "", ""] + [ + f"{render_inline(h, page)}" for j, h in enumerate(headers) + ] + ["", "", ""] + for row in rows: + parts.append("") + for j in range(len(headers)): + parts.append(f"{render_inline(row[j] if j < len(row) else '', page)}") + parts.append("") + out.append("\n".join(parts + ["", "
"])); continue + + if _list_info(line): + items: list[tuple[int, str, list[str]]] = [] + while i < n: + if not lines[i].strip(): + if i + 1 < n and _list_info(lines[i + 1]): + i += 1; continue + break + info = _list_info(lines[i]) + if not info: break + level, kind, body = info + chunk = [body]; i += 1 + while i < n and lines[i].strip() and _list_info(lines[i]) is None: + t = lines[i].strip() + if t.startswith("```") or _HEADING.match(t) or _HR.fullmatch(t): break + chunk.append(lines[i]); i += 1 + items.append((level, kind, chunk)) + + def render_list(start: int, min_level: int) -> tuple[str, int]: + if start >= len(items) or items[start][0] < min_level: + return "", start + kind, chunks, idx = items[start][1], [f"<{items[start][1]}>"], start + while idx < len(items) and items[idx][0] >= min_level: + level, ikind, body_lines = items[idx] + if level > min_level: + nested, idx = render_list(idx, level) + chunks[-1] = (chunks[-1][:-5] + nested + "") if chunks[-1].endswith("") else chunks[-1] + nested + continue + if ikind != kind: + chunks += [f"", f"<{ikind}>"]; kind = ikind + idx += 1 + body = render_inline("\n".join(body_lines), page) + nested = "" + if idx < len(items) and items[idx][0] > min_level: + nested, idx = render_list(idx, min_level + 1) + chunks.append(f"
  • {body}{nested}\n
  • " if nested else f"
  • {body}
  • ") + chunks.append(f"") + return "\n".join(chunks), idx + + out.append(render_list(0, items[0][0])[0]); continue + + if s.startswith(">"): + q = [] + while i < n and lines[i].strip().startswith(">"): + q.append(re.sub(r"^>\s?", "", lines[i].strip())); i += 1 + out.append(f"
    \n

    {render_inline(chr(10).join(q), page)}

    \n
    "); continue + + if s.startswith("", i + 4); end = n if end < 0 else end + 3 - out.append(rewrite_html_urls(text[i:end], page)); i = end; continue + end = text.find("-->", i + 4) + end = n if end < 0 else end + 3 + out.append(rewrite_html_urls(text[i:end], page)) + i = end + continue if m := re.match(r"<[^>]+>", text[i:]): - out.append(rewrite_html_urls(m.group(0), page)); i += len(m.group(0)); continue - if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i:i+2], i+2)) != -1: - out.append(f"{render_inline(text[i+2:end], page)}"); i = end + 2; continue - if text[i] in "*_" and i + 1 < n and text[i+1] not in " \t\n" and (end := text.find(text[i], i+1)) > i + 1: - out.append(f"{render_inline(text[i+1:end], page)}"); i = end + 1; continue + out.append(rewrite_html_urls(m.group(0), page)) + i += len(m.group(0)) + continue + if (text.startswith("**", i) or text.startswith("__", i)) and (end := text.find(text[i : i + 2], i + 2)) != -1: + out.append(f"{render_inline(text[i + 2 : end], page)}") + i = end + 2 + continue + if text[i] in "*_" and i + 1 < n and text[i + 1] not in " \t\n" and (end := text.find(text[i], i + 1)) > i + 1: + out.append(f"{render_inline(text[i + 1 : end], page)}") + i = end + 1 + continue j = i + 1 while j < n and text[j] not in "`[ list[str]: return [c.strip() for c in line.strip().removeprefix("|").removesuffix("|").split("|")] + def _is_sep(line: str) -> bool: return "|" in line and all(re.fullmatch(r":?-{3,}:?", c.strip()) for c in _trow(line)) + def _align(sep: str) -> str: s = sep.strip() - if s.startswith(":") and s.endswith(":"): return ' style="text-align: center;"' - if s.endswith(":"): return ' style="text-align: right;"' - if s.startswith(":"): return ' style="text-align: left;"' + if s.startswith(":") and s.endswith(":"): + return ' style="text-align: center;"' + if s.endswith(":"): + return ' style="text-align: right;"' + if s.startswith(":"): + return ' style="text-align: left;"' return "" + def _list_info(line: str) -> tuple[int, str, str] | None: m = _LIST.match(line) return None if not m else (len(m.group(1)) // 4, "ol" if m.group(2)[-1] == "." else "ul", m.group(3)) -def render_markdown(text: str, page: str) -> str: + +def _render_blocks(text: str, page: str) -> str: lines, out, i, n = text.splitlines(), [], 0, 0 n = len(lines) while i < n: line, s = lines[i], lines[i].strip() if not s: - i += 1; continue + i += 1 + continue if s.startswith("```"): lang, body = s[3:].strip(), [] i += 1 while i < n and not lines[i].strip().startswith("```"): - body.append(lines[i]); i += 1 - if i < n: i += 1 - code, cls = html.escape("\n".join(body) + ("\n" if body else "")), (f' class="language-{html.escape(lang)}"' if lang else "") - out.append(f"
    {code}
    "); continue + body.append(lines[i]) + i += 1 + if i < n: + i += 1 + code = html.escape("\n".join(body) + ("\n" if body else "")) + cls = f' class="language-{html.escape(lang)}"' if lang else "" + out.append(f"
    {code}
    ") + continue if m := _HEADING.match(s): content, level = m.group(2).rstrip("#").strip(), len(m.group(1)) sid = slugify(content) out.append(f'{render_inline(content, page)}#') - i += 1; continue + i += 1 + continue if _HR.fullmatch(s): - out.append("
    "); i += 1; continue + out.append("
    ") + i += 1 + continue if "|" in line and i + 1 < n and _is_sep(lines[i + 1]): headers, aligns = _trow(line), [_align(c) for c in _trow(lines[i + 1])] - i += 2; rows = [] + i += 2 + rows = [] while i < n and "|" in lines[i] and lines[i].strip(): - rows.append(_trow(lines[i])); i += 1 - parts = ["", "", ""] + [ - f"{render_inline(h, page)}" for j, h in enumerate(headers) - ] + ["", "", ""] + rows.append(_trow(lines[i])) + i += 1 + parts = ( + ["
    ", "", ""] + + [f"{render_inline(h, page)}" for j, h in enumerate(headers)] + + ["", "", ""] + ) for row in rows: parts.append("") for j in range(len(headers)): parts.append(f"{render_inline(row[j] if j < len(row) else '', page)}") parts.append("") - out.append("\n".join(parts + ["", "
    "])); continue + out.append("\n".join(parts + ["", ""])) + continue if _list_info(line): items: list[tuple[int, str, list[str]]] = [] while i < n: if not lines[i].strip(): if i + 1 < n and _list_info(lines[i + 1]): - i += 1; continue + i += 1 + continue break info = _list_info(lines[i]) - if not info: break + if not info: + break level, kind, body = info - chunk = [body]; i += 1 + chunk = [body] + i += 1 while i < n and lines[i].strip() and _list_info(lines[i]) is None: t = lines[i].strip() - if t.startswith("```") or _HEADING.match(t) or _HR.fullmatch(t): break - chunk.append(lines[i]); i += 1 + if t.startswith(("```", ">")) or _HEADING.match(t) or _HR.fullmatch(t): + break + chunk.append(lines[i]) + i += 1 items.append((level, kind, chunk)) - def render_list(start: int, min_level: int) -> tuple[str, int]: + def render_list(items: list[tuple[int, str, list[str]]], start: int, min_level: int) -> tuple[str, int]: if start >= len(items) or items[start][0] < min_level: return "", start kind, chunks, idx = items[start][1], [f"<{items[start][1]}>"], start while idx < len(items) and items[idx][0] >= min_level: level, ikind, body_lines = items[idx] if level > min_level: - nested, idx = render_list(idx, level) + nested, idx = render_list(items, idx, level) chunks[-1] = (chunks[-1][:-5] + nested + "") if chunks[-1].endswith("") else chunks[-1] + nested continue if ikind != kind: - chunks += [f"", f"<{ikind}>"]; kind = ikind + chunks += [f"", f"<{ikind}>"] + kind = ikind idx += 1 body = render_inline("\n".join(body_lines), page) nested = "" if idx < len(items) and items[idx][0] > min_level: - nested, idx = render_list(idx, min_level + 1) + nested, idx = render_list(items, idx, min_level + 1) chunks.append(f"
  • {body}{nested}\n
  • " if nested else f"
  • {body}
  • ") chunks.append(f"") return "\n".join(chunks), idx - out.append(render_list(0, items[0][0])[0]); continue + out.append(render_list(items, 0, items[0][0])[0]) + continue if s.startswith(">"): q = [] while i < n and lines[i].strip().startswith(">"): - q.append(re.sub(r"^>\s?", "", lines[i].strip())); i += 1 - out.append(f"
    \n

    {render_inline(chr(10).join(q), page)}

    \n
    "); continue + q.append(re.sub(r"^>\s?", "", lines[i].strip())) + i += 1 + m = _ADMONITION.match(q[0].strip()) if q else None + if m: + kind = m.group(1).lower() + title = m.group(1).capitalize() + body = _render_blocks("\n".join(q[1:]), page) + out.append(f'
    \n

    {title}

    \n{body}\n
    ') + else: + out.append(f"
    \n

    {render_inline(chr(10).join(q), page)}

    \n
    ") + continue - if s.startswith(" - - - - - - - - - - - From 96ca1f8ed54354a6b7bdff4f3680e6cb5c48c70e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 13:10:32 -0700 Subject: [PATCH 176/325] ui: load fonts on the fly (#38567) --- openpilot/selfdrive/assets/fonts/process.py | 136 -------------------- openpilot/selfdrive/ui/SConscript | 17 +-- openpilot/system/ui/lib/application.py | 33 +++-- 3 files changed, 23 insertions(+), 163 deletions(-) delete mode 100755 openpilot/selfdrive/assets/fonts/process.py diff --git a/openpilot/selfdrive/assets/fonts/process.py b/openpilot/selfdrive/assets/fonts/process.py deleted file mode 100755 index 62ae6ec684..0000000000 --- a/openpilot/selfdrive/assets/fonts/process.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path -import json - -import pyray as rl - -FONT_DIR = Path(__file__).resolve().parent -SELFDRIVE_DIR = FONT_DIR.parents[1] -TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations" -LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json" - -GLYPH_PADDING = 6 -EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥" -UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"} - - -def _languages(): - if not LANGUAGES_FILE.exists(): - return {} - with LANGUAGES_FILE.open(encoding="utf-8") as f: - return json.load(f) - - -def _char_sets(): - base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS) - unifont = set(base) - - for language, code in _languages().items(): - unifont.update(language) - po_path = TRANSLATIONS_DIR / f"app_{code}.po" - try: - chars = set(po_path.read_text(encoding="utf-8")) - except FileNotFoundError: - continue - (unifont if code in UNIFONT_LANGUAGES else base).update(chars) - - return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont)) - - -def _glyph_metrics(glyphs, rects, glyph_count: int): - entries = [] - min_offset_y, max_extent = None, 0 - for idx in range(glyph_count): - glyph = glyphs[idx] - rect = rects[idx] - width = int(round(rect.width)) - height = int(round(rect.height)) - offset_y = int(round(glyph.offsetY)) - min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y) - max_extent = max(max_extent, offset_y + height) - entries.append({ - "id": glyph.value, - "x": int(round(rect.x)), - "y": int(round(rect.y)), - "width": width, - "height": height, - "xoffset": int(round(glyph.offsetX)), - "yoffset": offset_y, - "xadvance": int(round(glyph.advanceX)), - }) - - if min_offset_y is None: - raise RuntimeError("No glyphs were generated") - - line_height = int(round(max_extent - min_offset_y)) - base = int(round(max_extent)) - return entries, line_height, base - - -def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries): - # TODO: why doesn't raylib calculate these metrics correctly? - if line_height != font_size: - print("using font size for line height", atlas_name) - line_height = font_size - lines = [ - f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0", - f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4", - f"page id=0 file=\"{atlas_name}\"", - f"chars count={len(entries)}", - ] - for entry in entries: - lines.append( - ("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " + - "xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry) - ) - path.write_text("\n".join(lines) + "\n") - - -def _process_font(font_path: Path, codepoints: tuple[int, ...]): - print(f"Processing {font_path.name}...") - - font_size = { - "unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph - }.get(font_path.name, 200) - - data = font_path.read_bytes() - file_buf = rl.ffi.new("unsigned char[]", data) - cp_buffer = rl.ffi.new("int[]", codepoints) - cp_ptr = rl.ffi.cast("int *", cp_buffer) - glyph_count = rl.ffi.new("int *", len(codepoints)) - glyphs = rl.load_font_data( - rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), - rl.FontType.FONT_DEFAULT, glyph_count - ) - if glyphs == rl.ffi.NULL: - raise RuntimeError("raylib failed to load font data") - - rects_ptr = rl.ffi.new("Rectangle **") - image = rl.gen_image_font_atlas(glyphs, rects_ptr, glyph_count[0], font_size, GLYPH_PADDING, 0) - if image.width == 0 or image.height == 0: - raise RuntimeError("raylib returned an empty atlas") - - rects = rects_ptr[0] - atlas_name = f"{font_path.stem}.png" - atlas_path = FONT_DIR / atlas_name - entries, line_height, base = _glyph_metrics(glyphs, rects, glyph_count[0]) - - if not rl.export_image(image, atlas_path.as_posix()): - raise RuntimeError("Failed to export atlas image") - - _write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries) - - -def main(): - base_cp, unifont_cp = _char_sets() - fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf")) - for font in fonts: - if "emoji" in font.name.lower(): - continue - glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp - _process_font(font, glyphs) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/openpilot/selfdrive/ui/SConscript b/openpilot/selfdrive/ui/SConscript index d187b4ca3c..2b02759687 100644 --- a/openpilot/selfdrive/ui/SConscript +++ b/openpilot/selfdrive/ui/SConscript @@ -1,23 +1,8 @@ -from pathlib import Path import importlib.util +from pathlib import Path Import('env', 'arch', 'common') -# build the fonts -generator = File("#openpilot/selfdrive/assets/fonts/process.py") -source_files = Glob("#openpilot/selfdrive/assets/fonts/*.ttf") + Glob("#openpilot/selfdrive/assets/fonts/*.otf") -output_files = [ - (f"#{Path(f.path).with_suffix('.fnt')}", f"#{Path(f.path).with_suffix('.png')}") - for f in source_files - if "NotoColor" not in f.name -] -env.Command( - target=output_files, - source=[generator, source_files], - action=f"python3 {generator}", -) - - if GetOption('extras') and arch == "larch64": # build installers raylib_dir = Path(importlib.util.find_spec("raylib").submodule_search_locations[0]) / "install" diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index 3fa47ebd20..e294b02eaf 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -19,7 +19,7 @@ from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog from openpilot.common.hardware import HARDWARE, PC -from openpilot.system.ui.lib.multilang import multilang +from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, UNIFONT_LANGUAGES, multilang from openpilot.common.realtime import Ratekeeper _DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60))) @@ -92,19 +92,20 @@ FONT_SCALE = 1.242 if BIG_UI else 1.16 ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") +EXTRA_FONT_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥" class FontWeight(StrEnum): - NORMAL = "Inter-Regular.fnt" if BIG_UI else "Inter-Medium.fnt" - MEDIUM = "Inter-Medium.fnt" - BOLD = "Inter-Bold.fnt" - SEMI_BOLD = "Inter-SemiBold.fnt" - UNIFONT = "unifont.fnt" + NORMAL = "Inter-Regular.ttf" if BIG_UI else "Inter-Medium.ttf" + MEDIUM = "Inter-Medium.ttf" + BOLD = "Inter-Bold.ttf" + SEMI_BOLD = "Inter-SemiBold.ttf" + UNIFONT = "unifont.otf" # Small UI fonts - DISPLAY_REGULAR = "Inter-Regular.fnt" - ROMAN = "Inter-Regular.fnt" - DISPLAY = "Inter-Bold.fnt" + DISPLAY_REGULAR = "Inter-Regular.ttf" + ROMAN = "Inter-Regular.ttf" + DISPLAY = "Inter-Bold.ttf" def font_fallback(font: rl.Font) -> rl.Font: @@ -684,10 +685,20 @@ class GuiApplication: return self._height def _load_fonts(self): + base_chars = set(map(chr, range(32, 127))) | set(EXTRA_FONT_CHARS) + unifont_chars = set(base_chars) + for language, code in multilang.languages.items(): + unifont_chars.update(language) + chars = set(TRANSLATIONS_DIR.joinpath(f"app_{code}.po").read_text(encoding="utf-8")) + (unifont_chars if code in UNIFONT_LANGUAGES else base_chars).update(chars) + for font_weight_file in FontWeight: with as_file(FONT_DIR) as fspath: - fnt_path = fspath / font_weight_file - font = rl.load_font(fnt_path.as_posix()) + unifont = font_weight_file == FontWeight.UNIFONT + codepoints = sorted(map(ord, unifont_chars if unifont else base_chars)) + codepoint_buffer = rl.ffi.new("int[]", codepoints) + font = rl.load_font_ex((fspath / font_weight_file).as_posix(), 16 if unifont else 200, + rl.ffi.cast("int *", codepoint_buffer), len(codepoints)) if font_weight_file != FontWeight.UNIFONT: rl.gen_texture_mipmaps(font.texture) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR) From e6939dbedeb12e12cfe6b2863d09d043c8722b45 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 13:19:02 -0700 Subject: [PATCH 177/325] inline modem serial support (#38564) --- openpilot/common/hardware/tici/modem.py | 56 +++++++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/openpilot/common/hardware/tici/modem.py b/openpilot/common/hardware/tici/modem.py index 54251d4815..171ed8c0e4 100755 --- a/openpilot/common/hardware/tici/modem.py +++ b/openpilot/common/hardware/tici/modem.py @@ -3,17 +3,19 @@ import fcntl import json import logging import os +import select import signal +import struct import subprocess import tempfile +import termios import time +from contextlib import contextmanager from ipaddress import IPv4Address, AddressValueError from enum import Enum -from openpilot.common.serial import Serial - logging.basicConfig( level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)-7s modem: %(message)s", @@ -64,6 +66,39 @@ INITIAL_STATE: dict[str, object] = { } +@contextmanager +def _serial_port(port: str, baudrate: int): + fd = os.open(port, os.O_RDWR | os.O_NOCTTY) + try: + attrs = termios.tcgetattr(fd) + attrs[0] = 0 + attrs[1] = 0 + attrs[2] = termios.CLOCAL | termios.CREAD | termios.CS8 + attrs[3] = 0 + attrs[4] = attrs[5] = getattr(termios, f"B{baudrate}") + attrs[6][termios.VMIN] = 0 + attrs[6][termios.VTIME] = 0 + termios.tcsetattr(fd, termios.TCSANOW, attrs) + yield fd + finally: + os.close(fd) + + +def _read_line(fd: int, timeout: float) -> bytes: + data = bytearray() + deadline = time.monotonic() + timeout + while True: + readable, _, _ = select.select([fd], [], [], max(0.0, deadline - time.monotonic())) + if not readable: + return bytes(data) + byte = os.read(fd, 1) + if not byte: + return bytes(data) + data.extend(byte) + if byte == b"\n": + return bytes(data) + + class State(Enum): INITIALIZING = "INITIALIZING" SEARCHING = "SEARCHING" @@ -97,10 +132,11 @@ class PPPSession: def reset_data_port(): """Drop DTR on PPP_PORT so the modem terminates any stuck PPP session.""" try: - with Serial(PPP_PORT, baudrate=460800, timeout=1) as s: - s.dtr = False + with _serial_port(PPP_PORT, 460800) as fd: + dtr = struct.pack("I", termios.TIOCM_DTR) + fcntl.ioctl(fd, termios.TIOCMBIC, dtr) time.sleep(0.2) - s.dtr = True + fcntl.ioctl(fd, termios.TIOCMBIS, dtr) except Exception as e: logging.warning(f"data port reset failed: {e}") @@ -221,12 +257,14 @@ class Modem: os.close(fd) return [] try: - with Serial(AT_PORT, baudrate=9600, timeout=5) as ser: - ser.reset_input_buffer() - ser.write((cmd + "\r").encode()) + with _serial_port(AT_PORT, 9600) as serial_fd: + termios.tcflush(serial_fd, termios.TCIFLUSH) + command = (cmd + "\r").encode() + while command: + command = command[os.write(serial_fd, command):] lines = [] while True: - raw = ser.readline() + raw = _read_line(serial_fd, 5) if not raw: raise TimeoutError("AT timeout") line = raw.decode(errors="ignore").strip() From b77da00697d00728134f776a068298d674f246c7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 13:20:53 -0700 Subject: [PATCH 178/325] rm unused NotoColorEmoji.ttf --- openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf diff --git a/openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf b/openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf deleted file mode 100644 index 778e821ce3..0000000000 --- a/openpilot/selfdrive/assets/fonts/NotoColorEmoji.ttf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93cdc4ee9aa40e2afceecc63da0ca05ec7aab4bec991ece51a6b52389f48a477 -size 10788068 From 66b3590b637db36c059c176fd08f0c15870a6006 Mon Sep 17 00:00:00 2001 From: eFini Date: Sat, 8 Aug 2026 04:30:58 +0800 Subject: [PATCH 179/325] Use Noto fonts for Asian languages (#36514) ui: use Noto fonts for Asian languages Co-authored-by: Adeeb Shihadeh --- .../assets/fonts/NotoSansCJKjp-Regular.otf | 3 ++ .../assets/fonts/NotoSansCJKkr-Regular.otf | 3 ++ .../assets/fonts/NotoSansCJKsc-Regular.otf | 3 ++ .../assets/fonts/NotoSansCJKtc-Regular.otf | 3 ++ .../assets/fonts/NotoSansThai-Regular.ttf | 3 ++ openpilot/system/ui/lib/application.py | 40 ++++++++++++++++--- openpilot/system/ui/lib/multilang.py | 7 ++-- 7 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 openpilot/selfdrive/assets/fonts/NotoSansCJKjp-Regular.otf create mode 100644 openpilot/selfdrive/assets/fonts/NotoSansCJKkr-Regular.otf create mode 100644 openpilot/selfdrive/assets/fonts/NotoSansCJKsc-Regular.otf create mode 100644 openpilot/selfdrive/assets/fonts/NotoSansCJKtc-Regular.otf create mode 100644 openpilot/selfdrive/assets/fonts/NotoSansThai-Regular.ttf diff --git a/openpilot/selfdrive/assets/fonts/NotoSansCJKjp-Regular.otf b/openpilot/selfdrive/assets/fonts/NotoSansCJKjp-Regular.otf new file mode 100644 index 0000000000..045f73ba40 --- /dev/null +++ b/openpilot/selfdrive/assets/fonts/NotoSansCJKjp-Regular.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80135e7ab71496c3b744047ef9100f5d24edaa275d248a3e0488bc769e453baa +size 240500 diff --git a/openpilot/selfdrive/assets/fonts/NotoSansCJKkr-Regular.otf b/openpilot/selfdrive/assets/fonts/NotoSansCJKkr-Regular.otf new file mode 100644 index 0000000000..f06e31bae8 --- /dev/null +++ b/openpilot/selfdrive/assets/fonts/NotoSansCJKkr-Regular.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86fb33e127558b845aec3016d46c36118811a1e30fd9dacc374840ad3001221 +size 109356 diff --git a/openpilot/selfdrive/assets/fonts/NotoSansCJKsc-Regular.otf b/openpilot/selfdrive/assets/fonts/NotoSansCJKsc-Regular.otf new file mode 100644 index 0000000000..ccd161ef62 --- /dev/null +++ b/openpilot/selfdrive/assets/fonts/NotoSansCJKsc-Regular.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71933d4233b691fb499524cbf9b3d74ccdf6465c81791687e661667aefa8e660 +size 196068 diff --git a/openpilot/selfdrive/assets/fonts/NotoSansCJKtc-Regular.otf b/openpilot/selfdrive/assets/fonts/NotoSansCJKtc-Regular.otf new file mode 100644 index 0000000000..fec43d0c6f --- /dev/null +++ b/openpilot/selfdrive/assets/fonts/NotoSansCJKtc-Regular.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:292f8ad096fd0a47e4016bd3607cb4edcd3b1f97634f67a115c0b0973ba5002f +size 245884 diff --git a/openpilot/selfdrive/assets/fonts/NotoSansThai-Regular.ttf b/openpilot/selfdrive/assets/fonts/NotoSansThai-Regular.ttf new file mode 100644 index 0000000000..c0a05b3965 --- /dev/null +++ b/openpilot/selfdrive/assets/fonts/NotoSansThai-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8656eb9e4e182333e58c59cd07ae0dd82c913d31c29b0d2726887ffbbef137a2 +size 26536 diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index e294b02eaf..5e980937d7 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -19,7 +19,7 @@ from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog from openpilot.common.hardware import HARDWARE, PC -from openpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, UNIFONT_LANGUAGES, multilang +from openpilot.system.ui.lib.multilang import FONT_FALLBACK_LANGUAGES, TRANSLATIONS_DIR, multilang from openpilot.common.realtime import Ratekeeper _DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60))) @@ -93,6 +93,13 @@ FONT_SCALE = 1.242 if BIG_UI else 1.16 ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") EXTRA_FONT_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥" +NOTO_FONTS = { + "ja": "NotoSansCJKjp-Regular.otf", + "ko": "NotoSansCJKkr-Regular.otf", + "th": "NotoSansThai-Regular.ttf", + "zh-CHS": "NotoSansCJKsc-Regular.otf", + "zh-CHT": "NotoSansCJKtc-Regular.otf", +} class FontWeight(StrEnum): @@ -109,9 +116,9 @@ class FontWeight(StrEnum): def font_fallback(font: rl.Font) -> rl.Font: - """Fall back to unifont for languages that require it.""" - if multilang.requires_unifont(): - return gui_app.font(FontWeight.UNIFONT) + """Use a Noto fallback for languages not covered by Inter.""" + if multilang.requires_font_fallback(): + return gui_app.fallback_font() return font @@ -199,6 +206,7 @@ class GuiApplication: self._set_log_callback() self._fonts: dict[FontWeight, rl.Font] = {} + self._fallback_fonts: dict[str, rl.Font] = {} self._width = width if width is not None else GuiApplication._default_width() self._height = height if height is not None else GuiApplication._default_height() @@ -554,6 +562,9 @@ class GuiApplication: for font in self._fonts.values(): rl.unload_font(font) self._fonts = {} + for font in self._fallback_fonts.values(): + rl.unload_font(font) + self._fallback_fonts = {} if self._render_texture is not None: rl.unload_render_texture(self._render_texture) @@ -676,6 +687,21 @@ class GuiApplication: def font(self, font_weight: FontWeight = FontWeight.NORMAL) -> rl.Font: return self._fonts[font_weight] + def fallback_font(self) -> rl.Font: + language = multilang.language + if language not in self._fallback_fonts: + chars = set(map(chr, range(32, 127))) | set(EXTRA_FONT_CHARS) + chars.update(TRANSLATIONS_DIR.joinpath(f"app_{language}.po").read_text(encoding="utf-8")) + codepoints = sorted(map(ord, chars)) + codepoint_buffer = rl.ffi.new("int[]", codepoints) + with as_file(FONT_DIR) as fspath: + font = rl.load_font_ex((fspath / NOTO_FONTS[language]).as_posix(), 48, + rl.ffi.cast("int *", codepoint_buffer), len(codepoints)) + rl.gen_texture_mipmaps(font.texture) + rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR) + self._fallback_fonts[language] = font + return self._fallback_fonts[language] + @property def width(self): return self._width @@ -689,8 +715,8 @@ class GuiApplication: unifont_chars = set(base_chars) for language, code in multilang.languages.items(): unifont_chars.update(language) - chars = set(TRANSLATIONS_DIR.joinpath(f"app_{code}.po").read_text(encoding="utf-8")) - (unifont_chars if code in UNIFONT_LANGUAGES else base_chars).update(chars) + if code not in FONT_FALLBACK_LANGUAGES: + base_chars.update(TRANSLATIONS_DIR.joinpath(f"app_{code}.po").read_text(encoding="utf-8")) for font_weight_file in FontWeight: with as_file(FONT_DIR) as fspath: @@ -703,6 +729,8 @@ class GuiApplication: rl.gen_texture_mipmaps(font.texture) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR) self._fonts[font_weight_file] = font + if multilang.requires_font_fallback(): + self.fallback_font() rl.gui_set_font(self._fonts[FontWeight.NORMAL]) def _set_styles(self): diff --git a/openpilot/system/ui/lib/multilang.py b/openpilot/system/ui/lib/multilang.py index 3d786a7248..6bdf18f1cd 100644 --- a/openpilot/system/ui/lib/multilang.py +++ b/openpilot/system/ui/lib/multilang.py @@ -19,7 +19,7 @@ UI_DIR = files("openpilot.selfdrive.ui") TRANSLATIONS_DIR = UI_DIR.joinpath("translations") LANGUAGES_FILE = TRANSLATIONS_DIR.joinpath("languages.json") -UNIFONT_LANGUAGES = [ +FONT_FALLBACK_LANGUAGES = [ "th", "zh-CHT", "zh-CHS", @@ -164,9 +164,8 @@ class Multilang: def language(self) -> str: return self._language - def requires_unifont(self) -> bool: - """Certain languages require unifont to render their glyphs.""" - return self._language in UNIFONT_LANGUAGES + def requires_font_fallback(self) -> bool: + return self._language in FONT_FALLBACK_LANGUAGES def setup(self): try: From 0819f5c0fd3c608e3b2775d4fc5237f6dca47bee Mon Sep 17 00:00:00 2001 From: GavinnnK Date: Fri, 7 Aug 2026 16:34:11 -0400 Subject: [PATCH 180/325] common: clean up failed atomic writes (#38552) * common: clean up failed atomic writes * common: remove atomic write regression test --------- Co-authored-by: GavinnnK <239280171+GavinnnK@users.noreply.github.com> Co-authored-by: Adeeb Shihadeh --- openpilot/common/utils.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpilot/common/utils.py b/openpilot/common/utils.py index 22d024fca7..a73d2b8226 100644 --- a/openpilot/common/utils.py +++ b/openpilot/common/utils.py @@ -108,10 +108,14 @@ def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str if not overwrite and os.path.exists(path): raise FileExistsError(f"File '{path}' already exists. To overwrite it, set 'overwrite' to True.") - with tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) as tmp_file: - yield tmp_file - tmp_file_name = tmp_file.name - os.replace(tmp_file_name, path) + tmp_file = tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, dir=dir_name, delete=False) + try: + with tmp_file: + yield tmp_file + os.replace(tmp_file.name, path) + finally: + with contextlib.suppress(FileNotFoundError): + os.unlink(tmp_file.name) def get_upload_stream(filepath: str, should_compress: bool) -> tuple[io.BufferedIOBase, int]: From 5fadc71a3aa9f484abac2ded7476fa6bd2290e02 Mon Sep 17 00:00:00 2001 From: PeterPhuTran Date: Fri, 7 Aug 2026 13:37:57 -0700 Subject: [PATCH 181/325] swaglog: delete oldest logs on rollover, not newest (#38322) --- openpilot/common/swaglog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/common/swaglog.py b/openpilot/common/swaglog.py index 8b629b3fba..7a8c119c6b 100644 --- a/openpilot/common/swaglog.py +++ b/openpilot/common/swaglog.py @@ -45,7 +45,8 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler): fp = os.path.join(base_dir, fn) if fp.startswith(self.base_filename) and os.path.isfile(fp): log_files.append(fp) - return sorted(log_files) + # newest first, matching _open()'s insert(0, ...) so doRollover()'s pop() deletes the oldest + return sorted(log_files, reverse=True) def shouldRollover(self, record): size_exceeded = self.max_bytes > 0 and self.stream.tell() >= self.max_bytes From edd938105edbdd2d59ef4657f6393400b0275f84 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 14:11:12 -0700 Subject: [PATCH 182/325] remove old regen infra (#38570) --- .github/workflows/tests.yaml | 6 - .../selfdrive/test/process_replay/regen.py | 118 ------------------ .../test/process_replay/regen_all.py | 54 -------- tools/test_runner.py | 1 - 4 files changed, 179 deletions(-) delete mode 100755 openpilot/selfdrive/test/process_replay/regen.py delete mode 100755 openpilot/selfdrive/test/process_replay/regen_all.py diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 85475189f8..b8b1ace97a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -173,12 +173,6 @@ jobs: timeout_minutes: 2 max_attempts: 3 command: cd ${{ github.workspace }}/ci-artifacts && git push origin process-replay --force - - name: Run regen - if: false - timeout-minutes: 4 - env: - ONNXCPU: 1 - run: python openpilot/selfdrive/test/process_replay/test_regen.py simulator_driving: name: simulator driving diff --git a/openpilot/selfdrive/test/process_replay/regen.py b/openpilot/selfdrive/test/process_replay/regen.py deleted file mode 100755 index c501a4b250..0000000000 --- a/openpilot/selfdrive/test/process_replay/regen.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -import os -import argparse -import time -import capnp - -from typing import Any -from collections.abc import Iterable - -from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, FAKEDATA, ProcessConfig, replay_process, get_process_config, \ - check_openpilot_enabled, check_most_messages_valid, get_custom_params_from_lr -from openpilot.selfdrive.test.update_ci_routes import upload_route -from openpilot.tools.lib.framereader import FrameReader -from openpilot.tools.lib.logreader import LogReader, LogIterable, save_log -from openpilot.tools.lib.openpilotci import get_url - - -def regen_segment( - lr: LogIterable, frs: dict[str, Any] | None = None, - processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False -) -> list[capnp._DynamicStructReader]: - all_msgs = sorted(lr, key=lambda m: m.logMonoTime) - custom_params = get_custom_params_from_lr(all_msgs) - - print("Replayed processes:", [p.proc_name for p in processes]) - print("\n\n", "*"*30, "\n\n", sep="") - - output_logs = replay_process(processes, all_msgs, frs, return_all_logs=True, custom_params=custom_params, disable_progress=disable_tqdm) - - return output_logs - - -def setup_data_readers( - route: str, sidx: int, needs_driver_cam: bool = True, needs_road_cam: bool = True, dummy_driver_cam: bool = False -) -> tuple[LogReader, dict[str, Any]]: - lr = LogReader(f"{route}/{sidx}/r") - frs = {} - if needs_road_cam: - frs['roadCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) - if next((True for m in lr if m.which() == "wideRoadCameraState"), False): - frs['wideRoadCameraState'] = FrameReader(get_url(route, str(sidx), "ecamera.hevc")) - if needs_driver_cam: - if dummy_driver_cam: - frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "fcamera.hevc")) # Use fcam as dummy - else: - device_type = next(str(msg.initData.deviceType) for msg in lr if msg.which() == "initData") - assert device_type != "neo", "Driver camera not supported on neo segments. Use dummy dcamera." - frs['driverCameraState'] = FrameReader(get_url(route, str(sidx), "dcamera.hevc")) - - return lr, frs - - -def regen_and_save( - route: str, sidx: int, processes: str | Iterable[str] = "all", outdir: str = FAKEDATA, - upload: bool = False, disable_tqdm: bool = False, dummy_driver_cam: bool = False -) -> str: - if not isinstance(processes, str) and not hasattr(processes, "__iter__"): - raise ValueError("whitelist_proc must be a string or iterable") - - if processes != "all": - if isinstance(processes, str): - raise ValueError(f"Invalid value for processes: {processes}") - - replayed_processes = [] - for d in processes: - cfg = get_process_config(d) - replayed_processes.append(cfg) - else: - replayed_processes = CONFIGS - - all_vision_pubs = {pub for cfg in replayed_processes for pub in cfg.vision_pubs} - lr, frs = setup_data_readers(route, sidx, - needs_driver_cam="driverCameraState" in all_vision_pubs, - needs_road_cam="roadCameraState" in all_vision_pubs or "wideRoadCameraState" in all_vision_pubs, - dummy_driver_cam=dummy_driver_cam) - output_logs = regen_segment(lr, frs, replayed_processes, disable_tqdm=disable_tqdm) - - log_dir = os.path.join(outdir, time.strftime("%Y-%m-%d--%H-%M-%S--0", time.gmtime())) - rel_log_dir = os.path.relpath(log_dir) - rpath = os.path.join(log_dir, "rlog.zst") - - os.makedirs(log_dir) - save_log(rpath, output_logs, compress=True) - - print("\n\n", "*"*30, "\n\n", sep="") - print("New route:", rel_log_dir, "\n") - - if not check_openpilot_enabled(output_logs): - raise Exception("Route did not engage for long enough") - if not check_most_messages_valid(output_logs): - raise Exception("Route has too many invalid messages") - - if upload: - upload_route(rel_log_dir) - - return rel_log_dir - - -if __name__ == "__main__": - def comma_separated_list(string): - return string.split(",") - - all_procs = [p.proc_name for p in CONFIGS] - parser = argparse.ArgumentParser(description="Generate new segments from old ones") - parser.add_argument("--upload", action="store_true", help="Upload the new segment to the CI bucket") - parser.add_argument("--outdir", help="log output dir", default=FAKEDATA) - parser.add_argument("--dummy-dcamera", action='store_true', help="Use dummy blank driver camera") - parser.add_argument("--whitelist-procs", type=comma_separated_list, default=all_procs, - help="Comma-separated whitelist of processes to regen (e.g. controlsd,radard)") - parser.add_argument("--blacklist-procs", type=comma_separated_list, default=[], - help="Comma-separated blacklist of processes to regen (e.g. controlsd,radard)") - parser.add_argument("route", type=str, help="The source route") - parser.add_argument("seg", type=int, help="Segment in source route") - args = parser.parse_args() - - blacklist_set = set(args.blacklist_procs) - processes = [p for p in args.whitelist_procs if p not in blacklist_set] - regen_and_save(args.route, args.seg, processes=processes, upload=args.upload, outdir=args.outdir, dummy_driver_cam=args.dummy_dcamera) diff --git a/openpilot/selfdrive/test/process_replay/regen_all.py b/openpilot/selfdrive/test/process_replay/regen_all.py deleted file mode 100755 index 78a90b420c..0000000000 --- a/openpilot/selfdrive/test/process_replay/regen_all.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import concurrent.futures -import os -import random -import traceback -from tqdm import tqdm - -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.selfdrive.test.process_replay.regen import regen_and_save -from openpilot.selfdrive.test.process_replay.test_processes import FAKEDATA, source_segments as segments -from openpilot.tools.lib.route import SegmentName - - -def regen_job(segment, upload, disable_tqdm): - with OpenpilotPrefix(): - sn = SegmentName(segment[1]) - fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for _ in range(11)) - try: - relr = regen_and_save(sn.route_name.canonical_name, sn.segment_num, upload=upload, - outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm, dummy_driver_cam=True) - relr = '|'.join(relr.split('/')[-2:]) - return f' ("{segment[0]}", "{relr}"), ' - except Exception as e: - err = f" {segment} failed: {str(e)}" - err += traceback.format_exc() - err += "\n\n" - return err - - -if __name__ == "__main__": - all_cars = {car for car, _ in segments} - - parser = argparse.ArgumentParser(description="Generate new segments from old ones") - parser.add_argument("-j", "--jobs", type=int, default=1) - parser.add_argument("--no-upload", action="store_true") - parser.add_argument("--whitelist-cars", type=str, nargs="*", default=all_cars, - help="Whitelist given cars from the test (e.g. HONDA)") - parser.add_argument("--blacklist-cars", type=str, nargs="*", default=[], - help="Blacklist given cars from the test (e.g. HONDA)") - args = parser.parse_args() - - tested_cars = set(args.whitelist_cars) - set(args.blacklist_cars) - tested_cars = {c.upper() for c in tested_cars} - tested_segments = [(car, segment) for car, segment in segments if car in tested_cars] - - with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: - p = pool.map(regen_job, tested_segments, [not args.no_upload] * len(tested_segments), [args.jobs > 1] * len(tested_segments)) - msg = "Copy these new segments into test_processes.py:" - for seg in tqdm(p, desc="Generating segments", total=len(tested_segments)): - msg += "\n" + str(seg) - print() - print() - print(msg) diff --git a/tools/test_runner.py b/tools/test_runner.py index eb2f109f97..1979ce01e4 100755 --- a/tools/test_runner.py +++ b/tools/test_runner.py @@ -16,7 +16,6 @@ import warnings ROOT = Path(__file__).resolve().parents[1] IGNORED = ( ROOT / "openpilot/selfdrive/test/process_replay/test_processes.py", - ROOT / "openpilot/selfdrive/test/process_replay/test_regen.py", ROOT / "openpilot/tools/sim", ) FAILURES = {"failed", "error", "xpassed"} From 612d97cfdbae347394dc7f3e80139eea7b3a52ad Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 14:56:18 -0700 Subject: [PATCH 183/325] paramsd: don't learn in reverse gear (#38573) --- openpilot/selfdrive/locationd/paramsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/locationd/paramsd.py b/openpilot/selfdrive/locationd/paramsd.py index c1088a8180..9b5ff7143e 100755 --- a/openpilot/selfdrive/locationd/paramsd.py +++ b/openpilot/selfdrive/locationd/paramsd.py @@ -121,7 +121,7 @@ class VehicleParamsLearner: in_linear_region = abs(steering_angle) < 45 self.observed_speed = msg.vEgo - self.active = self.observed_speed > MIN_ACTIVE_SPEED and in_linear_region + self.active = self.observed_speed > MIN_ACTIVE_SPEED and in_linear_region and msg.gearShifter != car.CarState.GearShifter.reverse if self.active: self.kf.predict_and_observe(t, ObservationKind.STEER_ANGLE, np.array([[np.radians(steering_angle)]])) From 4eb515b32d9ab416a2e9cb894f49af6800eb58d9 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 7 Aug 2026 15:39:59 -0700 Subject: [PATCH 184/325] DM: Zoom Zoom Model (#38554) --- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx index bd3f2c85a6..87a116cdba 100644 --- a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e7b31dfbc0a5234f1baf196513b77fc6af12204b8a8ffe8ee0417e48352f316 -size 7494962 +oid sha256:607ed8f64a2d756657b621aaccf92a5cadb296b0a1d13d11605766901510cef1 +size 7486861 From 8a8860880086b8fa9f983364c07393f2498e7b54 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 15:41:28 -0700 Subject: [PATCH 185/325] rm qrcode (#38575) --- openpilot/common/qrcode.py | 218 ++++++++++++++++++ .../selfdrive/ui/mici/layouts/onboarding.py | 18 +- .../ui/mici/widgets/pairing_dialog.py | 20 +- .../selfdrive/ui/widgets/pairing_dialog.py | 20 +- pyproject.toml | 1 - uv.lock | 14 -- 6 files changed, 224 insertions(+), 67 deletions(-) create mode 100644 openpilot/common/qrcode.py diff --git a/openpilot/common/qrcode.py b/openpilot/common/qrcode.py new file mode 100644 index 0000000000..b6b345c227 --- /dev/null +++ b/openpilot/common/qrcode.py @@ -0,0 +1,218 @@ +"""Small QR encoder for the UI's byte-mode, error-correction-level-L codes.""" + +import numpy as np +import pyray as rl + + +# Indexes are QR versions. These are the only two Reed-Solomon parameters needed +# for error-correction level L. +_ECC_LEN = (0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28) +_NUM_BLOCKS = (0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8) + +# 15 format-info bits for level L (01) with mask 0: ((0x08 << 10) | bch_remainder) ^ 0x5412 +_FORMAT_BITS = 0b111011111000100 + + +def _raw_modules(version: int) -> int: + result = (16 * version + 128) * version + 64 + if version >= 2: + align = version // 7 + 2 + result -= (25 * align - 10) * align - 55 + return result - (36 if version >= 7 else 0) + + +def _capacity(version: int) -> int: + return _raw_modules(version) // 8 - _ECC_LEN[version] * _NUM_BLOCKS[version] + + +def _append_bits(bits: list[int], value: int, length: int) -> None: + bits.extend((value >> i) & 1 for i in range(length - 1, -1, -1)) + + +def _data_codewords(data: bytes, version: int) -> bytes: + """Byte-mode-encode the payload, terminated and padded to the version's capacity.""" + capacity = _capacity(version) + bits: list[int] = [] + _append_bits(bits, 4, 4) # byte mode + _append_bits(bits, len(data), 8 if version <= 9 else 16) + for value in data: + _append_bits(bits, value, 8) + bits.extend([0] * min(4, capacity * 8 - len(bits))) # terminator + bits.extend([0] * (-len(bits) % 8)) # byte alignment + result = bytearray(sum(bits[i + j] << (7 - j) for j in range(8)) for i in range(0, len(bits), 8)) + pad = (0xEC, 0x11) + while len(result) < capacity: + result.append(pad[(len(result) - (len(bits) // 8)) & 1]) + return bytes(result) + + +def _codewords(data: bytes, version: int) -> bytes: + """Split data codewords into Reed-Solomon blocks and interleave data + ECC.""" + data = _data_codewords(data, version) + num_blocks = _NUM_BLOCKS[version] + ecc_len = _ECC_LEN[version] + raw_codewords = _raw_modules(version) // 8 + short_len = raw_codewords // num_blocks + num_short = num_blocks - raw_codewords % num_blocks + divisor = _divisor(ecc_len) + blocks: list[tuple[bytes, bytes]] = [] + offset = 0 + for i in range(num_blocks): + length = short_len - ecc_len + (0 if i < num_short else 1) + block = data[offset:offset + length] + blocks.append((block, _remainder(block, divisor))) + offset += length + result = bytearray() + for i in range(short_len - ecc_len + 1): + for block, _ in blocks: + result.extend(block[i:i + 1]) + for i in range(ecc_len): + for _, ecc in blocks: + result.append(ecc[i]) + return bytes(result) + + +def _multiply(x: int, y: int) -> int: + result = 0 + for _ in range(8): + result = (result << 1) ^ (0x11D if result & 0x80 else 0) + if y & 0x80: + result ^= x + y <<= 1 + return result + + +def _divisor(degree: int) -> bytes: + result = bytearray([0] * (degree - 1) + [1]) + root = 1 + for _ in range(degree): + for j in range(degree): + result[j] = _multiply(result[j], root) + if j + 1 < degree: + result[j] ^= result[j + 1] + root = _multiply(root, 2) + return bytes(result) + + +def _remainder(data: bytes, divisor: bytes) -> bytes: + result = bytearray(len(divisor)) + for value in data: + factor = value ^ result.pop(0) + result.append(0) + for i, coefficient in enumerate(divisor): + result[i] ^= _multiply(coefficient, factor) + return bytes(result) + + +def _alignment_positions(version: int) -> list[int]: + if version == 1: + return [] + count = version // 7 + 2 + step = ((version * 4 + count * 2 + 1) // (count * 2 - 2)) * 2 + return [6] + [version * 4 + 10 - step * i for i in range(count - 1)][::-1] + + +class _Qr: + def __init__(self, version: int, data: bytes): + self.version = version + self.size = version * 4 + 17 + self.modules = [[False] * self.size for _ in range(self.size)] + self.function = [[False] * self.size for _ in range(self.size)] + self._draw_functions() + self._draw_data(_codewords(data, version)) + for y in range(self.size): + for x in range(self.size): + if not self.function[y][x]: + self.modules[y][x] ^= (x + y) % 2 == 0 + self._format() + + def _set_function(self, x: int, y: int, dark: bool) -> None: + if 0 <= x < self.size and 0 <= y < self.size: + self.modules[y][x] = dark + self.function[y][x] = True + + def _finder(self, x: int, y: int) -> None: + for dy in range(-4, 5): + for dx in range(-4, 5): + distance = max(abs(dx), abs(dy)) + self._set_function(x + dx, y + dy, distance != 2 and distance != 4) + + def _alignment(self, x: int, y: int) -> None: + for dy in range(-2, 3): + for dx in range(-2, 3): + self._set_function(x + dx, y + dy, max(abs(dx), abs(dy)) != 1) + + def _draw_functions(self) -> None: + for i in range(self.size): + self._set_function(6, i, i % 2 == 0) + self._set_function(i, 6, i % 2 == 0) + self._finder(3, 3) + self._finder(self.size - 4, 3) + self._finder(3, self.size - 4) + positions = _alignment_positions(self.version) + for y in positions: + for x in positions: + if not ((x == 6 and y in (6, self.size - 7)) or (x == self.size - 7 and y == 6)): + self._alignment(x, y) + # reserve the format-info modules before the data is placed; the real + # values are written by the second _format call after masking + self._format() + if self.version >= 7: + value = self.version + for _ in range(12): + value = (value << 1) ^ ((value >> 11) * 0x1F25) + value = self.version << 12 | value + for i in range(18): + bit = ((value >> i) & 1) != 0 + a = self.size - 11 + i % 3 + b = i // 3 + self._set_function(a, b, bit) + self._set_function(b, a, bit) + + def _format(self) -> None: + for i in range(15): + bit = ((_FORMAT_BITS >> i) & 1) != 0 + y_pos = i if i < 6 else i + 1 if i < 8 else self.size - 15 + i + self._set_function(8, y_pos, bit) + x_pos = self.size - 1 - i if i < 8 else 15 - i if i < 9 else 14 - i + self._set_function(x_pos, 8, bit) + self._set_function(8, self.size - 8, True) + + def _draw_data(self, data: bytes) -> None: + bits = ((byte >> s) & 1 for byte in data for s in reversed(range(8))) + upward = True + right = self.size - 1 + while right >= 1: + if right == 6: # skip the vertical timing column + right = 5 + for vert in range(self.size): + y = self.size - 1 - vert if upward else vert + for x in (right, right - 1): + if not self.function[y][x]: + self.modules[y][x] = bool(next(bits, 0)) + upward = not upward + right -= 2 + + +def make_texture(data: str, inverted: bool = False) -> rl.Texture: + """Render a URL as the RGBA QR texture used by the UI. The texture upload + copies the pixels, so the intermediate image/array don't need to outlive it.""" + raw = data.encode() + for version in range(1, 21): + count_bits = 8 if version <= 9 else 16 + if 4 + count_bits + len(raw) * 8 <= _capacity(version) * 8: + break + else: + raise ValueError("QR URL is too long") + modules = np.pad(_Qr(version, raw).modules, 0 if inverted else 4) + modules = np.repeat(np.repeat(modules, 10, axis=0), 10, axis=1) + gray = ((modules == inverted) * 255).astype(np.uint8) + img_array = np.dstack((gray, gray, gray, np.full_like(gray, 255))) + + rl_image = rl.Image() + rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data) + rl_image.width = img_array.shape[1] + rl_image.height = img_array.shape[0] + rl_image.mipmaps = 1 + rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 + return rl.load_texture_from_image(rl_image) diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py index 8d80df3a89..f1ebd3bacd 100644 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -1,9 +1,9 @@ import math import numpy as np -import qrcode import pyray as rl from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.qrcode import make_texture from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import SmallCircleIconButton @@ -284,21 +284,7 @@ class QRCodeWidget(Widget): self._generate_qr(url) def _generate_qr(self, url: str): - qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0) - qr.add_data(url) - qr.make(fit=True) - - pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA') - img_array = np.array(pil_img, dtype=np.uint8) - - rl_image = rl.Image() - rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data) - rl_image.width = pil_img.width - rl_image.height = pil_img.height - rl_image.mipmaps = 1 - rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 - - self._qr_texture = rl.load_texture_from_image(rl_image) + self._qr_texture = make_texture(url, inverted=True) def _render(self, _): if self._qr_texture: diff --git a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py index a18b26ec02..7e05bc2fa0 100644 --- a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -1,9 +1,8 @@ import pyray as rl -import qrcode -import numpy as np import time from openpilot.common.api import Api +from openpilot.common.qrcode import make_texture from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import ui_state @@ -37,24 +36,9 @@ class PairingDialog(NavWidget): def _generate_qr_code(self) -> None: try: - qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0) - qr.add_data(self._get_pairing_url()) - qr.make(fit=True) - - pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA') - img_array = np.array(pil_img, dtype=np.uint8) - if self._qr_texture and self._qr_texture.id != 0: rl.unload_texture(self._qr_texture) - - rl_image = rl.Image() - rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data) - rl_image.width = pil_img.width - rl_image.height = pil_img.height - rl_image.mipmaps = 1 - rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 - - self._qr_texture = rl.load_texture_from_image(rl_image) + self._qr_texture = make_texture(self._get_pairing_url(), inverted=True) except Exception as e: cloudlog.warning(f"QR code generation failed: {e}") self._qr_texture = None diff --git a/openpilot/selfdrive/ui/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/widgets/pairing_dialog.py index 1ff550e4b6..54ccfe19be 100644 --- a/openpilot/selfdrive/ui/widgets/pairing_dialog.py +++ b/openpilot/selfdrive/ui/widgets/pairing_dialog.py @@ -1,9 +1,8 @@ import pyray as rl -import qrcode -import numpy as np import time from openpilot.common.api import Api +from openpilot.common.qrcode import make_texture from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params from openpilot.system.ui.widgets import Widget @@ -39,24 +38,9 @@ class PairingDialog(Widget): def _generate_qr_code(self) -> None: try: - qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4) - qr.add_data(self._get_pairing_url()) - qr.make(fit=True) - - pil_img = qr.make_image(fill_color="black", back_color="white").convert('RGBA') - img_array = np.array(pil_img, dtype=np.uint8) - if self.qr_texture and self.qr_texture.id != 0: rl.unload_texture(self.qr_texture) - - rl_image = rl.Image() - rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data) - rl_image.width = pil_img.width - rl_image.height = pil_img.height - rl_image.mipmaps = 1 - rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 - - self.qr_texture = rl.load_texture_from_image(rl_image) + self.qr_texture = make_texture(self._get_pairing_url()) except Exception: cloudlog.exception("QR code generation failed") self.qr_texture = None diff --git a/pyproject.toml b/pyproject.toml index 00c21165ec..78fb53ed1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ dependencies = [ # ui "comma-deps-raylib", - "qrcode", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index df38331dfc..efd0aad651 100644 --- a/uv.lock +++ b/uv.lock @@ -588,7 +588,6 @@ dependencies = [ { name = "pycapnp" }, { name = "pyjwt", extra = ["crypto"] }, { name = "pyzmq" }, - { name = "qrcode" }, { name = "requests" }, { name = "scons" }, { name = "sentry-sdk" }, @@ -654,7 +653,6 @@ requires-dist = [ { name = "pycapnp", specifier = "==2.1.0" }, { name = "pyjwt", extras = ["crypto"] }, { name = "pyzmq" }, - { name = "qrcode" }, { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, @@ -835,18 +833,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, ] -[[package]] -name = "qrcode" -version = "8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, -] - [[package]] name = "rednose" version = "0.0.1" From a75cd2d2776a2a50d173669e19ac1ee4b5a026d5 Mon Sep 17 00:00:00 2001 From: Fang Li Date: Fri, 7 Aug 2026 16:56:49 -0700 Subject: [PATCH 186/325] Process eSIM notifications after profile operations per SGP.22 spec (#38241) * Process eSIM notifications after profile operations * Clarify sleep purpose after operation in execute_and_process_notifications Added comment to clarify the purpose of the sleep. --- openpilot/common/esim/esim.py | 20 +++++++++++++++++--- openpilot/common/esim/lpa.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/openpilot/common/esim/esim.py b/openpilot/common/esim/esim.py index cb155a1d88..1a05fbe85d 100755 --- a/openpilot/common/esim/esim.py +++ b/openpilot/common/esim/esim.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import argparse +import sys +import time from openpilot.common.hardware import HARDWARE from openpilot.common.esim.base import LPABase, Profile @@ -27,6 +29,17 @@ def print_profiles(lpa: LPABase) -> None: print(f'{i}. {p.iccid} (nickname: {p.nickname or ""}) (provider: {p.provider}) - {"enabled" if p.enabled else "disabled"}') +def execute_and_process_notifications(lpa: LPABase, operation) -> None: + try: + operation() + finally: + time.sleep(1) # Need to wait for 1s after the operation is finished so the eUICC/modem can settle down. + try: + lpa.process_notifications() + except Exception as e: + print(f'failed to process eSIM notifications: {e}', file=sys.stderr) + + if __name__ == '__main__': parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai') sub = parser.add_subparsers(dest='cmd') @@ -53,17 +66,18 @@ if __name__ == '__main__': if not lpa.is_euicc(): raise SystemExit("no eUICC detected") if args.cmd == 'switch': - lpa.switch_profile(resolve_iccid(lpa, args.profile)) + iccid = resolve_iccid(lpa, args.profile) + execute_and_process_notifications(lpa, lambda: lpa.switch_profile(iccid)) elif args.cmd == 'delete': iccid = resolve_iccid(lpa, args.profile) confirm = input(f'are you sure you want to delete profile {iccid}? (y/N) ') if confirm == 'y': - lpa.delete_profile(iccid) + execute_and_process_notifications(lpa, lambda: lpa.delete_profile(iccid)) else: print('cancelled') exit(0) elif args.cmd == 'download': - lpa.download_profile(args.qr, args.name) + execute_and_process_notifications(lpa, lambda: lpa.download_profile(args.qr, args.name)) elif args.cmd == 'nickname': lpa.nickname_profile(resolve_iccid(lpa, args.profile), args.name) else: diff --git a/openpilot/common/esim/lpa.py b/openpilot/common/esim/lpa.py index 80138b54ae..545f5306d4 100644 --- a/openpilot/common/esim/lpa.py +++ b/openpilot/common/esim/lpa.py @@ -451,7 +451,7 @@ def process_notifications(client: AtClient) -> None: response = es10x_command(client, request) content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"), TAG_OK, "RetrieveNotificationsListResponse") - pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None) + pending_notif = next((content[start:end] for t, _, start, end in iter_tlv(content, with_positions=True) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None) if pending_notif is None: raise RuntimeError("Missing PendingNotification") From cbda3f7994322ed40b428ea49c5bd868ff018700 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 7 Aug 2026 18:49:27 -0700 Subject: [PATCH 187/325] Rename hardware/tici/ -> hardware/comma/ (#38580) --- .gitattributes | 2 +- Jenkinsfile | 2 +- launch_chffrplus.sh | 6 +++--- openpilot/common/hardware/__init__.py | 2 +- openpilot/common/hardware/{tici => comma}/__init__.py | 0 openpilot/common/hardware/{tici => comma}/agnos.json | 0 openpilot/common/hardware/{tici => comma}/agnos.py | 2 +- .../common/hardware/{tici => comma}/all-partitions.json | 0 openpilot/common/hardware/{tici => comma}/amplifier.py | 0 openpilot/common/hardware/{tici => comma}/hardware.h | 0 openpilot/common/hardware/{tici => comma}/hardware.py | 4 ++-- openpilot/common/hardware/{tici => comma}/id_rsa | 0 openpilot/common/hardware/{tici => comma}/modem.py | 0 openpilot/common/hardware/{tici => comma}/pins.py | 0 openpilot/common/hardware/{tici => comma}/power_monitor.py | 0 openpilot/common/hardware/{tici => comma}/tests/__init__.py | 0 .../hardware/{tici => comma}/tests/test_agnos_updater.py | 0 .../common/hardware/{tici => comma}/tests/test_amplifier.py | 2 +- openpilot/common/hardware/{tici => comma}/updater | 0 openpilot/common/hardware/hw.h | 2 +- openpilot/selfdrive/pandad/tests/test_pandad.py | 2 +- openpilot/selfdrive/test/test_onroad.py | 2 +- openpilot/selfdrive/test/test_power_draw.py | 2 +- openpilot/system/hardware/{tici => comma}/agnos.json | 0 openpilot/system/manager/process_config.py | 2 +- openpilot/system/qcomgpsd/nmeaport.py | 2 +- openpilot/system/qcomgpsd/qcomgpsd.py | 2 +- openpilot/system/ubloxd/pigeond.py | 2 +- openpilot/system/ubloxd/tests/test_pigeond.py | 2 +- openpilot/system/updated/updated.py | 4 ++-- tools/op.sh | 4 ++-- tools/scripts/ssh.py | 2 +- 32 files changed, 24 insertions(+), 24 deletions(-) rename openpilot/common/hardware/{tici => comma}/__init__.py (100%) rename openpilot/common/hardware/{tici => comma}/agnos.json (100%) rename openpilot/common/hardware/{tici => comma}/agnos.py (99%) rename openpilot/common/hardware/{tici => comma}/all-partitions.json (100%) rename openpilot/common/hardware/{tici => comma}/amplifier.py (100%) rename openpilot/common/hardware/{tici => comma}/hardware.h (100%) rename openpilot/common/hardware/{tici => comma}/hardware.py (99%) rename openpilot/common/hardware/{tici => comma}/id_rsa (100%) rename openpilot/common/hardware/{tici => comma}/modem.py (100%) rename openpilot/common/hardware/{tici => comma}/pins.py (100%) rename openpilot/common/hardware/{tici => comma}/power_monitor.py (100%) rename openpilot/common/hardware/{tici => comma}/tests/__init__.py (100%) rename openpilot/common/hardware/{tici => comma}/tests/test_agnos_updater.py (100%) rename openpilot/common/hardware/{tici => comma}/tests/test_amplifier.py (96%) rename openpilot/common/hardware/{tici => comma}/updater (100%) rename openpilot/system/hardware/{tici => comma}/agnos.json (100%) diff --git a/.gitattributes b/.gitattributes index 1cf541aa3a..5cb404146d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,4 +11,4 @@ *.wav filter=lfs diff=lfs merge=lfs -text openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text -openpilot/common/hardware/tici/updater filter=lfs diff=lfs merge=lfs -text +openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text diff --git a/Jenkinsfile b/Jenkinsfile index 7c8d1c060e..341df8a8dd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -248,7 +248,7 @@ node { step("build openpilot", "cd openpilot/system/manager && ./build.py"), step("test pandad loopback", "./openpilot/selfdrive/pandad/tests/test_pandad_loopback.py"), step("test pandad spi", "./openpilot/selfdrive/pandad/tests/test_pandad_spi.py"), - step("test amp", "./openpilot/common/hardware/tici/tests/test_amplifier.py"), + step("test amp", "./openpilot/common/hardware/comma/tests/test_amplifier.py"), ]) }, diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 4f767f3b19..62ebffd27b 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -19,12 +19,12 @@ function agnos_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - AGNOS_PY="$DIR/openpilot/common/hardware/tici/agnos.py" - MANIFEST="$DIR/openpilot/system/hardware/tici/agnos.json" + AGNOS_PY="$DIR/openpilot/common/hardware/comma/agnos.py" + MANIFEST="$DIR/openpilot/system/hardware/comma/agnos.json" if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi - $DIR/openpilot/common/hardware/tici/updater $AGNOS_PY $MANIFEST + $DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST fi } diff --git a/openpilot/common/hardware/__init__.py b/openpilot/common/hardware/__init__.py index 3387d8a0ca..99e619b33b 100644 --- a/openpilot/common/hardware/__init__.py +++ b/openpilot/common/hardware/__init__.py @@ -2,7 +2,7 @@ import os from typing import cast from openpilot.common.hardware.base import HardwareBase -from openpilot.common.hardware.tici.hardware import Tici +from openpilot.common.hardware.comma.hardware import Tici from openpilot.common.hardware.pc.hardware import Pc TICI = os.path.isfile('/TICI') diff --git a/openpilot/common/hardware/tici/__init__.py b/openpilot/common/hardware/comma/__init__.py similarity index 100% rename from openpilot/common/hardware/tici/__init__.py rename to openpilot/common/hardware/comma/__init__.py diff --git a/openpilot/common/hardware/tici/agnos.json b/openpilot/common/hardware/comma/agnos.json similarity index 100% rename from openpilot/common/hardware/tici/agnos.json rename to openpilot/common/hardware/comma/agnos.json diff --git a/openpilot/common/hardware/tici/agnos.py b/openpilot/common/hardware/comma/agnos.py similarity index 99% rename from openpilot/common/hardware/tici/agnos.py rename to openpilot/common/hardware/comma/agnos.py index b3b5e05176..de0fb24551 100755 --- a/openpilot/common/hardware/tici/agnos.py +++ b/openpilot/common/hardware/comma/agnos.py @@ -12,7 +12,7 @@ import requests SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') -AGNOS_MANIFEST_FILE = "openpilot/system/hardware/tici/agnos.json" +AGNOS_MANIFEST_FILE = "openpilot/system/hardware/comma/agnos.json" class StreamingDecompressor: diff --git a/openpilot/common/hardware/tici/all-partitions.json b/openpilot/common/hardware/comma/all-partitions.json similarity index 100% rename from openpilot/common/hardware/tici/all-partitions.json rename to openpilot/common/hardware/comma/all-partitions.json diff --git a/openpilot/common/hardware/tici/amplifier.py b/openpilot/common/hardware/comma/amplifier.py similarity index 100% rename from openpilot/common/hardware/tici/amplifier.py rename to openpilot/common/hardware/comma/amplifier.py diff --git a/openpilot/common/hardware/tici/hardware.h b/openpilot/common/hardware/comma/hardware.h similarity index 100% rename from openpilot/common/hardware/tici/hardware.h rename to openpilot/common/hardware/comma/hardware.h diff --git a/openpilot/common/hardware/tici/hardware.py b/openpilot/common/hardware/comma/hardware.py similarity index 99% rename from openpilot/common/hardware/tici/hardware.py rename to openpilot/common/hardware/comma/hardware.py index aa125ed97d..3bd1538a36 100644 --- a/openpilot/common/hardware/tici/hardware.py +++ b/openpilot/common/hardware/comma/hardware.py @@ -13,8 +13,8 @@ from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.common.esim.base import LPABase from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone from openpilot.common.esim.lpa import TiciLPA -from openpilot.common.hardware.tici.pins import GPIO -from openpilot.common.hardware.tici.amplifier import Amplifier +from openpilot.common.hardware.comma.pins import GPIO +from openpilot.common.hardware.comma.amplifier import Amplifier MODEM_STATE_PATH = "/dev/shm/modem" diff --git a/openpilot/common/hardware/tici/id_rsa b/openpilot/common/hardware/comma/id_rsa similarity index 100% rename from openpilot/common/hardware/tici/id_rsa rename to openpilot/common/hardware/comma/id_rsa diff --git a/openpilot/common/hardware/tici/modem.py b/openpilot/common/hardware/comma/modem.py similarity index 100% rename from openpilot/common/hardware/tici/modem.py rename to openpilot/common/hardware/comma/modem.py diff --git a/openpilot/common/hardware/tici/pins.py b/openpilot/common/hardware/comma/pins.py similarity index 100% rename from openpilot/common/hardware/tici/pins.py rename to openpilot/common/hardware/comma/pins.py diff --git a/openpilot/common/hardware/tici/power_monitor.py b/openpilot/common/hardware/comma/power_monitor.py similarity index 100% rename from openpilot/common/hardware/tici/power_monitor.py rename to openpilot/common/hardware/comma/power_monitor.py diff --git a/openpilot/common/hardware/tici/tests/__init__.py b/openpilot/common/hardware/comma/tests/__init__.py similarity index 100% rename from openpilot/common/hardware/tici/tests/__init__.py rename to openpilot/common/hardware/comma/tests/__init__.py diff --git a/openpilot/common/hardware/tici/tests/test_agnos_updater.py b/openpilot/common/hardware/comma/tests/test_agnos_updater.py similarity index 100% rename from openpilot/common/hardware/tici/tests/test_agnos_updater.py rename to openpilot/common/hardware/comma/tests/test_agnos_updater.py diff --git a/openpilot/common/hardware/tici/tests/test_amplifier.py b/openpilot/common/hardware/comma/tests/test_amplifier.py similarity index 96% rename from openpilot/common/hardware/tici/tests/test_amplifier.py rename to openpilot/common/hardware/comma/tests/test_amplifier.py index 2845805860..d26e2e0518 100755 --- a/openpilot/common/hardware/tici/tests/test_amplifier.py +++ b/openpilot/common/hardware/comma/tests/test_amplifier.py @@ -7,7 +7,7 @@ import unittest from panda import Panda from openpilot.common.test import OpenpilotTestCase from openpilot.common.hardware import HARDWARE -from openpilot.common.hardware.tici.amplifier import Amplifier +from openpilot.common.hardware.comma.amplifier import Amplifier class TestAmplifier(OpenpilotTestCase): diff --git a/openpilot/common/hardware/tici/updater b/openpilot/common/hardware/comma/updater similarity index 100% rename from openpilot/common/hardware/tici/updater rename to openpilot/common/hardware/comma/updater diff --git a/openpilot/common/hardware/hw.h b/openpilot/common/hardware/hw.h index 1e097d99b1..d5711de24d 100644 --- a/openpilot/common/hardware/hw.h +++ b/openpilot/common/hardware/hw.h @@ -6,7 +6,7 @@ #include "common/util.h" #if __TICI__ -#include "common/hardware/tici/hardware.h" +#include "common/hardware/comma/hardware.h" #define Hardware HardwareTici #else #include "common/hardware/pc/hardware.h" diff --git a/openpilot/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py index 0b5ea89a9d..bead2ffa84 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -11,7 +11,7 @@ from openpilot.common.gpio import gpio_set, gpio_init from panda import Panda, PandaDFU from openpilot.system.manager.process_config import managed_processes from openpilot.common.hardware import HARDWARE -from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.comma.pins import GPIO HERE = os.path.dirname(os.path.realpath(__file__)) diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 57bf48ee81..9a514139c2 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -69,7 +69,7 @@ PROCS = { "openpilot.system.loggerd.deleter": 1.0, "./pandad": 19.0, "openpilot.system.qcomgpsd.qcomgpsd": 1.0, - "openpilot.common.hardware.tici.modem": 10.0, + "openpilot.common.hardware.comma.modem": 10.0, } TIMINGS = { diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index 4c2a15a41f..01856f81e8 100755 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -13,7 +13,7 @@ from openpilot.cereal.services import SERVICE_LIST from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.mock import mock_messages from openpilot.common.params import Params -from openpilot.common.hardware.tici.power_monitor import get_power +from openpilot.common.hardware.comma.power_monitor import get_power from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.manager import manager_cleanup diff --git a/openpilot/system/hardware/tici/agnos.json b/openpilot/system/hardware/comma/agnos.json similarity index 100% rename from openpilot/system/hardware/tici/agnos.json rename to openpilot/system/hardware/comma/agnos.json diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index 1e9e236c97..a4326b4107 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -112,7 +112,7 @@ procs = [ PythonProcess("lateral_maneuversd", "openpilot.tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), PythonProcess("radard", "openpilot.selfdrive.controls.radard", only_onroad), PythonProcess("hardwared", "openpilot.system.hardware.hardwared", always_run), - PythonProcess("modem", "openpilot.common.hardware.tici.modem", always_run, enabled=TICI), + PythonProcess("modem", "openpilot.common.hardware.comma.modem", always_run, enabled=TICI), PythonProcess("tombstoned", "openpilot.system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "openpilot.system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "openpilot.system.loggerd.uploader", always_run), diff --git a/openpilot/system/qcomgpsd/nmeaport.py b/openpilot/system/qcomgpsd/nmeaport.py index db1fe5552d..e270e23a3a 100644 --- a/openpilot/system/qcomgpsd/nmeaport.py +++ b/openpilot/system/qcomgpsd/nmeaport.py @@ -121,7 +121,7 @@ def process_nmea_port_messages(device:str="/dev/ttyUSB1") -> NoReturn: def main() -> NoReturn: from openpilot.common.gpio import gpio_init, gpio_set - from openpilot.common.hardware.tici.pins import GPIO + from openpilot.common.hardware.comma.pins import GPIO from openpilot.system.qcomgpsd.qcomgpsd import at_cmd try: diff --git a/openpilot/system/qcomgpsd/qcomgpsd.py b/openpilot/system/qcomgpsd/qcomgpsd.py index 4e52154419..6261b18d56 100755 --- a/openpilot/system/qcomgpsd/qcomgpsd.py +++ b/openpilot/system/qcomgpsd/qcomgpsd.py @@ -15,7 +15,7 @@ import openpilot.cereal.messaging as messaging from openpilot.common.gpio import gpio_init, gpio_set from openpilot.common.utils import retry from openpilot.common.time_helpers import system_time_valid -from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.comma.pins import GPIO from openpilot.common.serial import Serial from openpilot.common.swaglog import cloudlog from openpilot.system.qcomgpsd.modemdiag import ModemDiag, DIAG_LOG_F, setup_logs, send_recv diff --git a/openpilot/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py index 9891285465..f704761e35 100755 --- a/openpilot/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -14,7 +14,7 @@ from openpilot.common.serial import Serial from openpilot.common.swaglog import cloudlog from openpilot.common.hardware import TICI from openpilot.common.gpio import gpio_init, gpio_set -from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.comma.pins import GPIO UBLOX_TTY = "/dev/ttyHS0" diff --git a/openpilot/system/ubloxd/tests/test_pigeond.py b/openpilot/system/ubloxd/tests/test_pigeond.py index b7a0cd216d..87e3f18523 100644 --- a/openpilot/system/ubloxd/tests/test_pigeond.py +++ b/openpilot/system/ubloxd/tests/test_pigeond.py @@ -6,7 +6,7 @@ from openpilot.cereal.services import SERVICE_LIST from openpilot.common.gpio import gpio_read from openpilot.selfdrive.test.helpers import with_processes from openpilot.system.manager.process_config import managed_processes -from openpilot.common.hardware.tici.pins import GPIO +from openpilot.common.hardware.comma.pins import GPIO # TODO: test TTFF when we have good A-GNSS diff --git a/openpilot/system/updated/updated.py b/openpilot/system/updated/updated.py index 9610785861..24acb2fd17 100755 --- a/openpilot/system/updated/updated.py +++ b/openpilot/system/updated/updated.py @@ -203,7 +203,7 @@ def finalize_update() -> None: def handle_agnos_update() -> None: - from openpilot.common.hardware.tici.agnos import flash_agnos_update, get_target_slot_number + from openpilot.common.hardware.comma.agnos import flash_agnos_update, get_target_slot_number cur_version = HARDWARE.get_os_version() updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \ @@ -219,7 +219,7 @@ def handle_agnos_update() -> None: cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") set_offroad_alert("Offroad_NeosUpdate", True) - manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/tici/agnos.json") + manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/comma/agnos.json") target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) set_offroad_alert("Offroad_NeosUpdate", False) diff --git a/tools/op.sh b/tools/op.sh index a21b1c6ab1..3d7d17a76b 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -364,8 +364,8 @@ function op_check_agnos_update() { echo -e "${BOLD}AGNOS update available:${NC} $current_version → $target_version" if read -r -p "Install it now? [y/N] " choice && [[ "$choice" =~ ^[Yy]$ ]]; then - op_run_command "$OPENPILOT_ROOT/openpilot/common/hardware/tici/agnos.py" --swap \ - "$OPENPILOT_ROOT/openpilot/common/hardware/tici/agnos.json" + op_run_command "$OPENPILOT_ROOT/openpilot/common/hardware/comma/agnos.py" --swap \ + "$OPENPILOT_ROOT/openpilot/common/hardware/comma/agnos.json" if read -r -p "Reboot now to apply the update? [y/N] " choice && [[ "$choice" =~ ^[Yy]$ ]]; then op_run_command sudo reboot diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py index 86e86c7eed..2d8ff46604 100755 --- a/tools/scripts/ssh.py +++ b/tools/scripts/ssh.py @@ -14,7 +14,7 @@ if __name__ == "__main__": parser.add_argument("device", help="device name or dongle id") parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai") parser.add_argument("--port", help="ssh jump server port", default=22, type=int) - parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "openpilot/common/hardware/tici/id_rsa")) + parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "openpilot/common/hardware/comma/id_rsa")) parser.add_argument("--debug", help="enable debug output", action="store_true") args = parser.parse_args() From b755d32276a4934690e9e6f6f5824888245d582f Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 7 Aug 2026 19:38:13 -0700 Subject: [PATCH 188/325] fix telemetry hanging big model (#38565) * modeld: fail fast when the gpu hangs mid run * modeld: harden chestnut telemetry * selfdrived: rm the big model lagging trigger --- openpilot/selfdrive/modeld/modeld.py | 49 ++++++++++++-------- openpilot/selfdrive/selfdrived/selfdrived.py | 3 -- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 92cb9e0866..be08c9c4c7 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -77,31 +77,38 @@ class ChestnutState: @cached_property def power_limit(self) -> int: smu = Device["AMD"].iface.dev_impl.smu - return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True) + return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) def send(self) -> None: msg = messaging.new_message('chestnutState') state = msg.chestnutState - try: - smu = Device["AMD"].iface.dev_impl.smu - metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics - state.tempC = metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT] - state.memoryTempC = metrics.AvgTemperature[smu.smu_mod.TEMP_MEM] - state.powerDrawW = metrics.AverageSocketPower - state.powerLimitW = self.power_limit - state.gpuUsagePercent = metrics.AverageGfxActivity - state.gpuClockMhz = metrics.AverageGfxclkFrequencyPostDs - state.fanSpeedRpm = metrics.AvgFanRpm - asm = Device["AMD"].iface.pci_dev.usb - state.pcieLtssm = asm.read(0xB450, 1)[0] - state.supplyVoltage, state.supplyCurrent = struct.unpack(' Date: Fri, 7 Aug 2026 20:12:34 -0700 Subject: [PATCH 189/325] rename tici hardware platform to comma (#38581) * rename tici hardware platform to comma * no /TICI * lil more * lil more * you had a good life larch64 * lil more * one more --- .gitignore | 2 +- Jenkinsfile | 4 +-- SConstruct | 32 +++++++++---------- openpilot/cereal/log.capnp | 8 ++--- openpilot/common/esim/lpa.py | 2 +- openpilot/common/hardware/__init__.py | 14 ++++---- openpilot/common/hardware/comma/hardware.h | 2 +- openpilot/common/hardware/comma/hardware.py | 18 ++++++++--- .../hardware/comma/tests/test_amplifier.py | 2 +- openpilot/common/hardware/hw.h | 4 +-- openpilot/common/hardware/pc/hardware.py | 2 +- openpilot/common/test.py | 16 +++++----- openpilot/selfdrive/modeld/SConscript | 2 +- .../selfdrive/pandad/tests/test_pandad.py | 2 +- .../pandad/tests/test_pandad_loopback.py | 2 +- .../selfdrive/pandad/tests/test_pandad_spi.py | 2 +- openpilot/selfdrive/test/test_onroad.py | 2 +- openpilot/selfdrive/test/test_power_draw.py | 2 +- openpilot/selfdrive/ui/SConscript | 2 +- .../ui/mici/onroad/alert_renderer.py | 4 +-- .../selfdrive/ui/mici/onroad/cameraview.py | 18 +++++------ .../selfdrive/ui/onroad/alert_renderer.py | 4 +-- openpilot/selfdrive/ui/onroad/cameraview.py | 18 +++++------ openpilot/selfdrive/ui/ui.py | 4 +-- .../system/athena/tests/test_athenad_ping.py | 8 ++--- openpilot/system/camerad/test/test_camerad.py | 2 +- openpilot/system/hardware/hardwared.py | 4 +-- openpilot/system/loggerd/SConscript | 2 +- openpilot/system/loggerd/encoderd.cc | 8 ++--- .../system/loggerd/tests/test_encoder.py | 8 ++--- .../system/loggerd/tests/test_loggerd.py | 4 +-- openpilot/system/manager/process_config.py | 10 +++--- .../system/sensord/tests/test_sensord.py | 2 +- openpilot/system/ubloxd/pigeond.py | 4 +-- openpilot/system/ubloxd/tests/test_pigeond.py | 2 +- openpilot/system/ui/lib/scroll_panel2.py | 4 +-- openpilot/system/ui/mici_setup.py | 4 +-- openpilot/system/ui/mici_updater.py | 4 +-- 38 files changed, 122 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index e1897e0ed9..54f9f176b7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ a.out .cache/ bin/ -# created at launch for TICI PYTHONPATH (PC uses editable installs via pyproject.toml) +# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml) /msgq /opendbc /rednose diff --git a/Jenkinsfile b/Jenkinsfile index 341df8a8dd..db18d13ddd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -30,14 +30,14 @@ export GIT_COMMIT=${env.GIT_COMMIT} export CI_ARTIFACTS_TOKEN=${env.CI_ARTIFACTS_TOKEN} export GITHUB_COMMENTS_TOKEN=${env.GITHUB_COMMENTS_TOKEN} export AZURE_TOKEN='${env.AZURE_TOKEN}' -# only use 1 thread for tici tests since most require HIL +# only use 1 thread since most require real hardware that can't be shared export PYTEST_ADDOPTS="-n0 -s" export GIT_SSH_COMMAND="ssh -i /data/gitkey" source ~/.bash_profile -if [ -f /TICI ]; then +if [ -f /AGNOS ]; then source /etc/profile rm -rf /tmp/tmp* diff --git a/SConstruct b/SConstruct index 18efb9f8a1..bb5c35feb7 100644 --- a/SConstruct +++ b/SConstruct @@ -10,7 +10,7 @@ import numpy as np import SCons.Errors from SCons.Defaults import _stripixes -TICI = os.path.isfile('/TICI') +COMMA_HARDWARE = os.path.isfile('/AGNOS') SCons.Warnings.warningAsException(True) @@ -24,7 +24,7 @@ release = not os.path.exists(File('#.gitattributes').abspath) # file absent on r AddOption('--minimal', action='store_false', dest='extras', - default=(not TICI and not release), + default=(not COMMA_HARDWARE and not release), help='the minimum build to run openpilot. no tests, tools, etc.') submodule_python_paths = [ @@ -46,13 +46,13 @@ if external_pythonpath := os.environ.get("PYTHONPATH"): arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() if platform.system() == "Darwin": arch = "Darwin" -elif arch == "aarch64" and TICI: - arch = "larch64" +elif arch == "aarch64" and COMMA_HARDWARE: + arch = "comma_arm64" assert arch in [ - "larch64", # linux tici arm64 - "aarch64", # linux pc arm64 - "x86_64", # linux pc x64 - "Darwin", # macOS arm64 (x86 not supported) + "comma_arm64", # linux comma hardware (AGNOS) arm64 + "aarch64", # linux pc arm64 + "x86_64", # linux pc x64 + "Darwin", # macOS arm64 (x86 not supported) ] pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] @@ -61,7 +61,7 @@ acados = pkgs[pkg_names.index('acados')] ffmpeg = pkgs[pkg_names.index('ffmpeg')] # Shared package ships .so/.dylib; older device venvs still have static .a only. # Keep static link deps (x264/z/va/drm) when the installed package is static so -# TICI CI works without upgrading the device venv yet. +# COMMA_HARDWARE CI works without upgrading the device venv yet. # TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94 _ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else [] ffmpeg_shared = any( @@ -133,7 +133,7 @@ env = Environment( "-O2", "-Wunused", "-Werror", - "-Wshadow" if arch in ("Darwin", "larch64") else "-Wshadow=local", + "-Wshadow" if arch in ("Darwin", "comma_arm64") else "-Wshadow=local", "-Wno-unknown-warning-option", "-Wno-inconsistent-missing-override", "-Wno-c99-designator", @@ -172,17 +172,17 @@ if arch == "Darwin": env["RPATHPREFIX"] = "-Wl,-rpath," env["RPATHSUFFIX"] = "" env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}" -if arch != "larch64": +if arch != "comma_arm64": env['_LIBFLAGS'] = _libflags # Arch-specific flags and paths -if arch == "larch64": +if arch == "comma_arm64": env["CC"] = "clang" env["CXX"] = "clang++" env.Append(LIBPATH=[ "/usr/lib/aarch64-linux-gnu", ]) - arch_flags = ["-D__TICI__", "-mcpu=cortex-a57"] + arch_flags = ["-D__COMMA_HARDWARE__", "-mcpu=cortex-a57"] env.Append(CCFLAGS=arch_flags) env.Append(CXXFLAGS=arch_flags) elif arch == "Darwin": @@ -234,7 +234,7 @@ Export('envCython', 'np_version') Export('env', 'arch', 'acados', 'ffmpeg_libs') # Setup cache dir -cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache' +cache_dir = '/data/scons_cache' if arch == "comma_arm64" else '/tmp/scons_cache' cache_size_limit = 4e9 if "CI" in os.environ else 2e9 CacheDir(cache_dir) Clean(["."], cache_dir) @@ -280,7 +280,7 @@ SConscript([ 'openpilot/system/loggerd/SConscript', ]) -if arch == "larch64": +if arch == "comma_arm64": SConscript(['openpilot/system/camerad/SConscript']) # Build selfdrive @@ -293,7 +293,7 @@ SConscript([ ]) # Build desktop-only tools -if GetOption('extras') and arch != "larch64": +if GetOption('extras') and arch != "comma_arm64": SConscript([ 'openpilot/tools/replay/SConscript', 'openpilot/tools/cabana/SConscript', diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fdb025d838..da700b58a7 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -180,13 +180,13 @@ struct InitData { enum DeviceType { unknown @0; - neo @1; + neo @1; # NEO, EON, & comma two chffrAndroid @2; chffrIos @3; - tici @4; + tici @4; # comma three pc @5; - tizi @6; - mici @7; + tizi @6; # comma 3X + mici @7; # comma four } struct PandaInfo { diff --git a/openpilot/common/esim/lpa.py b/openpilot/common/esim/lpa.py index 545f5306d4..fb128c32e1 100644 --- a/openpilot/common/esim/lpa.py +++ b/openpilot/common/esim/lpa.py @@ -682,7 +682,7 @@ def download_profile(client: AtClient, activation_code: str) -> str: session.close() -class TiciLPA(LPABase): +class LPA(LPABase): def __init__(self): if hasattr(self, '_client'): return diff --git a/openpilot/common/hardware/__init__.py b/openpilot/common/hardware/__init__.py index 99e619b33b..58e18736fd 100644 --- a/openpilot/common/hardware/__init__.py +++ b/openpilot/common/hardware/__init__.py @@ -2,15 +2,15 @@ import os from typing import cast from openpilot.common.hardware.base import HardwareBase -from openpilot.common.hardware.comma.hardware import Tici -from openpilot.common.hardware.pc.hardware import Pc +from openpilot.common.hardware.comma.hardware import HardwareComma +from openpilot.common.hardware.pc.hardware import HardwarePc -TICI = os.path.isfile('/TICI') AGNOS = os.path.isfile('/AGNOS') -PC = not TICI +COMMA_HARDWARE = AGNOS +PC = not COMMA_HARDWARE -if TICI: - HARDWARE = cast(HardwareBase, Tici()) +if COMMA_HARDWARE: + HARDWARE = cast(HardwareBase, HardwareComma()) else: - HARDWARE = cast(HardwareBase, Pc()) + HARDWARE = cast(HardwareBase, HardwarePc()) diff --git a/openpilot/common/hardware/comma/hardware.h b/openpilot/common/hardware/comma/hardware.h index a1a8090c35..6292183d9d 100644 --- a/openpilot/common/hardware/comma/hardware.h +++ b/openpilot/common/hardware/comma/hardware.h @@ -9,7 +9,7 @@ #include "common/util.h" #include "common/hardware/base.h" -class HardwareTici : public HardwareNone { +class HardwareComma : public HardwareNone { public: static std::string get_name() { static const std::string name = []() { diff --git a/openpilot/common/hardware/comma/hardware.py b/openpilot/common/hardware/comma/hardware.py index 3bd1538a36..dfbf1e3f17 100644 --- a/openpilot/common/hardware/comma/hardware.py +++ b/openpilot/common/hardware/comma/hardware.py @@ -12,7 +12,7 @@ from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.common.esim.base import LPABase from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone -from openpilot.common.esim.lpa import TiciLPA +from openpilot.common.esim.lpa import LPA from openpilot.common.hardware.comma.pins import GPIO from openpilot.common.hardware.comma.amplifier import Amplifier @@ -58,7 +58,17 @@ def get_default_route_iface(): routes = [(int(route[6]), route[0]) for line in f.readlines()[1:] if (route := line.split())[1] == "00000000" and int(route[3], 16) & 0x1] return min(routes)[1] if routes else None -class Tici(HardwareBase): +class HardwareComma(HardwareBase): + """ + This platform covers the Snapdragon 845-based comma devices: + - tici = comma three + - tizi = comma 3X + - mici = comma four + + We strictly use only the device codenames in this codebase for + consistency, though all user-facing UI should use the product names. + """ + @cached_property def amplifier(self): if self.get_device_type() == "mici": @@ -145,7 +155,7 @@ class Tici(HardwareBase): } def get_sim_lpa(self) -> LPABase: - return TiciLPA() + return LPA() def get_imei(self): return self.get_modem_state().get('imei', '') @@ -413,7 +423,7 @@ class Tici(HardwareBase): return True if __name__ == "__main__": - t = Tici() + t = HardwareComma() t.initialize_hardware() t.set_power_save(False) print(t.get_sim_info()) diff --git a/openpilot/common/hardware/comma/tests/test_amplifier.py b/openpilot/common/hardware/comma/tests/test_amplifier.py index d26e2e0518..1752bf6e5e 100755 --- a/openpilot/common/hardware/comma/tests/test_amplifier.py +++ b/openpilot/common/hardware/comma/tests/test_amplifier.py @@ -11,7 +11,7 @@ from openpilot.common.hardware.comma.amplifier import Amplifier class TestAmplifier(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True def setup_method(self): # clear dmesg diff --git a/openpilot/common/hardware/hw.h b/openpilot/common/hardware/hw.h index d5711de24d..83dc452da8 100644 --- a/openpilot/common/hardware/hw.h +++ b/openpilot/common/hardware/hw.h @@ -5,9 +5,9 @@ #include "common/hardware/base.h" #include "common/util.h" -#if __TICI__ +#if __COMMA_HARDWARE__ #include "common/hardware/comma/hardware.h" -#define Hardware HardwareTici +#define Hardware HardwareComma #else #include "common/hardware/pc/hardware.h" #define Hardware HardwarePC diff --git a/openpilot/common/hardware/pc/hardware.py b/openpilot/common/hardware/pc/hardware.py index b4aa56c904..417d2db37d 100644 --- a/openpilot/common/hardware/pc/hardware.py +++ b/openpilot/common/hardware/pc/hardware.py @@ -1,7 +1,7 @@ from openpilot.cereal import log from openpilot.common.hardware.base import HardwareBase -class Pc(HardwareBase): +class HardwarePc(HardwareBase): def get_device_type(self): return "pc" diff --git a/openpilot/common/test.py b/openpilot/common/test.py index 6ccc76c5e3..23b934bfd5 100644 --- a/openpilot/common/test.py +++ b/openpilot/common/test.py @@ -6,7 +6,7 @@ import subprocess import unittest from unittest import mock -from openpilot.common.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager @@ -24,7 +24,7 @@ def clean_env(): class OpenpilotTestCase(unittest.TestCase): """TestCase with openpilot's per-test isolation.""" - TICI_TEST = False + COMMA_HARDWARE_TEST = False SHARED_DOWNLOAD_CACHE = False def __init_subclass__(cls, **kwargs): @@ -61,7 +61,7 @@ class OpenpilotTestCase(unittest.TestCase): def run(self, result=None): # This boundary cannot live in setUp/tearDown: existing unittest classes # are allowed to override those hooks without calling super(). - if (self.TICI_TEST and not TICI) or getattr(type(self), "__unittest_skip__", False): + if (self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE) or getattr(type(self), "__unittest_skip__", False): return super().run(result) test_env = clean_env() test_env.__enter__() @@ -80,8 +80,8 @@ class OpenpilotTestCase(unittest.TestCase): @classmethod def setUpClass(cls): super().setUpClass() - if cls.TICI_TEST and not TICI: - raise unittest.SkipTest("Skipping tici test on PC") + if cls.COMMA_HARDWARE_TEST and not COMMA_HARDWARE: + raise unittest.SkipTest("Skipping comma hardware test on PC") cls._class_env = clean_env() cls._class_env.__enter__() setup_class = getattr(cls, "setup_class", None) @@ -100,10 +100,10 @@ class OpenpilotTestCase(unittest.TestCase): def setUp(self): super().setUp() - if self.TICI_TEST and not TICI: - self.skipTest("Skipping tici test on PC") + if self.COMMA_HARDWARE_TEST and not COMMA_HARDWARE: + self.skipTest("Skipping comma hardware test on PC") - if self.TICI_TEST: + if self.COMMA_HARDWARE_TEST: HARDWARE.initialize_hardware() HARDWARE.set_power_save(False) subprocess.run(["pkill", "-9", "-f", "athena"], check=False) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 02d79ef7b4..2085d65835 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -27,7 +27,7 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " def estimate_pickle_max_size(onnx_size): return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty -if arch == 'larch64': +if arch == 'comma_arm64': tg_backend = 'QCOM' tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1' else: diff --git a/openpilot/selfdrive/pandad/tests/test_pandad.py b/openpilot/selfdrive/pandad/tests/test_pandad.py index bead2ffa84..891508f2d7 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad.py @@ -17,7 +17,7 @@ HERE = os.path.dirname(os.path.realpath(__file__)) class TestPandad(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True def setUp(self): super().setUp() diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py index 5895a32060..ad35018f5f 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_loopback.py @@ -73,7 +73,7 @@ def send_random_can_messages(sendcan, count): class TestBoarddLoopback(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True @classmethod def setup_class(cls): os.environ['STARTED'] = '1' diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py index 9cb0ac30c8..ba2e31ce82 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -16,7 +16,7 @@ from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ class TestBoarddSpi(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True @classmethod def setup_class(cls): os.environ['STARTED'] = '1' diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 9a514139c2..2ee876a6ef 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -105,7 +105,7 @@ def cputime_total(ct): class TestOnroad(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True @classmethod def setup_class(cls): diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index 01856f81e8..4fba2e8c99 100755 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -42,7 +42,7 @@ PROCS = [ class TestPowerDraw(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True def setup_method(self): Params().put("CarParams", get_demo_car_params().to_bytes(), block=True) diff --git a/openpilot/selfdrive/ui/SConscript b/openpilot/selfdrive/ui/SConscript index 2b02759687..b5c81a9009 100644 --- a/openpilot/selfdrive/ui/SConscript +++ b/openpilot/selfdrive/ui/SConscript @@ -3,7 +3,7 @@ from pathlib import Path Import('env', 'arch', 'common') -if GetOption('extras') and arch == "larch64": +if GetOption('extras') and arch == "comma_arm64": # build installers raylib_dir = Path(importlib.util.find_spec("raylib").submodule_search_locations[0]) / "install" raylib_env = env.Clone() diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index 12b62924c4..fe96bbd032 100644 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -9,7 +9,7 @@ from openpilot.cereal import messaging, log from opendbc.car.structs import car from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel @@ -132,7 +132,7 @@ class AlertRenderer(Widget): return ALERT_STARTUP_PENDING # 2. Lost communication with selfdriveState after receiving it - if TICI and not waiting_for_startup: + if COMMA_HARDWARE and not waiting_for_startup: ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] if ss_missing > SELFDRIVE_STATE_TIMEOUT: if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT: diff --git a/openpilot/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py index 4a31a9e04e..4ccfb04687 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget @@ -38,7 +38,7 @@ void main() { """ # Choose fragment shader based on platform capabilities -if TICI: +if COMMA_HARDWARE: FRAME_FRAGMENT_SHADER = """ #version 300 es #extension GL_OES_EGL_image_external_essl3 : enable @@ -121,7 +121,7 @@ class CameraView(Widget): self._texture_needs_update = True self.last_connection_attempt: float = 0.0 self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER) - self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1 + self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not COMMA_HARDWARE else -1 self._engaged_loc = rl.get_shader_location(self.shader, "engaged") self._engaged_val = rl.ffi.new("int[1]", [1]) self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver") @@ -137,8 +137,8 @@ class CameraView(Widget): self._placeholder_color: rl.Color | None = None - # Initialize EGL for zero-copy rendering on TICI - if TICI: + # Initialize EGL for zero-copy rendering on COMMA_HARDWARE + if COMMA_HARDWARE: if not init_egl(): raise RuntimeError("Failed to initialize EGL") @@ -185,7 +185,7 @@ class CameraView(Widget): self._clear_textures() # Clean up EGL texture - if TICI and self.egl_texture: + if COMMA_HARDWARE and self.egl_texture: rl.unload_texture(self.egl_texture) self.egl_texture = None @@ -260,7 +260,7 @@ class CameraView(Widget): dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y) # Render with appropriate method - if TICI: + if COMMA_HARDWARE: self._render_egl(src_rect, dst_rect) else: self._render_textures(src_rect, dst_rect) @@ -384,7 +384,7 @@ class CameraView(Widget): def _initialize_textures(self): self._clear_textures() - if not TICI: + if not COMMA_HARDWARE: self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), @@ -400,7 +400,7 @@ class CameraView(Widget): self.texture_uv = None # Clean up EGL resources - if TICI: + if COMMA_HARDWARE: for data in self.egl_images.values(): destroy_egl_image(data) self.egl_images = {} diff --git a/openpilot/selfdrive/ui/onroad/alert_renderer.py b/openpilot/selfdrive/ui/onroad/alert_renderer.py index 0ce404bec3..29ace66287 100644 --- a/openpilot/selfdrive/ui/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/onroad/alert_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -96,7 +96,7 @@ class AlertRenderer(Widget): return ALERT_STARTUP_PENDING # 2. Lost communication with selfdriveState after receiving it - if TICI and not waiting_for_startup: + if COMMA_HARDWARE and not waiting_for_startup: ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] if ss_missing > SELFDRIVE_STATE_TIMEOUT: if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT: diff --git a/openpilot/selfdrive/ui/onroad/cameraview.py b/openpilot/selfdrive/ui/onroad/cameraview.py index 846bf20bbc..1fed2dd683 100644 --- a/openpilot/selfdrive/ui/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/onroad/cameraview.py @@ -4,7 +4,7 @@ import pyray as rl from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage from openpilot.system.ui.widgets import Widget @@ -38,7 +38,7 @@ void main() { """ # Choose fragment shader based on platform capabilities -if TICI: +if COMMA_HARDWARE: FRAME_FRAGMENT_SHADER = """ #version 300 es #extension GL_OES_EGL_image_external_essl3 : enable @@ -82,7 +82,7 @@ class CameraView(Widget): self._texture_needs_update = True self.last_connection_attempt: float = 0.0 self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER) - self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1 + self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not COMMA_HARDWARE else -1 self.frame: VisionBuf | None = None self.texture_y: rl.Texture | None = None @@ -94,8 +94,8 @@ class CameraView(Widget): self._placeholder_color: rl.Color | None = None - # Initialize EGL for zero-copy rendering on TICI - if TICI: + # Initialize EGL for zero-copy rendering on COMMA_HARDWARE + if COMMA_HARDWARE: if not init_egl(): raise RuntimeError("Failed to initialize EGL") @@ -146,7 +146,7 @@ class CameraView(Widget): self._clear_textures() # Clean up EGL texture - if TICI and self.egl_texture: + if COMMA_HARDWARE and self.egl_texture: rl.unload_texture(self.egl_texture) self.egl_texture = None @@ -220,7 +220,7 @@ class CameraView(Widget): dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y) # Render with appropriate method - if TICI: + if COMMA_HARDWARE: self._render_egl(src_rect, dst_rect) else: self._render_textures(src_rect, dst_rect) @@ -337,7 +337,7 @@ class CameraView(Widget): def _initialize_textures(self): self._clear_textures() - if not TICI: + if not COMMA_HARDWARE: self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride), int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE)) self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2), @@ -353,7 +353,7 @@ class CameraView(Widget): self.texture_uv = None # Clean up EGL resources - if TICI: + if COMMA_HARDWARE: for data in self.egl_images.values(): destroy_egl_image(data) self.egl_images = {} diff --git a/openpilot/selfdrive/ui/ui.py b/openpilot/selfdrive/ui/ui.py index ff2bc7d3ce..30bba681c3 100755 --- a/openpilot/selfdrive/ui/ui.py +++ b/openpilot/selfdrive/ui/ui.py @@ -3,7 +3,7 @@ import os import time from openpilot.cereal import messaging -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.realtime import Priority, config_realtime_process, set_core_affinity from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout @@ -31,7 +31,7 @@ def main(): if should_render: # reaffine after power save offlines our core - if TICI and os.sched_getaffinity(0) != cores: + if COMMA_HARDWARE and os.sched_getaffinity(0) != cores: try: set_core_affinity(list(cores)) except OSError: diff --git a/openpilot/system/athena/tests/test_athenad_ping.py b/openpilot/system/athena/tests/test_athenad_ping.py index d5592c40f1..33876a6235 100644 --- a/openpilot/system/athena/tests/test_athenad_ping.py +++ b/openpilot/system/athena/tests/test_athenad_ping.py @@ -8,13 +8,13 @@ from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE TIMEOUT_TOLERANCE = 20 # seconds def wifi_radio(on: bool) -> None: - if not TICI: + if not COMMA_HARDWARE: return print(f"wifi {'on' if on else 'off'}") subprocess.run(["nmcli", "radio", "wifi", "on" if on else "off"], check=True) @@ -91,12 +91,12 @@ class TestAthenadPing(OpenpilotTestCase): time.sleep(0.1) print("ping received") - @unittest.skipIf(not TICI, "only run on desk") + @unittest.skipIf(not COMMA_HARDWARE, "only run on desk") def test_offroad(self, subtests, mocker) -> None: self.params.put_bool("IsOffroad", True, block=True) self.assertTimeout(60 + TIMEOUT_TOLERANCE, subtests, mocker) # based using TCP keepalive settings - @unittest.skipIf(not TICI, "only run on desk") + @unittest.skipIf(not COMMA_HARDWARE, "only run on desk") def test_onroad(self, subtests, mocker) -> None: self.params.put_bool("IsOffroad", False, block=True) self.assertTimeout(21 + TIMEOUT_TOLERANCE, subtests, mocker) diff --git a/openpilot/system/camerad/test/test_camerad.py b/openpilot/system/camerad/test/test_camerad.py index 0d66f0e16b..580a30683b 100755 --- a/openpilot/system/camerad/test/test_camerad.py +++ b/openpilot/system/camerad/test/test_camerad.py @@ -73,7 +73,7 @@ def _camera_session(): return ts, exposure class TestCamerad(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True @classmethod def setUpClass(cls): diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 10ee0aee3b..270a75ade6 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -17,7 +17,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert -from openpilot.common.hardware import HARDWARE, TICI, PC +from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state from openpilot.common.linux import LinuxSystemStats @@ -478,7 +478,7 @@ def main(): threading.Thread(target=hardware_thread, args=(end_event, hw_queue)), ] - if TICI: + if COMMA_HARDWARE: threads.append(threading.Thread(target=touch_thread, args=(end_event,))) for t in threads: diff --git a/openpilot/system/loggerd/SConscript b/openpilot/system/loggerd/SConscript index 3f54552695..6890c29655 100644 --- a/openpilot/system/loggerd/SConscript +++ b/openpilot/system/loggerd/SConscript @@ -4,7 +4,7 @@ libs = [common, messaging, visionipc] + ffmpeg_libs + ['pthread', 'm', 'zstd'] frameworks = [] src = ['logger.cc', 'zstd_writer.cc', 'video_writer.cc', 'encoder/encoder.cc', 'encoder/jpeg_encoder.cc'] -if arch == "larch64": +if arch == "comma_arm64": src += ['clip_encoder.cc', 'encoder/v4l_encoder.cc', 'encoder/v4l_decoder.cc'] else: src += ['encoder/ffmpeg_encoder.cc'] diff --git a/openpilot/system/loggerd/encoderd.cc b/openpilot/system/loggerd/encoderd.cc index f5b69b3c42..55f0777fbb 100644 --- a/openpilot/system/loggerd/encoderd.cc +++ b/openpilot/system/loggerd/encoderd.cc @@ -1,16 +1,16 @@ #include -#ifdef __TICI__ +#ifdef __COMMA_HARDWARE__ #include #include #endif -#ifdef __TICI__ +#ifdef __COMMA_HARDWARE__ #include "system/loggerd/clip_encoder.h" #endif #include "system/loggerd/loggerd.h" #include "system/loggerd/encoder/jpeg_encoder.h" -#ifdef __TICI__ +#ifdef __COMMA_HARDWARE__ #include "system/loggerd/encoder/v4l_encoder.h" #define Encoder V4LEncoder #else @@ -178,7 +178,7 @@ void encoderd_thread(const LogCameraInfo (&cameras)[N]) { } int main(int argc, char* argv[]) { -#ifdef __TICI__ +#ifdef __COMMA_HARDWARE__ if (argc > 1 && std::string(argv[1]) == "--clip") { if (argc < 6) { fprintf(stderr, "usage: encoderd --clip OUTPUT START DURATION [--bitrate BPS] [--speedup N] " diff --git a/openpilot/system/loggerd/tests/test_encoder.py b/openpilot/system/loggerd/tests/test_encoder.py index 62c040cc19..307a89ba95 100755 --- a/openpilot/system/loggerd/tests/test_encoder.py +++ b/openpilot/system/loggerd/tests/test_encoder.py @@ -13,7 +13,7 @@ from tqdm import trange from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.common.timeout import Timeout -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.manager.process_config import managed_processes from openpilot.tools.lib.logreader import LogReader from openpilot.common.hardware.hw import Paths @@ -33,7 +33,7 @@ FILE_SIZE_TOLERANCE = 0.7 class TestEncoder(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True def setup_method(self): self._clear_logs() @@ -86,7 +86,7 @@ class TestEncoder(OpenpilotTestCase): # TODO: this ffprobe call is really slow # get width and check frame count cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets,width -of csv=p=0 {file_path}" - if TICI: + if COMMA_HARDWARE: cmd = "LD_LIBRARY_PATH=/usr/local/lib " + cmd expected_frames = fps * SEGMENT_LENGTH @@ -130,7 +130,7 @@ class TestEncoder(OpenpilotTestCase): assert 1 == len(set(first_frames)) - if TICI: + if COMMA_HARDWARE: expected_frames = fps * SEGMENT_LENGTH assert min(counts) == expected_frames shutil.rmtree(f"{route_prefix_path}--{i}") diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 618e3d0016..039bb17064 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -18,7 +18,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.common.hardware.hw import Paths -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.loggerd.xattr_cache import getxattr from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_VALUE from openpilot.system.manager.process_config import managed_processes @@ -234,7 +234,7 @@ class TestLoggerd(OpenpilotTestCase): assert abs(boot.wallTimeNanos - time.time_ns()) < 5*1e9 # within 5s assert boot.launchLog == launch_log - if TICI: + if COMMA_HARDWARE: for fn in ["console-ramoops", "pmsg-ramoops-0"]: path = Path(os.path.join("/sys/fs/pstore/", fn)) if path.is_file(): diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index a4326b4107..32d8508696 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -4,7 +4,7 @@ import platform from opendbc.car.structs import car from openpilot.common.params import Params -from openpilot.common.hardware import PC, TICI +from openpilot.common.hardware import PC, COMMA_HARDWARE from openpilot.system.manager.process import PythonProcess, NativeProcess, DaemonProcess WEBCAM = os.getenv("USE_WEBCAM") is not None @@ -101,18 +101,18 @@ procs = [ PythonProcess("card", "openpilot.selfdrive.car.card", only_onroad), PythonProcess("deleter", "openpilot.system.loggerd.deleter", always_run), PythonProcess("dmonitoringd", "openpilot.selfdrive.monitoring.dmonitoringd", driverview, enabled=(WEBCAM or not PC)), - PythonProcess("qcomgpsd", "openpilot.system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), + PythonProcess("qcomgpsd", "openpilot.system.qcomgpsd.qcomgpsd", qcomgps, enabled=COMMA_HARDWARE), PythonProcess("pandad", "openpilot.selfdrive.pandad.pandad", always_run), PythonProcess("paramsd", "openpilot.selfdrive.locationd.paramsd", only_onroad), PythonProcess("lagd", "openpilot.selfdrive.locationd.lagd", only_onroad), - PythonProcess("ubloxd", "openpilot.system.ubloxd.ubloxd", ublox, enabled=TICI), - PythonProcess("pigeond", "openpilot.system.ubloxd.pigeond", ublox, enabled=TICI), + PythonProcess("ubloxd", "openpilot.system.ubloxd.ubloxd", ublox, enabled=COMMA_HARDWARE), + PythonProcess("pigeond", "openpilot.system.ubloxd.pigeond", ublox, enabled=COMMA_HARDWARE), PythonProcess("plannerd", "openpilot.selfdrive.controls.plannerd", not_long_maneuver), PythonProcess("maneuversd", "openpilot.tools.longitudinal_maneuvers.maneuversd", long_maneuver), PythonProcess("lateral_maneuversd", "openpilot.tools.lateral_maneuvers.lateral_maneuversd", lat_maneuver), PythonProcess("radard", "openpilot.selfdrive.controls.radard", only_onroad), PythonProcess("hardwared", "openpilot.system.hardware.hardwared", always_run), - PythonProcess("modem", "openpilot.common.hardware.comma.modem", always_run, enabled=TICI), + PythonProcess("modem", "openpilot.common.hardware.comma.modem", always_run, enabled=COMMA_HARDWARE), PythonProcess("tombstoned", "openpilot.system.tombstoned", always_run, enabled=not PC), PythonProcess("updated", "openpilot.system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "openpilot.system.loggerd.uploader", always_run), diff --git a/openpilot/system/sensord/tests/test_sensord.py b/openpilot/system/sensord/tests/test_sensord.py index e1580af9bb..ddbef7895b 100755 --- a/openpilot/system/sensord/tests/test_sensord.py +++ b/openpilot/system/sensord/tests/test_sensord.py @@ -58,7 +58,7 @@ def iter_measurements(events): yield measurement, getattr(measurement, measurement.which()) class TestSensord(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True @classmethod def setup_class(cls): # enable LSM self test diff --git a/openpilot/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py index f704761e35..58fba4d4e1 100755 --- a/openpilot/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -12,7 +12,7 @@ from openpilot.common.time_helpers import system_time_valid from openpilot.common.params import Params from openpilot.common.serial import Serial from openpilot.common.swaglog import cloudlog -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.gpio import gpio_init, gpio_set from openpilot.common.hardware.comma.pins import GPIO @@ -302,7 +302,7 @@ def run_receiving(duration: int = 0): def main(): - assert TICI, "unsupported hardware for pigeond" + assert COMMA_HARDWARE, "unsupported hardware for pigeond" run_receiving() if __name__ == "__main__": diff --git a/openpilot/system/ubloxd/tests/test_pigeond.py b/openpilot/system/ubloxd/tests/test_pigeond.py index 87e3f18523..0d2f5b00e7 100644 --- a/openpilot/system/ubloxd/tests/test_pigeond.py +++ b/openpilot/system/ubloxd/tests/test_pigeond.py @@ -11,7 +11,7 @@ from openpilot.common.hardware.comma.pins import GPIO # TODO: test TTFF when we have good A-GNSS class TestPigeond(OpenpilotTestCase): - TICI_TEST = True + COMMA_HARDWARE_TEST = True def teardown_method(self): managed_processes['pigeond'].stop() diff --git a/openpilot/system/ui/lib/scroll_panel2.py b/openpilot/system/ui/lib/scroll_panel2.py index b6193672c4..19faaa542d 100644 --- a/openpilot/system/ui/lib/scroll_panel2.py +++ b/openpilot/system/ui/lib/scroll_panel2.py @@ -5,7 +5,7 @@ from collections.abc import Callable from enum import Enum from typing import cast from openpilot.system.ui.lib.application import gui_app, MouseEvent -from openpilot.common.hardware import TICI +from openpilot.common.hardware import COMMA_HARDWARE from collections import deque MIN_VELOCITY = 10 # px/s, changes from auto scroll to steady state @@ -52,7 +52,7 @@ class GuiScrollPanel2: self._initial_click_event: MouseEvent | None = None self._previous_mouse_event: MouseEvent | None = None self._velocity = 0.0 # pixels per second - self._velocity_buffer: deque[float] = deque(maxlen=12 if TICI else 6) + self._velocity_buffer: deque[float] = deque(maxlen=12 if COMMA_HARDWARE else 6) self._enabled: bool | Callable[[], bool] = True def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: diff --git a/openpilot/system/ui/mici_setup.py b/openpilot/system/ui/mici_setup.py index 465cadda0b..e4977ffdf4 100755 --- a/openpilot/system/ui/mici_setup.py +++ b/openpilot/system/ui/mici_setup.py @@ -13,7 +13,7 @@ import pyray as rl from openpilot.cereal import log from openpilot.common.filter_simple import BounceFilter -from openpilot.common.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid @@ -570,7 +570,7 @@ class Setup(Widget): def main(): config_realtime_process(0, 51) # attempt to affine. AGNOS will start setup with all cores, should only fail when manually launching with screen off - if TICI: + if COMMA_HARDWARE: try: set_core_affinity([5]) except OSError: diff --git a/openpilot/system/ui/mici_updater.py b/openpilot/system/ui/mici_updater.py index a072354cf0..d9009b8259 100755 --- a/openpilot/system/ui/mici_updater.py +++ b/openpilot/system/ui/mici_updater.py @@ -5,7 +5,7 @@ import threading import pyray as rl from openpilot.common.realtime import config_realtime_process, set_core_affinity -from openpilot.common.hardware import HARDWARE, TICI +from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.swaglog import cloudlog from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -152,7 +152,7 @@ class Updater(Scroller): def main(): config_realtime_process(0, 51) # attempt to affine. AGNOS will start setup with all cores, should only fail when manually launching with screen off - if TICI: + if COMMA_HARDWARE: try: set_core_affinity([5]) except OSError: From e32b1a344ddf746c28985784ddf7b4d118d7154d Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 7 Aug 2026 20:16:13 -0700 Subject: [PATCH 190/325] chestnut: update firmware (#38576) * chestnut: update firmware to b0805491 * update to ed4e39b7 --- openpilot/common/hardware/usb.py | 2 +- .../hardware/chestnut/firmware_wrapped.bin | Bin 9207 -> 9207 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py index e06e77b31e..1ebcf571b5 100644 --- a/openpilot/common/hardware/usb.py +++ b/openpilot/common/hardware/usb.py @@ -1,7 +1,7 @@ import os from pathlib import Path -CHESTNUT_FW_VERSION = "1d368808" +CHESTNUT_FW_VERSION = "ed4e39b7" CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) USB_DEVICES_PATH = Path("/sys/bus/usb/devices") diff --git a/openpilot/system/hardware/chestnut/firmware_wrapped.bin b/openpilot/system/hardware/chestnut/firmware_wrapped.bin index a0911e06e23c4843d3ed07592ebaf5ea7a707669..1d738e4a759d05a8905e6ddf1b5d5ee6a324b04d 100644 GIT binary patch delta 34 qcmezF{@s1U9X3XW&3D Date: Fri, 7 Aug 2026 21:08:33 -0700 Subject: [PATCH 191/325] AGNOS 19.5 (#38582) * bump version * add staging * add production * fix AGNOS manifest symlink --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 22 +++++++++++----------- openpilot/system/hardware/comma/agnos.json | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index ce7b2bb15e..a201fd6c47 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.4" + export AGNOS_VERSION="19.5" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index eb50d4efd6..3a8d03e104 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4.img.xz", - "hash": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4", - "hash_raw": "5463317ca7a231f2ddd55a667a8c2b0d6692982359773ca875a99a0e6fd67fb4", + "url": "https://commadist.azureedge.net/agnosupdate/boot-f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94.img.xz", + "hash": "f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94", + "hash_raw": "f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94", "size": 46897152, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "daefa6f897bdcc277e50dbf562fd63e57d50d9fc7a599f2d171208a490e725d9" + "ondevice_hash": "269bc216189c4532a3e796ef2f4d47d0a03e774c883203be93feb8a8ed3c3adb" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img.xz", - "hash": "14ab660bb955604ab6c482b3f59e5239e2c40c54e798201afcaed43d5cb0dcfd", - "hash_raw": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d", + "url": "https://commadist.azureedge.net/agnosupdate/system-7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2.img.xz", + "hash": "414a9145d6ba13bc04760b31545507109e9a2509958dd44782dd1912b1cb96e9", + "hash_raw": "7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "18666ca3b529304f5e7a8fc2e3c9dce715b55941e3965a7af43c613f8e18cfce", + "ondevice_hash": "d4fadd9dfb4e20875650e60e37b9f8ccf2dc6c0fdc4f9a15912bee5ab8440a13", "alt": { - "hash": "2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d", - "url": "https://commadist.azureedge.net/agnosupdate/system-2e1ca22762e66898aae384fcc21f488cb015c020b6b588862a64fc8128f0ad1d.img", + "hash": "7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2", + "url": "https://commadist.azureedge.net/agnosupdate/system-7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2.img", "size": 4718592000 } } -] \ No newline at end of file +] diff --git a/openpilot/system/hardware/comma/agnos.json b/openpilot/system/hardware/comma/agnos.json index ffee992f25..b1465ff49d 120000 --- a/openpilot/system/hardware/comma/agnos.json +++ b/openpilot/system/hardware/comma/agnos.json @@ -1 +1 @@ -../../../common/hardware/tici/agnos.json \ No newline at end of file +../../../common/hardware/comma/agnos.json \ No newline at end of file From 5701fcad05e8f88ed7eae5902cb791b66ab755b2 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 7 Aug 2026 21:11:34 -0700 Subject: [PATCH 192/325] ui: show gpu icon longer (#38586) --- openpilot/selfdrive/ui/mici/onroad/hud_renderer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index d4fe668294..0d1532057e 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -168,8 +168,8 @@ class HudRenderer(Widget): if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): self._small_model_engaged = True - if engaged and not self._engaged: - self._egpu_fade_time = rl.get_time() + if engaged != self._engaged: + self._egpu_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() self._engaged = engaged @@ -223,7 +223,7 @@ class HudRenderer(Widget): if icon is not self._egpu_icon: self._egpu_fade_time = rl.get_time() self._egpu_icon = icon - alpha = self._egpu_alpha_filter.update(loading or (0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE and self._engaged)) + alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE) if alpha < 1e-2: return From 6f2a1d573c0bcc21c0ed94033163f5d766b043b8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 8 Aug 2026 01:27:33 -0400 Subject: [PATCH 193/325] ci: sunnypilot CI test routes (#1899) --- openpilot/selfdrive/car/tests/test_models.py | 4 +- openpilot/sunnypilot/tools/lib/__init__.py | 0 .../tools/lib/sunnypilot_car_segments.py | 21 ++++ .../sunnypilot/tools/upload_ci_routes.py | 68 ++++++++++ openpilot/tools/lib/logreader.py | 4 +- pyproject.toml | 1 + uv.lock | 117 +++++++++++++++++- 7 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 openpilot/sunnypilot/tools/lib/__init__.py create mode 100644 openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py create mode 100755 openpilot/sunnypilot/tools/upload_ci_routes.py diff --git a/openpilot/selfdrive/car/tests/test_models.py b/openpilot/selfdrive/car/tests/test_models.py index b98890838f..3dc7dd8a7b 100644 --- a/openpilot/selfdrive/car/tests/test_models.py +++ b/openpilot/selfdrive/car/tests/test_models.py @@ -25,6 +25,8 @@ from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotc from openpilot.tools.lib.file_sources import Source from openpilot.tools.lib.route import SegmentName +from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source + SafetyModel = car.CarParams.SafetyModel SteerControlType = structs.CarParams.SteerControlType @@ -132,7 +134,7 @@ class TestCarModelBase(unittest.TestCase): segment_range = f"{cls.test_route.route}/{seg}" try: - sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] + sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source, sunnypilot_car_segments_source] lr = LogReader(segment_range, sources=sources, sort_by_time=True) return cls.get_testing_data_from_logreader(lr) except (LogsUnavailable, AssertionError): diff --git a/openpilot/sunnypilot/tools/lib/__init__.py b/openpilot/sunnypilot/tools/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py b/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py new file mode 100644 index 0000000000..a3a0e576cd --- /dev/null +++ b/openpilot/sunnypilot/tools/lib/sunnypilot_car_segments.py @@ -0,0 +1,21 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import os + +SUNNYPILOT_CAR_SEGMENTS_REPO = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO", + "https://huggingface.co/datasets/sunnypilot/sunnypilotCarSegments") +SUNNYPILOT_CAR_SEGMENTS_BRANCH = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_BRANCH", "main") + + +def get_url(route, segment, file="rlog.zst"): + return f"{SUNNYPILOT_CAR_SEGMENTS_REPO}/resolve/{SUNNYPILOT_CAR_SEGMENTS_BRANCH}/segments/{route.replace('|', '/')}/{segment}/{file}" + + +def sunnypilot_car_segments_source(sr, seg_idxs, fns, /): + from openpilot.tools.lib.file_sources import eval_source + return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs}) diff --git a/openpilot/sunnypilot/tools/upload_ci_routes.py b/openpilot/sunnypilot/tools/upload_ci_routes.py new file mode 100755 index 0000000000..3f511a6a72 --- /dev/null +++ b/openpilot/sunnypilot/tools/upload_ci_routes.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import argparse +import os +import tempfile + +import requests +from huggingface_hub import HfApi +from tqdm import tqdm + +from openpilot.tools.lib.route import Route + +REPO_ID = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO_ID", "sunnypilot/sunnypilotCarSegments") + + +def upload_route(route_name: str, dry_run: bool = False) -> None: + route = Route(route_name) + log_paths = route.log_paths() + valid_segments = [(i, url) for i, url in enumerate(log_paths) if url is not None] + + print(f"Route: {route_name}") + print(f"Segments: {len(valid_segments)}/{len(log_paths)}") + + if not valid_segments: + print("No segments found.") + return + + api = HfApi() + + with tempfile.TemporaryDirectory() as tmpdir: + for seg_idx, url in tqdm(valid_segments, desc="Uploading"): + filename = url.split("?")[0].rsplit("/", 1)[-1] + local_path = os.path.join(tmpdir, f"{seg_idx}_{filename}") + resp = requests.get(url, stream=True) + resp.raise_for_status() + with open(local_path, "wb") as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + + repo_path = f"segments/{route_name.replace('|', '/')}/{seg_idx}/{filename}" + + if dry_run: + size_mb = os.path.getsize(local_path) / 1024 / 1024 + print(f" [{seg_idx}] {size_mb:.1f} MB -> {repo_path}") + else: + api.upload_file( + path_or_fileobj=local_path, + path_in_repo=repo_path, + repo_id=REPO_ID, + repo_type="dataset", + ) + + print("Done.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Upload route rlogs to sunnypilot HuggingFace dataset") + parser.add_argument("route", help="Route ID (e.g. 5beb9b58bd12b691/0000010a--a51155e496)") + parser.add_argument("--dry-run", action="store_true", help="Download and show sizes without uploading") + args = parser.parse_args() + + upload_route(args.route, dry_run=args.dry_run) diff --git a/openpilot/tools/lib/logreader.py b/openpilot/tools/lib/logreader.py index 805e411b53..fbfb28dbe0 100755 --- a/openpilot/tools/lib/logreader.py +++ b/openpilot/tools/lib/logreader.py @@ -22,6 +22,8 @@ from openpilot.tools.lib.file_sources import comma_api_source, internal_source, from openpilot.tools.lib.route import SegmentRange, FileName from openpilot.tools.lib.log_time_series import msgs_to_time_series +from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source + LogMessage = type[capnp._DynamicStructReader] LogIterable = Iterable[LogMessage] RawLogIterable = Iterable[bytes] @@ -246,7 +248,7 @@ class LogReader: def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG, sources: list[Source] | None = None, sort_by_time=False, only_union_types=False): if sources is None: - sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source] + sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source, sunnypilot_car_segments_source] self.default_mode = default_mode self.sources = sources diff --git a/pyproject.toml b/pyproject.toml index d400ff5678..051eb5ffc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ testing = [ ] dev = [ + "huggingface_hub", "matplotlib", ] diff --git a/uv.lock b/uv.lock index a0a951d33d..b88ba23d7c 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,19 @@ requires-python = ">=3.12.3, <3.13" [manifest] overrides = [{ name = "opendbc", editable = "opendbc_repo" }] +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -395,6 +408,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "filelock" +version = "3.32.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, +] + [[package]] name = "fonttools" version = "4.63.0" @@ -412,6 +434,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" }, +] + [[package]] name = "hypothesis" version = "6.47.5" @@ -730,6 +834,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "huggingface-hub" }, { name = "matplotlib" }, ] docs = [ @@ -788,6 +893,7 @@ requires-dist = [ { name = "comma-deps-zstd" }, { name = "coverage", marker = "extra == 'testing'" }, { name = "cython" }, + { name = "huggingface-hub", marker = "extra == 'dev'" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, { name = "jeepney" }, @@ -1302,7 +1408,7 @@ provides-extras = ["dev"] [[package]] name = "tinygrad" -version = "0.12.0" +version = "0.13.0" source = { editable = "tinygrad_repo" } [package.metadata] @@ -1413,6 +1519,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" From cf790746ccf11bd40116f7a25c70f799f5068d3d Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:20:17 -0700 Subject: [PATCH 194/325] encoder/webrtc: resize streams to baseline 3.1 H264 profile (#38587) * add h264 profile option and different profiles for comma3X and comma4 * update to high profile * resize to be baseline 3.1 * fix * gop 15 and clean * revert gop 15 * fix white space * simplify teleoprtc * bump teleop --- .../system/loggerd/encoder/v4l_encoder.cc | 26 ++++++++++++++++--- openpilot/system/loggerd/loggerd.h | 22 ++++++++++++++++ teleoprtc_repo | 2 +- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/openpilot/system/loggerd/encoder/v4l_encoder.cc b/openpilot/system/loggerd/encoder/v4l_encoder.cc index 515468c643..0d7e744568 100644 --- a/openpilot/system/loggerd/encoder/v4l_encoder.cc +++ b/openpilot/system/loggerd/encoder/v4l_encoder.cc @@ -251,11 +251,29 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } else { + if (encoder_info.is_live) { + struct v4l2_control ctrls[] = { + { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, + { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_3_1}, + { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, + }; + for (auto ctrl : ctrls) { + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); + } + } else { + struct v4l2_control ctrls[] = { + { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, + { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN}, + { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, + }; + for (auto ctrl : ctrls) { + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); + } + } + struct v4l2_control ctrls[] = { - { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, - { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN}, - { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_ALPHA, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA, .value = 0}, diff --git a/openpilot/system/loggerd/loggerd.h b/openpilot/system/loggerd/loggerd.h index 110dbe4fd2..4d33a77095 100644 --- a/openpilot/system/loggerd/loggerd.h +++ b/openpilot/system/loggerd/loggerd.h @@ -25,6 +25,22 @@ const auto MAIN_ENCODE_TYPE = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BO const bool LOGGERD_TEST = getenv("LOGGERD_TEST"); const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) : 60; +inline int livestream_width() { + switch (Hardware::get_device_type()) { + case cereal::InitData::DeviceType::TIZI: return 1152; + case cereal::InitData::DeviceType::MICI: return 1280; + default: return -1; + } +} + +inline int livestream_height() { + switch (Hardware::get_device_type()) { + case cereal::InitData::DeviceType::TIZI: + case cereal::InitData::DeviceType::MICI: return 720; + default: return -1; + } +} + constexpr char PRESERVE_ATTR_NAME[] = "user.preserve"; constexpr char PRESERVE_ATTR_VALUE = '1'; @@ -106,6 +122,8 @@ const EncoderInfo stream_road_encoder_info = { //.thumbnail_name = "thumbnail", .record = false, .is_live = true, + .frame_width = livestream_width(), + .frame_height = livestream_height(), .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; @@ -114,6 +132,8 @@ const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", .record = false, .is_live = true, + .frame_width = livestream_width(), + .frame_height = livestream_height(), .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; @@ -122,6 +142,8 @@ const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", .record = false, .is_live = true, + .frame_width = livestream_width(), + .frame_height = livestream_height(), .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; diff --git a/teleoprtc_repo b/teleoprtc_repo index 15ecd3b62b..31db236a9e 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit 15ecd3b62ba6c29679e2f6ff44aa7845637f82f8 +Subproject commit 31db236a9ef820d7051ccd53488153cfbc84d3b9 From df0bd5e6e3702ddd8b64086a820407468972ad91 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 8 Aug 2026 08:51:58 -0700 Subject: [PATCH 195/325] update translations (#38590) --- openpilot/selfdrive/ui/translations/app.pot | 26 ++---- openpilot/selfdrive/ui/translations/app_de.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_en.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_es.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_fr.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_ja.po | 89 +++++++++--------- openpilot/selfdrive/ui/translations/app_ko.po | 89 +++++++++--------- .../selfdrive/ui/translations/app_pt-BR.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_th.po | 89 +++++++++--------- openpilot/selfdrive/ui/translations/app_tr.po | 90 +++++++++--------- openpilot/selfdrive/ui/translations/app_uk.po | 91 +++++++++---------- .../selfdrive/ui/translations/app_zh-CHS.po | 90 +++++++++--------- .../selfdrive/ui/translations/app_zh-CHT.po | 90 +++++++++--------- 13 files changed, 514 insertions(+), 590 deletions(-) diff --git a/openpilot/selfdrive/ui/translations/app.pot b/openpilot/selfdrive/ui/translations/app.pot index 3f9f86af8e..cb0491e107 100644 --- a/openpilot/selfdrive/ui/translations/app.pot +++ b/openpilot/selfdrive/ui/translations/app.pot @@ -2,7 +2,6 @@ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" -#: openpilot/selfdrive/ui/layouts/sidebar.py #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/html_render.py msgid "OK" @@ -162,8 +161,9 @@ msgstr "" msgid "Open" msgstr "" +#: openpilot/selfdrive/ui/layouts/settings/firehose.py #: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" +msgid "Firehose Mode" msgstr "" #: openpilot/selfdrive/ui/widgets/setup.py @@ -364,6 +364,10 @@ msgstr "" msgid "Unknown" msgstr "" +#: openpilot/selfdrive/ui/layouts/sidebar.py +msgid "HIGH" +msgstr "" + #: openpilot/selfdrive/ui/layouts/sidebar.py msgid "NO" msgstr "" @@ -372,10 +376,6 @@ msgstr "" msgid "PANDA" msgstr "" -#: openpilot/selfdrive/ui/layouts/sidebar.py -msgid "HIGH" -msgstr "" - #: openpilot/selfdrive/ui/layouts/sidebar.py msgid "ERROR" msgstr "" @@ -528,16 +528,6 @@ msgstr "" msgid " Steering torque response calibration is {}% complete." msgstr "" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "Firehose Mode" -msgstr "" - -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "" -msgstr[1] "" - #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Enable ADB" msgstr "" @@ -558,6 +548,10 @@ msgstr "" msgid "Longitudinal Maneuver Mode" msgstr "" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "openpilot Longitudinal Control (Alpha)" msgstr "" diff --git a/openpilot/selfdrive/ui/translations/app_de.po b/openpilot/selfdrive/ui/translations/app_de.po index 287ecde1a0..43baa133ae 100644 --- a/openpilot/selfdrive/ui/translations/app_de.po +++ b/openpilot/selfdrive/ui/translations/app_de.po @@ -52,7 +52,7 @@ msgstr "

    Kalibrierung der Lenkverzögerung zu {}% abgeschlossen." msgid "ADD" msgstr "HINZUFÜGEN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN‑Einstellung" @@ -60,7 +60,7 @@ msgstr "APN‑Einstellung" msgid "Acknowledge Excessive Actuation" msgstr "Übermäßige Betätigung bestätigen" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Erweitert" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Sind Sie sicher, dass Sie deinstallieren möchten?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Zurück" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "CHILL‑MODUS AKTIV" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "VERBINDUNG" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "VERBINDUNG" -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Abbrechen" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Getaktete Mobilfunkverbindung" @@ -213,7 +213,7 @@ msgstr "Fahrerkamera" msgid "Driving Personality" msgstr "Fahrstil" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "BEARBEITEN" @@ -242,7 +242,7 @@ msgstr "ADB aktivieren" msgid "Enable Lane Departure Warnings" msgstr "Spurverlassenswarnungen aktivieren" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Roaming aktivieren" @@ -250,7 +250,7 @@ msgstr "Roaming aktivieren" msgid "Enable SSH" msgstr "SSH aktivieren" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Tethering aktivieren" @@ -266,19 +266,19 @@ msgstr "openpilot aktivieren" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Den Schalter für die openpilot-Längsregelung (Alpha) aktivieren, um den Experimentalmodus zu erlauben." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "APN eingeben" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "SSID eingeben" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Neues Tethering‑Passwort eingeben" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Passwort eingeben" @@ -286,7 +286,7 @@ msgstr "Passwort eingeben" msgid "Enter your GitHub username" msgstr "Geben Sie Ihren GitHub‑Benutzernamen ein" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Fehler" @@ -298,7 +298,7 @@ msgstr "Experimentalmodus" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "Der Experimentalmodus ist derzeit auf diesem Fahrzeug nicht verfügbar, da der serienmäßige ACC für die Längsregelung verwendet wird." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "WIRD VERGESSEN..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Datenstrom" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehose‑Modus" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Vergessen" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "WLAN‑Netz „{}“ vergessen?" @@ -334,7 +335,7 @@ msgstr "Gehen Sie auf Ihrem Telefon zu https://connect.comma.ai" msgid "HIGH" msgstr "HOCH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Verstecktes Netzwerk" @@ -342,7 +343,7 @@ msgstr "Verstecktes Netzwerk" msgid "INSTALL" msgstr "INSTALLIEREN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP‑Adresse" @@ -362,6 +363,10 @@ msgstr "LADEN" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Längsmanövermodus" @@ -398,9 +403,8 @@ msgstr "Keine Versionshinweise verfügbar." msgid "OFFLINE" msgstr "GETRENNT" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Bitte mit WLAN verbinden, um das erste Koppeln abzuschließen" msgid "Power Off" msgstr "Ausschalten" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Verhindern Sie das Hochladen großer Datenmengen bei einer getakteten WLAN-Verbindung" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Verhindern Sie das Hochladen großer Datenmengen bei einer getakteten Mobilfunkverbindung" @@ -549,11 +553,11 @@ msgstr "WÄHLEN" msgid "SSH Keys" msgstr "SSH‑Schlüssel" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "WLAN‑Netzwerke werden gesucht..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Auswählen" @@ -597,7 +601,7 @@ msgstr "TEMP." msgid "Target Branch" msgstr "Zielzweig" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Tethering‑Passwort" @@ -665,11 +669,11 @@ msgstr "Wenn aktiviert, deaktiviert das Drücken des Gaspedals openpilot." msgid "Wi-Fi" msgstr "WLAN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Getaktetes WLAN‑Netzwerk" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Falsches Passwort" @@ -693,7 +697,7 @@ msgstr "Überprüfung..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "Standard" @@ -713,7 +717,7 @@ msgstr "Überprüfung auf Updates fehlgeschlagen" msgid "finalizing update..." msgstr "Update wird finalisiert..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "für „{}“" @@ -721,7 +725,7 @@ msgstr "für „{}“" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "für automatische Konfiguration leer lassen" @@ -729,7 +733,7 @@ msgstr "für automatische Konfiguration leer lassen" msgid "left" msgstr "links" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "getaktet" @@ -765,7 +769,7 @@ msgstr "openpilot erfordert, dass das Gerät innerhalb von 4° nach links oder r msgid "right" msgstr "rechts" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "unbegrenzt" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "vor {} Minute" msgstr[1] "vor {} Minuten" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} Segment Ihrer Fahrten ist bisher im Trainingsdatensatz." -msgstr[1] "{} Segmente Ihrer Fahrten sind bisher im Trainingsdatensatz." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ ABONNIERT" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose‑Modus 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_en.po b/openpilot/selfdrive/ui/translations/app_en.po index 9f99c42b11..3e578de6a4 100644 --- a/openpilot/selfdrive/ui/translations/app_en.po +++ b/openpilot/selfdrive/ui/translations/app_en.po @@ -52,7 +52,7 @@ msgstr "

    Steering lag calibration is {}% complete." msgid "ADD" msgstr "ADD" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN Setting" @@ -60,7 +60,7 @@ msgstr "APN Setting" msgid "Acknowledge Excessive Actuation" msgstr "Acknowledge Excessive Actuation" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Advanced" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Are you sure you want to uninstall?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Back" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "CHILL MODE ON" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "CONNECT" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "CONNECTING..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Cancel" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Cellular Metered" @@ -213,7 +213,7 @@ msgstr "Driver Camera" msgid "Driving Personality" msgstr "Driving Personality" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "EDIT" @@ -242,7 +242,7 @@ msgstr "Enable ADB" msgid "Enable Lane Departure Warnings" msgstr "Enable Lane Departure Warnings" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Enable Roaming" @@ -250,7 +250,7 @@ msgstr "Enable Roaming" msgid "Enable SSH" msgstr "Enable SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Enable Tethering" @@ -266,19 +266,19 @@ msgstr "Enable openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "Enter APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "Enter SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Enter new tethering password" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Enter password" @@ -286,7 +286,7 @@ msgstr "Enter password" msgid "Enter your GitHub username" msgstr "Enter your GitHub username" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Error" @@ -298,7 +298,7 @@ msgstr "Experimental Mode" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "FORGETTING..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Firehose" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehose Mode" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Forget" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Forget Wi-Fi Network \"{}\"?" @@ -334,7 +335,7 @@ msgstr "Go to https://connect.comma.ai on your phone" msgid "HIGH" msgstr "HIGH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Hidden Network" @@ -342,7 +343,7 @@ msgstr "Hidden Network" msgid "INSTALL" msgstr "INSTALL" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP Address" @@ -362,6 +363,10 @@ msgstr "LOADING" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Longitudinal Maneuver Mode" @@ -398,9 +403,8 @@ msgstr "No release notes available." msgid "OFFLINE" msgstr "OFFLINE" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Please connect to Wi-Fi to complete initial pairing" msgid "Power Off" msgstr "Power Off" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Prevent large data uploads when on a metered Wi-Fi connection" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Prevent large data uploads when on a metered cellular connection" @@ -549,11 +553,11 @@ msgstr "SELECT" msgid "SSH Keys" msgstr "SSH Keys" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Scanning Wi-Fi networks..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Select" @@ -597,7 +601,7 @@ msgstr "TEMP" msgid "Target Branch" msgstr "Target Branch" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Tethering Password" @@ -665,11 +669,11 @@ msgstr "When enabled, pressing the accelerator pedal will disengage openpilot." msgid "Wi-Fi" msgstr "Wi-Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Wi-Fi Network Metered" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Wrong password" @@ -693,7 +697,7 @@ msgstr "checking..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "default" @@ -713,7 +717,7 @@ msgstr "failed to check for update" msgid "finalizing update..." msgstr "finalizing update..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "for \"{}\"" @@ -721,7 +725,7 @@ msgstr "for \"{}\"" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "leave blank for automatic configuration" @@ -729,7 +733,7 @@ msgstr "leave blank for automatic configuration" msgid "left" msgstr "left" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "metered" @@ -765,7 +769,7 @@ msgstr "openpilot requires the device to be mounted within 4° left or right and msgid "right" msgstr "right" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "unmetered" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "{} minute ago" msgstr[1] "{} minutes ago" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} segment of your driving is in the training dataset so far." -msgstr[1] "{} segments of your driving is in the training dataset so far." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ SUBSCRIBED" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose Mode 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_es.po b/openpilot/selfdrive/ui/translations/app_es.po index 707816bc00..e1c74f53c2 100644 --- a/openpilot/selfdrive/ui/translations/app_es.po +++ b/openpilot/selfdrive/ui/translations/app_es.po @@ -52,7 +52,7 @@ msgstr "

    La calibración del retraso de dirección está completa en un { msgid "ADD" msgstr "AÑADIR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "Configuración de APN" @@ -60,7 +60,7 @@ msgstr "Configuración de APN" msgid "Acknowledge Excessive Actuation" msgstr "Reconocer actuación excesiva" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Avanzado" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "¿Seguro que quieres desinstalar?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Atrás" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "MODO CHILL ACTIVADO" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "CONECTAR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "CONECTAR" -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Cancelar" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Medición celular" @@ -213,7 +213,7 @@ msgstr "Cámara del conductor" msgid "Driving Personality" msgstr "Estilo de conducción" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "EDITAR" @@ -242,7 +242,7 @@ msgstr "Activar ADB" msgid "Enable Lane Departure Warnings" msgstr "Activar advertencias de salida de carril" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Activar roaming" @@ -250,7 +250,7 @@ msgstr "Activar roaming" msgid "Enable SSH" msgstr "Activar SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Activar anclaje" @@ -266,19 +266,19 @@ msgstr "Activar openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Activa el interruptor de control longitudinal de openpilot (alpha) para permitir el modo Experimental." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "Introduce APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "Introduzca SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Ingrese una nueva contraseña de anclaje a red" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Introduce la contraseña" @@ -286,7 +286,7 @@ msgstr "Introduce la contraseña" msgid "Enter your GitHub username" msgstr "Introduce tu nombre de usuario de GitHub" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Fallo" @@ -298,7 +298,7 @@ msgstr "Modo experimental" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "El modo experimental no está disponible actualmente en este coche, ya que se usa el ACC de fábrica para el control longitudinal." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "OLVIDAR..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Flujo masivo" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Modo Firehose" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Olvidar" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "¿Olvidaste la red Wi-Fi \"{}\"?" @@ -334,7 +335,7 @@ msgstr "Ve a https://connect.comma.ai en tu teléfono" msgid "HIGH" msgstr "ALTO" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Red oculta" @@ -342,7 +343,7 @@ msgstr "Red oculta" msgid "INSTALL" msgstr "INSTALAR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "Dirección IP" @@ -362,6 +363,10 @@ msgstr "CARGANDO" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Modo de maniobra longitudinal" @@ -398,9 +403,8 @@ msgstr "No hay notas de versión disponibles." msgid "OFFLINE" msgstr "SIN CONEXIÓN" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Conéctate a Wi‑Fi para completar el emparejamiento inicial" msgid "Power Off" msgstr "Apagar" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Evite grandes cargas de datos cuando esté en una conexión Wi-Fi medida" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Evite grandes cargas de datos cuando esté en una conexión celular medida" @@ -549,11 +553,11 @@ msgstr "SELECCIONAR" msgid "SSH Keys" msgstr "Claves SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Escaneando redes Wi-Fi..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Seleccionar" @@ -597,7 +601,7 @@ msgstr "TEMP." msgid "Target Branch" msgstr "Rama objetivo" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Contraseña de anclaje" @@ -665,11 +669,11 @@ msgstr "Cuando está activado, al presionar el pedal del acelerador se desactiva msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Red Wi-Fi medida" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Contraseña incorrecta" @@ -693,7 +697,7 @@ msgstr "de cheques..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "por defecto" @@ -713,7 +717,7 @@ msgstr "Error al buscar actualizaciones" msgid "finalizing update..." msgstr "finalizando actualización..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "para \"{}\"" @@ -721,7 +725,7 @@ msgstr "para \"{}\"" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "dejar en blanco para configuración automática" @@ -729,7 +733,7 @@ msgstr "dejar en blanco para configuración automática" msgid "left" msgstr "izquierda" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "medido" @@ -765,7 +769,7 @@ msgstr "openpilot requiere que el dispositivo esté montado dentro de 4° a izqu msgid "right" msgstr "derecha" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "sin medir" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "hace {} minuto" msgstr[1] "hace {} minutos" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} segmento de tu conducción está en el conjunto de entrenamiento hasta ahora." -msgstr[1] "{} segmentos de tu conducción están en el conjunto de entrenamiento hasta ahora." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ SUSCRITO" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Modo Firehose 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_fr.po b/openpilot/selfdrive/ui/translations/app_fr.po index 7c0aecc9ec..b76018394f 100644 --- a/openpilot/selfdrive/ui/translations/app_fr.po +++ b/openpilot/selfdrive/ui/translations/app_fr.po @@ -52,7 +52,7 @@ msgstr "

    L'étalonnage du délai de réponse de la direction est terminé msgid "ADD" msgstr "AJOUTER" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "Paramètres APN" @@ -60,7 +60,7 @@ msgstr "Paramètres APN" msgid "Acknowledge Excessive Actuation" msgstr "Accuser réception d'actionnement excessif" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Avancé" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Êtes-vous sûr de vouloir désinstaller ?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Retour" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "MODE CHILL ACTIVÉ" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "CONNECTER" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "CONNECTER..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Annuler" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Données cellulaire limitées" @@ -213,7 +213,7 @@ msgstr "Caméra conducteur" msgid "Driving Personality" msgstr "Personnalité de conduite" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "EDITER" @@ -242,7 +242,7 @@ msgstr "Activer ADB" msgid "Enable Lane Departure Warnings" msgstr "Activer les alertes de sortie de voie" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Activer openpilot" @@ -250,7 +250,7 @@ msgstr "Activer openpilot" msgid "Enable SSH" msgstr "Activer SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Activer les alertes de sortie de voie" @@ -266,19 +266,19 @@ msgstr "Activer openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Activez l'option de contrôle longitudinal openpilot (alpha) pour autoriser le mode expérimental." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "Saisir l'APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "Entrer le SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Saisir le mot de passe du partage de connexion" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Saisir le mot de passe" @@ -286,7 +286,7 @@ msgstr "Saisir le mot de passe" msgid "Enter your GitHub username" msgstr "Entrez votre nom d'utilisateur GitHub" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Erreur" @@ -298,7 +298,7 @@ msgstr "Mode expérimental" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "Le mode expérimental est actuellement indisponible sur cette voiture car l'ACC d'origine est utilisé pour le contrôle longitudinal." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "OUBLIER..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Flux continu" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Mode Firehose" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Oublier" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Oublier le réseau Wi-Fi \"{}\" ?" @@ -334,7 +335,7 @@ msgstr "Allez sur https://connect.comma.ai sur votre téléphone" msgid "HIGH" msgstr "ÉLEVÉ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Réseau" @@ -342,7 +343,7 @@ msgstr "Réseau" msgid "INSTALL" msgstr "INSTALLER" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "Adresse IP" @@ -362,6 +363,10 @@ msgstr "CHARGEMENT" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Mode de manœuvre longitudinale" @@ -398,9 +403,8 @@ msgstr "Aucune note de version disponible." msgid "OFFLINE" msgstr "HORS LIGNE" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Veuillez vous connecter au Wi‑Fi pour terminer l'association initiale" msgid "Power Off" msgstr "Éteindre" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Eviter les transferts de données volumineux lorsque vous êtes connecté à un réseau Wi-Fi limité" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Eviter les transferts de données volumineux lors d'une connexion à un réseau cellulaire limité" @@ -549,11 +553,11 @@ msgstr "SELECTIONNER" msgid "SSH Keys" msgstr "Clefs SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Analyse des réseaux Wi-Fi..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Sélectionner" @@ -597,7 +601,7 @@ msgstr "TEMPÉRATURE" msgid "Target Branch" msgstr "Branche cible" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Mot de passe du partage de connexion" @@ -665,11 +669,11 @@ msgstr "Lorsque activé, appuyer sur la pédale d'accélérateur désengagera op msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Réseau Wi-Fi limité" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Mauvais mot de passe" @@ -693,7 +697,7 @@ msgstr "vérification..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "défaut" @@ -713,7 +717,7 @@ msgstr "échec de la vérification de mise à jour" msgid "finalizing update..." msgstr "finalisation de la mise à jour..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "pour \"{}\"" @@ -721,7 +725,7 @@ msgstr "pour \"{}\"" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "ne pas remplir pour une configuration automatique" @@ -729,7 +733,7 @@ msgstr "ne pas remplir pour une configuration automatique" msgid "left" msgstr "gauche" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "limité" @@ -765,7 +769,7 @@ msgstr "openpilot exige que l'appareil soit monté à moins de 4° à gauche ou msgid "right" msgstr "droite" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "non limité" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "il y a {} minute" msgstr[1] "il y a {} minutes" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} segment de votre conduite est dans l'ensemble d'entraînement jusqu'à présent." -msgstr[1] "{} segments de votre conduite sont dans l'ensemble d'entraînement jusqu'à présent." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ ABONNÉ" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Mode Firehose 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_ja.po b/openpilot/selfdrive/ui/translations/app_ja.po index 78d3cf17c6..4fbdb6fe80 100644 --- a/openpilot/selfdrive/ui/translations/app_ja.po +++ b/openpilot/selfdrive/ui/translations/app_ja.po @@ -52,7 +52,7 @@ msgstr "

    ステアリング遅延のキャリブレーションは{}%完 msgid "ADD" msgstr "追加" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN設定" @@ -60,7 +60,7 @@ msgstr "APN設定" msgid "Acknowledge Excessive Actuation" msgstr "過度な作動を承認" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "詳細設定" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "本当にアンインストールしますか?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "戻る" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "チルモードON" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "接続" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "接続中..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "キャンセル" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "従量課金の携帯回線" @@ -213,7 +213,7 @@ msgstr "ドライバーカメラ" msgid "Driving Personality" msgstr "走行性格" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "編集" @@ -242,7 +242,7 @@ msgstr "ADBを有効化" msgid "Enable Lane Departure Warnings" msgstr "車線逸脱警報を有効化" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "ローミングを有効化" @@ -250,7 +250,7 @@ msgstr "ローミングを有効化" msgid "Enable SSH" msgstr "SSHを有効化" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "テザリングを有効化" @@ -266,19 +266,19 @@ msgstr "openpilotを有効化" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "openpilot縦制御(アルファ)のトグルを有効にすると実験モードが使用できます。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "APNを入力" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "SSIDを入力" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "新しいテザリングのパスワードを入力" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "パスワードを入力" @@ -286,7 +286,7 @@ msgstr "パスワードを入力" msgid "Enter your GitHub username" msgstr "GitHubユーザー名を入力" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "エラー" @@ -298,7 +298,7 @@ msgstr "実験モード" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "この車では縦制御に純正ACCを使用するため、現在実験モードは利用できません。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "削除中..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "大量配信" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehoseモード" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "削除" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Wi‑Fiネットワーク「{}」を削除しますか?" @@ -334,7 +335,7 @@ msgstr "スマートフォンで https://connect.comma.ai にアクセス" msgid "HIGH" msgstr "高温" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "非公開ネットワーク" @@ -342,7 +343,7 @@ msgstr "非公開ネットワーク" msgid "INSTALL" msgstr "インストール" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IPアドレス" @@ -362,6 +363,10 @@ msgstr "読み込み中" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "縦制御マヌーバーモード" @@ -398,9 +403,8 @@ msgstr "リリースノートはありません。" msgid "OFFLINE" msgstr "オフライン" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "初回ペアリングを完了するにはWi‑Fiに接続してくだ msgid "Power Off" msgstr "電源オフ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "従量課金のWi‑Fi接続時は大きなデータのアップロードを抑制" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "従量課金の携帯回線接続時は大きなデータのアップロードを抑制" @@ -549,11 +553,11 @@ msgstr "選択" msgid "SSH Keys" msgstr "SSH鍵" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Wi‑Fiネットワークを検索中..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "選択" @@ -597,7 +601,7 @@ msgstr "温度" msgid "Target Branch" msgstr "対象ブランチ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "テザリングのパスワード" @@ -665,11 +669,11 @@ msgstr "有効にすると、アクセルを踏むとopenpilotが解除されま msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Wi‑Fiネットワーク(従量課金)" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "パスワードが違います" @@ -693,7 +697,7 @@ msgstr "チェック中..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "既定" @@ -713,7 +717,7 @@ msgstr "アップデートの確認に失敗しました" msgid "finalizing update..." msgstr "アップデートを終了しています..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "「{}」向け" @@ -721,7 +725,7 @@ msgstr "「{}」向け" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "自動設定の場合は空欄のままにしてください" @@ -729,7 +733,7 @@ msgstr "自動設定の場合は空欄のままにしてください" msgid "left" msgstr "左" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "従量" @@ -765,7 +769,7 @@ msgstr "openpilotでは、デバイスの取り付け角度が左右±4°、上 msgid "right" msgstr "右" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "非従量" @@ -805,16 +809,7 @@ msgid "{} minute ago" msgid_plural "{} minutes ago" msgstr[0] "{}分前" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "これまでにあなたの走行の{}セグメントが学習データセットに含まれています。" - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ 登録済み" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehoseモード 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_ko.po b/openpilot/selfdrive/ui/translations/app_ko.po index 24306ae02a..29d297be88 100644 --- a/openpilot/selfdrive/ui/translations/app_ko.po +++ b/openpilot/selfdrive/ui/translations/app_ko.po @@ -52,7 +52,7 @@ msgstr "

    스티어링 지연 보정이 {}% 완료되었습니다." msgid "ADD" msgstr "추가" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN 설정" @@ -60,7 +60,7 @@ msgstr "APN 설정" msgid "Acknowledge Excessive Actuation" msgstr "과도한 작동을 확인" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "고급" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "정말 제거하시겠습니까?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "뒤로" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "안정적 모드 켜짐" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "연결" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "연결 중..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "취소" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "종량제 셀룰러" @@ -213,7 +213,7 @@ msgstr "운전자 카메라" msgid "Driving Personality" msgstr "주행 성향" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "편집" @@ -242,7 +242,7 @@ msgstr "ADB 사용" msgid "Enable Lane Departure Warnings" msgstr "차선 이탈 경고 사용" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "로밍 사용" @@ -250,7 +250,7 @@ msgstr "로밍 사용" msgid "Enable SSH" msgstr "SSH 사용" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "테더링 사용" @@ -266,19 +266,19 @@ msgstr "openpilot 사용" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "실험 모드를 사용하려면 openpilot 롱컨 제어(알파) 토글을 켜세요." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "APN 입력" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "SSID 입력" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "새 테더링 비밀번호 입력" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "비밀번호 입력" @@ -286,7 +286,7 @@ msgstr "비밀번호 입력" msgid "Enter your GitHub username" msgstr "GitHub 사용자 이름 입력" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "오류" @@ -298,7 +298,7 @@ msgstr "실험 모드" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "이 차량은 롱컨 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습니다." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "삭제 중..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "파이어호스" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "파이어호스 모드" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "삭제" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Wi‑Fi 네트워크 \"{}\"를 삭제하시겠습니까?" @@ -334,7 +335,7 @@ msgstr "휴대폰에서 https://connect.comma.ai 에 접속하세요" msgid "HIGH" msgstr "높음" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "숨겨진 네트워크" @@ -342,7 +343,7 @@ msgstr "숨겨진 네트워크" msgid "INSTALL" msgstr "설치" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP 주소" @@ -362,6 +363,10 @@ msgstr "로딩 중" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "롱컨 기동 모드" @@ -398,9 +403,8 @@ msgstr "릴리스 노트가 없습니다." msgid "OFFLINE" msgstr "오프라인" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "확인" @@ -453,11 +457,11 @@ msgstr "초기 페어링을 완료하려면 Wi‑Fi에 연결하세요" msgid "Power Off" msgstr "전원 끄기" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "종량제 Wi‑Fi 연결 시 대용량 업로드 방지" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "종량제 셀룰러 연결 시 대용량 업로드 방지" @@ -549,11 +553,11 @@ msgstr "선택" msgid "SSH Keys" msgstr "SSH 키" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Wi‑Fi 네트워크 검색 중..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "선택" @@ -597,7 +601,7 @@ msgstr "온도" msgid "Target Branch" msgstr "대상 브랜치" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "테더링 비밀번호" @@ -665,11 +669,11 @@ msgstr "이 옵션을 켜면 가속 페달을 밟을 때 openpilot이 해제됩 msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Wi‑Fi 네트워크 종량제" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "비밀번호가 올바르지 않습니다" @@ -693,7 +697,7 @@ msgstr "확인 중..." msgid "comma prime" msgstr "comma 프라임" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "기본값" @@ -713,7 +717,7 @@ msgstr "업데이트 확인 실패" msgid "finalizing update..." msgstr "업데이트 마무리 중..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "\"{}\"용" @@ -721,7 +725,7 @@ msgstr "\"{}\"용" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "자동 구성을 사용하려면 비워 두세요" @@ -729,7 +733,7 @@ msgstr "자동 구성을 사용하려면 비워 두세요" msgid "left" msgstr "왼쪽" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "종량제" @@ -765,7 +769,7 @@ msgstr "openpilot은 장치를 좌우 4°, 위쪽 5°, 아래쪽 9° 이내로 msgid "right" msgstr "오른쪽" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "비종량제" @@ -805,16 +809,7 @@ msgid "{} minute ago" msgid_plural "{} minutes ago" msgstr[0] "{}분 전" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "현재까지 귀하의 주행 {}구간이 학습 데이터셋에 포함되었습니다." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ 구독됨" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 파이어호스 모드 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_pt-BR.po b/openpilot/selfdrive/ui/translations/app_pt-BR.po index 58f2094479..29a95b22fd 100644 --- a/openpilot/selfdrive/ui/translations/app_pt-BR.po +++ b/openpilot/selfdrive/ui/translations/app_pt-BR.po @@ -52,7 +52,7 @@ msgstr "

    A calibração da latência da direção está {}% concluída." msgid "ADD" msgstr "ADICIONAR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "Configuração de APN" @@ -60,7 +60,7 @@ msgstr "Configuração de APN" msgid "Acknowledge Excessive Actuation" msgstr "Reconhecer Atuação Excessiva" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Avançado" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Tem certeza de que deseja desinstalar?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Voltar" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "MODO CHILL ATIVO" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "CONECTAR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "CONECTANDO..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Cancelar" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Dados móveis limitados" @@ -213,7 +213,7 @@ msgstr "Câmera do Motorista" msgid "Driving Personality" msgstr "Personalidade" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "EDITAR" @@ -242,7 +242,7 @@ msgstr "Ativar ADB" msgid "Enable Lane Departure Warnings" msgstr "Ativar alertas de saída de faixa" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Ativar roaming" @@ -250,7 +250,7 @@ msgstr "Ativar roaming" msgid "Enable SSH" msgstr "Ativar SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Ativar compartilhamento" @@ -266,19 +266,19 @@ msgstr "Ativar openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Ative a opção de controle longitudinal do openpilot (alpha) para permitir o Modo Experimental." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "Digite APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "Digite SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Digite nova senha tethering" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Digite a senha" @@ -286,7 +286,7 @@ msgstr "Digite a senha" msgid "Enter your GitHub username" msgstr "Digite seu nome de usuário do GitHub" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Erro" @@ -298,7 +298,7 @@ msgstr "Modo Experimental" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "O Modo Experimental está indisponível neste carro pois o ACC original do carro é usado para controle longitudinal." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "ESQUECENDO..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Fluxo contínuo" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Modo Firehose" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Esquecer" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Esquecer rede Wi-Fi \"{}\"?" @@ -334,7 +335,7 @@ msgstr "Acesse https://connect.comma.ai no seu telefone" msgid "HIGH" msgstr "ALTO" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Rede oculta" @@ -342,7 +343,7 @@ msgstr "Rede oculta" msgid "INSTALL" msgstr "INSTALAR" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "Endereço IP" @@ -362,6 +363,10 @@ msgstr "CARREGANDO" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Modo de Manobra Longitudinal" @@ -398,9 +403,8 @@ msgstr "Sem notas de versão disponíveis." msgid "OFFLINE" msgstr "OFF-LINE" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Conecte-se ao Wi‑Fi para concluir o emparelhamento inicial" msgid "Power Off" msgstr "Desligar" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Evitar uploads grandes de dados em conexões Wi-Fi limitadas" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Evitar uploads grandes de dados em conexões móveis limitadas" @@ -549,11 +553,11 @@ msgstr "SELECIONAR" msgid "SSH Keys" msgstr "Chaves SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Procurando redes Wi-Fi..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Selecione" @@ -597,7 +601,7 @@ msgstr "TEMP." msgid "Target Branch" msgstr "Branch Alvo" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Senha Tethering" @@ -665,11 +669,11 @@ msgstr "Quando ativado, pressionar o pedal do acelerador desengajará o openpilo msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Rede Wi-Fi limitada" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Senha errada" @@ -693,7 +697,7 @@ msgstr "verificando..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "padrão" @@ -713,7 +717,7 @@ msgstr "falha ao verificar atualização" msgid "finalizing update..." msgstr "finalizando atualização..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "para \"{}\"" @@ -721,7 +725,7 @@ msgstr "para \"{}\"" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "deixe em branco para configuração automática" @@ -729,7 +733,7 @@ msgstr "deixe em branco para configuração automática" msgid "left" msgstr "à esquerda" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "limitados" @@ -765,7 +769,7 @@ msgstr "o openpilot requer que o dispositivo seja montado dentro de 4° para a e msgid "right" msgstr "à direita" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "ilimitados" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "{} minuto atrás" msgstr[1] "{} minutos atrás" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} segmento da sua condução está no conjunto de treinamento até agora." -msgstr[1] "{} segmentos da sua condução estão no conjunto de treinamento até agora." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ ASSINADO" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Modo Firehose 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_th.po b/openpilot/selfdrive/ui/translations/app_th.po index 4e45cae14b..59cc8df33d 100644 --- a/openpilot/selfdrive/ui/translations/app_th.po +++ b/openpilot/selfdrive/ui/translations/app_th.po @@ -52,7 +52,7 @@ msgstr "

    การปรับเทียบความล่าช้ msgid "ADD" msgstr "เพิ่ม" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "การตั้งค่า APN" @@ -60,7 +60,7 @@ msgstr "การตั้งค่า APN" msgid "Acknowledge Excessive Actuation" msgstr "รับทราบการดำเนินการที่มากเกินไป" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "ขั้นสูง" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้ง?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "กลับ" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "เปิดโหมด Chill" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "เชื่อมต่อ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "กำลังเชื่อมต่อ..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "ยกเลิก" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "เซลล์วัดแสง" @@ -213,7 +213,7 @@ msgstr "กล้องไดร์เวอร์" msgid "Driving Personality" msgstr "บุคลิกภาพในการขับขี่" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "แก้ไข" @@ -242,7 +242,7 @@ msgstr "เปิดใช้งาน ADB" msgid "Enable Lane Departure Warnings" msgstr "เปิดใช้งานคำเตือนการออกนอกเลน" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "เปิดใช้งานโรมมิ่ง" @@ -250,7 +250,7 @@ msgstr "เปิดใช้งานโรมมิ่ง" msgid "Enable SSH" msgstr "เปิดใช้งาน SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "เปิดใช้งานการปล่อยสัญญาณ" @@ -266,19 +266,19 @@ msgstr "เปิดใช้งานโอเพ่นไพลอต" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "เปิดใช้งานการสลับการควบคุมตามยาวของ openpilot (อัลฟา) เพื่ออนุญาตโหมดการทดลอง" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "ป้อน APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "ป้อน SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "ป้อนรหัสผ่านการปล่อยสัญญาณใหม่" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "ใส่รหัสผ่าน" @@ -286,7 +286,7 @@ msgstr "ใส่รหัสผ่าน" msgid "Enter your GitHub username" msgstr "ป้อนชื่อผู้ใช้ GitHub ของคุณ" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "ข้อผิดพลาด" @@ -298,7 +298,7 @@ msgstr "โหมดทดลอง" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "ขณะนี้โหมดทดลองไม่สามารถใช้งานได้บนรถคันนี้ เนื่องจาก ACC ในสต็อกของรถใช้สำหรับการควบคุมตามยาว" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "กำลังลืม..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "สายดับเพลิง" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "โหมดสายดับเพลิง" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "ลืม" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "ลืมเครือข่าย Wi-Fi \"{}\" หรือไม่" @@ -334,7 +335,7 @@ msgstr "ไปที่ https://connect.comma.ai บนโทรศัพท์ msgid "HIGH" msgstr "สูง" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "เครือข่ายที่ซ่อนอยู่" @@ -342,7 +343,7 @@ msgstr "เครือข่ายที่ซ่อนอยู่" msgid "INSTALL" msgstr "ติดตั้ง" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "ที่อยู่ IP" @@ -362,6 +363,10 @@ msgstr "กำลังโหลด" msgid "LTE" msgstr "แอลทีที" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "โหมดการซ้อมรบตามยาว" @@ -398,9 +403,8 @@ msgstr "ไม่มีบันทึกประจำรุ่น" msgid "OFFLINE" msgstr "ออฟไลน์" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "ตกลง" @@ -453,11 +457,11 @@ msgstr "โปรดเชื่อมต่อ Wi-Fi เพื่อทำก msgid "Power Off" msgstr "ปิดเครื่อง" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "ป้องกันการอัปโหลดข้อมูลขนาดใหญ่เมื่อใช้การเชื่อมต่อ Wi-Fi แบบมิเตอร์" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "ป้องกันการอัพโหลดข้อมูลขนาดใหญ่เมื่อใช้การเชื่อมต่อมือถือแบบคิดค่าบริการตามปริมาณข้อมูล" @@ -549,11 +553,11 @@ msgstr "เลือก" msgid "SSH Keys" msgstr "คีย์ SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "กำลังสแกนเครือข่าย Wi-Fi..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "เลือก" @@ -597,7 +601,7 @@ msgstr "อุณหภูมิ" msgid "Target Branch" msgstr "สาขาเป้าหมาย" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "รหัสผ่านการแชร์อินเทอร์เน็ต" @@ -665,11 +669,11 @@ msgstr "เมื่อเปิดใช้งาน การกดแป้ msgid "Wi-Fi" msgstr "อินเตอร์เน็ตไร้สาย" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "เครือข่าย Wi-Fi มีการตรวจวัด" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "รหัสผ่านผิด" @@ -693,7 +697,7 @@ msgstr "กำลังตรวจสอบ..." msgid "comma prime" msgstr "เครื่องหมายลูกน้ำเฉพาะ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "ค่าเริ่มต้น" @@ -713,7 +717,7 @@ msgstr "ไม่สามารถตรวจสอบการอัปเด msgid "finalizing update..." msgstr "กำลังสรุปการอัปเดต..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "สำหรับ \"{}\"" @@ -721,7 +725,7 @@ msgstr "สำหรับ \"{}\"" msgid "km/h" msgstr "กม./ชม" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "เว้นว่างไว้เพื่อกำหนดค่าอัตโนมัติ" @@ -729,7 +733,7 @@ msgstr "เว้นว่างไว้เพื่อกำหนดค่า msgid "left" msgstr "ซ้าย" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "คิดค่าบริการตามปริมาณ" @@ -765,7 +769,7 @@ msgstr "openpilot ต้องติดตั้งอุปกรณ์ให msgid "right" msgstr "ขวา" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "ไม่จำกัดปริมาณ" @@ -805,16 +809,7 @@ msgid "{} minute ago" msgid_plural "{} minutes ago" msgstr[0] "{} นาทีที่แล้ว" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "ขณะนี้มีช่วงการขับขี่ของคุณ {} ช่วงอยู่ในชุดข้อมูลฝึกสอนแล้ว" - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ สมัครแล้ว" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 โหมด Firehose 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_tr.po b/openpilot/selfdrive/ui/translations/app_tr.po index dbb5b325a6..cf15e2c806 100644 --- a/openpilot/selfdrive/ui/translations/app_tr.po +++ b/openpilot/selfdrive/ui/translations/app_tr.po @@ -52,7 +52,7 @@ msgstr "

    Direksiyon gecikmesi kalibrasyonu %{} tamamlandı." msgid "ADD" msgstr "EKLE" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN Ayarı" @@ -60,7 +60,7 @@ msgstr "APN Ayarı" msgid "Acknowledge Excessive Actuation" msgstr "Aşırı Müdahaleyi Onayla" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Gelişmiş" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Kaldırmak istediğinizden emin misiniz?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Geri" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "CHILL MODU AÇIK" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "BAĞLAN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "BAĞLAN" -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "İptal" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Ölçülü Hücresel" @@ -213,7 +213,7 @@ msgstr "Sürücü Kamerası" msgid "Driving Personality" msgstr "Sürüş Kişiliği" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "DÜZENLE" @@ -242,7 +242,7 @@ msgstr "ADB'yi Etkinleştir" msgid "Enable Lane Departure Warnings" msgstr "Şerit Terk Uyarılarını Etkinleştir" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Dolaşımı Etkinleştir" @@ -250,7 +250,7 @@ msgstr "Dolaşımı Etkinleştir" msgid "Enable SSH" msgstr "SSH'yi Etkinleştir" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "İnternet Paylaşımını Etkinleştir" @@ -266,19 +266,19 @@ msgstr "openpilot'u etkinleştir" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Deneysel modu etkinleştirmek için openpilot boylamsal kontrolünü (alfa) açın." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "APN girin" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "SSID girin" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Yeni internet paylaşımı şifresini girin" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Şifre girin" @@ -286,7 +286,7 @@ msgstr "Şifre girin" msgid "Enter your GitHub username" msgstr "GitHub kullanıcı adınızı girin" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Hata" @@ -298,7 +298,7 @@ msgstr "Deneysel Mod" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "Bu araçta boylamsal kontrol için stok ACC kullanıldığından şu anda Deneysel mod kullanılamıyor." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "UNUTULUYOR..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Yoğun veri akışı" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehose Modu" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Unut" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "\"{}\" Wi‑Fi ağı unutulsun mu?" @@ -334,7 +335,7 @@ msgstr "Telefonunuzda https://connect.comma.ai adresine gidin" msgid "HIGH" msgstr "YÜKSEK" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Gizli Ağ" @@ -342,7 +343,7 @@ msgstr "Gizli Ağ" msgid "INSTALL" msgstr "YÜKLE" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP Adresi" @@ -362,6 +363,10 @@ msgstr "YÜKLENİYOR" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Boylamsal Manevra Modu" @@ -398,9 +403,8 @@ msgstr "Sürüm notu mevcut değil." msgid "OFFLINE" msgstr "ÇEVRİMDIŞI" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "İlk eşleştirmeyi tamamlamak için lütfen Wi‑Fi'a bağlanın" msgid "Power Off" msgstr "Kapat" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Ölçülü bir Wi‑Fi bağlantısındayken büyük veri yüklemelerini engelle" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Ölçülü bir hücresel bağlantıdayken büyük veri yüklemelerini engelle" @@ -549,11 +553,11 @@ msgstr "SEÇ" msgid "SSH Keys" msgstr "SSH Anahtarları" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Wi‑Fi ağları taranıyor..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Seç" @@ -597,7 +601,7 @@ msgstr "SIC." msgid "Target Branch" msgstr "Hedef Dal" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "İnternet Paylaşımı Şifresi" @@ -665,11 +669,11 @@ msgstr "Etkinleştirildiğinde, gaz pedalına basmak openpilot'u devreden çıka msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Ölçülü Wi‑Fi Ağı" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Yanlış şifre" @@ -693,7 +697,7 @@ msgstr "kontrol ediliyor..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "varsayılan" @@ -713,7 +717,7 @@ msgstr "güncelleme kontrolü başarısız" msgid "finalizing update..." msgstr "güncelleme tamamlanıyor..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "\"{}\" için" @@ -721,7 +725,7 @@ msgstr "\"{}\" için" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "otomatik yapılandırma için boş bırakın" @@ -729,7 +733,7 @@ msgstr "otomatik yapılandırma için boş bırakın" msgid "left" msgstr "sol" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "ölçülü" @@ -765,7 +769,7 @@ msgstr "openpilot, cihazın sağa/sola 4° ve yukarı 5° veya aşağı 9° içi msgid "right" msgstr "sağ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "ölçüsüz" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "{} dakika önce" msgstr[1] "{} dakika önce" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} segment sürüşünüz eğitim veri setinde." -msgstr[1] "{} segment sürüşünüz eğitim veri setinde." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ ABONE" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose Modu 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_uk.po b/openpilot/selfdrive/ui/translations/app_uk.po index 3f3d186657..24fab4f2e8 100644 --- a/openpilot/selfdrive/ui/translations/app_uk.po +++ b/openpilot/selfdrive/ui/translations/app_uk.po @@ -52,7 +52,7 @@ msgstr "

    Калібрування затримки кермування msgid "ADD" msgstr "ДОДАТИ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "Налаштування APN" @@ -60,7 +60,7 @@ msgstr "Налаштування APN" msgid "Acknowledge Excessive Actuation" msgstr "Визнайте надмірне спрацьовування" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "Розширені" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "Ви впевнені, що хочете видалити?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "Назад" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "СПОКІЙНИЙ РЕЖИМ" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "ПІДКЛЮЧИТИ" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "ПІДКЛЮЧА..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "Скасувати" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "Лімітне стільникове з'єднання" @@ -213,7 +213,7 @@ msgstr "Камера водія" msgid "Driving Personality" msgstr "Стиль водіння" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "РЕДАГ." @@ -242,7 +242,7 @@ msgstr "Увімкнути ADB" msgid "Enable Lane Departure Warnings" msgstr "Увімкнути попередження про виїзд зі смуги" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "Увімкнути роумінг" @@ -250,7 +250,7 @@ msgstr "Увімкнути роумінг" msgid "Enable SSH" msgstr "Увімкнути SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "Увімкнути точку доступу" @@ -266,19 +266,19 @@ msgstr "Увімкнути openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "Увімкніть перемикач поздовжнього керування openpilot (альфа), щоб увімкнути експериментальний режим." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "Введіть APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "Введіть SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "Введіть новий пароль для модему" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "Введіть пароль" @@ -286,7 +286,7 @@ msgstr "Введіть пароль" msgid "Enter your GitHub username" msgstr "Введіть ваш логін GitHub" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "Помилка" @@ -298,7 +298,7 @@ msgstr "Експериментальний режим" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "Експериментальний режим наразі недоступний для цього автомобіля, оскільки для поздовжнього керування використовується штатний адаптивний круїз-контроль (ACC)." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "ЗАБУВАЮ..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "Злива" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Режим зливи" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "Забути" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "Забути мережу Wi-Fi \"{}\"?" @@ -334,7 +335,7 @@ msgstr "Перейдіть на сайт https://connect.comma.ai на своє msgid "HIGH" msgstr "ВИСОКА" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "Прихована мережа" @@ -342,7 +343,7 @@ msgstr "Прихована мережа" msgid "INSTALL" msgstr "ВСТАНОВ." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP-адреса" @@ -362,6 +363,10 @@ msgstr "ЗАВАНТАЖЕННЯ" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "Режим поздовжнього маневрування" @@ -398,9 +403,8 @@ msgstr "Інформація про випуск відсутня." msgid "OFFLINE" msgstr "ОФЛАЙН" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "OK" @@ -453,11 +457,11 @@ msgstr "Будь ласка, підключіться до Wi-Fi, щоб зав msgid "Power Off" msgstr "Вимкнути" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "Запобігайте завантаженню великих обсягів даних під час використання Wi-Fi-з'єднання з обмеженим трафіком" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Запобігати великим завантаженням даних під час лімітного стільникового з'єднання" @@ -549,11 +553,11 @@ msgstr "ВИБРАТИ" msgid "SSH Keys" msgstr "SSH ключі" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "Пошук мереж..." -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "Вибрати" @@ -597,7 +601,7 @@ msgstr "ТЕМП" msgid "Target Branch" msgstr "Цільова гілка" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "Пароль для точки доступу" @@ -665,11 +669,11 @@ msgstr "Якщо увімкнено, натискання на педаль ак msgid "Wi-Fi" msgstr "Wi-Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Трафік Wi-Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "Невірний пароль" @@ -693,7 +697,7 @@ msgstr "перевіряю..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "замовч." @@ -713,7 +717,7 @@ msgstr "не вдалося перевірити оновлення" msgid "finalizing update..." msgstr "завершую..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "для \"{}\"" @@ -721,7 +725,7 @@ msgstr "для \"{}\"" msgid "km/h" msgstr "км/год" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "залиште порожнім для автоматичного налаштування" @@ -729,7 +733,7 @@ msgstr "залиште порожнім для автоматичного нал msgid "left" msgstr "вліво" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "обмеж." @@ -765,7 +769,7 @@ msgstr "Для роботи openpilot потрібно, щоб пристрій msgid "right" msgstr "вправо" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "необмеж." @@ -813,18 +817,7 @@ msgstr[0] "{} хвилина тому" msgstr[1] "{} хвилини тому" msgstr[2] "{} хвилин тому" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "{} сегмент вашого водіння на даний момент містяться в тренувальному наборі даних." -msgstr[1] "{} сегменти вашого водіння на даний момент містяться в тренувальному наборі даних." -msgstr[2] "{} сегментів вашого водіння на даний момент містяться в тренувальному наборі даних." - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ ПІДПИСАНО" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🌧️ Режим зливи 🌧️" - diff --git a/openpilot/selfdrive/ui/translations/app_zh-CHS.po b/openpilot/selfdrive/ui/translations/app_zh-CHS.po index 55a7c329f6..2028b6ac1f 100644 --- a/openpilot/selfdrive/ui/translations/app_zh-CHS.po +++ b/openpilot/selfdrive/ui/translations/app_zh-CHS.po @@ -52,7 +52,7 @@ msgstr "

    转向延迟校准已完成 {}%。" msgid "ADD" msgstr "添加" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN 设置" @@ -60,7 +60,7 @@ msgstr "APN 设置" msgid "Acknowledge Excessive Actuation" msgstr "确认过度作动" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "高级" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "确定要卸载吗?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "返回" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "安稳模式已开启" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "连接" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "连接中..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "取消" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "蜂窝计量" @@ -213,7 +213,7 @@ msgstr "车内摄像头" msgid "Driving Personality" msgstr "驾驶风格" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "编辑" @@ -242,7 +242,7 @@ msgstr "启用 ADB" msgid "Enable Lane Departure Warnings" msgstr "启用车道偏离警示" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "启用漫游" @@ -250,7 +250,7 @@ msgstr "启用漫游" msgid "Enable SSH" msgstr "启用 SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "启用网络共享" @@ -266,19 +266,19 @@ msgstr "启用 openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "启用 openpilot 纵向控制(alpha)开关,以使用实验模式。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "输入 APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "输入 SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "输入新的网络共享密码" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "输入密码" @@ -286,7 +286,7 @@ msgstr "输入密码" msgid "Enter your GitHub username" msgstr "输入您的 GitHub 用户名" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "错误" @@ -298,7 +298,7 @@ msgstr "实验模式" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "此车型当前无法使用实验模式,因为纵向控制使用的是原厂 ACC。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "正在遗忘..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "数据洪流" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehose 模式" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "忘记" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "要忘记 Wi‑Fi 网络“{}”吗?" @@ -334,7 +335,7 @@ msgstr "在手机上前往 https://connect.comma.ai" msgid "HIGH" msgstr "高" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "隐藏网络" @@ -342,7 +343,7 @@ msgstr "隐藏网络" msgid "INSTALL" msgstr "安装" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP 地址" @@ -362,6 +363,10 @@ msgstr "加载中" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "纵向操作模式" @@ -398,9 +403,8 @@ msgstr "暂无发行说明。" msgid "OFFLINE" msgstr "离线" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "确定" @@ -453,11 +457,11 @@ msgstr "请连接 Wi‑Fi 以完成初始配对" msgid "Power Off" msgstr "关机" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "在计量制 Wi‑Fi 连接时避免大量上传" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "在计量制蜂窝网络时避免大量上传" @@ -549,11 +553,11 @@ msgstr "选择" msgid "SSH Keys" msgstr "SSH 密钥" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "正在扫描 Wi‑Fi 网络…" -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "选择" @@ -597,7 +601,7 @@ msgstr "温度" msgid "Target Branch" msgstr "目标分支" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "网络共享密码" @@ -665,11 +669,11 @@ msgstr "启用后,踩下加速踏板将会脱离 openpilot。" msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Wi‑Fi 计量网络" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "密码错误" @@ -693,7 +697,7 @@ msgstr "检查中..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "默认" @@ -713,7 +717,7 @@ msgstr "检查更新失败" msgid "finalizing update..." msgstr "正在完成更新..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "用于“{}”" @@ -721,7 +725,7 @@ msgstr "用于“{}”" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "留空以自动配置" @@ -729,7 +733,7 @@ msgstr "留空以自动配置" msgid "left" msgstr "左" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "计量" @@ -765,7 +769,7 @@ msgstr "openpilot 要求设备安装在左右 4°、上 5° 或下 9° 以内。 msgid "right" msgstr "右" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "不限流量" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "{} 分钟前" msgstr[1] "{} 分钟前" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "目前已有 {} 个您的驾驶片段被纳入训练数据集。" -msgstr[1] "目前已有 {} 个您的驾驶片段被纳入训练数据集。" - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ 已订阅" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose 模式 🔥" - diff --git a/openpilot/selfdrive/ui/translations/app_zh-CHT.po b/openpilot/selfdrive/ui/translations/app_zh-CHT.po index 93f9b9ed8e..39a67b432d 100644 --- a/openpilot/selfdrive/ui/translations/app_zh-CHT.po +++ b/openpilot/selfdrive/ui/translations/app_zh-CHT.po @@ -52,7 +52,7 @@ msgstr "

    轉向延遲校正已完成 {}%。" msgid "ADD" msgstr "新增" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "APN Setting" msgstr "APN 設定" @@ -60,7 +60,7 @@ msgstr "APN 設定" msgid "Acknowledge Excessive Actuation" msgstr "確認過度作動" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Advanced" msgstr "進階" @@ -93,7 +93,7 @@ msgid "Are you sure you want to uninstall?" msgstr "確定要解除安裝嗎?" #: openpilot/selfdrive/ui/layouts/onboarding.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Back" msgstr "返回" @@ -118,22 +118,22 @@ msgid "CHILL MODE ON" msgstr "安穩模式已開啟" #: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECT" msgstr "連線" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "CONNECTING..." msgstr "連線中..." -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/keyboard.py -#: system/ui/widgets/network.py -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/keyboard.py +#: openpilot/system/ui/widgets/network.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Cancel" msgstr "取消" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Cellular Metered" msgstr "行動網路計量" @@ -213,7 +213,7 @@ msgstr "車內鏡頭" msgid "Driving Personality" msgstr "駕駛風格" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "EDIT" msgstr "編輯" @@ -242,7 +242,7 @@ msgstr "啟用 ADB" msgid "Enable Lane Departure Warnings" msgstr "啟用偏離車道警示" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Roaming" msgstr "啟用漫遊" @@ -250,7 +250,7 @@ msgstr "啟用漫遊" msgid "Enable SSH" msgstr "啟用 SSH" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enable Tethering" msgstr "啟用網路共享" @@ -266,19 +266,19 @@ msgstr "啟用 openpilot" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "啟用 openpilot 縱向控制(alpha)切換,以使用實驗模式。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter APN" msgstr "輸入 APN" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter SSID" msgstr "輸入 SSID" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter new tethering password" msgstr "輸入新的網路共享密碼" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Enter password" msgstr "輸入密碼" @@ -286,7 +286,7 @@ msgstr "輸入密碼" msgid "Enter your GitHub username" msgstr "輸入您的 GitHub 使用者名稱" -#: system/ui/widgets/list_view.py +#: openpilot/system/ui/widgets/list_view.py msgid "Error" msgstr "錯誤" @@ -298,7 +298,7 @@ msgstr "實驗模式" msgid "Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control." msgstr "此車款目前無法使用實驗模式,因為縱向控制使用的是原廠 ACC。" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "FORGETTING..." msgstr "正在遺忘..." @@ -311,14 +311,15 @@ msgid "Firehose" msgstr "資料洪流" #: openpilot/selfdrive/ui/layouts/settings/firehose.py +#: openpilot/selfdrive/ui/widgets/setup.py msgid "Firehose Mode" msgstr "Firehose 模式" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget" msgstr "忘記" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Forget Wi-Fi Network \"{}\"?" msgstr "要忘記 Wi‑Fi 網路「{}」嗎?" @@ -334,7 +335,7 @@ msgstr "在手機上前往 https://connect.comma.ai" msgid "HIGH" msgstr "高" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Hidden Network" msgstr "隱藏網路" @@ -342,7 +343,7 @@ msgstr "隱藏網路" msgid "INSTALL" msgstr "安裝" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "IP Address" msgstr "IP 位址" @@ -362,6 +363,10 @@ msgstr "載入中" msgid "LTE" msgstr "LTE" +#: openpilot/selfdrive/ui/layouts/settings/developer.py +msgid "Lateral Maneuver Mode" +msgstr "" + #: openpilot/selfdrive/ui/layouts/settings/developer.py msgid "Longitudinal Maneuver Mode" msgstr "縱向操作模式" @@ -398,9 +403,8 @@ msgstr "無可用發行說明。" msgid "OFFLINE" msgstr "離線" -#: openpilot/selfdrive/ui/layouts/sidebar.py -#: system/ui/widgets/confirm_dialog.py -#: system/ui/widgets/html_render.py +#: openpilot/system/ui/widgets/confirm_dialog.py +#: openpilot/system/ui/widgets/html_render.py msgid "OK" msgstr "確定" @@ -453,11 +457,11 @@ msgstr "請連線至 Wi‑Fi 以完成初始化配對" msgid "Power Off" msgstr "關機" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered Wi-Fi connection" msgstr "在計量制 Wi‑Fi 連線時避免大量上傳" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Prevent large data uploads when on a metered cellular connection" msgstr "在計量制行動網路時避免大量上傳" @@ -549,11 +553,11 @@ msgstr "選取" msgid "SSH Keys" msgstr "SSH 金鑰" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Scanning Wi-Fi networks..." msgstr "正在掃描 Wi‑Fi 網路…" -#: system/ui/widgets/option_dialog.py +#: openpilot/system/ui/widgets/option_dialog.py msgid "Select" msgstr "選取" @@ -597,7 +601,7 @@ msgstr "溫度" msgid "Target Branch" msgstr "目標分支" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Tethering Password" msgstr "網路共享密碼" @@ -665,11 +669,11 @@ msgstr "啟用後,踩下加速踏板將會脫離 openpilot。" msgid "Wi-Fi" msgstr "Wi‑Fi" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wi-Fi Network Metered" msgstr "Wi‑Fi 計量網路" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "Wrong password" msgstr "密碼錯誤" @@ -693,7 +697,7 @@ msgstr "檢查中..." msgid "comma prime" msgstr "comma prime" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "default" msgstr "預設" @@ -713,7 +717,7 @@ msgstr "檢查更新失敗" msgid "finalizing update..." msgstr "正在完成更新..." -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "for \"{}\"" msgstr "適用於「{}」" @@ -721,7 +725,7 @@ msgstr "適用於「{}」" msgid "km/h" msgstr "km/h" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "leave blank for automatic configuration" msgstr "留空以自動設定" @@ -729,7 +733,7 @@ msgstr "留空以自動設定" msgid "left" msgstr "左" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "metered" msgstr "計量" @@ -765,7 +769,7 @@ msgstr "openpilot 要求裝置安裝在左右 4°、上 5° 或下 9° 以內。 msgid "right" msgstr "右" -#: system/ui/widgets/network.py +#: openpilot/system/ui/widgets/network.py msgid "unmetered" msgstr "不限流量" @@ -809,17 +813,7 @@ msgid_plural "{} minutes ago" msgstr[0] "{} 分鐘前" msgstr[1] "{} 分鐘前" -#: openpilot/selfdrive/ui/layouts/settings/firehose.py -msgid "{} segment of your driving is in the training dataset so far." -msgid_plural "{} segments of your driving is in the training dataset so far." -msgstr[0] "目前已有 {} 個您的駕駛片段納入訓練資料集。" -msgstr[1] "目前已有 {} 個您的駕駛片段納入訓練資料集。" - #: openpilot/selfdrive/ui/widgets/prime.py msgid "✓ SUBSCRIBED" msgstr "✓ 已訂閱" -#: openpilot/selfdrive/ui/widgets/setup.py -msgid "🔥 Firehose Mode 🔥" -msgstr "🔥 Firehose 模式 🔥" - From 9738db0e392f58a78359cfa4da6727783b7ce742 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 8 Aug 2026 09:32:47 -0700 Subject: [PATCH 196/325] Move camera stream types out of msgq (#38591) --- msgq_repo | 2 +- openpilot/cereal/visionipc.py | 8 ++++++++ openpilot/cereal/visionstream.h | 10 ++++++++++ openpilot/selfdrive/modeld/dmonitoringmodeld.py | 3 ++- openpilot/selfdrive/modeld/modeld.py | 3 ++- openpilot/selfdrive/selfdrived/selfdrived.py | 3 ++- openpilot/selfdrive/test/process_replay/vision_meta.py | 2 +- .../selfdrive/ui/mici/onroad/augmented_road_view.py | 2 +- openpilot/selfdrive/ui/mici/onroad/cameraview.py | 6 ++++-- .../selfdrive/ui/mici/onroad/driver_camera_dialog.py | 2 +- openpilot/selfdrive/ui/onroad/augmented_road_view.py | 2 +- openpilot/selfdrive/ui/onroad/cameraview.py | 6 ++++-- openpilot/selfdrive/ui/onroad/driver_camera_dialog.py | 2 +- openpilot/selfdrive/ui/tests/profile_onroad.py | 3 ++- openpilot/selfdrive/ui/watch3.py | 2 +- openpilot/system/camerad/cameras/hw.h | 1 + openpilot/system/camerad/snapshot.py | 3 ++- openpilot/system/camerad/webcam/camerad.py | 3 ++- openpilot/system/loggerd/loggerd.h | 1 + openpilot/system/loggerd/tests/test_loggerd.py | 3 ++- openpilot/tools/cabana/cameraview.h | 1 + openpilot/tools/camerastream/compressed_vipc.py | 3 ++- openpilot/tools/clip/run.py | 3 ++- openpilot/tools/replay/camera.h | 1 + openpilot/tools/replay/ui.py | 2 +- openpilot/tools/sim/lib/camerad.py | 3 ++- 26 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 openpilot/cereal/visionipc.py create mode 100644 openpilot/cereal/visionstream.h diff --git a/msgq_repo b/msgq_repo index deecc5d246..0e266c1dbc 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit deecc5d246e79d91b62874d08b43be0cc023b241 +Subproject commit 0e266c1dbcf7328beee3e57b4a8688555387c877 diff --git a/openpilot/cereal/visionipc.py b/openpilot/cereal/visionipc.py new file mode 100644 index 0000000000..d3c842e58e --- /dev/null +++ b/openpilot/cereal/visionipc.py @@ -0,0 +1,8 @@ +from enum import IntEnum + + +class VisionStreamType(IntEnum): + VISION_STREAM_ROAD = 0 + VISION_STREAM_DRIVER = 1 + VISION_STREAM_WIDE_ROAD = 2 + VISION_STREAM_MAP = 3 diff --git a/openpilot/cereal/visionstream.h b/openpilot/cereal/visionstream.h new file mode 100644 index 0000000000..36105ee046 --- /dev/null +++ b/openpilot/cereal/visionstream.h @@ -0,0 +1,10 @@ +#pragma once + +#include "msgq/visionipc/visionbuf.h" + +enum VisionStreamValues : VisionStreamType { + VISION_STREAM_ROAD = 0, + VISION_STREAM_DRIVER = 1, + VISION_STREAM_WIDE_ROAD = 2, + VISION_STREAM_MAP = 3, +}; diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 554407a223..1ccc0b2a89 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -8,7 +8,8 @@ import numpy as np from openpilot.cereal import messaging from openpilot.cereal.messaging import PubMaster, SubMaster -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient, VisionBuf from openpilot.common.swaglog import cloudlog from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.model import dmonitoringmodel_intrinsics diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index be08c9c4c7..ef61c58333 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -13,7 +13,8 @@ from openpilot.cereal import log from opendbc.car.structs import car from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.services import SERVICE_LIST -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient, VisionBuf from opendbc.car.car_helpers import get_demo_car_params from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 6df72666c0..2ca462a9e6 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -7,7 +7,8 @@ import openpilot.cereal.messaging as messaging from openpilot.cereal import log from opendbc.car.structs import car -from msgq.visionipc import VisionIpcClient, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient from openpilot.common.params import Params diff --git a/openpilot/selfdrive/test/process_replay/vision_meta.py b/openpilot/selfdrive/test/process_replay/vision_meta.py index 12deb58724..28f2829650 100644 --- a/openpilot/selfdrive/test/process_replay/vision_meta.py +++ b/openpilot/selfdrive/test/process_replay/vision_meta.py @@ -1,5 +1,5 @@ from collections import namedtuple -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.common.realtime import DT_MDL, DT_DMON from openpilot.common.transformations.camera import DEVICE_CAMERAS diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 25061f12ae..92f6b835e3 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -2,7 +2,7 @@ import numpy as np import pyray as rl from openpilot.cereal import log from opendbc.car.structs import car -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer diff --git a/openpilot/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py index 4ccfb04687..64dd6d023d 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -2,7 +2,8 @@ import platform import numpy as np import pyray as rl -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient, VisionBuf from openpilot.common.swaglog import cloudlog from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app @@ -110,7 +111,7 @@ class CameraView(Widget): self._name = name # Primary stream self.client = VisionIpcClient(name, stream_type, conflate=True) - self._stream_type = stream_type + self._stream_type: VisionStreamType = stream_type self.available_streams: list[VisionStreamType] = [] # Target stream for switching @@ -370,6 +371,7 @@ class CameraView(Widget): del self.client # Switch to target + assert self._target_client is not None and self._target_stream_type is not None self.client = self._target_client self._stream_type = self._target_stream_type self._texture_needs_update = True diff --git a/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py index e81877b402..24276c42b8 100644 --- a/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -1,6 +1,6 @@ import pyray as rl from openpilot.cereal import log, messaging -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device diff --git a/openpilot/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py index 1b0c84f0fe..ff0cfb5eea 100644 --- a/openpilot/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -1,7 +1,7 @@ import numpy as np import pyray as rl from openpilot.cereal import log -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui import UI_BORDER_SIZE from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer diff --git a/openpilot/selfdrive/ui/onroad/cameraview.py b/openpilot/selfdrive/ui/onroad/cameraview.py index 1fed2dd683..3b6a2fcd30 100644 --- a/openpilot/selfdrive/ui/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/onroad/cameraview.py @@ -2,7 +2,8 @@ import platform import numpy as np import pyray as rl -from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient, VisionBuf from openpilot.common.swaglog import cloudlog from openpilot.common.hardware import COMMA_HARDWARE from openpilot.system.ui.lib.application import gui_app @@ -71,7 +72,7 @@ class CameraView(Widget): self._name = name # Primary stream self.client = VisionIpcClient(name, stream_type, conflate=True) - self._stream_type = stream_type + self._stream_type: VisionStreamType = stream_type self.available_streams: list[VisionStreamType] = [] # Target stream for switching @@ -323,6 +324,7 @@ class CameraView(Widget): del self.client # Switch to target + assert self._target_client is not None and self._target_stream_type is not None self.client = self._target_client self._stream_type = self._target_stream_type self._texture_needs_update = True diff --git a/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py index 7e07e44210..8735774458 100644 --- a/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py @@ -1,6 +1,6 @@ import numpy as np import pyray as rl -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device diff --git a/openpilot/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py index ec15e71c35..7c73bfa97b 100755 --- a/openpilot/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -5,7 +5,8 @@ import cProfile import pyray as rl import numpy as np -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer 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 diff --git a/openpilot/selfdrive/ui/watch3.py b/openpilot/selfdrive/ui/watch3.py index bb64cdc4d5..162df82658 100755 --- a/openpilot/selfdrive/ui/watch3.py +++ b/openpilot/selfdrive/ui/watch3.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import pyray as rl -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.onroad.cameraview import CameraView diff --git a/openpilot/system/camerad/cameras/hw.h b/openpilot/system/camerad/cameras/hw.h index be0bea872d..45546258a9 100644 --- a/openpilot/system/camerad/cameras/hw.h +++ b/openpilot/system/camerad/cameras/hw.h @@ -2,6 +2,7 @@ #include "common/util.h" #include "openpilot/cereal/gen/cpp/log.capnp.h" +#include "openpilot/cereal/visionstream.h" #include "msgq/visionipc/visionipc_server.h" #include "media/cam_isp_ife.h" diff --git a/openpilot/system/camerad/snapshot.py b/openpilot/system/camerad/snapshot.py index 8383865fce..f865a452ff 100755 --- a/openpilot/system/camerad/snapshot.py +++ b/openpilot/system/camerad/snapshot.py @@ -3,7 +3,8 @@ import numpy as np import openpilot.cereal.messaging as messaging -from msgq.visionipc import VisionIpcClient, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcClient from openpilot.common.realtime import DT_MDL diff --git a/openpilot/system/camerad/webcam/camerad.py b/openpilot/system/camerad/webcam/camerad.py index c46482360d..bba982f897 100755 --- a/openpilot/system/camerad/webcam/camerad.py +++ b/openpilot/system/camerad/webcam/camerad.py @@ -4,7 +4,8 @@ import os import platform from collections import namedtuple -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer from openpilot.cereal import messaging from openpilot.system.camerad.webcam.camera import Camera diff --git a/openpilot/system/loggerd/loggerd.h b/openpilot/system/loggerd/loggerd.h index 4d33a77095..c6c7b41af1 100644 --- a/openpilot/system/loggerd/loggerd.h +++ b/openpilot/system/loggerd/loggerd.h @@ -5,6 +5,7 @@ #include "openpilot/cereal/messaging/messaging.h" #include "openpilot/cereal/services.h" +#include "openpilot/cereal/visionstream.h" #include "msgq/visionipc/visionipc_client.h" #include "common/hardware/hw.h" #include "common/params.h" diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 039bb17064..333891ad91 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -25,7 +25,8 @@ from openpilot.system.manager.process_config import managed_processes from openpilot.common.version import get_version from openpilot.tools.lib.helpers import RE from openpilot.tools.lib.logreader import LogReader -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer SentinelType = log.Sentinel.SentinelType diff --git a/openpilot/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h index 40d3776005..3b55dd9ed9 100644 --- a/openpilot/tools/cabana/cameraview.h +++ b/openpilot/tools/cabana/cameraview.h @@ -10,6 +10,7 @@ #include #include +#include "openpilot/cereal/visionstream.h" #include "msgq/visionipc/visionipc_client.h" class CameraWidget : public QWidget { diff --git a/openpilot/tools/camerastream/compressed_vipc.py b/openpilot/tools/camerastream/compressed_vipc.py index 56ed12889f..2ec538981b 100755 --- a/openpilot/tools/camerastream/compressed_vipc.py +++ b/openpilot/tools/camerastream/compressed_vipc.py @@ -8,7 +8,8 @@ from collections import deque import openpilot.cereal.messaging as messaging -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError V4L2_BUF_FLAG_KEYFRAME = 8 diff --git a/openpilot/tools/clip/run.py b/openpilot/tools/clip/run.py index 594383f7fd..6c847ca342 100755 --- a/openpilot/tools/clip/run.py +++ b/openpilot/tools/clip/run.py @@ -22,7 +22,8 @@ from openpilot.tools.lib.framereader import FrameReader, ffprobe from openpilot.selfdrive.test.process_replay.migration import migrate_all from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.utils import Timer -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer FRAMERATE = 20 DEMO_ROUTE, DEMO_START, DEMO_END = '5beb9b58bd12b691/0000010a--a51155e496', 90, 105 diff --git a/openpilot/tools/replay/camera.h b/openpilot/tools/replay/camera.h index 9433018848..35fab5f7d3 100644 --- a/openpilot/tools/replay/camera.h +++ b/openpilot/tools/replay/camera.h @@ -5,6 +5,7 @@ #include #include +#include "openpilot/cereal/visionstream.h" #include "msgq/visionipc/visionipc_server.h" #include "common/queue.h" #include "tools/replay/framereader.h" diff --git a/openpilot/tools/replay/ui.py b/openpilot/tools/replay/ui.py index 4fbede2be9..2c253a1364 100755 --- a/openpilot/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -21,7 +21,7 @@ from openpilot.tools.replay.lib.ui_helpers import ( plot_lead, plot_model, ) -from msgq.visionipc import VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView os.environ['BASEDIR'] = BASEDIR diff --git a/openpilot/tools/sim/lib/camerad.py b/openpilot/tools/sim/lib/camerad.py index 8efb3e5dab..d4825eada7 100644 --- a/openpilot/tools/sim/lib/camerad.py +++ b/openpilot/tools/sim/lib/camerad.py @@ -1,6 +1,7 @@ import numpy as np -from msgq.visionipc import VisionIpcServer, VisionStreamType +from openpilot.cereal.visionipc import VisionStreamType +from msgq.visionipc import VisionIpcServer from openpilot.cereal import messaging from openpilot.tools.sim.lib.common import W, H From fedea9fe8d489fd73f3a93ef707656bf10c94eb9 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 8 Aug 2026 09:44:21 -0700 Subject: [PATCH 197/325] camera names that make sense (#38585) --- docs/CONTRIBUTING.md | 2 +- docs/LIMITATIONS.md | 4 +- docs/concepts/logs.md | 6 +-- docs/contributing/feedback.md | 6 +-- openpilot/cereal/log.capnp | 24 ++++----- openpilot/cereal/services.py | 24 ++++----- openpilot/cereal/visionipc.py | 4 +- openpilot/cereal/visionstream.h | 4 +- openpilot/common/transformations/camera.py | 8 +-- .../selfdrive/modeld/dmonitoringmodeld.py | 4 +- openpilot/selfdrive/modeld/modeld.py | 20 +++---- openpilot/selfdrive/monitoring/policy.py | 12 ++--- openpilot/selfdrive/pandad/pandad.cc | 18 +++---- openpilot/selfdrive/selfdrived/events.py | 2 +- openpilot/selfdrive/selfdrived/selfdrived.py | 12 ++--- .../selfdrive/test/process_replay/README.md | 4 +- .../test/process_replay/migration.py | 8 +-- .../test/process_replay/model_replay.py | 11 ++-- .../test/process_replay/process_replay.py | 20 +++---- .../test/process_replay/vision_meta.py | 10 ++-- openpilot/selfdrive/test/test_onroad.py | 18 +++---- openpilot/selfdrive/test/test_power_draw.py | 2 +- .../selfdrive/ui/layouts/settings/device.py | 8 +-- .../selfdrive/ui/layouts/settings/toggles.py | 4 +- .../selfdrive/ui/mici/layouts/onboarding.py | 8 +-- .../ui/mici/layouts/settings/device.py | 10 ++-- .../ui/mici/layouts/settings/toggles.py | 2 +- .../ui/mici/onroad/augmented_road_view.py | 18 +++---- ...amera_dialog.py => cabin_camera_dialog.py} | 16 +++--- .../selfdrive/ui/mici/onroad/cameraview.py | 8 +-- .../ui/mici/tests/test_widget_leaks.py | 12 ++--- .../ui/onroad/augmented_road_view.py | 18 +++---- ...amera_dialog.py => cabin_camera_dialog.py} | 12 ++--- openpilot/selfdrive/ui/onroad/cameraview.py | 6 +-- .../selfdrive/ui/tests/diff/replay_script.py | 2 +- .../selfdrive/ui/tests/profile_onroad.py | 4 +- openpilot/selfdrive/ui/translations/app.pot | 16 +++--- openpilot/selfdrive/ui/translations/app_de.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_en.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_es.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_fr.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_ja.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_ko.po | 22 ++++---- .../selfdrive/ui/translations/app_pt-BR.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_th.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_tr.po | 22 ++++---- openpilot/selfdrive/ui/translations/app_uk.po | 22 ++++---- .../selfdrive/ui/translations/app_zh-CHS.po | 22 ++++---- .../selfdrive/ui/translations/app_zh-CHT.po | 22 ++++---- openpilot/selfdrive/ui/ui_state.py | 2 +- openpilot/selfdrive/ui/watch3.py | 4 +- .../system/camerad/cameras/camera_qcom2.cc | 4 +- openpilot/system/camerad/cameras/hw.h | 18 +++---- openpilot/system/camerad/snapshot.py | 6 +-- openpilot/system/camerad/test/test_camerad.py | 14 ++--- openpilot/system/camerad/webcam/camerad.py | 6 +-- openpilot/system/loggerd/clip_encoder.cc | 4 +- openpilot/system/loggerd/loggerd.h | 54 +++++++++---------- .../system/loggerd/tests/test_encoder.py | 4 +- .../system/loggerd/tests/test_loggerd.py | 12 ++--- openpilot/system/webrtc/device/video.py | 4 +- .../webrtc/tests/test_stream_session.py | 2 +- openpilot/tools/cabana/README.md | 4 +- openpilot/tools/cabana/cabana.cc | 20 +++---- openpilot/tools/cabana/cameraview.cc | 4 +- .../tools/cabana/streams/replaystream.cc | 6 +-- openpilot/tools/cabana/tools/routeinfo.cc | 6 +-- openpilot/tools/cabana/videowidget.cc | 2 +- .../tools/camerastream/compressed_vipc.py | 8 +-- openpilot/tools/clip/run.py | 4 +- openpilot/tools/jotpluggler/app.h | 4 +- openpilot/tools/jotpluggler/common.h | 2 +- .../jotpluggler/layouts/camera-timings.json | 2 +- .../jotpluggler/layouts/cameras-and-map.json | 2 +- .../layouts/driver-monitoring-debug.json | 2 +- openpilot/tools/jotpluggler/runtime.cc | 22 ++++---- openpilot/tools/jotpluggler/sketch_layout.cc | 20 +++---- openpilot/tools/lib/log_time_series.py | 4 +- .../plotjuggler/layouts/camera-timings.xml | 37 +++++++------ openpilot/tools/replay/README.md | 8 +-- openpilot/tools/replay/camera.h | 4 +- openpilot/tools/replay/logreader.cc | 4 +- openpilot/tools/replay/logreader.h | 2 +- openpilot/tools/replay/main.cc | 16 +++--- openpilot/tools/replay/replay.cc | 6 +-- openpilot/tools/replay/replay.h | 4 +- openpilot/tools/replay/route.cc | 12 ++--- openpilot/tools/replay/route.h | 4 +- openpilot/tools/replay/ui.py | 10 ++-- openpilot/tools/replay/util.h | 4 +- openpilot/tools/sim/lib/camerad.py | 6 +-- tools/scripts/cycle_alerts.py | 4 +- 92 files changed, 502 insertions(+), 496 deletions(-) rename openpilot/selfdrive/ui/mici/onroad/{driver_camera_dialog.py => cabin_camera_dialog.py} (96%) rename openpilot/selfdrive/ui/onroad/{driver_camera_dialog.py => cabin_camera_dialog.py} (92%) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cbeb5f6d3a..1d9338e457 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -58,7 +58,7 @@ A good pull request has all of the following: * Report bugs in GitHub issues. * Report driving issues in the `#driving-feedback` Discord channel. -* Consider opting into driver camera uploads to improve the driver monitoring model. +* Consider opting into cabin camera uploads to improve the driver monitoring model. * Connect your device to Wi-Fi regularly, so that we can pull data for training better driving models. * Run the `nightly` branch and report issues. This branch is like `master` but it's built just like a release. * Annotate images in the [comma10k dataset](https://github.com/commaai/comma10k). diff --git a/docs/LIMITATIONS.md b/docs/LIMITATIONS.md index 8c112659c2..5f9b7517bd 100644 --- a/docs/LIMITATIONS.md +++ b/docs/LIMITATIONS.md @@ -52,7 +52,7 @@ Many factors can impact the performance of openpilot DM, causing it to be unable * Low light conditions, such as driving at night or in dark tunnels. * Bright light (due to oncoming headlights, direct sunlight, etc.). -* The driver's face is partially or completely outside field of view of the driver facing camera. -* The driver facing camera is obstructed, covered, or damaged. +* The driver's face is partially or completely outside field of view of the cabin camera. +* The cabin camera is obstructed, covered, or damaged. The list above does not represent an exhaustive list of situations that may interfere with proper operation of openpilot components. A driver should not rely on openpilot DM to assess their level of attention. diff --git a/docs/concepts/logs.md b/docs/concepts/logs.md index dd954d4c76..a1e655935d 100644 --- a/docs/concepts/logs.md +++ b/docs/concepts/logs.md @@ -10,13 +10,13 @@ For each segment, openpilot records the following log types: rlogs contain all the messages passed amongst openpilot's processes. See [openpilot/cereal/services.py](https://github.com/commaai/openpilot/blob/master/openpilot/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages. -## {f,e,d}camera.hevc +## camera video files Each camera stream is H.265 encoded and written to its respective file. -* `fcamera.hevc` is the road camera +* `fcamera.hevc` is the narrow road camera (the main forward camera) * `ecamera.hevc` is the wide road camera -* `dcamera.hevc` is the driver camera +* `dcamera.hevc` is the cabin camera ## qlog.zst & qcamera.ts diff --git a/docs/contributing/feedback.md b/docs/contributing/feedback.md index 335d24e13a..587816d140 100644 --- a/docs/contributing/feedback.md +++ b/docs/contributing/feedback.md @@ -21,14 +21,14 @@ In general, driver monitoring feedback is very actionable, and we can fix your c To post your feedback: 1. Join the [community Discord](https://discord.comma.ai). -2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue. -3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera. +2. If cabin camera recording is toggled off, temporarily enable cabin camera recording in the settings until you reproduce the issue. +3. Using comma connect, identify the relevant segment and upload the segment's logs and cabin camera. 4. Post the segment in the `#openpilot-experience` channel on Discord with a good description. Before posting feedback, please ensure: - **openpilot is up to date** you should be on the latest openpilot release or nightly -- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc. +- **the cabin camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc. - **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield ## Other bugs diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index da700b58a7..fa185aa483 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2562,17 +2562,17 @@ struct Event { driverStateV2 @92 :DriverStateV2; # camera stuff, each camera state has a matching encode idx - roadCameraState @2 :FrameData; - driverCameraState @70: FrameData; + narrowRoadCameraState @2 :FrameData; + cabinCameraState @70: FrameData; wideRoadCameraState @74: FrameData; - roadEncodeIdx @15 :EncodeIndex; - driverEncodeIdx @76 :EncodeIndex; + narrowRoadEncodeIdx @15 :EncodeIndex; + cabinEncodeIdx @76 :EncodeIndex; wideRoadEncodeIdx @77 :EncodeIndex; - qRoadEncodeIdx @90 :EncodeIndex; + qNarrowRoadEncodeIdx @90 :EncodeIndex; - livestreamRoadEncodeIdx @117 :EncodeIndex; + livestreamNarrowRoadEncodeIdx @117 :EncodeIndex; livestreamWideRoadEncodeIdx @118 :EncodeIndex; - livestreamDriverEncodeIdx @119 :EncodeIndex; + livestreamCabinEncodeIdx @119 :EncodeIndex; # microphone data soundPressure @103 :SoundPressure; @@ -2602,15 +2602,15 @@ struct Event { # *********** debug *********** testJoystick @52 :Joystick; - roadEncodeData @86 :EncodeData; - driverEncodeData @87 :EncodeData; + narrowRoadEncodeData @86 :EncodeData; + cabinEncodeData @87 :EncodeData; wideRoadEncodeData @88 :EncodeData; - qRoadEncodeData @89 :EncodeData; + qNarrowRoadEncodeData @89 :EncodeData; alertDebug @133 :DebugAlert; - livestreamRoadEncodeData @120 :EncodeData; + livestreamNarrowRoadEncodeData @120 :EncodeData; livestreamWideRoadEncodeData @121 :EncodeData; - livestreamDriverEncodeData @122 :EncodeData; + livestreamCabinEncodeData @122 :EncodeData; # *********** Custom: reserved for forks *********** diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index a41a890f97..61fb8fcbd2 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -33,7 +33,7 @@ _services: dict[str, tuple] = { "pandaStates": (True, 10., 1), "peripheralState": (True, 2., 1), "radarState": (True, 20., 5), - "roadEncodeIdx": (False, 20., 1), + "narrowRoadEncodeIdx": (False, 20., 1), "liveTracks": (True, 20.), "sendcan": (True, 100., 139, QueueSize.MEDIUM), "logMessage": (True, 0., None, QueueSize.BIG), @@ -61,9 +61,9 @@ _services: dict[str, tuple] = { "thumbnail": (True, 1 / 60., 1), "onroadEvents": (True, 1., 1), "carParams": (True, 0.02, 1), - "roadCameraState": (True, 20., 20), - "driverCameraState": (True, 20., 20), - "driverEncodeIdx": (False, 20., 1), + "narrowRoadCameraState": (True, 20., 20), + "cabinCameraState": (True, 20., 20), + "cabinEncodeIdx": (False, 20., 1), "driverStateV2": (True, 20., 10), "driverMonitoringState": (True, 20., 10), "wideRoadEncodeIdx": (False, 20., 1), @@ -71,26 +71,26 @@ _services: dict[str, tuple] = { "drivingModelData": (True, 20., 10), "modelV2": (True, 20., None, QueueSize.BIG), "managerState": (True, 2., 1), - "qRoadEncodeIdx": (False, 20.), + "qNarrowRoadEncodeIdx": (False, 20.), "userBookmark": (True, 0., 1), "soundPressure": (True, 10., 10), "rawAudioData": (False, 20.), "bookmarkButton": (True, 0., 1), - "roadEncodeData": (False, 20., None, QueueSize.BIG), - "driverEncodeData": (False, 20., None, QueueSize.BIG), + "narrowRoadEncodeData": (False, 20., None, QueueSize.BIG), + "cabinEncodeData": (False, 20., None, QueueSize.BIG), "wideRoadEncodeData": (False, 20., None, QueueSize.BIG), - "qRoadEncodeData": (False, 20., None, QueueSize.BIG), + "qNarrowRoadEncodeData": (False, 20., None, QueueSize.BIG), # debug "uiDebug": (True, 0., 1), "testJoystick": (True, 0.), "alertDebug": (True, 20., 5), "livestreamWideRoadEncodeIdx": (False, 20.), - "livestreamRoadEncodeIdx": (False, 20.), - "livestreamDriverEncodeIdx": (False, 20.), + "livestreamNarrowRoadEncodeIdx": (False, 20.), + "livestreamCabinEncodeIdx": (False, 20.), "livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), - "livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), - "livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamNarrowRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamCabinEncodeData": (False, 20., None, QueueSize.MEDIUM), "customReservedRawData0": (True, 0.), } SERVICE_LIST = {name: Service(*vals) for diff --git a/openpilot/cereal/visionipc.py b/openpilot/cereal/visionipc.py index d3c842e58e..74439babb9 100644 --- a/openpilot/cereal/visionipc.py +++ b/openpilot/cereal/visionipc.py @@ -2,7 +2,7 @@ from enum import IntEnum class VisionStreamType(IntEnum): - VISION_STREAM_ROAD = 0 - VISION_STREAM_DRIVER = 1 + VISION_STREAM_NARROW_ROAD = 0 + VISION_STREAM_CABIN = 1 VISION_STREAM_WIDE_ROAD = 2 VISION_STREAM_MAP = 3 diff --git a/openpilot/cereal/visionstream.h b/openpilot/cereal/visionstream.h index 36105ee046..adaff7b477 100644 --- a/openpilot/cereal/visionstream.h +++ b/openpilot/cereal/visionstream.h @@ -3,8 +3,8 @@ #include "msgq/visionipc/visionbuf.h" enum VisionStreamValues : VisionStreamType { - VISION_STREAM_ROAD = 0, - VISION_STREAM_DRIVER = 1, + VISION_STREAM_NARROW_ROAD = 0, + VISION_STREAM_CABIN = 1, VISION_STREAM_WIDE_ROAD = 2, VISION_STREAM_MAP = 3, }; diff --git a/openpilot/common/transformations/camera.py b/openpilot/common/transformations/camera.py index ada9c5b398..17ac226cdb 100644 --- a/openpilot/common/transformations/camera.py +++ b/openpilot/common/transformations/camera.py @@ -37,12 +37,12 @@ class _NoneCameraConfig(CameraConfig): @dataclass(frozen=True) class DeviceCameraConfig: - fcam: CameraConfig - dcam: CameraConfig - ecam: CameraConfig + narrow_road: CameraConfig + cabin: CameraConfig + wide_road: CameraConfig def all_cams(self): - for cam in ['fcam', 'dcam', 'ecam']: + for cam in ['narrow_road', 'cabin', 'wide_road']: if not isinstance(getattr(self, cam), _NoneCameraConfig): yield cam, getattr(self, cam) diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 1ccc0b2a89..02391aeee3 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -110,8 +110,8 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t def main(): config_realtime_process(7, 5) - cloudlog.warning("connecting to driver stream") - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True) + cloudlog.warning("connecting to cabin stream") + vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_CABIN, True) while not vipc_client.connect(False): time.sleep(0.1) assert vipc_client.is_connected() diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index ef61c58333..f724e530e2 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -215,12 +215,12 @@ def main(demo=False): while True: available_streams = VisionIpcClient.available_streams("camerad", block=False) if available_streams: - use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_ROAD in available_streams - main_wide_camera = VisionStreamType.VISION_STREAM_ROAD not in available_streams + use_extra_client = VisionStreamType.VISION_STREAM_WIDE_ROAD in available_streams and VisionStreamType.VISION_STREAM_NARROW_ROAD in available_streams + main_wide_camera = VisionStreamType.VISION_STREAM_NARROW_ROAD not in available_streams break time.sleep(.1) - vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_ROAD + vipc_client_main_stream = VisionStreamType.VISION_STREAM_WIDE_ROAD if main_wide_camera else VisionStreamType.VISION_STREAM_NARROW_ROAD vipc_client_main = VisionIpcClient("camerad", vipc_client_main_stream, True) vipc_client_extra = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD, False) cloudlog.warning(f"vision stream set up, main_wide_camera: {main_wide_camera}, use_extra_client: {use_extra_client}") @@ -262,7 +262,7 @@ def main(demo=False): # messaging pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if USBGPU else []) pm = PubMaster(pub_socks) - sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) + sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() params = Params() @@ -330,15 +330,17 @@ def main(demo=False): sm.update(0) desire = DH.desire is_rhd = sm["driverMonitoringState"].isRHD - frame_id = sm["roadCameraState"].frameId + frame_id = sm["narrowRoadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS - if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: + if sm.updated["liveCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) - dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] - model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32) + dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] + main_intrinsics = dc.wide_road.intrinsics if main_wide_camera else dc.narrow_road.intrinsics + model_transform_main = get_warp_matrix(device_from_calib_euler, main_intrinsics, False).astype(np.float32) has_wide_camera = use_extra_client or main_wide_camera - model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if has_wide_camera else dc.fcam.intrinsics, True).astype(np.float32) + extra_intrinsics = dc.wide_road.intrinsics if has_wide_camera else dc.narrow_road.intrinsics + model_transform_extra = get_warp_matrix(device_from_calib_euler, extra_intrinsics, True).astype(np.float32) live_calib_seen = True traffic_convention = np.zeros(2) diff --git a/openpilot/selfdrive/monitoring/policy.py b/openpilot/selfdrive/monitoring/policy.py index 1ce9f03b3e..b58f1e66dc 100644 --- a/openpilot/selfdrive/monitoring/policy.py +++ b/openpilot/selfdrive/monitoring/policy.py @@ -107,17 +107,17 @@ class DriverBlink: self.right = 0. # model output refers to center of undistorted+leveled image -ref_undistorted_cam = DEVICE_CAMERAS[("tici", "ar0231")].dcam -dcam_undistorted_FL = 598.0 -dcam_undistorted_W, dcam_undistorted_H = (ref_undistorted_cam.width, ref_undistorted_cam.height) +ref_undistorted_cam = DEVICE_CAMERAS[("tici", "ar0231")].cabin +cabin_undistorted_FL = 598.0 +cabin_undistorted_W, cabin_undistorted_H = (ref_undistorted_cam.width, ref_undistorted_cam.height) def face_orientation_from_model(orient_model, pos_model, rpy_calib): pitch_model = orient_model[0] yaw_model = orient_model[1] - face_pixel_position = ((pos_model[0]+0.5)*dcam_undistorted_W, (pos_model[1]+0.5)*dcam_undistorted_H) - yaw_focal_angle = atan2(face_pixel_position[0] - dcam_undistorted_W//2, dcam_undistorted_FL) - pitch_focal_angle = atan2(face_pixel_position[1] - dcam_undistorted_H//2, dcam_undistorted_FL) + face_pixel_position = ((pos_model[0]+0.5)*cabin_undistorted_W, (pos_model[1]+0.5)*cabin_undistorted_H) + yaw_focal_angle = atan2(face_pixel_position[0] - cabin_undistorted_W//2, cabin_undistorted_FL) + pitch_focal_angle = atan2(face_pixel_position[1] - cabin_undistorted_H//2, cabin_undistorted_FL) pitch = pitch_model + pitch_focal_angle yaw = -yaw_model + yaw_focal_angle diff --git a/openpilot/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc index 68d74f81a1..78d12bd2ed 100644 --- a/openpilot/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -278,9 +278,9 @@ void process_panda_state(Panda *panda, PubMaster *pm, bool engaged, bool is_onro void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, bool is_onroad) { static Params params; - static SubMaster sm({"deviceState", "driverCameraState"}); + static SubMaster sm({"deviceState", "cabinCameraState"}); - static uint64_t last_driver_camera_t = 0; + static uint64_t last_cabin_camera_t = 0; static uint16_t prev_fan_speed = 999; static int ir_pwr = 0; static int prev_ir_pwr = 999; @@ -304,20 +304,20 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, } } - if (sm.updated("driverCameraState")) { - auto event = sm["driverCameraState"]; - int cur_integ_lines = event.getDriverCameraState().getIntegLines(); + if (sm.updated("cabinCameraState")) { + auto event = sm["cabinCameraState"]; + int cur_integ_lines = event.getCabinCameraState().getIntegLines(); // reset the filter when camerad restarts - if (event.getDriverCameraState().getFrameId() < prev_frame_id) { + if (event.getCabinCameraState().getFrameId() < prev_frame_id) { integ_lines_filter.reset(0); integ_lines_filter_driver_view.reset(0); driver_view = params.getBool("IsDriverViewEnabled"); } - prev_frame_id = event.getDriverCameraState().getFrameId(); + prev_frame_id = event.getCabinCameraState().getFrameId(); cur_integ_lines = (driver_view ? integ_lines_filter_driver_view : integ_lines_filter).update(cur_integ_lines); - last_driver_camera_t = event.getLogMonoTime(); + last_cabin_camera_t = event.getLogMonoTime(); if (cur_integ_lines <= CUTOFF_IL) { ir_pwr = 0; @@ -329,7 +329,7 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, } // Disable IR on input timeout - if (nanos_since_boot() - last_driver_camera_t > 1e9) { + if (nanos_since_boot() - last_cabin_camera_t > 1e9) { ir_pwr = 0; } diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index c9bae1f1d3..0464647c3b 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -297,7 +297,7 @@ def comm_issue_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaste def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - all_cams = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') + all_cams = ('narrowRoadCameraState', 'cabinCameraState', 'wideRoadCameraState') bad_cams = [s.replace('State', '') for s in all_cams if s in sm.data.keys() and not sm.all_checks([s, ])] return NormalPermanentAlert("Camera Malfunction", ', '.join(bad_cams)) diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 2ca462a9e6..59c5f2695e 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -77,17 +77,17 @@ class SelfdriveD: self.gps_location_service = get_gps_location_service(self.params) self.gps_packets = [self.gps_location_service] self.sensor_packets = ["accelerometer", "gyroscope"] - self.camera_packets = ["roadCameraState", "driverCameraState", "wideRoadCameraState"] + self.camera_packets = ["narrowRoadCameraState", "cabinCameraState", "wideRoadCameraState"] # TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches self.car_state_sock = messaging.sub_sock('carState', timeout=20) ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] if SIMULATION: - ignore += ['driverCameraState', 'managerState'] + ignore += ['cabinCameraState', 'managerState'] if REPLAY: # no vipc in replay will make them ignored anyways - ignore += ['roadCameraState', 'wideRoadCameraState'] + ignore += ['narrowRoadCameraState', 'wideRoadCameraState'] self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', @@ -467,9 +467,9 @@ class SelfdriveD: timed_out = self.sm.frame * DT_CTRL > 6. if all_valid or timed_out or (SIMULATION and not REPLAY): available_streams = VisionIpcClient.available_streams("camerad", block=False) - if VisionStreamType.VISION_STREAM_ROAD not in available_streams: - self.sm.ignore_alive.append('roadCameraState') - self.sm.ignore_valid.append('roadCameraState') + if VisionStreamType.VISION_STREAM_NARROW_ROAD not in available_streams: + self.sm.ignore_alive.append('narrowRoadCameraState') + self.sm.ignore_valid.append('narrowRoadCameraState') if VisionStreamType.VISION_STREAM_WIDE_ROAD not in available_streams: self.sm.ignore_alive.append('wideRoadCameraState') self.sm.ignore_valid.append('wideRoadCameraState') diff --git a/openpilot/selfdrive/test/process_replay/README.md b/openpilot/selfdrive/test/process_replay/README.md index 28f3b7cd2a..f39333a3c6 100644 --- a/openpilot/selfdrive/test/process_replay/README.md +++ b/openpilot/selfdrive/test/process_replay/README.md @@ -104,9 +104,9 @@ Replaying processes that use VisionIPC (e.g. modeld, dmonitoringmodeld) require from openpilot.tools.lib.framereader import FrameReader frs = { - 'roadCameraState': FrameReader(...), + 'narrowRoadCameraState': FrameReader(...), 'wideRoadCameraState': FrameReader(...), - 'driverCameraState': FrameReader(...), + 'cabinCameraState': FrameReader(...), } output_logs = replay_process_with_name(['modeld', 'dmonitoringmodeld'], lr, frs=frs) diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index e38d1e1042..ff98389e2c 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -361,7 +361,7 @@ def migrate_peripheralState(msgs): return [], add_ops, [] -@migration(inputs=["roadEncodeIdx", "wideRoadEncodeIdx", "driverEncodeIdx", "roadCameraState", "wideRoadCameraState", "driverCameraState"]) +@migration(inputs=["narrowRoadEncodeIdx", "wideRoadEncodeIdx", "cabinEncodeIdx", "narrowRoadCameraState", "wideRoadCameraState", "cabinCameraState"]) def migrate_cameraStates(msgs): add_ops, del_ops = [], [] frame_to_encode_id = defaultdict(dict) @@ -369,7 +369,7 @@ def migrate_cameraStates(msgs): min_frame_id = defaultdict(lambda: float('inf')) for _, msg in msgs: - if msg.which() not in ["roadEncodeIdx", "wideRoadEncodeIdx", "driverEncodeIdx"]: + if msg.which() not in ["narrowRoadEncodeIdx", "wideRoadEncodeIdx", "cabinEncodeIdx"]: continue encode_index = getattr(msg, msg.which()) @@ -379,7 +379,7 @@ def migrate_cameraStates(msgs): frame_to_encode_id[meta.camera_state][encode_index.frameId] = encode_index.segmentId for index, msg in msgs: - if msg.which() not in ["roadCameraState", "wideRoadCameraState", "driverCameraState"]: + if msg.which() not in ["narrowRoadCameraState", "wideRoadCameraState", "cabinCameraState"]: continue camera_state = getattr(msg, msg.which()) @@ -392,7 +392,7 @@ def migrate_cameraStates(msgs): del_ops.append(index) continue - # fallback mechanism for logs without encodeIdx (e.g. logs from before 2022 with dcamera recording disabled) + # fallback mechanism for logs without encodeIdx (e.g. logs from before 2022 with driver recording disabled) # try to fake encode_id by subtracting lowest frameId encode_id = camera_state.frameId - min_frame_id[msg.which()] print(f"Faking encodeId to {encode_id} for camera feed {msg.which()} with frameId: {camera_state.frameId}") diff --git a/openpilot/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py index 3e5616e702..3b5038b24c 100755 --- a/openpilot/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -146,9 +146,10 @@ def trim_logs(logs, start_frame, end_frame, frs_types, include_all_types): def model_replay(lr, frs): # modeld is using frame pairs - modeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"roadCameraState", "wideRoadCameraState"}, - {"roadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "can"}) - dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"driverCameraState"}, {"driverEncodeIdx", "carParams", "can"}) + camera_states = {"narrowRoadCameraState", "wideRoadCameraState"} + modeld_logs = trim_logs(lr, START_FRAME, END_FRAME, camera_states, + {"narrowRoadEncodeIdx", "wideRoadEncodeIdx", "carParams", "carState", "carControl", "can"}) + dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"cabinCameraState"}, {"cabinEncodeIdx", "carParams", "can"}) if not SEND_EXTRA_INPUTS: modeld_logs = [msg for msg in modeld_logs if msg.which() != 'liveCalibration'] @@ -209,8 +210,8 @@ def get_frames(): print(f"Failed to load frames from cache {cache_name}: {e}") frs = { - 'roadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), - 'driverCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), + 'narrowRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "fcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), + 'cabinCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "dcamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), 'wideRoadCameraState': FrameReader(get_url(TEST_ROUTE, SEGMENT, "ecamera.hevc"), pix_fmt='nv12', cache_size=END_FRAME - START_FRAME), } for fr in frs.values(): diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index 5d2e5a22fa..346d532140 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -396,7 +396,7 @@ class ModeldCameraSyncRcvCallback: def __call__(self, msg, cfg, frame): self.is_dual_camera = len(cfg.vision_pubs) == 2 - if msg.which() == "roadCameraState": + if msg.which() == "narrowRoadCameraState": self.road_present = True elif msg.which() == "wideRoadCameraState": self.wide_road_present = True @@ -434,7 +434,7 @@ CONFIGS = [ pubs=[ "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", - "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", + "cabinCameraState", "narrowRoadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", ], @@ -549,28 +549,28 @@ CONFIGS = [ ), ProcessConfig( proc_name="modeld", - pubs=["deviceState", "roadCameraState", "wideRoadCameraState", "liveCalibration", "liveDelay", "driverMonitoringState", "carState", "carControl"], + pubs=["deviceState", "narrowRoadCameraState", "wideRoadCameraState", "liveCalibration", "liveDelay", "driverMonitoringState", "carState", "carControl"], subs=["modelV2", "drivingModelData", "cameraOdometry"], ignore=["logMonoTime", "modelV2.frameDropPerc", "modelV2.modelExecutionTime", "drivingModelData.frameDropPerc", "drivingModelData.modelExecutionTime"], should_recv_callback=ModeldCameraSyncRcvCallback(), tolerance=NUMPY_TOLERANCE, processing_time=0.020, - main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("roadCameraState").stream), - vision_pubs=["roadCameraState", "wideRoadCameraState"], + main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("narrowRoadCameraState").stream), + vision_pubs=["narrowRoadCameraState", "wideRoadCameraState"], ignore_alive_pubs=["wideRoadCameraState"], init_callback=get_car_params_callback, ), ProcessConfig( proc_name="dmonitoringmodeld", - pubs=["liveCalibration", "driverCameraState"], + pubs=["liveCalibration", "cabinCameraState"], subs=["driverStateV2"], ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"], - should_recv_callback=MessageBasedRcvCallback("driverCameraState"), + should_recv_callback=MessageBasedRcvCallback("cabinCameraState"), tolerance=NUMPY_TOLERANCE, processing_time=0.020, - main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("driverCameraState").stream), - vision_pubs=["driverCameraState"], - ignore_alive_pubs=["driverCameraState"], + main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("cabinCameraState").stream), + vision_pubs=["cabinCameraState"], + ignore_alive_pubs=["cabinCameraState"], ), ] diff --git a/openpilot/selfdrive/test/process_replay/vision_meta.py b/openpilot/selfdrive/test/process_replay/vision_meta.py index 28f2829650..6942d88367 100644 --- a/openpilot/selfdrive/test/process_replay/vision_meta.py +++ b/openpilot/selfdrive/test/process_replay/vision_meta.py @@ -4,14 +4,14 @@ from openpilot.common.realtime import DT_MDL, DT_DMON from openpilot.common.transformations.camera import DEVICE_CAMERAS VideoStreamMeta = namedtuple("VideoStreamMeta", ["camera_state", "encode_index", "stream", "dt", "frame_sizes"]) -ROAD_CAMERA_FRAME_SIZES = {k: (v.dcam.width, v.dcam.height) for k, v in DEVICE_CAMERAS.items()} -WIDE_ROAD_CAMERA_FRAME_SIZES = {k: (v.ecam.width, v.ecam.height) for k, v in DEVICE_CAMERAS.items() if v.ecam is not None} -DRIVER_CAMERA_FRAME_SIZES = {k: (v.dcam.width, v.dcam.height) for k, v in DEVICE_CAMERAS.items()} +NARROW_ROAD_CAMERA_FRAME_SIZES = {k: (v.narrow_road.width, v.narrow_road.height) for k, v in DEVICE_CAMERAS.items()} +WIDE_ROAD_CAMERA_FRAME_SIZES = {k: (v.wide_road.width, v.wide_road.height) for k, v in DEVICE_CAMERAS.items() if v.wide_road is not None} +CABIN_CAMERA_FRAME_SIZES = {k: (v.cabin.width, v.cabin.height) for k, v in DEVICE_CAMERAS.items()} VIPC_STREAM_METADATA = [ # metadata: (state_msg_type, encode_msg_type, stream_type, dt, frame_sizes) - ("roadCameraState", "roadEncodeIdx", VisionStreamType.VISION_STREAM_ROAD, DT_MDL, ROAD_CAMERA_FRAME_SIZES), + ("narrowRoadCameraState", "narrowRoadEncodeIdx", VisionStreamType.VISION_STREAM_NARROW_ROAD, DT_MDL, NARROW_ROAD_CAMERA_FRAME_SIZES), ("wideRoadCameraState", "wideRoadEncodeIdx", VisionStreamType.VISION_STREAM_WIDE_ROAD, DT_MDL, WIDE_ROAD_CAMERA_FRAME_SIZES), - ("driverCameraState", "driverEncodeIdx", VisionStreamType.VISION_STREAM_DRIVER, DT_DMON, DRIVER_CAMERA_FRAME_SIZES), + ("cabinCameraState", "cabinEncodeIdx", VisionStreamType.VISION_STREAM_CABIN, DT_DMON, CABIN_CAMERA_FRAME_SIZES), ] diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 2ee876a6ef..fb8cd1393f 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -83,8 +83,8 @@ TIMINGS = { "controlsState": [2.5, 0.35], "longitudinalPlan": [2.5, 0.5], "driverAssistance": [2.5, 0.5], - "roadCameraState": [2.5, 0.35], - "driverCameraState": [2.5, 0.35], + "narrowRoadCameraState": [2.5, 0.35], + "cabinCameraState": [2.5, 0.35], "modelV2": [2.5, 0.35], "driverStateV2": [2.5, 0.40], "livePose": [2.5, 0.35], @@ -304,7 +304,7 @@ class TestOnroad(OpenpilotTestCase): result += "------------------------------------------------\n" result += "----------------- SOF Timing ------------------\n" result += "------------------------------------------------\n" - for name in ['roadCameraState', 'wideRoadCameraState', 'driverCameraState']: + for name in ['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']: ts = self.ts[name]['timestampSof'] d_ms = np.diff(ts) / 1e6 d50 = np.abs(d_ms-50) @@ -317,8 +317,8 @@ class TestOnroad(OpenpilotTestCase): print(result) def test_camera_sync(self, subtests): - cam_states = ['roadCameraState', 'wideRoadCameraState', 'driverCameraState'] - encode_cams = ['roadEncodeIdx', 'wideRoadEncodeIdx', 'driverEncodeIdx'] + cam_states = ['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState'] + encode_cams = ['narrowRoadEncodeIdx', 'wideRoadEncodeIdx', 'cabinEncodeIdx'] for cams in (cam_states, encode_cams): with subtests.test(cams=cams): # sanity checks within a single cam @@ -349,15 +349,15 @@ class TestOnroad(OpenpilotTestCase): diff = (max(ts.values()) - min(ts.values())) assert diff < 2, f"Cameras not synced properly: frame_id={start+i}, {diff=:.1f}ms, {ts=}" - # driver camera should be staggered ~25ms from road camera + # cabin camera should be staggered ~25ms from road camera offset_ms = abs(self.ts[cams[2]]['timestampSof'][i] - self.ts[cams[0]]['timestampSof'][i]) / 1e6 - assert 20 < offset_ms < 30, f"driver camera stagger out of range at frame {start+i}: {offset_ms:.1f}ms" + assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {start+i}: {offset_ms:.1f}ms" def test_camera_encoder_matches(self, subtests): # sanity check that the frame metadata is consistent with the encoded frames - pairs = [('roadCameraState', 'roadEncodeIdx'), + pairs = [('narrowRoadCameraState', 'narrowRoadEncodeIdx'), ('wideRoadCameraState', 'wideRoadEncodeIdx'), - ('driverCameraState', 'driverEncodeIdx')] + ('cabinCameraState', 'cabinEncodeIdx')] for cam, enc in pairs: with subtests.test(camera=cam, encoder=enc): cam_frames = {fid: (sof, eof) for fid, sof, eof in zip( diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index 4fba2e8c99..8cad68978f 100755 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -34,7 +34,7 @@ class Proc: PROCS = [ - Proc(['camerad'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), + Proc(['camerad'], 1.65, atol=0.4, msgs=['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState']), Proc(['modeld'], 1.5, atol=0.2, msgs=['modelV2']), Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), diff --git a/openpilot/selfdrive/ui/layouts/settings/device.py b/openpilot/selfdrive/ui/layouts/settings/device.py index 22853b0bd9..ddc5c23160 100644 --- a/openpilot/selfdrive/ui/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/layouts/settings/device.py @@ -5,7 +5,7 @@ from openpilot.cereal import messaging, log from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog +from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.layouts.onboarding import TrainingGuide from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog @@ -21,7 +21,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller # Description constants DESCRIPTIONS = { 'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."), - 'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"), + 'cabin_camera': tr_noop("Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)"), 'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."), 'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"), } @@ -57,8 +57,8 @@ class DeviceLayout(Widget): text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))), text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))), self._pair_device_btn, - button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']), - callback=lambda: gui_app.push_widget(DriverCameraDialog()), enabled=ui_state.is_offroad), + button_item(lambda: tr("Cabin Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['cabin_camera']), + callback=lambda: gui_app.push_widget(CabinCameraDialog()), enabled=ui_state.is_offroad), self._reset_calib_btn, button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']), self._on_review_training_guide, enabled=ui_state.is_offroad), diff --git a/openpilot/selfdrive/ui/layouts/settings/toggles.py b/openpilot/selfdrive/ui/layouts/settings/toggles.py index b03ecd10ec..090c49f14e 100644 --- a/openpilot/selfdrive/ui/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -28,7 +28,7 @@ DESCRIPTIONS = { "without a turn signal activated while driving over 31 mph (50 km/h)." ), "AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."), - 'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), + 'RecordFront': tr_noop("Upload data from the cabin camera and help improve the driver monitoring algorithm."), "IsMetric": tr_noop("Display speed in km/h instead of mph."), "RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), } @@ -73,7 +73,7 @@ class TogglesLayout(Widget): False, ), "RecordFront": ( - lambda: tr("Record and Upload Driver Camera"), + lambda: tr("Record and Upload Cabin Camera"), DESCRIPTIONS["RecordFront"], "monitoring.png", True, diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py index f1ebd3bacd..2b448ac73d 100644 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -16,10 +16,10 @@ from openpilot.common.version import terms_version, training_version from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import BaseDriverCameraDialog +from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import BaseCabinCameraDialog -class DriverCameraSetupDialog(BaseDriverCameraDialog): +class CabinCameraSetupDialog(BaseCabinCameraDialog): def __init__(self): super().__init__() self.driver_state_renderer = DriverStateRenderer(inset=True) @@ -104,7 +104,7 @@ class TrainingGuideDMTutorial(NavWidget): self._good_button.set_enabled(False) self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) - self._dialog = DriverCameraSetupDialog() + self._dialog = CabinCameraSetupDialog() self._bad_face_page = DMBadFaceDetected() # Disable driver monitoring model when device times out for inactivity @@ -231,7 +231,7 @@ class TrainingGuideRecordFront(NavScroller): exit_on_confirm=False) self._scroller.add_widgets([ - GreyBigButton("driver camera data", "do you want to share video data for training?", + GreyBigButton("cabin camera data", "do you want to share video data for training?", gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)), GreyBigButton("", "Sharing your data with comma helps improve openpilot for everyone."), self._accept_button, diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py index 7f4a9ab0b8..6038ff7d35 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/device.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog -from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog +from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr @@ -189,9 +189,9 @@ class DeviceLayoutMici(NavScroller): regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) regulatory_btn.set_click_callback(self._on_regulatory) - driver_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64)) - driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) - driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) + cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64)) + cabin_cam_btn.set_click_callback(lambda: gui_app.push_widget(CabinCameraDialog())) + cabin_cam_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn = BigButton("review\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self)))) @@ -204,7 +204,7 @@ class DeviceLayoutMici(NavScroller): DeviceInfoLayoutMici(), PairBigButton(), review_training_guide_btn, - driver_cam_btn, + cabin_cam_btn, terms_btn, regulatory_btn, reset_calibration_btn, diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py index 57cbf9410b..4d5113448d 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py @@ -47,7 +47,7 @@ class TogglesLayoutMici(NavScroller): is_metric_toggle = BigParamControl("use metric units", "IsMetric") ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled") always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM") - record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback) + record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback) record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback) enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback) diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 92f6b835e3..46348a9f16 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -21,7 +21,7 @@ from enum import IntEnum OpState = log.SelfdriveState.OpenpilotState CALIBRATED = log.LiveCalibrationData.Status.calibrated -ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD +NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] @@ -130,7 +130,7 @@ class BookmarkIcon(Widget): class AugmentedRoadView(CameraView): - def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): + def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_NARROW_ROAD): super().__init__("camerad", stream_type) self._bookmark_callback = bookmark_callback self._set_placeholder_color(rl.BLACK) @@ -250,12 +250,12 @@ class AugmentedRoadView(CameraView): if v_ego < WIDE_CAM_MAX_SPEED: target = WIDE_CAM elif v_ego > ROAD_CAM_MIN_SPEED: - target = ROAD_CAM + target = NARROW_ROAD_CAM else: # Hysteresis zone - keep current stream target = self.stream_type else: - target = ROAD_CAM + target = NARROW_ROAD_CAM if self.stream_type != target: self.switch_stream(target) @@ -263,8 +263,8 @@ class AugmentedRoadView(CameraView): def _update_calibration(self): # Update device camera if not already set sm = ui_state.sm - if not self.device_camera and sm.seen['roadCameraState'] and sm.seen['deviceState']: - self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: + self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] # Check if live calibration data is available and valid if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']): @@ -298,7 +298,7 @@ class AugmentedRoadView(CameraView): # Get camera configuration device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA is_wide_camera = self.stream_type == WIDE_CAM - intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics + intrinsic = device_camera.wide_road.intrinsics if is_wide_camera else device_camera.narrow_road.intrinsics calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib if is_wide_camera: zoom = 0.7 * 1.5 @@ -353,14 +353,14 @@ class AugmentedRoadView(CameraView): if __name__ == "__main__": gui_app.init_window("OnRoad Camera View") - road_camera_view = AugmentedRoadView(lambda: None, stream_type=ROAD_CAM) + road_camera_view = AugmentedRoadView(lambda: None, stream_type=NARROW_ROAD_CAM) print("***press space to switch camera view***") try: for _ in gui_app.render(): ui_state.update() if rl.is_key_released(rl.KeyboardKey.KEY_SPACE): if WIDE_CAM in road_camera_view.available_streams: - stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM + stream = NARROW_ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM road_camera_view.switch_stream(stream) road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: diff --git a/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py similarity index 96% rename from openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py rename to openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py index 24276c42b8..e86c0aa739 100644 --- a/openpilot/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py @@ -11,7 +11,7 @@ from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.label import gui_label -class DriverCameraView(CameraView): +class CabinCameraView(CameraView): def _calc_frame_matrix(self, rect: rl.Rectangle): base = super()._calc_frame_matrix(rect) driver_view_ratio = 1.5 @@ -20,11 +20,11 @@ class DriverCameraView(CameraView): return base -class BaseDriverCameraDialog(Widget): +class BaseCabinCameraDialog(Widget): # Not a NavWidget so training guide can use this without back navigation def __init__(self): super().__init__() - self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) + self._camera_view = CabinCameraView("camerad", VisionStreamType.VISION_STREAM_CABIN) self.driver_state_renderer = DriverStateRenderer(lines=True) self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) self.driver_state_renderer.load_icons() @@ -229,7 +229,7 @@ class BaseDriverCameraDialog(Widget): rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob))) -class DriverCameraDialog(NavWidget, BaseDriverCameraDialog): +class CabinCameraDialog(NavWidget, BaseCabinCameraDialog): def __init__(self): super().__init__() # TODO: this can grow unbounded, should be given some thought @@ -237,12 +237,12 @@ class DriverCameraDialog(NavWidget, BaseDriverCameraDialog): if __name__ == "__main__": - gui_app.init_window("Driver Camera View (mici)") + gui_app.init_window("Cabin Camera View (mici)") - driver_camera_view = DriverCameraDialog() - gui_app.push_widget(driver_camera_view) + cabin_camera_view = CabinCameraDialog() + gui_app.push_widget(cabin_camera_view) try: for _ in gui_app.render(): ui_state.update() finally: - driver_camera_view.close() + cabin_camera_view.close() diff --git a/openpilot/selfdrive/ui/mici/onroad/cameraview.py b/openpilot/selfdrive/ui/mici/onroad/cameraview.py index 64dd6d023d..8a3e2cbed5 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/mici/onroad/cameraview.py @@ -126,7 +126,7 @@ class CameraView(Widget): self._engaged_loc = rl.get_shader_location(self.shader, "engaged") self._engaged_val = rl.ffi.new("int[1]", [1]) self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver") - self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0]) + self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_CABIN else 0]) self.frame: VisionBuf | None = None self.texture_y: rl.Texture | None = None @@ -243,8 +243,8 @@ class CameraView(Widget): transform = self._calc_frame_matrix(rect) src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) - # Flip driver camera horizontally - if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER: + # Flip cabin camera horizontally + if self._stream_type == VisionStreamType.VISION_STREAM_CABIN: src_rect.width = -src_rect.width # Calculate scale @@ -410,6 +410,6 @@ class CameraView(Widget): if __name__ == "__main__": gui_app.init_window("camera view") - road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + road = CameraView("camerad", VisionStreamType.VISION_STREAM_NARROW_ROAD) for _ in gui_app.render(): road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py index 271cb2704f..c0f1ac1510 100755 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -4,13 +4,13 @@ import unittest # FIXME: known small leaks not worth worrying about at the moment KNOWN_LEAKS = { - "openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog.DriverCameraView", + "openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog.CabinCameraView", "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", - "openpilot.selfdrive.ui.onroad.driver_camera_dialog.DriverCameraDialog", + "openpilot.selfdrive.ui.onroad.cabin_camera_dialog.CabinCameraDialog", "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", @@ -51,13 +51,13 @@ class TestWidgetLeaks(OpenpilotTestCase): # mici dialogs from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow - from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog + from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog as MiciCabinCameraDialog from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal # tici dialogs - from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog + from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog as TiciCabinCameraDialog from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog @@ -71,7 +71,7 @@ class TestWidgetLeaks(OpenpilotTestCase): for ctor in ( # mici - MiciDriverCameraDialog, MiciPairingDialog, + MiciCabinCameraDialog, MiciPairingDialog, lambda: MiciTrainingGuide(lambda: None), lambda: MiciOnboardingWindow(lambda: None), lambda: BigDialog("test", "test"), @@ -79,7 +79,7 @@ class TestWidgetLeaks(OpenpilotTestCase): lambda: BigInputDialog("test"), lambda: MiciFccModal(text="test"), # tici - TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, + TiciCabinCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, lambda: ConfirmDialog("test", "ok"), lambda: MultiOptionDialog("test", ["a", "b"]), lambda: HtmlModal(text="test"), diff --git a/openpilot/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py index ff0cfb5eea..44d8910a03 100644 --- a/openpilot/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -15,7 +15,7 @@ from openpilot.common.transformations.orientation import rot_from_euler OpState = log.SelfdriveState.OpenpilotState CALIBRATED = log.LiveCalibrationData.Status.calibrated -ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD +NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] @@ -31,7 +31,7 @@ INF_POINT = np.array([1000.0, 0.0, 0.0]) class AugmentedRoadView(CameraView): - def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): + def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_NARROW_ROAD): super().__init__("camerad", stream_type) self._set_placeholder_color(BORDER_COLORS[UIStatus.DISENGAGED]) @@ -115,12 +115,12 @@ class AugmentedRoadView(CameraView): if v_ego < WIDE_CAM_MAX_SPEED: target = WIDE_CAM elif v_ego > ROAD_CAM_MIN_SPEED: - target = ROAD_CAM + target = NARROW_ROAD_CAM else: # Hysteresis zone - keep current stream target = self.stream_type else: - target = ROAD_CAM + target = NARROW_ROAD_CAM if self.stream_type != target: self.switch_stream(target) @@ -128,8 +128,8 @@ class AugmentedRoadView(CameraView): def _update_calibration(self): # Update device camera if not already set sm = ui_state.sm - if not self.device_camera and sm.seen['roadCameraState'] and sm.seen['deviceState']: - self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] + if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: + self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] # Check if live calibration data is available and valid if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']): @@ -162,7 +162,7 @@ class AugmentedRoadView(CameraView): # Get camera configuration device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA is_wide_camera = self.stream_type == WIDE_CAM - intrinsic = device_camera.ecam.intrinsics if is_wide_camera else device_camera.fcam.intrinsics + intrinsic = device_camera.wide_road.intrinsics if is_wide_camera else device_camera.narrow_road.intrinsics calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib zoom = 2.0 if is_wide_camera else 1.1 @@ -213,7 +213,7 @@ class AugmentedRoadView(CameraView): if __name__ == "__main__": gui_app.init_window("OnRoad Camera View") - road_camera_view = AugmentedRoadView(ROAD_CAM) + road_camera_view = AugmentedRoadView(NARROW_ROAD_CAM) gui_app.push_widget(road_camera_view) print("***press space to switch camera view***") try: @@ -221,7 +221,7 @@ if __name__ == "__main__": ui_state.update() if rl.is_key_released(rl.KeyboardKey.KEY_SPACE): if WIDE_CAM in road_camera_view.available_streams: - stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM + stream = NARROW_ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM road_camera_view.switch_stream(stream) finally: road_camera_view.close() diff --git a/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py similarity index 92% rename from openpilot/selfdrive/ui/onroad/driver_camera_dialog.py rename to openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py index 8735774458..7bb1917c35 100644 --- a/openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py @@ -9,9 +9,9 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.label import gui_label -class DriverCameraDialog(CameraView): +class CabinCameraDialog(CameraView): def __init__(self): - super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER) + super().__init__("camerad", VisionStreamType.VISION_STREAM_CABIN) self.driver_state_renderer = DriverStateRenderer() # TODO: this can grow unbounded, should be given some thought device.add_interactive_timeout_callback(gui_app.pop_widget) @@ -100,12 +100,12 @@ class DriverCameraDialog(CameraView): if __name__ == "__main__": - gui_app.init_window("Driver Camera View") + gui_app.init_window("Cabin Camera View") - driver_camera_view = DriverCameraDialog() - gui_app.push_widget(driver_camera_view) + cabin_camera_view = CabinCameraDialog() + gui_app.push_widget(cabin_camera_view) try: for _ in gui_app.render(): ui_state.update() finally: - driver_camera_view.close() + cabin_camera_view.close() diff --git a/openpilot/selfdrive/ui/onroad/cameraview.py b/openpilot/selfdrive/ui/onroad/cameraview.py index 3b6a2fcd30..aa4f2271dc 100644 --- a/openpilot/selfdrive/ui/onroad/cameraview.py +++ b/openpilot/selfdrive/ui/onroad/cameraview.py @@ -203,8 +203,8 @@ class CameraView(Widget): transform = self._calc_frame_matrix(rect) src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) - # Flip driver camera horizontally - if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER: + # Flip cabin camera horizontally + if self._stream_type == VisionStreamType.VISION_STREAM_CABIN: src_rect.width = -src_rect.width # Calculate scale @@ -363,6 +363,6 @@ class CameraView(Widget): if __name__ == "__main__": gui_app.init_window("camera view") - road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + road = CameraView("camerad", VisionStreamType.VISION_STREAM_NARROW_ROAD) for _ in gui_app.render(): road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/openpilot/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py index 91e6d3b921..30e811dbc9 100644 --- a/openpilot/selfdrive/ui/tests/diff/replay_script.py +++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py @@ -321,7 +321,7 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None: lambda: swipe_left(width * 2), click, # first page, click next lambda: swipe_left(width * 2), swipe_down # second page, go back (TODO: make driver cam preview work) ), - None, # TODO: preview driver camera; enabling this causes MultiplePublishersError later in onroad alert tests + None, # TODO: preview cabin camera; enabling this causes MultiplePublishersError later in onroad alert tests lambda: explore_setting(swipe_left), # terms & conditions (swipe to view QR code) lambda: explore_setting(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # regulatory info lambda: run_actions(click, lambda: swipe_left(width)), # reset calibration confirm (goes back automatically) diff --git a/openpilot/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py index 7c73bfa97b..98816f9ddf 100755 --- a/openpilot/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -90,7 +90,7 @@ if __name__ == "__main__": W, H = 2048, 1216 vipc = VisionIpcServer("camerad") - vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H) + vipc.create_buffers(VisionStreamType.VISION_STREAM_NARROW_ROAD, 5, W, H) vipc.start_listener() yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 yuv_data = np.random.default_rng().integers(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() @@ -100,7 +100,7 @@ if __name__ == "__main__": 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) + vipc.send(VisionStreamType.VISION_STREAM_NARROW_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof) ui_state.update() pr.dump_stats(f'{args.output}_deterministic.stats') diff --git a/openpilot/selfdrive/ui/translations/app.pot b/openpilot/selfdrive/ui/translations/app.pot index cb0491e107..cdd0af4c86 100644 --- a/openpilot/selfdrive/ui/translations/app.pot +++ b/openpilot/selfdrive/ui/translations/app.pot @@ -381,7 +381,7 @@ msgid "ERROR" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/device.py @@ -449,7 +449,7 @@ msgid "Serial" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" +msgid "Cabin Camera" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/device.py @@ -704,7 +704,7 @@ msgid "Enable driver monitoring even when openpilot is not engaged." msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -748,7 +748,7 @@ msgid "Always-On Driver Monitoring" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" +msgid "Record and Upload Cabin Camera" msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py @@ -779,10 +779,6 @@ msgstr "" msgid "Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode." msgstr "" -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py -msgid "camera starting" -msgstr "" - #: openpilot/selfdrive/ui/onroad/hud_renderer.py msgid "MAX" msgstr "" @@ -795,6 +791,10 @@ msgstr "" msgid "mph" msgstr "" +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py +msgid "camera starting" +msgstr "" + #: openpilot/selfdrive/ui/onroad/alert_renderer.py msgid "openpilot Unavailable" msgstr "" diff --git a/openpilot/selfdrive/ui/translations/app_de.po b/openpilot/selfdrive/ui/translations/app_de.po index 43baa133ae..1a3cd51ac4 100644 --- a/openpilot/selfdrive/ui/translations/app_de.po +++ b/openpilot/selfdrive/ui/translations/app_de.po @@ -126,6 +126,10 @@ msgstr "VERBINDUNG" msgid "CONNECTING..." msgstr "VERBINDUNG" +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "Dongle-ID" msgid "Download" msgstr "Herunterladen" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Fahrerkamera" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Fahrstil" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Verhindern Sie das Hochladen großer Datenmengen bei einer getakteten Mobilfunkverbindung" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Vorschau der Fahrer‑Kamera, um sicherzustellen, dass die Fahrerüberwachung gute Sicht hat. (Fahrzeug muss ausgeschaltet sein)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Neustarten und aktualisieren" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Fahrerkamera aufzeichnen und hochladen" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Jetzt abonnieren" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Daten von der Fahrer‑Kamera hochladen und den Fahrerüberwachungs‑Algorithmus verbessern." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "Sie müssen die Nutzungsbedingungen akzeptieren, um openpilot zu verwend msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "Sie müssen die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden. Lesen Sie die aktuellen Bedingungen unter https://comma.ai/terms, bevor Sie fortfahren." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "Kamera startet" diff --git a/openpilot/selfdrive/ui/translations/app_en.po b/openpilot/selfdrive/ui/translations/app_en.po index 3e578de6a4..c45da337eb 100644 --- a/openpilot/selfdrive/ui/translations/app_en.po +++ b/openpilot/selfdrive/ui/translations/app_en.po @@ -126,6 +126,10 @@ msgstr "CONNECT" msgid "CONNECTING..." msgstr "CONNECTING..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "Dongle ID" msgid "Download" msgstr "Download" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Driver Camera" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Driving Personality" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Prevent large data uploads when on a metered cellular connection" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Reboot and Update" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Record and Upload Driver Camera" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Upgrade Now" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Upload data from the driver facing camera and help improve the driver monitoring algorithm." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "You must accept the Terms and Conditions in order to use openpilot." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "camera starting" diff --git a/openpilot/selfdrive/ui/translations/app_es.po b/openpilot/selfdrive/ui/translations/app_es.po index e1c74f53c2..55ba293128 100644 --- a/openpilot/selfdrive/ui/translations/app_es.po +++ b/openpilot/selfdrive/ui/translations/app_es.po @@ -126,6 +126,10 @@ msgstr "CONECTAR" msgid "CONNECTING..." msgstr "CONECTAR" +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "ID del dongle" msgid "Download" msgstr "Descargar" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Cámara del conductor" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Estilo de conducción" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Evite grandes cargas de datos cuando esté en una conexión celular medida" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Previsualiza la cámara hacia el conductor para asegurarte de que la supervisión del conductor tenga buena visibilidad. (el vehículo debe estar apagado)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Reiniciar y actualizar" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Grabar y subir cámara del conductor" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Mejorar ahora" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Sube datos de la cámara orientada al conductor y ayuda a mejorar el algoritmo de supervisión del conductor." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "Debes aceptar los Términos y Condiciones para poder usar openpilot." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "Debes aceptar los Términos y Condiciones para usar openpilot. Lee los términos más recientes en https://comma.ai/terms antes de continuar." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "iniciando cámara" diff --git a/openpilot/selfdrive/ui/translations/app_fr.po b/openpilot/selfdrive/ui/translations/app_fr.po index b76018394f..1e0f370dcd 100644 --- a/openpilot/selfdrive/ui/translations/app_fr.po +++ b/openpilot/selfdrive/ui/translations/app_fr.po @@ -126,6 +126,10 @@ msgstr "CONNECTER" msgid "CONNECTING..." msgstr "CONNECTER..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "ID du dongle" msgid "Download" msgstr "Télécharger" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Caméra conducteur" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Personnalité de conduite" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Eviter les transferts de données volumineux lors d'une connexion à un réseau cellulaire limité" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Prévisualisez la caméra orientée conducteur pour vous assurer que la surveillance du conducteur a une bonne visibilité. (le véhicule doit être éteint)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Redémarrer et mettre à jour" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Enregistrer et téléverser la caméra conducteur" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Mettre à niveau maintenant" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Téléverser les données de la caméra orientée conducteur et aider à améliorer l'algorithme de surveillance du conducteur." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "Vous devez accepter les conditions générales pour utiliser openpilot." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "Vous devez accepter les conditions générales pour utiliser openpilot. Lisez les dernières conditions sur https://comma.ai/terms avant de continuer." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "démarrage de la caméra" diff --git a/openpilot/selfdrive/ui/translations/app_ja.po b/openpilot/selfdrive/ui/translations/app_ja.po index 4fbdb6fe80..2a2f76ae15 100644 --- a/openpilot/selfdrive/ui/translations/app_ja.po +++ b/openpilot/selfdrive/ui/translations/app_ja.po @@ -126,6 +126,10 @@ msgstr "接続" msgid "CONNECTING..." msgstr "接続中..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "ドングルID" msgid "Download" msgstr "ダウンロード" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "ドライバーカメラ" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "走行性格" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "従量課金の携帯回線接続時は大きなデータのアップロードを抑制" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "ドライバー向きカメラのプレビューでモニタリングの視界を確認します。(車両は停止状態である必要があります)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "再起動して更新" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "ドライバーカメラを記録してアップロード" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "今すぐアップグレード" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "ドライバー向きカメラのデータをアップロードしてモニタリングアルゴリズムの改善に協力してください。" +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "openpilotを使用するには、利用規約に同意する必要があ msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "openpilotを使用するには利用規約に同意する必要があります。続行する前に https://comma.ai/terms の最新の規約をお読みください。" -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "カメラを起動中" diff --git a/openpilot/selfdrive/ui/translations/app_ko.po b/openpilot/selfdrive/ui/translations/app_ko.po index 29d297be88..863986c0b6 100644 --- a/openpilot/selfdrive/ui/translations/app_ko.po +++ b/openpilot/selfdrive/ui/translations/app_ko.po @@ -126,6 +126,10 @@ msgstr "연결" msgid "CONNECTING..." msgstr "연결 중..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "동글 ID" msgid "Download" msgstr "다운로드" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "운전자 카메라" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "주행 성향" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "종량제 셀룰러 연결 시 대용량 업로드 방지" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "운전자 모니터링의 가시성을 확인하기 위해 운전자 카메라를 미리 봅니다. (차량은 꺼져 있어야 합니다)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "재시작 및 업데이트" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "운전자 카메라 기록 및 업로드" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "지금 업그레이드" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "운전자 방향 카메라 데이터를 업로드하여 운전자 모니터링 알고리즘 개선에 도움을 주세요." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "openpilot을 사용하려면 약관에 동의해야 합니다." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "openpilot을 사용하려면 약관에 동의해야 합니다. 계속하기 전에 https://comma.ai/terms 에서 최신 약관을 읽어주세요." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "카메라 시작 중" diff --git a/openpilot/selfdrive/ui/translations/app_pt-BR.po b/openpilot/selfdrive/ui/translations/app_pt-BR.po index 29a95b22fd..a478ccc931 100644 --- a/openpilot/selfdrive/ui/translations/app_pt-BR.po +++ b/openpilot/selfdrive/ui/translations/app_pt-BR.po @@ -126,6 +126,10 @@ msgstr "CONECTAR" msgid "CONNECTING..." msgstr "CONECTANDO..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "ID do Dongle" msgid "Download" msgstr "Baixar" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Câmera do Motorista" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Personalidade" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Evitar uploads grandes de dados em conexões móveis limitadas" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Pré-visualize a câmera voltada para o motorista para garantir que o monitoramento do motorista tenha boa visibilidade. (veículo deve estar desligado)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Reiniciar e Atualizar" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Gravar e Enviar Câmera do Motorista" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Atualizar Agora" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Envie dados da câmera voltada para o motorista e ajude a melhorar o algoritmo de monitoramento do motorista." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "Você deve aceitar os Termos e Condições para usar o openpilot." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "Você deve aceitar os Termos e Condições para usar o openpilot. Leia os termos mais recentes em https://comma.ai/terms antes de continuar." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "câmera iniciando" diff --git a/openpilot/selfdrive/ui/translations/app_th.po b/openpilot/selfdrive/ui/translations/app_th.po index 59cc8df33d..c15d859954 100644 --- a/openpilot/selfdrive/ui/translations/app_th.po +++ b/openpilot/selfdrive/ui/translations/app_th.po @@ -126,6 +126,10 @@ msgstr "เชื่อมต่อ" msgid "CONNECTING..." msgstr "กำลังเชื่อมต่อ..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "รหัสดองเกิล" msgid "Download" msgstr "ดาวน์โหลด" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "กล้องไดร์เวอร์" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "บุคลิกภาพในการขับขี่" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "ป้องกันการอัพโหลดข้อมูลขนาดใหญ่เมื่อใช้การเชื่อมต่อมือถือแบบคิดค่าบริการตามปริมาณข้อมูล" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "ดูตัวอย่างกล้องที่หันหน้าไปทางคนขับเพื่อให้แน่ใจว่าการตรวจสอบผู้ขับขี่มีทัศนวิสัยที่ดี (รถจะต้องถูกปิด)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "รีบูตและอัปเดต" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "บันทึกและอัพโหลดกล้องไดร์เวอร์" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "อัพเกรดทันที" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "อัปโหลดข้อมูลจากกล้องที่หันเข้าหาคนขับและช่วยปรับปรุงอัลกอริธึมการตรวจสอบผู้ขับขี่" +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "คุณต้องยอมรับข้อกำหนดและ msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "คุณต้องยอมรับข้อกำหนดและเงื่อนไขเพื่อใช้ openpilot อ่านข้อกำหนดล่าสุดได้ที่ https://comma.ai/terms ก่อนดำเนินการต่อ" -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "กำลังเริ่มกล้อง" diff --git a/openpilot/selfdrive/ui/translations/app_tr.po b/openpilot/selfdrive/ui/translations/app_tr.po index cf15e2c806..aefbb535de 100644 --- a/openpilot/selfdrive/ui/translations/app_tr.po +++ b/openpilot/selfdrive/ui/translations/app_tr.po @@ -126,6 +126,10 @@ msgstr "BAĞLAN" msgid "CONNECTING..." msgstr "BAĞLAN" +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "Dongle kimliği" msgid "Download" msgstr "İndir" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Sürücü Kamerası" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Sürüş Kişiliği" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Ölçülü bir hücresel bağlantıdayken büyük veri yüklemelerini engelle" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Sürücü izleme görünürlüğünün iyi olduğundan emin olmak için sürücüye bakan kamerayı önizleyin. (araç kapalı olmalıdır)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Yeniden Başlat ve Güncelle" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Sürücü Kamerasını Kaydet ve Yükle" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Şimdi Yükselt" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Sürücüye bakan kameradan veri yükleyin ve sürücü izleme algoritmasını geliştirmeye yardımcı olun." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "openpilot'u kullanmak için Şartlar ve Koşulları kabul etmelisiniz." msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "openpilot'u kullanmak için Şartlar ve Koşulları kabul etmelisiniz. Devam etmeden önce en güncel şartları https://comma.ai/terms adresinde okuyun." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "kamera başlatılıyor" diff --git a/openpilot/selfdrive/ui/translations/app_uk.po b/openpilot/selfdrive/ui/translations/app_uk.po index 24fab4f2e8..e4717ae462 100644 --- a/openpilot/selfdrive/ui/translations/app_uk.po +++ b/openpilot/selfdrive/ui/translations/app_uk.po @@ -126,6 +126,10 @@ msgstr "ПІДКЛЮЧИТИ" msgid "CONNECTING..." msgstr "ПІДКЛЮЧА..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "ID ключа" msgid "Download" msgstr "Завантажити" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "Камера водія" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "Стиль водіння" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "Запобігати великим завантаженням даних під час лімітного стільникового з'єднання" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "Попередньо перегляньте камеру, спрямовану на водія, щоб переконатися, що система моніторингу водія має добру видимість. (автомобіль повинен бути вимкнений)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "Перезавантажити та оновити" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "Писати та вантажити відео з камери водія" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "Оновити зараз" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "Завантажуйте дані з камери, спрямованої на водія, та допоможіть покращити алгоритм моніторингу водія." +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "Ви повинні прийняти Умови та положення, msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "Ви повинні прийняти Умови використання, щоб користуватися openpilot. Перед тим, як продовжити, ознайомтеся з останніми умовами на сайті https://comma.ai/terms." -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "запуск камери" diff --git a/openpilot/selfdrive/ui/translations/app_zh-CHS.po b/openpilot/selfdrive/ui/translations/app_zh-CHS.po index 2028b6ac1f..f1ec22857f 100644 --- a/openpilot/selfdrive/ui/translations/app_zh-CHS.po +++ b/openpilot/selfdrive/ui/translations/app_zh-CHS.po @@ -126,6 +126,10 @@ msgstr "连接" msgid "CONNECTING..." msgstr "连接中..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "设备 ID" msgid "Download" msgstr "下载" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "车内摄像头" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "驾驶风格" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "在计量制蜂窝网络时避免大量上传" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "预览车内摄像头以确保驾驶员监控视野良好。(车辆必须熄火)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "重启并更新" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "录制并上传车内摄像头" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "立即升级" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "上传车内摄像头数据,帮助改进驾驶员监控算法。" +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "您必须接受条款与条件才能使用 openpilot。" msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "您必须接受条款与条件才能使用 openpilot。继续前请阅读 https://comma.ai/terms 上的最新条款。" -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "相机启动中" diff --git a/openpilot/selfdrive/ui/translations/app_zh-CHT.po b/openpilot/selfdrive/ui/translations/app_zh-CHT.po index 39a67b432d..c5e5289984 100644 --- a/openpilot/selfdrive/ui/translations/app_zh-CHT.po +++ b/openpilot/selfdrive/ui/translations/app_zh-CHT.po @@ -126,6 +126,10 @@ msgstr "連線" msgid "CONNECTING..." msgstr "連線中..." +#: openpilot/selfdrive/ui/layouts/settings/device.py +msgid "Cabin Camera" +msgstr "" + #: openpilot/system/ui/widgets/confirm_dialog.py #: openpilot/system/ui/widgets/keyboard.py #: openpilot/system/ui/widgets/network.py @@ -205,10 +209,6 @@ msgstr "裝置 ID" msgid "Download" msgstr "下載" -#: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Driver Camera" -msgstr "車內鏡頭" - #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Driving Personality" msgstr "駕駛風格" @@ -466,8 +466,8 @@ msgid "Prevent large data uploads when on a metered cellular connection" msgstr "在計量制行動網路時避免大量上傳" #: openpilot/selfdrive/ui/layouts/settings/device.py -msgid "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)" -msgstr "預覽車內鏡頭以確保駕駛監控視野良好。(車輛須熄火)" +msgid "Preview the cabin camera to ensure that driver monitoring has good visibility. (vehicle must be off)" +msgstr "" #: openpilot/selfdrive/ui/widgets/pairing_dialog.py msgid "QR Code Error" @@ -498,8 +498,8 @@ msgid "Reboot and Update" msgstr "重新啟動並更新" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Record and Upload Driver Camera" -msgstr "錄製並上傳車內鏡頭" +msgid "Record and Upload Cabin Camera" +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Record and Upload Microphone Audio" @@ -638,8 +638,8 @@ msgid "Upgrade Now" msgstr "立即升級" #: openpilot/selfdrive/ui/layouts/settings/toggles.py -msgid "Upload data from the driver facing camera and help improve the driver monitoring algorithm." -msgstr "上傳車內鏡頭資料,協助改善駕駛監控演算法。" +msgid "Upload data from the cabin camera and help improve the driver monitoring algorithm." +msgstr "" #: openpilot/selfdrive/ui/layouts/settings/toggles.py msgid "Use Metric System" @@ -685,7 +685,7 @@ msgstr "您必須接受條款與細則才能使用 openpilot。" msgid "You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing." msgstr "您必須接受條款與細則才能使用 openpilot。繼續前請閱讀 https://comma.ai/terms 上的最新條款。" -#: openpilot/selfdrive/ui/onroad/driver_camera_dialog.py +#: openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py msgid "camera starting" msgstr "相機啟動中" diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index dca583d107..943ba46014 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -48,7 +48,7 @@ class UIState: "driverMonitoringState", "carState", "driverStateV2", - "roadCameraState", + "narrowRoadCameraState", "wideRoadCameraState", "managerState", "selfdriveState", diff --git a/openpilot/selfdrive/ui/watch3.py b/openpilot/selfdrive/ui/watch3.py index 162df82658..c601ccabe9 100755 --- a/openpilot/selfdrive/ui/watch3.py +++ b/openpilot/selfdrive/ui/watch3.py @@ -8,8 +8,8 @@ from openpilot.selfdrive.ui.onroad.cameraview import CameraView if __name__ == "__main__": gui_app.init_window("watch3") - road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) - driver = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) + road = CameraView("camerad", VisionStreamType.VISION_STREAM_NARROW_ROAD) + driver = CameraView("camerad", VisionStreamType.VISION_STREAM_CABIN) wide = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) for _ in gui_app.render(): road.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) diff --git a/openpilot/system/camerad/cameras/camera_qcom2.cc b/openpilot/system/camerad/cameras/camera_qcom2.cc index 63640920db..4d30cd1e2c 100644 --- a/openpilot/system/camerad/cameras/camera_qcom2.cc +++ b/openpilot/system/camerad/cameras/camera_qcom2.cc @@ -234,11 +234,11 @@ void CameraState::sendState() { framed.setSensor(camera.sensor->image_sensor); // Log raw frames for road camera - if (env_log_raw_frames && camera.cc.stream_type == VISION_STREAM_ROAD && meta.frame_id % 100 == 5) { // no overlap with qlog decimation + if (env_log_raw_frames && camera.cc.stream_type == VISION_STREAM_NARROW_ROAD && meta.frame_id % 100 == 5) { // no overlap with qlog decimation framed.setImage(get_raw_frame_image(&camera.buf)); } - set_camera_exposure(calculate_exposure_value(&camera.buf, ae_xywh, 2, camera.cc.stream_type != VISION_STREAM_DRIVER ? 2 : 4)); + set_camera_exposure(calculate_exposure_value(&camera.buf, ae_xywh, 2, camera.cc.stream_type != VISION_STREAM_CABIN ? 2 : 4)); // Send the message pm->send(camera.cc.publish_name, msg); diff --git a/openpilot/system/camerad/cameras/hw.h b/openpilot/system/camerad/cameras/hw.h index 45546258a9..8f9de5bede 100644 --- a/openpilot/system/camerad/cameras/hw.h +++ b/openpilot/system/camerad/cameras/hw.h @@ -44,12 +44,12 @@ const CameraConfig WIDE_ROAD_CAMERA_CONFIG = { .staggered_sof = false, }; -const CameraConfig ROAD_CAMERA_CONFIG = { +const CameraConfig NARROW_ROAD_CAMERA_CONFIG = { .camera_num = 1, - .stream_type = VISION_STREAM_ROAD, + .stream_type = VISION_STREAM_NARROW_ROAD, .focal_len = 8.0, - .publish_name = "roadCameraState", - .init_camera_state = &cereal::Event::Builder::initRoadCameraState, + .publish_name = "narrowRoadCameraState", + .init_camera_state = &cereal::Event::Builder::initNarrowRoadCameraState, .enabled = !getenv("DISABLE_ROAD"), .phy = CAM_ISP_IFE_IN_RES_PHY_1, .vignetting_correction = true, @@ -57,12 +57,12 @@ const CameraConfig ROAD_CAMERA_CONFIG = { .staggered_sof = false, }; -const CameraConfig DRIVER_CAMERA_CONFIG = { +const CameraConfig CABIN_CAMERA_CONFIG = { .camera_num = 2, - .stream_type = VISION_STREAM_DRIVER, + .stream_type = VISION_STREAM_CABIN, .focal_len = 1.71, - .publish_name = "driverCameraState", - .init_camera_state = &cereal::Event::Builder::initDriverCameraState, + .publish_name = "cabinCameraState", + .init_camera_state = &cereal::Event::Builder::initCabinCameraState, .enabled = !getenv("DISABLE_DRIVER"), .phy = CAM_ISP_IFE_IN_RES_PHY_2, .vignetting_correction = false, @@ -70,4 +70,4 @@ const CameraConfig DRIVER_CAMERA_CONFIG = { .staggered_sof = true, }; -const CameraConfig ALL_CAMERA_CONFIGS[] = {WIDE_ROAD_CAMERA_CONFIG, ROAD_CAMERA_CONFIG, DRIVER_CAMERA_CONFIG}; +const CameraConfig ALL_CAMERA_CONFIGS[] = {WIDE_ROAD_CAMERA_CONFIG, NARROW_ROAD_CAMERA_CONFIG, CABIN_CAMERA_CONFIG}; diff --git a/openpilot/system/camerad/snapshot.py b/openpilot/system/camerad/snapshot.py index f865a452ff..622da4faef 100755 --- a/openpilot/system/camerad/snapshot.py +++ b/openpilot/system/camerad/snapshot.py @@ -9,8 +9,8 @@ from openpilot.common.realtime import DT_MDL VISION_STREAMS = { - "roadCameraState": VisionStreamType.VISION_STREAM_ROAD, - "driverCameraState": VisionStreamType.VISION_STREAM_DRIVER, + "narrowRoadCameraState": VisionStreamType.VISION_STREAM_NARROW_ROAD, + "cabinCameraState": VisionStreamType.VISION_STREAM_CABIN, "wideRoadCameraState": VisionStreamType.VISION_STREAM_WIDE_ROAD, } @@ -45,7 +45,7 @@ def extract_image(buf): return yuv_to_rgb(y, u, v) -def get_snapshots(frame="roadCameraState", front_frame="driverCameraState"): +def get_snapshots(frame="narrowRoadCameraState", front_frame="cabinCameraState"): sockets = [s for s in (frame, front_frame) if s is not None] sm = messaging.SubMaster(sockets) vipc_clients = {s: VisionIpcClient("camerad", VISION_STREAMS[s], True) for s in sockets} diff --git a/openpilot/system/camerad/test/test_camerad.py b/openpilot/system/camerad/test/test_camerad.py index 580a30683b..410ea9fdb3 100755 --- a/openpilot/system/camerad/test/test_camerad.py +++ b/openpilot/system/camerad/test/test_camerad.py @@ -13,7 +13,7 @@ from openpilot.system.camerad.snapshot import get_snapshots from openpilot.selfdrive.test.helpers import collect_logs, log_collector, processes_context TEST_TIMESPAN = 10 -CAMERAS = ('roadCameraState', 'driverCameraState', 'wideRoadCameraState') +CAMERAS = ('narrowRoadCameraState', 'cabinCameraState', 'wideRoadCameraState') EXPOSURE_STABLE_COUNT = 3 EXPOSURE_RANGE = (0.15, 0.35) MAX_TEST_TIME = 25 @@ -49,7 +49,7 @@ def _camera_session(): exposure = {cam: [] for cam in CAMERAS} start = time.monotonic() while time.monotonic() - start < MAX_TEST_TIME: - rpic, dpic = get_snapshots(frame="roadCameraState", front_frame="driverCameraState") + rpic, dpic = get_snapshots(frame="narrowRoadCameraState", front_frame="cabinCameraState") wpic, _ = get_snapshots(frame="wideRoadCameraState") for cam, img in zip(CAMERAS, [rpic, dpic, wpic], strict=True): exposure[cam].append(_exposure_stats(img)) @@ -106,8 +106,8 @@ class TestCamerad(OpenpilotTestCase): assert set(np.diff(self.logs[c]['frameId'])) == {1, }, f"{c} has frame skips" def test_frame_sync(self): - SYNCED_CAMS = ('roadCameraState', 'wideRoadCameraState') - n = range(len(self.logs['roadCameraState']['t'][:-10])) + SYNCED_CAMS = ('narrowRoadCameraState', 'wideRoadCameraState') + n = range(len(self.logs['narrowRoadCameraState']['t'][:-10])) frame_ids = {i: [self.logs[cam]['frameId'][i] for cam in CAMERAS] for i in n} assert all(len(set(v)) == 1 for v in frame_ids.values()), "frame IDs not aligned" @@ -118,10 +118,10 @@ class TestCamerad(OpenpilotTestCase): laggy_frames = {k: v for k, v in diffs.items() if v > 1.1} assert len(laggy_frames) == 0, f"Frames not synced properly: {laggy_frames=}" - # driver camera should be staggered ~25ms from road camera + # cabin camera should be staggered ~25ms from road camera for i in n: - offset_ms = abs(self.logs['driverCameraState']['timestampSof'][i] - self.logs['roadCameraState']['timestampSof'][i]) / 1e6 - assert 20 < offset_ms < 30, f"driver camera stagger out of range at frame {i}: {offset_ms:.1f}ms (expected ~25ms)" + offset_ms = abs(self.logs['cabinCameraState']['timestampSof'][i] - self.logs['narrowRoadCameraState']['timestampSof'][i]) / 1e6 + assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {i}: {offset_ms:.1f}ms (expected ~25ms)" def test_sanity_checks(self): self._sanity_checks(self.logs) diff --git a/openpilot/system/camerad/webcam/camerad.py b/openpilot/system/camerad/webcam/camerad.py index bba982f897..4dfa5f3371 100755 --- a/openpilot/system/camerad/webcam/camerad.py +++ b/openpilot/system/camerad/webcam/camerad.py @@ -11,19 +11,19 @@ from openpilot.cereal import messaging from openpilot.system.camerad.webcam.camera import Camera from openpilot.common.realtime import Ratekeeper -ROAD_CAM = os.getenv("ROAD_CAM", "0") +NARROW_ROAD_CAM = os.getenv("NARROW_ROAD_CAM", os.getenv("ROAD_CAM", "0")) WIDE_CAM = os.getenv("WIDE_CAM") DRIVER_CAM = os.getenv("DRIVER_CAM") CameraType = namedtuple("CameraType", ["msg_name", "stream_type", "cam_id"]) CAMERAS = [ - CameraType("roadCameraState", VisionStreamType.VISION_STREAM_ROAD, ROAD_CAM) + CameraType("narrowRoadCameraState", VisionStreamType.VISION_STREAM_NARROW_ROAD, NARROW_ROAD_CAM) ] if WIDE_CAM: CAMERAS.append(CameraType("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD, WIDE_CAM)) if DRIVER_CAM: - CAMERAS.append(CameraType("driverCameraState", VisionStreamType.VISION_STREAM_DRIVER, DRIVER_CAM)) + CAMERAS.append(CameraType("cabinCameraState", VisionStreamType.VISION_STREAM_CABIN, DRIVER_CAM)) class Camerad: def __init__(self): diff --git a/openpilot/system/loggerd/clip_encoder.cc b/openpilot/system/loggerd/clip_encoder.cc index 884ef3f091..7e8f3a9026 100644 --- a/openpilot/system/loggerd/clip_encoder.cc +++ b/openpilot/system/loggerd/clip_encoder.cc @@ -29,11 +29,11 @@ constexpr int CLIP_FPS = 20; constexpr double PARALLEL_CLIP_MIN_DURATION = 2 * SEGMENT_DURATION; const EncoderInfo clip_encoder_info = { - .publish_name = "livestreamRoadEncodeData", + .publish_name = "livestreamNarrowRoadEncodeData", .record = false, .fps = CLIP_FPS, .get_settings = [](int) { return EncoderSettings::StreamEncoderSettings(); }, - INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), + INIT_ENCODE_FUNCTIONS(LivestreamNarrowRoadEncode), }; bool open_input(const std::string &path, AVFormatContext **ctx, int *stream_index) { diff --git a/openpilot/system/loggerd/loggerd.h b/openpilot/system/loggerd/loggerd.h index c6c7b41af1..beabd7d791 100644 --- a/openpilot/system/loggerd/loggerd.h +++ b/openpilot/system/loggerd/loggerd.h @@ -96,11 +96,11 @@ public: }; const EncoderInfo main_road_encoder_info = { - .publish_name = "roadEncodeData", + .publish_name = "narrowRoadEncodeData", .thumbnail_name = "thumbnail", .filename = "fcamera.hevc", .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, - INIT_ENCODE_FUNCTIONS(RoadEncode), + INIT_ENCODE_FUNCTIONS(NarrowRoadEncode), }; const EncoderInfo main_wide_road_encoder_info = { @@ -110,23 +110,23 @@ const EncoderInfo main_wide_road_encoder_info = { INIT_ENCODE_FUNCTIONS(WideRoadEncode), }; -const EncoderInfo main_driver_encoder_info = { - .publish_name = "driverEncodeData", +const EncoderInfo main_cabin_encoder_info = { + .publish_name = "cabinEncodeData", .filename = "dcamera.hevc", .record = Params().getBool("RecordFront"), .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, - INIT_ENCODE_FUNCTIONS(DriverEncode), + INIT_ENCODE_FUNCTIONS(CabinEncode), }; const EncoderInfo stream_road_encoder_info = { - .publish_name = "livestreamRoadEncodeData", + .publish_name = "livestreamNarrowRoadEncodeData", //.thumbnail_name = "thumbnail", .record = false, .is_live = true, .frame_width = livestream_width(), .frame_height = livestream_height(), .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, - INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), + INIT_ENCODE_FUNCTIONS(LivestreamNarrowRoadEncode), }; const EncoderInfo stream_wide_road_encoder_info = { @@ -139,29 +139,29 @@ const EncoderInfo stream_wide_road_encoder_info = { INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; -const EncoderInfo stream_driver_encoder_info = { - .publish_name = "livestreamDriverEncodeData", +const EncoderInfo stream_cabin_encoder_info = { + .publish_name = "livestreamCabinEncodeData", .record = false, .is_live = true, .frame_width = livestream_width(), .frame_height = livestream_height(), .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, - INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), + INIT_ENCODE_FUNCTIONS(LivestreamCabinEncode), }; const EncoderInfo qcam_encoder_info = { - .publish_name = "qRoadEncodeData", + .publish_name = "qNarrowRoadEncodeData", .filename = "qcamera.ts", .include_audio = Params().getBool("RecordAudio"), .frame_width = 526, .frame_height = 330, .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, - INIT_ENCODE_FUNCTIONS(QRoadEncode), + INIT_ENCODE_FUNCTIONS(QNarrowRoadEncode), }; -const LogCameraInfo road_camera_info{ - .thread_name = "road_cam_encoder", - .stream_type = VISION_STREAM_ROAD, +const LogCameraInfo narrow_road_camera_info{ + .thread_name = "narrow_road_cam_encoder", + .stream_type = VISION_STREAM_NARROW_ROAD, .encoder_infos = {main_road_encoder_info, qcam_encoder_info} }; @@ -171,15 +171,15 @@ const LogCameraInfo wide_road_camera_info{ .encoder_infos = {main_wide_road_encoder_info} }; -const LogCameraInfo driver_camera_info{ - .thread_name = "driver_cam_encoder", - .stream_type = VISION_STREAM_DRIVER, - .encoder_infos = {main_driver_encoder_info} +const LogCameraInfo cabin_camera_info{ + .thread_name = "cabin_cam_encoder", + .stream_type = VISION_STREAM_CABIN, + .encoder_infos = {main_cabin_encoder_info} }; const LogCameraInfo stream_road_camera_info{ - .thread_name = "road_cam_encoder", - .stream_type = VISION_STREAM_ROAD, + .thread_name = "narrow_road_cam_encoder", + .stream_type = VISION_STREAM_NARROW_ROAD, .encoder_infos = {stream_road_encoder_info}, }; @@ -189,11 +189,11 @@ const LogCameraInfo stream_wide_road_camera_info{ .encoder_infos = {stream_wide_road_encoder_info}, }; -const LogCameraInfo stream_driver_camera_info{ - .thread_name = "driver_cam_encoder", - .stream_type = VISION_STREAM_DRIVER, - .encoder_infos = {stream_driver_encoder_info}, +const LogCameraInfo stream_cabin_camera_info{ + .thread_name = "cabin_cam_encoder", + .stream_type = VISION_STREAM_CABIN, + .encoder_infos = {stream_cabin_encoder_info}, }; -const LogCameraInfo cameras_logged[] = {road_camera_info, wide_road_camera_info, driver_camera_info}; -const LogCameraInfo stream_cameras_logged[] = {stream_road_camera_info, stream_wide_road_camera_info, stream_driver_camera_info}; +const LogCameraInfo cameras_logged[] = {narrow_road_camera_info, wide_road_camera_info, cabin_camera_info}; +const LogCameraInfo stream_cameras_logged[] = {stream_road_camera_info, stream_wide_road_camera_info, stream_cabin_camera_info}; diff --git a/openpilot/system/loggerd/tests/test_encoder.py b/openpilot/system/loggerd/tests/test_encoder.py index 307a89ba95..10cd586f35 100755 --- a/openpilot/system/loggerd/tests/test_encoder.py +++ b/openpilot/system/loggerd/tests/test_encoder.py @@ -22,8 +22,8 @@ SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 def hevc_size(w): return FULL_SIZE // 2 if w <= 1344 else FULL_SIZE CAMERAS = [ - ("fcamera.hevc", 20, hevc_size, "roadEncodeIdx"), - ("dcamera.hevc", 20, hevc_size, "driverEncodeIdx"), + ("fcamera.hevc", 20, hevc_size, "narrowRoadEncodeIdx"), + ("dcamera.hevc", 20, hevc_size, "cabinEncodeIdx"), ("ecamera.hevc", 20, hevc_size, "wideRoadEncodeIdx"), ("qcamera.ts", 20, lambda x: 130000, None), ] diff --git a/openpilot/system/loggerd/tests/test_loggerd.py b/openpilot/system/loggerd/tests/test_loggerd.py index 333891ad91..de91cae1db 100644 --- a/openpilot/system/loggerd/tests/test_loggerd.py +++ b/openpilot/system/loggerd/tests/test_loggerd.py @@ -112,12 +112,12 @@ class TestLoggerd(OpenpilotTestCase): w, h = 320, 240 frame_spec = (w, h, w * h * 3 // 2, w, w * h) streams = [ - (VisionStreamType.VISION_STREAM_ROAD, frame_spec, "roadCameraState"), - (VisionStreamType.VISION_STREAM_DRIVER, frame_spec, "driverCameraState"), + (VisionStreamType.VISION_STREAM_NARROW_ROAD, frame_spec, "narrowRoadCameraState"), + (VisionStreamType.VISION_STREAM_CABIN, frame_spec, "cabinCameraState"), (VisionStreamType.VISION_STREAM_WIDE_ROAD, frame_spec, "wideRoadCameraState"), ] - sm = messaging.SubMaster(["roadEncodeData"]) + sm = messaging.SubMaster(["narrowRoadEncodeData"]) pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"]) vipc_server = VisionIpcServer("camerad") for stream_type, frame_spec, _ in streams: @@ -128,7 +128,7 @@ class TestLoggerd(OpenpilotTestCase): os.environ["LOGGERD_SEGMENT_LENGTH"] = str(segment_length) managed_processes["loggerd"].start() managed_processes["encoderd"].start() - assert pm.wait_for_readers_to_update("roadCameraState", timeout=5) + assert pm.wait_for_readers_to_update("narrowRoadCameraState", timeout=5) fps = 20 for n in range(1, int(num_segs * segment_length * fps) + 1): @@ -322,8 +322,8 @@ class TestLoggerd(OpenpilotTestCase): self._publish_camera_and_audio_messages() - dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc')) - assert dcamera_hevc_exists == record_front + cabin_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc')) + assert cabin_hevc_exists == record_front @parameterized.expand([True, False]) def test_record_audio(self, record_audio): diff --git a/openpilot/system/webrtc/device/video.py b/openpilot/system/webrtc/device/video.py index c300f76485..cb85dff73e 100644 --- a/openpilot/system/webrtc/device/video.py +++ b/openpilot/system/webrtc/device/video.py @@ -32,9 +32,9 @@ class EncodedVideoFrame: class LiveStreamVideoStreamTrack(TiciVideoStreamTrack): camera_to_sock_mapping = { - "driver": "livestreamDriverEncodeData", + "driver": "livestreamCabinEncodeData", "wideRoad": "livestreamWideRoadEncodeData", - "road": "livestreamRoadEncodeData", + "road": "livestreamNarrowRoadEncodeData", } def __init__(self, camera_type: str, video_enabled: bool = True): diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index 5a9dc8772c..d64cf6aed6 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -65,7 +65,7 @@ class TestStreamSession(OpenpilotTestCase): mocked_pubmaster.reset_mock() def test_livestream_track(self, mocker): - fake_msg = messaging.new_message("livestreamDriverEncodeData") + fake_msg = messaging.new_message("livestreamCabinEncodeData") config = {"receive.return_value": fake_msg.to_bytes()} mocker.patch("msgq.SubSocket", spec=True, **config) diff --git a/openpilot/tools/cabana/README.md b/openpilot/tools/cabana/README.md index b30c29640e..fbdfbb40e5 100644 --- a/openpilot/tools/cabana/README.md +++ b/openpilot/tools/cabana/README.md @@ -15,7 +15,7 @@ Options: --auto Auto load the route from the best available source (no video): internal, openpilotci, comma_api, car_segments, testing_closet --qcam load qcamera - --ecam load wide road camera + --wide-road load wide road camera --msgq read can messages from msgq --panda read can messages from panda --panda-serial read can messages from panda with given serial @@ -55,7 +55,7 @@ Replace "5beb9b58bd12b691/0000010a--a51155e496" with your desired route identifi To run Cabana with multiple cameras, use the following command: ```shell -cabana "5beb9b58bd12b691/0000010a--a51155e496" --dcam --ecam +cabana "5beb9b58bd12b691/0000010a--a51155e496" --cabin --wide-road ``` ### Streaming CAN Messages from a comma Device diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index d8e21815dc..8b21143faf 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -19,8 +19,8 @@ struct CabanaArgs { bool demo = false; bool auto_source = false; bool qcam = false; - bool ecam = false; - bool dcam = false; + bool wide_road = false; + bool cabin = false; bool msgq = false; bool panda = false; bool no_vipc = false; @@ -44,8 +44,8 @@ void printUsage(const char *argv0) { " --auto Auto load the route from the best available source (no video):\n" " internal, openpilotci, comma_api, car_segments, testing_closet\n" " --qcam load qcamera\n" - " --ecam load wide road camera\n" - " --dcam load driver camera\n" + " --wide-road load wide road camera (alias: --ecam)\n" + " --cabin load cabin camera (alias: --dcam)\n" " --msgq read can messages from the msgq\n" " --panda read can messages from panda\n" " --panda-serial read can messages from panda with given serial\n" @@ -83,10 +83,10 @@ int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) { args.auto_source = true; } else if (std::strcmp(a, "--qcam") == 0) { args.qcam = true; - } else if (std::strcmp(a, "--ecam") == 0) { - args.ecam = true; - } else if (std::strcmp(a, "--dcam") == 0) { - args.dcam = true; + } else if (std::strcmp(a, "--wide-road") == 0 || std::strcmp(a, "--ecam") == 0) { + args.wide_road = true; + } else if (std::strcmp(a, "--cabin") == 0 || std::strcmp(a, "--dcam") == 0) { + args.cabin = true; } else if (std::strcmp(a, "--msgq") == 0) { args.msgq = true; } else if (std::strcmp(a, "--panda") == 0) { @@ -162,9 +162,9 @@ int main(int argc, char *argv[]) { #endif } else { uint32_t replay_flags = REPLAY_FLAG_NONE; - if (args.ecam) replay_flags |= REPLAY_FLAG_ECAM; + if (args.wide_road) replay_flags |= REPLAY_FLAG_WIDE_ROAD; if (args.qcam) replay_flags |= REPLAY_FLAG_QCAMERA; - if (args.dcam) replay_flags |= REPLAY_FLAG_DCAM; + if (args.cabin) replay_flags |= REPLAY_FLAG_CABIN_CAMERA; if (args.no_vipc) replay_flags |= REPLAY_FLAG_NO_VIPC; QString route; diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc index a8df373dc4..9bd6b9be19 100644 --- a/openpilot/tools/cabana/cameraview.cc +++ b/openpilot/tools/cabana/cameraview.cc @@ -57,8 +57,8 @@ void CameraWidget::paintEvent(QPaintEvent *event) { QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h); p.setRenderHint(QPainter::SmoothPixmapTransform); - if (active_stream_type == VISION_STREAM_DRIVER) { - // mirror driver camera horizontally + if (active_stream_type == VISION_STREAM_CABIN) { + // mirror cabin camera horizontally const qreal cx = video_rect.x() + video_rect.width() / 2.0; p.translate(cx, 0); p.scale(-1, 1); diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index dbd44b54e3..6d54369aef 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -46,7 +46,7 @@ void ReplayStream::mergeSegments() { } bool ReplayStream::loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags, bool auto_source) { - replay.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"}, + replay.reset(new Replay(route, {"can", "narrowRoadEncodeIdx", "cabinEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, auto_source)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter([this](const Event *event) { return eventFilter(event); }); @@ -163,8 +163,8 @@ AbstractStream *OpenReplayWidget::open() { } else { auto replay_stream = std::make_unique(qApp); uint32_t flags = REPLAY_FLAG_NONE; - if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_DCAM; - if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM; + if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; + if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { diff --git a/openpilot/tools/cabana/tools/routeinfo.cc b/openpilot/tools/cabana/tools/routeinfo.cc index 77a0e065cd..dc272e3d12 100644 --- a/openpilot/tools/cabana/tools/routeinfo.cc +++ b/openpilot/tools/cabana/tools/routeinfo.cc @@ -14,7 +14,7 @@ RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) { table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); - table->setHorizontalHeaderLabels({"", "rlog", "fcam", "ecam", "dcam", "qlog", "qcam"}); + table->setHorizontalHeaderLabels({"", "rlog", "narrow road", "wide road", "driver", "qlog", "qcam"}); table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); table->verticalHeader()->setVisible(false); table->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -23,9 +23,9 @@ RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) { for (const auto &[seg_num, seg] : replay->route().segments()) { table->setItem(row, 0, new QTableWidgetItem(QString::number(seg_num))); table->setItem(row, 1, new QTableWidgetItem(seg.rlog.empty() ? "--" : "Yes")); - table->setItem(row, 2, new QTableWidgetItem(seg.road_cam.empty() ? "--" : "Yes")); + table->setItem(row, 2, new QTableWidgetItem(seg.narrow_road_cam.empty() ? "--" : "Yes")); table->setItem(row, 3, new QTableWidgetItem(seg.wide_road_cam.empty() ? "--" : "Yes")); - table->setItem(row, 4, new QTableWidgetItem(seg.driver_cam.empty() ? "--" : "Yes")); + table->setItem(row, 4, new QTableWidgetItem(seg.cabin_cam.empty() ? "--" : "Yes")); table->setItem(row, 5, new QTableWidgetItem(seg.qlog.empty() ? "--" : "Yes")); table->setItem(row, 6, new QTableWidgetItem(seg.qcamera.empty() ? "--" : "Yes")); ++row; diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 67b89cb080..bd61573658 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -148,7 +148,7 @@ QWidget *VideoWidget::createCameraWidget() { camera_tab->setAutoHide(true); camera_tab->setExpanding(false); - l->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_ROAD)); + l->addWidget(cam_widget = new StreamCameraView("camerad", VISION_STREAM_NARROW_ROAD)); cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); diff --git a/openpilot/tools/camerastream/compressed_vipc.py b/openpilot/tools/camerastream/compressed_vipc.py index 2ec538981b..e85c88d40c 100755 --- a/openpilot/tools/camerastream/compressed_vipc.py +++ b/openpilot/tools/camerastream/compressed_vipc.py @@ -19,8 +19,8 @@ V4L2_BUF_FLAG_KEYFRAME = 8 # then run this "./compressed_vipc.py " ENCODE_SOCKETS = { - VisionStreamType.VISION_STREAM_ROAD: "roadEncodeData", - VisionStreamType.VISION_STREAM_DRIVER: "driverEncodeData", + VisionStreamType.VISION_STREAM_NARROW_ROAD: "narrowRoadEncodeData", + VisionStreamType.VISION_STREAM_CABIN: "cabinEncodeData", VisionStreamType.VISION_STREAM_WIDE_ROAD: "wideRoadEncodeData", } @@ -146,8 +146,8 @@ if __name__ == "__main__": args = parser.parse_args() vision_streams = [ - VisionStreamType.VISION_STREAM_ROAD, - VisionStreamType.VISION_STREAM_DRIVER, + VisionStreamType.VISION_STREAM_NARROW_ROAD, + VisionStreamType.VISION_STREAM_CABIN, VisionStreamType.VISION_STREAM_WIDE_ROAD, ] diff --git a/openpilot/tools/clip/run.py b/openpilot/tools/clip/run.py index 6c847ca342..81145e2259 100755 --- a/openpilot/tools/clip/run.py +++ b/openpilot/tools/clip/run.py @@ -320,7 +320,7 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, wide_frame_queue = FrameQueue(ecamera_paths, start, end, fps=FRAMERATE) vipc = VisionIpcServer("camerad") - vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) + vipc.create_buffers(VisionStreamType.VISION_STREAM_NARROW_ROAD, 4, frame_queue.frame_w, frame_queue.frame_h) if wide_frame_queue: vipc.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 4, wide_frame_queue.frame_w, wide_frame_queue.frame_h) vipc.start_listener() @@ -339,7 +339,7 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, if frame_idx >= len(message_chunks): break _, frame_bytes = frame_queue.get() - vipc.send(VisionStreamType.VISION_STREAM_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) + vipc.send(VisionStreamType.VISION_STREAM_NARROW_ROAD, frame_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) if wide_frame_queue: _, wide_bytes = wide_frame_queue.get() vipc.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, wide_bytes, frame_idx, int(frame_idx * 5e7), int(frame_idx * 5e7)) diff --git a/openpilot/tools/jotpluggler/app.h b/openpilot/tools/jotpluggler/app.h index b7754f51b2..9a6777ee90 100644 --- a/openpilot/tools/jotpluggler/app.h +++ b/openpilot/tools/jotpluggler/app.h @@ -87,7 +87,7 @@ enum class PaneKind : uint8_t { enum class CameraViewKind : uint8_t { Road, - Driver, + Cabin, WideRoad, QRoad, }; @@ -322,7 +322,7 @@ struct RouteData { std::vector roots; std::vector can_messages; CameraFeedIndex road_camera; - CameraFeedIndex driver_camera; + CameraFeedIndex cabin_camera; CameraFeedIndex wide_road_camera; CameraFeedIndex qroad_camera; std::vector thumbnails; diff --git a/openpilot/tools/jotpluggler/common.h b/openpilot/tools/jotpluggler/common.h index 5c68b0ed22..7149bbe54d 100644 --- a/openpilot/tools/jotpluggler/common.h +++ b/openpilot/tools/jotpluggler/common.h @@ -23,7 +23,7 @@ struct SpecialItemSpec { inline constexpr std::array kCameraViewSpecs = {{ {CameraViewKind::Road, "Road Camera", "road", "road", "camera_road", &RouteData::road_camera}, - {CameraViewKind::Driver, "Driver Camera", "driver", "driver", "camera_driver", &RouteData::driver_camera}, + {CameraViewKind::Cabin, "Cabin Camera", "driver", "driver", "camera_driver", &RouteData::cabin_camera}, {CameraViewKind::WideRoad, "Wide Road Camera", "wide", "wide_road", "camera_wide_road", &RouteData::wide_road_camera}, {CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera}, }}; diff --git a/openpilot/tools/jotpluggler/layouts/camera-timings.json b/openpilot/tools/jotpluggler/layouts/camera-timings.json index 64decf15d3..643a7bc43a 100644 --- a/openpilot/tools/jotpluggler/layouts/camera-timings.json +++ b/openpilot/tools/jotpluggler/layouts/camera-timings.json @@ -1 +1 @@ -{"current_tab_index":0,"tabs":[{"name":"SOF / EOF (encodeIdx)","root":{"split":"vertical","sizes":[0.500885,0.499115],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/driverEncodeIdx/timestampSof","color":"#1f77b4","transform":"derivative","derivative_dt":1.0},{"name":"/roadEncodeIdx/timestampSof","color":"#d62728","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadEncodeIdx/timestampSof","color":"#1ac938","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}},{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/driverEncodeIdx/timestampEof","color":"#f14cc1","transform":"derivative","derivative_dt":1.0},{"name":"/roadEncodeIdx/timestampEof","color":"#9467bd","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadEncodeIdx/timestampEof","color":"#17becf","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}}]}},{"name":"model timings","root":{"split":"vertical","sizes":[0.5,0.5],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.016865,"bottom":0.015143},"curves":[{"name":"/modelV2/modelExecutionTime","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.1,"bottom":-0.1},"curves":[{"name":"/modelV2/frameDropPerc","color":"#f14cc1"}]}]}},{"name":"sensor info","root":{"split":"vertical","sizes":[1.0],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.1,"bottom":-0.1},"curves":[{"name":"/driverCameraState/sensor","color":"#bcbd22"},{"name":"/roadCameraState/sensor","color":"#1f77b4"},{"name":"/wideRoadCameraState/sensor","color":"#d62728"}]}]}},{"name":"SOF / EOF (cameraState)","root":{"split":"vertical","sizes":[0.500885,0.499115],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/driverCameraState/timestampSof","color":"#1f77b4","transform":"derivative","derivative_dt":1.0},{"name":"/roadCameraState/timestampSof","color":"#d62728","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadCameraState/timestampSof","color":"#1ac938","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}},{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/driverCameraState/timestampEof","color":"#ff7f0e","transform":"derivative","derivative_dt":1.0},{"name":"/roadCameraState/timestampEof","color":"#f14cc1","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadCameraState/timestampEof","color":"#9467bd","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}}]}}]} +{"current_tab_index":0,"tabs":[{"name":"SOF / EOF (encodeIdx)","root":{"split":"vertical","sizes":[0.500885,0.499115],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/cabinEncodeIdx/timestampSof","color":"#1f77b4","transform":"derivative","derivative_dt":1.0},{"name":"/narrowRoadEncodeIdx/timestampSof","color":"#d62728","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadEncodeIdx/timestampSof","color":"#1ac938","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}},{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/cabinEncodeIdx/timestampEof","color":"#f14cc1","transform":"derivative","derivative_dt":1.0},{"name":"/narrowRoadEncodeIdx/timestampEof","color":"#9467bd","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadEncodeIdx/timestampEof","color":"#17becf","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}}]}},{"name":"model timings","root":{"split":"vertical","sizes":[0.5,0.5],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.016865,"bottom":0.015143},"curves":[{"name":"/modelV2/modelExecutionTime","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.1,"bottom":-0.1},"curves":[{"name":"/modelV2/frameDropPerc","color":"#f14cc1"}]}]}},{"name":"sensor info","root":{"split":"vertical","sizes":[1.0],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":0.1,"bottom":-0.1},"curves":[{"name":"/cabinCameraState/sensor","color":"#bcbd22"},{"name":"/narrowRoadCameraState/sensor","color":"#1f77b4"},{"name":"/wideRoadCameraState/sensor","color":"#d62728"}]}]}},{"name":"SOF / EOF (cameraState)","root":{"split":"vertical","sizes":[0.500885,0.499115],"children":[{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/cabinCameraState/timestampSof","color":"#1f77b4","transform":"derivative","derivative_dt":1.0},{"name":"/narrowRoadCameraState/timestampSof","color":"#d62728","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadCameraState/timestampSof","color":"#1ac938","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}},{"title":"...","range":{"left":0.0,"right":630.006367,"top":65000000.0,"bottom":35000000.0},"curves":[{"name":"/cabinCameraState/timestampEof","color":"#ff7f0e","transform":"derivative","derivative_dt":1.0},{"name":"/narrowRoadCameraState/timestampEof","color":"#f14cc1","transform":"derivative","derivative_dt":1.0},{"name":"/wideRoadCameraState/timestampEof","color":"#9467bd","transform":"derivative","derivative_dt":1.0}],"y_limits":{"min":35000000.0,"max":65000000.0}}]}}]} diff --git a/openpilot/tools/jotpluggler/layouts/cameras-and-map.json b/openpilot/tools/jotpluggler/layouts/cameras-and-map.json index 68c590f7bc..7e120fcd5c 100644 --- a/openpilot/tools/jotpluggler/layouts/cameras-and-map.json +++ b/openpilot/tools/jotpluggler/layouts/cameras-and-map.json @@ -1 +1 @@ -{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"curves": [], "kind": "map", "title": "Map"}, {"camera_view": "road", "curves": [], "kind": "camera", "title": "Road Camera"}], "sizes": [0.5, 0.5], "split": "horizontal"}, {"children": [{"camera_view": "wide_road", "curves": [], "kind": "camera", "title": "Wide Road Camera"}, {"camera_view": "driver", "curves": [], "kind": "camera", "title": "Driver Camera"}], "sizes": [0.5, 0.5], "split": "horizontal"}], "sizes": [0.5, 0.5], "split": "vertical"}}]} +{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"curves": [], "kind": "map", "title": "Map"}, {"camera_view": "road", "curves": [], "kind": "camera", "title": "Road Camera"}], "sizes": [0.5, 0.5], "split": "horizontal"}, {"children": [{"camera_view": "wide_road", "curves": [], "kind": "camera", "title": "Wide Road Camera"}, {"camera_view": "driver", "curves": [], "kind": "camera", "title": "Cabin Camera"}], "sizes": [0.5, 0.5], "split": "horizontal"}], "sizes": [0.5, 0.5], "split": "vertical"}}]} diff --git a/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json b/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json index 07a5c2fd6e..3197f40ad8 100644 --- a/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json +++ b/openpilot/tools/jotpluggler/layouts/driver-monitoring-debug.json @@ -1 +1 @@ -{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"camera_view": "driver", "curves": [], "kind": "camera", "title": "Driver Camera"}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/alertLevel"}], "title": "..."}], "sizes": [0.5, 0.5], "split": "vertical"}, {"children": [{"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/activePolicy"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/faceDetected"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/eye"}, {"color": "#dc5234", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/phone"}, {"color": "#43a047", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/pose"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/awarenessPercent"}], "title": "..."}], "sizes": [0.25, 0.25, 0.25, 0.25], "split": "vertical"}], "sizes": [0.5, 0.5], "split": "horizontal"}}]} +{"current_tab_index": 0, "tabs": [{"name": "tab1", "root": {"children": [{"children": [{"camera_view": "driver", "curves": [], "kind": "camera", "title": "Cabin Camera"}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/alertLevel"}], "title": "..."}], "sizes": [0.5, 0.5], "split": "vertical"}, {"children": [{"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/activePolicy"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/faceDetected"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/eye"}, {"color": "#dc5234", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/phone"}, {"color": "#43a047", "name": "/driverMonitoringState/visionPolicyState/distractedTypes/pose"}], "title": "..."}, {"curves": [{"color": "#236bb4", "name": "/driverMonitoringState/visionPolicyState/awarenessPercent"}], "title": "..."}], "sizes": [0.25, 0.25, 0.25, 0.25], "split": "vertical"}], "sizes": [0.5, 0.5], "split": "horizontal"}}]} diff --git a/openpilot/tools/jotpluggler/runtime.cc b/openpilot/tools/jotpluggler/runtime.cc index a1e47c7e8e..d7d0fc79c4 100644 --- a/openpilot/tools/jotpluggler/runtime.cc +++ b/openpilot/tools/jotpluggler/runtime.cc @@ -39,11 +39,11 @@ const bool kLogCameraTimings = env_flag_enabled("JOTP_CAMERA_TIMINGS"); CameraType decoder_camera_type(CameraViewKind view) { switch (view) { - case CameraViewKind::Driver: return DriverCam; + case CameraViewKind::Cabin: return CabinCam; case CameraViewKind::WideRoad: return WideRoadCam; - case CameraViewKind::QRoad: return RoadCam; + case CameraViewKind::QRoad: return NarrowRoadCam; case CameraViewKind::Road: - default: return RoadCam; + default: return NarrowRoadCam; } } @@ -59,17 +59,17 @@ bool stream_batch_has_data(const StreamExtractBatch &batch) { bool should_subscribe_stream_service(const std::string &name) { static const std::array kSkippedServices = {{ - "roadEncodeIdx", - "driverEncodeIdx", + "narrowRoadEncodeIdx", + "cabinEncodeIdx", "wideRoadEncodeIdx", - "qRoadEncodeIdx", - "roadEncodeData", - "driverEncodeData", + "qNarrowRoadEncodeIdx", + "narrowRoadEncodeData", + "cabinEncodeData", "wideRoadEncodeData", - "qRoadEncodeData", + "qNarrowRoadEncodeData", "livestreamWideRoadEncodeIdx", - "livestreamRoadEncodeIdx", - "livestreamDriverEncodeIdx", + "livestreamNarrowRoadEncodeIdx", + "livestreamCabinEncodeIdx", "thumbnail", }}; if (name == "rawAudioData") return false; diff --git a/openpilot/tools/jotpluggler/sketch_layout.cc b/openpilot/tools/jotpluggler/sketch_layout.cc index ddb38fd473..5440d9dd8d 100644 --- a/openpilot/tools/jotpluggler/sketch_layout.cc +++ b/openpilot/tools/jotpluggler/sketch_layout.cc @@ -45,9 +45,9 @@ struct RouteSelection { struct SegmentLogs { std::string rlog; std::string qlog; - std::string fcamera; - std::string dcamera; - std::string ecamera; + std::string narrow_road; + std::string cabin; + std::string wide_road; std::string qcamera; }; @@ -295,11 +295,11 @@ void add_log_file_to_segments(std::map *segments, int segment_ } else if (name == "qlog.bz2" || name == "qlog.zst" || name == "qlog") { segment.qlog = file; } else if (name == "fcamera.hevc") { - segment.fcamera = file; + segment.narrow_road = file; } else if (name == "dcamera.hevc") { - segment.dcamera = file; + segment.cabin = file; } else if (name == "ecamera.hevc") { - segment.ecamera = file; + segment.wide_road = file; } else if (name == "qcamera.ts") { segment.qcamera = file; } @@ -1886,10 +1886,10 @@ RouteData load_route_data(const std::string &route_name, metadata.car_fingerprint, resolved_dbc); route_data.route_id = make_route_identifier(route, segments); - build_camera_index(segments, route_data, &SegmentLogs::fcamera, "roadEncodeIdx", &route_data.road_camera); - build_camera_index(segments, route_data, &SegmentLogs::dcamera, "driverEncodeIdx", &route_data.driver_camera); - build_camera_index(segments, route_data, &SegmentLogs::ecamera, "wideRoadEncodeIdx", &route_data.wide_road_camera); - build_camera_index(segments, route_data, &SegmentLogs::qcamera, "qRoadEncodeIdx", &route_data.qroad_camera); + build_camera_index(segments, route_data, &SegmentLogs::narrow_road, "narrowRoadEncodeIdx", &route_data.road_camera); + build_camera_index(segments, route_data, &SegmentLogs::cabin, "cabinEncodeIdx", &route_data.cabin_camera); + build_camera_index(segments, route_data, &SegmentLogs::wide_road, "wideRoadEncodeIdx", &route_data.wide_road_camera); + build_camera_index(segments, route_data, &SegmentLogs::qcamera, "qNarrowRoadEncodeIdx", &route_data.qroad_camera); stats.load_end = LoadStats::Clock::now(); stats.publish(RouteLoadStage::Finished, segments.size(), {}); stats.print_summary(route_data.series.size()); diff --git a/openpilot/tools/lib/log_time_series.py b/openpilot/tools/lib/log_time_series.py index 5e8aa4f73b..eba821bc84 100644 --- a/openpilot/tools/lib/log_time_series.py +++ b/openpilot/tools/lib/log_time_series.py @@ -80,5 +80,5 @@ if __name__ == "__main__": import sys from openpilot.tools.lib.logreader import LogReader m = msgs_to_time_series(LogReader(sys.argv[1])) - print(m['driverCameraState']['t']) - print(np.diff(m['driverCameraState']['timestampSof'])) + print(m['cabinCameraState']['t']) + print(np.diff(m['cabinCameraState']['timestampSof'])) diff --git a/openpilot/tools/plotjuggler/layouts/camera-timings.xml b/openpilot/tools/plotjuggler/layouts/camera-timings.xml index f91cbd3f53..2abff4d508 100644 --- a/openpilot/tools/plotjuggler/layouts/camera-timings.xml +++ b/openpilot/tools/plotjuggler/layouts/camera-timings.xml @@ -8,13 +8,13 @@ - - + + - - + + @@ -29,13 +29,13 @@ - - + + - - + + @@ -76,8 +76,8 @@ - - + + @@ -91,13 +91,13 @@ - - + + - - + + @@ -112,13 +112,13 @@ - - + + - - + + @@ -145,4 +145,3 @@ - diff --git a/openpilot/tools/replay/README.md b/openpilot/tools/replay/README.md index 108df08935..1d500eac96 100644 --- a/openpilot/tools/replay/README.md +++ b/openpilot/tools/replay/README.md @@ -68,8 +68,8 @@ Options: internal, openpilotci, comma_api, car_segments, testing_closet --data_dir local directory with routes --prefix set OPENPILOT_PREFIX - --dcam load driver camera - --ecam load wide road camera + --cabin load cabin camera + --wide-road load wide road camera --no-loop stop at the end of the route --no-cache turn off local cache --qcam load qcamera @@ -103,11 +103,11 @@ openpilot/tools/plotjuggler/juggle.py --stream watch all three cameras simultaneously from your comma three routes with watch3 -simply replay a route using the `--dcam` and `--ecam` flags: +simply replay a route using the `--cabin` and `--wide-road` flags: ```bash # start a replay -cd openpilot/tools/replay && ./replay --demo --dcam --ecam +cd openpilot/tools/replay && ./replay --demo --cabin --wide-road # then start watch3 cd openpilot/selfdrive/ui && ./watch3.py diff --git a/openpilot/tools/replay/camera.h b/openpilot/tools/replay/camera.h index 35fab5f7d3..81b3bb1a81 100644 --- a/openpilot/tools/replay/camera.h +++ b/openpilot/tools/replay/camera.h @@ -33,8 +33,8 @@ protected: VisionBuf *getFrame(Camera &cam, FrameReader *fr, int32_t segment_id, uint32_t frame_id); Camera cameras_[MAX_CAMERAS] = { - {.type = RoadCam, .stream_type = VISION_STREAM_ROAD}, - {.type = DriverCam, .stream_type = VISION_STREAM_DRIVER}, + {.type = NarrowRoadCam, .stream_type = VISION_STREAM_NARROW_ROAD}, + {.type = CabinCam, .stream_type = VISION_STREAM_CABIN}, {.type = WideRoadCam, .stream_type = VISION_STREAM_WIDE_ROAD}, }; std::atomic publishing_ = 0; diff --git a/openpilot/tools/replay/logreader.cc b/openpilot/tools/replay/logreader.cc index 0416413b4c..315aed9741 100644 --- a/openpilot/tools/replay/logreader.cc +++ b/openpilot/tools/replay/logreader.cc @@ -74,8 +74,8 @@ bool LogReader::load(const char *data, size_t size, std::atomic *abort, uint64_t mono_time = event.getLogMonoTime(); const Event &evt = events.emplace_back(which, mono_time, event_data); // Add encodeIdx packet again as a frame packet for the video stream - if (evt.which == cereal::Event::ROAD_ENCODE_IDX || - evt.which == cereal::Event::DRIVER_ENCODE_IDX || + if (evt.which == cereal::Event::NARROW_ROAD_ENCODE_IDX || + evt.which == cereal::Event::CABIN_ENCODE_IDX || evt.which == cereal::Event::WIDE_ROAD_ENCODE_IDX) { auto idx = capnp::AnyStruct::Reader(event).getPointerSection()[0].getAs(); if (idx.getType() == cereal::EncodeIndex::Type::FULL_H_E_V_C) { diff --git a/openpilot/tools/replay/logreader.h b/openpilot/tools/replay/logreader.h index 63f7468401..52e82c46ad 100644 --- a/openpilot/tools/replay/logreader.h +++ b/openpilot/tools/replay/logreader.h @@ -8,7 +8,7 @@ #include "openpilot/cereal/gen/cpp/log.capnp.h" #include "tools/replay/util.h" -const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam}; +const CameraType ALL_CAMERAS[] = {NarrowRoadCam, CabinCam, WideRoadCam}; const int MAX_CAMERAS = std::size(ALL_CAMERAS); class Event { diff --git a/openpilot/tools/replay/main.cc b/openpilot/tools/replay/main.cc index bdb9cf4f35..50233189a1 100644 --- a/openpilot/tools/replay/main.cc +++ b/openpilot/tools/replay/main.cc @@ -26,8 +26,8 @@ Options: internal, openpilotci, comma_api, car_segments, testing_closet -d, --data_dir Local directory with routes -p, --prefix Set OPENPILOT_PREFIX - --dcam Load driver camera - --ecam Load wide road camera + --cabin Load cabin camera (alias: --dcam) + --wide-road Load wide road camera (alias: --ecam) --no-loop Stop at the end of the route --no-cache Turn off local cache --qcam Load qcamera @@ -62,8 +62,10 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) { {"auto", no_argument, nullptr, 0}, {"data_dir", required_argument, nullptr, 'd'}, {"prefix", required_argument, nullptr, 'p'}, - {"dcam", no_argument, nullptr, 0}, - {"ecam", no_argument, nullptr, 0}, + {"cabin", no_argument, nullptr, 0}, + {"dcam", no_argument, nullptr, 0}, // deprecated alias + {"wide-road", no_argument, nullptr, 0}, + {"ecam", no_argument, nullptr, 0}, // deprecated alias {"no-loop", no_argument, nullptr, 0}, {"no-cache", no_argument, nullptr, 0}, {"qcam", no_argument, nullptr, 0}, @@ -76,8 +78,10 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) { }; const std::map flag_map = { - {"dcam", REPLAY_FLAG_DCAM}, - {"ecam", REPLAY_FLAG_ECAM}, + {"cabin", REPLAY_FLAG_CABIN_CAMERA}, + {"dcam", REPLAY_FLAG_CABIN_CAMERA}, // deprecated alias + {"wide-road", REPLAY_FLAG_WIDE_ROAD}, + {"ecam", REPLAY_FLAG_WIDE_ROAD}, // deprecated alias {"no-loop", REPLAY_FLAG_NO_LOOP}, {"no-cache", REPLAY_FLAG_NO_FILE_CACHE}, {"qcam", REPLAY_FLAG_QCAMERA}, diff --git a/openpilot/tools/replay/replay.cc b/openpilot/tools/replay/replay.cc index 75aecfd0c2..6449fc30ea 100644 --- a/openpilot/tools/replay/replay.cc +++ b/openpilot/tools/replay/replay.cc @@ -251,13 +251,13 @@ void Replay::publishMessage(const Event *e) { void Replay::publishFrame(const Event *e) { CameraType cam; switch (e->which) { - case cereal::Event::ROAD_ENCODE_IDX: cam = RoadCam; break; - case cereal::Event::DRIVER_ENCODE_IDX: cam = DriverCam; break; + case cereal::Event::NARROW_ROAD_ENCODE_IDX: cam = NarrowRoadCam; break; + case cereal::Event::CABIN_ENCODE_IDX: cam = CabinCam; break; case cereal::Event::WIDE_ROAD_ENCODE_IDX: cam = WideRoadCam; break; default: return; // Invalid event type } - if ((cam == DriverCam && !hasFlag(REPLAY_FLAG_DCAM)) || (cam == WideRoadCam && !hasFlag(REPLAY_FLAG_ECAM))) + if ((cam == CabinCam && !hasFlag(REPLAY_FLAG_CABIN_CAMERA)) || (cam == WideRoadCam && !hasFlag(REPLAY_FLAG_WIDE_ROAD))) return; // Camera isdisabled auto seg_it = event_data_->segments.find(e->eidx_segnum); diff --git a/openpilot/tools/replay/replay.h b/openpilot/tools/replay/replay.h index ce6d75bd6d..7cd5fbecd0 100644 --- a/openpilot/tools/replay/replay.h +++ b/openpilot/tools/replay/replay.h @@ -16,8 +16,8 @@ enum REPLAY_FLAGS { REPLAY_FLAG_NONE = 0x0000, - REPLAY_FLAG_DCAM = 0x0002, - REPLAY_FLAG_ECAM = 0x0004, + REPLAY_FLAG_CABIN_CAMERA = 0x0002, + REPLAY_FLAG_WIDE_ROAD = 0x0004, REPLAY_FLAG_NO_LOOP = 0x0010, REPLAY_FLAG_NO_FILE_CACHE = 0x0020, REPLAY_FLAG_QCAMERA = 0x0040, diff --git a/openpilot/tools/replay/route.cc b/openpilot/tools/replay/route.cc index 326d28d726..f38c2b1971 100644 --- a/openpilot/tools/replay/route.cc +++ b/openpilot/tools/replay/route.cc @@ -180,9 +180,9 @@ void Route::addFileToSegment(int n, const std::string &file) { } else if (name == "qlog.bz2" || name == "qlog.zst" || name == "qlog") { segments_[n].qlog = file; } else if (name == "fcamera.hevc") { - segments_[n].road_cam = file; + segments_[n].narrow_road_cam = file; } else if (name == "dcamera.hevc") { - segments_[n].driver_cam = file; + segments_[n].cabin_cam = file; } else if (name == "ecamera.hevc") { segments_[n].wide_road_cam = file; } else if (name == "qcamera.ts") { @@ -195,11 +195,11 @@ void Route::addFileToSegment(int n, const std::string &file) { Segment::Segment(int n, const SegmentFile &files, uint32_t flags, const std::vector &filters, std::function callback) : seg_num(n), flags(flags), filters_(filters), on_load_finished_(callback) { - // [RoadCam, DriverCam, WideRoadCam, log]. fallback to qcamera/qlog + // [NarrowRoadCam, CabinCam, WideRoadCam, log]. fallback to qcamera/qlog const std::array file_list = { - (flags & REPLAY_FLAG_QCAMERA) || files.road_cam.empty() ? files.qcamera : files.road_cam, - flags & REPLAY_FLAG_DCAM ? files.driver_cam : "", - flags & REPLAY_FLAG_ECAM ? files.wide_road_cam : "", + (flags & REPLAY_FLAG_QCAMERA) || files.narrow_road_cam.empty() ? files.qcamera : files.narrow_road_cam, + flags & REPLAY_FLAG_CABIN_CAMERA ? files.cabin_cam : "", + flags & REPLAY_FLAG_WIDE_ROAD ? files.wide_road_cam : "", files.rlog.empty() ? files.qlog : files.rlog, }; for (int i = 0; i < file_list.size(); ++i) { diff --git a/openpilot/tools/replay/route.h b/openpilot/tools/replay/route.h index 50f3eba854..7a55997969 100644 --- a/openpilot/tools/replay/route.h +++ b/openpilot/tools/replay/route.h @@ -33,8 +33,8 @@ struct RouteIdentifier { struct SegmentFile { std::string rlog; std::string qlog; - std::string road_cam; - std::string driver_cam; + std::string narrow_road_cam; + std::string cabin_cam; std::string wide_road_cam; std::string qcamera; }; diff --git a/openpilot/tools/replay/ui.py b/openpilot/tools/replay/ui.py index 2c253a1364..16c050f8a6 100755 --- a/openpilot/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -57,7 +57,7 @@ def ui_thread(addr): font_path = os.path.join(BASEDIR, "openpilot/selfdrive/assets/fonts/JetBrainsMono-Medium.ttf") font = rl.load_font_ex(font_path, 32, None, 0) - camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_NARROW_ROAD) # Overlay texture for model/lane line drawing overlay_img = np.zeros((480, 640, 4), dtype='uint8') @@ -82,7 +82,7 @@ def ui_thread(addr): 'liveTracks', 'modelV2', 'liveParameters', - 'roadCameraState', + 'narrowRoadCameraState', ], addr=addr, ) @@ -152,13 +152,13 @@ def ui_thread(addr): sm.update(0) - camera = DEVICE_CAMERAS[("tici", str(sm['roadCameraState'].sensor))] - calib_scale = camera.fcam.width / 640.0 + camera = DEVICE_CAMERAS[("tici", str(sm['narrowRoadCameraState'].sensor))] + calib_scale = camera.narrow_road.width / 640.0 if camera_view.frame: num_px = camera_view.frame.width * camera_view.frame.height - intrinsic_matrix = camera.fcam.intrinsics + intrinsic_matrix = camera.narrow_road.intrinsics w = sm['controlsState'].lateralControlState.which() if w == 'lqrStateDEPRECATED': diff --git a/openpilot/tools/replay/util.h b/openpilot/tools/replay/util.h index 45d6f7cd0b..5cffc11706 100644 --- a/openpilot/tools/replay/util.h +++ b/openpilot/tools/replay/util.h @@ -10,8 +10,8 @@ #include "openpilot/cereal/messaging/messaging.h" enum CameraType { - RoadCam = 0, - DriverCam, + NarrowRoadCam = 0, + CabinCam, WideRoadCam }; diff --git a/openpilot/tools/sim/lib/camerad.py b/openpilot/tools/sim/lib/camerad.py index d4825eada7..206e4fdf90 100644 --- a/openpilot/tools/sim/lib/camerad.py +++ b/openpilot/tools/sim/lib/camerad.py @@ -38,20 +38,20 @@ def rgb_to_nv12(rgb): class Camerad: """Simulates the camerad daemon""" def __init__(self, dual_camera): - self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState']) + self.pm = messaging.PubMaster(['narrowRoadCameraState', 'wideRoadCameraState']) self.frame_road_id = 0 self.frame_wide_id = 0 self.vipc_server = VisionIpcServer("camerad") - self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H) + self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_NARROW_ROAD, 5, W, H) if dual_camera: self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 5, W, H) self.vipc_server.start_listener() def cam_send_yuv_road(self, yuv): - self._send_yuv(yuv, self.frame_road_id, 'roadCameraState', VisionStreamType.VISION_STREAM_ROAD) + self._send_yuv(yuv, self.frame_road_id, 'narrowRoadCameraState', VisionStreamType.VISION_STREAM_NARROW_ROAD) self.frame_road_id += 1 def cam_send_yuv_wide_road(self, yuv): diff --git a/tools/scripts/cycle_alerts.py b/tools/scripts/cycle_alerts.py index 0bd42bcd3f..cf4a1d8999 100755 --- a/tools/scripts/cycle_alerts.py +++ b/tools/scripts/cycle_alerts.py @@ -50,11 +50,11 @@ def cycle_alerts(duration=200, is_metric=False): (EventName.cameraFrameRate, ET.PERMANENT), ] - cameras = ['roadCameraState', 'wideRoadCameraState', 'driverCameraState'] + cameras = ['narrowRoadCameraState', 'wideRoadCameraState', 'cabinCameraState'] CS = car.CarState.new_message() CP = CarInterface.get_non_essential_params("HONDA_CIVIC") - sm = messaging.SubMaster(['deviceState', 'pandaStates', 'roadCameraState', 'modelV2', 'liveCalibration', + sm = messaging.SubMaster(['deviceState', 'pandaStates', 'narrowRoadCameraState', 'modelV2', 'liveCalibration', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'managerState'] + cameras) From f531952be32993cf46ec1a57b65178f2929d608c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 8 Aug 2026 19:49:06 -0400 Subject: [PATCH 198/325] ci: sunnypilot process replay (#1900) * ci: sunnypilot process replay * ours * temp * Revert "temp" This reverts commit d198cbb32739343f6b8360761f9f85c6ce2d4265. * fixes, hopefully * bump --- .github/workflows/tests.yaml | 9 ++++----- opendbc_repo | 2 +- .../selfdrive/test/process_replay/test_processes.py | 2 +- .../selfdrive/controls/lib/latcontrol_torque_v0.py | 2 +- openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py | 5 +++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b5f941fc76..621aa123c5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -132,7 +132,6 @@ jobs: process_replay: name: process replay - if: false # disable process_replay for forks runs-on: ${{ (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || @@ -169,14 +168,14 @@ jobs: name: diff_report_${{ github.event.number }} path: openpilot/selfdrive/test/process_replay/diff_report.txt - name: Checkout ci-artifacts - if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' uses: actions/checkout@v7 with: - repository: commaai/ci-artifacts + repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/ci-artifacts - name: Prepare refs - if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' working-directory: ${{ github.workspace }}/ci-artifacts run: | git config user.name "GitHub Actions Bot" @@ -188,7 +187,7 @@ jobs: git add . git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit" - name: Push refs - if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master' + if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 with: timeout_minutes: 2 diff --git a/opendbc_repo b/opendbc_repo index 063414f63f..8b9fd4a653 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 063414f63f14f6fe8a662bac6ca372019ccda418 +Subproject commit 8b9fd4a653ab9d9e4f455f01eb61ef22dbc96988 diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index d9d827add5..1627ab0658 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -66,7 +66,7 @@ segments = [ # dashcamOnly makes don't need to be tested until a full port is done excluded_interfaces = ["mock", "body", "psa"] -BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/" +BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/" REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"} diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py index 4d9e4492f9..6ddfaea231 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_v0.py @@ -82,7 +82,7 @@ class LatControlTorque(LatControl): future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 self.lat_accel_request_buffer.append(future_desired_lateral_accel) gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation - desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay + desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / max(lat_delay, self.dt) measurement = measured_curvature * CS.vEgo ** 2 measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index e66072f86f..740e7092ea 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -36,12 +36,13 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): super().__init__(lac_torque, CP, CP_SP, CI) self.params = Params() self.enabled = self.params.get_bool("NeuralNetworkLateralControl") - self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH + model_path = CP_SP.neuralNetworkLateralControl.model.path + self.has_nn_model = model_path not in (MOCK_MODEL_PATH, '') # NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data # of lat accel and roll # Past value is computed using previous desired lat accel and observed roll - self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path) + self.model = NNTorqueModel(model_path) if self.has_nn_model else None self.pitch = FirstOrderFilter(0.0, 0.5, 0.01) self.pitch_last = 0.0 From f8d50e062d00e1cb9bcc6ecf5a06f63ac5599eaf Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 8 Aug 2026 22:55:32 -0400 Subject: [PATCH 199/325] updated: add fallback mechanism for `agnos.json` path resolution (#38593) * updated: add fallback mechanism for agnos.json path resolution * nvm fallback seems overkill, red diff * add symlink for tici/agnos.json --------- Co-authored-by: stefpi <19478336+stefpi@users.noreply.github.com> --- openpilot/system/hardware/tici/agnos.json | 1 + system/hardware/tici/agnos.json | 1 + 2 files changed, 2 insertions(+) create mode 120000 openpilot/system/hardware/tici/agnos.json create mode 120000 system/hardware/tici/agnos.json diff --git a/openpilot/system/hardware/tici/agnos.json b/openpilot/system/hardware/tici/agnos.json new file mode 120000 index 0000000000..87cb197052 --- /dev/null +++ b/openpilot/system/hardware/tici/agnos.json @@ -0,0 +1 @@ +../comma/agnos.json \ No newline at end of file diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json new file mode 120000 index 0000000000..7bc8df1b8c --- /dev/null +++ b/system/hardware/tici/agnos.json @@ -0,0 +1 @@ +../../../openpilot/system/hardware/comma/agnos.json \ No newline at end of file From 9e32dee2814fd2c61c62e3988f0dc16f356b8b6f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 9 Aug 2026 03:47:59 -0400 Subject: [PATCH 200/325] ci: fix process replay with missing sunnypilot service ignores (#1902) --- openpilot/selfdrive/controls/plannerd.py | 2 +- openpilot/selfdrive/selfdrived/selfdrived.py | 2 +- openpilot/selfdrive/test/process_replay/process_replay.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index d80b69ad19..0af341b121 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -23,7 +23,7 @@ def main(): cloudlog.info("plannerd got CarParamsSP") gps_location_service = get_gps_location_service(params) - ignore_services = ["liveMapDataSP", gps_location_service] + ignore_services = ["liveMapDataSP", "carStateSP", "selfdriveStateSP", gps_location_service] ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP, CP_SP) diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index cfad1433db..2bc0574e81 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -93,7 +93,7 @@ class SelfdriveD(CruiseHelper): # TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches self.car_state_sock = messaging.sub_sock('carState', timeout=20) - ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP'] + ignore = self.sensor_packets + self.gps_packets + ['alertDebug', 'lateralManeuverPlan'] + ['modelDataV2SP', 'longitudinalPlanSP'] if SIMULATION: ignore += ['driverCameraState', 'managerState'] if REPLAY: diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index fc3463b376..4834cc44e6 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -500,7 +500,7 @@ CONFIGS = [ ), ProcessConfig( proc_name="dmonitoringd", - pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], + pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState", "carControl"], subs=["driverMonitoringState"], ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("driverStateV2"), @@ -511,7 +511,7 @@ CONFIGS = [ pubs=[ "cameraOdometry", "accelerometer", "gyroscope", "liveCalibration", "carState" ], - subs=["liveLocationKalman", "livePose"], + subs=["livePose"], ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), tolerance=NUMPY_TOLERANCE, From fc4699a74783a2f80094f4b890a106c713f7fb96 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 9 Aug 2026 15:19:35 -0400 Subject: [PATCH 201/325] controls: abstract update_output_torque to ExtBase (#1903) --- .../selfdrive/controls/lib/latcontrol_torque_ext_base.py | 7 +++++++ openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index df773889a9..31ac615db8 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -132,3 +132,10 @@ class LatControlTorqueExtBase: self.lat_accel_friction_factor = 1.0 self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk self.lateral_jerk_measurement = self.lat_jerk_friction_factor * self.actual_lateral_jerk + + def update_output_torque(self, CS): + freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + self._output_torque = self._pid.update(self._pid_log.error, + feedforward=self._ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 740e7092ea..73c5b526b2 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -85,13 +85,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) - def update_output_torque(self, CS): - freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 - self._output_torque = self._pid.update(self._pid_log.error, - feedforward=self._ff, - speed=CS.vEgo, - freeze_integrator=freeze_integrator) - def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: if not self._nnlc_enabled: return From 91a53aa1610891070cf2877c84f00c2c2965ec31 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 9 Aug 2026 15:56:33 -0400 Subject: [PATCH 202/325] Controls: Lateral Jerk Torque Controller (#693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init * more init * keep it alive * fixes * more fixes * more fix * new submodule for nn data * bump submodule * update path to submodule * spacing??? * update submodule path * update submodule path * bump * dump * bump * introduce params * Add Neural Network Lateral Control toggle to developer panel This introduces a new toggle for enabling Neural Network Lateral Control (NNLC), providing detailed descriptions of its functionality and compatibility. It includes UI integration, car compatibility checks, and feedback links for unsupported vehicles. * decouple even more * static * codespell * remove debug * in structs * fix import * convert to capnp * fixes * debug * only initialize if NNLC is enabled or allow to enable * oops * fix initialization * only allow engage if nnlc is off * fix toggle param * fix tests * lint * fix more test * capnp test * try this out * validate if it's not None * make it 33 to match * align * share the same friction input calculation * return stock values if not enabled * unused * split base and child * space * rename * NeuralNetworkFeedForwardModel * less * just use file name * try this * more explicit * rename * move it * child class for additional controllers * rename * time to split out custom lateral acceleration * move around * space * fix * TODO-SP * TODO-SP * update regardless, it's an extension now * update name and expose toggle * ui: sunnypilot Panel -> Steering Panel * Update selfdrive/ui/sunnypilot/qt/offroad/settings/lateral_panel.h * merge * move to steering panel * no need for this * live params in a thread * no live for now * new structs * more ui * more flexible * more ui * no longer needed * another ui * cereal changes * bump opendbc * simplify checks * all in one place * just Enhanced Lat Accel only * no submodule for this * Enhanced Lateral Acceleration: fix bugs, restore NNLC, add UI/schema Fix 4 bugs in latcontrol_torque_lat_accel.py: missing CI param, wrong method name (torque_from_lateral_accel → torque_from_lateral_accel_in_torque_space), self.enabled collision with NNLC, missing output torque recomputation. Restore NNLC + ExtOverride inheritance chain with Enhanced slotted between ExtBase and NNLC. Add mutual exclusion constraints, Python UI toggle, sunnylink schema entries, and controller init/update tests. * fix lint: remove unused default params list, add ty ignore, fix test fixture * use the og name * rename * need gates * TODOs * wrong * gate them all on init --------- Co-authored-by: DevTekVE --- openpilot/common/params_keys.h | 1 + .../sunnypilot/layouts/settings/steering.py | 3 +- .../steering_sub_layouts/torque_settings.py | 10 ++ openpilot/selfdrive/ui/sunnypilot/ui_state.py | 6 + .../sunnypilot/selfdrive/car/interfaces.py | 6 + .../controls/lib/latcontrol_torque_ext.py | 1 + .../lib/latcontrol_torque_jerk_aware.py | 45 ++++++++ .../selfdrive/controls/lib/nnlc/nnlc.py | 6 +- .../lib/tests/test_latcontrol_torque_ext.py | 108 ++++++++++++++++++ .../sunnypilot/sunnylink/settings_ui.json | 31 +++++ .../settings_ui_src/pages/models.yaml | 3 + .../settings_ui_src/pages/steering.yaml | 15 +++ .../sunnylink/tests/test_settings_schema.py | 10 +- 13 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 279d36abfd..f0921b6723 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -273,6 +273,7 @@ inline static std::unordered_map keys = { // Torque lateral control custom params {"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}}, {"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}}, + {"LateralJerkTorqueController", {PERSISTENT | BACKUP, BOOL, "0"}}, {"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}}, {"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}}, {"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py index 15cb6a15e0..28d9236361 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -139,7 +139,8 @@ class SteeringLayout(Widget): self._nnlc_toggle.action_item.set_state(False) enforce_torque_enabled = False nnlc_enabled = False - self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled) + jerk_aware_enabled = ui_state.params.get_bool("LateralJerkTorqueController") + self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled and not jerk_aware_enabled) self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled) self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state()) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py index f3c4419e45..6dae8308cd 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/torque_settings.py @@ -40,6 +40,13 @@ class TorqueSettingsLayout(Widget): self.cached_torque_versions = json.load(f) def _initialize_items(self): + self._jerk_aware_toggle = toggle_item_sp( + param="LateralJerkTorqueController", + title=lambda: tr("Lateral Jerk Torque Controller"), + description=lambda: tr("Looks ahead at planned steering to reduce sudden corrections, so the wheel moves " + + "more smoothly through turns. Works with Self-Tune and custom tuning. " + + "Thanks to @twilsonco for the implementation."), + ) self._torque_control_versions = ListItemSP( title=tr("Torque Control Tune Version"), description="Select the version of Torque Control Tune to use.", @@ -95,6 +102,7 @@ class TorqueSettingsLayout(Widget): ) items = [ + self._jerk_aware_toggle, self._torque_control_versions, self._self_tune_toggle, self._relaxed_tune_toggle, @@ -107,6 +115,8 @@ class TorqueSettingsLayout(Widget): def _update_state(self): super()._update_state() + nnlc_enabled = ui_state.params.get_bool("NeuralNetworkLateralControl") + self._jerk_aware_toggle.action_item.set_enabled(ui_state.is_offroad() and not nnlc_enabled) if not ui_state.params.get_bool("LiveTorqueParamsToggle"): ui_state.params.remove("LiveTorqueParamsRelaxedToggle") self._relaxed_tune_toggle.action_item.set_state(False) diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index c2729bbd91..4828f37103 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -184,10 +184,15 @@ class UIStateSP: self.params.put_bool("EnforceTorqueControl", False, block=True) self.params.put_bool("NeuralNetworkLateralControl", False, block=True) + if self.params.get_bool("LateralJerkTorqueController") and self.params.get_bool("NeuralNetworkLateralControl"): + self.params.put_bool("LateralJerkTorqueController", False, block=True) + self.params.put_bool("NeuralNetworkLateralControl", False, block=True) + # Angle steering: no torque-based lateral controls if CP.steerControlType == car.CarParams.SteerControlType.angle: self.params.remove("EnforceTorqueControl") self.params.remove("NeuralNetworkLateralControl") + self.params.remove("LateralJerkTorqueController") # Alpha longitudinal: clear if not available if not CP.alphaLongitudinalAvailable: @@ -200,6 +205,7 @@ class UIStateSP: # No CarParams: clear all car-dependent params as safety default self.params.remove("EnforceTorqueControl") self.params.remove("NeuralNetworkLateralControl") + self.params.remove("LateralJerkTorqueController") self.params.remove("AlphaLongitudinalEnabled") # No longitudinal control: no experimental mode or DEC diff --git a/openpilot/sunnypilot/selfdrive/car/interfaces.py b/openpilot/sunnypilot/selfdrive/car/interfaces.py index ed5b71d4b1..ecabc53b57 100644 --- a/openpilot/sunnypilot/selfdrive/car/interfaces.py +++ b/openpilot/sunnypilot/selfdrive/car/interfaces.py @@ -73,10 +73,16 @@ def _cleanup_unsupported_params(CP: structs.CarParams, CP_SP: structs.CarParamsS if params is None: params = Params() + if params.get_bool("LateralJerkTorqueController") and params.get_bool("NeuralNetworkLateralControl"): + cloudlog.warning("LateralJerkTorqueController and NeuralNetworkLateralControl both enabled, disabling both") + params.put_bool("LateralJerkTorqueController", False, block=True) + params.put_bool("NeuralNetworkLateralControl", False, block=True) + if CP.steerControlType == structs.CarParams.SteerControlType.angle: cloudlog.warning("SteerControlType is angle, cleaning up params") params.remove("NeuralNetworkLateralControl") params.remove("EnforceTorqueControl") + params.remove("LateralJerkTorqueController") if not CP_SP.intelligentCruiseButtonManagementAvailable or CP.openpilotLongitudinalControl: cloudlog.warning("ICBM not available or openpilot Longitudinal Control enabled, cleaning up params") diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index 39525b3b8e..50add19cd2 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -33,6 +33,7 @@ class LatControlTorqueExt(NeuralNetworkLateralControl, LatControlTorqueExtOverri self._output_torque = output_torque self.update_calculations(CS, VM, desired_lateral_accel) + self.update_jerk_aware_torque_control(CS, roll_compensation, gravity_adjusted_lateral_accel) self.update_neural_network_feedforward(CS, params, calibrated_pose) return self._pid_log, self._output_torque diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py new file mode 100644 index 0000000000..8d780ed4cc --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/latcontrol_torque_jerk_aware.py @@ -0,0 +1,45 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from opendbc.car.lateral import FRICTION_THRESHOLD +from opendbc.sunnypilot.car.interfaces import LatControlInputs +from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space +from openpilot.common.params import Params + +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase + + +class LatControlTorqueJerkAware(LatControlTorqueExtBase): + def __init__(self, lac_torque, CP, CP_SP, CI): + super().__init__(lac_torque, CP, CP_SP, CI) + self.params = Params() + self._jerk_aware_enabled = self.params.get_bool("LateralJerkTorqueController") + + def update_limits(self): + if not self._jerk_aware_enabled: + return + self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max) + + def update_jerk_aware_torque_control(self, CS, roll_compensation, gravity_adjusted_lateral_accel): + if not self._jerk_aware_enabled: + return + + torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space( + LatControlInputs(self._setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False + ) + torque_from_measurement = self.torque_from_lateral_accel_in_torque_space( + LatControlInputs(self._measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=False + ) + + self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) # ty: ignore[invalid-assignment] + self._ff = self.torque_from_lateral_accel_in_torque_space( + LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True + ) + + friction_input = self.update_friction_input(self._desired_lateral_accel, self._actual_lateral_accel) + self._ff += get_friction_in_torque_space(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + + self.update_output_torque(CS) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 73c5b526b2..9684c86688 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -14,7 +14,8 @@ from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_t from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import LatControlTorqueExtBase, sign +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext_base import sign +from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_jerk_aware import LatControlTorqueJerkAware from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.helpers import MOCK_MODEL_PATH from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.model import NNTorqueModel @@ -31,7 +32,7 @@ def roll_pitch_adjust(roll, pitch): return roll * math.cos(pitch) -class NeuralNetworkLateralControl(LatControlTorqueExtBase): +class NeuralNetworkLateralControl(LatControlTorqueJerkAware): def __init__(self, lac_torque, CP, CP_SP, CI): super().__init__(lac_torque, CP, CP_SP, CI) self.params = Params() @@ -65,6 +66,7 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): return self.enabled and self.model_valid and self.has_nn_model def update_limits(self): + super().update_limits() if not self._nnlc_enabled: return diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py new file mode 100644 index 0000000000..2b47977514 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py @@ -0,0 +1,108 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +from openpilot.cereal import log, messaging +from opendbc.car.structs import car +from opendbc.car.car_helpers import interfaces +from opendbc.car.honda.values import CAR as HONDA +from opendbc.car.vehicle_model import VehicleModel +from openpilot.common.params import Params +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.car.helpers import convert_to_capnp +from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque +from openpilot.selfdrive.locationd.helpers import Pose +from openpilot.common.mock.generators import generate_livePose +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces +from openpilot.selfdrive.modeld.constants import ModelConstants + + +def _make_controller(enhanced=False, nnlc=False): + params = Params() + params.put_bool("EnforceTorqueControl", True, block=True) + params.put_bool("LateralJerkTorqueController", enhanced, block=True) + params.put_bool("NeuralNetworkLateralControl", nnlc, block=True) + + car_name = HONDA.HONDA_CIVIC + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CI, params) + CP_SP = convert_to_capnp(CP_SP) + VM = VehicleModel(CP) + controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI, DT_CTRL) + return controller, VM, CP + + +def _make_model_v2(): + model = messaging.new_message('modelV2') + position = log.XYZTData.new_message() + position.x = [float(x) for x in 30.0 * np.array(ModelConstants.T_IDXS)] + model.modelV2.position = position + orientation = log.XYZTData.new_message() + orientation.x = [0.0 for _ in ModelConstants.T_IDXS] + orientation.y = [0.0 for _ in ModelConstants.T_IDXS] + model.modelV2.orientation = orientation + velocity = log.XYZTData.new_message() + velocity.x = [30.0 for _ in ModelConstants.T_IDXS] + model.modelV2.velocity = velocity + acceleration = log.XYZTData.new_message() + acceleration.x = [0.0 for _ in ModelConstants.T_IDXS] + acceleration.y = [0.0 for _ in ModelConstants.T_IDXS] + model.modelV2.acceleration = acceleration + return model + + +def _run_update(controller, VM): + CS = car.CarState.new_message() + CS.vEgo = 30 + CS.steeringPressed = False + lp = generate_livePose() + pose = Pose.from_live_pose(lp.livePose) + params = log.LiveParametersData.new_message() + model_v2 = _make_model_v2().modelV2 + controller.extension.update_model_v2(model_v2) + controller.extension.update_lateral_lag(0.2) + return controller.update(True, CS, VM, params, False, 0.5, pose, False, 0.2) + + +class TestLatControlTorqueExt: + def test_init_enhanced_only(self): + controller, VM, _ = _make_controller(enhanced=True, nnlc=False) + assert controller.extension._jerk_aware_enabled + assert not controller.extension.enabled # NNLC disabled + + def test_init_nnlc_only(self): + controller, VM, _ = _make_controller(enhanced=False, nnlc=True) + assert not controller.extension._jerk_aware_enabled + assert controller.extension.enabled + + def test_init_neither(self): + controller, VM, _ = _make_controller(enhanced=False, nnlc=False) + assert not controller.extension._jerk_aware_enabled + assert not controller.extension.enabled + + def test_init_both_no_crash(self): + controller, VM, _ = _make_controller(enhanced=True, nnlc=True) + assert not controller.extension._jerk_aware_enabled + assert not controller.extension.enabled + + def test_update_enhanced_only(self): + controller, VM, _ = _make_controller(enhanced=True, nnlc=False) + output_torque, _, pid_log = _run_update(controller, VM) + assert pid_log.active + + def test_update_neither(self): + controller, VM, _ = _make_controller(enhanced=False, nnlc=False) + output_torque, _, pid_log = _run_update(controller, VM) + assert pid_log.active + + def test_update_both_no_crash(self): + controller, VM, _ = _make_controller(enhanced=True, nnlc=True) + output_torque, _, pid_log = _run_update(controller, VM) + assert pid_log.active diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index cd5f0b118f..1e6422ac84 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -323,6 +323,32 @@ "equals": true }, "items": [ + { + "key": "LateralJerkTorqueController", + "widget": "toggle", + "title": "Lateral Jerk Torque Controller", + "description": "Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation.", + "visibility": [ + { + "type": "not", + "condition": { + "type": "capability", + "field": "steer_control_type", + "equals": "angle" + } + } + ], + "enablement": [ + { + "type": "offroad_only" + }, + { + "type": "param", + "key": "NeuralNetworkLateralControl", + "equals": false + } + ] + }, { "key": "LiveTorqueParamsToggle", "widget": "toggle", @@ -2037,6 +2063,11 @@ "type": "param", "key": "EnforceTorqueControl", "equals": false + }, + { + "type": "param", + "key": "LateralJerkTorqueController", + "equals": false } ] } diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml index 4ae1fca88b..bcb8b895b9 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/models.yaml @@ -73,6 +73,9 @@ sections: - type: param key: EnforceTorqueControl equals: false + - type: param + key: LateralJerkTorqueController + equals: false - id: camera title: Camera description: Camera position and calibration diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml index 697c5f4f21..a09796ab7a 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml @@ -127,6 +127,21 @@ sections: key: EnforceTorqueControl equals: true items: + - key: LateralJerkTorqueController + widget: toggle + title: Lateral Jerk Torque Controller + description: Looks ahead at planned steering to reduce sudden corrections, so the wheel moves more smoothly through turns. Works with Self-Tune and custom tuning. Thanks to @twilsonco for the implementation. + visibility: + - type: not + condition: + type: capability + field: steer_control_type + equals: angle + enablement: + - $ref: '#/macros/offroad' + - type: param + key: NeuralNetworkLateralControl + equals: false - key: LiveTorqueParamsToggle widget: toggle title: Self-Tune diff --git a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py index 61cc0131cf..579d72b60b 100644 --- a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py +++ b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py @@ -257,20 +257,26 @@ class TestKnownPanels: assert "mads_settings" in sub_ids def test_mutual_exclusion_torque_nnlc(self, schema): - """EnforceTorqueControl and NNLC must reference each other in enablement.""" - torque = nnlc = None + """EnforceTorqueControl, EnhancedLatAccel, and NNLC must reference each other in enablement.""" + torque = nnlc = enhanced = None for panel in schema["panels"]: for item in _iter_panel_items(panel): if item["key"] == "EnforceTorqueControl": torque = item elif item["key"] == "NeuralNetworkLateralControl": nnlc = item + elif item["key"] == "LateralJerkTorqueController": + enhanced = item assert torque is not None, "EnforceTorqueControl item missing" assert nnlc is not None, "NeuralNetworkLateralControl item missing" + assert enhanced is not None, "LateralJerkTorqueController item missing" torque_enable_keys = {r.get("key") for r in torque.get("enablement", []) if r.get("type") == "param"} assert "NeuralNetworkLateralControl" in torque_enable_keys nnlc_enable_keys = {r.get("key") for r in nnlc.get("enablement", []) if r.get("type") == "param"} assert "EnforceTorqueControl" in nnlc_enable_keys + assert "LateralJerkTorqueController" in nnlc_enable_keys + enhanced_enable_keys = {r.get("key") for r in enhanced.get("enablement", []) if r.get("type") == "param"} + assert "NeuralNetworkLateralControl" in enhanced_enable_keys class TestKnownVehicleSettings: From 855c99bf923aaf10b72e9938bc52dfe61f17cc65 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Sun, 9 Aug 2026 16:26:55 -0700 Subject: [PATCH 203/325] speedup chestnut probe (#38596) use link-up in scons --- openpilot/selfdrive/modeld/SConscript | 18 ++++++------------ openpilot/system/hardware/chestnut/flash.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 2085d65835..30b008078e 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -1,7 +1,6 @@ import glob import json import os -import sys, subprocess import time from SCons.Script import Action, Value from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_existing_chunks @@ -88,18 +87,13 @@ for usbgpu in [False, True] if USBGPU else [False]: onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum)) def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets): + from openpilot.system.hardware.chestnut.flash import link_up # chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars - probe_cmd = [sys.executable, '-c', 'from openpilot.selfdrive.modeld.compile_modeld import Device; Device[Device.DEFAULT]'] - probe_env = {**env['ENV'], 'DEV': 'USB+AMD'} - ready = False - for attempt in range(3): - if attempt: time.sleep(1) - try: - ready = subprocess.run(probe_cmd, env=probe_env, capture_output=True, timeout=10).returncode == 0 - except subprocess.TimeoutExpired: - pass - if ready: break - if not ready: + for _ in range(10): + if link_up(): + break + time.sleep(1) + else: print("Chestnut not ready, skipping big model build") return if ret := env.Execute(command): diff --git a/openpilot/system/hardware/chestnut/flash.py b/openpilot/system/hardware/chestnut/flash.py index cd9901b476..32aa5a443a 100755 --- a/openpilot/system/hardware/chestnut/flash.py +++ b/openpilot/system/hardware/chestnut/flash.py @@ -104,6 +104,26 @@ def open_device(path): return os.open(f"/dev/bus/usb/{bus:03d}/{dev:03d}", os.O_RDWR) +def link_up() -> bool: + # asm enumerates on USB-C alone, gpu is only usable once pcie link is up + try: + path, _, _ = find_chestnut() + if path is None: + return False + fd = open_device(path) + except (OSError, RuntimeError): + return False + try: + fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0x40, 0xF3, 1, 0, 0, 2000, None)) + buf = (ctypes.c_ubyte * 1)() + fcntl.ioctl(fd, USBDEVFS_CONTROL, Ctrl(0xC0, 0xE4, 0xB450, 0, 1, 1000, ctypes.cast(buf, ctypes.c_void_p))) + return buf[0] == 0x78 # LTSSM L0 + except OSError: + return False + finally: + os.close(fd) + + def claim_interface(path, setup=False): # unbind usb-storage, which binds to the ROM bootloader disable_runtime_pm(path) From 617fcd40849f2c8d4df2ab77edefe06d8d1b1781 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:22:41 -0400 Subject: [PATCH 204/325] [bot] Update Python packages (#1866) * Update Python packages * no --------- Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- docs/CARS.md | 15 ++++++++------- opendbc_repo | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index c52707f4f3..22c35b9a12 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -1,10 +1,10 @@ - + # Supported Cars A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 341 Supported Cars +# 342 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
     |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -78,8 +78,8 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Accord 2018-22|All|openpilot available[1,5](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch C connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Honda|Accord Hybrid 2018-22|All|openpilot available[1,5](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| -|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch C connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| -|Honda|City (Brazil only) 2023|All|openpilot available[1,5](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch B connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| +|Honda|Accord Hybrid 2023-26|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch C connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| +|Honda|City (Brazil only) 2023-25|All|openpilot available[1,5](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch B connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Nidec connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Honda|Civic 2019-21|All|openpilot available[1,5](#footnotes)|0 mph|2 mph[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Honda|Civic 2022-24|All|openpilot available[1,5](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Honda Bosch B connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| @@ -187,7 +187,7 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai F connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai B connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai G connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| -|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai H connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| +|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai H connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai E connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai E connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| @@ -230,14 +230,15 @@ A supported vehicle is one that just works when you install a comma device. All |Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Mazda connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Nissan[6](#footnotes)|Altima 2019-24|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Nissan B connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Nissan[6](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Nissan A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| +|Nissan[6](#footnotes)|Leaf IC 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Nissan A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Nissan[6](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Nissan A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Nissan[6](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Nissan A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Ram connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Ram connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Ram connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian B connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian B connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |SEAT[12](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |SEAT[12](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| diff --git a/opendbc_repo b/opendbc_repo index 8b9fd4a653..ae445c9b5e 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 8b9fd4a653ab9d9e4f455f01eb61ef22dbc96988 +Subproject commit ae445c9b5ea18cc66ce3b6d53584db238555dd17 From c6595fd99bfac45ff4ce030b9c9b4428b870b947 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 9 Aug 2026 23:12:51 -0400 Subject: [PATCH 205/325] offroad alerts: replace comma references with sunnypilot community and sunnylink (#1904) --- openpilot/selfdrive/selfdrived/alerts_offroad.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index c90497e8c1..8a77bd29a4 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -26,7 +26,7 @@ "severity": 1 }, "Offroad_CarUnrecognized": { - "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", + "text": "sunnypilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please select your vehicle manually at https://www.sunnylink.ai/. Need help? Visit https://community.sunnypilot.ai/", "severity": 0 }, "Offroad_Recalibration": { @@ -42,7 +42,7 @@ "severity": 0 }, "Offroad_ExcessiveActuation": { - "text": "Excessive %1 actuation detected on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", + "text": "Excessive %1 actuation detected on your last drive. Please visit https://community.sunnypilot.ai/ and share your device's Dongle ID for troubleshooting.", "severity": 1, "_comment": "Set extra field to lateral or longitudinal." }, From ea4dbc89364155c78d8e2d35d7b7fdcde88854f8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 9 Aug 2026 21:09:50 -0700 Subject: [PATCH 206/325] Remove unused code (#38598) --- openpilot/common/git.py | 9 ----- openpilot/common/stat_live.py | 3 -- .../selfdrive/controls/lib/drive_helpers.py | 1 - openpilot/selfdrive/controls/radard.py | 3 -- openpilot/selfdrive/selfdrived/events.py | 5 --- openpilot/selfdrive/ui/body/animations.py | 19 --------- .../ui/mici/layouts/settings/settings.py | 1 - openpilot/selfdrive/ui/mici/widgets/button.py | 1 - openpilot/selfdrive/ui/mici/widgets/dialog.py | 14 +------ openpilot/system/updated/common.py | 16 -------- tools/scripts/profiling/clpeak/no_print.patch | 39 ------------------- tools/scripts/test_fw_query_on_routes.py | 2 - 12 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 openpilot/system/updated/common.py delete mode 100644 tools/scripts/profiling/clpeak/no_print.patch diff --git a/openpilot/common/git.py b/openpilot/common/git.py index 6b662e5719..d285116957 100644 --- a/openpilot/common/git.py +++ b/openpilot/common/git.py @@ -31,12 +31,3 @@ def get_origin(cwd: str | None = None) -> str: return run_cmd(["git", "config", "remote." + tracking_remote + ".url"], cwd=cwd) except subprocess.CalledProcessError: # Not on a branch, fallback return run_cmd_default(["git", "config", "--get", "remote.origin.url"], cwd=cwd) - - -@cache -def get_normalized_origin(cwd: str | None = None) -> str: - return get_origin(cwd) \ - .replace("git@", "", 1) \ - .replace(".git", "", 1) \ - .replace("https://", "", 1) \ - .replace(":", "/", 1) diff --git a/openpilot/common/stat_live.py b/openpilot/common/stat_live.py index 3901c448d8..db0919b993 100644 --- a/openpilot/common/stat_live.py +++ b/openpilot/common/stat_live.py @@ -48,9 +48,6 @@ class RunningStat: def std(self): return np.sqrt(self.variance()) - def params_to_save(self): - return [self.M, self.S, self.n] - class RunningStatFilter: def __init__(self, raw_priors=None, filtered_priors=None, max_trackable=-1): self.raw_stat = RunningStat(raw_priors, -1) diff --git a/openpilot/selfdrive/controls/lib/drive_helpers.py b/openpilot/selfdrive/controls/lib/drive_helpers.py index 7ac2f50de0..35ca45e56e 100644 --- a/openpilot/selfdrive/controls/lib/drive_helpers.py +++ b/openpilot/selfdrive/controls/lib/drive_helpers.py @@ -7,7 +7,6 @@ CONTROL_N = 17 CAR_ROTATION_RADIUS = 0.0 # This is a turn radius smaller than most cars can achieve MAX_CURVATURE = 0.2 -MAX_VEL_ERR = 5.0 # m/s MIN_STABLE_DELAY = 0.3 # EU guidelines diff --git a/openpilot/selfdrive/controls/radard.py b/openpilot/selfdrive/controls/radard.py index 30824c07b8..6fecfffb1d 100755 --- a/openpilot/selfdrive/controls/radard.py +++ b/openpilot/selfdrive/controls/radard.py @@ -178,8 +178,6 @@ def get_lead(v_ego: float, ready: bool, tracks: dict[int, Track], lead_msg: capn class RadarD: def __init__(self, delay: float = 0.0): - self.current_time = 0.0 - self.tracks: dict[int, Track] = {} self.kalman_params = KalmanParams(DT_MDL) self.lead_prob_filters = [FirstOrderFilter(0.0, 0.2, DT_MDL) for _ in range(2)] @@ -195,7 +193,6 @@ class RadarD: def update(self, sm: messaging.SubMaster, rr: car.RadarData): self.ready = sm.seen['modelV2'] - self.current_time = 1e-9*max(sm.logMonoTime.values()) if sm.recv_frame['carState'] != self.last_v_ego_frame: self.v_ego = sm['carState'].vEgo diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 0464647c3b..2127cdeceb 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -339,11 +339,6 @@ def low_memory_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaste return NormalPermanentAlert("Low Memory", f"{sm['deviceState'].memoryUsagePercent}% used") -def high_cpu_usage_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - x = max(sm['deviceState'].cpuUsagePercent, default=0.) - return NormalPermanentAlert("High CPU Usage", f"{x}% used") - - def modeld_lagging_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: return NormalPermanentAlert("Driving Model Lagging", f"{sm['modelV2'].frameDropPerc:.1f}% frames dropped") diff --git a/openpilot/selfdrive/ui/body/animations.py b/openpilot/selfdrive/ui/body/animations.py index 302f8989bf..3aecd52e97 100644 --- a/openpilot/selfdrive/ui/body/animations.py +++ b/openpilot/selfdrive/ui/body/animations.py @@ -87,21 +87,12 @@ BROW_LOWERED = [ (2, 0) ] BROW_STRAIGHT = [(1, 0), (1, 1), (1, 2)] -BROW_DOWN = [ -(0, 1), (0, 2), - (1, 3) -] - # Mouths (centered, not mirrored) MOUTH_SMILE = [ (6, 6), (6, 9), (7, 7), (7, 8), ] MOUTH_NORMAL = [(7, 7), (7, 8)] -MOUTH_SAD = [ - (6, 7), (6, 8), -(7, 6), (7, 9) -] # --- Animations --- @@ -168,16 +159,6 @@ INQUISITIVE = Animation( repeat_interval=10 ) -WINK = Animation( - frames=[ - _make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE), - _make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, _mirror(_shift(BROW_DOWN, (0, 2))), MOUTH_SMILE), - ], - mode=AnimationMode.ONCE_FORWARD_BACKWARD, - frame_duration=0.75, -) - - # --- Face Animator Class --- class FaceAnimator: diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py index 56a953a65d..eb7789cba9 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py @@ -50,7 +50,6 @@ class SettingsLayout(NavScroller): device_btn, software_btn, PairBigButton(), - #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, developer_btn, ]) diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index dcb1dc2fd0..0ecda6a0c5 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -17,7 +17,6 @@ else: except (ImportError, OSError): Params = None -SCROLLING_SPEED_PX_S = 50 COMPLICATION_SIZE = 36 LABEL_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) COMPLICATION_GREY = rl.Color(0xAA, 0xAA, 0xAA, 255) diff --git a/openpilot/selfdrive/ui/mici/widgets/dialog.py b/openpilot/selfdrive/ui/mici/widgets/dialog.py index ed1466449b..77dfa3cb97 100644 --- a/openpilot/selfdrive/ui/mici/widgets/dialog.py +++ b/openpilot/selfdrive/ui/mici/widgets/dialog.py @@ -10,7 +10,7 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, BigButton, GreyBigButton +from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton, GreyBigButton DEBUG = False @@ -216,18 +216,6 @@ class BigInputDialog(BigDialogBase): self._confirm_callback() -class BigDialogButton(BigButton): - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""): - super().__init__(text, value, icon) - self._description = description - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - dlg = BigDialog(self.text, self._description) - gui_app.push_widget(dlg) - - class BigConfirmationCircleButton(BigCircleButton): def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable[[], None], exit_on_confirm: bool = True, red: bool = False, icon_offset: tuple[int, int] = (0, 0)): diff --git a/openpilot/system/updated/common.py b/openpilot/system/updated/common.py deleted file mode 100644 index 6bb745f6b0..0000000000 --- a/openpilot/system/updated/common.py +++ /dev/null @@ -1,16 +0,0 @@ -import os -import pathlib - - -def get_consistent_flag(path: str) -> bool: - consistent_file = pathlib.Path(os.path.join(path, ".overlay_consistent")) - return consistent_file.is_file() - -def set_consistent_flag(path: str, consistent: bool) -> None: - os.sync() - consistent_file = pathlib.Path(os.path.join(path, ".overlay_consistent")) - if consistent: - consistent_file.touch() - elif not consistent: - consistent_file.unlink(missing_ok=True) - os.sync() diff --git a/tools/scripts/profiling/clpeak/no_print.patch b/tools/scripts/profiling/clpeak/no_print.patch deleted file mode 100644 index 44a5efd10f..0000000000 --- a/tools/scripts/profiling/clpeak/no_print.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/src/logger.cpp b/src/logger.cpp -index a63c6dd..a1d9860 100644 ---- a/src/logger.cpp -+++ b/src/logger.cpp -@@ -24,34 +24,22 @@ logger::~logger() - - void logger::print(string str) - { -- cout << str; -- cout.flush(); - } - - void logger::print(double val) - { -- cout << setprecision(2) << fixed; -- cout << val; -- cout.flush(); - } - - void logger::print(float val) - { -- cout << setprecision(2) << fixed; -- cout << val; -- cout.flush(); - } - - void logger::print(int val) - { -- cout << val; -- cout.flush(); - } - - void logger::print(unsigned int val) - { -- cout << val; -- cout.flush(); - } - - void logger::xmlOpenTag(string tag) diff --git a/tools/scripts/test_fw_query_on_routes.py b/tools/scripts/test_fw_query_on_routes.py index ab539e4feb..33e1aa39ba 100755 --- a/tools/scripts/test_fw_query_on_routes.py +++ b/tools/scripts/test_fw_query_on_routes.py @@ -12,7 +12,6 @@ from openpilot.tools.lib.logreader import LogReader, ReadMode from openpilot.tools.lib.route import SegmentRange -NO_API = "NO_API" in os.environ SUPPORTED_BRANDS = VERSIONS.keys() SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]] UNKNOWN_BRAND = "unknown" @@ -178,4 +177,3 @@ if __name__ == "__main__": print(f"Correct fuzzy matches: {good_fuzzy}") print(f"Wrong fuzzy matches: {wrong_fuzzy}") print() - From 2a16b0fbba64de33b8eb04e833b68a31ec982ec7 Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 10 Aug 2026 04:27:30 -0400 Subject: [PATCH 207/325] ui: screensaver (#1551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * param to control stock vs sp ui * init styles * SP Toggles * Lint * optimizations * Panels. With Icons. And Scroller. * patience, grasshopper * more patience, grasshopper * sp raylib preview * fix callback * fix ui preview * add ui previews * Option Control * Need this * introducing ui_state_sp for py * param to control stock vs sp ui * better * add ui_update callback * better padding * this * listitem -> listitemsp * Revert "add ui_update callback" This reverts commit 4da32cc0097434aab0aa6a3c35465eabb23c8958. * add show_description method * remove padding from line separator. like, WHY? 😩😩 * simplify * I. SAID. SIMPLIFY. * AAARGGGGGG..... * init * option control value fix * add all controls * hide all controls * lint * scroller -> scroller_tici * scroller -> scroller_tici * ui: `GuiApplicationExt` * add to readme * use gui_app.sunnypilot_ui() * use gui_app.sunnypilot_ui() * use gui_app.sunnypilot_ui() * optimizations * Removed hide for now * why? because! * oops * better * add param, timeout * add toggle * not doing this * we doing this now @devtekve? FINE!! * it wasn't me * really @devtekve? REALLY?? * refresh controls * ugh * changes * fix * inline everything again * handle default * use ui_state param * better * fix * lint * better * solve for lint * default on pls * screen saver hue distance and increase minimum color shift * debounce screen saver color changes * fix dismiss flow --------- Co-authored-by: Jason Wen Co-authored-by: DevTekVE --- openpilot/common/params_keys.h | 2 + .../ui/sunnypilot/layouts/settings/display.py | 20 ++- openpilot/selfdrive/ui/sunnypilot/ui_state.py | 21 ++++ openpilot/selfdrive/ui/ui_state.py | 2 + .../sunnypilot/sunnylink/settings_ui.json | 61 +++++++++ .../settings_ui_src/pages/display.yaml | 33 +++++ .../ui/sunnypilot/widgets/screen_saver.py | 118 ++++++++++++++++++ 7 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 openpilot/system/ui/sunnypilot/widgets/screen_saver.py diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index f0921b6723..198b7a92a1 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -180,6 +180,8 @@ inline static std::unordered_map keys = { {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py index 8ba5663662..897d34085a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -9,7 +9,7 @@ from enum import IntEnum from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES @@ -61,10 +61,26 @@ class DisplayLayout(Widget): f"{value} s" if value < 60 else f"{int(value/60)} m"), inline=True ) + self._screensaver_toggle = toggle_item_sp( + param="ScreenSaverEnabled", + title=lambda: tr("Screen Saver"), + description=lambda: tr("Show a screen saver when the device is offroad and idle, instead of turning the screen off."), + ) + self._screensaver_timeout = option_item_sp( + param="ScreenSaverTimeout", + title=lambda: tr("Screen Saver Duration"), + description=lambda: tr("How long the screen saver runs before the screen turns off."), + min_value=60, + max_value=600, + value_change_step=60, + label_callback=lambda value: f"{int(value/60)} m" + ) items = [ self._onroad_brightness, self._onroad_brightness_timer, self._interactivity_timeout, + self._screensaver_toggle, + self._screensaver_timeout, ] return items @@ -87,6 +103,8 @@ class DisplayLayout(Widget): brightness_val = self._onroad_brightness.action_item.current_value self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)) + self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state()) + def _render(self, rect): self._scroller.render(rect) diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 4828f37103..3f2889de9a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -12,6 +12,7 @@ from openpilot.common.params import Params from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP OpenpilotState = log.SelfdriveState.OpenpilotState MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -38,6 +39,9 @@ class UIStateSP: self.sunnylink_state = SunnylinkState() + self.screensaver = ScreenSaverSP(params=self.params) + self.screensaver_enabled: bool = False + self.active_bundle = None self.blindspot: bool = False self.chevron_metrics = None @@ -170,6 +174,7 @@ class UIStateSP: self.turn_signals = self.params.get_bool("ShowTurnSignals") self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) self.always_offroad = self.params.get_bool("OffroadMode") + self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled") if not self._sp_initialized: self._sp_initialized = True @@ -230,10 +235,26 @@ class UIStateSP: class DeviceSP: + def __init__(self): + self._blocked_by_screensaver: bool = False + def _set_awake(self, on: bool, _ui_state=None): + self._blocked_by_screensaver = False + if _ui_state.boot_offroad_mode == 1 and not on: _ui_state.params.put_bool("OffroadMode", True) + if not on and _ui_state.screensaver_enabled: + if _ui_state.screensaver.was_dismissed: + if gui_app.get_active_widget() == _ui_state.screensaver: + gui_app.pop_widget() + elif _ui_state.screensaver.is_active: + self._blocked_by_screensaver = True + else: + _ui_state.screensaver.initialize() + gui_app.push_widget(_ui_state.screensaver) + self._blocked_by_screensaver = True + @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: if not awake or not _ui_state.started: diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 59742edfed..67845bdc87 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -340,6 +340,8 @@ class Device(DeviceSP): def _set_awake(self, on: bool, _ui_state=None): if on != self._awake: super()._set_awake(on, _ui_state or ui_state) + if self._blocked_by_screensaver: + return self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index 1e6422ac84..dd972c2957 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -1286,6 +1286,67 @@ "label": "2 m" } ] + }, + { + "key": "ScreenSaverEnabled", + "widget": "toggle", + "title": "Screen Saver", + "description": "Show a screen saver when the device is offroad and idle, instead of turning the screen off." + }, + { + "key": "ScreenSaverTimeout", + "widget": "multiple_button", + "title": "Screen Saver Duration", + "description": "How long the screen saver runs before the screen turns off.", + "options": [ + { + "value": 60, + "label": "1 m" + }, + { + "value": 120, + "label": "2 m" + }, + { + "value": 180, + "label": "3 m" + }, + { + "value": 240, + "label": "4 m" + }, + { + "value": 300, + "label": "5 m" + }, + { + "value": 360, + "label": "6 m" + }, + { + "value": 420, + "label": "7 m" + }, + { + "value": 480, + "label": "8 m" + }, + { + "value": 540, + "label": "9 m" + }, + { + "value": 600, + "label": "10 m" + } + ], + "enablement": [ + { + "type": "param", + "key": "ScreenSaverEnabled", + "equals": true + } + ] } ] } diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml index 39a8cbaf80..3e3b16c374 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml @@ -128,3 +128,36 @@ sections: label: 1 m - value: 120 label: 2 m + - key: ScreenSaverEnabled + widget: toggle + title: Screen Saver + description: Show a screen saver when the device is offroad and idle, instead of turning the screen off. + - key: ScreenSaverTimeout + widget: multiple_button + title: Screen Saver Duration + description: How long the screen saver runs before the screen turns off. + options: + - value: 60 + label: 1 m + - value: 120 + label: 2 m + - value: 180 + label: 3 m + - value: 240 + label: 4 m + - value: 300 + label: 5 m + - value: 360 + label: 6 m + - value: 420 + label: 7 m + - value: 480 + label: 8 m + - value: 540 + label: 9 m + - value: 600 + label: 10 m + enablement: + - type: param + key: ScreenSaverEnabled + equals: true diff --git a/openpilot/system/ui/sunnypilot/widgets/screen_saver.py b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py new file mode 100644 index 0000000000..bf218306d8 --- /dev/null +++ b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py @@ -0,0 +1,118 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import os +import time + +import pyray as rl + +from openpilot.common.hardware import HARDWARE +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +class ScreenSaverSP(Widget): + def __init__(self, params: Params | None = None): + super().__init__() + self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + self._params = params or Params() + self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1") + + self.x = 0.0 + self.y = 100.0 + self.vx = 120.0 if self._is_mici else 300.0 + self.vy = 70.0 if self._is_mici else 200.0 + self._hue = 150 + self.color = rl.color_from_hsv(self._hue, 1, 1) + + self.text = "sunnypilot" + self.font_size = 50 if self._is_mici else 200 + self._start_time = None + self._dismiss = False + self._screensaver_timeout = 300 + self._hit_last_frame = False + + @property + def is_active(self) -> bool: + return self._start_time is not None and not self._dismiss + + @property + def was_dismissed(self) -> bool: + return self._dismiss + + def initialize(self): + self._screensaver_timeout = self._params.get("ScreenSaverTimeout", return_default=True) + if self._start_time is None: + self._start_time = time.monotonic() + self._dismiss = False + + def hide_event(self): + super().hide_event() + self._dismiss = False + self._start_time = None + + def _handle_mouse_release(self, mouse_pos): + self._dismiss = True + self._start_time = None + gui_app.pop_widget() + return super()._handle_mouse_release(mouse_pos) + + def _update_state(self): + super()._update_state() + + self.font = gui_app.font(FontWeight.AUDIOWIDE) + text_size = measure_text_cached(self.font, self.text, self.font_size, 0) + self.logo_width = text_size.x + self.logo_height = text_size.y + + if self._start_time and time.monotonic() - self._start_time > self._screensaver_timeout: + self._dismiss = True + self._start_time = None + + dt = rl.get_frame_time() + + self.x += self.vx * dt + self.y += self.vy * dt + + hit_x = hit_y = False + if self.x + self.logo_width > self.rect.width: + self.vx *= -1 + self.x = self.rect.width - self.logo_width + hit_x = True + elif self.x < 0: + self.vx *= -1 + self.x = 0 + hit_x = True + + if self.y + self.logo_height > self.rect.height: + self.vy *= -1 + self.y = self.rect.height - self.logo_height + hit_y = True + elif self.y < 0: + self.vy *= -1 + self.y = 0 + hit_y = True + + hit = hit_x or hit_y + if hit and not self._hit_last_frame: + while self._hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 120: + pass + self._hue = new_hue + self.color = rl.color_from_hsv(self._hue, 1, 1) + self._hit_last_frame = hit + + @staticmethod + def _hue_dist(a, b): + d = abs(a - b) + return min(d, 360 - d) + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + rl.clear_background(rl.BLACK) + rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color) + return -1 From 65c007a9a2f59e9f290d566a09acc59d65090435 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 10 Aug 2026 08:16:57 -0700 Subject: [PATCH 208/325] tools: setup.sh isn't for AGNOS (#38588) * tools: setup.sh isn't for AGNOS * Update setup.sh --- tools/setup.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/setup.sh b/tools/setup.sh index d4b68d15cb..ced451ab11 100755 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -33,6 +33,13 @@ cat << 'EOF' EOF } +function check_platform() { + if [[ -f /AGNOS ]]; then + echo -e "[${RED}✗${NC}] This installer is for PCs only. The environment is pre-configured in AGNOS." + return 1 + fi +} + function check_stdin() { if [ -t 0 ]; then INTERACTIVE=1 @@ -135,6 +142,7 @@ function install_with_op() { } show_motd +check_platform check_stdin ask_dir check_dir From 434fbad5fcfc68697f447e8bfcf812bf6a1d56cc Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Mon, 10 Aug 2026 08:36:12 -0700 Subject: [PATCH 209/325] [bot] Update Python packages (#38599) * Update Python packages * Fix package update CI --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- .github/workflows/diff_report.yaml | 3 +- opendbc_repo | 2 +- .../test/process_replay/migration.py | 2 +- tinygrad_repo | 2 +- uv.lock | 76 +++++++++---------- 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/.github/workflows/diff_report.yaml b/.github/workflows/diff_report.yaml index 0f706ea2ed..202bc0ec79 100644 --- a/.github/workflows/diff_report.yaml +++ b/.github/workflows/diff_report.yaml @@ -40,6 +40,7 @@ jobs: echo "run-id=$run_id" >> "$GITHUB_OUTPUT" - name: Download diff if: steps.wait.outcome == 'success' + continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -48,7 +49,7 @@ jobs: name: diff_report_${{ github.event.number }} path: . - name: Comment on PR - if: steps.wait.outcome == 'success' + if: steps.wait.outcome == 'success' && hashFiles('diff_report.txt') != '' uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b with: file-path: diff_report.txt diff --git a/opendbc_repo b/opendbc_repo index b0685818f3..44f2987cb6 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b0685818f31df1e9d225ebc762087a8ebd563edb +Subproject commit 44f2987cb6ed28f7dcd99d5930abf6c2917d8f60 diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index ff98389e2c..65d54221ee 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -313,7 +313,7 @@ def migrate_pandaStates(msgs): "CHEVROLET_BOLT_EUV": GMSafetyFlags.EV | GMSafetyFlags.HW_CAM, } # TODO: get new Ford route - safety_param_migration |= dict.fromkeys((set(FORD) - FORD.with_flags(FordFlags.CANFD)), FordSafetyFlags.LONG_CONTROL) + safety_param_migration |= dict.fromkeys({p for p in FORD if not (p.config.flags & FordFlags.CANFD)}, FordSafetyFlags.LONG_CONTROL) # Migrate safety param base on carParams CP = next((m.carParams for _, m in msgs if m.which() == 'carParams'), None) diff --git a/tinygrad_repo b/tinygrad_repo index 1858f1fd9a..8611fe22a7 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 1858f1fd9aa94ca4e302b60a88f075d0d1dd88bc +Subproject commit 8611fe22a7fcc7d1928bbde19ded66277cb12f3e diff --git a/uv.lock b/uv.lock index efd0aad651..5717d0a38d 100644 --- a/uv.lock +++ b/uv.lock @@ -513,21 +513,21 @@ provides-extras = ["dev"] [[package]] name = "numpy" -version = "2.5.1" +version = "2.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, ] [[package]] @@ -879,27 +879,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.1" +version = "0.16.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, - { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, - { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, - { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, - { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, - { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, - { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, - { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, - { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, - { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, - { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] [[package]] @@ -913,15 +913,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.66.1" +version = "2.67.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/8a/b2eec40df8a67bf073e244d29001d04ee365d163bc4f15efdfce35f53090/sentry_sdk-2.67.1.tar.gz", hash = "sha256:f263d8c9aa4137750640de8fb0ed5404df6bb564e20e4b59cb16a6eeba18d4ed", size = 990599, upload-time = "2026-08-10T13:05:55.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/34/e2/70692eba662037cddf93391cbbf98297159f3038612e9b9a8129e16feb7a/sentry_sdk-2.67.1-py3-none-any.whl", hash = "sha256:a66bfbce1cd8a93c51c369d642ad85b46253ea7a6f7938141315b83e2823cda5", size = 515591, upload-time = "2026-08-10T13:05:54.213Z" }, ] [[package]] @@ -944,11 +944,11 @@ wheels = [ [[package]] name = "setuptools" -version = "83.0.0" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] From 6d5d1f691708ae95896516e45089676e487c876e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 10 Aug 2026 09:30:22 -0700 Subject: [PATCH 210/325] Rename "live" cereal services (#38601) --- openpilot/cereal/log.capnp | 24 +++--- .../messaging/tests/test_pub_sub_master.py | 4 +- openpilot/cereal/services.py | 12 +-- openpilot/common/mock/__init__.py | 4 +- openpilot/common/mock/generators.py | 18 ++--- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/car/card.py | 8 +- openpilot/selfdrive/controls/controlsd.py | 24 +++--- .../controls/lib/latcontrol_torque.py | 2 +- .../lib/longitudinal_mpc_lib/long_mpc.py | 2 +- .../controls/lib/longitudinal_planner.py | 2 +- openpilot/selfdrive/controls/plannerd.py | 2 +- openpilot/selfdrive/controls/radard.py | 4 +- .../controls/tests/test_latcontrol.py | 2 +- .../tests/test_latcontrol_torque_buffer.py | 2 +- .../selfdrive/controls/tests/test_leads.py | 2 +- .../tests/test_torqued_lat_accel_offset.py | 16 ++-- openpilot/selfdrive/locationd/calibrationd.py | 56 +++++++------- openpilot/selfdrive/locationd/helpers.py | 18 ++--- openpilot/selfdrive/locationd/lagd.py | 58 +++++++-------- openpilot/selfdrive/locationd/locationd.py | 38 +++++----- openpilot/selfdrive/locationd/paramsd.py | 74 +++++++++---------- .../locationd/test/test_calibrationd.py | 20 ++--- .../selfdrive/locationd/test/test_lagd.py | 50 ++++++------- .../test/test_locationd_scenarios.py | 2 +- .../selfdrive/locationd/test/test_paramsd.py | 22 +++--- .../selfdrive/locationd/test/test_torqued.py | 6 +- openpilot/selfdrive/locationd/torqued.py | 64 ++++++++-------- .../selfdrive/modeld/dmonitoringmodeld.py | 6 +- openpilot/selfdrive/modeld/fill_model_msg.py | 4 +- openpilot/selfdrive/modeld/modeld.py | 16 ++-- .../selfdrive/monitoring/dmonitoringd.py | 2 +- openpilot/selfdrive/monitoring/policy.py | 2 +- openpilot/selfdrive/selfdrived/events.py | 18 ++--- openpilot/selfdrive/selfdrived/helpers.py | 8 +- openpilot/selfdrive/selfdrived/selfdrived.py | 33 +++++---- openpilot/selfdrive/test/helpers.py | 6 +- .../test/longitudinal_maneuvers/plant.py | 4 +- .../selfdrive/test/process_replay/README.md | 2 +- .../test/process_replay/migration.py | 28 +++---- .../test/process_replay/model_replay.py | 6 +- .../test/process_replay/process_replay.py | 69 ++++++++--------- openpilot/selfdrive/test/test_onroad.py | 4 +- openpilot/selfdrive/test/test_power_draw.py | 2 +- .../selfdrive/ui/layouts/settings/device.py | 8 +- .../ui/mici/onroad/augmented_road_view.py | 10 +-- .../ui/mici/onroad/model_renderer.py | 6 +- .../selfdrive/ui/mici/onroad/torque_bar.py | 4 +- .../ui/onroad/augmented_road_view.py | 10 +-- .../selfdrive/ui/onroad/model_renderer.py | 6 +- .../selfdrive/ui/tests/diff/replay_script.py | 22 +++--- openpilot/selfdrive/ui/ui_state.py | 4 +- .../jotpluggler/layouts/locationd_debug.json | 2 +- .../jotpluggler/layouts/max-torque-debug.json | 2 +- .../layouts/torque-controller.json | 2 +- openpilot/tools/joystick/joystickd.py | 6 +- .../longitudinal_maneuvers/generate_report.py | 8 +- .../plotjuggler/layouts/locationd_debug.xml | 5 +- .../plotjuggler/layouts/max-torque-debug.xml | 7 +- .../plotjuggler/layouts/torque-controller.xml | 37 +++++----- openpilot/tools/replay/consoleui.cc | 4 +- openpilot/tools/replay/ui.py | 20 ++--- tools/scripts/car/max_lat_accel.py | 4 +- tools/scripts/cycle_alerts.py | 6 +- 64 files changed, 460 insertions(+), 460 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fa185aa483..7b93e4aa73 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -763,7 +763,7 @@ struct RadarState @0x9a185389d6fdd05f { } } -struct LiveCalibrationData { +struct ExtrinsicsCalibration @0x96df70754d8390bc { calStatus @11 :Status; calCycle @2 :Int32; calPerc @3 :Int8; @@ -1384,7 +1384,7 @@ struct LiveLocationKalman { } -struct LivePose { +struct DeviceMotion @0xc24ca2b57206b44d { # More info on reference frames: # https://github.com/commaai/openpilot/tree/master/openpilot/common/transformations orientationNED @0 :XYZMeasurement; @@ -2266,7 +2266,7 @@ struct Boot { } } -struct LiveParametersData { +struct VehicleParameters @0xd9058dcb967c2753 { valid @0 :Bool; gyroBias @1 :Float32; angleOffsetDeg @2 :Float32; @@ -2300,8 +2300,8 @@ struct LiveParametersData { } } -struct LiveTorqueParametersData { - liveValid @0 :Bool; +struct LateralTorqueParameters @0xe61690eb0b091692 { + valid @0 :Bool; latAccelFactorRaw @1 :Float32; latAccelOffsetRaw @2 :Float32; frictionCoefficientRaw @3 :Float32; @@ -2317,7 +2317,7 @@ struct LiveTorqueParametersData { calPerc @13 :Int8; } -struct LiveDelayData { +struct LateralDelay @0x98dfdb22c44df8d4 { lateralDelay @0 :Float32; validBlocks @1 :Int32; status @2 :Status; @@ -2535,9 +2535,9 @@ struct Event { pandaStates @81 :List(PandaState); peripheralState @80 :PeripheralState; radarState @13 :RadarState; - liveTracks @131 :Car.RadarData; + radarTracks @131 :Car.RadarData; sendcan @17 :List(CanData); - liveCalibration @19 :LiveCalibrationData; + extrinsicsCalibration @19 :ExtrinsicsCalibration; carState @22 :Car.CarState; carControl @23 :Car.CarControl; carOutput @127 :Car.CarOutput; @@ -2548,15 +2548,15 @@ struct Event { qcomGnss @31 :QcomGnss; gpsLocationExternal @48 :GpsLocationData; gpsLocation @21 :GpsLocationData; - liveParameters @61 :LiveParametersData; - liveTorqueParameters @94 :LiveTorqueParametersData; - liveDelay @146 : LiveDelayData; + vehicleParameters @61 :VehicleParameters; + lateralTorqueParameters @94 :LateralTorqueParameters; + lateralDelay @146 : LateralDelay; cameraOdometry @63 :CameraOdometry; thumbnail @66: Thumbnail; onroadEvents @134: List(OnroadEvent); carParams @69: Car.CarParams; driverMonitoringState @151 :DriverMonitoringState; - livePose @129 :LivePose; + deviceMotion @129 :DeviceMotion; modelV2 @75 :ModelDataV2; drivingModelData @128 :DrivingModelData; driverStateV2 @92 :DriverStateV2; diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index c9304a3e65..24ee68d4fd 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -70,7 +70,7 @@ class TestSubMaster(OpenpilotTestCase): def test_avg_frequency_checks(self): for poll in (True, False): - sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "liveCalibration"], + sm = messaging.SubMaster(["modelV2", "carParams", "carState", "cameraOdometry", "extrinsicsCalibration"], poll=("modelV2" if poll else None), frequency=(20. if not poll else None)) @@ -78,7 +78,7 @@ class TestSubMaster(OpenpilotTestCase): "carState": (20, 20), "modelV2": (20, 20 if poll else 10), "cameraOdometry": (20, 10), - "liveCalibration": (4, 4), + "extrinsicsCalibration": (4, 4), "carParams": (None, None), "userBookmark": (None, None), } diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index 61fb8fcbd2..ebe8be1d60 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -34,13 +34,13 @@ _services: dict[str, tuple] = { "peripheralState": (True, 2., 1), "radarState": (True, 20., 5), "narrowRoadEncodeIdx": (False, 20., 1), - "liveTracks": (True, 20.), + "radarTracks": (True, 20.), "sendcan": (True, 100., 139, QueueSize.MEDIUM), "logMessage": (True, 0., None, QueueSize.BIG), "errorLogMessage": (True, 0., 1, QueueSize.BIG), - "liveCalibration": (True, 4., 4), - "liveTorqueParameters": (True, 4., 1), - "liveDelay": (True, 4., 1), + "extrinsicsCalibration": (True, 4., 4), + "lateralTorqueParameters": (True, 4., 1), + "lateralDelay": (True, 4., 1), "operatingSystemLog": (True, 0.), "carState": (True, 100., 10), "carControl": (True, 100., 10), @@ -55,8 +55,8 @@ _services: dict[str, tuple] = { "qcomGnss": (True, 2.), "clocks": (True, 0.1, 1), "ubloxRaw": (True, 20.), - "livePose": (True, 20., 4), - "liveParameters": (True, 20., 5), + "deviceMotion": (True, 20., 4), + "vehicleParameters": (True, 20., 5), "cameraOdometry": (True, 20., 10), "thumbnail": (True, 1 / 60., 1), "onroadEvents": (True, 1., 1), diff --git a/openpilot/common/mock/__init__.py b/openpilot/common/mock/__init__.py index ff4dd32b92..9fa1e4b4d7 100644 --- a/openpilot/common/mock/__init__.py +++ b/openpilot/common/mock/__init__.py @@ -8,12 +8,12 @@ import functools import threading from openpilot.cereal.messaging import PubMaster from openpilot.cereal.services import SERVICE_LIST -from openpilot.common.mock.generators import generate_livePose +from openpilot.common.mock.generators import generate_deviceMotion from openpilot.common.realtime import Ratekeeper MOCK_GENERATOR = { - "livePose": generate_livePose + "deviceMotion": generate_deviceMotion } diff --git a/openpilot/common/mock/generators.py b/openpilot/common/mock/generators.py index 28a3b98e58..176471f1ab 100644 --- a/openpilot/common/mock/generators.py +++ b/openpilot/common/mock/generators.py @@ -1,14 +1,14 @@ from openpilot.cereal import messaging -def generate_livePose(): - msg = messaging.new_message('livePose') +def generate_deviceMotion(): + msg = messaging.new_message('deviceMotion') meas = {'x': 0.0, 'y': 0.0, 'z': 0.0, 'xStd': 0.0, 'yStd': 0.0, 'zStd': 0.0, 'valid': True} - msg.livePose.orientationNED = meas - msg.livePose.velocityDevice = meas - msg.livePose.angularVelocityDevice = meas - msg.livePose.accelerationDevice = meas - msg.livePose.inputsOK = True - msg.livePose.posenetOK = True - msg.livePose.sensorsOK = True + msg.deviceMotion.orientationNED = meas + msg.deviceMotion.velocityDevice = meas + msg.deviceMotion.angularVelocityDevice = meas + msg.deviceMotion.accelerationDevice = meas + msg.deviceMotion.inputsOK = True + msg.deviceMotion.posenetOK = True + msg.deviceMotion.sensorsOK = True return msg diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 5bdfdf8636..0019f6c9ac 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -77,6 +77,7 @@ inline static std::unordered_map keys = { {"LastUpdateRouteCount", {PERSISTENT, INT, "0"}}, {"LastUpdateTime", {PERSISTENT, TIME}}, {"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, + // TODO: rename the Live* learner cache keys to match their Cereal services, with migration for persisted values. {"LiveDelay", {PERSISTENT, BYTES}}, {"LiveParametersV2", {PERSISTENT, BYTES}}, {"LivestreamEncoderBitrate", {CLEAR_ON_MANAGER_START | DONT_LOG, INT}}, diff --git a/openpilot/selfdrive/car/card.py b/openpilot/selfdrive/car/card.py index 96c169a1da..86f8fbbde5 100755 --- a/openpilot/selfdrive/car/card.py +++ b/openpilot/selfdrive/car/card.py @@ -66,7 +66,7 @@ class Car: def __init__(self, CI=None, RI=None) -> None: self.can_sock = messaging.sub_sock('can', timeout=20) self.sm = messaging.SubMaster(['pandaStates', 'carControl', 'onroadEvents']) - self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'liveTracks']) + self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput', 'radarTracks']) self.can_rcv_cum_timeout_counter = 0 @@ -216,10 +216,10 @@ class Car: self.pm.send('carState', cs_send) if RD is not None: - tracks_msg = messaging.new_message('liveTracks') + tracks_msg = messaging.new_message('radarTracks') tracks_msg.valid = not any(RD.errors.to_dict().values()) - tracks_msg.liveTracks = RD - self.pm.send('liveTracks', tracks_msg) + tracks_msg.radarTracks = RD + self.pm.send('radarTracks', tracks_msg) def controls_update(self, CS: car.CarState, CC: car.CarControl): """control update loop, driven by carControl""" diff --git a/openpilot/selfdrive/controls/controlsd.py b/openpilot/selfdrive/controls/controlsd.py index 9afcdf1d27..aef92fb7ab 100755 --- a/openpilot/selfdrive/controls/controlsd.py +++ b/openpilot/selfdrive/controls/controlsd.py @@ -38,8 +38,8 @@ class Controls: self.CI = interfaces[self.CP.carFingerprint](self.CP) - self.sm = messaging.SubMaster(['liveDelay', 'liveParameters', 'liveTorqueParameters', 'modelV2', 'selfdriveState', - 'liveCalibration', 'livePose', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput', + self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState', + 'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput', 'driverMonitoringState', 'onroadEvents', 'driverAssistance'], poll='selfdriveState') self.pm = messaging.PubMaster(['carControl', 'controlsState']) @@ -64,17 +64,17 @@ class Controls: def update(self): self.sm.update(15) - if self.sm.updated["liveCalibration"]: - self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) - if self.sm.updated["livePose"]: - device_pose = Pose.from_live_pose(self.sm['livePose']) - self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) + if self.sm.updated["extrinsicsCalibration"]: + self.pose_calibrator.feed_extrinsics_calibration(self.sm['extrinsicsCalibration']) + if self.sm.updated["deviceMotion"]: + device_motion = Pose.from_device_motion(self.sm['deviceMotion']) + self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_motion) def state_control(self): CS = self.sm['carState'] # Update VehicleModel - lp = self.sm['liveParameters'] + lp = self.sm['vehicleParameters'] x = max(lp.stiffnessFactor, 0.1) sr = max(lp.steerRatio, 0.1) self.VM.update_params(x, sr) @@ -84,9 +84,9 @@ class Controls: # Update Torque Params if self.CP.lateralTuning.which() == 'torque': - torque_params = self.sm['liveTorqueParameters'] - if self.sm.all_checks(['liveTorqueParameters']) and torque_params.useParams: - self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered, + torque_params = self.sm['lateralTorqueParameters'] + if self.sm.all_checks(['lateralTorqueParameters']) and torque_params.useParams: + self.LaC.update_torque_parameters(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered, torque_params.frictionCoefficientFiltered) long_plan = self.sm['longitudinalPlan'] @@ -125,7 +125,7 @@ class Controls: else: new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll) - lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS + lat_delay = self.sm["lateralDelay"].lateralDelay + LAT_SMOOTH_SECONDS actuators.curvature = self.desired_curvature steer, lateral_output, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, diff --git a/openpilot/selfdrive/controls/lib/latcontrol_torque.py b/openpilot/selfdrive/controls/lib/latcontrol_torque.py index 846f1daf00..267c5b5967 100644 --- a/openpilot/selfdrive/controls/lib/latcontrol_torque.py +++ b/openpilot/selfdrive/controls/lib/latcontrol_torque.py @@ -46,7 +46,7 @@ class LatControlTorque(LatControl): self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt) self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) - def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): + def update_torque_parameters(self, latAccelFactor, latAccelOffset, friction): self.torque_params.latAccelFactor = latAccelFactor self.torque_params.latAccelOffset = latAccelOffset self.torque_params.friction = friction diff --git a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index 725491eed2..0a0722fbc6 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -102,7 +102,7 @@ def gen_long_model(): a_ego_dot = SX.sym('a_ego_dot') model.xdot = vertcat(x_ego_dot, v_ego_dot, a_ego_dot) - # live parameters + # runtime parameters a_min = SX.sym('a_min') a_max = SX.sym('a_max') x_obstacle = SX.sym('x_obstacle') diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 8855d85af4..c2b8b94abb 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -96,7 +96,7 @@ class LongitudinalPlanner: throttle_prob = throttle_probs[1] if len(throttle_probs) > 1 else 1.0 self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED - steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg + steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['vehicleParameters'].angleOffsetDeg if reset_state: self.v_desired_filter.x = v_ego diff --git a/openpilot/selfdrive/controls/plannerd.py b/openpilot/selfdrive/controls/plannerd.py index 60a6525853..110c124166 100755 --- a/openpilot/selfdrive/controls/plannerd.py +++ b/openpilot/selfdrive/controls/plannerd.py @@ -19,7 +19,7 @@ def main(): ldw = LaneDepartureWarning() longitudinal_planner = LongitudinalPlanner(CP) pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance']) - sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], + sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'vehicleParameters', 'radarState', 'modelV2', 'selfdriveState'], poll='modelV2') while True: diff --git a/openpilot/selfdrive/controls/radard.py b/openpilot/selfdrive/controls/radard.py index 6fecfffb1d..e6b5a0aea8 100755 --- a/openpilot/selfdrive/controls/radard.py +++ b/openpilot/selfdrive/controls/radard.py @@ -260,7 +260,7 @@ def main() -> None: cloudlog.info("radard got CarParams") # *** setup messaging - sm = messaging.SubMaster(['modelV2', 'carState', 'liveTracks'], poll='modelV2') + sm = messaging.SubMaster(['modelV2', 'carState', 'radarTracks'], poll='modelV2') pm = messaging.PubMaster(['radarState']) RD = RadarD(CP.radarDelay) @@ -268,7 +268,7 @@ def main() -> None: while 1: sm.update() - RD.update(sm, sm['liveTracks']) + RD.update(sm, sm['radarTracks']) RD.publish(pm) diff --git a/openpilot/selfdrive/controls/tests/test_latcontrol.py b/openpilot/selfdrive/controls/tests/test_latcontrol.py index 7f2ab8d6b7..2b7a0cf695 100644 --- a/openpilot/selfdrive/controls/tests/test_latcontrol.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol.py @@ -31,7 +31,7 @@ class TestLatControl(OpenpilotTestCase): CS.vEgo = 30 CS.steeringPressed = False - params = log.LiveParametersData.new_message() + params = log.VehicleParameters.new_message() # Saturate for curvature limited and controller limited for _ in range(1000): diff --git a/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py index 65befdec0f..6be242821c 100644 --- a/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py +++ b/openpilot/selfdrive/controls/tests/test_latcontrol_torque_buffer.py @@ -27,7 +27,7 @@ class TestLatControlTorqueBuffer(OpenpilotTestCase): CS = car.CarState.new_message() CS.vEgo = 30 CS.steeringPressed = False - params = log.LiveParametersData.new_message() + params = log.VehicleParameters.new_message() for _ in range(buffer_steps): controller.update(True, CS, VM, params, False, 0.001, False, 0.2) diff --git a/openpilot/selfdrive/controls/tests/test_leads.py b/openpilot/selfdrive/controls/tests/test_leads.py index a3e226cb4b..45257c545b 100644 --- a/openpilot/selfdrive/controls/tests/test_leads.py +++ b/openpilot/selfdrive/controls/tests/test_leads.py @@ -26,7 +26,7 @@ class TestLeads(OpenpilotTestCase): msgs = [m for _ in range(3) for m in single_iter_pkg()] out = replay_process_with_name("card", msgs, fingerprint=TOYOTA.TOYOTA_COROLLA_TSS2) - states = [m for m in out if m.which() == "liveTracks"] + states = [m for m in out if m.which() == "radarTracks"] failures = [not state.valid for state in states] assert len(states) == 0 or all(failures) diff --git a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py index 4fe855544e..011713dfb3 100644 --- a/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ b/openpilot/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -41,7 +41,7 @@ def simulate_straight_road_msgs(est): carControl = messaging.new_message('carControl').carControl carOutput = messaging.new_message('carOutput').carOutput carState = messaging.new_message('carState').carState - livePose = messaging.new_message('livePose').livePose + deviceMotion = messaging.new_message('deviceMotion').deviceMotion carControl.latActive = True carState.vEgo = V_EGO carState.steeringPressed = False @@ -50,11 +50,11 @@ def simulate_straight_road_msgs(est): lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True): carOutput.actuatorsOutput.torque = float(-steer_torque) - livePose.orientationNED = {'x': float(np.deg2rad(ROLL_BIAS_DEG)), 'valid': True} - livePose.angularVelocityDevice = {'z': float(lat_accel / V_EGO), 'valid': True} - livePose.inputsOK, livePose.sensorsOK, livePose.posenetOK = True, True, True - livePose.timestamp = int(t * 1e9) - for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): + deviceMotion.orientationNED = {'x': float(np.deg2rad(ROLL_BIAS_DEG)), 'valid': True} + deviceMotion.angularVelocityDevice = {'z': float(lat_accel / V_EGO), 'valid': True} + deviceMotion.inputsOK, deviceMotion.sensorsOK, deviceMotion.posenetOK = True, True, True + deviceMotion.timestamp = int(t * 1e9) + for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('deviceMotion', deviceMotion)): est.handle_log(t, which, msg) class TestTorquedLatAccelOffset(OpenpilotTestCase): @@ -63,11 +63,11 @@ class TestTorquedLatAccelOffset(OpenpilotTestCase): est = get_warmed_up_estimator(steer_torques, lat_accels) msg = est.get_msg() # TODO add lataccelfactor and friction check when we have more accurate estimates - assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 + assert abs(msg.lateralTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 def test_straight_road_roll_bias(self): steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) est = get_warmed_up_estimator(steer_torques, lat_accels) simulate_straight_road_msgs(est) msg = est.get_msg() - assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) + assert (msg.lateralTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.lateralTorqueParameters.latAccelOffsetRaw) diff --git a/openpilot/selfdrive/locationd/calibrationd.py b/openpilot/selfdrive/locationd/calibrationd.py index b9616bf632..ec9a225634 100755 --- a/openpilot/selfdrive/locationd/calibrationd.py +++ b/openpilot/selfdrive/locationd/calibrationd.py @@ -74,15 +74,15 @@ class Calibrator: wide_from_device_euler = WIDE_FROM_DEVICE_EULER_INIT height = HEIGHT_INIT valid_blocks = 0 - self.cal_status = log.LiveCalibrationData.Status.uncalibrated + self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated if param_put and calibration_params: try: with log.Event.from_bytes(calibration_params) as msg: - rpy_init = np.array(msg.liveCalibration.rpyCalib) - valid_blocks = msg.liveCalibration.validBlocks - wide_from_device_euler = np.array(msg.liveCalibration.wideFromDeviceEuler) - height = np.array(msg.liveCalibration.height) + rpy_init = np.array(msg.extrinsicsCalibration.rpyCalib) + valid_blocks = msg.extrinsicsCalibration.validBlocks + wide_from_device_euler = np.array(msg.extrinsicsCalibration.wideFromDeviceEuler) + height = np.array(msg.extrinsicsCalibration.height) except Exception: cloudlog.exception("Error reading cached CalibrationParams") @@ -149,22 +149,22 @@ class Calibrator: self.calib_spread = np.zeros(3) if self.valid_blocks < INPUTS_NEEDED: - if self.cal_status == log.LiveCalibrationData.Status.recalibrating: - self.cal_status = log.LiveCalibrationData.Status.recalibrating + if self.cal_status == log.ExtrinsicsCalibration.Status.recalibrating: + self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating else: - self.cal_status = log.LiveCalibrationData.Status.uncalibrated + self.cal_status = log.ExtrinsicsCalibration.Status.uncalibrated elif is_calibration_valid(self.rpy): - self.cal_status = log.LiveCalibrationData.Status.calibrated + self.cal_status = log.ExtrinsicsCalibration.Status.calibrated else: - self.cal_status = log.LiveCalibrationData.Status.invalid + self.cal_status = log.ExtrinsicsCalibration.Status.invalid # If spread is too high, assume mounting was changed and reset to last block. # Make the transition smooth. Abrupt transitions are not good for feedback loop through supercombo model. # TODO: add height spread check with smooth transition too spread_too_high = self.calib_spread[1] > MAX_ALLOWED_PITCH_SPREAD or self.calib_spread[2] > MAX_ALLOWED_YAW_SPREAD - if spread_too_high and self.cal_status == log.LiveCalibrationData.Status.calibrated: + if spread_too_high and self.cal_status == log.ExtrinsicsCalibration.Status.calibrated: self.reset(self.rpys[self.block_idx - 1], valid_blocks=1, smooth_from=self.rpy) - self.cal_status = log.LiveCalibrationData.Status.recalibrating + self.cal_status = log.ExtrinsicsCalibration.Status.recalibrating write_this_cycle = (self.idx == 0) and (self.block_idx % (INPUTS_WANTED//5) == 5) if self.param_put and write_this_cycle: @@ -234,35 +234,35 @@ class Calibrator: def get_msg(self, valid: bool) -> capnp.lib.capnp._DynamicStructBuilder: smooth_rpy = self.get_smooth_rpy() - msg = messaging.new_message('liveCalibration') + msg = messaging.new_message('extrinsicsCalibration') msg.valid = valid - liveCalibration = msg.liveCalibration - liveCalibration.validBlocks = self.valid_blocks - liveCalibration.calStatus = self.cal_status - liveCalibration.calPerc = min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100) - liveCalibration.rpyCalib = smooth_rpy.tolist() - liveCalibration.rpyCalibSpread = self.calib_spread.tolist() - liveCalibration.wideFromDeviceEuler = self.wide_from_device_euler.tolist() - liveCalibration.height = self.height.tolist() + extrinsicsCalibration = msg.extrinsicsCalibration + extrinsicsCalibration.validBlocks = self.valid_blocks + extrinsicsCalibration.calStatus = self.cal_status + extrinsicsCalibration.calPerc = min(100 * (self.valid_blocks * BLOCK_SIZE + self.idx) // (INPUTS_NEEDED * BLOCK_SIZE), 100) + extrinsicsCalibration.rpyCalib = smooth_rpy.tolist() + extrinsicsCalibration.rpyCalibSpread = self.calib_spread.tolist() + extrinsicsCalibration.wideFromDeviceEuler = self.wide_from_device_euler.tolist() + extrinsicsCalibration.height = self.height.tolist() if self.not_car: - liveCalibration.validBlocks = INPUTS_NEEDED - liveCalibration.calStatus = log.LiveCalibrationData.Status.calibrated - liveCalibration.calPerc = 100. - liveCalibration.rpyCalib = [0, 0, 0] - liveCalibration.rpyCalibSpread = self.calib_spread.tolist() + extrinsicsCalibration.validBlocks = INPUTS_NEEDED + extrinsicsCalibration.calStatus = log.ExtrinsicsCalibration.Status.calibrated + extrinsicsCalibration.calPerc = 100. + extrinsicsCalibration.rpyCalib = [0, 0, 0] + extrinsicsCalibration.rpyCalibSpread = self.calib_spread.tolist() return msg def send_data(self, pm: messaging.PubMaster, valid: bool) -> None: - pm.send('liveCalibration', self.get_msg(valid)) + pm.send('extrinsicsCalibration', self.get_msg(valid)) def main() -> NoReturn: config_realtime_process([0, 1, 2, 3], 5) - pm = messaging.PubMaster(['liveCalibration']) + pm = messaging.PubMaster(['extrinsicsCalibration']) sm = messaging.SubMaster(['cameraOdometry', 'carState'], poll='cameraOdometry') params_reader = Params() diff --git a/openpilot/selfdrive/locationd/helpers.py b/openpilot/selfdrive/locationd/helpers.py index 9ea9b6bf4f..48e1ee25c2 100644 --- a/openpilot/selfdrive/locationd/helpers.py +++ b/openpilot/selfdrive/locationd/helpers.py @@ -130,7 +130,7 @@ class Measurement: self.xyz_std: np.ndarray = xyz_std @classmethod - def from_measurement_xyz(cls, measurement: log.LivePose.XYZMeasurement) -> 'Measurement': + def from_measurement_xyz(cls, measurement: log.DeviceMotion.XYZMeasurement) -> 'Measurement': return cls( xyz=np.array([measurement.x, measurement.y, measurement.z]), xyz_std=np.array([measurement.xStd, measurement.yStd, measurement.zStd]) @@ -145,12 +145,12 @@ class Pose: self.angular_velocity = angular_velocity @classmethod - def from_live_pose(cls, live_pose: log.LivePose) -> 'Pose': + def from_device_motion(cls, device_motion: log.DeviceMotion) -> 'Pose': return Pose( - orientation=Measurement.from_measurement_xyz(live_pose.orientationNED), - velocity=Measurement.from_measurement_xyz(live_pose.velocityDevice), - acceleration=Measurement.from_measurement_xyz(live_pose.accelerationDevice), - angular_velocity=Measurement.from_measurement_xyz(live_pose.angularVelocityDevice) + orientation=Measurement.from_measurement_xyz(device_motion.orientationNED), + velocity=Measurement.from_measurement_xyz(device_motion.velocityDevice), + acceleration=Measurement.from_measurement_xyz(device_motion.accelerationDevice), + angular_velocity=Measurement.from_measurement_xyz(device_motion.angularVelocityDevice) ) @@ -178,8 +178,8 @@ class PoseCalibrator: return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib) - def feed_live_calib(self, live_calib: log.LiveCalibrationData): - calib_rpy = np.array(live_calib.rpyCalib) + def feed_extrinsics_calibration(self, extrinsics_calibration: log.ExtrinsicsCalibration): + calib_rpy = np.array(extrinsics_calibration.rpyCalib) device_from_calib = rot_from_euler(calib_rpy) self.calib_from_device = device_from_calib.T - self.calib_valid = live_calib.calStatus == log.LiveCalibrationData.Status.calibrated + self.calib_valid = extrinsics_calibration.calStatus == log.ExtrinsicsCalibration.Status.calibrated diff --git a/openpilot/selfdrive/locationd/lagd.py b/openpilot/selfdrive/locationd/lagd.py index d3c0c5195b..a198ce915f 100755 --- a/openpilot/selfdrive/locationd/lagd.py +++ b/openpilot/selfdrive/locationd/lagd.py @@ -171,7 +171,7 @@ class BlockAverage: class LateralLagEstimator: - inputs = {"carControl", "carState", "controlsState", "liveCalibration", "livePose"} + inputs = {"carControl", "carState", "controlsState", "extrinsicsCalibration", "deviceMotion"} def __init__(self, CP: car.CarParams, dt: float, block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, @@ -219,39 +219,39 @@ class LateralLagEstimator: self.block_avg = BlockAverage(self.block_count, self.block_size, valid_blocks, initial_lag) def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder: - msg = messaging.new_message('liveDelay') + msg = messaging.new_message('lateralDelay') msg.valid = valid - liveDelay = msg.liveDelay + lateralDelay = msg.lateralDelay valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get() if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): if valid_std > MAX_LAG_STD: - liveDelay.status = log.LiveDelayData.Status.invalid + lateralDelay.status = log.LateralDelay.Status.invalid else: - liveDelay.status = log.LiveDelayData.Status.estimated + lateralDelay.status = log.LateralDelay.Status.estimated else: - liveDelay.status = log.LiveDelayData.Status.unestimated + lateralDelay.status = log.LateralDelay.Status.unestimated - if liveDelay.status == log.LiveDelayData.Status.estimated: - liveDelay.lateralDelay = min(MAX_LAG, max(MIN_LAG, valid_mean_lag)) + if lateralDelay.status == log.LateralDelay.Status.estimated: + lateralDelay.lateralDelay = min(MAX_LAG, max(MIN_LAG, valid_mean_lag)) else: - liveDelay.lateralDelay = self.initial_lag + lateralDelay.lateralDelay = self.initial_lag if not np.isnan(current_mean_lag) and not np.isnan(current_std): - liveDelay.lateralDelayEstimate = current_mean_lag - liveDelay.lateralDelayEstimateStd = current_std + lateralDelay.lateralDelayEstimate = current_mean_lag + lateralDelay.lateralDelayEstimateStd = current_std else: - liveDelay.lateralDelayEstimate = self.initial_lag - liveDelay.lateralDelayEstimateStd = 0.0 + lateralDelay.lateralDelayEstimate = self.initial_lag + lateralDelay.lateralDelayEstimateStd = 0.0 - liveDelay.validBlocks = self.block_avg.valid_blocks - liveDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) // + lateralDelay.validBlocks = self.block_avg.valid_blocks + lateralDelay.calPerc = min(100 * (self.block_avg.valid_blocks * self.block_size + self.block_avg.idx) // (self.min_valid_block_count * self.block_size), 100) if debug: - liveDelay.points = self.block_avg.values.flatten().tolist() - liveDelay.version = VERSION + lateralDelay.points = self.block_avg.values.flatten().tolist() + lateralDelay.version = VERSION return msg @@ -264,11 +264,11 @@ class LateralLagEstimator: elif which == "controlsState": self.steering_saturated = getattr(msg.lateralControlState, msg.lateralControlState.which()).saturated self.desired_curvature = msg.desiredCurvature - elif which == "liveCalibration": - self.calibrator.feed_live_calib(msg) - elif which == "livePose": - device_pose = Pose.from_live_pose(msg) - calibrated_pose = self.calibrator.build_calibrated_pose(device_pose) + elif which == "extrinsicsCalibration": + self.calibrator.feed_extrinsics_calibration(msg) + elif which == "deviceMotion": + device_motion = Pose.from_device_motion(msg) + calibrated_pose = self.calibrator.build_calibrated_pose(device_motion) self.yaw_rate = calibrated_pose.angular_velocity.yaw self.yaw_rate_std = calibrated_pose.angular_velocity.yaw_std self.pose_valid = msg.angularVelocityDevice.valid and msg.posenetOK and msg.inputsOK @@ -368,13 +368,13 @@ def retrieve_initial_lag(params: Params, CP: car.CarParams): if last_lag_data is not None: try: with log.Event.from_bytes(last_lag_data) as last_lag_msg, car.CarParams.from_bytes(last_carparams_data) as last_CP: - ld = last_lag_msg.liveDelay + ld = last_lag_msg.lateralDelay if last_CP.carFingerprint != CP.carFingerprint: raise Exception("Car model mismatch") lag, valid_blocks, status, version = ld.lateralDelayEstimate, ld.validBlocks, ld.status, ld.version assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks" - assert status != log.LiveDelayData.Status.invalid, "Lag estimate is invalid" + assert status != log.LateralDelay.Status.invalid, "Lag estimate is invalid" assert version == VERSION, f"Lag estimate is from a different version (got {version}, expected {VERSION})" return lag, valid_blocks except Exception as e: @@ -389,13 +389,13 @@ def main(): DEBUG = bool(int(os.getenv("DEBUG", "0"))) - pm = messaging.PubMaster(['liveDelay']) - sm = messaging.SubMaster(['livePose', 'liveCalibration', 'carState', 'controlsState', 'carControl'], poll='livePose') + pm = messaging.PubMaster(['lateralDelay']) + sm = messaging.SubMaster(['deviceMotion', 'extrinsicsCalibration', 'carState', 'controlsState', 'carControl'], poll='deviceMotion') params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency) + lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['deviceMotion'].frequency) if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None: lag, valid_blocks = initial_lag_params lag_learner.reset(lag, valid_blocks) @@ -409,12 +409,12 @@ def main(): lag_learner.handle_log(t, which, sm[which]) lag_learner.update_points() - # 4Hz driven by livePose + # 4Hz driven by deviceMotion if sm.frame % 5 == 0: lag_learner.update_estimate() lag_msg = lag_learner.get_msg(sm.all_checks(), DEBUG) lag_msg_dat = lag_msg.to_bytes() - pm.send('liveDelay', lag_msg_dat) + pm.send('lateralDelay', lag_msg_dat) if sm.frame % 1200 == 0: # cache every 60 seconds params.put("LiveDelay", lag_msg_dat) diff --git a/openpilot/selfdrive/locationd/locationd.py b/openpilot/selfdrive/locationd/locationd.py index eb3c42fce2..9fbec991d5 100755 --- a/openpilot/selfdrive/locationd/locationd.py +++ b/openpilot/selfdrive/locationd/locationd.py @@ -148,7 +148,7 @@ class LocationEstimator: elif which == "carState": self.car_speed = abs(msg.vEgo) - elif which == "liveCalibration": + elif which == "extrinsicsCalibration": # Note that we use this message during calibration if len(msg.rpyCalib) > 0: calib = np.array(msg.rpyCalib) @@ -217,19 +217,19 @@ class LocationEstimator: angular_velocity_device, angular_velocity_device_std = state[States.ANGULAR_VELOCITY], std[States.ANGULAR_VELOCITY] acceleration_device, acceleration_device_std = state[States.ACCELERATION], std[States.ACCELERATION] - msg = messaging.new_message("livePose") + msg = messaging.new_message("deviceMotion") msg.valid = filter_valid - livePose = msg.livePose - init_xyz_measurement(livePose.orientationNED, orientation_ned, orientation_ned_std, filter_valid) - init_xyz_measurement(livePose.velocityDevice, velocity_device, velocity_device_std, filter_valid) - init_xyz_measurement(livePose.angularVelocityDevice, angular_velocity_device, angular_velocity_device_std, filter_valid) - init_xyz_measurement(livePose.accelerationDevice, acceleration_device, acceleration_device_std, filter_valid) + deviceMotion = msg.deviceMotion + init_xyz_measurement(deviceMotion.orientationNED, orientation_ned, orientation_ned_std, filter_valid) + init_xyz_measurement(deviceMotion.velocityDevice, velocity_device, velocity_device_std, filter_valid) + init_xyz_measurement(deviceMotion.angularVelocityDevice, angular_velocity_device, angular_velocity_device_std, filter_valid) + init_xyz_measurement(deviceMotion.accelerationDevice, acceleration_device, acceleration_device_std, filter_valid) if self.debug: - livePose.debugFilterState.value = state.tolist() - livePose.debugFilterState.std = std.tolist() - livePose.debugFilterState.valid = filter_valid - livePose.debugFilterState.observations = [ + deviceMotion.debugFilterState.value = state.tolist() + deviceMotion.debugFilterState.std = std.tolist() + deviceMotion.debugFilterState.valid = filter_valid + deviceMotion.debugFilterState.observations = [ {'kind': k, 'value': self.observations[k].tolist(), 'error': self.observation_errors[k].tolist()} for k in self.observations.keys() ] @@ -238,10 +238,10 @@ class LocationEstimator: new_mean = np.mean(self.posenet_stds[POSENET_STD_HIST_HALF:]) std_spike = (new_mean / old_mean) > 4.0 and new_mean > 7.0 - livePose.inputsOK = inputs_valid - livePose.posenetOK = not std_spike or self.car_speed <= 5.0 - livePose.sensorsOK = sensors_valid - livePose.timestamp = int(np.nan_to_num(self.kf.t) * 1e9) + deviceMotion.inputsOK = inputs_valid + deviceMotion.posenetOK = not std_spike or self.car_speed <= 5.0 + deviceMotion.sensorsOK = sensors_valid + deviceMotion.timestamp = int(np.nan_to_num(self.kf.t) * 1e9) return msg @@ -267,8 +267,8 @@ def main(): DEBUG = bool(int(os.getenv("DEBUG", "0"))) SIMULATION = bool(int(os.getenv("SIMULATION", "0"))) - pm = messaging.PubMaster(['livePose']) - sm = messaging.SubMaster(['carState', 'liveCalibration', 'cameraOdometry'], poll='cameraOdometry') + pm = messaging.PubMaster(['deviceMotion']) + sm = messaging.SubMaster(['carState', 'extrinsicsCalibration', 'cameraOdometry'], poll='cameraOdometry') # separate sensor sockets for efficiency sensor_sockets = [messaging.sub_sock(which, timeout=20) for which in ['accelerometer', 'gyroscope']] sensor_alive, sensor_valid, sensor_recv_time = defaultdict(bool), defaultdict(bool), defaultdict(float) @@ -288,7 +288,7 @@ def main(): initial_pose_data = params.get("LocationFilterInitialState") if initial_pose_data is not None: with log.Event.from_bytes(initial_pose_data) as lp_msg: - filter_state = lp_msg.livePose.debugFilterState + filter_state = lp_msg.deviceMotion.debugFilterState x_initial = np.array(filter_state.value, dtype=np.float64) if len(filter_state.value) != 0 else PoseKalman.initial_x P_initial = np.diag(np.array(filter_state.std, dtype=np.float64)) if len(filter_state.std) != 0 else PoseKalman.initial_P estimator.reset(None, x_initial, P_initial) @@ -333,7 +333,7 @@ def main(): sensors_valid = sensor_all_checks(acc_msgs, gyro_msgs, sensor_valid, sensor_recv_time, sensor_alive, SIMULATION) msg = estimator.get_msg(sensors_valid, inputs_valid, filter_initialized) - pm.send("livePose", msg) + pm.send("deviceMotion", msg) if __name__ == "__main__": diff --git a/openpilot/selfdrive/locationd/paramsd.py b/openpilot/selfdrive/locationd/paramsd.py index 9b5ff7143e..760176bc5c 100755 --- a/openpilot/selfdrive/locationd/paramsd.py +++ b/openpilot/selfdrive/locationd/paramsd.py @@ -65,10 +65,10 @@ class VehicleParamsLearner: self.avg_angle_offset = self.angle_offset def handle_log(self, t: float, which: str, msg: capnp._DynamicStructReader): - if which == 'livePose': + if which == 'deviceMotion': t = msg.timestamp * 1e-9 - device_pose = Pose.from_live_pose(msg) - calibrated_pose = self.calibrator.build_calibrated_pose(device_pose) + device_motion = Pose.from_device_motion(msg) + calibrated_pose = self.calibrator.build_calibrated_pose(device_motion) yaw_rate, yaw_rate_std = calibrated_pose.angular_velocity.z, calibrated_pose.angular_velocity.z_std yaw_rate_valid = msg.angularVelocityDevice.valid @@ -79,7 +79,7 @@ class VehicleParamsLearner: yaw_rate, yaw_rate_std = 0.0, np.radians(10.0) self.observed_yaw_rate = yaw_rate - localizer_roll, localizer_roll_std = device_pose.orientation.x, device_pose.orientation.x_std + localizer_roll, localizer_roll_std = device_motion.orientation.x, device_motion.orientation.x_std localizer_roll_std = np.radians(1) if np.isnan(localizer_roll_std) else localizer_roll_std roll_valid = (localizer_roll_std < ROLL_STD_MAX) and (ROLL_MIN < localizer_roll < ROLL_MAX) and msg.sensorsOK if roll_valid: @@ -113,8 +113,8 @@ class VehicleParamsLearner: self.kf.predict_and_observe(t, ObservationKind.STIFFNESS, np.array([[stiffness]])) self.kf.predict_and_observe(t, ObservationKind.STEER_RATIO, np.array([[steer_ratio]])) - elif which == 'liveCalibration': - self.calibrator.feed_live_calib(msg) + elif which == 'extrinsicsCalibration': + self.calibrator.feed_extrinsics_calibration(msg) elif which == 'carState': steering_angle = msg.steeringAngleDeg @@ -136,7 +136,7 @@ class VehicleParamsLearner: x = self.kf.x P = np.sqrt(self.kf.P.diagonal()) if not np.all(np.isfinite(x)): - cloudlog.error("NaN in liveParameters estimate. Resetting to default values") + cloudlog.error("NaN in vehicleParameters estimate. Resetting to default values") self.reset(self.kf.t) x = self.kf.x @@ -156,38 +156,38 @@ class VehicleParamsLearner: self.total_offset_valid = check_valid_with_hysteresis(self.total_offset_valid, self.angle_offset, OFFSET_MAX, OFFSET_LOWERED_MAX) self.roll_valid = check_valid_with_hysteresis(self.roll_valid, self.roll, ROLL_MAX, ROLL_LOWERED_MAX) - msg = messaging.new_message('liveParameters') + msg = messaging.new_message('vehicleParameters') msg.valid = valid - liveParameters = msg.liveParameters - liveParameters.posenetValid = True - liveParameters.sensorValid = sensors_valid - liveParameters.steerRatio = float(x[States.STEER_RATIO].item()) - liveParameters.stiffnessFactor = float(x[States.STIFFNESS].item()) - liveParameters.roll = float(self.roll) - liveParameters.angleOffsetAverageDeg = float(self.avg_angle_offset) - liveParameters.angleOffsetDeg = float(self.angle_offset) - liveParameters.steerRatioValid = self.min_sr <= liveParameters.steerRatio <= self.max_sr - liveParameters.stiffnessFactorValid = 0.2 <= liveParameters.stiffnessFactor <= 5.0 - liveParameters.angleOffsetAverageValid = bool(self.avg_offset_valid) - liveParameters.angleOffsetValid = bool(self.total_offset_valid) - liveParameters.valid = all(( - liveParameters.angleOffsetAverageValid, - liveParameters.angleOffsetValid , + vehicleParameters = msg.vehicleParameters + vehicleParameters.posenetValid = True + vehicleParameters.sensorValid = sensors_valid + vehicleParameters.steerRatio = float(x[States.STEER_RATIO].item()) + vehicleParameters.stiffnessFactor = float(x[States.STIFFNESS].item()) + vehicleParameters.roll = float(self.roll) + vehicleParameters.angleOffsetAverageDeg = float(self.avg_angle_offset) + vehicleParameters.angleOffsetDeg = float(self.angle_offset) + vehicleParameters.steerRatioValid = self.min_sr <= vehicleParameters.steerRatio <= self.max_sr + vehicleParameters.stiffnessFactorValid = 0.2 <= vehicleParameters.stiffnessFactor <= 5.0 + vehicleParameters.angleOffsetAverageValid = bool(self.avg_offset_valid) + vehicleParameters.angleOffsetValid = bool(self.total_offset_valid) + vehicleParameters.valid = all(( + vehicleParameters.angleOffsetAverageValid, + vehicleParameters.angleOffsetValid , self.roll_valid, roll_std < ROLL_STD_MAX, - liveParameters.stiffnessFactorValid, - liveParameters.steerRatioValid, + vehicleParameters.stiffnessFactorValid, + vehicleParameters.steerRatioValid, )) - liveParameters.steerRatioStd = float(P[States.STEER_RATIO].item()) - liveParameters.stiffnessFactorStd = float(P[States.STIFFNESS].item()) - liveParameters.angleOffsetAverageStd = float(P[States.ANGLE_OFFSET].item()) - liveParameters.angleOffsetFastStd = float(P[States.ANGLE_OFFSET_FAST].item()) + vehicleParameters.steerRatioStd = float(P[States.STEER_RATIO].item()) + vehicleParameters.stiffnessFactorStd = float(P[States.STIFFNESS].item()) + vehicleParameters.angleOffsetAverageStd = float(P[States.ANGLE_OFFSET].item()) + vehicleParameters.angleOffsetFastStd = float(P[States.ANGLE_OFFSET_FAST].item()) if debug: - liveParameters.debugFilterState = log.LiveParametersData.FilterState.new_message() - liveParameters.debugFilterState.value = x.tolist() - liveParameters.debugFilterState.std = P.tolist() + vehicleParameters.debugFilterState = log.VehicleParameters.FilterState.new_message() + vehicleParameters.debugFilterState.value = x.tolist() + vehicleParameters.debugFilterState.std = P.tolist() return msg @@ -210,7 +210,7 @@ def retrieve_initial_vehicle_params(params: Params, CP: car.CarParams, replay: b if last_parameters_data is not None and last_carparams_data is not None: try: with log.Event.from_bytes(last_parameters_data) as last_lp_msg, car.CarParams.from_bytes(last_carparams_data) as last_CP: - lp = last_lp_msg.liveParameters + lp = last_lp_msg.vehicleParameters # Check if car model matches if last_CP.carFingerprint != CP.carFingerprint: raise Exception("Car model mismatch") @@ -248,8 +248,8 @@ def main(): DEBUG = bool(int(os.getenv("DEBUG", "0"))) REPLAY = bool(int(os.getenv("REPLAY", "0"))) - pm = messaging.PubMaster(['liveParameters']) - sm = messaging.SubMaster(['livePose', 'liveCalibration', 'carState'], poll='livePose') + pm = messaging.PubMaster(['vehicleParameters']) + sm = messaging.SubMaster(['deviceMotion', 'extrinsicsCalibration', 'carState'], poll='deviceMotion') params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) @@ -265,14 +265,14 @@ def main(): t = sm.logMonoTime[which] * 1e-9 learner.handle_log(t, which, sm[which]) - if sm.updated['livePose']: + if sm.updated['deviceMotion']: msg = learner.get_msg(sm.all_checks(), debug=DEBUG) msg_dat = msg.to_bytes() if sm.frame % 1200 == 0: # once a minute params.put("LiveParametersV2", msg_dat) - pm.send('liveParameters', msg_dat) + pm.send('vehicleParameters', msg_dat) if __name__ == "__main__": diff --git a/openpilot/selfdrive/locationd/test/test_calibrationd.py b/openpilot/selfdrive/locationd/test/test_calibrationd.py index b3722e344c..7afd43b35b 100644 --- a/openpilot/selfdrive/locationd/test/test_calibrationd.py +++ b/openpilot/selfdrive/locationd/test/test_calibrationd.py @@ -33,16 +33,16 @@ def process_messages(c, cam_odo_calib, cycles, class TestCalibrationd(OpenpilotTestCase): def test_read_saved_params(self): - msg = messaging.new_message('liveCalibration') - msg.liveCalibration.validBlocks = random.randint(1, 10) - msg.liveCalibration.rpyCalib = [random.random() for _ in range(3)] - msg.liveCalibration.height = [random.random() for _ in range(1)] + msg = messaging.new_message('extrinsicsCalibration') + msg.extrinsicsCalibration.validBlocks = random.randint(1, 10) + msg.extrinsicsCalibration.rpyCalib = [random.random() for _ in range(3)] + msg.extrinsicsCalibration.height = [random.random() for _ in range(1)] Params().put("CalibrationParams", msg.to_bytes(), block=True) c = Calibrator(param_put=True) - np.testing.assert_allclose(msg.liveCalibration.rpyCalib, c.rpy) - np.testing.assert_allclose(msg.liveCalibration.height, c.height) - assert msg.liveCalibration.validBlocks == c.valid_blocks + np.testing.assert_allclose(msg.extrinsicsCalibration.rpyCalib, c.rpy) + np.testing.assert_allclose(msg.extrinsicsCalibration.height, c.height) + assert msg.extrinsicsCalibration.validBlocks == c.valid_blocks def test_calibration_basics(self): @@ -92,7 +92,7 @@ class TestCalibrationd(OpenpilotTestCase): np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0], atol=1e-3) process_messages(c, [0.0, MAX_ALLOWED_PITCH_SPREAD*0.9, MAX_ALLOWED_YAW_SPREAD*0.9], BLOCK_SIZE + 10) assert c.valid_blocks == INPUTS_NEEDED + 1 - assert c.cal_status == log.LiveCalibrationData.Status.calibrated + assert c.cal_status == log.ExtrinsicsCalibration.Status.calibrated c = Calibrator(param_put=False) process_messages(c, [0.0, 0.0, 0.0], BLOCK_SIZE * INPUTS_NEEDED) @@ -100,7 +100,7 @@ class TestCalibrationd(OpenpilotTestCase): np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0]) process_messages(c, [0.0, MAX_ALLOWED_PITCH_SPREAD*1.1, 0.0], BLOCK_SIZE + 10) assert c.valid_blocks == 1 - assert c.cal_status == log.LiveCalibrationData.Status.recalibrating + assert c.cal_status == log.ExtrinsicsCalibration.Status.recalibrating np.testing.assert_allclose(c.rpy, [0.0, MAX_ALLOWED_PITCH_SPREAD*1.1, 0.0], atol=1e-2) c = Calibrator(param_put=False) @@ -109,5 +109,5 @@ class TestCalibrationd(OpenpilotTestCase): np.testing.assert_allclose(c.rpy, [0.0, 0.0, 0.0]) process_messages(c, [0.0, 0.0, MAX_ALLOWED_YAW_SPREAD*1.1], BLOCK_SIZE + 10) assert c.valid_blocks == 1 - assert c.cal_status == log.LiveCalibrationData.Status.recalibrating + assert c.cal_status == log.ExtrinsicsCalibration.Status.recalibrating np.testing.assert_allclose(c.rpy, [0.0, 0.0, MAX_ALLOWED_YAW_SPREAD*1.1], atol=1e-2) diff --git a/openpilot/selfdrive/locationd/test/test_lagd.py b/openpilot/selfdrive/locationd/test/test_lagd.py index d6006c6cf9..0acc3e646f 100644 --- a/openpilot/selfdrive/locationd/test/test_lagd.py +++ b/openpilot/selfdrive/locationd/test/test_lagd.py @@ -43,9 +43,9 @@ def process_messages(estimator, lag_frames, n_frames, vego=25.0, rejection_thres (t, "carControl", car.CarControl(latActive=not rejected)), (t, "carState", car.CarState(vEgo=vego, steeringPressed=False)), (t, "controlsState", log.ControlsState(desiredCurvature=desired_cuvature)), - (t, "livePose", log.LivePose(angularVelocityDevice=log.LivePose.XYZMeasurement(z=actual_yr, valid=True), + (t, "deviceMotion", log.DeviceMotion(angularVelocityDevice=log.DeviceMotion.XYZMeasurement(z=actual_yr, valid=True), posenetOK=True, inputsOK=True)), - (t, "liveCalibration", log.LiveCalibrationData(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)), + (t, "extrinsicsCalibration", log.ExtrinsicsCalibration(rpyCalib=[0, 0, 0], calStatus=log.ExtrinsicsCalibration.Status.calibrated)), ] for t, w, m in msgs: estimator.handle_log(t, w, m) @@ -59,10 +59,10 @@ class TestLagd(OpenpilotTestCase): CP = get_test_car_params() - msg = messaging.new_message('liveDelay') - msg.liveDelay.lateralDelayEstimate = random.random() - msg.liveDelay.validBlocks = random.randint(1, 10) - msg.liveDelay.version = VERSION + msg = messaging.new_message('lateralDelay') + msg.lateralDelay.lateralDelayEstimate = random.random() + msg.lateralDelay.validBlocks = random.randint(1, 10) + msg.lateralDelay.version = VERSION params.put("LiveDelay", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) @@ -70,8 +70,8 @@ class TestLagd(OpenpilotTestCase): assert saved_lag_params is not None lag, valid_blocks = saved_lag_params - assert lag == msg.liveDelay.lateralDelayEstimate - assert valid_blocks == msg.liveDelay.validBlocks + assert lag == msg.lateralDelay.lateralDelayEstimate + assert valid_blocks == msg.lateralDelay.validBlocks def test_read_invalid_saved_params(self, subtests): params = Params() @@ -79,9 +79,9 @@ class TestLagd(OpenpilotTestCase): CP = get_test_car_params() for msg_dict in [{'version': 0}, {'status': 'invalid'}, {'validBlocks': 100}]: - with subtests.test(msg=f"liveDelay={msg_dict}"): - msg = messaging.new_message('liveDelay') - msg.liveDelay = msg_dict + with subtests.test(msg=f"lateralDelay={msg_dict}"): + msg = messaging.new_message('lateralDelay') + msg.lateralDelay = msg_dict params.put("LiveDelay", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) assert retrieve_initial_lag(params, CP) is None @@ -114,11 +114,11 @@ class TestLagd(OpenpilotTestCase): mocked_CP = car.CarParams(steerActuatorDelay=0.5) estimator = LateralLagEstimator(mocked_CP, DT) msg = estimator.get_msg(True) - assert msg.liveDelay.status == 'unestimated' - assert np.allclose(msg.liveDelay.lateralDelay, estimator.initial_lag) - assert np.allclose(msg.liveDelay.lateralDelayEstimate, estimator.initial_lag) - assert msg.liveDelay.validBlocks == 0 - assert msg.liveDelay.calPerc == 0 + assert msg.lateralDelay.status == 'unestimated' + assert np.allclose(msg.lateralDelay.lateralDelay, estimator.initial_lag) + assert np.allclose(msg.lateralDelay.lateralDelayEstimate, estimator.initial_lag) + assert msg.lateralDelay.validBlocks == 0 + assert msg.lateralDelay.calPerc == 0 def test_estimator_basics(self, subtests): for lag_frames in range(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1): @@ -127,21 +127,21 @@ class TestLagd(OpenpilotTestCase): estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0) process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) msg = estimator.get_msg(True) - assert msg.liveDelay.status == 'estimated' - assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) - assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED - assert msg.liveDelay.calPerc == 100 + assert msg.lateralDelay.status == 'estimated' + assert np.allclose(msg.lateralDelay.lateralDelay, lag_frames * DT, atol=0.01) + assert np.allclose(msg.lateralDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) + assert np.allclose(msg.lateralDelay.lateralDelayEstimateStd, 0.0, atol=0.01) + assert msg.lateralDelay.validBlocks == BLOCK_NUM_NEEDED + assert msg.lateralDelay.calPerc == 100 def test_estimator_masking(self): mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.5), random.randint(LAGD_MIN_LAG_FRAMES, LAGD_MAX_LAG_FRAMES - 1) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) process_messages(estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) msg = estimator.get_msg(True) - assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) - assert msg.liveDelay.calPerc == 100 + assert np.allclose(msg.lateralDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) + assert np.allclose(msg.lateralDelay.lateralDelayEstimateStd, 0.0, atol=0.01) + assert msg.lateralDelay.calPerc == 100 @unittest.skipIf(PC, "only on device") def test_estimator_performance(self): diff --git a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py index c5fe1908bc..01fc3f1177 100644 --- a/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py +++ b/openpilot/selfdrive/locationd/test/test_locationd_scenarios.py @@ -45,7 +45,7 @@ def get_select_fields_data(logs): for key in keys: val = getattr(val, key) if isinstance(key, str) else val[key] return val - lp = [x.livePose for x in logs if x.which() == 'livePose'] + lp = [x.deviceMotion for x in logs if x.which() == 'deviceMotion'] data = defaultdict(list) for msg in lp: for key, fields in SELECT_COMPARE_FIELDS.items(): diff --git a/openpilot/selfdrive/locationd/test/test_paramsd.py b/openpilot/selfdrive/locationd/test/test_paramsd.py index 09a067f4cf..515c7a814a 100644 --- a/openpilot/selfdrive/locationd/test/test_paramsd.py +++ b/openpilot/selfdrive/locationd/test/test_paramsd.py @@ -11,12 +11,12 @@ from openpilot.common.params import Params from openpilot.tools.lib.logreader import LogReader -def get_random_live_parameters(CP): - msg = messaging.new_message("liveParameters") - msg.liveParameters.steerRatio = (random.random() + 0.5) * CP.steerRatio - msg.liveParameters.stiffnessFactor = random.random() - msg.liveParameters.angleOffsetAverageDeg = random.random() - msg.liveParameters.debugFilterState.std = [random.random() for _ in range(CarKalman.P_initial.shape[0])] +def get_random_vehicle_parameters(CP): + msg = messaging.new_message("vehicleParameters") + msg.vehicleParameters.steerRatio = (random.random() + 0.5) * CP.steerRatio + msg.vehicleParameters.stiffnessFactor = random.random() + msg.vehicleParameters.angleOffsetAverageDeg = random.random() + msg.vehicleParameters.debugFilterState.std = [random.random() for _ in range(CarKalman.P_initial.shape[0])] return msg @@ -27,13 +27,13 @@ class TestParamsd(OpenpilotTestCase): lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams]) CP = next(m for m in lr if m.which() == "carParams").carParams - msg = get_random_live_parameters(CP) + msg = get_random_vehicle_parameters(CP) params.put("LiveParametersV2", msg.to_bytes(), block=True) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True) sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True) - np.testing.assert_allclose(sr, msg.liveParameters.steerRatio) - np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor) - np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg) + np.testing.assert_allclose(sr, msg.vehicleParameters.steerRatio) + np.testing.assert_allclose(sf, msg.vehicleParameters.stiffnessFactor) + np.testing.assert_allclose(offset, msg.vehicleParameters.angleOffsetAverageDeg) np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape) - np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std) + np.testing.assert_allclose(np.diagonal(p_init), msg.vehicleParameters.debugFilterState.std) diff --git a/openpilot/selfdrive/locationd/test/test_torqued.py b/openpilot/selfdrive/locationd/test/test_torqued.py index af3aeb95d6..3c5cb29bfc 100644 --- a/openpilot/selfdrive/locationd/test/test_torqued.py +++ b/openpilot/selfdrive/locationd/test/test_torqued.py @@ -7,7 +7,7 @@ class TestTorqued(OpenpilotTestCase): def test_cal_percent(self): est = TorqueEstimator(car.CarParams()) msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 0 + assert msg.lateralTorqueParameters.calPerc == 0 for (low, high), min_pts in zip(est.filtered_points.buckets.keys(), est.filtered_points.buckets_min_points.values(), strict=True): @@ -16,7 +16,7 @@ class TestTorqued(OpenpilotTestCase): # enough bucket points, but not enough total points msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 + assert msg.lateralTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2 # add enough points to bucket with most capacity key = list(est.filtered_points.buckets)[0] @@ -24,4 +24,4 @@ class TestTorqued(OpenpilotTestCase): est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0) msg = est.get_msg() - assert msg.liveTorqueParameters.calPerc == 100 + assert msg.lateralTorqueParameters.calPerc == 100 diff --git a/openpilot/selfdrive/locationd/torqued.py b/openpilot/selfdrive/locationd/torqued.py index d36684563c..21b2af7f12 100755 --- a/openpilot/selfdrive/locationd/torqued.py +++ b/openpilot/selfdrive/locationd/torqued.py @@ -102,11 +102,11 @@ class TorqueEstimator(ParameterEstimator): if params_cache is not None and torque_cache is not None: try: with log.Event.from_bytes(torque_cache) as log_evt: - cache_ltp = log_evt.liveTorqueParameters + cache_ltp = log_evt.lateralTorqueParameters with car.CarParams.from_bytes(params_cache) as msg: cache_CP = msg if self.get_restore_key(cache_CP, cache_ltp.version) == self.get_restore_key(CP, VERSION): - if cache_ltp.liveValid: + if cache_ltp.valid: initial_params = { 'latAccelFactor': cache_ltp.latAccelFactorFiltered, 'latAccelOffset': cache_ltp.latAccelOffsetFiltered, @@ -154,7 +154,7 @@ class TorqueEstimator(ParameterEstimator): _, spread = np.matmul(points[:, [0, 2]], slope2rot(slope)).T friction_coeff = np.std(spread) * FRICTION_FACTOR except np.linalg.LinAlgError as e: - cloudlog.exception(f"Error computing live torque params: {e}") + cloudlog.exception(f"Error computing lateral torque parameters: {e}") slope = offset = friction_coeff = np.nan return slope, offset, friction_coeff @@ -176,21 +176,21 @@ class TorqueEstimator(ParameterEstimator): # TODO: check if high aEgo affects resulting lateral accel self.raw_points["vego"].append(msg.vEgo) self.raw_points["steer_override"].append(msg.steeringPressed) - elif which == "liveCalibration": - self.calibrator.feed_live_calib(msg) - elif which == "liveDelay": + elif which == "extrinsicsCalibration": + self.calibrator.feed_extrinsics_calibration(msg) + elif which == "lateralDelay": self.lag = msg.lateralDelay # calculate lateral accel from past steering torque - elif which == "livePose": + elif which == "deviceMotion": is_valid = msg.angularVelocityDevice.valid and msg.orientationNED.valid and msg.inputsOK and msg.sensorsOK and msg.posenetOK if len(self.raw_points['steer_torque']) == self.hist_len and is_valid: t = msg.timestamp * 1e-9 - device_pose = Pose.from_live_pose(msg) - calibrated_pose = self.calibrator.build_calibrated_pose(device_pose) + device_motion = Pose.from_device_motion(msg) + calibrated_pose = self.calibrator.build_calibrated_pose(device_motion) angular_velocity_calibrated = calibrated_pose.angular_velocity yaw_rate = angular_velocity_calibrated.yaw - roll = device_pose.orientation.roll + roll = device_motion.orientation.roll # check lat active up to now (without lag compensation) lat_active = np.interp(np.arange(t - MIN_ENGAGE_BUFFER, t + self.lag, DT_MDL), self.raw_points['carControl_t'], self.raw_points['lat_active']).astype(bool) @@ -207,40 +207,40 @@ class TorqueEstimator(ParameterEstimator): self.all_torque_points.append([steer, lateral_acc]) def get_msg(self, valid=True, with_points=False): - msg = messaging.new_message('liveTorqueParameters') + msg = messaging.new_message('lateralTorqueParameters') msg.valid = valid - liveTorqueParameters = msg.liveTorqueParameters - liveTorqueParameters.version = VERSION - liveTorqueParameters.useParams = self.use_params + lateralTorqueParameters = msg.lateralTorqueParameters + lateralTorqueParameters.version = VERSION + lateralTorqueParameters.useParams = self.use_params # Calculate raw estimates when possible, only update filters when enough points are gathered if self.filtered_points.is_calculable(): latAccelFactor, latAccelOffset, frictionCoeff = self.estimate_params() - liveTorqueParameters.latAccelFactorRaw = float(latAccelFactor) - liveTorqueParameters.latAccelOffsetRaw = float(latAccelOffset) - liveTorqueParameters.frictionCoefficientRaw = float(frictionCoeff) + lateralTorqueParameters.latAccelFactorRaw = float(latAccelFactor) + lateralTorqueParameters.latAccelOffsetRaw = float(latAccelOffset) + lateralTorqueParameters.frictionCoefficientRaw = float(frictionCoeff) if self.filtered_points.is_valid(): if any(val is None or np.isnan(val) for val in [latAccelFactor, latAccelOffset, frictionCoeff]): - cloudlog.exception("Live torque parameters are invalid.") - liveTorqueParameters.liveValid = False + cloudlog.exception("Lateral torque parameters are invalid.") + lateralTorqueParameters.valid = False self.reset() else: - liveTorqueParameters.liveValid = True + lateralTorqueParameters.valid = True latAccelFactor = np.clip(latAccelFactor, self.min_lataccel_factor, self.max_lataccel_factor) frictionCoeff = np.clip(frictionCoeff, self.min_friction, self.max_friction) self.update_params({'latAccelFactor': latAccelFactor, 'latAccelOffset': latAccelOffset, 'frictionCoefficient': frictionCoeff}) if with_points: - liveTorqueParameters.points = self.filtered_points.get_points()[:, [0, 2]].tolist() + lateralTorqueParameters.points = self.filtered_points.get_points()[:, [0, 2]].tolist() - liveTorqueParameters.latAccelFactorFiltered = float(self.filtered_params['latAccelFactor'].x) - liveTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x) - liveTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x) - liveTorqueParameters.totalBucketPoints = len(self.filtered_points) - liveTorqueParameters.calPerc = self.filtered_points.get_valid_percent() - liveTorqueParameters.decay = self.decay - liveTorqueParameters.maxResets = self.resets + lateralTorqueParameters.latAccelFactorFiltered = float(self.filtered_params['latAccelFactor'].x) + lateralTorqueParameters.latAccelOffsetFiltered = float(self.filtered_params['latAccelOffset'].x) + lateralTorqueParameters.frictionCoefficientFiltered = float(self.filtered_params['frictionCoefficient'].x) + lateralTorqueParameters.totalBucketPoints = len(self.filtered_points) + lateralTorqueParameters.calPerc = self.filtered_points.get_valid_percent() + lateralTorqueParameters.decay = self.decay + lateralTorqueParameters.maxResets = self.resets return msg @@ -249,8 +249,8 @@ def main(demo=False): DEBUG = bool(int(os.getenv("DEBUG", "0"))) - pm = messaging.PubMaster(['liveTorqueParameters']) - sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'liveCalibration', 'livePose', 'liveDelay'], poll='livePose') + pm = messaging.PubMaster(['lateralTorqueParameters']) + sm = messaging.SubMaster(['carControl', 'carOutput', 'carState', 'extrinsicsCalibration', 'deviceMotion', 'lateralDelay'], poll='deviceMotion') params = Params() estimator = TorqueEstimator(messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)) @@ -263,9 +263,9 @@ def main(demo=False): t = sm.logMonoTime[which] * 1e-9 estimator.handle_log(t, which, sm[which]) - # 4Hz driven by livePose + # 4Hz driven by deviceMotion if sm.frame % 5 == 0: - pm.send('liveTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG)) + pm.send('lateralTorqueParameters', estimator.get_msg(valid=sm.all_checks(), with_points=DEBUG)) # Cache points every 60 seconds while onroad if sm.frame % 240 == 0: diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 02391aeee3..67161c35bd 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -120,7 +120,7 @@ def main(): model = ModelState(vipc_client.width, vipc_client.height) cloudlog.warning("models loaded, dmonitoringmodeld starting") - sm = SubMaster(["liveCalibration"]) + sm = SubMaster(["extrinsicsCalibration"]) pm = PubMaster(["driverStateV2"]) calib = np.zeros(model.numpy_inputs['calib'].size, dtype=np.float32) @@ -136,8 +136,8 @@ def main(): model_transform = np.linalg.inv(np.dot(dmonitoringmodel_intrinsics, np.linalg.inv(cam.intrinsics))).astype(np.float32) sm.update(0) - if sm.updated["liveCalibration"]: - calib[:] = np.array(sm["liveCalibration"].rpyCalib) + if sm.updated["extrinsicsCalibration"]: + calib[:] = np.array(sm["extrinsicsCalibration"].rpyCalib) t1 = time.perf_counter() model_output, gpu_execution_time = model.run(buf, calib, model_transform) diff --git a/openpilot/selfdrive/modeld/fill_model_msg.py b/openpilot/selfdrive/modeld/fill_model_msg.py index d926ace3da..558f881b37 100644 --- a/openpilot/selfdrive/modeld/fill_model_msg.py +++ b/openpilot/selfdrive/modeld/fill_model_msg.py @@ -174,8 +174,8 @@ def fill_model_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, modelV2.rawPredictions = net_output_data['raw_pred'].tobytes() def fill_pose_msg(msg: capnp._DynamicStructBuilder, net_output_data: dict[str, np.ndarray], - vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, live_calib_seen: bool) -> None: - msg.valid = live_calib_seen & (vipc_dropped_frames < 1) + vipc_frame_id: int, vipc_dropped_frames: int, timestamp_eof: int, extrinsics_calibration_seen: bool) -> None: + msg.valid = extrinsics_calibration_seen & (vipc_dropped_frames < 1) cameraOdometry = msg.cameraOdometry cameraOdometry.frameId = vipc_frame_id diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index f724e530e2..e87d4f3111 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -262,7 +262,7 @@ def main(demo=False): # messaging pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if USBGPU else []) pm = PubMaster(pub_socks) - sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) + sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() @@ -276,7 +276,7 @@ def main(demo=False): model_transform_main = np.zeros((3, 3), dtype=np.float32) model_transform_extra = np.zeros((3, 3), dtype=np.float32) - live_calib_seen = False + extrinsics_calibration_seen = False buf_main, buf_extra = None, None meta_main = FrameMeta() meta_extra = FrameMeta() @@ -332,16 +332,16 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["narrowRoadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS - if sm.updated["liveCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: - device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) + lat_delay = sm["lateralDelay"].lateralDelay + LAT_SMOOTH_SECONDS + if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: + device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] main_intrinsics = dc.wide_road.intrinsics if main_wide_camera else dc.narrow_road.intrinsics model_transform_main = get_warp_matrix(device_from_calib_euler, main_intrinsics, False).astype(np.float32) has_wide_camera = use_extra_client or main_wide_camera extra_intrinsics = dc.wide_road.intrinsics if has_wide_camera else dc.narrow_road.intrinsics model_transform_extra = get_warp_matrix(device_from_calib_euler, extra_intrinsics, True).astype(np.float32) - live_calib_seen = True + extrinsics_calibration_seen = True traffic_convention = np.zeros(2) traffic_convention[int(is_rhd)] = 1 @@ -396,7 +396,7 @@ def main(demo=False): prev_action = action fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, - frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen) + frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) modelv2_send.modelV2.big = model.usbgpu desire_state = modelv2_send.modelV2.meta.desireState @@ -408,7 +408,7 @@ def main(demo=False): modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction fill_driving_model_data(drivingdata_send, modelv2_send) - fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen) + fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, extrinsics_calibration_seen) pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) pm.send('cameraOdometry', posenet_send) diff --git a/openpilot/selfdrive/monitoring/dmonitoringd.py b/openpilot/selfdrive/monitoring/dmonitoringd.py index a70659cb06..e686266fe8 100755 --- a/openpilot/selfdrive/monitoring/dmonitoringd.py +++ b/openpilot/selfdrive/monitoring/dmonitoringd.py @@ -10,7 +10,7 @@ def dmonitoringd_thread(): params = Params() pm = messaging.PubMaster(['driverMonitoringState']) - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') + sm = messaging.SubMaster(['driverStateV2', 'extrinsicsCalibration', 'carState', 'selfdriveState', 'modelV2'], poll='driverStateV2') DM = DriverMonitoring(rhd_saved=params.get_bool("IsRhdDetected"), always_on=params.get_bool("AlwaysOnDM")) demo_mode=False diff --git a/openpilot/selfdrive/monitoring/policy.py b/openpilot/selfdrive/monitoring/policy.py index b58f1e66dc..06aca55407 100644 --- a/openpilot/selfdrive/monitoring/policy.py +++ b/openpilot/selfdrive/monitoring/policy.py @@ -441,7 +441,7 @@ class DriverMonitoring: driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s steering_angle_deg = sm['carState'].steeringAngleDeg - rpyCalib = sm['liveCalibration'].rpyCalib + rpyCalib = sm['extrinsicsCalibration'].rpyCalib self._set_pose_strictness( brake_disengage_prob=brake_disengage_prob, diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 2127cdeceb..1482a4e334 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -255,9 +255,9 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - first_word = 'Recalibrating' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibrating' + first_word = 'Recalibrating' if sm['extrinsicsCalibration'].calStatus == log.ExtrinsicsCalibration.Status.recalibrating else 'Calibrating' return Alert( - f"{first_word}: {sm['liveCalibration'].calPerc:.0f}%", + f"{first_word}: {sm['extrinsicsCalibration'].calPerc:.0f}%", f"Drive Above {get_display_speed(MIN_SPEED_FILTER, metric)}", AlertStatus.normal, AlertSize.mid, Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) @@ -303,7 +303,7 @@ def camera_malfunction_alert(CP: car.CarParams, CS: car.CarState, sm: messaging. def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - rpy = sm['liveCalibration'].rpyCalib + rpy = sm['extrinsicsCalibration'].rpyCalib yaw = math.degrees(rpy[2] if len(rpy) == 3 else math.nan) pitch = math.degrees(rpy[1] if len(rpy) == 3 else math.nan) angles = f"Remount Device (Pitch: {pitch:.1f}°, Yaw: {yaw:.1f}°)" @@ -311,16 +311,16 @@ def calibration_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging def paramsd_invalid_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - if not sm['liveParameters'].angleOffsetValid: - angle_offset_deg = sm['liveParameters'].angleOffsetDeg + if not sm['vehicleParameters'].angleOffsetValid: + angle_offset_deg = sm['vehicleParameters'].angleOffsetDeg title = "Steering misalignment detected" text = f"Angle offset too high (Offset: {angle_offset_deg:.1f}°)" - elif not sm['liveParameters'].steerRatioValid: - steer_ratio = sm['liveParameters'].steerRatio + elif not sm['vehicleParameters'].steerRatioValid: + steer_ratio = sm['vehicleParameters'].steerRatio title = "Steer ratio mismatch" text = f"Steering rack geometry may be off (Ratio: {steer_ratio:.1f})" - elif not sm['liveParameters'].stiffnessFactorValid: - stiffness_factor = sm['liveParameters'].stiffnessFactor + elif not sm['vehicleParameters'].stiffnessFactorValid: + stiffness_factor = sm['vehicleParameters'].stiffnessFactor title = "Abnormal tire stiffness" text = f"Check tires, pressure, or alignment (Factor: {stiffness_factor:.1f})" else: diff --git a/openpilot/selfdrive/selfdrived/helpers.py b/openpilot/selfdrive/selfdrived/helpers.py index 3b3c44ecaf..e7dca0f9d5 100644 --- a/openpilot/selfdrive/selfdrived/helpers.py +++ b/openpilot/selfdrive/selfdrived/helpers.py @@ -31,7 +31,7 @@ class ExcessiveActuationCheck: # lateral yaw_rate = calibrated_pose.angular_velocity.yaw - roll = sm['liveParameters'].roll + roll = sm['vehicleParameters'].roll roll_compensated_lateral_accel = (CS.vEgo * yaw_rate) - (math.sin(roll) * ACCELERATION_DUE_TO_GRAVITY) # Prevent false positives after overriding @@ -41,9 +41,9 @@ class ExcessiveActuationCheck: if abs(roll_compensated_lateral_accel) > ISO_LATERAL_ACCEL * 2: excessive_lat_actuation = True - # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements - livepose_valid = abs(CS.aEgo - accel_calibrated) < 2 - self._excessive_counter = self._excessive_counter + 1 if livepose_valid and (excessive_long_actuation or excessive_lat_actuation) else 0 + # deviceMotion acceleration can be noisy due to bad mounting or aliased deviceMotion measurements + device_motion_valid = abs(CS.aEgo - accel_calibrated) < 2 + self._excessive_counter = self._excessive_counter + 1 if device_motion_valid and (excessive_long_actuation or excessive_lat_actuation) else 0 excessive_type = None if self._excessive_counter > MIN_EXCESSIVE_ACTUATION_COUNT: diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 59c5f2695e..73f382ac3c 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -88,9 +88,9 @@ class SelfdriveD: if REPLAY: # no vipc in replay will make them ignored anyways ignore += ['narrowRoadCameraState', 'wideRoadCameraState'] - self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', - 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', - 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', + self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'extrinsicsCalibration', + 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'deviceMotion', 'lateralDelay', + 'managerState', 'vehicleParameters', 'radarState', 'lateralTorqueParameters', 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'lateralManeuverPlan'] + \ self.camera_packets + self.sensor_packets + self.gps_packets, @@ -270,11 +270,11 @@ class SelfdriveD: self.last_functional_fan_frame = self.sm.frame # Handle calibration status - cal_status = self.sm['liveCalibration'].calStatus - if cal_status != log.LiveCalibrationData.Status.calibrated: - if cal_status == log.LiveCalibrationData.Status.uncalibrated: + cal_status = self.sm['extrinsicsCalibration'].calStatus + if cal_status != log.ExtrinsicsCalibration.Status.calibrated: + if cal_status == log.ExtrinsicsCalibration.Status.uncalibrated: self.events.add(EventName.calibrationIncomplete) - elif cal_status == log.LiveCalibrationData.Status.recalibrating: + elif cal_status == log.ExtrinsicsCalibration.Status.recalibrating: if not self.recalibrating_seen: set_offroad_alert("Offroad_Recalibration", True) self.recalibrating_seen = True @@ -291,11 +291,11 @@ class SelfdriveD: # NOTE: To fork maintainers. # Disabling or nerfing safety features will get you and your users banned from our servers. # We recommend that you do not change these numbers from the defaults. - if self.sm.updated['liveCalibration']: - self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) - if self.sm.updated['livePose']: - device_pose = Pose.from_live_pose(self.sm['livePose']) - self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) + if self.sm.updated['extrinsicsCalibration']: + self.pose_calibrator.feed_extrinsics_calibration(self.sm['extrinsicsCalibration']) + if self.sm.updated['deviceMotion']: + device_motion = Pose.from_device_motion(self.sm['deviceMotion']) + self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_motion) if self.calibrated_pose is not None: excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) @@ -395,11 +395,12 @@ class SelfdriveD: self.logged_comm_issue = None if not self.CP.notCar and not big_model_settling: # localization has nothing to work with during the load - if not self.sm['livePose'].posenetOK: + if not self.sm['deviceMotion'].posenetOK: self.events.add(EventName.posenetInvalid) - if not self.sm['livePose'].inputsOK: + if not self.sm['deviceMotion'].inputsOK: self.events.add(EventName.locationdTemporaryError) - if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY): + if (not self.sm['vehicleParameters'].valid and cal_status == log.ExtrinsicsCalibration.Status.calibrated and + not TESTING_CLOSET and (not SIMULATION or REPLAY)): self.events.add(EventName.paramsdTemporaryError) # conservative HW alert. if the data or frequency are off, locationd will throw an error @@ -438,7 +439,7 @@ class SelfdriveD: # GPS checks gps_ok = self.sm.recv_frame[self.gps_location_service] > 0 and (self.sm.frame - self.sm.recv_frame[self.gps_location_service]) * DT_CTRL < 2.0 - if not gps_ok and self.sm['livePose'].inputsOK and (self.distance_traveled > 1500): + if not gps_ok and self.sm['deviceMotion'].inputsOK and (self.distance_traveled > 1500): self.events.add(EventName.noGps) if gps_ok: self.distance_traveled = 0 diff --git a/openpilot/selfdrive/test/helpers.py b/openpilot/selfdrive/test/helpers.py index 2bcb6d8409..71cdcee5e7 100644 --- a/openpilot/selfdrive/test/helpers.py +++ b/openpilot/selfdrive/test/helpers.py @@ -22,9 +22,9 @@ def set_params_enabled(): params.put_bool("OpenpilotEnabledToggle", True, block=True) # valid calib - msg = messaging.new_message('liveCalibration') - msg.liveCalibration.validBlocks = 20 - msg.liveCalibration.rpyCalib = [0.0, 0.0, 0.0] + msg = messaging.new_message('extrinsicsCalibration') + msg.extrinsicsCalibration.validBlocks = 20 + msg.extrinsicsCalibration.rpyCalib = [0.0, 0.0, 0.0] params.put("CalibrationParams", msg.to_bytes(), block=True) def release_only(f): diff --git a/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py index bdd5c51ee4..036aaa2ed3 100755 --- a/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/openpilot/selfdrive/test/longitudinal_maneuvers/plant.py @@ -64,7 +64,7 @@ class Plant: control = messaging.new_message('controlsState') ss = messaging.new_message('selfdriveState') car_state = messaging.new_message('carState') - lp = messaging.new_message('liveParameters') + lp = messaging.new_message('vehicleParameters') car_control = messaging.new_message('carControl') model = messaging.new_message('modelV2') a_lead = (v_lead - self.v_lead_prev)/self.ts @@ -132,7 +132,7 @@ class Plant: 'carControl': car_control.carControl, 'controlsState': control.controlsState, 'selfdriveState': ss.selfdriveState, - 'liveParameters': lp.liveParameters, + 'vehicleParameters': lp.vehicleParameters, 'modelV2': model.modelV2} self.planner.update(sm) self.acceleration = self.planner.output_a_target diff --git a/openpilot/selfdrive/test/process_replay/README.md b/openpilot/selfdrive/test/process_replay/README.md index f39333a3c6..36d669dda6 100644 --- a/openpilot/selfdrive/test/process_replay/README.md +++ b/openpilot/selfdrive/test/process_replay/README.md @@ -85,7 +85,7 @@ Supported processes: * modeld * dmonitoringmodeld -Certain processes may require an initial state, which is usually supplied within `Params` and persisting from segment to segment (e.g CalibrationParams, LiveParameters). The `custom_params` is dictionary used to prepopulate `Params` with arbitrary values. The `get_custom_params_from_lr` helper is provided to fetch meaningful values from log files. +Certain processes may require an initial state, which is usually supplied within `Params` and persists from segment to segment (for example `CalibrationParams` or the learner cache keys like `LiveParametersV2`). The `custom_params` is a dictionary used to prepopulate `Params` with arbitrary values. The `get_custom_params_from_lr` helper is provided to fetch meaningful values from log files. ```py from openpilot.selfdrive.test.process_replay import get_custom_params_from_lr diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index 65d54221ee..2fb8932abc 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -40,7 +40,7 @@ def migrate_all(lr: LogIterable, manager_states: bool = False, panda_states: boo migrate_controlsState, migrate_carState, migrate_liveLocationKalman, - migrate_livePose, + migrate_deviceMotion, migrate_liveTracks, migrate_driverAssistance, migrate_drivingModelData, @@ -161,11 +161,11 @@ def migrate_drivingModelData(msgs): return [], add_ops, [] -@migration(inputs=["liveTracksDEPRECATED"], product="liveTracks") +@migration(inputs=["liveTracksDEPRECATED"], product="radarTracks") def migrate_liveTracks(msgs): ops = [] for index, msg in msgs: - new_msg = messaging.new_message('liveTracks') + new_msg = messaging.new_message('radarTracks') new_msg.valid = msg.valid new_msg.logMonoTime = msg.logMonoTime @@ -179,42 +179,42 @@ def migrate_liveTracks(msgs): pt.vRel = track.vRel pts.append(pt) - new_msg.liveTracks.points = pts + new_msg.radarTracks.points = pts ops.append((index, as_reader(new_msg))) return ops, [], [] -@migration(inputs=["liveLocationKalmanDEPRECATED"], product="livePose") +@migration(inputs=["liveLocationKalmanDEPRECATED"], product="deviceMotion") def migrate_liveLocationKalman(msgs): nans = [float('nan')] * 3 ops = [] for index, msg in msgs: - m = messaging.new_message('livePose') + m = messaging.new_message('deviceMotion') m.valid = msg.valid m.logMonoTime = msg.logMonoTime - m.livePose.timestamp = msg.logMonoTime + m.deviceMotion.timestamp = msg.logMonoTime for field in ["orientationNED", "velocityDevice", "accelerationDevice", "angularVelocityDevice"]: - lp_field, llk_field = getattr(m.livePose, field), getattr(msg.liveLocationKalmanDEPRECATED, field) + lp_field, llk_field = getattr(m.deviceMotion, field), getattr(msg.liveLocationKalmanDEPRECATED, field) lp_field.x, lp_field.y, lp_field.z = llk_field.value or nans lp_field.xStd, lp_field.yStd, lp_field.zStd = llk_field.std or nans lp_field.valid = llk_field.valid for flag in ["inputsOK", "posenetOK", "sensorsOK"]: - setattr(m.livePose, flag, getattr(msg.liveLocationKalmanDEPRECATED, flag)) + setattr(m.deviceMotion, flag, getattr(msg.liveLocationKalmanDEPRECATED, flag)) ops.append((index, as_reader(m))) return ops, [], [] -@migration(inputs=["livePose"]) -def migrate_livePose(msgs): +@migration(inputs=["deviceMotion"]) +def migrate_deviceMotion(msgs): ops = [] - needs_migration = all(msg.livePose.timestamp == 0 for _, msg in msgs if msg.which() == 'livePose') + needs_migration = all(msg.deviceMotion.timestamp == 0 for _, msg in msgs if msg.which() == 'deviceMotion') if not needs_migration: return [], [], [] for index, msg in msgs: - if msg.which() == "livePose": + if msg.which() == "deviceMotion": new_msg = msg.as_builder() - new_msg.livePose.timestamp = msg.logMonoTime + new_msg.deviceMotion.timestamp = msg.logMonoTime ops.append((index, as_reader(new_msg))) return ops, [], [] diff --git a/openpilot/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py index 3b5038b24c..ba610c6a2b 100755 --- a/openpilot/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -152,11 +152,11 @@ def model_replay(lr, frs): dmodeld_logs = trim_logs(lr, START_FRAME, END_FRAME, {"cabinCameraState"}, {"cabinEncodeIdx", "carParams", "can"}) if not SEND_EXTRA_INPUTS: - modeld_logs = [msg for msg in modeld_logs if msg.which() != 'liveCalibration'] - dmodeld_logs = [msg for msg in dmodeld_logs if msg.which() != 'liveCalibration'] + modeld_logs = [msg for msg in modeld_logs if msg.which() != 'extrinsicsCalibration'] + dmodeld_logs = [msg for msg in dmodeld_logs if msg.which() != 'extrinsicsCalibration'] # initial setup - for s in ('liveCalibration', 'deviceState'): + for s in ('extrinsicsCalibration', 'deviceState'): msg = next(msg for msg in lr if msg.which() == s).as_builder() msg.logMonoTime = lr[0].logMonoTime modeld_logs.insert(1, msg.as_reader()) diff --git a/openpilot/selfdrive/test/process_replay/process_replay.py b/openpilot/selfdrive/test/process_replay/process_replay.py index 346d532140..5abfab2c35 100755 --- a/openpilot/selfdrive/test/process_replay/process_replay.py +++ b/openpilot/selfdrive/test/process_replay/process_replay.py @@ -432,9 +432,9 @@ CONFIGS = [ ProcessConfig( proc_name="selfdrived", pubs=[ - "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", - "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", - "cabinCameraState", "narrowRoadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", + "carState", "deviceState", "pandaStates", "peripheralState", "extrinsicsCalibration", "driverMonitoringState", + "longitudinalPlan", "deviceMotion", "lateralDelay", "vehicleParameters", "radarState", "modelV2", + "cabinCameraState", "narrowRoadCameraState", "wideRoadCameraState", "managerState", "lateralTorqueParameters", "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", ], @@ -448,8 +448,8 @@ CONFIGS = [ ), ProcessConfig( proc_name="controlsd", - pubs=["liveParameters", "liveTorqueParameters", "modelV2", "selfdriveState", - "liveCalibration", "livePose", "longitudinalPlan", "carState", "carOutput", + pubs=["vehicleParameters", "lateralTorqueParameters", "modelV2", "selfdriveState", + "extrinsicsCalibration", "deviceMotion", "longitudinalPlan", "carState", "carOutput", "driverMonitoringState", "onroadEvents", "driverAssistance"], subs=["carControl", "controlsState"], ignore=["logMonoTime", ], @@ -460,7 +460,7 @@ CONFIGS = [ ProcessConfig( proc_name="card", pubs=["pandaStates", "carControl", "onroadEvents", "can"], - subs=["sendcan", "carState", "carParams", "carOutput", "liveTracks"], + subs=["sendcan", "carState", "carParams", "carOutput", "radarTracks"], ignore=["logMonoTime", "carState.cumLagMs"], init_callback=card_fingerprint_callback, should_recv_callback=card_rcv_callback, @@ -471,7 +471,7 @@ CONFIGS = [ ), ProcessConfig( proc_name="radard", - pubs=["liveTracks", "carState", "modelV2"], + pubs=["radarTracks", "carState", "modelV2"], subs=["radarState"], ignore=["logMonoTime"], init_callback=get_car_params_callback, @@ -479,7 +479,7 @@ CONFIGS = [ ), ProcessConfig( proc_name="plannerd", - pubs=["modelV2", "carControl", "carState", "controlsState", "liveParameters", "radarState", "selfdriveState"], + pubs=["modelV2", "carControl", "carState", "controlsState", "vehicleParameters", "radarState", "selfdriveState"], subs=["longitudinalPlan", "driverAssistance"], ignore=["logMonoTime", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime"], init_callback=get_car_params_callback, @@ -489,14 +489,14 @@ CONFIGS = [ ProcessConfig( proc_name="calibrationd", pubs=["carState", "cameraOdometry"], - subs=["liveCalibration"], + subs=["extrinsicsCalibration"], ignore=["logMonoTime"], init_callback=get_car_params_callback, should_recv_callback=MessageBasedRcvCallback("cameraOdometry", True), ), ProcessConfig( proc_name="dmonitoringd", - pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], + pubs=["driverStateV2", "extrinsicsCalibration", "carState", "modelV2", "selfdriveState"], subs=["driverMonitoringState"], ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("driverStateV2"), @@ -505,9 +505,9 @@ CONFIGS = [ ProcessConfig( proc_name="locationd", pubs=[ - "cameraOdometry", "accelerometer", "gyroscope", "liveCalibration", "carState" + "cameraOdometry", "accelerometer", "gyroscope", "extrinsicsCalibration", "carState" ], - subs=["livePose"], + subs=["deviceMotion"], ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), tolerance=NUMPY_TOLERANCE, @@ -515,21 +515,21 @@ CONFIGS = [ ), ProcessConfig( proc_name="paramsd", - pubs=["livePose", "liveCalibration", "carState"], - subs=["liveParameters"], + pubs=["deviceMotion", "extrinsicsCalibration", "carState"], + subs=["vehicleParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=MessageBasedRcvCallback("livePose"), + should_recv_callback=MessageBasedRcvCallback("deviceMotion"), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), ProcessConfig( proc_name="lagd", - pubs=["livePose", "liveCalibration", "carState", "carControl", "controlsState"], - subs=["liveDelay"], + pubs=["deviceMotion", "extrinsicsCalibration", "carState", "carControl", "controlsState"], + subs=["lateralDelay"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=MessageBasedRcvCallback("livePose"), + should_recv_callback=MessageBasedRcvCallback("deviceMotion"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -540,16 +540,17 @@ CONFIGS = [ ), ProcessConfig( proc_name="torqued", - pubs=["livePose", "liveCalibration", "liveDelay", "carState", "carControl", "carOutput"], - subs=["liveTorqueParameters"], + pubs=["deviceMotion", "extrinsicsCalibration", "lateralDelay", "carState", "carControl", "carOutput"], + subs=["lateralTorqueParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=MessageBasedRcvCallback("livePose", True), + should_recv_callback=MessageBasedRcvCallback("deviceMotion", True), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( proc_name="modeld", - pubs=["deviceState", "narrowRoadCameraState", "wideRoadCameraState", "liveCalibration", "liveDelay", "driverMonitoringState", "carState", "carControl"], + pubs=["deviceState", "narrowRoadCameraState", "wideRoadCameraState", "extrinsicsCalibration", "lateralDelay", + "driverMonitoringState", "carState", "carControl"], subs=["modelV2", "drivingModelData", "cameraOdometry"], ignore=["logMonoTime", "modelV2.frameDropPerc", "modelV2.modelExecutionTime", "drivingModelData.frameDropPerc", "drivingModelData.modelExecutionTime"], should_recv_callback=ModeldCameraSyncRcvCallback(), @@ -562,7 +563,7 @@ CONFIGS = [ ), ProcessConfig( proc_name="dmonitoringmodeld", - pubs=["liveCalibration", "cabinCameraState"], + pubs=["extrinsicsCalibration", "cabinCameraState"], subs=["driverStateV2"], ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"], should_recv_callback=MessageBasedRcvCallback("cabinCameraState"), @@ -586,30 +587,30 @@ def get_custom_params_from_lr(lr: LogIterable, initial_state: str = "first") -> """ Use this to get custom params dict based on provided logs. Useful when replaying following processes: calibrationd, paramsd, torqued - The params may be based on first or last message of given type (carParams, liveCalibration, liveParameters, liveTorqueParameters) in the logs. + The params may be based on first or last message of given type (carParams, extrinsicsCalibration, vehicleParameters, lateralTorqueParameters) in the logs. """ car_params = [m for m in lr if m.which() == "carParams"] - live_calibration = [m for m in lr if m.which() == "liveCalibration"] - live_parameters = [m for m in lr if m.which() == "liveParameters"] - live_torque_parameters = [m for m in lr if m.which() == "liveTorqueParameters"] + extrinsics_calibration = [m for m in lr if m.which() == "extrinsicsCalibration"] + vehicle_parameters = [m for m in lr if m.which() == "vehicleParameters"] + torque_parameters = [m for m in lr if m.which() == "lateralTorqueParameters"] assert initial_state in ["first", "last"] msg_index = 0 if initial_state == "first" else -1 - assert len(car_params) > 0, "carParams required for initial state of liveParameters and CarParamsPrevRoute" + assert len(car_params) > 0, "carParams required for initial state of vehicleParameters and CarParamsPrevRoute" CP = car_params[msg_index].carParams custom_params = { "CarParamsPrevRoute": CP.as_builder().to_bytes() } - if len(live_calibration) > 0: - custom_params["CalibrationParams"] = live_calibration[msg_index].as_builder().to_bytes() - if len(live_parameters) > 0: - custom_params["LiveParametersV2"] = live_parameters[msg_index].as_builder().to_bytes() - if len(live_torque_parameters) > 0: - custom_params["LiveTorqueParameters"] = live_torque_parameters[msg_index].as_builder().to_bytes() + if len(extrinsics_calibration) > 0: + custom_params["CalibrationParams"] = extrinsics_calibration[msg_index].as_builder().to_bytes() + if len(vehicle_parameters) > 0: + custom_params["LiveParametersV2"] = vehicle_parameters[msg_index].as_builder().to_bytes() + if len(torque_parameters) > 0: + custom_params["LiveTorqueParameters"] = torque_parameters[msg_index].as_builder().to_bytes() return custom_params diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index fb8cd1393f..6768dc1d98 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -87,8 +87,8 @@ TIMINGS = { "cabinCameraState": [2.5, 0.35], "modelV2": [2.5, 0.35], "driverStateV2": [2.5, 0.40], - "livePose": [2.5, 0.35], - "liveParameters": [2.5, 0.35], + "deviceMotion": [2.5, 0.35], + "vehicleParameters": [2.5, 0.35], "wideRoadCameraState": [1.5, 0.35], } diff --git a/openpilot/selfdrive/test/test_power_draw.py b/openpilot/selfdrive/test/test_power_draw.py index 8cad68978f..1d0be24ef0 100755 --- a/openpilot/selfdrive/test/test_power_draw.py +++ b/openpilot/selfdrive/test/test_power_draw.py @@ -95,7 +95,7 @@ class TestPowerDraw(OpenpilotTestCase): return now, msg_counts, time.monotonic() - start_time - SAMPLE_TIME - @mock_messages(['livePose']) + @mock_messages(['deviceMotion']) def test_camera_procs(self, subtests): baseline = get_power() diff --git a/openpilot/selfdrive/ui/layouts/settings/device.py b/openpilot/selfdrive/ui/layouts/settings/device.py index ddc5c23160..093fc738a0 100644 --- a/openpilot/selfdrive/ui/layouts/settings/device.py +++ b/openpilot/selfdrive/ui/layouts/settings/device.py @@ -116,9 +116,9 @@ class DeviceLayout(Widget): calib_bytes = self._params.get("CalibrationParams") if calib_bytes: try: - calib = messaging.log_from_bytes(calib_bytes, log.Event).liveCalibration + calib = messaging.log_from_bytes(calib_bytes, log.Event).extrinsicsCalibration - if calib.calStatus != log.LiveCalibrationData.Status.uncalibrated: + if calib.calStatus != log.ExtrinsicsCalibration.Status.uncalibrated: pitch = math.degrees(calib.rpyCalib[1]) yaw = math.degrees(calib.rpyCalib[2]) desc += tr(" Your device is pointed {:.1f}° {} and {:.1f}° {}.").format(abs(pitch), tr("down") if pitch > 0 else tr("up"), @@ -130,7 +130,7 @@ class DeviceLayout(Widget): lag_bytes = self._params.get("LiveDelay") if lag_bytes: try: - lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).liveDelay.calPerc + lag_perc = messaging.log_from_bytes(lag_bytes, log.Event).lateralDelay.calPerc except Exception: cloudlog.exception("invalid LiveDelay") if lag_perc < 100: @@ -141,7 +141,7 @@ class DeviceLayout(Widget): torque_bytes = self._params.get("LiveTorqueParameters") if torque_bytes: try: - torque = messaging.log_from_bytes(torque_bytes, log.Event).liveTorqueParameters + torque = messaging.log_from_bytes(torque_bytes, log.Event).lateralTorqueParameters # don't add for non-torque cars if torque.useParams: torque_perc = torque.calPerc diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 46348a9f16..4f4e4b2f6c 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -20,7 +20,7 @@ from openpilot.common.transformations.orientation import rot_from_euler from enum import IntEnum OpState = log.SelfdriveState.OpenpilotState -CALIBRATED = log.LiveCalibrationData.Status.calibrated +CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] @@ -266,11 +266,11 @@ class AugmentedRoadView(CameraView): if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] - # Check if live calibration data is available and valid - if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']): + # Check if camera calibration data is available and valid + if not (sm.updated["extrinsicsCalibration"] and sm.valid['extrinsicsCalibration']): return - calib = sm['liveCalibration'] + calib = sm['extrinsicsCalibration'] if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED: return @@ -285,7 +285,7 @@ class AugmentedRoadView(CameraView): def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: cache_key = ( - ui_state.sm.recv_frame['liveCalibration'], + ui_state.sm.recv_frame['extrinsicsCalibration'], int(self._content_rect.width), int(self._content_rect.height), self.stream_type, diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py index 4d19850769..dd0d6d9734 100644 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py @@ -100,7 +100,7 @@ class ModelRenderer(Widget): self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) # Check if data is up-to-date - if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or + if (sm.recv_frame["extrinsicsCalibration"] < ui_state.started_frame or sm.recv_frame["modelV2"] < ui_state.started_frame): return @@ -112,8 +112,8 @@ class ModelRenderer(Widget): # Update state self._experimental_mode = sm['selfdriveState'].experimentalMode - live_calib = sm['liveCalibration'] - self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0] + extrinsics_calibration = sm['extrinsicsCalibration'] + self._path_offset_z = extrinsics_calibration.height[0] if extrinsics_calibration.height else HEIGHT_INIT[0] if sm.updated['carParams']: self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl diff --git a/openpilot/selfdrive/ui/mici/onroad/torque_bar.py b/openpilot/selfdrive/ui/mici/onroad/torque_bar.py index 6a1d12b6c4..3c0b963666 100644 --- a/openpilot/selfdrive/ui/mici/onroad/torque_bar.py +++ b/openpilot/selfdrive/ui/mici/onroad/torque_bar.py @@ -166,7 +166,7 @@ class TorqueBar(Widget): if ui_state.sm['controlsState'].lateralControlState.which() in ('angleState', 'curvatureState'): controls_state = ui_state.sm['controlsState'] car_state = ui_state.sm['carState'] - live_parameters = ui_state.sm['liveParameters'] + vehicle_parameters = ui_state.sm['vehicleParameters'] car_control = ui_state.sm['carControl'] # Include lateral accel error in estimated torque utilization @@ -176,7 +176,7 @@ class TorqueBar(Widget): # Include road roll in estimated torque utilization # Roll is less accurate near standstill, so reduce its effect at low speed - roll_compensation = live_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0]) + roll_compensation = vehicle_parameters.roll * ACCELERATION_DUE_TO_GRAVITY * np.interp(car_state.vEgo, [5, 15], [0.0, 1.0]) lateral_acceleration = actual_lateral_accel - roll_compensation max_lateral_acceleration = ui_state.CP.maxLateralAccel if ui_state.CP else DEFAULT_MAX_LAT_ACCEL diff --git a/openpilot/selfdrive/ui/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/onroad/augmented_road_view.py index 44d8910a03..cae462e41d 100644 --- a/openpilot/selfdrive/ui/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/onroad/augmented_road_view.py @@ -14,7 +14,7 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera from openpilot.common.transformations.orientation import rot_from_euler OpState = log.SelfdriveState.OpenpilotState -CALIBRATED = log.LiveCalibrationData.Status.calibrated +CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] @@ -131,11 +131,11 @@ class AugmentedRoadView(CameraView): if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] - # Check if live calibration data is available and valid - if not (sm.updated["liveCalibration"] and sm.valid['liveCalibration']): + # Check if camera calibration data is available and valid + if not (sm.updated["extrinsicsCalibration"] and sm.valid['extrinsicsCalibration']): return - calib = sm['liveCalibration'] + calib = sm['extrinsicsCalibration'] if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED: return @@ -151,7 +151,7 @@ class AugmentedRoadView(CameraView): def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: # Check if we can use cached matrix cache_key = ( - ui_state.sm.recv_frame['liveCalibration'], + ui_state.sm.recv_frame['extrinsicsCalibration'], self._content_rect.width, self._content_rect.height, self.stream_type diff --git a/openpilot/selfdrive/ui/onroad/model_renderer.py b/openpilot/selfdrive/ui/onroad/model_renderer.py index 8a40c90025..18fe45d8e1 100644 --- a/openpilot/selfdrive/ui/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/onroad/model_renderer.py @@ -85,7 +85,7 @@ class ModelRenderer(Widget): sm = ui_state.sm # Check if data is up-to-date - if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or + if (sm.recv_frame["extrinsicsCalibration"] < ui_state.started_frame or sm.recv_frame["modelV2"] < ui_state.started_frame): return @@ -97,8 +97,8 @@ class ModelRenderer(Widget): # Update state self._experimental_mode = sm['selfdriveState'].experimentalMode - live_calib = sm['liveCalibration'] - self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0] + extrinsics_calibration = sm['extrinsicsCalibration'] + self._path_offset_z = extrinsics_calibration.height[0] if extrinsics_calibration.height else HEIGHT_INIT[0] if sm.updated['carParams']: self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl diff --git a/openpilot/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py index 30e811dbc9..962d409344 100644 --- a/openpilot/selfdrive/ui/tests/diff/replay_script.py +++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py @@ -144,19 +144,19 @@ def set_updater_state(state: str) -> None: def setup_calibration_params() -> None: params = Params() - # live calibration - calib = messaging.new_message('liveCalibration') - calib.liveCalibration.calStatus = log.LiveCalibrationData.Status.calibrated - calib.liveCalibration.rpyCalib = [0.0, math.radians(2.5), math.radians(-1.2)] + # camera calibration + calib = messaging.new_message('extrinsicsCalibration') + calib.extrinsicsCalibration.calStatus = log.ExtrinsicsCalibration.Status.calibrated + calib.extrinsicsCalibration.rpyCalib = [0.0, math.radians(2.5), math.radians(-1.2)] params.put("CalibrationParams", calib.to_bytes(), block=True) - # live delay - delay = messaging.new_message('liveDelay') - delay.liveDelay.calPerc = 75 + # lateral delay + delay = messaging.new_message('lateralDelay') + delay.lateralDelay.calPerc = 75 params.put("LiveDelay", delay.to_bytes(), block=True) - # live torque parameters - torque = messaging.new_message('liveTorqueParameters') - torque.liveTorqueParameters.useParams = True - torque.liveTorqueParameters.calPerc = 60 + # lateral torque parameters + torque = messaging.new_message('lateralTorqueParameters') + torque.lateralTorqueParameters.useParams = True + torque.lateralTorqueParameters.calPerc = 60 params.put("LiveTorqueParameters", torque.to_bytes(), block=True) diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 943ba46014..42e8086351 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -40,7 +40,7 @@ class UIState: "modelV2", "controlsState", "onroadEvents", - "liveCalibration", + "extrinsicsCalibration", "radarState", "deviceState", "pandaStates", @@ -56,7 +56,7 @@ class UIState: "gpsLocationExternal", "carOutput", "carControl", - "liveParameters", + "vehicleParameters", "testJoystick", "rawAudioData", ] diff --git a/openpilot/tools/jotpluggler/layouts/locationd_debug.json b/openpilot/tools/jotpluggler/layouts/locationd_debug.json index 0541427bc1..53112f31d6 100644 --- a/openpilot/tools/jotpluggler/layouts/locationd_debug.json +++ b/openpilot/tools/jotpluggler/layouts/locationd_debug.json @@ -1 +1 @@ -{"current_tab_index":0,"tabs":[{"name":"tab1","root":{"split":"vertical","sizes":[0.166588,0.167062,0.166113,0.166588,0.167062,0.166588],"children":[{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1.025,"bottom":-0.025},"curves":[{"name":"/livePose/inputsOK","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":14.542814,"bottom":-5.586039},"curves":[{"name":"/accelerometer/acceleration/v/0","color":"#f14cc1"},{"name":"/accelerometer/acceleration/v/1","color":"#9467bd"},{"name":"/accelerometer/acceleration/v/2","color":"#17becf"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":0.988911,"bottom":-0.745939},"curves":[{"name":"/gyroscope/gyroUncalibrated/v/0","color":"#d62728"},{"name":"/gyroscope/gyroUncalibrated/v/1","color":"#1ac938"},{"name":"/gyroscope/gyroUncalibrated/v/2","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1.025,"bottom":-0.025},"curves":[{"name":"/accelerometer/__valid","color":"#17becf"},{"name":"/gyroscope/__valid","color":"#bcbd22"},{"name":"/carState/__valid","color":"#f14cc1"},{"name":"/liveCalibration/__valid","color":"#1ac938"},{"name":"/cameraOdometry/__valid","color":"#9467bd"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1000000000.292252,"bottom":999999999.735447},"curves":[{"name":"/gyroscope/__logMonoTime","color":"#1f77b4","transform":"derivative"},{"name":"/accelerometer/__logMonoTime","color":"#d62728","transform":"derivative"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":20790107743.93223,"bottom":-529653831.495853},"curves":[{"name":"/accelerometer/timestamp","color":"#bcbd22","transform":"derivative"},{"name":"/gyroscope/timestamp","color":"#1f77b4","transform":"derivative"}]}]}}]} +{"current_tab_index":0,"tabs":[{"name":"tab1","root":{"split":"vertical","sizes":[0.166588,0.167062,0.166113,0.166588,0.167062,0.166588],"children":[{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1.025,"bottom":-0.025},"curves":[{"name":"/deviceMotion/inputsOK","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":14.542814,"bottom":-5.586039},"curves":[{"name":"/accelerometer/acceleration/v/0","color":"#f14cc1"},{"name":"/accelerometer/acceleration/v/1","color":"#9467bd"},{"name":"/accelerometer/acceleration/v/2","color":"#17becf"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":0.988911,"bottom":-0.745939},"curves":[{"name":"/gyroscope/gyroUncalibrated/v/0","color":"#d62728"},{"name":"/gyroscope/gyroUncalibrated/v/1","color":"#1ac938"},{"name":"/gyroscope/gyroUncalibrated/v/2","color":"#ff7f0e"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1.025,"bottom":-0.025},"curves":[{"name":"/accelerometer/__valid","color":"#17becf"},{"name":"/gyroscope/__valid","color":"#bcbd22"},{"name":"/carState/__valid","color":"#f14cc1"},{"name":"/extrinsicsCalibration/__valid","color":"#1ac938"},{"name":"/cameraOdometry/__valid","color":"#9467bd"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":1000000000.292252,"bottom":999999999.735447},"curves":[{"name":"/gyroscope/__logMonoTime","color":"#1f77b4","transform":"derivative"},{"name":"/accelerometer/__logMonoTime","color":"#d62728","transform":"derivative"}]},{"title":"...","range":{"left":0.0,"right":2280.128382,"top":20790107743.93223,"bottom":-529653831.495853},"curves":[{"name":"/accelerometer/timestamp","color":"#bcbd22","transform":"derivative"},{"name":"/gyroscope/timestamp","color":"#1f77b4","transform":"derivative"}]}]}}]} diff --git a/openpilot/tools/jotpluggler/layouts/max-torque-debug.json b/openpilot/tools/jotpluggler/layouts/max-torque-debug.json index 3a87fb3217..b587b695a9 100644 --- a/openpilot/tools/jotpluggler/layouts/max-torque-debug.json +++ b/openpilot/tools/jotpluggler/layouts/max-torque-debug.json @@ -1 +1 @@ -{"current_tab_index":0,"tabs":[{"name":"tab1","root":{"split":"vertical","sizes":[0.249724,0.250829,0.249724,0.249724],"children":[{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":6.050533,"bottom":-7.599037},"curves":[{"name":"Actual lateral accel (roll compensated)","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/liveParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"Desired lateral accel (roll compensated)","color":"#ff7f0e","custom_python":{"linked_source":"/controlsState/desiredCurvature","additional_sources":["/carState/vEgo","/liveParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":5.384416,"bottom":-7.503945},"curves":[{"name":"roll compensated lateral acceleration","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/liveParameters/roll","/carState/steeringPressed","/carControl/latActive"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2, v3, v4):\n if (v3 == 0 and v4 == 1):\n return (value * v1 ** 2) - (v2 * 9.81)\n return 0\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i], v3[__jotpluggler_i], v4[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":1.05,"bottom":-1.05},"curves":[{"name":"/carState/steeringPressed","color":"#0097ff"},{"name":"/carOutput/actuatorsOutput/torque","color":"#d62728"}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":80.762969,"bottom":-2.181837},"curves":[{"name":"/carState/vEgo","color":"#f14cc1","transform":"scale","scale":2.23694,"offset":0.0}]}]}}]} +{"current_tab_index":0,"tabs":[{"name":"tab1","root":{"split":"vertical","sizes":[0.249724,0.250829,0.249724,0.249724],"children":[{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":6.050533,"bottom":-7.599037},"curves":[{"name":"Actual lateral accel (roll compensated)","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/vehicleParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"Desired lateral accel (roll compensated)","color":"#ff7f0e","custom_python":{"linked_source":"/controlsState/desiredCurvature","additional_sources":["/carState/vEgo","/vehicleParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":5.384416,"bottom":-7.503945},"curves":[{"name":"roll compensated lateral acceleration","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/vehicleParameters/roll","/carState/steeringPressed","/carControl/latActive"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2, v3, v4):\n if (v3 == 0 and v4 == 1):\n return (value * v1 ** 2) - (v2 * 9.81)\n return 0\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i], v3[__jotpluggler_i], v4[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":1.05,"bottom":-1.05},"curves":[{"name":"/carState/steeringPressed","color":"#0097ff"},{"name":"/carOutput/actuatorsOutput/torque","color":"#d62728"}]},{"title":"...","range":{"left":0.00045,"right":2483.624998,"top":80.762969,"bottom":-2.181837},"curves":[{"name":"/carState/vEgo","color":"#f14cc1","transform":"scale","scale":2.23694,"offset":0.0}]}]}}]} diff --git a/openpilot/tools/jotpluggler/layouts/torque-controller.json b/openpilot/tools/jotpluggler/layouts/torque-controller.json index 7e269e59e6..a794c725d6 100644 --- a/openpilot/tools/jotpluggler/layouts/torque-controller.json +++ b/openpilot/tools/jotpluggler/layouts/torque-controller.json @@ -1 +1 @@ -{"current_tab_index":0,"tabs":[{"name":"Lateral Plan Conformance","root":{"split":"vertical","sizes":[0.250949,0.249051,0.250949,0.249051],"children":[{"title":"desired vs actual lateral acceleration (closer means better conformance to plan)","range":{"left":0.000194,"right":1138.891674,"top":1.858161,"bottom":-1.823407},"curves":[{"name":"/controlsState/lateralControlState/torqueState/actualLateralAccel","color":"#1f77b4"},{"name":"/controlsState/lateralControlState/torqueState/desiredLateralAccel","color":"#d62728"}]},{"title":"desired vs actual lateral acceleration, road-roll factored out (closer means better conformance to plan)","range":{"left":0.000194,"right":1138.891674,"top":2.749816,"bottom":-3.723091},"curves":[{"name":"Actual lateral accel (roll compensated)","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/liveParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"Desired lateral accel (roll compensated)","color":"#ff7f0e","custom_python":{"linked_source":"/controlsState/desiredCurvature","additional_sources":["/carState/vEgo","/liveParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"controller feed-forward vs actuator output (closer means controller prediction is more accurate)","range":{"left":0.000194,"right":1138.891674,"top":1.978032,"bottom":-1.570956},"curves":[{"name":"/carOutput/actuatorsOutput/torque","color":"#9467bd","transform":"scale","scale":-1.0,"offset":0.0},{"name":"/controlsState/lateralControlState/torqueState/f","color":"#1f77b4"},{"name":"/carState/steeringPressed","color":"#ff000f"}]},{"title":"vehicle speed","range":{"left":0.000194,"right":1138.891674,"top":105.981304,"bottom":-2.709314},"curves":[{"name":"carState.vEgo mph","color":"#d62728","custom_python":{"linked_source":"/carState/vEgo","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value * 2.23694\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"carState.vEgo kmh","color":"#1ac938","custom_python":{"linked_source":"/carState/vEgo","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value * 3.6\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"/carState/vEgo","color":"#ff7f0e"}]}]}},{"name":"Vehicle Dynamics","root":{"split":"vertical","sizes":[0.334282,0.331437,0.334282],"children":[{"title":"configured-initial vs online-learned steerRatio, set configured value to match learned","range":{"left":0.0,"right":1138.816328,"top":19.665784,"bottom":19.359553},"curves":[{"name":"/carParams/steerRatio","color":"#1f77b4"},{"name":"/liveParameters/steerRatio","color":"#1ac938"}]},{"title":"configured-initial vs online-learned tireStiffnessRatio, set configured value to match learned","range":{"left":0.0,"right":1138.816328,"top":1.11221,"bottom":0.995631},"curves":[{"name":"/carParams/tireStiffnessFactor","color":"#d62728"},{"name":"/liveParameters/stiffnessFactor","color":"#ff7f0e"}]},{"title":"live steering angle offsets for straight-ahead driving, large values here may indicate alignment problems","range":{"left":0.0,"right":1138.816328,"top":-1.081041,"bottom":-4.494133},"curves":[{"name":"/liveParameters/angleOffsetAverageDeg","color":"#f14cc1"},{"name":"/liveParameters/angleOffsetDeg","color":"#9467bd"}]}]}},{"name":"Actuator Performance","root":{"split":"vertical","sizes":[0.333333,0.333333,0.333333],"children":[{"title":"offline-calculated vs online-learned lateral accel scaling factor, accel obtained from 100% actuator output","range":{"left":0.0,"right":1138.920072,"top":1.21611,"bottom":0.539474},"curves":[{"name":"/liveTorqueParameters/latAccelFactorFiltered","color":"#1f77b4"},{"name":"/liveTorqueParameters/latAccelFactorRaw","color":"#d62728"},{"name":"/carParams/lateralTuning/torque/latAccelFactor","color":"#1c9222"}]},{"title":"learned lateral accel offset, vehicle-specific compensation to obtain true zero lateral accel","range":{"left":0.0,"right":1138.920072,"top":-0.304367,"bottom":-0.418688},"curves":[{"name":"/liveTorqueParameters/latAccelOffsetFiltered","color":"#1ac938"},{"name":"/liveTorqueParameters/latAccelOffsetRaw","color":"#ff7f0e"}]},{"title":"offline-calculated vs online-learned EPS friction factor, necessary to start moving the steering wheel","range":{"left":0.0,"right":1138.920072,"top":0.226389,"bottom":0.15805},"curves":[{"name":"/liveTorqueParameters/frictionCoefficientFiltered","color":"#f14cc1"},{"name":"/liveTorqueParameters/frictionCoefficientRaw","color":"#9467bd"},{"name":"/carParams/lateralTuning/torque/friction","color":"#1c9222"}]}]}},{"name":"Actuator Delay","root":{"split":"vertical","sizes":[0.30441,0.358464,0.337127],"children":[{"title":"actuator lag learning state, 0 = learning, 1 = learned/applying, 2 = invalid","range":{"left":0.0,"right":1138.749979,"top":1.025,"bottom":-0.025},"curves":[{"name":"/liveDelay/status","color":"#ff7f0e"}]},{"title":"offline default vs online estimated steering actuator lag","range":{"left":0.0,"right":1138.749979,"top":0.419648,"bottom":0.318362},"curves":[{"name":"/liveDelay/lateralDelay","color":"#1f77b4"},{"name":"/liveDelay/lateralDelayEstimate","color":"#d62728"},{"name":"opendbc default steering lag","color":"#1ac938","custom_python":{"linked_source":"/carParams/steerActuatorDelay","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value + 0.2\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"online estimated steering actuator lag, standard deviation","range":{"left":0.0,"right":1138.749979,"top":0.06732,"bottom":-0.001642},"curves":[{"name":"/liveDelay/lateralDelayEstimateStd","color":"#f14cc1"}]}]}},{"name":"Controls Performance","root":{"split":"vertical","sizes":[0.265655,0.251898,0.245731,0.236717],"children":[{"title":"rate-of-change limits on steering actuator (blue = original, green = rate-limited before CAN output)","range":{"left":0.000194,"right":1138.891921,"top":1.05,"bottom":-1.05},"curves":[{"name":"/carControl/actuators/torque","color":"#0c00f2"},{"name":"/carOutput/actuatorsOutput/torque","color":"#2cd63a"}]},{"title":"controller feed-forward vs actuator output (closer means controller prediction is more accurate)","range":{"left":0.000194,"right":1138.891921,"top":1.978032,"bottom":-1.570956},"curves":[{"name":"/carOutput/actuatorsOutput/torque","color":"#9467bd","transform":"scale","scale":-1.0,"offset":0.0},{"name":"/controlsState/lateralControlState/torqueState/f","color":"#1f77b4"},{"name":"/carState/steeringPressed","color":"#ff000f"}]},{"title":"proportional, integral, and feed-forward terms (actuator output = sum of PIF terms)","range":{"left":0.000194,"right":1138.891921,"top":2.099784,"bottom":-4.027542},"curves":[{"name":"/controlsState/lateralControlState/torqueState/f","color":"#0ab027"},{"name":"/controlsState/lateralControlState/torqueState/p","color":"#d62728"},{"name":"/controlsState/lateralControlState/torqueState/i","color":"#ffaf00"},{"name":"Zero","color":"#756a6a","custom_python":{"linked_source":"/carState/canValid","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return (0)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"road roll angle, from openpilot localizer","range":{"left":0.000194,"right":1138.891921,"top":0.109446,"bottom":-0.045525},"curves":[{"name":"/liveParameters/roll","color":"#f14cc1"}]}]}}]} +{"current_tab_index":0,"tabs":[{"name":"Lateral Plan Conformance","root":{"split":"vertical","sizes":[0.250949,0.249051,0.250949,0.249051],"children":[{"title":"desired vs actual lateral acceleration (closer means better conformance to plan)","range":{"left":0.000194,"right":1138.891674,"top":1.858161,"bottom":-1.823407},"curves":[{"name":"/controlsState/lateralControlState/torqueState/actualLateralAccel","color":"#1f77b4"},{"name":"/controlsState/lateralControlState/torqueState/desiredLateralAccel","color":"#d62728"}]},{"title":"desired vs actual lateral acceleration, road-roll factored out (closer means better conformance to plan)","range":{"left":0.000194,"right":1138.891674,"top":2.749816,"bottom":-3.723091},"curves":[{"name":"Actual lateral accel (roll compensated)","color":"#1ac938","custom_python":{"linked_source":"/controlsState/curvature","additional_sources":["/carState/vEgo","/vehicleParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"Desired lateral accel (roll compensated)","color":"#ff7f0e","custom_python":{"linked_source":"/controlsState/desiredCurvature","additional_sources":["/carState/vEgo","/vehicleParameters/roll"],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value, v1, v2):\n return (value * v1 ** 2) - (v2 * 9.81)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i], v1[__jotpluggler_i], v2[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"controller feed-forward vs actuator output (closer means controller prediction is more accurate)","range":{"left":0.000194,"right":1138.891674,"top":1.978032,"bottom":-1.570956},"curves":[{"name":"/carOutput/actuatorsOutput/torque","color":"#9467bd","transform":"scale","scale":-1.0,"offset":0.0},{"name":"/controlsState/lateralControlState/torqueState/f","color":"#1f77b4"},{"name":"/carState/steeringPressed","color":"#ff000f"}]},{"title":"vehicle speed","range":{"left":0.000194,"right":1138.891674,"top":105.981304,"bottom":-2.709314},"curves":[{"name":"carState.vEgo mph","color":"#d62728","custom_python":{"linked_source":"/carState/vEgo","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value * 2.23694\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"carState.vEgo kmh","color":"#1ac938","custom_python":{"linked_source":"/carState/vEgo","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value * 3.6\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}},{"name":"/carState/vEgo","color":"#ff7f0e"}]}]}},{"name":"Vehicle Dynamics","root":{"split":"vertical","sizes":[0.334282,0.331437,0.334282],"children":[{"title":"configured-initial vs online-learned steerRatio, set configured value to match learned","range":{"left":0.0,"right":1138.816328,"top":19.665784,"bottom":19.359553},"curves":[{"name":"/carParams/steerRatio","color":"#1f77b4"},{"name":"/vehicleParameters/steerRatio","color":"#1ac938"}]},{"title":"configured-initial vs online-learned tireStiffnessRatio, set configured value to match learned","range":{"left":0.0,"right":1138.816328,"top":1.11221,"bottom":0.995631},"curves":[{"name":"/carParams/tireStiffnessFactor","color":"#d62728"},{"name":"/vehicleParameters/stiffnessFactor","color":"#ff7f0e"}]},{"title":"online-learned steering angle offsets for straight-ahead driving, large values here may indicate alignment problems","range":{"left":0.0,"right":1138.816328,"top":-1.081041,"bottom":-4.494133},"curves":[{"name":"/vehicleParameters/angleOffsetAverageDeg","color":"#f14cc1"},{"name":"/vehicleParameters/angleOffsetDeg","color":"#9467bd"}]}]}},{"name":"Actuator Performance","root":{"split":"vertical","sizes":[0.333333,0.333333,0.333333],"children":[{"title":"offline-calculated vs online-learned lateral accel scaling factor, accel obtained from 100% actuator output","range":{"left":0.0,"right":1138.920072,"top":1.21611,"bottom":0.539474},"curves":[{"name":"/lateralTorqueParameters/latAccelFactorFiltered","color":"#1f77b4"},{"name":"/lateralTorqueParameters/latAccelFactorRaw","color":"#d62728"},{"name":"/carParams/lateralTuning/torque/latAccelFactor","color":"#1c9222"}]},{"title":"learned lateral accel offset, vehicle-specific compensation to obtain true zero lateral accel","range":{"left":0.0,"right":1138.920072,"top":-0.304367,"bottom":-0.418688},"curves":[{"name":"/lateralTorqueParameters/latAccelOffsetFiltered","color":"#1ac938"},{"name":"/lateralTorqueParameters/latAccelOffsetRaw","color":"#ff7f0e"}]},{"title":"offline-calculated vs online-learned EPS friction factor, necessary to start moving the steering wheel","range":{"left":0.0,"right":1138.920072,"top":0.226389,"bottom":0.15805},"curves":[{"name":"/lateralTorqueParameters/frictionCoefficientFiltered","color":"#f14cc1"},{"name":"/lateralTorqueParameters/frictionCoefficientRaw","color":"#9467bd"},{"name":"/carParams/lateralTuning/torque/friction","color":"#1c9222"}]}]}},{"name":"Actuator Delay","root":{"split":"vertical","sizes":[0.30441,0.358464,0.337127],"children":[{"title":"actuator lag learning state, 0 = learning, 1 = learned/applying, 2 = invalid","range":{"left":0.0,"right":1138.749979,"top":1.025,"bottom":-0.025},"curves":[{"name":"/lateralDelay/status","color":"#ff7f0e"}]},{"title":"offline default vs online estimated steering actuator lag","range":{"left":0.0,"right":1138.749979,"top":0.419648,"bottom":0.318362},"curves":[{"name":"/lateralDelay/lateralDelay","color":"#1f77b4"},{"name":"/lateralDelay/lateralDelayEstimate","color":"#d62728"},{"name":"opendbc default steering lag","color":"#1ac938","custom_python":{"linked_source":"/carParams/steerActuatorDelay","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return value + 0.2\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"online estimated steering actuator lag, standard deviation","range":{"left":0.0,"right":1138.749979,"top":0.06732,"bottom":-0.001642},"curves":[{"name":"/lateralDelay/lateralDelayEstimateStd","color":"#f14cc1"}]}]}},{"name":"Controls Performance","root":{"split":"vertical","sizes":[0.265655,0.251898,0.245731,0.236717],"children":[{"title":"rate-of-change limits on steering actuator (blue = original, green = rate-limited before CAN output)","range":{"left":0.000194,"right":1138.891921,"top":1.05,"bottom":-1.05},"curves":[{"name":"/carControl/actuators/torque","color":"#0c00f2"},{"name":"/carOutput/actuatorsOutput/torque","color":"#2cd63a"}]},{"title":"controller feed-forward vs actuator output (closer means controller prediction is more accurate)","range":{"left":0.000194,"right":1138.891921,"top":1.978032,"bottom":-1.570956},"curves":[{"name":"/carOutput/actuatorsOutput/torque","color":"#9467bd","transform":"scale","scale":-1.0,"offset":0.0},{"name":"/controlsState/lateralControlState/torqueState/f","color":"#1f77b4"},{"name":"/carState/steeringPressed","color":"#ff000f"}]},{"title":"proportional, integral, and feed-forward terms (actuator output = sum of PIF terms)","range":{"left":0.000194,"right":1138.891921,"top":2.099784,"bottom":-4.027542},"curves":[{"name":"/controlsState/lateralControlState/torqueState/f","color":"#0ab027"},{"name":"/controlsState/lateralControlState/torqueState/p","color":"#d62728"},{"name":"/controlsState/lateralControlState/torqueState/i","color":"#ffaf00"},{"name":"Zero","color":"#756a6a","custom_python":{"linked_source":"/carState/canValid","additional_sources":[],"globals_code":"","function_code":"def __jotpluggler_eval_sample(time, value):\n return (0)\n\n__jotpluggler_result = np.empty_like(value, dtype=np.float64)\nfor __jotpluggler_i in range(len(value)):\n __jotpluggler_result[__jotpluggler_i] = __jotpluggler_eval_sample(time[__jotpluggler_i], value[__jotpluggler_i])\nreturn __jotpluggler_result"}}]},{"title":"road roll angle, from openpilot localizer","range":{"left":0.000194,"right":1138.891921,"top":0.109446,"bottom":-0.045525},"curves":[{"name":"/vehicleParameters/roll","color":"#f14cc1"}]}]}}]} diff --git a/openpilot/tools/joystick/joystickd.py b/openpilot/tools/joystick/joystickd.py index 2b84683863..f8a2598361 100755 --- a/openpilot/tools/joystick/joystickd.py +++ b/openpilot/tools/joystick/joystickd.py @@ -21,7 +21,7 @@ def joystickd_thread(): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) VM = VehicleModel(CP) - sm = messaging.SubMaster(['carState', 'onroadEvents', 'liveParameters', 'selfdriveState', 'testJoystick'], frequency=1. / DT_CTRL) + sm = messaging.SubMaster(['carState', 'onroadEvents', 'vehicleParameters', 'selfdriveState', 'testJoystick'], frequency=1. / DT_CTRL) pm = messaging.PubMaster(['carControl', 'controlsState']) rk = Ratekeeper(100, print_delay_threshold=None) @@ -55,7 +55,7 @@ def joystickd_thread(): if CC.latActive: max_curvature = MAX_LAT_ACCEL / max(sm['carState'].vEgo ** 2, 5) - max_angle = math.degrees(VM.get_steer_from_curvature(max_curvature, sm['carState'].vEgo, sm['liveParameters'].roll)) + max_angle = math.degrees(VM.get_steer_from_curvature(max_curvature, sm['carState'].vEgo, sm['vehicleParameters'].roll)) actuators.torque = float(np.clip(joystick_axes[1], -1, 1)) actuators.steeringAngleDeg, actuators.curvature = actuators.torque * max_angle, actuators.torque * -max_curvature @@ -67,7 +67,7 @@ def joystickd_thread(): controlsState = cs_msg.controlsState controlsState.lateralControlState.init('debugState') - lp = sm['liveParameters'] + lp = sm['vehicleParameters'] steer_angle_without_offset = math.radians(sm['carState'].steeringAngleDeg - lp.angleOffsetDeg) controlsState.curvature = -VM.calc_curvature(steer_angle_without_offset, sm['carState'].vEgo, lp.roll) diff --git a/openpilot/tools/longitudinal_maneuvers/generate_report.py b/openpilot/tools/longitudinal_maneuvers/generate_report.py index dbd9f6db91..2d7f81a7f8 100755 --- a/openpilot/tools/longitudinal_maneuvers/generate_report.py +++ b/openpilot/tools/longitudinal_maneuvers/generate_report.py @@ -44,14 +44,14 @@ def report(platform, route, _description, CP, ID, maneuvers): t_carControl, carControl = zip(*[(m.logMonoTime, m.carControl) for m in msgs if m.which() == 'carControl'], strict=True) t_carOutput, carOutput = zip(*[(m.logMonoTime, m.carOutput) for m in msgs if m.which() == 'carOutput'], strict=True) t_carState, carState = zip(*[(m.logMonoTime, m.carState) for m in msgs if m.which() == 'carState'], strict=True) - t_livePose, livePose = zip(*[(m.logMonoTime, m.livePose) for m in msgs if m.which() == 'livePose'], strict=True) + t_deviceMotion, deviceMotion = zip(*[(m.logMonoTime, m.deviceMotion) for m in msgs if m.which() == 'deviceMotion'], strict=True) t_longitudinalPlan, longitudinalPlan = zip(*[(m.logMonoTime, m.longitudinalPlan) for m in msgs if m.which() == 'longitudinalPlan'], strict=True) # make time relative seconds t_carControl = [(t - t_carControl[0]) / 1e9 for t in t_carControl] t_carOutput = [(t - t_carOutput[0]) / 1e9 for t in t_carOutput] t_carState = [(t - t_carState[0]) / 1e9 for t in t_carState] - t_livePose = [(t - t_livePose[0]) / 1e9 for t in t_livePose] + t_deviceMotion = [(t - t_deviceMotion[0]) / 1e9 for t in t_deviceMotion] t_longitudinalPlan = [(t - t_longitudinalPlan[0]) / 1e9 for t in t_longitudinalPlan] # maneuver validity @@ -70,7 +70,7 @@ def report(platform, route, _description, CP, ID, maneuvers): # Localizer is noisy, require two consecutive 20Hz frames above threshold prev_crossed = False - for t, lp in zip(t_livePose, livePose, strict=True): + for t, lp in zip(t_deviceMotion, deviceMotion, strict=True): crossed = (0 < aTarget < lp.accelerationDevice.x) or (0 > aTarget > lp.accelerationDevice.x) if crossed and prev_crossed: builder.append(f', crossed in {t:.3f}s') @@ -95,7 +95,7 @@ def report(platform, route, _description, CP, ID, maneuvers): ax[0].plot(t_carOutput, [m.actuatorsOutput.accel for m in carOutput], label='carOutput.actuatorsOutput.accel', linewidth=6) ax[0].plot(t_longitudinalPlan, [m.aTarget for m in longitudinalPlan], label='longitudinalPlan.aTarget', linewidth=6) ax[0].plot(t_carState, [m.aEgo for m in carState], label='carState.aEgo', linewidth=6) - ax[0].plot(t_livePose, [m.accelerationDevice.x for m in livePose], label='livePose.accelerationDevice.x', linewidth=6) + ax[0].plot(t_deviceMotion, [m.accelerationDevice.x for m in deviceMotion], label='deviceMotion.accelerationDevice.x', linewidth=6) # TODO localizer accel ax[0].set_ylabel('Acceleration (m/s^2)') #ax[0].set_ylim(-6.5, 6.5) diff --git a/openpilot/tools/plotjuggler/layouts/locationd_debug.xml b/openpilot/tools/plotjuggler/layouts/locationd_debug.xml index 5377e1535c..6e1cd35039 100644 --- a/openpilot/tools/plotjuggler/layouts/locationd_debug.xml +++ b/openpilot/tools/plotjuggler/layouts/locationd_debug.xml @@ -8,7 +8,7 @@ - + @@ -36,7 +36,7 @@ - +
    @@ -97,4 +97,3 @@ - diff --git a/openpilot/tools/plotjuggler/layouts/max-torque-debug.xml b/openpilot/tools/plotjuggler/layouts/max-torque-debug.xml index 9a6693165e..2089b826d9 100644 --- a/openpilot/tools/plotjuggler/layouts/max-torque-debug.xml +++ b/openpilot/tools/plotjuggler/layouts/max-torque-debug.xml @@ -62,7 +62,7 @@ return 0 /controlsState/curvature /carState/vEgo - /liveParameters/roll + /vehicleParameters/roll /carState/steeringPressed /carControl/latActive @@ -73,7 +73,7 @@ return 0 /controlsState/desiredCurvature /carState/vEgo - /liveParameters/roll + /vehicleParameters/roll @@ -82,11 +82,10 @@ return 0 /controlsState/curvature /carState/vEgo - /liveParameters/roll + /vehicleParameters/roll - diff --git a/openpilot/tools/plotjuggler/layouts/torque-controller.xml b/openpilot/tools/plotjuggler/layouts/torque-controller.xml index 8e9a1a8526..671b47c355 100644 --- a/openpilot/tools/plotjuggler/layouts/torque-controller.xml +++ b/openpilot/tools/plotjuggler/layouts/torque-controller.xml @@ -53,7 +53,7 @@ - +
    @@ -61,15 +61,15 @@ - +
    - + - - + + @@ -82,8 +82,8 @@ - - + + @@ -91,16 +91,16 @@ - - + + - - + + @@ -114,15 +114,15 @@ - + - - + + @@ -130,7 +130,7 @@ - + @@ -174,7 +174,7 @@ - + @@ -221,7 +221,7 @@ /controlsState/desiredCurvature /carState/vEgo - /liveParameters/roll + /vehicleParameters/roll @@ -230,7 +230,7 @@ /controlsState/curvature /carState/vEgo - /liveParameters/roll + /vehicleParameters/roll @@ -247,4 +247,3 @@ - diff --git a/openpilot/tools/replay/consoleui.cc b/openpilot/tools/replay/consoleui.cc index 2d21b4efc0..aa58cc959d 100644 --- a/openpilot/tools/replay/consoleui.cc +++ b/openpilot/tools/replay/consoleui.cc @@ -60,7 +60,7 @@ ExitHandler do_exit; } // namespace -ConsoleUI::ConsoleUI(Replay *replay) : replay(replay), sm({"carState", "liveParameters"}) { +ConsoleUI::ConsoleUI(Replay *replay) : replay(replay), sm({"carState", "vehicleParameters"}) { // Initialize curses initscr(); clear(); @@ -174,7 +174,7 @@ void ConsoleUI::updateStatus() { std::string current_segment = " - " + std::to_string((int)(replay->currentSeconds() / 60)); write_item(0, 25, "TIME: ", time_string, current_segment, true); - auto p = sm["liveParameters"].getLiveParameters(); + auto p = sm["vehicleParameters"].getVehicleParameters(); write_item(1, 0, "STIFFNESS: ", util::string_format("%.2f %%", p.getStiffnessFactor() * 100), " "); write_item(1, 25, "SPEED: ", util::string_format("%.2f", sm["carState"].getCarState().getVEgo()), " m/s"); write_item(2, 0, "STEER RATIO: ", util::string_format("%.2f", p.getSteerRatio()), ""); diff --git a/openpilot/tools/replay/ui.py b/openpilot/tools/replay/ui.py index 16c050f8a6..7f34f1303d 100755 --- a/openpilot/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -76,12 +76,12 @@ def ui_thread(addr): 'longitudinalPlan', 'carControl', 'radarState', - 'liveCalibration', + 'extrinsicsCalibration', 'controlsState', 'selfdriveState', - 'liveTracks', + 'radarTracks', 'modelV2', - 'liveParameters', + 'vehicleParameters', 'narrowRoadCameraState', ], addr=addr, @@ -195,10 +195,10 @@ def ui_thread(addr): plot_lead(sm['radarState'], top_down) # draw all radar points - maybe_update_radar_points(sm['liveTracks'].points, top_down[1]) + maybe_update_radar_points(sm['radarTracks'].points, top_down[1]) - if sm.updated['liveCalibration'] and num_px: - rpyCalib = np.asarray(sm['liveCalibration'].rpyCalib) + if sm.updated['extrinsicsCalibration'] and num_px: + rpyCalib = np.asarray(sm['extrinsicsCalibration'].rpyCalib) calibration = Calibration(num_px, rpyCalib, intrinsic_matrix, calib_scale) # Update overlay texture (RGB img -> RGBA with non-black pixels visible) @@ -232,10 +232,10 @@ def ui_thread(addr): ("LONG CONTROL STATE: " + str(sm['controlsState'].longControlState), YELLOW), ("LONG MPC SOURCE: " + str(sm['longitudinalPlan'].longitudinalPlanSource), YELLOW), None, - ("ANGLE OFFSET (AVG): " + str(round(sm['liveParameters'].angleOffsetAverageDeg, 2)) + " deg", YELLOW), - ("ANGLE OFFSET (INSTANT): " + str(round(sm['liveParameters'].angleOffsetDeg, 2)) + " deg", YELLOW), - ("STIFFNESS: " + str(round(sm['liveParameters'].stiffnessFactor * 100.0, 2)) + " %", YELLOW), - ("STEER RATIO: " + str(round(sm['liveParameters'].steerRatio, 2)), YELLOW), + ("ANGLE OFFSET (AVG): " + str(round(sm['vehicleParameters'].angleOffsetAverageDeg, 2)) + " deg", YELLOW), + ("ANGLE OFFSET (INSTANT): " + str(round(sm['vehicleParameters'].angleOffsetDeg, 2)) + " deg", YELLOW), + ("STIFFNESS: " + str(round(sm['vehicleParameters'].stiffnessFactor * 100.0, 2)) + " %", YELLOW), + ("STEER RATIO: " + str(round(sm['vehicleParameters'].steerRatio, 2)), YELLOW), ] for i, line in enumerate(lines): diff --git a/tools/scripts/car/max_lat_accel.py b/tools/scripts/car/max_lat_accel.py index dc44e8ac40..b76b3302cc 100755 --- a/tools/scripts/car/max_lat_accel.py +++ b/tools/scripts/car/max_lat_accel.py @@ -62,8 +62,8 @@ def find_events(lr: LogReader, extrapolate: bool = False, qlog: bool = False) -> elif msg.which() == 'controlsState': curvature = msg.controlsState.curvature - elif msg.which() == 'liveParameters': - roll = msg.liveParameters.roll + elif msg.which() == 'vehicleParameters': + roll = msg.vehicleParameters.roll if lat_active > min_lat_active and steering_unpressed > min_steering_unpressed and requesting_max > min_requesting_max: # TODO: record max lat accel at the end of the event, need to use the past lat accel as overriding can happen before we detect it diff --git a/tools/scripts/cycle_alerts.py b/tools/scripts/cycle_alerts.py index cf4a1d8999..f78dfe1090 100755 --- a/tools/scripts/cycle_alerts.py +++ b/tools/scripts/cycle_alerts.py @@ -54,8 +54,8 @@ def cycle_alerts(duration=200, is_metric=False): CS = car.CarState.new_message() CP = CarInterface.get_non_essential_params("HONDA_CIVIC") - sm = messaging.SubMaster(['deviceState', 'pandaStates', 'narrowRoadCameraState', 'modelV2', 'liveCalibration', - 'driverMonitoringState', 'longitudinalPlan', 'livePose', + sm = messaging.SubMaster(['deviceState', 'pandaStates', 'narrowRoadCameraState', 'modelV2', 'extrinsicsCalibration', + 'driverMonitoringState', 'longitudinalPlan', 'deviceMotion', 'managerState'] + cameras) pm = messaging.PubMaster(['selfdriveState', 'pandaStates', 'deviceState']) @@ -87,7 +87,7 @@ def cycle_alerts(duration=200, is_metric=False): procs[i].shouldBeRunning = True sm['managerState'].processes = procs - sm['liveCalibration'].rpyCalib = [-1 * random.random() for _ in range(random.randint(0, 3))] + sm['extrinsicsCalibration'].rpyCalib = [-1 * random.random() for _ in range(random.randint(0, 3))] for s in sm.data.keys(): prob = 0.3 if s in cameras else 0.08 From eecff738506eafb38debb527c42cb65426aa2e02 Mon Sep 17 00:00:00 2001 From: Vraj Parikh <58668812+TheConverseEngineer@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:21:16 -0700 Subject: [PATCH 211/325] Sleep shadow mode (#38578) * dmvision c703670b-6a4a-4743-8f79-69d5486cadf7 (a62f9948 with new sleep head) * dmvision c703670b-6a4a-4743-8f79-69d5486cadf7 (a62f9948 with new sleep head and same batchnorms) --- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx index 87a116cdba..1e592e2192 100644 --- a/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:607ed8f64a2d756657b621aaccf92a5cadb296b0a1d13d11605766901510cef1 -size 7486861 +oid sha256:dd299afabe7a3e0d04cbe2bd97fdb0c93bba8ad6d3cc3663a0e0ededaf243ac2 +size 7497335 From b5b156825d437483b0fdd54fa820caff938c9535 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 10 Aug 2026 20:02:52 -0700 Subject: [PATCH 212/325] ui: fix inverted DM head tracking on RHD (#38607) * ui: fix inverted DM head tracking on RHD b29d0a17af switched the mici driver state renderer from computing pose from driverStateV2 itself to consuming driverMonitoringState.visionPolicyState.pose. That published yaw is mirrored for RHD (policy.py:269) so the asymmetric yaw offsets/thresholds can be tuned in a single frame, so the UI's unconditional sign flip left RHD head tracking backwards. LHD was unaffected, which is why the refactor looked like a no-op. Undo the mirror on the UI side. * Apply suggestions from code review * fold the sign into the RHD multiplier --- openpilot/selfdrive/ui/mici/onroad/driver_state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/driver_state.py b/openpilot/selfdrive/ui/mici/onroad/driver_state.py index 85b90704ac..ad5a521518 100644 --- a/openpilot/selfdrive/ui/mici/onroad/driver_state.py +++ b/openpilot/selfdrive/ui/mici/onroad/driver_state.py @@ -169,8 +169,8 @@ class DriverStateRenderer(Widget): self._is_rhd = dm_state.isRHD self._face_detected = dm_state.visionPolicyState.faceDetected self._awareness_unfull = self.effective_active and dm_state.visionPolicyState.awarenessPercent < self.AWARENESS_UNFULL_PERCENT - self._face_pitch = dm_state.visionPolicyState.pose.pitch + math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward - self._face_yaw = -dm_state.visionPolicyState.pose.yaw # undo sign flip in face_orientation_from_model to match UI convention + self._face_pitch = dm_state.visionPolicyState.pose.pitch + math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward + self._face_yaw = dm_state.visionPolicyState.pose.yaw * (1 if self._is_rhd else -1) # undo sign flip in face_orientation_from_model to match UI convention driverstate = sm["driverStateV2"] driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData From b1cdf387b5779d4bdc585122b085c4cd3990b85f Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Mon, 10 Aug 2026 21:23:59 -0700 Subject: [PATCH 213/325] sync after a build (#38606) * build: sync after a successful build * sync on failed builds too --- openpilot/system/manager/build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpilot/system/manager/build.py b/openpilot/system/manager/build.py index 75a7dc63eb..c5fdd6d92a 100755 --- a/openpilot/system/manager/build.py +++ b/openpilot/system/manager/build.py @@ -50,6 +50,8 @@ def build() -> None: if scons.returncode == 0: break + os.sync() + if scons.returncode != 0: # Build failed log errors error_s = b"\n".join(compile_output).decode('utf8', 'replace') From 122a8ca00ab4ab78d62adeaf4fe26f7e5253815e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 10 Aug 2026 22:36:54 -0700 Subject: [PATCH 214/325] camerad: improve IFE robustness to lags (#38548) camerad: keep request IDs monotonic --- openpilot/system/camerad/cameras/spectra.cc | 25 ++++++++++++--------- openpilot/system/camerad/cameras/spectra.h | 5 +++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/openpilot/system/camerad/cameras/spectra.cc b/openpilot/system/camerad/cameras/spectra.cc index f2d849ae28..d4c3e5c646 100644 --- a/openpilot/system/camerad/cameras/spectra.cc +++ b/openpilot/system/camerad/cameras/spectra.cc @@ -297,7 +297,7 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { LOGD("camera init %d", cc.camera_num); buf.init(this, v, ife_buf_depth, cc.stream_type); camera_map_bufs(); - clearAndRequeue(1); + clearAndRequeue(); } void SpectraCamera::sensors_start() { @@ -942,7 +942,10 @@ void SpectraCamera::config_ife(int idx, int request_id, bool init) { assert(ret == 0); } -void SpectraCamera::enqueue_frame(uint64_t request_id) { +void SpectraCamera::enqueue_frame() { + // The kernel reports only requests newer than reported_req_id, which a flush does not reset. + // https://github.com/commaai/agnos-kernel-sdm845/blob/93ddd472ce522ab8669456b11bf3924ad32e9882/drivers/media/platform/msm/camera/cam_isp/cam_isp_context.c#L609-L613 + uint64_t request_id = next_request_id++; int i = request_id % ife_buf_depth; assert(sync_objs_ife[i] == 0); @@ -1428,14 +1431,14 @@ bool SpectraCamera::handle_camera_event(const cam_req_mgr_message *event_data) { if (!waitForFrameReady(request_id)) { // Reset queue on sync failure to prevent frame tearing LOGE("camera %d sync failure %ld %ld ", cc.camera_num, request_id, ife_frame_id); - clearAndRequeue(request_id + 1); + clearAndRequeue(); return false; } int buf_idx = request_id % ife_buf_depth; bool ret = processFrame(buf_idx, request_id, ife_frame_id, timestamp); destroySyncObjectAt(buf_idx); - enqueue_frame(request_id + ife_buf_depth); // request next frame for this slot + enqueue_frame(); // request next frame for this slot return ret; } @@ -1445,7 +1448,7 @@ bool SpectraCamera::validateEvent(uint64_t request_id, uint64_t ife_frame_id) { if (request_id == 0) { if (invalid_request_count++ > ife_buf_depth+2) { LOGE("camera %d reset after half second of invalid requests", cc.camera_num); - clearAndRequeue(last_valid_request_id + 1); + clearAndRequeue(); invalid_request_count = 0; } return false; @@ -1456,26 +1459,26 @@ bool SpectraCamera::validateEvent(uint64_t request_id, uint64_t ife_frame_id) { if (!skip_expected) { if (ife_frame_id != last_valid_ife_frame_id + 1) { LOGE("camera %d frame ID skipped, %lu -> %lu", cc.camera_num, last_valid_ife_frame_id, ife_frame_id); - clearAndRequeue(request_id + 1); + clearAndRequeue(); return false; } if (request_id != last_valid_request_id + 1) { LOGE("camera %d requests skipped %ld -> %ld", cc.camera_num, last_valid_request_id, request_id); - clearAndRequeue(request_id + 1); + clearAndRequeue(); return false; } } return true; } -void SpectraCamera::clearAndRequeue(uint64_t from_request_id) { +void SpectraCamera::clearAndRequeue() { // clear everything, then queue up a fresh set of frames - LOGW("clearing and requeuing camera %d from %lu", cc.camera_num, from_request_id); + LOGW("clearing and requeuing camera %d from %lu", cc.camera_num, next_request_id); clear_req_queue(); last_requeue_ts = nanos_since_boot(); - for (uint64_t id = from_request_id; id < from_request_id + ife_buf_depth; ++id) { - enqueue_frame(id); + for (int i = 0; i < ife_buf_depth; ++i) { + enqueue_frame(); } skip_expected = true; } diff --git a/openpilot/system/camerad/cameras/spectra.h b/openpilot/system/camerad/cameras/spectra.h index acb2000313..ebb2d3f1de 100644 --- a/openpilot/system/camerad/cameras/spectra.h +++ b/openpilot/system/camerad/cameras/spectra.h @@ -144,7 +144,7 @@ public: void config_ife(int idx, int request_id, bool init=false); int clear_req_queue(); - void enqueue_frame(uint64_t request_id); + void enqueue_frame(); int sensors_init(); void sensors_start(); @@ -205,6 +205,7 @@ public: int buf_handle_raw[MAX_IFE_BUFS] = {}; int sync_objs_ife[MAX_IFE_BUFS] = {}; int sync_objs_bps[MAX_IFE_BUFS] = {}; + uint64_t next_request_id = 1; uint64_t last_valid_request_id = 0; uint64_t last_requeue_ts = 0; uint64_t last_valid_ife_frame_id = 0; @@ -215,7 +216,7 @@ public: SpectraMaster *m; private: - void clearAndRequeue(uint64_t from_request_id); + void clearAndRequeue(); bool validateEvent(uint64_t request_id, uint64_t ife_frame_id); bool waitForFrameReady(uint64_t request_id); bool processFrame(int buf_idx, uint64_t request_id, uint64_t ife_frame_id, uint64_t timestamp); From 062cb672e8cbdb8b905b85120f375779fc6d2c9c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 11 Aug 2026 00:39:51 -0700 Subject: [PATCH 215/325] camerad: force BPS clock to max (#38539) * camerad: account for BPS clock overhead * force to max --- openpilot/system/camerad/cameras/spectra.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/system/camerad/cameras/spectra.cc b/openpilot/system/camerad/cameras/spectra.cc index d4c3e5c646..0ce3867034 100644 --- a/openpilot/system/camerad/cameras/spectra.cc +++ b/openpilot/system/camerad/cameras/spectra.cc @@ -609,7 +609,7 @@ void SpectraCamera::config_bps(int idx, int request_id) { tmp.header = CAM_ICP_CMD_GENERIC_BLOB_CLK; tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8; tmp.clk.budget_ns = 0x1fca058; - tmp.clk.frame_cycles = sensor->frame_width * sensor->frame_height; // matches striping lib pixelCount + tmp.clk.frame_cycles = 20000000; // force max BPS clock (600 MHz) tmp.clk.rt_flag = 0x0; tmp.clk.uncompressed_bw = 0x38512180; tmp.clk.compressed_bw = 0x38512180; From 2d859a8ca610bc20f48b0b3e4f7d570a80b59c29 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:29:14 -0700 Subject: [PATCH 216/325] Controls: Road Edge Lane Change Controller (#1085) * relc * so picky * clean up * switch to distance based * same calls * all you brother i'm just here to red diff --------- Co-authored-by: Jason Wen --- openpilot/cereal/custom.capnp | 3 + openpilot/common/params_keys.h | 1 + .../selfdrive/controls/lib/desire_helper.py | 6 +- openpilot/selfdrive/modeld/modeld.py | 5 +- openpilot/selfdrive/selfdrived/selfdrived.py | 7 + .../lane_change_settings.py | 7 + openpilot/sunnypilot/modeld_v2/modeld.py | 5 +- .../selfdrive/controls/lib/dec/dec.py | 2 +- .../sunnypilot/selfdrive/controls/lib/relc.py | 98 ++++++++++ .../lib/tests/test_lane_turn_desire.py | 19 +- .../selfdrive/controls/lib/tests/test_relc.py | 169 ++++++++++++++++++ .../sunnypilot/selfdrive/selfdrived/events.py | 8 + .../sunnypilot/sunnylink/settings_ui.json | 6 + .../settings_ui_src/pages/steering.yaml | 4 + openpilot/system/manager/process_config.py | 2 +- 15 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/relc.py create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index a77997ffbb..c20bf923be 100644 --- a/openpilot/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -351,6 +351,7 @@ struct OnroadEventSP @0xda96579883444c35 { speedLimitChanged @21; speedLimitPending @22; e2eChime @23; + laneChangeRoadEdge @24; } } @@ -457,6 +458,8 @@ struct LiveMapDataSP @0xf416ec09499d9d19 { struct ModelDataV2SP @0xa1680744031fdb2d { laneTurnDirection @0 :TurnDirection; + leftLaneChangeEdgeBlock @1 :Bool; + rightLaneChangeEdgeBlock @2 :Bool; enum TurnDirection { none @0; diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 198b7a92a1..7df58a0ba9 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -179,6 +179,7 @@ inline static std::unordered_map keys = { {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}}, {"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}}, diff --git a/openpilot/selfdrive/controls/lib/desire_helper.py b/openpilot/selfdrive/controls/lib/desire_helper.py index df4e5c56ab..334b360a62 100644 --- a/openpilot/selfdrive/controls/lib/desire_helper.py +++ b/openpilot/selfdrive/controls/lib/desire_helper.py @@ -33,7 +33,7 @@ class DesireHelper: def get_lane_change_direction(CS): return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right - def update(self, carstate, lateral_active, lane_change_prob): + def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False): self.alc.update_params() self.lane_turn_controller.update_params() v_ego = carstate.vEgo @@ -64,8 +64,8 @@ class DesireHelper: ((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or (carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right)) - blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or - (carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right)) + blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or + ((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right)) self.alc.update_lane_change(blindspot_detected, carstate.brakePressed) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 1ca1a71f35..5dc8712112 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -29,6 +29,7 @@ from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase +from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -213,6 +214,7 @@ def main(demo=False): prev_action = log.ModelDataV2.Action() DH = DesireHelper() + RELC = RoadEdgeLaneChangeController() while True: # Keep receiving frames until we are at least 1 frame ahead of previous extra frame @@ -313,7 +315,8 @@ def main(demo=False): l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] r_lane_change_prob = desire_state[log.Desire.laneChangeRight] lane_change_prob = l_lane_change_prob + r_lane_change_prob - DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego) + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge) modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 2bc0574e81..8035b2934e 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -327,9 +327,16 @@ class SelfdriveD(CruiseHelper): # Handle lane change if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange: direction = self.sm['modelV2'].meta.laneChangeDirection + mdv2sp = self.sm['modelDataV2SP'] + if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \ (CS.rightBlindspot and direction == LaneChangeDirection.right): self.events.add(EventName.laneChangeBlocked) + + elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \ + (mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right): + self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge) + else: if direction == LaneChangeDirection.left: self.events.add(EventName.preLaneChangeLeft) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py index fbb9ce7cf7..82419a5567 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/steering_sub_layouts/lane_change_settings.py @@ -51,11 +51,18 @@ class LaneChangeSettingsLayout(Widget): description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " + "(BSM) detects a obstructing vehicle, ensuring safe maneuvering."), ) + self._road_edge_block = toggle_item_sp( + param="RoadEdgeLaneChangeEnabled", + title=lambda: tr("Block Lane Change: Road Edge Detection"), + description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."), + ) items = [ self._lane_change_timer, LineSeparatorSP(40), self._bsm_delay, + LineSeparatorSP(40), + self._road_edge_block, ] return items diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index b53ab18c73..9f3a75a662 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -49,6 +49,7 @@ from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, mak from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle +from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad" @@ -367,6 +368,7 @@ def main(demo=False): DH = DesireHelper() meta_constants = load_meta_constants() + RELC = RoadEdgeLaneChangeController() while True: # Keep receiving frames until we are at least 1 frame ahead of previous extra frame @@ -479,7 +481,8 @@ def main(demo=False): l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] r_lane_change_prob = desire_state[log.Desire.laneChangeRight] lane_change_prob = l_lane_change_prob + r_lane_change_prob - DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) + left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego) + DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge) modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py index 1ba5ab0618..fb854edae8 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -1,5 +1,5 @@ """ -Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors. +Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/relc.py b/openpilot/sunnypilot/selfdrive/controls/lib/relc.py new file mode 100644 index 0000000000..031e751b43 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/relc.py @@ -0,0 +1,98 @@ +""" +Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +from openpilot.common.constants import CV +from openpilot.common.realtime import DT_MDL +from openpilot.common.params import Params + +NEARSIDE_PROB = 0.2 +EDGE_PROB = 0.35 +EDGE_REACTION_TIME = 1.0 +EDGE_CLEAR_TIME = 0.3 +MIN_SPEED = 20 * CV.MPH_TO_MS +VEHICLE_EDGE_MARGIN = 1.08 +EDGE_CLEARANCE = 3.7 + + +class RoadEdgeLaneChangeController: + def __init__(self): + self.params = Params() + self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled") + self.param_read_counter = 0 + self.left_edge_detected = False + self.right_edge_detected = False + self.left_edge_timer = 0.0 + self.right_edge_timer = 0.0 + self.left_clear_timer = 0.0 + self.right_clear_timer = 0.0 + + def read_params(self) -> None: + self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled") + + def update_params(self) -> None: + if self.param_read_counter % 50 == 0: + self.read_params() + self.param_read_counter += 1 + + def reset(self) -> None: + self.left_edge_detected = False + self.right_edge_detected = False + self.left_edge_timer = 0.0 + self.right_edge_timer = 0.0 + self.left_clear_timer = 0.0 + self.right_clear_timer = 0.0 + + def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None: + self.update_params() + + if not self.enabled or v_ego < MIN_SPEED: + self.reset() + return + + left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0) + right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0) + left_lane_prob = lane_line_probs[0] + right_lane_prob = lane_line_probs[3] + + if road_edges is not None and len(road_edges) == 2 and len(road_edges[0].y) > 0 and len(road_edges[1].y) > 0: + left_clearance = abs(road_edges[0].y[0]) - VEHICLE_EDGE_MARGIN + right_clearance = abs(road_edges[1].y[0]) - VEHICLE_EDGE_MARGIN + else: + left_clearance = 0.0 + right_clearance = 0.0 + + left_cond = left_edge_prob > EDGE_PROB and left_lane_prob < NEARSIDE_PROB and left_clearance < EDGE_CLEARANCE + right_cond = right_edge_prob > EDGE_PROB and right_lane_prob < NEARSIDE_PROB and right_clearance < EDGE_CLEARANCE + + if left_cond: + self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME) + self.left_clear_timer = 0.0 + if self.left_edge_timer > EDGE_REACTION_TIME: + self.left_edge_detected = True + else: + self.left_clear_timer += DT_MDL + if self.left_clear_timer > EDGE_CLEAR_TIME: + self.left_edge_timer = 0.0 + self.left_edge_detected = False + + if right_cond: + self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME) + self.right_clear_timer = 0.0 + if self.right_edge_timer > EDGE_REACTION_TIME: + self.right_edge_detected = True + else: + self.right_clear_timer += DT_MDL + if self.right_clear_timer > EDGE_CLEAR_TIME: + self.right_edge_timer = 0.0 + self.right_edge_detected = False + + def update_and_fill(self, modelv2, mdv2sp, v_ego): + self.update(modelv2.roadEdgeStds, modelv2.laneLineProbs, v_ego, modelv2.roadEdges) + mdv2sp.leftLaneChangeEdgeBlock = self.left_edge_detected + mdv2sp.rightLaneChangeEdgeBlock = self.right_edge_detected + return self.left_edge_detected, self.right_edge_detected diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py index c3e96fd778..434b12110b 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py @@ -2,10 +2,11 @@ import pytest from openpilot.cereal import log, custom from openpilot.common.params import Params -from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode + TurnDirection = custom.ModelDataV2SP.TurnDirection @@ -109,5 +110,17 @@ def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, e dh = DesireHelper() dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE for _ in range(10): - dh.update(carstate, lateral_active, lane_change_prob) - assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers + dh.update(carstate, lateral_active, lane_change_prob, + left_edge_detected=False, right_edge_detected=False) + assert dh.desire == expected_desire + + +def test_edge_blocks_lane_change(set_lane_turn_params): + dh = DesireHelper() + dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE + carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1) + for _ in range(10): + dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False) + assert dh.lane_change_state == LaneChangeState.preLaneChange + assert dh.lane_change_direction == LaneChangeDirection.left + assert dh.desire == log.Desire.none diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py new file mode 100644 index 0000000000..71b153c8b7 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py @@ -0,0 +1,169 @@ +""" +Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pytest + +from openpilot.common.realtime import DT_MDL +from openpilot.sunnypilot.selfdrive.controls.lib.relc import ( + RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED, + VEHICLE_EDGE_MARGIN, EDGE_CLEARANCE, +) + +V_HIGH = MIN_SPEED + 2.0 +V_LOW = MIN_SPEED - 1.0 + + +class MockEdge: + def __init__(self, y_val): + self.y = [y_val] * 33 + + +def edges(left_y, right_y): + return [MockEdge(left_y), MockEdge(right_y)] + + +CLOSE_EDGES = edges(-2.0, 1.5) +FAR_EDGES = edges(-10.0, 10.0) + + +@pytest.fixture +def relc(mocker): + mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params") + controller = RoadEdgeLaneChangeController() + controller.enabled = True + return controller + + +def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=CLOSE_EDGES): + for _ in range(int(seconds / DT_MDL) + 1): + controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges) + + +@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [ + ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"), + ([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"), +]) +def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr): + drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1) + assert getattr(relc, attr) + + +def test_edge_detection_requires_time(relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05) + assert not relc.left_edge_detected + + +def test_both_edges_detected(relc): + drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + assert relc.right_edge_detected + + +def test_noise_doesnt_clear(relc): + edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) + clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) + + drive(relc, *edge, EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + + relc.update(*clear, V_HIGH, CLOSE_EDGES) + relc.update(*edge, V_HIGH, CLOSE_EDGES) + assert relc.left_edge_detected + + +def test_clears_after_window(relc): + edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) + clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) + + drive(relc, *edge, EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + + drive(relc, *clear, EDGE_CLEAR_TIME + 0.05) + assert not relc.left_edge_detected + assert relc.left_edge_timer == 0.0 + + +def test_low_speed_skips(relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW) + assert not relc.left_edge_detected + assert relc.left_edge_timer == 0.0 + + +def test_speed_drop_resets(relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + + relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES) + assert not relc.left_edge_detected + + +def test_param_off_resets(relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + + relc.params.get_bool.return_value = False + relc.read_params() + relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES) + assert not relc.left_edge_detected + assert not relc.right_edge_detected + + +def test_lane_line_prevents_detection(relc): + drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert not relc.left_edge_detected + + +def test_one_side_blocks_other_allows(relc): + drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert relc.right_edge_detected + assert not relc.left_edge_detected + + +def test_disabled_no_detection(relc): + relc.enabled = False + relc.params.get_bool.return_value = False + drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert not relc.left_edge_detected + assert not relc.right_edge_detected + + +def test_far_edge_no_block(relc): + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES) + assert not relc.left_edge_detected + + +def test_close_edge_blocks(relc): + drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-8.0, 1.5)) + assert relc.right_edge_detected + assert not relc.left_edge_detected + + +def test_wide_road_no_lines_no_block(relc): + drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-8.0, 8.0)) + assert not relc.left_edge_detected + assert not relc.right_edge_detected + + +def test_narrow_road_both_block(relc): + drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-2.5, 2.5)) + assert relc.left_edge_detected + assert relc.right_edge_detected + + +def test_clearance_boundary(relc): + boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-(boundary - 0.1), 10.0)) + assert relc.left_edge_detected + + relc.reset() + + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-(boundary + 0.1), 10.0)) + assert not relc.left_edge_detected diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/events.py b/openpilot/sunnypilot/selfdrive/selfdrived/events.py index 2001d0dbee..3c010cc776 100644 --- a/openpilot/sunnypilot/selfdrive/selfdrived/events.py +++ b/openpilot/sunnypilot/selfdrive/selfdrived/events.py @@ -244,4 +244,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { AlertStatus.normal, AlertSize.none, Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.), }, + + EventNameSP.laneChangeRoadEdge: { + ET.WARNING: Alert( + "Lane Change Unavailable: Road Edge", + "", + AlertStatus.userPrompt, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1), + }, } diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index dd972c2957..63bf342fcb 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -545,6 +545,12 @@ } ] }, + { + "key": "RoadEdgeLaneChangeEnabled", + "widget": "toggle", + "title": "Block Lane Change: Road Edge Detection", + "description": "Blocks lane change when the model sees a road edge on the side you signal." + }, { "key": "AutoLaneChangeBsmDelay", "widget": "toggle", diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml index a09796ab7a..73d46681a6 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/steering.yaml @@ -257,6 +257,10 @@ sections: label: 2 seconds - value: 5 label: 3 seconds + - key: RoadEdgeLaneChangeEnabled + widget: toggle + title: 'Block Lane Change: Road Edge Detection' + description: Blocks lane change when the model sees a road edge on the side you signal. - key: AutoLaneChangeBsmDelay widget: toggle title: 'Auto Lane Change: Delay with Blind Spot' diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index a72675385d..08e1aabef3 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -88,7 +88,7 @@ def use_sunnylink_uploader_shim(started, params, CP: car.CarParams) -> bool: return use_sunnylink_uploader(params) def is_tinygrad_model(started, params, CP: car.CarParams) -> bool: - """Check if the active model runner is SNPE.""" + """Check if the active model runner is tinygrad.""" return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.tinygrad) def is_stock_model(started, params, CP: car.CarParams) -> bool: From 32b518d51d7660d59ad4b057c90d8c747a16badd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 11 Aug 2026 17:47:53 -0700 Subject: [PATCH 217/325] set long maneuvers to actuation limits --- openpilot/tools/longitudinal_maneuvers/maneuversd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/tools/longitudinal_maneuvers/maneuversd.py b/openpilot/tools/longitudinal_maneuvers/maneuversd.py index 0e20840274..cd5ebe76bc 100755 --- a/openpilot/tools/longitudinal_maneuvers/maneuversd.py +++ b/openpilot/tools/longitudinal_maneuvers/maneuversd.py @@ -117,8 +117,8 @@ MANEUVERS = [ initial_speed=20. * CV.MPH_TO_MS, ), Maneuver( - "brake step response: -4m/s^2 from 20mph", - [Action([-4], [3])], + "brake step response: -3.5m/s^2 from 20mph", + [Action([-3.5], [3])], repeat=2, initial_speed=20. * CV.MPH_TO_MS, ), @@ -129,8 +129,8 @@ MANEUVERS = [ initial_speed=20. * CV.MPH_TO_MS, ), Maneuver( - "gas step response: +4m/s^2 from 20mph", - [Action([4], [3])], + "gas step response: +2m/s^2 from 20mph", + [Action([2], [3])], repeat=2, initial_speed=20. * CV.MPH_TO_MS, ), From ad5afe222d543c59522906f0d2eae438d321bdb1 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 11 Aug 2026 18:09:25 -0700 Subject: [PATCH 218/325] log chestnut ASM at 10Hz (#38609) * modeld: only read the smu while the big model runs * chestnutState at 10Hz * smu polling --- openpilot/cereal/services.py | 2 +- openpilot/selfdrive/modeld/modeld.py | 41 ++++++++++++++++++---------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index ebe8be1d60..08633d6975 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -25,7 +25,7 @@ _services: dict[str, tuple] = { "accelerometer": (True, 104., 104), "temperatureSensor": (True, 2., 200), "deviceState": (True, 2., 1), - "chestnutState": (True, 0.1, 1), + "chestnutState": (True, 10., 10), "touch": (True, 20., 1), "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "controlsState": (True, 100., 10, QueueSize.MEDIUM), diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index e87d4f3111..7f049912dd 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -71,9 +71,12 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. class ChestnutState: # only modeld can access chestnut - def __init__(self, pm: PubMaster): + def __init__(self, pm: PubMaster, big: bool): self.pm = pm + self.big = big self.valid = True + self.sends = 0 + self.metrics = {} @cached_property def power_limit(self) -> int: @@ -83,33 +86,41 @@ class ChestnutState: def send(self) -> None: msg = messaging.new_message('chestnutState') state = msg.chestnutState - valid = False - if "AMD" in Device._opened_devices: + self.sends += 1 + if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: smu = Device["AMD"].iface.dev_impl.smu smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics - state.tempC = metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT] - state.memoryTempC = metrics.AvgTemperature[smu.smu_mod.TEMP_MEM] - state.powerDrawW = metrics.AverageSocketPower - state.powerLimitW = self.power_limit - state.gpuUsagePercent = metrics.AverageGfxActivity - state.gpuClockMhz = metrics.AverageGfxclkFrequencyPostDs - state.fanSpeedRpm = metrics.AvgFanRpm - valid = True + self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], + 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], + 'powerDrawW': metrics.AverageSocketPower, + 'powerLimitW': self.power_limit, + 'gpuUsagePercent': metrics.AverageGfxActivity, + 'gpuClockMhz': metrics.AverageGfxclkFrequencyPostDs, + 'fanSpeedRpm': metrics.AvgFanRpm} + self.valid = True except Exception: if self.valid: cloudlog.exception("chestnut state read failed") + self.valid = False + self.metrics.clear() + if self.big: + for k, v in self.metrics.items(): + setattr(state, k, v) + + asm_valid = False + if "AMD" in Device._opened_devices: try: # ASM runs on USB-C power, these still read without a gpu asm = Device["AMD"].iface.pci_dev.usb state.pcieLtssm = asm.read(0xB450, 1)[0] state.supplyVoltage, state.supplyCurrent = struct.unpack(' Date: Tue, 11 Aug 2026 18:13:02 -0700 Subject: [PATCH 219/325] Revert "camerad: improve IFE robustness to lags (#38548)" This reverts commit 122a8ca00ab4ab78d62adeaf4fe26f7e5253815e. --- openpilot/system/camerad/cameras/spectra.cc | 25 +++++++++------------ openpilot/system/camerad/cameras/spectra.h | 5 ++--- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/openpilot/system/camerad/cameras/spectra.cc b/openpilot/system/camerad/cameras/spectra.cc index 0ce3867034..2cbf3594c4 100644 --- a/openpilot/system/camerad/cameras/spectra.cc +++ b/openpilot/system/camerad/cameras/spectra.cc @@ -297,7 +297,7 @@ void SpectraCamera::camera_open(VisionIpcServer *v) { LOGD("camera init %d", cc.camera_num); buf.init(this, v, ife_buf_depth, cc.stream_type); camera_map_bufs(); - clearAndRequeue(); + clearAndRequeue(1); } void SpectraCamera::sensors_start() { @@ -942,10 +942,7 @@ void SpectraCamera::config_ife(int idx, int request_id, bool init) { assert(ret == 0); } -void SpectraCamera::enqueue_frame() { - // The kernel reports only requests newer than reported_req_id, which a flush does not reset. - // https://github.com/commaai/agnos-kernel-sdm845/blob/93ddd472ce522ab8669456b11bf3924ad32e9882/drivers/media/platform/msm/camera/cam_isp/cam_isp_context.c#L609-L613 - uint64_t request_id = next_request_id++; +void SpectraCamera::enqueue_frame(uint64_t request_id) { int i = request_id % ife_buf_depth; assert(sync_objs_ife[i] == 0); @@ -1431,14 +1428,14 @@ bool SpectraCamera::handle_camera_event(const cam_req_mgr_message *event_data) { if (!waitForFrameReady(request_id)) { // Reset queue on sync failure to prevent frame tearing LOGE("camera %d sync failure %ld %ld ", cc.camera_num, request_id, ife_frame_id); - clearAndRequeue(); + clearAndRequeue(request_id + 1); return false; } int buf_idx = request_id % ife_buf_depth; bool ret = processFrame(buf_idx, request_id, ife_frame_id, timestamp); destroySyncObjectAt(buf_idx); - enqueue_frame(); // request next frame for this slot + enqueue_frame(request_id + ife_buf_depth); // request next frame for this slot return ret; } @@ -1448,7 +1445,7 @@ bool SpectraCamera::validateEvent(uint64_t request_id, uint64_t ife_frame_id) { if (request_id == 0) { if (invalid_request_count++ > ife_buf_depth+2) { LOGE("camera %d reset after half second of invalid requests", cc.camera_num); - clearAndRequeue(); + clearAndRequeue(last_valid_request_id + 1); invalid_request_count = 0; } return false; @@ -1459,26 +1456,26 @@ bool SpectraCamera::validateEvent(uint64_t request_id, uint64_t ife_frame_id) { if (!skip_expected) { if (ife_frame_id != last_valid_ife_frame_id + 1) { LOGE("camera %d frame ID skipped, %lu -> %lu", cc.camera_num, last_valid_ife_frame_id, ife_frame_id); - clearAndRequeue(); + clearAndRequeue(request_id + 1); return false; } if (request_id != last_valid_request_id + 1) { LOGE("camera %d requests skipped %ld -> %ld", cc.camera_num, last_valid_request_id, request_id); - clearAndRequeue(); + clearAndRequeue(request_id + 1); return false; } } return true; } -void SpectraCamera::clearAndRequeue() { +void SpectraCamera::clearAndRequeue(uint64_t from_request_id) { // clear everything, then queue up a fresh set of frames - LOGW("clearing and requeuing camera %d from %lu", cc.camera_num, next_request_id); + LOGW("clearing and requeuing camera %d from %lu", cc.camera_num, from_request_id); clear_req_queue(); last_requeue_ts = nanos_since_boot(); - for (int i = 0; i < ife_buf_depth; ++i) { - enqueue_frame(); + for (uint64_t id = from_request_id; id < from_request_id + ife_buf_depth; ++id) { + enqueue_frame(id); } skip_expected = true; } diff --git a/openpilot/system/camerad/cameras/spectra.h b/openpilot/system/camerad/cameras/spectra.h index ebb2d3f1de..acb2000313 100644 --- a/openpilot/system/camerad/cameras/spectra.h +++ b/openpilot/system/camerad/cameras/spectra.h @@ -144,7 +144,7 @@ public: void config_ife(int idx, int request_id, bool init=false); int clear_req_queue(); - void enqueue_frame(); + void enqueue_frame(uint64_t request_id); int sensors_init(); void sensors_start(); @@ -205,7 +205,6 @@ public: int buf_handle_raw[MAX_IFE_BUFS] = {}; int sync_objs_ife[MAX_IFE_BUFS] = {}; int sync_objs_bps[MAX_IFE_BUFS] = {}; - uint64_t next_request_id = 1; uint64_t last_valid_request_id = 0; uint64_t last_requeue_ts = 0; uint64_t last_valid_ife_frame_id = 0; @@ -216,7 +215,7 @@ public: SpectraMaster *m; private: - void clearAndRequeue(); + void clearAndRequeue(uint64_t from_request_id); bool validateEvent(uint64_t request_id, uint64_t ife_frame_id); bool waitForFrameReady(uint64_t request_id); bool processFrame(int buf_idx, uint64_t request_id, uint64_t ife_frame_id, uint64_t timestamp); From 473ab1787692a8a61df7b78fa5d84893f5bbb519 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 11 Aug 2026 18:15:10 -0700 Subject: [PATCH 220/325] add cupra to release notes --- RELEASES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 446ec88093..36ddadb431 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,4 +1,4 @@ -Version 0.11.2 (2026-08-11) +Version 0.11.2 (2026-08-12) ======================= * New driving model * Big model with 880M parameters @@ -7,6 +7,7 @@ Version 0.11.2 (2026-08-11) * Generate dashcam clips from comma connect * Remote comma body control from comma connect * New alert sounds +* CUPRA Born 2021-2023 support thanks to DaHansi! * Volkswagen ID.4 2021-2025 support thanks to DaHansi! Version 0.11.1 (2026-05-18) From f1746f2e2de9c92ec2fc1bccacf6c9c715a66f96 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 11 Aug 2026 20:42:08 -0700 Subject: [PATCH 221/325] fix spinner crash (#38612) ui: keep spinner independent of build artifacts --- openpilot/system/ui/widgets/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/system/ui/widgets/__init__.py b/openpilot/system/ui/widgets/__init__.py index 9904dfde2c..4e13920d60 100644 --- a/openpilot/system/ui/widgets/__init__.py +++ b/openpilot/system/ui/widgets/__init__.py @@ -16,7 +16,7 @@ def _get_device() -> DeviceLike: try: from openpilot.selfdrive.ui.ui_state import device return device - except ImportError: + except (ImportError, OSError): class Device: awake = True return Device() From 927e822b5a8bcc2448ec1572c733b7f92ca5bf35 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 11 Aug 2026 21:25:45 -0700 Subject: [PATCH 222/325] fix tap to factory reset (#38613) hardware: lazily import optional SIM support --- openpilot/common/hardware/comma/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/common/hardware/comma/hardware.py b/openpilot/common/hardware/comma/hardware.py index dfbf1e3f17..effb4d9dbe 100644 --- a/openpilot/common/hardware/comma/hardware.py +++ b/openpilot/common/hardware/comma/hardware.py @@ -12,7 +12,6 @@ from openpilot.common.utils import sudo_read, sudo_write from openpilot.common.gpio import gpio_set, gpio_init, get_irqs_for_action from openpilot.common.esim.base import LPABase from openpilot.common.hardware.base import HardwareBase, ThermalConfig, ThermalZone -from openpilot.common.esim.lpa import LPA from openpilot.common.hardware.comma.pins import GPIO from openpilot.common.hardware.comma.amplifier import Amplifier @@ -155,6 +154,7 @@ class HardwareComma(HardwareBase): } def get_sim_lpa(self) -> LPABase: + from openpilot.common.esim.lpa import LPA return LPA() def get_imei(self): From 7a76a50c4fb6619923fe826be2a14d51af342c08 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 11 Aug 2026 21:48:27 -0700 Subject: [PATCH 223/325] Add release-chestnut branch --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0f248cf35a..6c0d944a2e 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,12 @@ We have detailed instructions for [how to install the harness and device in a ca Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches: -| comma four branch | comma 3X branch | URL | description | -|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| -| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | -| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | +| comma four branch | comma four + chestnut branch | comma 3X branch | URL | description | +|------------------------|------------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| +| `release-mici` | `release-chestnut` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | +| `release-mici-staging` | | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly` | | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-dev` | | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | To start developing openpilot ------ From 10502adf953a1dae835d2356186fdb04a0d7cf40 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 11 Aug 2026 23:27:16 -0700 Subject: [PATCH 224/325] AGNOS 19.6 (#38611) * bump version * add production --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 28 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index a201fd6c47..094622005a 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.5" + export AGNOS_VERSION="19.6" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index 3a8d03e104..ae1cbbcd38 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -23,14 +23,14 @@ }, { "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c.img.xz", - "hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", - "hash_raw": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c", + "url": "https://commadist.azureedge.net/agnosupdate/abl-29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35.img.xz", + "hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35", + "hash_raw": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35", "size": 274432, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "b6fba807b9bcd66a31f2afb0eba5163ec239693ad32e2e4200f6c356adfe098c" + "ondevice_hash": "29fd7ed1c012e599420764840f9f11286d34dbff4adaf102a447f06d8c5e0b35" }, { "name": "aop", @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94.img.xz", - "hash": "f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94", - "hash_raw": "f716b81d557c274be707abc200276ba9e8320ad0386586f36f9065dd563fee94", + "url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz", + "hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", + "hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", "size": 46897152, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "269bc216189c4532a3e796ef2f4d47d0a03e774c883203be93feb8a8ed3c3adb" + "ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2.img.xz", - "hash": "414a9145d6ba13bc04760b31545507109e9a2509958dd44782dd1912b1cb96e9", - "hash_raw": "7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2", + "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz", + "hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b", + "hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "d4fadd9dfb4e20875650e60e37b9f8ccf2dc6c0fdc4f9a15912bee5ab8440a13", + "ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468", "alt": { - "hash": "7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2", - "url": "https://commadist.azureedge.net/agnosupdate/system-7b3da36853ee71f7f43ff9eb0f59ca8c4e80a2ba96a9a4911731641a5d008ae2.img", + "hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", + "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img", "size": 4718592000 } } From fe47e752f6d49feceecebc71cec1f58d4865c5ec Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 12 Aug 2026 20:15:10 -0700 Subject: [PATCH 225/325] log USB3 lane (#38617) --- openpilot/cereal/log.capnp | 7 +++++++ openpilot/common/hardware/usb.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 7b93e4aa73..fdf022fa63 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -704,6 +704,13 @@ struct UsbState { manufacturer @6 :Text; product @5 :Text; linkErrorCount @7 :UInt16; + usb3Lane @8 :Usb3Lane; + + enum Usb3Lane { + unknown @0; + a @1; + b @2; + } } } diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py index 1ebcf571b5..b9f6db2757 100644 --- a/openpilot/common/hardware/usb.py +++ b/openpilot/common/hardware/usb.py @@ -5,6 +5,8 @@ CHESTNUT_FW_VERSION = "ed4e39b7" CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) USB_DEVICES_PATH = Path("/sys/bus/usb/devices") +TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") +PRIMARY_USB_CONTROLLER = "a600000.ssusb" def get_usb_topology() -> set[str]: @@ -45,6 +47,7 @@ def controller(device: Path) -> Path | None: def get_usb_state() -> list[dict]: devices = [] + typec_orientation = read_int(TYPEC_CC_ORIENTATION_PATH) for device in usb_devices(): vendor_id = read_int(device / "idVendor", 16) product_id = read_int(device / "idProduct", 16) @@ -58,6 +61,7 @@ def get_usb_state() -> list[dict]: "manufacturer": read(device / "manufacturer") or "", "product": read(device / "product") or "", "linkErrorCount": read_int(ctrl / "portli", 0) & 0xFFFF if ctrl is not None else 0, + "usb3Lane": {1: "a", 2: "b"}.get(typec_orientation, "unknown") if ctrl is not None and ctrl.name == PRIMARY_USB_CONTROLLER else "unknown", }) return devices @@ -75,6 +79,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None: entry.manufacturer = device["manufacturer"] entry.product = device["product"] entry.linkErrorCount = device["linkErrorCount"] + entry.usb3Lane = device.get("usb3Lane", "unknown") if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS: chestnut_present = True From b7c333cf3fee117779515c9ebfd7b2beb164fa81 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 12 Aug 2026 20:31:41 -0700 Subject: [PATCH 226/325] pigeond: add comma AGPS source (#38616) --- openpilot/system/ubloxd/pigeond.py | 44 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/openpilot/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py index 58fba4d4e1..3da9350241 100755 --- a/openpilot/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -8,6 +8,7 @@ import urllib.parse from datetime import datetime, UTC from openpilot.cereal import messaging +from openpilot.common.api import Api from openpilot.common.time_helpers import system_time_valid from openpilot.common.params import Params from openpilot.common.serial import Serial @@ -41,15 +42,24 @@ def add_ubx_checksum(msg: bytes) -> bytes: B = (B + A) % 256 return msg + bytes([A, B]) -def get_assistnow_messages(token: str) -> list[bytes]: - # make request - # TODO: implement adding the last known location - r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ - 'token': token, - 'gnss': 'gps,glo', - 'datatype': 'eph,alm,aux', - }, safe=':,'), timeout=5) - assert r.status_code == 200, "Got invalid status code" +def get_assistnow_messages() -> list[bytes]: + params = Params() + if token := params.get('AssistNowToken'): + cloudlog.warning("Downloading AssistNow data directly from u-blox") + # TODO: implement adding the last known location + r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ + 'token': token, + 'gnss': 'gps,glo', + 'datatype': 'eph,alm,aux', + }, safe=':,'), timeout=5) + elif dongle_id := params.get('DongleId'): + cloudlog.warning("Downloading AssistNow data from comma's AGPS proxy") + api = Api(dongle_id) + r = api.get(f"v1/{dongle_id}/assist", access_token=api.get_token(), timeout=5) + else: + raise RuntimeError("Neither AssistNowToken nor DongleId is configured") + + r.raise_for_status() dat = r.content # split up messages @@ -230,15 +240,13 @@ def init_pigeon(pigeon: TTYPigeon) -> bool: )) pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) - # try getting AssistNow if we have a token - token = Params().get('AssistNowToken') - if token is not None: - try: - for msg in get_assistnow_messages(token): - pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) - cloudlog.warning("AssistNow messages sent") - except Exception: - cloudlog.warning("failed to get AssistNow messages") + # A configured u-blox token takes precedence over comma's AGPS proxy. + try: + for msg in get_assistnow_messages(): + pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) + cloudlog.warning("AssistNow messages sent") + except Exception: + cloudlog.warning("failed to get AssistNow messages") cloudlog.warning("Pigeon GPS on!") break From bdc8e4b02c368e10edc6c0aa980a4c263a6793c6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 13 Aug 2026 22:25:35 -0700 Subject: [PATCH 227/325] deprecate long kp (#38614) * deprecate long kp * bump --- opendbc_repo | 2 +- openpilot/selfdrive/controls/lib/longcontrol.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 44f2987cb6..c536b211b7 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 44f2987cb6ed28f7dcd99d5930abf6c2917d8f60 +Subproject commit c536b211b762c37c6d869923ae8ba59ca2f18a0c diff --git a/openpilot/selfdrive/controls/lib/longcontrol.py b/openpilot/selfdrive/controls/lib/longcontrol.py index 437bcef777..c57f4c4cce 100644 --- a/openpilot/selfdrive/controls/lib/longcontrol.py +++ b/openpilot/selfdrive/controls/lib/longcontrol.py @@ -39,8 +39,7 @@ class LongControl: def __init__(self, CP): self.CP = CP self.long_control_state = LongCtrlState.off - self.pid = PIDController((CP.longitudinalTuning.kpBP, CP.longitudinalTuning.kpV), - (CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV), + self.pid = PIDController(0.0, (CP.longitudinalTuning.kiBP, CP.longitudinalTuning.kiV), rate=1 / DT_CTRL) self.last_output_accel = 0.0 From c988e7889372fc8646ff7b310bd0483378bb6106 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 13 Aug 2026 22:46:13 -0700 Subject: [PATCH 228/325] jenkins fixups (#38532) --- .../selfdrive/pandad/tests/test_pandad_spi.py | 1 - openpilot/selfdrive/test/test_onroad.py | 33 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py index ba2e31ce82..8ca39445a2 100755 --- a/openpilot/selfdrive/pandad/tests/test_pandad_spi.py +++ b/openpilot/selfdrive/pandad/tests/test_pandad_spi.py @@ -102,7 +102,6 @@ class TestBoarddSpi(OpenpilotTestCase): edt = 1e3 / SERVICE_LIST[service].frequency assert edt*0.9 < np.mean(dts) < edt*1.1 assert np.max(dts) < edt*8 - assert np.min(dts) < edt assert len(dts) >= ((et-0.5)*SERVICE_LIST[service].frequency*0.8) with subtests.test(msg="CAN traffic"): diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 6768dc1d98..4426c196d4 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -67,7 +67,7 @@ PROCS = { "openpilot.selfdrive.pandad.pandad": 0, "openpilot.system.loggerd.uploader": 15.0, "openpilot.system.loggerd.deleter": 1.0, - "./pandad": 19.0, + "./pandad": 40.0, "openpilot.system.qcomgpsd.qcomgpsd": 1.0, "openpilot.common.hardware.comma.modem": 10.0, } @@ -107,6 +107,10 @@ def cputime_total(ct): class TestOnroad(OpenpilotTestCase): COMMA_HARDWARE_TEST = True + def setUp(self): + # Hardware setup is handled once for the full onroad test in setup_class. + unittest.TestCase.setUp(self) + @classmethod def setup_class(cls): if "DEBUG" in os.environ: @@ -331,27 +335,34 @@ class TestOnroad(OpenpilotTestCase): assert np.all(eof_sof_diff > 0) assert np.all(eof_sof_diff < 50*1e6) + # TODO: loggerd doesn't start fast enough to be ready before the first frames come out first_fid = {min(self.ts[c]['frameId']) for c in cams} - assert len(first_fid) == 1, "Cameras don't start on same frame ID" - if cam.endswith('CameraState'): + #assert len(first_fid) == 1, "Cameras don't start on same frame ID" + if cams[0].endswith('CameraState'): # camerad guarantees that all cams start on frame ID 0 # (note loggerd also needs to start up fast enough to catch it) - assert next(iter(first_fid)) < 100, "Cameras start on frame ID too high" + assert min(first_fid) < 100, "Cameras start on frame ID too high" # we don't do a full segment rotation, so these might not match exactly last_fid = {max(self.ts[c]['frameId']) for c in cams} assert max(last_fid) - min(last_fid) < 10 - start, end = min(first_fid), min(last_fid) - for i in range(end-start): + timestamps = { + cam: dict(zip(self.ts[cam]['frameId'], self.ts[cam]['timestampSof'], strict=True)) + for cam in cams + } + common_frame_ids = set.intersection(*(set(ts) for ts in timestamps.values())) + assert common_frame_ids, "Cameras have no overlapping frame IDs" + + for frame_id in sorted(common_frame_ids): # road and wide cameras (first two) should be synced within 2ms - ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams[:2]} - diff = (max(ts.values()) - min(ts.values())) - assert diff < 2, f"Cameras not synced properly: frame_id={start+i}, {diff=:.1f}ms, {ts=}" + ts = {cam: timestamps[cam][frame_id] / 1e6 for cam in cams[:2]} + diff = max(ts.values()) - min(ts.values()) + assert diff < 2, f"Cameras not synced properly: {frame_id=}, {diff=:.1f}ms, {ts=}" # cabin camera should be staggered ~25ms from road camera - offset_ms = abs(self.ts[cams[2]]['timestampSof'][i] - self.ts[cams[0]]['timestampSof'][i]) / 1e6 - assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {start+i}: {offset_ms:.1f}ms" + offset_ms = abs(timestamps[cams[2]][frame_id] - timestamps[cams[0]][frame_id]) / 1e6 + assert 20 < offset_ms < 30, f"cabin camera stagger out of range at frame {frame_id}: {offset_ms:.1f}ms" def test_camera_encoder_matches(self, subtests): # sanity check that the frame metadata is consistent with the encoded frames From ac4ab9a9b84d11c9b3f3edc1f74475656697f1c7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 13 Aug 2026 17:47:37 -0400 Subject: [PATCH 229/325] tests: migrate sunnypilot tests to unittest and remove pytest --- conftest.py | 101 ---------- openpilot/common/parameterized.py | 1 + .../selfdrive/monitoring/test_monitoring.py | 50 ++--- .../mads/tests/test_mads_state_machine.py | 11 +- .../mads/tests/test_mads_steering_mode.py | 23 +-- .../mapd/tests/test_mapd_version.py | 3 +- .../tests/{conftest.py => helpers.py} | 14 +- .../tests/test_camera_offset_helper.py | 3 +- .../tests/test_combined_pkl_loader.py | 75 ++++---- .../modeld_v2/tests/test_compile_modeld.py | 31 +-- .../modeld_v2/tests/test_recovery_power.py | 96 +++++----- .../models/tests/test_default_model.py | 3 +- .../models/tests/test_tinygrad_ref.py | 22 ++- .../dec/tests/pytest_dynamic_controller.py | 94 --------- .../lib/dec/tests/test_dynamic_controller.py | 91 +++++++++ .../lib/nnlc/tests/test_fingerprint.py | 3 +- .../lib/nnlc/tests/test_load_model.py | 3 +- .../controls/lib/nnlc/tests/test_nnlc.py | 9 +- .../tests/test_map_controller.py | 6 +- .../tests/test_vision_controller.py | 17 +- .../tests/test_speed_limit_assist.py | 32 ++-- .../tests/test_speed_limit_resolver.py | 37 ++-- .../lib/tests/test_auto_lane_change.py | 3 +- .../lib/tests/test_blinker_pause_lateral.py | 3 +- .../lib/tests/test_lane_turn_desire.py | 178 +++++++++-------- .../lib/tests/test_latcontrol_torque_ext.py | 3 +- .../selfdrive/controls/lib/tests/test_relc.py | 179 +++++++++--------- .../locationd/tests/test_locationd.py | 16 +- .../tests/test_button_state_tracker.py | 3 +- .../sunnylink/athena/tests/test_sunnylinkd.py | 3 +- .../sunnylink/tests/test_capabilities.py | 9 +- .../tests/test_compile_settings_ui.py | 28 ++- .../sunnylink/tests/test_settings_changes.py | 31 +-- .../sunnylink/tests/test_settings_schema.py | 24 ++- .../system/sensord/tests/test_sensord.py | 7 +- .../tests/test_sp_branch_migrations.py | 116 ++++++------ pyproject.toml | 24 --- uv.lock | 108 ----------- 38 files changed, 621 insertions(+), 839 deletions(-) delete mode 100644 conftest.py rename openpilot/sunnypilot/modeld_v2/tests/{conftest.py => helpers.py} (97%) delete mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dynamic_controller.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 7e7237457b..0000000000 --- a/conftest.py +++ /dev/null @@ -1,101 +0,0 @@ -# TODO-SP: upstream migrated from pytest to unittest with a custom test runner (tools/test_runner.py). -# Once sunnypilot test files are converted to unittest, this conftest and the pytest deps can be removed. -import contextlib -import gc -import os -import pytest - -from openpilot.common.prefix import OpenpilotPrefix -from openpilot.system.manager import manager -from openpilot.common.hardware import COMMA_HARDWARE, HARDWARE - -# these are heavy CI-only tests, invoked explicitly in .github/workflows/tests.yaml -collect_ignore = [ - "openpilot/selfdrive/test/process_replay/test_processes.py", - "openpilot/selfdrive/test/process_replay/test_regen.py", - - "openpilot/tools/sim/", - - # tinygrad JIT has process-global state. Other test files import modeld → tinygrad, - # which corrupts JIT captures for test_warp.py in the same process. Run separately in CI. - "openpilot/sunnypilot/modeld_v2/tests/test_warp.py", -] - - -def pytest_sessionstart(session): - # TODO: fix tests and enable test order randomization - if session.config.pluginmanager.hasplugin('randomly'): - session.config.option.randomly_reorganize = False - - -@pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_runtest_call(item): - # ensure we run as a hook after capturemanager's - if item.get_closest_marker("nocapture") is not None: - capmanager = item.config.pluginmanager.getplugin("capturemanager") - with capmanager.global_and_fixture_disabled(): - yield - else: - yield - - -@contextlib.contextmanager -def clean_env(): - starting_env = dict(os.environ) - yield - os.environ.clear() - os.environ.update(starting_env) - - -@pytest.fixture(scope="function", autouse=True) -def openpilot_function_fixture(request): - with clean_env(): - # setup a clean environment for each test - with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: - prefix = os.environ["OPENPILOT_PREFIX"] - - yield - - # ensure the test doesn't change the prefix - assert "OPENPILOT_PREFIX" in os.environ and prefix == os.environ["OPENPILOT_PREFIX"] - - # cleanup any started processes - manager.manager_cleanup() - - # some processes disable gc for performance, re-enable here - if not gc.isenabled(): - gc.enable() - gc.collect() - -# If you use setUpClass, the environment variables won't be cleared properly, -# so we need to hook both the function and class pytest fixtures -@pytest.fixture(scope="class", autouse=True) -def openpilot_class_fixture(): - with clean_env(): - yield - - -@pytest.fixture(scope="function") -def tici_setup_fixture(request, openpilot_function_fixture): - """Ensure a consistent state for tests on-device. Needs the openpilot function fixture to run first.""" - if 'skip_tici_setup' in request.keywords: - return - HARDWARE.initialize_hardware() - HARDWARE.set_power_save(False) - os.system("pkill -9 -f athena") - - -@pytest.hookimpl(tryfirst=True) -def pytest_collection_modifyitems(config, items): - skipper = pytest.mark.skip(reason="Skipping tici test on PC") - for item in items: - if "tici" in item.keywords: - if not COMMA_HARDWARE: - item.add_marker(skipper) - else: - item.fixturenames.append('tici_setup_fixture') - - if "xdist_group_class_property" in item.keywords: - class_property_name = item.get_closest_marker('xdist_group_class_property').args[0] - class_property_value = getattr(item.cls, class_property_name) - item.add_marker(pytest.mark.xdist_group(class_property_value)) diff --git a/openpilot/common/parameterized.py b/openpilot/common/parameterized.py index 2782fb9a57..16b89553c5 100644 --- a/openpilot/common/parameterized.py +++ b/openpilot/common/parameterized.py @@ -83,6 +83,7 @@ def parameterized_class(attrs, input_list=None): new_cls = type(name, (cls,), dict(params)) new_cls.__module__ = cls.__module__ new_cls.__unittest_skip__ = False + new_cls.__unittest_skip_why__ = "" # else inherited from the base and the collector drops it globs[name] = new_cls # Don't collect the un-parametrised base. cls.__unittest_skip__ = True diff --git a/openpilot/selfdrive/monitoring/test_monitoring.py b/openpilot/selfdrive/monitoring/test_monitoring.py index 95f3f4431c..d9889860f5 100644 --- a/openpilot/selfdrive/monitoring/test_monitoring.py +++ b/openpilot/selfdrive/monitoring/test_monitoring.py @@ -1,4 +1,4 @@ -import pytest +from openpilot.common.parameterized import parameterized from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import log @@ -251,28 +251,30 @@ def _build_sm(selfdrive_enabled, lat_active, steering_pressed, gas_pressed): } -@pytest.mark.parametrize("selfdrive_enabled, lat_active, steering, gas, expected_op_engaged, expected_driver_engaged", [ - (False, False, False, False, False, False), # disabled - (True, False, False, False, True, False), # OP enabled - (False, True, False, False, True, False), # MADS lat-only - (True, True, False, False, True, False), # both active - (False, True, False, True, True, False), # MADS lat-only + gas - (True, True, False, True, True, True), # full op + gas: override - (False, True, True, False, True, True), # MADS lat-only + wheel touch: override -]) -def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas, - expected_op_engaged, expected_driver_engaged): - sm = _build_sm(selfdrive_enabled, lat_active, steering, gas) - dm = DriverMonitoring() - captured = {} - orig = dm._update_events +class TestRunStepEngagement(OpenpilotTestCase): + @parameterized.expand([ + (False, False, False, False, False, False), # disabled + (True, False, False, False, True, False), # OP enabled + (False, True, False, False, True, False), # MADS lat-only + (True, True, False, False, True, False), # both active + (False, True, False, True, True, False), # MADS lat-only + gas + (True, True, False, True, True, True), # full op + gas: override + (False, True, True, False, True, True), # MADS lat-only + wheel touch: override + ], names=["selfdrive_enabled", "lat_active", "steering", "gas", + "expected_op_engaged", "expected_driver_engaged"]) + def test_run_step_engagement(self, selfdrive_enabled, lat_active, steering, gas, + expected_op_engaged, expected_driver_engaged): + sm = _build_sm(selfdrive_enabled, lat_active, steering, gas) + dm = DriverMonitoring() + captured = {} + orig = dm._update_events - def spy(driver_engaged, op_engaged, lowspeed, wrong_gear): - captured['driver_engaged'] = driver_engaged - captured['op_engaged'] = op_engaged - return orig(driver_engaged, op_engaged, lowspeed, wrong_gear) + def spy(driver_engaged, op_engaged, lowspeed, wrong_gear): + captured['driver_engaged'] = driver_engaged + captured['op_engaged'] = op_engaged + return orig(driver_engaged, op_engaged, lowspeed, wrong_gear) - object.__setattr__(dm, '_update_events', spy) - dm.run_step(sm, demo=False) - assert captured['op_engaged'] == expected_op_engaged - assert captured['driver_engaged'] == expected_driver_engaged + object.__setattr__(dm, '_update_events', spy) + dm.run_step(sm, demo=False) + assert captured['op_engaged'] == expected_op_engaged + assert captured['driver_engaged'] == expected_driver_engaged diff --git a/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py b/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py index 14549119fd..0147f2f4fb 100644 --- a/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py +++ b/openpilot/sunnypilot/mads/tests/test_mads_state_machine.py @@ -5,14 +5,13 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import pytest -from pytest_mock import MockerFixture from openpilot.cereal import custom from openpilot.common.realtime import DT_CTRL from openpilot.sunnypilot.mads.state import StateMachine, SOFT_DISABLE_TIME from openpilot.selfdrive.selfdrived.events import ET, NormalPermanentAlert, Events from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP, EVENTS_SP +from openpilot.common.test import OpenpilotTestCase State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventNameSP = custom.OnroadEventSP.EventName @@ -34,16 +33,16 @@ def make_event(event_types): class MockMADS: - def __init__(self, mocker: MockerFixture): + def __init__(self, mocker): self.selfdrive = mocker.MagicMock() self.selfdrive.state_machine = mocker.MagicMock() self.selfdrive.events = Events() self.selfdrive.events_sp = EventsSP() -class TestMADSStateMachine: - @pytest.fixture(autouse=True) - def setup_method(self, mocker: MockerFixture): +class TestMADSStateMachine(OpenpilotTestCase): + def setup_method(self): + mocker = self._fixture("mocker") self.mads = MockMADS(mocker) self.state_machine = StateMachine(self.mads) self.events = self.mads.selfdrive.events diff --git a/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py index 2bc6130ece..2058a8b10c 100644 --- a/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py +++ b/openpilot/sunnypilot/mads/tests/test_mads_steering_mode.py @@ -5,7 +5,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import pytest +from openpilot.common.parameterized import parameterized from openpilot.cereal import log, custom from opendbc.car import structs @@ -14,6 +14,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP +from openpilot.common.test import OpenpilotTestCase State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState EventName = log.OnroadEvent.EventName @@ -80,8 +81,8 @@ def run_frames(mads, sd, cs, n=1): # should_silent_lkas_enable across all modes -class TestShouldSilentLkasEnable: - @pytest.mark.parametrize("brake,regen", [(True, False), (False, True)]) +class TestShouldSilentLkasEnable(OpenpilotTestCase): + @parameterized.expand([(True, False), (False, True)], names=["brake", "regen"]) def test_pause_blocks_reenable_on_braking_at_standstill(self, mocker, brake, regen): mads, _ = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) cs = make_car_state(brake_pressed=brake, regen_braking=regen, standstill=True) @@ -105,7 +106,7 @@ class TestShouldSilentLkasEnable: # pause -class TestPauseMode: +class TestPauseMode(OpenpilotTestCase): def test_stays_paused_at_standstill_brake_held(self, mocker): mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) mads.state_machine.state = State.enabled @@ -150,7 +151,7 @@ class TestPauseMode: # disengage -class TestDisengageMode: +class TestDisengageMode(OpenpilotTestCase): def test_brake_while_enabled_disables(self, mocker): mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.DISENGAGE) mads.state_machine.state = State.enabled @@ -174,7 +175,7 @@ class TestDisengageMode: # remain active -class TestRemainActiveMode: +class TestRemainActiveMode(OpenpilotTestCase): def test_brake_does_not_pause_or_disable(self, mocker): mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.REMAIN_ACTIVE) mads.state_machine.state = State.enabled @@ -188,7 +189,7 @@ class TestRemainActiveMode: # lateral mismatch counter -class TestLateralMismatchCounter: +class TestLateralMismatchCounter(OpenpilotTestCase): def test_no_accumulation_while_paused(self, mocker): mads, sd = make_mads(mocker, MadsSteeringModeOnBrake.PAUSE) mads.state_machine.state = State.paused @@ -212,7 +213,7 @@ class TestLateralMismatchCounter: # brand restrictions -class TestBrandSteeringModeRestrictions: +class TestBrandSteeringModeRestrictions(OpenpilotTestCase): def test_rivian_forced_to_disengage(self, mocker): CP = structs.CarParams() CP.brand = "rivian" @@ -229,9 +230,9 @@ class TestBrandSteeringModeRestrictions: params = mocker.MagicMock() assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE - @pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER, + @parameterized.expand([MadsScreenButtonType.THREE_FINGER, MadsScreenButtonType.FOUR_FINGER, - MadsScreenButtonType.FIVE_FINGER]) + MadsScreenButtonType.FIVE_FINGER], names=["screen_button"]) def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button): CP = structs.CarParams() CP.brand = "tesla" @@ -250,7 +251,7 @@ class TestBrandSteeringModeRestrictions: "MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE}) assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE - @pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"]) + @parameterized.expand(["hyundai", "toyota", "honda", "gm"], names=["brand"]) def test_other_brands_use_param(self, mocker, brand): CP = structs.CarParams() CP.brand = brand diff --git a/openpilot/sunnypilot/mapd/tests/test_mapd_version.py b/openpilot/sunnypilot/mapd/tests/test_mapd_version.py index 5619d2ec29..acbbe51c37 100644 --- a/openpilot/sunnypilot/mapd/tests/test_mapd_version.py +++ b/openpilot/sunnypilot/mapd/tests/test_mapd_version.py @@ -7,9 +7,10 @@ See the LICENSE.md file in the root directory for more details. from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot.mapd import MAPD_PATH from openpilot.sunnypilot.mapd.update_version import MAPD_HASH_PATH +from openpilot.common.test import OpenpilotTestCase -class TestMapdVersion: +class TestMapdVersion(OpenpilotTestCase): def test_compare_versions(self): mapd_hash = get_file_hash(MAPD_PATH) diff --git a/openpilot/sunnypilot/modeld_v2/tests/conftest.py b/openpilot/sunnypilot/modeld_v2/tests/helpers.py similarity index 97% rename from openpilot/sunnypilot/modeld_v2/tests/conftest.py rename to openpilot/sunnypilot/modeld_v2/tests/helpers.py index f79cbe10b2..6925a61f08 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/conftest.py +++ b/openpilot/sunnypilot/modeld_v2/tests/helpers.py @@ -5,8 +5,9 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import pathlib import pickle -import pytest +import tempfile import openpilot.sunnypilot.models.helpers as helpers import openpilot.sunnypilot.modeld_v2.modeld as modeld_module @@ -181,16 +182,19 @@ def make_bundle(archetype): ) -@pytest.fixture +def tmp_path(): + with tempfile.TemporaryDirectory() as d: + yield pathlib.Path(d) + + def patch_modeld(monkeypatch): def _patch(bundle): - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False) + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle) return _patch -@pytest.fixture def model_state_factory(tmp_path, monkeypatch, patch_modeld): from openpilot.common.hardware import hw diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py b/openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py index d7e209390f..5398ac0ff1 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_camera_offset_helper.py @@ -9,6 +9,7 @@ import numpy as np from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper +from openpilot.common.test import OpenpilotTestCase class MockStruct: @@ -20,7 +21,7 @@ class MockStruct: return getattr(self, item) -class TestCameraOffset: +class TestCameraOffset(OpenpilotTestCase): def setup_method(self): self.camera_offset = CameraOffsetHelper() self.dc = DEVICE_CAMERAS[('mici', 'os04c10')] diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index 3c544586f8..3396649a1d 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -5,20 +5,27 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import pytest +from openpilot.common.parameterized import parameterized import openpilot.sunnypilot.models.helpers as helpers import openpilot.sunnypilot.modeld_v2.modeld as modeld_module from openpilot.sunnypilot.modeld_v2.modeld import _find_driving_pkl -from openpilot.sunnypilot.modeld_v2.tests.conftest import DummyModel, DummyBundle, ARCHETYPES, CAM_W, CAM_H, \ +from openpilot.sunnypilot.modeld_v2.tests import helpers as tests_helpers +from openpilot.sunnypilot.modeld_v2.tests.helpers import DummyModel, DummyBundle, ARCHETYPES, CAM_W, CAM_H, \ SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES +from openpilot.common.test import OpenpilotTestCase + +# resolved by name from this module when a test asks for them +tmp_path = tests_helpers.tmp_path +patch_modeld = tests_helpers.patch_modeld +model_state_factory = tests_helpers.model_state_factory ModelState = modeld_module.ModelState # Pkl discovery -class TestFindDrivingPkl: +class TestFindDrivingPkl(OpenpilotTestCase): def test_returns_none_when_no_bundle(self): assert _find_driving_pkl(None) is None @@ -49,16 +56,16 @@ class TestFindDrivingPkl: # Init — assertion guard -class TestModelStateCombinedInit: +class TestModelStateCombinedInit(OpenpilotTestCase): def test_asserts_when_no_pkl(self, monkeypatch): bundle = DummyBundle(models=[], is_20hz=True) - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False) - with pytest.raises(AssertionError, match="No driving pkl found"): + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle) + with self.assertRaisesRegex(AssertionError, "No driving pkl found"): ModelState(cam_w=CAM_W, cam_h=CAM_H) -class TestStockEquivalence: +class TestStockEquivalence(OpenpilotTestCase): def test_split_queue_keys_match_stock(self, model_state_factory): from openpilot.selfdrive.modeld.compile_modeld import make_input_queues @@ -100,8 +107,8 @@ class TestStockEquivalence: ARCHETYPE_NAMES = list(ARCHETYPES.keys()) -class TestModelTypeDetection: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestModelTypeDetection(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_combined_model_type(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -109,8 +116,8 @@ class TestModelTypeDetection: f"{arch.name}: got {state._combined_model_type}, expected {arch.expected_model_type}" -class TestConstantsSelection: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestConstantsSelection(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_constants_class(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -118,8 +125,8 @@ class TestConstantsSelection: f"{arch.name}: got {type(state.constants).__name__}, expected {arch.expected_constants_class.__name__}" -class TestParserSelection: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestParserSelection(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_parser_module(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -128,8 +135,8 @@ class TestParserSelection: f"{arch.name}: parser from {parser_module}, expected module ending with {arch.expected_parser_module}" -class TestDesireKeyDetection: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestDesireKeyDetection(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_desire_key(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -137,8 +144,8 @@ class TestDesireKeyDetection: f"{arch.name}: got {state.desire_key}, expected {arch.expected_desire_key}" -class TestVisionInputNames: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestVisionInputNames(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_vision_names_contain_img(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -147,14 +154,14 @@ class TestVisionInputNames: assert 'img' in name, f"{arch.name}: vision input name '{name}' missing 'img'" -class TestOutputSlices: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestOutputSlices(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_vision_slices_populated(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) assert len(state.vision_output_slices) > 0, f"{arch.name}: vision_output_slices empty" - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_policy_slices_match_type(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -164,14 +171,14 @@ class TestOutputSlices: assert len(state.policy_output_slices) > 0, f"{arch.name}: split/multi should have policy slices" -class TestInputQueueCreation: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestInputQueueCreation(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_queues_not_empty(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) assert len(state.input_queues) > 0, f"{arch.name}: input_queues empty" - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_npy_contains_transforms(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -180,7 +187,7 @@ class TestInputQueueCreation: assert state.numpy_inputs['tfm'].shape == (3, 3) assert state.numpy_inputs['big_tfm'].shape == (3, 3) - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_npy_contains_desire(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -188,8 +195,8 @@ class TestInputQueueCreation: f"{arch.name}: '{arch.expected_desire_key}' missing from npy" -class TestFrameBufferParams: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestFrameBufferParams(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_frame_buf_params_per_vision_input(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) @@ -199,29 +206,29 @@ class TestFrameBufferParams: assert len(nv12_info) >= 4, f"{arch.name}: nv12_info for '{name}' too short" -class TestBundleOverrides: - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) +class TestBundleOverrides(OpenpilotTestCase): + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_smoothing_params_from_overrides(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) assert state.LAT_SMOOTH_SECONDS == 0.1 assert state.LONG_SMOOTH_SECONDS == 0.3 - @pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES) + @parameterized.expand(ARCHETYPE_NAMES, names=["archetype_name"]) def test_generation_from_bundle(self, archetype_name, model_state_factory): arch = ARCHETYPES[archetype_name] state = model_state_factory(arch) assert state.generation == 10 -class TestMlsimProperty: +class TestMlsimProperty(OpenpilotTestCase): def test_mlsim_false_for_gen10(self, model_state_factory): state = model_state_factory(ARCHETYPES['supercombo_non20hz']) assert state.mlsim is False def test_mlsim_true_for_gen11(self, tmp_path, monkeypatch, patch_modeld): from openpilot.common.hardware import hw - from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl, ARCHETYPES as A + from openpilot.sunnypilot.modeld_v2.tests.helpers import write_pkl, ARCHETYPES as A arch = A['supercombo_non20hz'] write_pkl(tmp_path, arch) @@ -233,10 +240,10 @@ class TestMlsimProperty: assert state.mlsim is True -class TestCrossArchetypeMismatch: +class TestCrossArchetypeMismatch(OpenpilotTestCase): def test_wrong_is_20hz_changes_constants(self, tmp_path, monkeypatch, patch_modeld): from openpilot.common.hardware import hw - from openpilot.sunnypilot.modeld_v2.tests.conftest import write_pkl + from openpilot.sunnypilot.modeld_v2.tests.helpers import write_pkl from openpilot.sunnypilot.modeld_v2.constants import ModelConstants arch = ARCHETYPES['vision_policy_split'] diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py index c30dd46e33..885696b853 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py @@ -6,12 +6,13 @@ See the LICENSE.md file in the root directory for more details. """ import numpy as np -import pytest +from openpilot.common.parameterized import parameterized from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key +from openpilot.common.test import OpenpilotTestCase -class TestDeriveFrameSkip: +class TestDeriveFrameSkip(OpenpilotTestCase): def test_non20hz_supercombo(self): vision = {} policy = {'features_buffer': (1, 99, 512), 'desire': (1, 100, 8)} @@ -31,21 +32,21 @@ class TestDeriveFrameSkip: assert derive_frame_skip({}, {}) == 1 -class TestFrameSkipBufferLengthEquivalence: - @pytest.mark.parametrize("frame_skip,expected_buffer_length", [ +class TestFrameSkipBufferLengthEquivalence(OpenpilotTestCase): + @parameterized.expand([ (1, 2), (4, 5), - ]) + ], names=["frame_skip", "expected_buffer_length"]) def test_img_buffer_size_matches_warp_buffer_length(self, frame_skip, expected_buffer_length): n_frames = 2 img_buf_dim0 = frame_skip * (n_frames - 1) + 1 assert img_buf_dim0 == expected_buffer_length, \ f"frame_skip={frame_skip}: img_buf[0]={img_buf_dim0}, expected {expected_buffer_length}" - @pytest.mark.parametrize("is_20hz,expected_frame_skip,expected_buffer_length", [ + @parameterized.expand([ (False, 1, 2), (True, 4, 5), - ]) + ], names=["is_20hz", "expected_frame_skip", "expected_buffer_length"]) def test_is_20hz_to_frame_skip_to_buffer_length(self, is_20hz, expected_frame_skip, expected_buffer_length): if is_20hz: policy_shapes = {'features_buffer': (1, 24, 512)} @@ -59,7 +60,7 @@ class TestFrameSkipBufferLengthEquivalence: assert img_buf_dim0 == expected_buffer_length -class TestTemporalSamplingEquivalence: +class TestTemporalSamplingEquivalence(OpenpilotTestCase): def test_non20hz_desire_sampling_identity(self): buf = np.random.default_rng(0).standard_normal((100, 1, 8)).astype(np.float32) frame_skip = 1 @@ -94,12 +95,12 @@ class TestTemporalSamplingEquivalence: np.testing.assert_array_equal(sampled, buf[:, 0, :]) -class TestTemporalIdxEquivalence: - @pytest.mark.parametrize("mode,desire_shape,fb_shape,frame_skip", [ +class TestTemporalIdxEquivalence(OpenpilotTestCase): + @parameterized.expand([ ('non20hz', (1, 100, 8), (1, 99, 512), 1), ('20hz', (1, 25, 8), (1, 24, 512), 4), ('split', (1, 25, 8), (1, 25, 512), 4), - ]) + ], names=["mode", "desire_shape", "fb_shape", "frame_skip"]) def test_features_buffer_idx_equivalence(self, mode, desire_shape, fb_shape, frame_skip): history = fb_shape[1] @@ -118,11 +119,11 @@ class TestTemporalIdxEquivalence: assert len(modelstate_idxs) == fb_shape[1], \ f"{mode}: ModelState idx count {len(modelstate_idxs)} != input shape {fb_shape[1]}" - @pytest.mark.parametrize("mode,desire_shape,fb_shape,frame_skip", [ + @parameterized.expand([ ('non20hz', (1, 100, 8), (1, 99, 512), 1), ('20hz', (1, 25, 8), (1, 24, 512), 4), ('split', (1, 25, 8), (1, 25, 512), 4), - ]) + ], names=["mode", "desire_shape", "fb_shape", "frame_skip"]) def test_desire_idx_equivalence(self, mode, desire_shape, fb_shape, frame_skip): history = desire_shape[1] @@ -132,7 +133,7 @@ class TestTemporalIdxEquivalence: f"{mode}: compile desire samples {compile_sampled_count} != model input {history}" -class TestDetectDesireKey: +class TestDetectDesireKey(OpenpilotTestCase): def test_finds_desire(self): shapes = {'features_buffer': (1, 99, 512), 'desire': (1, 100, 8), 'traffic_convention': (1, 2)} assert _detect_desire_key(shapes) == 'desire' @@ -146,7 +147,7 @@ class TestDetectDesireKey: assert _detect_desire_key(shapes) is None -class TestOutputSlicePreservation: +class TestOutputSlicePreservation(OpenpilotTestCase): def test_vision_hidden_state_slice_used_for_features(self): mock_slices = {'hidden_state': slice(0, 512), 'plan': slice(512, 1024)} features_slice = mock_slices['hidden_state'] diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py index d7c8563e64..85305395ea 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py @@ -7,6 +7,7 @@ from openpilot.cereal import log from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.modeld import ModelState import openpilot.sunnypilot.modeld_v2.modeld as modeld +from openpilot.common.test import OpenpilotTestCase class MockStruct: @@ -15,58 +16,59 @@ class MockStruct: setattr(self, k, v) -def test_recovery_power_scaling(): - state: Any = MockStruct( - PLANPLUS_CONTROL=0.75, - LONG_SMOOTH_SECONDS=0.3, - LAT_SMOOTH_SECONDS=0.1, - MIN_LAT_CONTROL_SPEED=0.3, - mlsim=True, - generation=12, - constants=MockStruct(T_IDXS=np.arange(100), DESIRE_LEN=8) - ) - prev_action = log.ModelDataV2.Action() - recorded_vel: list = [] - recorded_curv_plans: list = [] +class TestRecoveryPower(OpenpilotTestCase): + def test_recovery_power_scaling(self): + state: Any = MockStruct( + PLANPLUS_CONTROL=0.75, + LONG_SMOOTH_SECONDS=0.3, + LAT_SMOOTH_SECONDS=0.1, + MIN_LAT_CONTROL_SPEED=0.3, + mlsim=True, + generation=12, + constants=MockStruct(T_IDXS=np.arange(100), DESIRE_LEN=8) + ) + prev_action = log.ModelDataV2.Action() + recorded_vel: list = [] + recorded_curv_plans: list = [] - def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0): - recorded_vel.append(plan_vel.copy()) - return 0.0, False + def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0): + recorded_vel.append(plan_vel.copy()) + return 0.0, False - def mock_curvature(output, plan, vego, lat_action_t, mlsim): - recorded_curv_plans.append(plan.copy()) - return 0.0 + def mock_curvature(output, plan, vego, lat_action_t, mlsim): + recorded_curv_plans.append(plan.copy()) + return 0.0 - modeld.get_accel_from_plan = mock_accel # ty: ignore[invalid-assignment] - modeld.get_curvature_from_output = mock_curvature # ty: ignore[invalid-assignment] - plan = np.random.default_rng(0).random((1, 100, 15)).astype(np.float32) - planplus = np.random.default_rng(1).random((1, 100, 15)).astype(np.float32) - merged_plan = plan + planplus + modeld.get_accel_from_plan = mock_accel # ty: ignore[invalid-assignment] + modeld.get_curvature_from_output = mock_curvature # ty: ignore[invalid-assignment] + plan = np.random.default_rng(0).random((1, 100, 15)).astype(np.float32) + planplus = np.random.default_rng(1).random((1, 100, 15)).astype(np.float32) + merged_plan = plan + planplus - model_output: dict = { - 'plan': merged_plan.copy(), - 'planplus': planplus.copy() - } + model_output: dict = { + 'plan': merged_plan.copy(), + 'planplus': planplus.copy() + } - test_cases: list = [ - # (control, v_ego) - (0.55, 20.0), - (1.0, 25.0), - (1.5, 25.1), - (2.0, 20.0), - (0.75, 19.0), - (0.8, 25.1), - ] + test_cases: list = [ + # (control, v_ego) + (0.55, 20.0), + (1.0, 25.0), + (1.5, 25.1), + (2.0, 20.0), + (0.75, 19.0), + (0.8, 25.1), + ] - for control, v_ego in test_cases: - state.PLANPLUS_CONTROL = control - recorded_vel.clear() - recorded_curv_plans.clear() - ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) # type: ignore[arg-type] + for control, v_ego in test_cases: + state.PLANPLUS_CONTROL = control + recorded_vel.clear() + recorded_curv_plans.clear() + ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) # type: ignore[arg-type] - expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0] - np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6) + expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0] + np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6) - # For the below, yes, I know this isn't the same slicing as fillmodlmsg. This is to show that the values are only scaled on curv - expected_curv_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + control * planplus[0, :, Plan.VELOCITY][:, 0] - np.testing.assert_allclose(recorded_curv_plans[0][:, Plan.VELOCITY][:, 0], expected_curv_plan_vel, rtol=1e-5, atol=1e-6) + # For the below, yes, I know this isn't the same slicing as fillmodlmsg. This is to show that the values are only scaled on curv + expected_curv_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + control * planplus[0, :, Plan.VELOCITY][:, 0] + np.testing.assert_allclose(recorded_curv_plans[0][:, Plan.VELOCITY][:, 0], expected_curv_plan_vel, rtol=1e-5, atol=1e-6) diff --git a/openpilot/sunnypilot/models/tests/test_default_model.py b/openpilot/sunnypilot/models/tests/test_default_model.py index ab51027442..450237e0e9 100644 --- a/openpilot/sunnypilot/models/tests/test_default_model.py +++ b/openpilot/sunnypilot/models/tests/test_default_model.py @@ -8,9 +8,10 @@ See the LICENSE.md file in the root directory for more details. from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH import hashlib +from openpilot.common.test import OpenpilotTestCase -class TestDefaultModel: +class TestDefaultModel(OpenpilotTestCase): def test_compare_onnx_hashes(self): supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) diff --git a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py index 3e60ab5308..1712c60410 100644 --- a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py +++ b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py @@ -2,6 +2,7 @@ import requests from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref from openpilot.sunnypilot.models.fetcher import ModelFetcher +from openpilot.common.test import OpenpilotTestCase def fetch_tinygrad_ref(): @@ -11,13 +12,14 @@ def fetch_tinygrad_ref(): return json_data.get("tinygrad_ref") -def test_tinygrad_ref(): - current_ref = get_tinygrad_ref() - remote_ref = fetch_tinygrad_ref() - assert remote_ref == current_ref, ( - f"""tinygrad_repo ref does not match remote tinygrad_ref of current compiled driving models json. - Current: {current_ref} - Remote: {remote_ref} - Please run build-all workflow to update models.""" - ) - print("tinygrad_repo ref matches current compiled driving models json ref.") +class TestTinygradRef(OpenpilotTestCase): + def test_tinygrad_ref(self): + current_ref = get_tinygrad_ref() + remote_ref = fetch_tinygrad_ref() + assert remote_ref == current_ref, ( + f"""tinygrad_repo ref does not match remote tinygrad_ref of current compiled driving models json. + Current: {current_ref} + Remote: {remote_ref} + Please run build-all workflow to update models.""" + ) + print("tinygrad_repo ref matches current compiled driving models json ref.") diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py deleted file mode 100644 index 407ed0af3a..0000000000 --- a/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ /dev/null @@ -1,94 +0,0 @@ -import pytest - -from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController - -class MockLeadOne: - def __init__(self, status=0.0): - self.status = status - -class MockRadarState: - def __init__(self, status=0.0): - self.leadOne = MockLeadOne(status=status) - -class MockCarState: - def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False): - self.vEgo = vEgo - self.vCruise = vCruise - self.standstill = standstill - -class MockModelData: - def __init__(self, valid=True): - size = 33 if valid else 10 # incomplete if invalid - self.position = type("Pos", (), {"x": [0.0] * size})() - self.orientation = type("Ori", (), {"x": [0.0] * size})() - -class MockSelfDriveState: - def __init__(self, experimentalMode=False): - self.experimentalMode = experimentalMode - -class MockParams: - def get_bool(self, name): - return True - -@pytest.fixture -def default_sm(): - sm = { - 'carState': MockCarState(vEgo=10.0, vCruise=20.0), - 'radarState': MockRadarState(status=1.0), - 'modelV2': MockModelData(valid=True), - 'selfdriveState': MockSelfDriveState(experimentalMode=True), - } - return sm - -@pytest.fixture -def mock_cp(): - class CP: - radarUnavailable = False - return CP() - -@pytest.fixture -def mock_mpc(): - class MPC: - crash_cnt = 0 - return MPC() - -# Fake Kalman Filter that always returns a given value -class FakeKalman: - def __init__(self, value=1.0): - self.value = value - def add_data(self, v): pass - def get_value(self): return self.value - def get_confidence(self): return 1.0 - def reset_data(self): pass - -def test_initial_mode_is_acc(mock_cp, mock_mpc): - controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - assert controller.mode() == "acc" - -def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm): - controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - default_sm['carState'].standstill = True - for _ in range(10): - controller.update(default_sm) - assert controller.mode() == "blended" - -def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm): - controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - mock_mpc.crash_cnt = 1 # simulate FCW - for _ in range(2): - controller.update(default_sm) - assert controller.mode() == "blended" - -def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm): - mock_cp.radarUnavailable = True - controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - - # Force conditions to simulate slowdown - controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment] - controller._v_ego_kph = 35.0 - default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory - - for _ in range(3): - controller.update(default_sm) - - assert controller.mode() == "blended" diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dynamic_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dynamic_controller.py new file mode 100644 index 0000000000..4fec6eaa52 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dynamic_controller.py @@ -0,0 +1,91 @@ +from openpilot.common.test import OpenpilotTestCase +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController + +class MockLeadOne: + def __init__(self, present=0.0): + self.present = present + +class MockRadarState: + def __init__(self, present=0.0): + self.leadOne = MockLeadOne(present=present) + +class MockCarState: + def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False): + self.vEgo = vEgo + self.vCruise = vCruise + self.standstill = standstill + +class MockModelData: + def __init__(self, valid=True): + size = 33 if valid else 10 # incomplete if invalid + self.position = type("Pos", (), {"x": [0.0] * size})() + self.orientation = type("Ori", (), {"x": [0.0] * size})() + +class MockSelfDriveState: + def __init__(self, experimentalMode=False): + self.experimentalMode = experimentalMode + +class MockParams: + def get_bool(self, name): + return True + +def default_sm(): + sm = { + 'carState': MockCarState(vEgo=10.0, vCruise=20.0), + 'radarState': MockRadarState(present=1.0), + 'modelV2': MockModelData(valid=True), + 'selfdriveState': MockSelfDriveState(experimentalMode=True), + } + return sm + +def mock_cp(): + class CP: + radarUnavailable = False + return CP() + +def mock_mpc(): + class MPC: + crash_cnt = 0 + return MPC() + +# Fake Kalman Filter that always returns a given value +class FakeKalman: + def __init__(self, value=1.0): + self.value = value + def add_data(self, v): pass + def get_value(self): return self.value + def get_confidence(self): return 1.0 + def reset_data(self): pass + +class TestDynamicExperimentalController(OpenpilotTestCase): + def test_initial_mode_is_acc(self, mock_cp, mock_mpc): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + assert controller.mode() == "acc" + + def test_standstill_triggers_blended(self, mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + default_sm['carState'].standstill = True + for _ in range(10): + controller.update(default_sm) + assert controller.mode() == "blended" + + def test_emergency_blended_on_fcw(self, mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + mock_mpc.crash_cnt = 1 # simulate FCW + for _ in range(2): + controller.update(default_sm) + assert controller.mode() == "blended" + + def test_radarless_slowdown_triggers_blended(self, mock_cp, mock_mpc, default_sm): + mock_cp.radarUnavailable = True + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + + # Force conditions to simulate slowdown + controller._slow_down_filter = FakeKalman(value=1.0) # ty: ignore[invalid-assignment] + controller._v_ego_kph = 35.0 + default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory + + for _ in range(3): + controller.update(default_sm) + + assert controller.mode() == "blended" diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py index 07e7d2852d..8eaa90babd 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_fingerprint.py @@ -7,6 +7,7 @@ from opendbc.car.tesla.values import CAR as TESLA from openpilot.common.parameterized import parameterized from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces +from openpilot.common.test import OpenpilotTestCase FINGERPRINT_EXACT_MATCH = [HONDA.HONDA_CIVIC_BOSCH, TOYOTA.TOYOTA_RAV4_TSS2_2022, HYUNDAI.HYUNDAI_IONIQ_5] @@ -14,7 +15,7 @@ FINGERPRINT_FUZZY_MATCH = [HONDA.HONDA_CIVIC_BOSCH_DIESEL, HYUNDAI.GENESIS_G70_2 FINGERPRINT_ANGLE_NO_MATCH = [TOYOTA.TOYOTA_RAV4_TSS2_2023, NISSAN.NISSAN_LEAF, TESLA.TESLA_MODEL_3] -class TestNNLCFingerprintBase: +class TestNNLCFingerprintBase(OpenpilotTestCase): @staticmethod def _setup_platform(car_name): diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py index 2f4e0d6993..4e0da4eeed 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_load_model.py @@ -8,9 +8,10 @@ from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.car.helpers import convert_to_capnp from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces +from openpilot.common.test import OpenpilotTestCase -class TestNNTorqueModel: +class TestNNTorqueModel(OpenpilotTestCase): @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN]) def test_load_model(self, car_name): diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index ca5127c1bc..4782b0c4f3 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -17,6 +17,7 @@ from openpilot.selfdrive.locationd.helpers import Pose from openpilot.common.mock.generators import generate_deviceMotion from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.common.test import OpenpilotTestCase def generate_modelV2(): @@ -42,7 +43,7 @@ def generate_modelV2(): return model -class TestNeuralNetworkLateralControl: +class TestNeuralNetworkLateralControl(OpenpilotTestCase): @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, GM.CHEVROLET_BOLT_EUV]) def test_saturation(self, car_name): @@ -81,7 +82,7 @@ class TestNeuralNetworkLateralControl: for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) - controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.update_torque_parameters(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True, 0.2) assert lac_log.saturated @@ -89,7 +90,7 @@ class TestNeuralNetworkLateralControl: for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) - controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.update_torque_parameters(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False, 0.2) assert not lac_log.saturated @@ -97,7 +98,7 @@ class TestNeuralNetworkLateralControl: for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) - controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.update_torque_parameters(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False, 0.2) assert lac_log.saturated diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py index dc27447c0b..a9e5cdd992 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_map_controller.py @@ -8,18 +8,18 @@ import json import math import platform -import pytest from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap +from openpilot.common.test import OpenpilotTestCase MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState -class TestSmartCruiseControlMap: +class TestSmartCruiseControlMap(OpenpilotTestCase): def setup_method(self): self.params = Params() @@ -70,6 +70,6 @@ class TestSmartCruiseControlMap: self.scc_m.update(True, False, 25.0, 0.0, 30.0) - assert self.scc_m.v_target == pytest.approx(24.0) + self.assertAlmostEqual(self.scc_m.v_target, 24.0, delta=24.0 * 1e-6) # TODO-SP: mock data from modelV2 to test other states diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py index 98b3ffc8e7..610acd47df 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. from typing import Any import numpy as np -import pytest +from openpilot.common.parameterized import parameterized import openpilot.cereal.messaging as messaging from openpilot.cereal import custom, log @@ -17,6 +17,7 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control import MIN_V from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision, _ENTERING_PRED_LAT_ACC_TH +from openpilot.common.test import OpenpilotTestCase VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState @@ -105,7 +106,7 @@ def generate_controlsState(): return controls_state -class TestSmartCruiseControlVision: +class TestSmartCruiseControlVision(OpenpilotTestCase): def setup_method(self): self.params = Params() @@ -145,19 +146,11 @@ class TestSmartCruiseControlVision: self.scc_v.update(self.sm, True, False, 0., 0., 0.) assert self.scc_v.state == VisionState.enabled - @pytest.mark.parametrize( - "case, should_enter", - [ + @parameterized.expand([ ("p97_just_above_threshold", True), ("single_spike_filtered", False), ("persistent_high_values", True), - ], - ids=[ - "p97>threshold_enters", - "single_spike_max_large_but_p97_below_threshold", - "high_values_persist_trigger_entering", - ], - ) + ], names=["case", "should_enter"]) def test_max_pred_lat_acc_uses_p97_and_threshold(self, case, should_enter): n = len(ModelConstants.T_IDXS) th = float(_ENTERING_PRED_LAT_ACC_TH) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py index 1f23fb2299..f3d32a4d5b 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_assist.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. import time -import pytest +from openpilot.common.parameterized import parameterized from openpilot.cereal import custom from opendbc.car.car_helpers import interfaces @@ -27,6 +27,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +from openpilot.common.test import OpenpilotTestCase ButtonEvent = car.CarState.ButtonEvent ButtonType = car.CarState.ButtonEvent.Type @@ -45,21 +46,10 @@ SPEED_LIMITS = { DEFAULT_CAR = TOYOTA.TOYOTA_RAV4_TSS2 -@pytest.fixture -def car_name(request): - return getattr(request, "param", DEFAULT_CAR) +class TestSpeedLimitAssist(OpenpilotTestCase): + car_name = DEFAULT_CAR - -@pytest.fixture(autouse=True) -def set_car_name_on_instance(request, car_name): - instance = getattr(request, "instance", None) - if instance: - instance.car_name = car_name - - -class TestSpeedLimitAssist: - - def setup_method(self, method): + def setup_method(self): self.params = Params() self.reset_custom_params() self.events_sp = EventsSP() @@ -69,7 +59,7 @@ class TestSpeedLimitAssist: self.pcm_long_max_set_speed = PCM_LONG_REQUIRED_MAX_SET_SPEED[self.sla.is_metric][1] # use 80 MPH for now self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH - def teardown_method(self, method): + def teardown_method(self): self.reset_state() def _setup_platform(self, car_name): @@ -112,13 +102,16 @@ class TestSpeedLimitAssist: assert not self.sla.is_active assert V_CRUISE_UNSET == self.sla.get_v_target_from_control() - @pytest.mark.parametrize("car_name", [RIVIAN.RIVIAN_R1, TESLA.TESLA_MODEL_Y], indirect=True) + @parameterized.expand([RIVIAN.RIVIAN_R1, TESLA.TESLA_MODEL_Y], names=["car_name"]) def test_disallowed_brands(self, car_name): """ Speed Limit Assist is disabled for the following brands and conditions: - All Tesla and is a release branch; - All Rivian """ + self.car_name = car_name + self.openpilot_setup_method() # rebuild the platform for this brand + assert not self.sla.enabled # stay disallowed even when the param may have changed from somewhere else @@ -285,9 +278,10 @@ class TestSpeedLimitAssist: assert self.sla.state in ACTIVE_STATES -class TestButtonStateTrackerSLAIntegration: +class TestButtonStateTrackerSLAIntegration(OpenpilotTestCase): + + def setup_method(self): - def setup_method(self, method): self.tracker = ButtonStateTracker() self.params = Params() self.params.put("IsReleaseSpBranch", True, block=True) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py index a18880c620..884749c72e 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/speed_limit/tests/test_speed_limit_resolver.py @@ -7,26 +7,26 @@ See the LICENSE.md file in the root directory for more details. import random import time -import pytest -from pytest_mock import MockerFixture +from openpilot.common.parameterized import parameterized from openpilot.cereal import custom from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import LIMIT_MAX_MAP_DATA_AGE from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Policy +from openpilot.common.test import OpenpilotTestCase SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source -def create_mock(properties, mocker: MockerFixture): +def create_mock(properties, mocker): mock = mocker.MagicMock() for _property, value in properties.items(): setattr(mock, _property, value) return mock -def setup_sm_mock(mocker: MockerFixture): +def setup_sm_mock(mocker): cruise_speed_limit = random.uniform(0, 120) live_map_data_limit = random.uniform(0, 120) @@ -58,21 +58,24 @@ def setup_sm_mock(mocker: MockerFixture): return sm_mock -parametrized_policies = pytest.mark.parametrize( - "policy, sm_key, function_key", [ +parametrized_policies = parameterized.expand( + [ (Policy.car_state_only, 'carStateSP', SpeedLimitSource.car), (Policy.car_state_priority, 'carStateSP', SpeedLimitSource.car), (Policy.map_data_only, 'liveMapDataSP', SpeedLimitSource.map), (Policy.map_data_priority, 'liveMapDataSP', SpeedLimitSource.map), ], - ids=lambda val: val.name if hasattr(val, 'name') else str(val) + names=["policy", "sm_key", "function_key"] ) -@pytest.mark.parametrize("resolver_class", [SpeedLimitResolver]) -class TestSpeedLimitResolverValidation: +def resolver_class(): + return SpeedLimitResolver - @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) + +class TestSpeedLimitResolverValidation(OpenpilotTestCase): + + @parameterized.expand(list(Policy), names=["policy"]) def test_initial_state(self, resolver_class, policy): resolver = resolver_class() resolver.policy = policy @@ -82,7 +85,7 @@ class TestSpeedLimitResolverValidation: assert resolver.distance_solutions[source] == 0. @parametrized_policies - def test_resolver(self, resolver_class, policy, sm_key, function_key, mocker: MockerFixture): + def test_resolver(self, resolver_class, policy, sm_key, function_key, mocker): resolver = resolver_class() resolver.policy = policy sm_mock = setup_sm_mock(mocker) @@ -93,7 +96,7 @@ class TestSpeedLimitResolverValidation: assert resolver.speed_limit == source_speed_limit assert resolver.source == ALL_SOURCES[function_key] - def test_resolver_combined(self, resolver_class, mocker: MockerFixture): + def test_resolver_combined(self, resolver_class, mocker): resolver = resolver_class() resolver.policy = Policy.combined sm_mock = setup_sm_mock(mocker) @@ -108,7 +111,7 @@ class TestSpeedLimitResolverValidation: assert resolver.source == socket_to_source[minimum_key] @parametrized_policies - def test_parser(self, resolver_class, policy, sm_key, function_key, mocker: MockerFixture): + def test_parser(self, resolver_class, policy, sm_key, function_key, mocker): resolver = resolver_class() resolver.policy = policy sm_mock = setup_sm_mock(mocker) @@ -119,8 +122,8 @@ class TestSpeedLimitResolverValidation: assert resolver.limit_solutions[ALL_SOURCES[function_key]] == source_speed_limit assert resolver.distance_solutions[ALL_SOURCES[function_key]] == 0. - @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) - def test_resolve_interaction_in_update(self, resolver_class, policy, mocker: MockerFixture): + @parameterized.expand(list(Policy), names=["policy"]) + def test_resolve_interaction_in_update(self, resolver_class, policy, mocker): v_ego = 50 resolver = resolver_class() resolver.policy = policy @@ -133,8 +136,8 @@ class TestSpeedLimitResolverValidation: assert resolver.distance is not None assert resolver.source is not None - @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) - def test_old_map_data_ignored(self, resolver_class, policy, mocker: MockerFixture): + @parameterized.expand(list(Policy), names=["policy"]) + def test_old_map_data_ignored(self, resolver_class, policy, mocker): resolver = resolver_class() resolver.policy = policy sm_mock = mocker.MagicMock() diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py index a0b78249b3..0d95550412 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_auto_lane_change.py @@ -9,6 +9,7 @@ from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \ AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY +from openpilot.common.test import OpenpilotTestCase AUTO_LANE_CHANGE_TIMER_COMBOS = [ (AutoLaneChangeMode.NUDGELESS, AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]), @@ -19,7 +20,7 @@ AUTO_LANE_CHANGE_TIMER_COMBOS = [ ] -class TestAutoLaneChangeController: +class TestAutoLaneChangeController(OpenpilotTestCase): def setup_method(self): self.DH = DesireHelper() self.alc = AutoLaneChangeController(self.DH) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index 7a72cfa1f2..4f0e6c03df 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -8,9 +8,10 @@ from opendbc.car.structs import car from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral +from openpilot.common.test import OpenpilotTestCase -class TestBlinkerPauseLateral: +class TestBlinkerPauseLateral(OpenpilotTestCase): def setup_method(self): self.blinker_pause_lateral = BlinkerPauseLateral() diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py index 434b12110b..52c6cecbfc 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py @@ -1,6 +1,7 @@ -import pytest from openpilot.cereal import log, custom from openpilot.common.params import Params +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN @@ -10,65 +11,63 @@ from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLan TurnDirection = custom.ModelDataV2SP.TurnDirection -@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [ - (True, False, 5, False, False, TurnDirection.turnLeft), - (False, True, 6, False, False, TurnDirection.turnRight), - (True, False, 9, False, False, TurnDirection.none), - (True, False, 7, True, False, TurnDirection.none), - (False, True, 6, False, True, TurnDirection.none), - (False, False, 5, False, False, TurnDirection.none), - (True, True, 5, False, False, TurnDirection.none), -]) -def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected): - dh = DesireHelper() - controller = LaneTurnController(dh) - controller.enabled = True - controller.lane_turn_value = LANE_CHANGE_SPEED_MIN - controller.turn_direction = TurnDirection.none - controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego) - assert controller.get_turn_direction() == expected +class TestLaneTurnDesire(OpenpilotTestCase): + @parameterized.expand([ + (True, False, 5, False, False, TurnDirection.turnLeft), + (False, True, 6, False, False, TurnDirection.turnRight), + (True, False, 9, False, False, TurnDirection.none), + (True, False, 7, True, False, TurnDirection.none), + (False, True, 6, False, True, TurnDirection.none), + (False, False, 5, False, False, TurnDirection.none), + (True, True, 5, False, False, TurnDirection.none), + ]) + def test_lane_turn_desire_conditions(self, left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego) + assert controller.get_turn_direction() == expected + def test_lane_turn_desire_disabled(self): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = False + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(False, False, True, False, 7) + assert controller.get_turn_direction() == TurnDirection.none -def test_lane_turn_desire_disabled(): - dh = DesireHelper() - controller = LaneTurnController(dh) - controller.enabled = False - controller.lane_turn_value = LANE_CHANGE_SPEED_MIN - controller.turn_direction = TurnDirection.none - controller.update_lane_turn(False, False, True, False, 7) - assert controller.get_turn_direction() == TurnDirection.none + def test_lane_turn_overrides_lane_change(self): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + # left turn desire + controller.update_lane_turn(False, False, True, False, 5) + assert controller.get_turn_direction() == TurnDirection.turnLeft + # right turn desire + controller.update_lane_turn(False, False, False, True, 6) + assert controller.get_turn_direction() == TurnDirection.turnRight + # no turn + controller.update_lane_turn(False, False, False, False, 7) + assert controller.get_turn_direction() == TurnDirection.none - -def test_lane_turn_overrides_lane_change(): - dh = DesireHelper() - controller = LaneTurnController(dh) - controller.enabled = True - controller.lane_turn_value = LANE_CHANGE_SPEED_MIN - controller.turn_direction = TurnDirection.none - # left turn desire - controller.update_lane_turn(False, False, True, False, 5) - assert controller.get_turn_direction() == TurnDirection.turnLeft - # right turn desire - controller.update_lane_turn(False, False, False, True, 6) - assert controller.get_turn_direction() == TurnDirection.turnRight - # no turn - controller.update_lane_turn(False, False, False, False, 7) - assert controller.get_turn_direction() == TurnDirection.none - - -@pytest.mark.parametrize("v_ego,expected", [ - (8.93, TurnDirection.turnLeft), # just below threshold - (8.96, TurnDirection.none), # above threshold - (8.95, TurnDirection.none), # just above threshold -]) -def test_lane_turn_desire_speed_boundary(v_ego, expected): - dh = DesireHelper() - controller = LaneTurnController(dh) - controller.enabled = True - controller.lane_turn_value = LANE_CHANGE_SPEED_MIN - controller.turn_direction = TurnDirection.none - controller.update_lane_turn(False, True, True, False, v_ego) - assert controller.get_turn_direction() == expected + @parameterized.expand([ + (8.93, TurnDirection.turnLeft), # just below threshold + (8.96, TurnDirection.none), # above threshold + (8.95, TurnDirection.none), # just above threshold + ]) + def test_lane_turn_desire_speed_boundary(self, v_ego, expected): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(False, True, True, False, v_ego) + assert controller.get_turn_direction() == expected class DummyCarState: @@ -84,43 +83,42 @@ class DummyCarState: self.brakePressed = brakePressed -@pytest.fixture def set_lane_turn_params(): params = Params() params.put("LaneTurnDesire", True) params.put("LaneTurnValue", 20.0) -@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [ - # Lane turn desire overrides lane change desire - (DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, - log.Desire.turnLeft), - (DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, - log.Desire.turnRight), - # Lane change desire only (no turn desires) - (DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, - steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft), - (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False, - steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight), - # No desire (inactive) - (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none), - (DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire! -]) -def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params): - dh = DesireHelper() - dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE - for _ in range(10): - dh.update(carstate, lateral_active, lane_change_prob, - left_edge_detected=False, right_edge_detected=False) - assert dh.desire == expected_desire +class TestDesireHelperIntegration(OpenpilotTestCase): + @parameterized.expand([ + # Lane turn desire overrides lane change desire + (DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, + log.Desire.turnLeft), + (DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, + log.Desire.turnRight), + # Lane change desire only (no turn desires) + (DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, + steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft), + (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False, + steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight), + # No desire (inactive) + (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none), + (DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire! + ], names=["carstate", "lateral_active", "lane_change_prob", "expected_desire"]) + def test_desire_helper_integration(self, carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params): + dh = DesireHelper() + dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE + for _ in range(10): + dh.update(carstate, lateral_active, lane_change_prob, + left_edge_detected=False, right_edge_detected=False) + assert dh.desire == expected_desire - -def test_edge_blocks_lane_change(set_lane_turn_params): - dh = DesireHelper() - dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE - carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1) - for _ in range(10): - dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False) - assert dh.lane_change_state == LaneChangeState.preLaneChange - assert dh.lane_change_direction == LaneChangeDirection.left - assert dh.desire == log.Desire.none + def test_edge_blocks_lane_change(self, set_lane_turn_params): + dh = DesireHelper() + dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE + carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1) + for _ in range(10): + dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False) + assert dh.lane_change_state == LaneChangeState.preLaneChange + assert dh.lane_change_direction == LaneChangeDirection.left + assert dh.desire == log.Desire.none diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py index 0df945214d..b4f1137081 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_latcontrol_torque_ext.py @@ -19,6 +19,7 @@ from openpilot.selfdrive.locationd.helpers import Pose from openpilot.common.mock.generators import generate_deviceMotion from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.common.test import OpenpilotTestCase def _make_controller(enhanced=False, nnlc=False): @@ -71,7 +72,7 @@ def _run_update(controller, VM): return controller.update(True, CS, VM, params, False, 0.5, pose, False, 0.2) -class TestLatControlTorqueExt: +class TestLatControlTorqueExt(OpenpilotTestCase): def test_init_enhanced_only(self): controller, VM, _ = _make_controller(enhanced=True, nnlc=False) assert controller.extension._jerk_aware_enabled diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py index 71b153c8b7..8fea65e1a1 100644 --- a/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py +++ b/openpilot/sunnypilot/selfdrive/controls/lib/tests/test_relc.py @@ -4,7 +4,8 @@ Copyright (c) 2021-, rav4kumar, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.relc import ( @@ -29,7 +30,6 @@ CLOSE_EDGES = edges(-2.0, 1.5) FAR_EDGES = edges(-10.0, 10.0) -@pytest.fixture def relc(mocker): mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params") controller = RoadEdgeLaneChangeController() @@ -42,128 +42,129 @@ def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, ro controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges) -@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [ - ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"), - ([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"), -]) -def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr): - drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1) - assert getattr(relc, attr) +class TestRELC(OpenpilotTestCase): + @parameterized.expand([ + ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], "left_edge_detected"), + ([0.9, 0.0], [0.8, 0.8, 0.8, 0.0], "right_edge_detected"), + ], names=["road_edge_stds", "lane_line_probs", "attr"]) + def test_edge_detection(self, relc, road_edge_stds, lane_line_probs, attr): + drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1) + assert getattr(relc, attr) -def test_edge_detection_requires_time(relc): - drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05) - assert not relc.left_edge_detected + def test_edge_detection_requires_time(self, relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME - 0.05) + assert not relc.left_edge_detected -def test_both_edges_detected(relc): - drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) - assert relc.left_edge_detected - assert relc.right_edge_detected + def test_both_edges_detected(self, relc): + drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected + assert relc.right_edge_detected -def test_noise_doesnt_clear(relc): - edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) - clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) + def test_noise_doesnt_clear(self, relc): + edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) + clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) - drive(relc, *edge, EDGE_REACTION_TIME + 0.1) - assert relc.left_edge_detected + drive(relc, *edge, EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected - relc.update(*clear, V_HIGH, CLOSE_EDGES) - relc.update(*edge, V_HIGH, CLOSE_EDGES) - assert relc.left_edge_detected + relc.update(*clear, V_HIGH, CLOSE_EDGES) + relc.update(*edge, V_HIGH, CLOSE_EDGES) + assert relc.left_edge_detected -def test_clears_after_window(relc): - edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) - clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) + def test_clears_after_window(self, relc): + edge = ([0.0, 0.9], [0.0, 0.8, 0.8, 0.8]) + clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8]) - drive(relc, *edge, EDGE_REACTION_TIME + 0.1) - assert relc.left_edge_detected + drive(relc, *edge, EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected - drive(relc, *clear, EDGE_CLEAR_TIME + 0.05) - assert not relc.left_edge_detected - assert relc.left_edge_timer == 0.0 + drive(relc, *clear, EDGE_CLEAR_TIME + 0.05) + assert not relc.left_edge_detected + assert relc.left_edge_timer == 0.0 -def test_low_speed_skips(relc): - drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW) - assert not relc.left_edge_detected - assert relc.left_edge_timer == 0.0 + def test_low_speed_skips(self, relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW) + assert not relc.left_edge_detected + assert relc.left_edge_timer == 0.0 -def test_speed_drop_resets(relc): - drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) - assert relc.left_edge_detected + def test_speed_drop_resets(self, relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected - relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES) - assert not relc.left_edge_detected + relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_LOW, CLOSE_EDGES) + assert not relc.left_edge_detected -def test_param_off_resets(relc): - drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) - assert relc.left_edge_detected + def test_param_off_resets(self, relc): + drive(relc, [0.0, 0.9], [0.0, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert relc.left_edge_detected - relc.params.get_bool.return_value = False - relc.read_params() - relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES) - assert not relc.left_edge_detected - assert not relc.right_edge_detected + relc.params.get_bool.return_value = False + relc.read_params() + relc.update([0.0, 0.9], [0.0, 0.8, 0.8, 0.8], V_HIGH, CLOSE_EDGES) + assert not relc.left_edge_detected + assert not relc.right_edge_detected -def test_lane_line_prevents_detection(relc): - drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) - assert not relc.left_edge_detected + def test_lane_line_prevents_detection(self, relc): + drive(relc, [0.0, 0.9], [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1) + assert not relc.left_edge_detected -def test_one_side_blocks_other_allows(relc): - drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) - assert relc.right_edge_detected - assert not relc.left_edge_detected + def test_one_side_blocks_other_allows(self, relc): + drive(relc, [0.9, 0.0], [0.8, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert relc.right_edge_detected + assert not relc.left_edge_detected -def test_disabled_no_detection(relc): - relc.enabled = False - relc.params.get_bool.return_value = False - drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) - assert not relc.left_edge_detected - assert not relc.right_edge_detected + def test_disabled_no_detection(self, relc): + relc.enabled = False + relc.params.get_bool.return_value = False + drive(relc, [0.0, 0.0], [0.0, 0.8, 0.8, 0.0], EDGE_REACTION_TIME + 0.1) + assert not relc.left_edge_detected + assert not relc.right_edge_detected -def test_far_edge_no_block(relc): - drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES) - assert not relc.left_edge_detected + def test_far_edge_no_block(self, relc): + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, road_edges=FAR_EDGES) + assert not relc.left_edge_detected -def test_close_edge_blocks(relc): - drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1, - road_edges=edges(-8.0, 1.5)) - assert relc.right_edge_detected - assert not relc.left_edge_detected + def test_close_edge_blocks(self, relc): + drive(relc, [0.9, 0.0], [0.05, 0.8, 0.8, 0.05], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-8.0, 1.5)) + assert relc.right_edge_detected + assert not relc.left_edge_detected -def test_wide_road_no_lines_no_block(relc): - drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1, - road_edges=edges(-8.0, 8.0)) - assert not relc.left_edge_detected - assert not relc.right_edge_detected + def test_wide_road_no_lines_no_block(self, relc): + drive(relc, [0.0, 0.0], [0.05, 0.4, 0.4, 0.05], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-8.0, 8.0)) + assert not relc.left_edge_detected + assert not relc.right_edge_detected -def test_narrow_road_both_block(relc): - drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1, - road_edges=edges(-2.5, 2.5)) - assert relc.left_edge_detected - assert relc.right_edge_detected + def test_narrow_road_both_block(self, relc): + drive(relc, [0.0, 0.0], [0.02, 0.4, 0.4, 0.02], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-2.5, 2.5)) + assert relc.left_edge_detected + assert relc.right_edge_detected -def test_clearance_boundary(relc): - boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m - drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, - road_edges=edges(-(boundary - 0.1), 10.0)) - assert relc.left_edge_detected + def test_clearance_boundary(self, relc): + boundary = VEHICLE_EDGE_MARGIN + EDGE_CLEARANCE # 4.78m + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-(boundary - 0.1), 10.0)) + assert relc.left_edge_detected - relc.reset() + relc.reset() - drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, - road_edges=edges(-(boundary + 0.1), 10.0)) - assert not relc.left_edge_detected + drive(relc, [0.0, 0.9], [0.05, 0.5, 0.5, 0.08], EDGE_REACTION_TIME + 0.1, + road_edges=edges(-(boundary + 0.1), 10.0)) + assert not relc.left_edge_detected diff --git a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py index 748032df3e..ac6ab6aaea 100644 --- a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py +++ b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py @@ -1,5 +1,5 @@ -import pytest import platform +import unittest import json import random import time @@ -11,13 +11,11 @@ from openpilot.common.params import Params from openpilot.common.transformations.coordinates import ecef2geodetic from openpilot.system.manager.process_config import managed_processes +from openpilot.common.test import OpenpilotTestCase -if platform.system() == 'Darwin': - pytest.skip("Skipping locationd test on macOS due to unsupported msgq.", allow_module_level=True) - - -class TestLocationdProc: +@unittest.skipIf(platform.system() == 'Darwin', "msgq unsupported on macOS") +class TestLocationdProc(OpenpilotTestCase): LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'extrinsicsCalibration', 'accelerometer', 'gyroscope'] @@ -88,6 +86,6 @@ class TestLocationdProc: time.sleep(1) # wait for async params write lastGPS = json.loads(self.params.get('LastGPSPositionLLK')) - assert lastGPS['latitude'] == pytest.approx(self.lat, abs=0.001) - assert lastGPS['longitude'] == pytest.approx(self.lon, abs=0.001) - assert lastGPS['altitude'] == pytest.approx(self.alt, abs=0.001) + self.assertAlmostEqual(lastGPS['latitude'], self.lat, delta=0.001) + self.assertAlmostEqual(lastGPS['longitude'], self.lon, delta=0.001) + self.assertAlmostEqual(lastGPS['altitude'], self.alt, delta=0.001) diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py b/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py index 798fab650b..5824f82531 100644 --- a/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py +++ b/openpilot/sunnypilot/selfdrive/selfdrived/tests/test_button_state_tracker.py @@ -6,12 +6,13 @@ See the LICENSE.md file in the root directory for more details. """ from opendbc.car.structs import car from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker +from openpilot.common.test import OpenpilotTestCase ButtonEvent = car.CarState.ButtonEvent ButtonType = car.CarState.ButtonEvent.Type -class TestButtonStateTracker: +class TestButtonStateTracker(OpenpilotTestCase): def setup_method(self) -> None: self.tracker = ButtonStateTracker() diff --git a/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py index 7b81da5ad4..9540d8329d 100644 --- a/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/tests/test_sunnylinkd.py @@ -5,9 +5,10 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ from openpilot.sunnypilot.sunnylink.athena import sunnylinkd +from openpilot.common.test import OpenpilotTestCase -class TestSunnylinkdMethods: +class TestSunnylinkdMethods(OpenpilotTestCase): def setup_method(self): self.saved_params = [] diff --git a/openpilot/sunnypilot/sunnylink/tests/test_capabilities.py b/openpilot/sunnypilot/sunnylink/tests/test_capabilities.py index 4af1462479..7e0272a7c9 100644 --- a/openpilot/sunnypilot/sunnylink/tests/test_capabilities.py +++ b/openpilot/sunnypilot/sunnylink/tests/test_capabilities.py @@ -12,7 +12,6 @@ the same commit so the bump shows up in code review. """ from __future__ import annotations -import pytest from openpilot.sunnypilot.sunnylink.capabilities import ( CAPABILITY_DEFAULTS, @@ -21,18 +20,18 @@ from openpilot.sunnypilot.sunnylink.capabilities import ( PROTOCOL_VERSION, generate_capabilities, ) +from openpilot.common.test import OpenpilotTestCase KNOWN_PROTOCOL_VERSIONS = (1,) LATEST_KNOWN = max(KNOWN_PROTOCOL_VERSIONS) -@pytest.fixture(scope="module") def caps(): return generate_capabilities() -class TestProtocolVersion: +class TestProtocolVersion(OpenpilotTestCase): def test_protocol_version_in_capability_fields(self): assert "protocol_version" in CAPABILITY_FIELDS @@ -63,7 +62,7 @@ class TestProtocolVersion: ) -class TestOpaquePerBrandFlags: +class TestOpaquePerBrandFlags(OpenpilotTestCase): def test_subaru_has_sng_field_present(self): assert "subaru_has_sng" in CAPABILITY_FIELDS @@ -77,7 +76,7 @@ class TestOpaquePerBrandFlags: assert caps["hyundai_alpha_long_available"] is False -class TestCapabilitiesShape: +class TestCapabilitiesShape(OpenpilotTestCase): def test_all_fields_present(self, caps): for field in CAPABILITY_FIELDS: assert field in caps, f"capabilities missing {field}" diff --git a/openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py b/openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py index 2fcd889ec3..797bf42558 100644 --- a/openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py +++ b/openpilot/sunnypilot/sunnylink/tests/test_compile_settings_ui.py @@ -19,7 +19,6 @@ import difflib import json import os -import pytest import yaml from openpilot.sunnypilot.sunnylink.tools.compile_settings_ui import ( @@ -29,20 +28,19 @@ from openpilot.sunnypilot.sunnylink.tools.compile_settings_ui import ( _resolve_refs, compile_schema, ) +from openpilot.common.test import OpenpilotTestCase -@pytest.fixture(scope="module") def compiled() -> dict: return compile_schema(DEFAULT_SRC) -@pytest.fixture(scope="module") def committed() -> dict: with open(DEFAULT_OUT) as f: return json.load(f) -class TestRoundtrip: +class TestRoundtrip(OpenpilotTestCase): def test_compiled_matches_committed(self, compiled, committed): """Compiled output must match the checked-in JSON.""" if compiled == committed: @@ -54,7 +52,7 @@ class TestRoundtrip: tofile="settings_ui.json (freshly compiled)", lineterm="", )) - pytest.fail(f"settings_ui.json schema mismatch — run compile_settings_ui.py\n\n{diff}") + self.fail(f"settings_ui.json schema mismatch — run compile_settings_ui.py\n\n{diff}") def test_committed_file_is_canonical(self): """Compiled output must byte-match the checked-in file (including trailing newline). @@ -72,10 +70,10 @@ class TestRoundtrip: tofile="settings_ui.json (freshly compiled)", lineterm="", )) - pytest.fail(f"settings_ui.json out of sync — run compile_settings_ui.py\n\n{diff}") + self.fail(f"settings_ui.json out of sync — run compile_settings_ui.py\n\n{diff}") -class TestRefResolution: +class TestRefResolution(OpenpilotTestCase): def test_list_context_splices(self): macros = {"a": [{"type": "offroad_only"}], "b": [{"type": "not_engaged"}]} out = _resolve_refs([{"$ref": "#/macros/a"}, {"$ref": "#/macros/b"}], macros) @@ -95,12 +93,12 @@ class TestRefResolution: assert out == [{"type": "offroad_only"}] def test_unknown_macro_raises(self): - with pytest.raises(CompileError, match="unknown macro"): + with self.assertRaisesRegex(CompileError, "unknown macro"): _resolve_refs([{"$ref": "#/macros/missing"}], {}) def test_cycle_raises(self): macros = {"a": [{"$ref": "#/macros/b"}], "b": [{"$ref": "#/macros/a"}]} - with pytest.raises(CompileError, match="cycle"): + with self.assertRaisesRegex(CompileError, "cycle"): _resolve_refs([{"$ref": "#/macros/a"}], macros) def test_depth_limit(self): @@ -111,20 +109,20 @@ class TestRefResolution: "l3": [{"$ref": "#/macros/l4"}], "l4": [{"type": "offroad_only"}], } - with pytest.raises(CompileError, match="depth"): + with self.assertRaisesRegex(CompileError, "depth"): _resolve_refs([{"$ref": "#/macros/l1"}], macros) def test_invalid_ref_scheme(self): - with pytest.raises(CompileError, match="unsupported"): + with self.assertRaisesRegex(CompileError, "unsupported"): _resolve_refs([{"$ref": "https://example.com/x"}], {}) def test_scalar_macro_in_list_context_raises(self): macros = {"x": {"type": "offroad_only"}} # macro is a single rule (dict), not a list - with pytest.raises(CompileError, match="must resolve to a list"): + with self.assertRaisesRegex(CompileError, "must resolve to a list"): _resolve_refs([{"$ref": "#/macros/x"}], macros) -class TestCompiledShape: +class TestCompiledShape(OpenpilotTestCase): def test_panels_present(self, compiled): assert isinstance(compiled["panels"], list) assert len(compiled["panels"]) == 9 @@ -145,7 +143,7 @@ class TestCompiledShape: def walk(node): if isinstance(node, dict): if "$ref" in node: - pytest.fail(f"unresolved $ref: {node}") + self.fail(f"unresolved $ref: {node}") for v in node.values(): walk(v) elif isinstance(node, list): @@ -154,7 +152,7 @@ class TestCompiledShape: walk(compiled) -class TestSourceTreeIntegrity: +class TestSourceTreeIntegrity(OpenpilotTestCase): def test_macros_yaml_well_formed(self): with open(os.path.join(DEFAULT_SRC, "_macros.yaml")) as f: doc = yaml.safe_load(f) diff --git a/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py index 07b05d4ac4..d9b1267a10 100644 --- a/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py +++ b/openpilot/sunnypilot/sunnylink/tests/test_settings_changes.py @@ -15,7 +15,7 @@ import json import os from typing import Any -import pytest +from openpilot.common.parameterized import parameterized from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import ( DEFINITION_PATH, @@ -24,6 +24,7 @@ from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import ( _load_torque_versions, generate_schema, ) +from openpilot.common.test import OpenpilotTestCase SCHEMA_VALIDATOR_PATH = os.path.join(os.path.dirname(DEFINITION_PATH), "settings_ui.schema.json") @@ -105,12 +106,11 @@ def _references_capability_field(rules: list[dict[str, Any]] | None, field: str) return found -@pytest.fixture(scope="module") def schema(): return generate_schema() -class TestMadsBrandGates: +class TestMadsBrandGates(OpenpilotTestCase): def test_mads_main_cruise_has_brand_gate(self, schema): """MadsMainCruiseAllowed must gate on brand and tesla_has_vehicle_bus.""" item = _find_item(schema, "MadsMainCruiseAllowed") @@ -126,7 +126,7 @@ class TestMadsBrandGates: assert _references_capability_field(item.get("enablement"), "tesla_has_vehicle_bus") -class TestTestManeuversSection: +class TestTestManeuversSection(OpenpilotTestCase): def test_lateral_maneuver_mode_in_test_maneuvers(self, schema): section = _find_section(schema, "developer", "test_maneuvers") assert section is not None, "developer.test_maneuvers section missing" @@ -153,10 +153,13 @@ class TestTestManeuversSection: "test_maneuvers must gate ShowAdvancedControls via enablement" -class TestValidator: +class TestValidator(OpenpilotTestCase): def test_validator_accepts_real_json(self): """settings_ui.json validates against settings_ui.schema.json.""" - jsonschema = pytest.importorskip("jsonschema") + try: + import jsonschema + except ImportError: + self.skipTest("jsonschema not installed") with open(DEFINITION_PATH) as f: data = json.load(f) with open(SCHEMA_VALIDATOR_PATH) as f: @@ -164,7 +167,7 @@ class TestValidator: jsonschema.validate(instance=data, schema=validator) -class TestTorqueOptionGeneration: +class TestTorqueOptionGeneration(OpenpilotTestCase): def test_torque_versions_match_generated_options(self, schema): versions = _load_torque_versions() assert versions, "latcontrol_torque_versions.json must have at least one version" @@ -179,11 +182,11 @@ class TestTorqueOptionGeneration: ) -class TestReleaseBranchGates: - @pytest.mark.parametrize("key", [ +class TestReleaseBranchGates(OpenpilotTestCase): + @parameterized.expand([ "EnableGithubRunner", "QuickBootToggle", - ]) + ], names=["key"]) def test_sp_dev_items_gate_on_is_sp_release(self, schema, key): """sunnypilot dev items must hide on sunnypilot release branches (is_sp_release gate).""" item = _find_item(schema, key) @@ -192,7 +195,7 @@ class TestReleaseBranchGates: assert _references_capability_field(rules, "is_sp_release"), f"{key} missing is_sp_release gate" -class TestSpuriousOffroadGatesDropped: +class TestSpuriousOffroadGatesDropped(OpenpilotTestCase): def test_disengage_on_accelerator_has_no_offroad_only(self, schema): item = _find_item(schema, "DisengageOnAccelerator") assert item is not None @@ -204,12 +207,12 @@ class TestSpuriousOffroadGatesDropped: assert "offroad_only" not in _flatten_rule_types(item.get("enablement")) -class TestNotEngagedReplacement: - @pytest.mark.parametrize("key", [ +class TestNotEngagedReplacement(OpenpilotTestCase): + @parameterized.expand([ "AlphaLongitudinalEnabled", "ToyotaEnforceStockLongitudinal", "ToyotaStopAndGoHack", - ]) + ], names=["key"]) def test_offroad_only_replaced_with_not_engaged(self, schema, key): """These items should use not_engaged, not offroad_only.""" item = _find_item(schema, key) diff --git a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py index 579d72b60b..e60ac000f4 100644 --- a/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py +++ b/openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py @@ -5,7 +5,6 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ import json -import pytest from openpilot.common.params import Params from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import ( @@ -16,6 +15,7 @@ from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import ( collect_capability_refs, ) from openpilot.sunnypilot.sunnylink.capabilities import CAPABILITY_FIELDS +from openpilot.common.test import OpenpilotTestCase VALID_WIDGET_TYPES = {"toggle", "option", "multiple_button", "button", "info"} @@ -55,18 +55,16 @@ def _brand_items(brand_data) -> list[dict]: return [] -@pytest.fixture(scope="module") def schema(): return generate_schema() -@pytest.fixture(scope="module") def all_param_keys(): """All keys registered in the device param store.""" return {k.decode("utf-8") for k in Params().all_keys()} -class TestSchemaStructure: +class TestSchemaStructure(OpenpilotTestCase): def test_schema_is_valid_json(self): """Schema serializes to valid JSON.""" raw = generate_schema_json() @@ -141,16 +139,16 @@ class TestSchemaStructure: for item in _iter_panel_items(panel): key = item["key"] if key in seen: - pytest.fail(f"Key '{key}' appears in both panel '{seen[key]}' and '{panel['id']}'") + self.fail(f"Key '{key}' appears in both panel '{seen[key]}' and '{panel['id']}'") seen[key] = panel["id"] for sub in item.get("sub_items", []): sub_key = sub["key"] if sub_key in seen: - pytest.fail(f"Sub-item key '{sub_key}' appears in both '{seen[sub_key]}' and '{panel['id']}'") + self.fail(f"Sub-item key '{sub_key}' appears in both '{seen[sub_key]}' and '{panel['id']}'") seen[sub_key] = panel["id"] -class TestSchemaCoverage: +class TestSchemaCoverage(OpenpilotTestCase): def test_all_schema_keys_exist_in_params(self, schema, all_param_keys): """Schema keys must exist in Params().all_keys().""" schema_keys = collect_all_keys(schema) @@ -169,7 +167,7 @@ class TestSchemaCoverage: assert set(schema["capability_fields"]) == set(CAPABILITY_FIELDS) -class TestRuleWellFormedness: +class TestRuleWellFormedness(OpenpilotTestCase): def _validate_rule(self, rule: dict, context: str = ""): """Recursively validate a single rule dict.""" assert "type" in rule, f"Rule missing 'type' in {context}" @@ -232,7 +230,7 @@ class TestRuleWellFormedness: key = item.get("key") for rule in item.get(rules_field, []): if rule.get("type") == "param" and rule.get("key") == key: - pytest.fail(f"Item {key} has self-referencing {rules_field} rule") + self.fail(f"Item {key} has self-referencing {rules_field} rule") for panel in schema["panels"]: for item in _iter_panel_items(panel): @@ -245,7 +243,7 @@ class TestRuleWellFormedness: _check_self_ref(item, "enablement") -class TestKnownPanels: +class TestKnownPanels(OpenpilotTestCase): def test_expected_panels_exist(self, schema): panel_ids = {p["id"] for p in schema["panels"]} expected = {"steering", "cruise", "display", "visuals", "device", "software", "developer"} @@ -279,7 +277,7 @@ class TestKnownPanels: assert "NeuralNetworkLateralControl" in enhanced_enable_keys -class TestKnownVehicleSettings: +class TestKnownVehicleSettings(OpenpilotTestCase): def test_hyundai_has_longitudinal_tuning(self, schema): keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))} assert "HyundaiLongitudinalTuning" in keys @@ -299,7 +297,7 @@ class TestKnownVehicleSettings: assert "SubaruStopAndGoManualParkingBrake" in keys -class TestItemCompleteness: +class TestItemCompleteness(OpenpilotTestCase): def _collect_all_items(self, schema): """Collect all items and sub_items from panels and vehicle_settings.""" items = [] @@ -319,7 +317,7 @@ class TestItemCompleteness: """All items must have titles.""" missing = [i["key"] for i in self._collect_all_items(schema) if "title" not in i] if len(missing) > MAX_ALLOWED_MISSING_TITLES: - pytest.fail(f"Items without titles ({len(missing)}): {missing[:10]}") + self.fail(f"Items without titles ({len(missing)}): {missing[:10]}") def test_no_default_titles(self, schema): """Item titles must differ from keys.""" diff --git a/openpilot/sunnypilot/system/sensord/tests/test_sensord.py b/openpilot/sunnypilot/system/sensord/tests/test_sensord.py index 53f5d27219..39486fde76 100644 --- a/openpilot/sunnypilot/system/sensord/tests/test_sensord.py +++ b/openpilot/sunnypilot/system/sensord/tests/test_sensord.py @@ -1,6 +1,5 @@ import os import subprocess -import pytest import time import numpy as np from collections import namedtuple, defaultdict @@ -12,6 +11,7 @@ from openpilot.common.gpio import get_irqs_for_action from openpilot.common.timeout import Timeout from openpilot.common.hardware import HARDWARE from openpilot.system.manager.process_config import managed_processes +from openpilot.common.test import OpenpilotTestCase BMX = { ('bmx055', 'acceleration'), @@ -103,8 +103,9 @@ def read_sensor_events(duration_sec): return {k: v for k, v in events.items() if len(v) > 0} -@pytest.mark.tici -class TestSensord: +class TestSensord(OpenpilotTestCase): + COMMA_HARDWARE_TEST = True + @classmethod def setup_class(cls): # enable LSM self test diff --git a/openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py b/openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py index b2742841a0..86f82fd9d2 100644 --- a/openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py +++ b/openpilot/sunnypilot/system/updated/tests/test_sp_branch_migrations.py @@ -4,70 +4,72 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import pytest +from openpilot.common.parameterized import parameterized +from openpilot.common.test import OpenpilotTestCase from openpilot.common.params import Params from openpilot.system.updated.updated import Updater -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tici", "staging-c3-new", "staging-tici"), - ("tici", "dev-c3-new", "staging-tici"), - ("tici", "master", "master-tici"), - ("tici", "master-dev-c3-new", "master-tici"), - ("tizi", "staging-c3-new", "staging"), - ("tizi", "dev-c3-new", "dev"), - ("tizi", "master-dev-c3-new", "master-dev"), - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_sp_branch_migrations_from_current_branch(mocker, device_type, branch, expected): - params = Params() - params.remove("UpdaterTargetBranch") - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - mocker.patch.object(Updater, "get_branch", return_value=branch) - - assert Updater().target_branch == expected - - -@pytest.mark.parametrize(("device_type", "branch", "expected"), [ - ("tici", "staging-c3-new", "staging-tici"), - ("tici", "dev-c3-new", "staging-tici"), - ("tici", "master", "master-tici"), - ("tici", "master-dev-c3-new", "master-tici"), - ("tizi", "staging-c3-new", "staging"), - ("tizi", "dev-c3-new", "dev"), - ("tizi", "master-dev-c3-new", "master-dev"), - ("tizi", "release3", "release-tizi"), - ("tizi", "release3-staging", "release-tizi-staging"), - ("mici", "release3", "release-mici"), - ("mici", "release3-staging", "release-mici-staging"), -]) -def test_sp_branch_migrations_from_param(mocker, device_type, branch, expected): - params = Params() - params.put("UpdaterTargetBranch", branch, block=True) - - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - - try: - assert Updater().target_branch == expected - finally: +class TestBranchMigrations(OpenpilotTestCase): + @parameterized.expand([ + ("tici", "staging-c3-new", "staging-tici"), + ("tici", "dev-c3-new", "staging-tici"), + ("tici", "master", "master-tici"), + ("tici", "master-dev-c3-new", "master-tici"), + ("tizi", "staging-c3-new", "staging"), + ("tizi", "dev-c3-new", "dev"), + ("tizi", "master-dev-c3-new", "master-dev"), + ("tizi", "release3", "release-tizi"), + ("tizi", "release3-staging", "release-tizi-staging"), + ("mici", "release3", "release-mici"), + ("mici", "release3-staging", "release-mici-staging"), + ], names=["device_type", "branch", "expected"]) + def test_sp_branch_migrations_from_current_branch(self, mocker, device_type, branch, expected): + params = Params() params.remove("UpdaterTargetBranch") + mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) + mocker.patch.object(Updater, "get_branch", return_value=branch) -@pytest.mark.parametrize(("device_type", "branch"), [ - ("tici", "unknown"), - ("tizi", "unknown"), - ("mici", "unknown"), -]) -def test_sp_branch_migrations_passthrough(mocker, device_type, branch): - params = Params() - params.remove("UpdaterTargetBranch") + assert Updater().target_branch == expected - mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) - mocker.patch.object(Updater, "get_branch", return_value=branch) - assert Updater().target_branch == branch + @parameterized.expand([ + ("tici", "staging-c3-new", "staging-tici"), + ("tici", "dev-c3-new", "staging-tici"), + ("tici", "master", "master-tici"), + ("tici", "master-dev-c3-new", "master-tici"), + ("tizi", "staging-c3-new", "staging"), + ("tizi", "dev-c3-new", "dev"), + ("tizi", "master-dev-c3-new", "master-dev"), + ("tizi", "release3", "release-tizi"), + ("tizi", "release3-staging", "release-tizi-staging"), + ("mici", "release3", "release-mici"), + ("mici", "release3-staging", "release-mici-staging"), + ], names=["device_type", "branch", "expected"]) + def test_sp_branch_migrations_from_param(self, mocker, device_type, branch, expected): + params = Params() + params.put("UpdaterTargetBranch", branch, block=True) + + mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) + + try: + assert Updater().target_branch == expected + finally: + params.remove("UpdaterTargetBranch") + + + @parameterized.expand([ + ("tici", "unknown"), + ("tizi", "unknown"), + ("mici", "unknown"), + ], names=["device_type", "branch"]) + def test_sp_branch_migrations_passthrough(self, mocker, device_type, branch): + params = Params() + params.remove("UpdaterTargetBranch") + + mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type) + mocker.patch.object(Updater, "get_branch", return_value=branch) + + assert Updater().target_branch == branch diff --git a/pyproject.toml b/pyproject.toml index 672ac277b2..dcc1024479 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,11 +65,6 @@ dev = [ testing = [ "coverage", "ty", - "pytest", - "pytest-cpp", - "pytest-subtests", - "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", - "pytest-mock", "ruff", "codespell", ] @@ -115,24 +110,6 @@ packages = [ [tool.hatch.metadata] allow-direct-references = true -# TODO-SP: upstream replaced pytest with a custom unittest runner (tools/test_runner.py). -# Remove this section and the pytest deps once sunnypilot tests are migrated to unittest. -[tool.pytest.ini_options] -minversion = "6.0" -addopts = "-Werror --strict-config --strict-markers --durations=10 -n auto --dist=loadgroup" -python_files = "test_*.py" -markers = [ - "slow: tests that take awhile to run and can be skipped with -m 'not slow'", - "tici: tests that are only meant to run on the C3/C3X", - "skip_tici_setup: mark test to skip tici setup fixture", - "nocapture: don't capture test output", - "shared_download_cache: share download cache between tests", - "xdist_group_class_property: group tests by a property of the class that contains them", -] -testpaths = [ - "openpilot", -] - [tool.codespell] quiet-level = 3 # if you've got a short variable name that's getting flagged, add it here @@ -171,7 +148,6 @@ exclude = [ lint.flake8-implicit-str-concat.allow-multiline = false [tool.ruff.lint.flake8-tidy-imports.banned-api] -"pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "time.time".msg = "Use time.monotonic. time.time can skip due to its reference clock, you probably want a monotonic clock" "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" "pyray.is_mouse_button_pressed".msg = "This can miss events. Use Widget._handle_mouse_press" diff --git a/uv.lock b/uv.lock index 102924151d..a86cd55d30 100644 --- a/uv.lock +++ b/uv.lock @@ -18,15 +18,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, ] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - [[package]] name = "certifi" version = "2026.7.22" @@ -378,15 +369,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, ] -[[package]] -name = "execnet" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, -] - [[package]] name = "filelock" version = "3.32.2" @@ -513,15 +495,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, ] -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - [[package]] name = "inputs" version = "0.5" @@ -808,11 +781,6 @@ submodules = [ testing = [ { name = "codespell" }, { name = "coverage" }, - { name = "pytest" }, - { name = "pytest-cpp" }, - { name = "pytest-mock" }, - { name = "pytest-subtests" }, - { name = "pytest-xdist" }, { name = "ruff" }, { name = "ty" }, ] @@ -856,11 +824,6 @@ requires-dist = [ { name = "pandacan", marker = "extra == 'submodules'", editable = "panda" }, { name = "pycapnp", specifier = "==2.1.0" }, { name = "pyjwt", extras = ["crypto"] }, - { name = "pytest", marker = "extra == 'testing'" }, - { name = "pytest-cpp", marker = "extra == 'testing'" }, - { name = "pytest-mock", marker = "extra == 'testing'" }, - { name = "pytest-subtests", marker = "extra == 'testing'" }, - { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pyzmq" }, { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, @@ -939,15 +902,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, ] -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - [[package]] name = "pycapnp" version = "2.1.0" @@ -1041,68 +995,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pytest" -version = "9.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, -] - -[[package]] -name = "pytest-cpp" -version = "2.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/a1/c2679d7ff2da20a0f89c7820ae2739cde739eac9b43c192531117b31b5f4/pytest_cpp-2.6.0.tar.gz", hash = "sha256:c2f49d3c038539ac84786a94d852e4f4619c34c95979c2bc69c20b3bdf051d85", size = 465490, upload-time = "2024-09-18T00:08:08.251Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/44/dc2f5d53165264ae5831f361fe7723c45da05718a97015b2eddc452cf503/pytest_cpp-2.6.0-py3-none-any.whl", hash = "sha256:b33de94609450feea2fba9efff3558b8ac8f1fdf40a99e263b395d4798b911bb", size = 15074, upload-time = "2024-09-18T00:08:06.415Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, -] - -[[package]] -name = "pytest-subtests" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/d9/20097971a8d315e011e055d512fa120fd6be3bdb8f4b3aa3e3c6bf77bebc/pytest_subtests-0.15.0.tar.gz", hash = "sha256:cb495bde05551b784b8f0b8adfaa27edb4131469a27c339b80fd8d6ba33f887c", size = 18525, upload-time = "2025-10-20T16:26:18.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/64/bba465299b37448b4c1b84c7a04178399ac22d47b3dc5db1874fe55a2bd3/pytest_subtests-0.15.0-py3-none-any.whl", hash = "sha256:da2d0ce348e1f8d831d5a40d81e3aeac439fec50bd5251cbb7791402696a9493", size = 9185, upload-time = "2025-10-20T16:26:17.239Z" }, -] - -[[package]] -name = "pytest-xdist" -version = "3.7.1.dev24+g2b4372b" -source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } -dependencies = [ - { name = "execnet" }, - { name = "pytest" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" From 6405f15d5a58dcb3aaac9a72ba194f8deb933e6c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 14 Aug 2026 01:51:28 -0400 Subject: [PATCH 230/325] models: use requests instead of aiohttp --- openpilot/sunnypilot/models/manager.py | 60 ++-- .../models/tests/test_manager_download.py | 300 ++++++++++++++++++ 2 files changed, 332 insertions(+), 28 deletions(-) create mode 100644 openpilot/sunnypilot/models/tests/test_manager_download.py diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index d742e968a9..7ba805ff95 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -9,7 +9,7 @@ import asyncio import os import time -import aiohttp +import requests from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog @@ -19,6 +19,9 @@ from openpilot.cereal import messaging, custom from openpilot.sunnypilot.models.fetcher import ModelFetcher from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file +# (connect, read) seconds. read is per-request inactivity, not a total cap +DOWNLOAD_TIMEOUT = (30, 30) + class ModelManagerSP: """Manages model downloads and status reporting""" @@ -63,30 +66,29 @@ class ModelManagerSP: """Downloads a file with progress tracking""" self._download_start_times[model.fileName] = time.monotonic() - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - response.raise_for_status() - total_size = int(response.headers.get("content-length", 0)) - bytes_downloaded = 0 + with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210 + response.raise_for_status() + total_size = int(response.headers.get("content-length", 0)) + bytes_downloaded = 0 - with open(path, 'wb') as f: # noqa: ASYNC230 - async for chunk in response.content.iter_chunked(self._chunk_size): # type: bytes - f.write(chunk) - bytes_downloaded += len(chunk) + with open(path, 'wb') as f: # noqa: ASYNC230 + for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes + f.write(chunk) + bytes_downloaded += len(chunk) - if self.params.get("ModelManager_DownloadIndex") is None: - raise Exception("Download cancelled") + if self.params.get("ModelManager_DownloadIndex") is None: + raise Exception("Download cancelled") - if total_size > 0: - progress = (bytes_downloaded / total_size) * 100 - model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading - model.downloadProgress.progress = progress - model.downloadProgress.eta = self._calculate_eta(model.fileName, progress) - self._sync_artifact_progress(model) - self._report_status() + if total_size > 0: + progress = (bytes_downloaded / total_size) * 100 + model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading + model.downloadProgress.progress = progress + model.downloadProgress.eta = self._calculate_eta(model.fileName, progress) + self._sync_artifact_progress(model) + self._report_status() - # Clean up start time after download completes - del self._download_start_times[model.fileName] + # Clean up start time after download completes + del self._download_start_times[model.fileName] async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None: from openpilot.common.file_chunker import get_chunk_name, get_manifest_path @@ -98,16 +100,18 @@ class ModelManagerSP: manifest_path = get_manifest_path(base_path) self._download_start_times[artifact.fileName] = time.monotonic() - for i, _ in enumerate(artifact.chunks): - chunk_url = get_chunk_name(base_url, i, num_chunks) - chunk_path = get_chunk_name(base_path, i, num_chunks) - chunk_downloaded = 0 - async with aiohttp.ClientSession() as session: - async with session.get(chunk_url) as response: + # Shared connection saves a TCP+TLS handshake per chunk. + # Keep sequential: the link saturates on one stream and Session is not thread-safe. + with requests.Session() as session: + for i, _ in enumerate(artifact.chunks): + chunk_url = get_chunk_name(base_url, i, num_chunks) + chunk_path = get_chunk_name(base_path, i, num_chunks) + chunk_downloaded = 0 + with session.get(chunk_url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: response.raise_for_status() chunk_size = int(response.headers.get("content-length", 0)) with open(chunk_path, 'wb') as f: # noqa: ASYNC230 - async for data in response.content.iter_chunked(self._chunk_size): + for data in response.iter_content(chunk_size=self._chunk_size): f.write(data) chunk_downloaded += len(data) if self.params.get("ModelManager_DownloadIndex") is None: diff --git a/openpilot/sunnypilot/models/tests/test_manager_download.py b/openpilot/sunnypilot/models/tests/test_manager_download.py new file mode 100644 index 0000000000..67fb9023af --- /dev/null +++ b/openpilot/sunnypilot/models/tests/test_manager_download.py @@ -0,0 +1,300 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import asyncio +import hashlib +import http.server +import os +import tempfile +import threading +import unittest +from typing import Any +from unittest import mock + +import requests +from urllib3.connectionpool import HTTPConnectionPool + +from openpilot.cereal import custom +from openpilot.common.test import OpenpilotTestCase +from openpilot.common.file_chunker import get_chunk_name, get_manifest_path +from openpilot.selfdrive.test.helpers import http_server_context +from openpilot.sunnypilot.models import manager as manager_module +from openpilot.sunnypilot.models.manager import ModelManagerSP + +CHUNK_BODIES = [b'A' * 5000, b'B' * 5000, b'C' * 3000] +WHOLE_BODY = b'Z' * 9000 + + +def sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +class DownloadHandler(http.server.BaseHTTPRequestHandler): + """Serves the fixture bodies. Class attributes are reset per test.""" + request_paths: list[str] = [] + fail_paths: dict[str, int] = {} + stall_paths: set[str] = set() + stall_event: threading.Event | None = None + + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 + pass + + def _body_for(self, path): + if path.endswith('.whole'): + return WHOLE_BODY + for i in range(len(CHUNK_BODIES)): + if path.endswith(get_chunk_name('', i, len(CHUNK_BODIES))): + return CHUNK_BODIES[i] + return None + + def do_GET(self): + type(self).request_paths.append(self.path) + + status = type(self).fail_paths.get(self.path) + if status: + self.send_response(status) + self.end_headers() + return + + body = self._body_for(self.path) + if body is None: + self.send_response(404) + self.end_headers() + return + + self.send_response(200) + self.send_header('Content-Length', str(len(body))) + self.end_headers() + + if self.path in type(self).stall_paths: + # write a little, then wait so the test can cancel mid-transfer + self.wfile.write(body[:100]) + self.wfile.flush() + if type(self).stall_event is not None: + type(self).stall_event.wait(timeout=5) + self.wfile.write(body[100:]) + else: + self.wfile.write(body) + + +class ManagerDownloadTestBase(OpenpilotTestCase): + def setUp(self): + super().setUp() + DownloadHandler.request_paths = [] + DownloadHandler.fail_paths = {} + DownloadHandler.stall_paths = set() + DownloadHandler.stall_event = None + + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.dest = self._tmp.name + + self.reported: list[float] = [] + + self.manager = ModelManagerSP.__new__(ModelManagerSP) + self.manager.params = mock.MagicMock() + self.manager.params.get.return_value = b'0' # not cancelled + self.manager.pm = mock.MagicMock() + self.manager.pm.send.side_effect = self._record_progress + self.manager.selected_bundle = None + self.manager.active_bundle = None + self.manager.available_models = [] + self.manager._chunk_size = 1024 + self.manager._download_start_times = {} + + def _record_progress(self, *args) -> None: + """Runs on every real _report_status send.""" + artifact = getattr(self, 'artifact', None) + if artifact is not None: + self.reported.append(float(artifact.downloadProgress.progress)) + + def make_artifact(self, chunked: bool): + bundle = custom.ModelManagerSP.ModelBundle.new_message() + bundle.init('models', 1) + artifact = bundle.models[0].artifact + artifact.fileName = 'driving_test_tinygrad.pkl' + if chunked: + artifact.downloadUri.uri = self.base_url + '/driving_test_tinygrad.pkl' + artifact.downloadUri.sha256 = sha256(b''.join(CHUNK_BODIES)) + artifact.init('chunks', len(CHUNK_BODIES)) + for i, body in enumerate(CHUNK_BODIES): + artifact.chunks[i].sha256 = sha256(body) + else: + artifact.downloadUri.uri = self.base_url + '/driving_test_tinygrad.pkl.whole' + artifact.downloadUri.sha256 = sha256(WHOLE_BODY) + self._bundle = bundle + self.artifact = artifact + return artifact + + def chunk_paths(self, base_path): + return [get_chunk_name(base_path, i, len(CHUNK_BODIES)) for i in range(len(CHUNK_BODIES))] + + def assert_no_partials(self, base_path): + leftovers = [p for p in [base_path, get_manifest_path(base_path)] + self.chunk_paths(base_path) + if os.path.isfile(p)] + assert leftovers == [], f"partial files left behind: {leftovers}" + + +class TestManagerDownload(ManagerDownloadTestBase): + """Exercises the real _download_file / _download_chunked against a local server.""" + + def run_with_server(self, fn): + with http_server_context(handler=DownloadHandler) as (host, port): + self.base_url = f'http://{host}:{port}' + return fn() + + def test_download_file_writes_exact_bytes(self): + def body(): + artifact = self.make_artifact(chunked=False) + path = os.path.join(self.dest, artifact.fileName) + asyncio.run(self.manager._download_file(artifact.downloadUri.uri, path, artifact)) + with open(path, 'rb') as f: + written = f.read() + assert written == WHOLE_BODY + assert sha256(written) == artifact.downloadUri.sha256 + assert artifact.fileName not in self.manager._download_start_times + self.run_with_server(body) + + def test_download_chunked_writes_all_chunks_and_manifest(self): + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + + for i, expected in enumerate(CHUNK_BODIES): + with open(get_chunk_name(base_path, i, len(CHUNK_BODIES)), 'rb') as f: + assert f.read() == expected, f"chunk {i} body mismatch" + + with open(get_manifest_path(base_path)) as f: + assert f.read() == str(len(CHUNK_BODIES)) + + assert not os.path.isfile(base_path), "base file should be removed after chunking" + assert artifact.fileName not in self.manager._download_start_times + self.run_with_server(body) + + def test_progress_is_monotonic_and_bounded(self): + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + + assert self.reported, "expected progress reports" + for a, b in zip(self.reported, self.reported[1:], strict=False): + assert b >= a, f"progress went backwards: {a} -> {b}" + assert max(self.reported) <= 99.0, f"chunked progress must stay <=99 until verify, got {max(self.reported)}" + self.run_with_server(body) + + def test_session_is_reused_across_chunks(self): + """One connection pool shared across every chunk.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + + pools = [] + original = HTTPConnectionPool.urlopen + + def tracked(pool_self, *args, **kwargs): + pools.append(id(pool_self)) + return original(pool_self, *args, **kwargs) + + with mock.patch.object(HTTPConnectionPool, 'urlopen', tracked): + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + + assert len(pools) == len(CHUNK_BODIES), f"expected one request per chunk, got {len(pools)}" + assert len(set(pools)) == 1, f"connection pool not reused across chunks: {len(set(pools))} pools" + self.run_with_server(body) + + def test_http_error_propagates(self): + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + failing = '/' + os.path.basename(get_chunk_name(artifact.downloadUri.uri, 1, len(CHUNK_BODIES))) + DownloadHandler.fail_paths = {failing: 404} + + with self.assertRaises(requests.exceptions.HTTPError): + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + + # chunk 1 failed, so its file and the manifest must not exist + assert not os.path.isfile(get_chunk_name(base_path, 1, len(CHUNK_BODIES))) + assert not os.path.isfile(get_manifest_path(base_path)) + self.run_with_server(body) + + def test_cancellation_mid_transfer(self): + """Cancellation is checked inside the byte loop; it must still fire after the port.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + self.manager.params.get.return_value = None # cancelled + + with self.assertRaises(Exception) as ctx: + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + assert 'cancelled' in str(ctx.exception).lower() + assert not os.path.isfile(get_manifest_path(base_path)) + self.run_with_server(body) + + def test_repeat_downloads_are_stable(self): + """Back-to-back runs must produce identical bytes and leak no start-time state.""" + def body(): + for _ in range(2): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + for i, expected in enumerate(CHUNK_BODIES): + with open(get_chunk_name(base_path, i, len(CHUNK_BODIES)), 'rb') as f: + assert f.read() == expected + assert self.manager._download_start_times == {} + self.run_with_server(body) + + +class TestManagerImports(OpenpilotTestCase): + """Catches undeclared dependencies. aiohttp lived only in the AGNOS venv; 19.6 dropped + it and models_manager died on device while CI stayed green.""" + + def test_manager_imports(self): + assert manager_module.ModelManagerSP is not None + + def test_no_undeclared_http_client(self): + with open(manager_module.__file__) as f: + src = f.read() + assert 'import aiohttp' not in src, "aiohttp is not available on AGNOS 19.6; use requests" + + def test_download_timeout_is_explicit(self): + connect, read = manager_module.DOWNLOAD_TIMEOUT + assert connect > 0 and read > 0, "requests defaults to no timeout; downloads would hang forever" + + +@unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network') +class TestLiveModelManifest(OpenpilotTestCase): + """Every artifact and chunk URL in the published manifest must resolve.""" + + def test_all_manifest_urls_available(self): + from openpilot.sunnypilot.models.fetcher import ModelFetcher + + manifest = requests.get(ModelFetcher.MODEL_URL, timeout=30).json() + session = requests.Session() + dead = [] + + for bundle in manifest.get('bundles', []): + for model in bundle.get('models', []): + artifact = model['artifact'] + url = artifact['download_uri']['url'] + chunks = artifact.get('chunks', []) + urls = ([url] if not chunks + else [get_chunk_name(url, i, len(chunks)) for i in range(len(chunks))]) + for u in urls: + try: + r = session.head(u, timeout=15, allow_redirects=True) + if r.status_code != 200: + dead.append(f"{bundle.get('short_name')}: HTTP {r.status_code} {u}") + except requests.RequestException as e: + dead.append(f"{bundle.get('short_name')}: {type(e).__name__} {u}") + + assert not dead, "unreachable model URLs:\n" + "\n".join(dead) + + +if __name__ == '__main__': + unittest.main() From 516ec1e68203439a73f340f1d0b3b91eabc626ee Mon Sep 17 00:00:00 2001 From: Toby Penner Date: Fri, 14 Aug 2026 13:45:22 -0700 Subject: [PATCH 231/325] Revert big RL model (#38627) --- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx index a74573ca29..4a04bd7833 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10926f2c0911821ca0e72439c1c3bf3ec11f0a08789aa14b7ee8f25379b2afa4 -size 1753235978 +oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff +size 1757355221 From d48cfa730bf21593b684efeaf3fd8940695e1dea Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 14 Aug 2026 17:21:34 -0400 Subject: [PATCH 232/325] ui: dismiss screensaver on wake instead of on awake transition (#1907) * fix wake-up behavior * ui: dismiss screensaver on wake instead of on awake transition --------- Co-authored-by: Jason Wen --- openpilot/selfdrive/ui/sunnypilot/ui_state.py | 17 ++++++++++++----- openpilot/selfdrive/ui/ui_state.py | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 510f19383a..602830a4db 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -241,19 +241,26 @@ class DeviceSP: def _set_awake(self, on: bool, _ui_state=None): self._blocked_by_screensaver = False - if _ui_state.boot_offroad_mode == 1 and not on: - _ui_state.params.put_bool("OffroadMode", True) - if not on and _ui_state.screensaver_enabled: if _ui_state.screensaver.was_dismissed: - if gui_app.get_active_widget() == _ui_state.screensaver: - gui_app.pop_widget() + self.dismiss_screensaver(_ui_state) elif _ui_state.screensaver.is_active: self._blocked_by_screensaver = True else: _ui_state.screensaver.initialize() gui_app.push_widget(_ui_state.screensaver) self._blocked_by_screensaver = True + else: + self.dismiss_screensaver(_ui_state) + + # blocked runs every frame, so write only when actually sleeping + if _ui_state.boot_offroad_mode == 1 and not on and not self._blocked_by_screensaver: + _ui_state.params.put_bool("OffroadMode", True) + + def dismiss_screensaver(self, _ui_state) -> None: + if gui_app.get_active_widget() == _ui_state.screensaver: + gui_app.pop_widget() + self._blocked_by_screensaver = False @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index ec5e1a27a4..e0aca74ff2 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -347,6 +347,10 @@ class Device(DeviceSP): self._set_awake(ui_state.ignition or not interaction_timeout or PC) def _set_awake(self, on: bool, _ui_state=None): + # screensaver holds _awake True, so waking is not a state change + if on and self._blocked_by_screensaver: + self.dismiss_screensaver(_ui_state or ui_state) + if on != self._awake: super()._set_awake(on, _ui_state or ui_state) if self._blocked_by_screensaver: From df4566ef2faa6a25a890b3b9a2b8b737798b64a8 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:01:22 -0700 Subject: [PATCH 233/325] ui: small software ui fixes (#38630) add icon and scroller for branch name --- openpilot/selfdrive/assets/icons_mici/settings/software.png | 4 ++-- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/assets/icons_mici/settings/software.png b/openpilot/selfdrive/assets/icons_mici/settings/software.png index 5cf528cbdd..15baccd725 100644 --- a/openpilot/selfdrive/assets/icons_mici/settings/software.png +++ b/openpilot/selfdrive/assets/icons_mici/settings/software.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4c38772e6080aa4b8bf5212d3619e949775468c64f3edb88a1a426d767c38d2 -size 1579 +oid sha256:190e196eba6feffec125ac66cf7e77620b759e346fa69b80e3a3884a5694cb15 +size 3225 diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index bd4b966f49..0f12004828 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -47,7 +47,7 @@ class SoftwareInfoLayoutMici(Widget): self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) + font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) def _update_state(self): desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") From d9c4120f891430da60a353e91600483ef0292de1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 12:03:34 -0700 Subject: [PATCH 234/325] ui: fix text and icon overlap on button (#38628) --- openpilot/selfdrive/ui/mici/widgets/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index 0ecda6a0c5..cad40d7d01 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -150,8 +150,8 @@ class BigButton(Widget): super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None) def _width_hint(self) -> int: - # Single line if scrolling, so hide behind icon if exists - icon_size = self._txt_icon.width if self._txt_icon and self._scroll and self.value else 0 + # A value moves the title to the top, where it shares space with the icon. + icon_size = self._txt_icon.width if self._txt_icon and self.value else 0 return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size) def _get_label_font_size(self): From 76b69af59ab278d50a3338780b256d7ddc5fe889 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 12:08:38 -0700 Subject: [PATCH 235/325] remove offroad OS update alert (#38633) --- openpilot/common/params_keys.h | 1 - openpilot/selfdrive/selfdrived/alerts_offroad.json | 4 ---- openpilot/system/updated/updated.py | 3 --- 3 files changed, 8 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 0019f6c9ac..7a914128fa 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -94,7 +94,6 @@ inline static std::unordered_map keys = { {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, - {"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index add8d89550..07bab0c377 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -17,10 +17,6 @@ "severity": 1, "_comment": "Set extra field to the failed reason." }, - "Offroad_NeosUpdate": { - "text": "An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install.", - "severity": 0 - }, "Offroad_UnregisteredHardware": { "text": "Failed to register with comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.", "severity": 1 diff --git a/openpilot/system/updated/updated.py b/openpilot/system/updated/updated.py index 24acb2fd17..1bba66d0c9 100755 --- a/openpilot/system/updated/updated.py +++ b/openpilot/system/updated/updated.py @@ -217,13 +217,10 @@ def handle_agnos_update() -> None: set_consistent_flag(False) cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") - set_offroad_alert("Offroad_NeosUpdate", True) manifest_path = os.path.join(OVERLAY_MERGED, "openpilot/system/hardware/comma/agnos.json") target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) - set_offroad_alert("Offroad_NeosUpdate", False) - class Updater: From 3447ec17895fb3b511551ef43e174943b5e5dab5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 12:28:38 -0700 Subject: [PATCH 236/325] selfdrived: excessive actuation check is not for notCars (#38634) --- openpilot/selfdrive/selfdrived/selfdrived.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 73f382ac3c..f2bd8a4c7b 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -297,7 +297,7 @@ class SelfdriveD: device_motion = Pose.from_device_motion(self.sm['deviceMotion']) self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_motion) - if self.calibrated_pose is not None: + if self.calibrated_pose is not None and not self.CP.notCar: excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) if not self.excessive_actuation and excessive_actuation is not None: set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) From 748c725e3e63a3e4625092d1e87afb147e6ec643 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 12:39:26 -0700 Subject: [PATCH 237/325] soundd: more robust test (#38635) --- openpilot/selfdrive/ui/tests/test_soundd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/ui/tests/test_soundd.py b/openpilot/selfdrive/ui/tests/test_soundd.py index 11349f1f40..ca3b7ab209 100644 --- a/openpilot/selfdrive/ui/tests/test_soundd.py +++ b/openpilot/selfdrive/ui/tests/test_soundd.py @@ -19,14 +19,14 @@ class TestSoundd(OpenpilotTestCase): sm.update(100) assert sm.updated['selfdriveState'] - received_at = sm.recv_time['selfdriveState'] - clock = mocker.patch("openpilot.selfdrive.ui.soundd.time.monotonic", return_value=received_at + SELFDRIVE_STATE_TIMEOUT) + sm.recv_time['selfdriveState'] = 0 + clock = mocker.patch("openpilot.selfdrive.ui.soundd.time.monotonic", return_value=SELFDRIVE_STATE_TIMEOUT) assert not check_selfdrive_timeout_alert(sm) - clock.return_value = received_at + SELFDRIVE_STATE_TIMEOUT + 0.1 + clock.return_value = SELFDRIVE_STATE_TIMEOUT + 0.1 assert check_selfdrive_timeout_alert(sm) - clock.return_value = received_at + SELFDRIVE_STATE_TIMEOUT + 10 + clock.return_value = SELFDRIVE_STATE_TIMEOUT + 10 assert not check_selfdrive_timeout_alert(sm) # TODO: add test with micd for checking that soundd actually outputs sounds From 28560d6cf1b44f2f01c1dc0e6855a1fb64fd1e11 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 13:18:02 -0700 Subject: [PATCH 238/325] show an offroad alert to switch to a chestnut branch (#38636) * show an offroad alert to switch to a chestnut branch * add to keys --- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/selfdrived/alerts_offroad.json | 4 ++++ openpilot/system/hardware/hardwared.py | 2 ++ 3 files changed, 7 insertions(+) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 7a914128fa..e44d8f8e34 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -91,6 +91,7 @@ inline static std::unordered_map keys = { {"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index 07bab0c377..b0179c0ac3 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -17,6 +17,10 @@ "severity": 1, "_comment": "Set extra field to the failed reason." }, + "Offroad_ChestnutBranch": { + "text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", + "severity": 0 + }, "Offroad_UnregisteredHardware": { "text": "Failed to register with comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.", "severity": 1 diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 270a75ade6..d7fac517fb 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -237,6 +237,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() + big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) @@ -298,6 +299,7 @@ def hardware_thread(end_event, hw_queue) -> None: set_usb_state(msg.deviceState, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state) + set_offroad_alert_if_changed("Offroad_ChestnutBranch", msg.deviceState.chestnutPresent and not big_model_available) # this subset is only used for offroad temp_sources = [ From fb555fdefdc3fc6978f42f0530c9da5d72b2a9fd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 14:28:45 -0700 Subject: [PATCH 239/325] big model doesn't need big build times (#38637) * big model doesn't need big build times * revert htat --- openpilot/selfdrive/modeld/SConscript | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 30b008078e..30a31aae27 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -79,7 +79,9 @@ for usbgpu in [False, True] if USBGPU else [False]: file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) - cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py ' + # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. + taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' + cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' f'--model-size {model_w}x{model_h} ' f'--camera-resolutions {camera_res_args} ' f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' From 5e3d17c72cf2cd90a57d1cd31771c3aacfc9f79b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 15:31:19 -0700 Subject: [PATCH 240/325] chestnut: fix flashing on old FW (#38639) --- openpilot/system/hardware/chestnut/flash.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpilot/system/hardware/chestnut/flash.py b/openpilot/system/hardware/chestnut/flash.py index 32aa5a443a..f0d828031d 100755 --- a/openpilot/system/hardware/chestnut/flash.py +++ b/openpilot/system/hardware/chestnut/flash.py @@ -33,6 +33,7 @@ USBDEVFS_SETCONFIGURATION = 0x80045505 USBDEVFS_CLAIMINTERFACE = 0x8004550F USBDEVFS_RESET = 0x5514 USBDEVFS_CLEAR_HALT = 0x80045515 +MAX_REGISTER_READ_SIZE = 255 _deadline = float("inf") @@ -146,6 +147,7 @@ def claim_interface(path, setup=False): class Flash: def __init__(self): self.fd = -1 + self.max_register_read_size = MAX_REGISTER_READ_SIZE def close(self): if self.fd >= 0: @@ -160,6 +162,9 @@ class Flash: if in_rom_bootloader(vid_pid, product): raise RomFallback("chestnut fell back to the ROM bootloader") if path is not None: + speed = int(open(path + "/speed").read()) + # USB2 firmware truncates larger reads to one full packet without a terminating ZLP. + self.max_register_read_size = 64 if speed < 5000 else MAX_REGISTER_READ_SIZE self.fd = claim_interface(path) return time.sleep(0.1) @@ -231,8 +236,8 @@ class Flash: while len(out) < length: n = min(4096, length - len(out)) self.transaction(0x03, addr + len(out), max(4096, n)) - for off in range(0, n, 255): - out += self.reg_read(0x7000 + off, min(255, n - off)) + for off in range(0, n, self.max_register_read_size): + out += self.reg_read(0x7000 + off, min(self.max_register_read_size, n - off)) return bytes(out) def erase_sector(self, addr): From 48b7f171a7e15fa2e98e6df85da579c7ce0203ba Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 15:33:43 -0700 Subject: [PATCH 241/325] release: speed up pushes by 9x (#38640) --- tools/release/build_release.sh | 3 ++- tools/release/build_stripped.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 6657962ade..4cca754a08 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -94,6 +94,7 @@ REFS=() for branch in ${RELEASE_BRANCH//,/ }; do REFS+=("$BUILD_BRANCH:$branch") done -git push -f origin "${REFS[@]}" +# uploading the larger pack is faster than spending CPU to optimize it +git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin "${REFS[@]}" echo "[-] done T=$SECONDS" diff --git a/tools/release/build_stripped.sh b/tools/release/build_stripped.sh index 0e957c9212..6c1ba4e097 100755 --- a/tools/release/build_stripped.sh +++ b/tools/release/build_stripped.sh @@ -83,7 +83,8 @@ fi if [ ! -z "$BRANCH" ]; then echo "[-] Pushing to $BRANCH T=$SECONDS" - git push -f origin tmp:$BRANCH + # uploading the larger pack is faster than spending CPU to optimize it + git -c pack.window=0 -c pack.depth=0 -c pack.compression=0 push -f origin tmp:$BRANCH fi echo "[-] done T=$SECONDS, ready at $TARGET_DIR" From 6ad35321133fd0a7979dd85415be110d80ba582a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 15:54:19 -0700 Subject: [PATCH 242/325] Document chestnut branches --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c0d944a2e..1bdb412b37 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ Running `master` and other branches directly is supported, but it's recommended | comma four branch | comma four + chestnut branch | comma 3X branch | URL | description | |------------------------|------------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| | `release-mici` | `release-chestnut` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | -| `release-mici-staging` | | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | +| `release-mici-staging` | `release-chestnut-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly` | `nightly-chestnut` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-dev` | `nightly-chestnut-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | To start developing openpilot ------ From a996f8ef90823611d6ed9129d2472ebcb2b9f4f0 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 15:59:20 -0700 Subject: [PATCH 243/325] chestnut gets its own table --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1bdb412b37..21737a1ae1 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,21 @@ We have detailed instructions for [how to install the harness and device in a ca Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches: -| comma four branch | comma four + chestnut branch | comma 3X branch | URL | description | -|------------------------|------------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| -| `release-mici` | `release-chestnut` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | -| `release-mici-staging` | `release-chestnut-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | `nightly-chestnut` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | `nightly-chestnut-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | +| comma four branch | comma 3X branch | URL | description | +|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------| +| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. | +| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | + +For [chestnut](https://comma.ai/shop/chestnut), use the following installer URLs: + +| branch | URL | description | +|------------------------------|------------------------------------------------------------|-------------------------------------------------------------------------------------| +| `release-chestnut` | installer.comma.ai/commaai/release-chestnut | This is openpilot's release branch. | +| `release-chestnut-staging` | installer.comma.ai/commaai/release-chestnut-staging | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly-chestnut` | installer.comma.ai/commaai/nightly-chestnut | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-chestnut-dev` | installer.comma.ai/commaai/nightly-chestnut-dev | Same as nightly, but includes experimental development features for some cars. | To start developing openpilot ------ From 391132465d6d216e455d5bf81de695a244d3eef6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 16:01:42 -0700 Subject: [PATCH 244/325] release: chestnut build scripts (#38638) * release: chestnut build scripts * simplify * only max * no new script * release: exclude local virtualenv --- tools/release/build_release.sh | 11 +++++++++++ tools/release/release_files.py | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 4cca754a08..5dca1626ca 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -46,10 +46,21 @@ echo "[-] committing version $VERSION T=$SECONDS" git add -f . git commit -a -m "openpilot v$VERSION release" +# use the full CPU available for speeding up the build. +# openpilot resets the CPU frequencies when test_onroad.py runs below. +for policy in /sys/devices/system/cpu/cpufreq/policy*; do + [ -d "$policy" ] || continue + hardware_max="$(cat "$policy/cpuinfo_max_freq")" + echo "$hardware_max" | sudo tee "$policy/scaling_max_freq" >/dev/null +done + # Build and test before launch_chffrplus.sh creates the on-device package # symlinks. SConstruct uses the same package roots for build subprocesses. export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" scons +if [ -n "$INCLUDE_BIG_MODEL" ]; then + test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest +fi if [ -z "$PANDA_DEBUG_BUILD" ]; then # release panda fw diff --git a/tools/release/release_files.py b/tools/release/release_files.py index 223e3f2c77..1558f04a26 100755 --- a/tools/release/release_files.py +++ b/tools/release/release_files.py @@ -8,13 +8,11 @@ ROOT = os.path.abspath(os.path.join(HERE, "../..")) blacklist = [ ".git/", + ".venv/", ".github/workflows/", "matlab.*.md", - # skip big model for now - "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx", - # no LFS or submodules in release ".lfsconfig", ".gitattributes", @@ -32,6 +30,8 @@ if __name__ == "__main__": continue rf = str(f.relative_to(ROOT)) + if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"): + continue blacklisted = any(re.search(p, rf) for p in blacklist) whitelisted = any(re.search(p, rf) for p in whitelist) if blacklisted and not whitelisted: From dcbd66ad81db19bb5bcc69c36c0c9e5bcf9c3f9a Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Sat, 15 Aug 2026 19:05:02 -0400 Subject: [PATCH 245/325] ui: big model failed alert (#38629) * big model failed + supply voltage check * remove ltssm and supply voltage stuff for seperate PR --- openpilot/selfdrive/selfdrived/events.py | 3 ++- openpilot/selfdrive/selfdrived/selfdrived.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/selfdrived/events.py b/openpilot/selfdrive/selfdrived/events.py index 1482a4e334..69fa900be4 100755 --- a/openpilot/selfdrive/selfdrived/events.py +++ b/openpilot/selfdrive/selfdrived/events.py @@ -409,7 +409,8 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { }, EventName.bigModelFailed: { - ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nnow driving on small model", duration=20.), + ET.SOFT_DISABLE: soft_disable_alert("Big Model Failed"), + ET.PERMANENT: NormalPermanentAlert("Big Model Failed ", "Restart the car to retry,\nsmall model is still available", duration=20.), }, EventName.lateralManeuver: { diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index f2bd8a4c7b..1f1f6f7349 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -343,6 +343,9 @@ class SelfdriveD: # All events here should at least have NO_ENTRY and SOFT_DISABLE. num_events = len(self.events) + if self.big_model_active and big_failed: + self.events.add(EventName.bigModelFailed) + not_running = {p.name for p in self.sm['managerState'].processes if not p.running and p.shouldBeRunning} if self.sm.recv_frame['managerState'] and len(not_running): if not_running != self.not_running_prev: From 97542f838f5be76be24f3026a286ec9430bbfe17 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 15 Aug 2026 16:17:21 -0700 Subject: [PATCH 246/325] check the pkl too --- openpilot/system/hardware/hardwared.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index d7fac517fb..a423d8f97d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -237,7 +237,8 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) + big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) or \ + os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest")) while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From 73fc740831e3ee8efad459de8311fda233c5ebef Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 15 Aug 2026 19:34:50 -0400 Subject: [PATCH 247/325] [TIZI/TICI] ui: fix developer UI crash on renamed field (#1910) ui: fix developer UI crash on renamed lateralTorqueParameters valid field --- .../selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py b/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py index f89edef48b..a8ecb5f8ab 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py @@ -252,7 +252,7 @@ class FrictionCoefficientElement: ltp = sm['lateralTorqueParameters'] value = f"{ltp.frictionCoefficientFiltered:.3f}" - color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE + color = rl.Color(0, 255, 0, 255) if ltp.valid else rl.WHITE return UiElement(value, "FRIC.", self.unit, color) @@ -266,7 +266,7 @@ class LatAccelFactorElement: ltp = sm['lateralTorqueParameters'] value = f"{ltp.latAccelFactorFiltered:.3f}" - color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE + color = rl.Color(0, 255, 0, 255) if ltp.valid else rl.WHITE return UiElement(value, "L.A.F.", self.unit, color) From 91d0f3309c8b0d470b5a6faf9af744cdb42301ff Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 15 Aug 2026 19:56:19 -0400 Subject: [PATCH 248/325] DEC: restore gate on longitudinal E2E output (#1911) dec: restore Dynamic Experimental Control gate on longitudinal e2e output --- .../controls/lib/longitudinal_planner.py | 13 +- .../lib/dec/tests/test_dec_planner_gate.py | 112 ++++++++++++++++++ 2 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dec_planner_gate.py diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index 12b4f9da61..8b62808dc0 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -139,23 +139,16 @@ class LongitudinalPlanner(LongitudinalPlannerSP): output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if self.is_e2e(sm): - output_a_target = min(output_a_target_e2e, output_a_target_mpc) - self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc - if output_a_target < output_a_target_mpc: - self.mpc.source = LongitudinalPlanSource.e2e - else: - output_a_target = output_a_target_mpc - self.output_should_stop = output_should_stop_mpc + is_e2e = self.is_e2e(sm) - self.a_cruise = get_cruise_accel(sm['selfdriveState'].experimentalMode, v_cruise, v_ego, + self.a_cruise = get_cruise_accel(is_e2e, v_cruise, v_ego, self.a_cruise, steer_angle_without_offset, self.CP, self.dt, accel_coast, self.allow_throttle) cruise_should_stop = should_stop(v_ego, self.a_cruise) candidates = [(output_a_target_mpc, self.mpc.source, output_should_stop_mpc), (self.a_cruise, LongitudinalPlanSource.cruise, cruise_should_stop)] - if sm['selfdriveState'].experimentalMode: + if is_e2e: candidates.append((output_a_target_e2e, LongitudinalPlanSource.e2e, output_should_stop_e2e)) output_a_target, self.mpc.source, _ = min(candidates, key=lambda c: c[0]) diff --git a/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dec_planner_gate.py b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dec_planner_gate.py new file mode 100644 index 0000000000..1f5c577028 --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/controls/lib/dec/tests/test_dec_planner_gate.py @@ -0,0 +1,112 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from typing import cast + +from openpilot.cereal import custom, messaging +from opendbc.car import structs +from openpilot.common.test import OpenpilotTestCase +from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, LongitudinalPlanSource +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController + +V_EGO = 20.0 +E2E_ACCEL = -3.0 # low enough that e2e wins the min() whenever it is a candidate + + +class MockDec: + def __init__(self, active: bool, mode: str): + self._active = active + self._mode = mode + + def update(self, sm): + pass + + def active(self) -> bool: + return self._active + + def mode(self) -> str: + return self._mode + + def enabled(self) -> bool: + return True + + +class MockSubMaster(dict): + def __init__(self, services: dict): + super().__init__(services) + self.valid = dict.fromkeys(services, True) + self.logMonoTime = dict.fromkeys(services, 0) + self.updated = dict.fromkeys(services, True) + self.recv_frame = dict.fromkeys(services, 1) + + def all_checks(self, service_list=None) -> bool: + return True + + +def build_sm(experimental_mode: bool) -> MockSubMaster: + services = {} + for service in ("radarState", "controlsState", "vehicleParameters", "carStateSP", + "liveMapDataSP", "gpsLocationExternal", "gpsLocation"): + services[service] = getattr(messaging.new_message(service), service) + + car_state = messaging.new_message('carState') + car_state.carState.vEgo = V_EGO + car_state.carState.vCruise = 100.0 + car_state.carState.vCruiseCluster = 100.0 + services['carState'] = car_state.carState.as_reader() + + selfdrive_state = messaging.new_message('selfdriveState') + selfdrive_state.selfdriveState.experimentalMode = experimental_mode + selfdrive_state.selfdriveState.enabled = True + services['selfdriveState'] = selfdrive_state.selfdriveState.as_reader() + + car_control = messaging.new_message('carControl') + car_control.carControl.enabled = True + services['carControl'] = car_control.carControl.as_reader() + + model = messaging.new_message('modelV2') + model.modelV2.orientationRate.z = [0.01] * 33 # nonzero: a straight path divides by zero in SCC vision + model.modelV2.velocity.x = [V_EGO] * 33 + model.modelV2.position.x = [float(i) for i in range(33)] + model.modelV2.action.desiredAcceleration = E2E_ACCEL + services['modelV2'] = model.modelV2.as_reader() + + return MockSubMaster(services) + + +def build_planner(dec_active: bool, dec_mode: str) -> LongitudinalPlanner: + CP = structs.CarParams() + CP.steerRatio = 15.0 + CP.wheelbase = 2.7 + CP.longitudinalActuatorDelay = 0.2 + CP_SP = custom.CarParamsSP.new_message().as_reader() + + planner = LongitudinalPlanner(CP, CP_SP, init_v=V_EGO) + planner.dec = cast(DynamicExperimentalController, MockDec(dec_active, dec_mode)) + return planner + + +class TestDecPlannerGate(OpenpilotTestCase): + """The e2e candidate must be gated on is_e2e(), not raw experimentalMode.""" + + def _source(self, experimental_mode: bool, dec_active: bool, dec_mode: str) -> LongitudinalPlanSource: + planner = build_planner(dec_active, dec_mode) + planner.update(build_sm(experimental_mode)) + return planner.mpc.source + + def test_no_e2e_when_experimental_mode_off(self): + assert self._source(False, False, 'acc') != LongitudinalPlanSource.e2e + + def test_e2e_when_dec_inactive(self): + # DEC off: behavior must match upstream + assert self._source(True, False, 'acc') == LongitudinalPlanSource.e2e + + def test_e2e_when_dec_blended(self): + assert self._source(True, True, 'blended') == LongitudinalPlanSource.e2e + + def test_no_e2e_when_dec_holds_acc(self): + # the regression + assert self._source(True, True, 'acc') != LongitudinalPlanSource.e2e From 0f40ca1d88049fcbd61ca5c62312c1650e91096d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 12:50:16 -0700 Subject: [PATCH 249/325] pigeond: continuously try to get AGPS (#38642) --- openpilot/system/ubloxd/pigeond.py | 38 ++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpilot/system/ubloxd/pigeond.py b/openpilot/system/ubloxd/pigeond.py index 3da9350241..08ed568d43 100755 --- a/openpilot/system/ubloxd/pigeond.py +++ b/openpilot/system/ubloxd/pigeond.py @@ -3,11 +3,12 @@ import sys import time import signal import struct +import threading import requests import urllib.parse from datetime import datetime, UTC -from openpilot.cereal import messaging +from openpilot.cereal import log, messaging from openpilot.common.api import Api from openpilot.common.time_helpers import system_time_valid from openpilot.common.params import Params @@ -46,7 +47,6 @@ def get_assistnow_messages() -> list[bytes]: params = Params() if token := params.get('AssistNowToken'): cloudlog.warning("Downloading AssistNow data directly from u-blox") - # TODO: implement adding the last known location r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ 'token': token, 'gnss': 'gps,glo', @@ -240,14 +240,6 @@ def init_pigeon(pigeon: TTYPigeon) -> bool: )) pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) - # A configured u-blox token takes precedence over comma's AGPS proxy. - try: - for msg in get_assistnow_messages(): - pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) - cloudlog.warning("AssistNow messages sent") - except Exception: - cloudlog.warning("failed to get AssistNow messages") - cloudlog.warning("Pigeon GPS on!") break except TimeoutError: @@ -287,12 +279,38 @@ def run_receiving(duration: int = 0): start_time = time.monotonic() last_almanac_save = time.monotonic() + assist_attempted = False + assist_messages = None + + def download_assistnow() -> None: + nonlocal assist_messages + sm = messaging.SubMaster(['deviceState']) + while assist_messages is None: + sm.update(1000) + if system_time_valid() and sm['deviceState'].networkType != log.DeviceState.NetworkType.none: + try: + assist_messages = get_assistnow_messages() + except Exception: + cloudlog.warning("failed to get AssistNow messages") + time.sleep(10.) + threading.Thread(target=download_assistnow, daemon=True).start() + while (duration == 0) or (time.monotonic() - start_time < duration): + if assist_messages is not None and not assist_attempted: + assist_attempted = True + try: + for msg in assist_messages: + pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK) + cloudlog.warning("AssistNow messages sent") + except Exception: + cloudlog.warning("failed to send AssistNow messages") + dat = pigeon.receive() if len(dat) > 0: if dat[0] == 0x00: cloudlog.warning("received invalid data from ublox, re-initing!") init(pigeon) + assist_attempted = False continue # send out to socket From ec86732af8d3acab8cedbfe784099a5535b3f92a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 13:11:50 -0700 Subject: [PATCH 250/325] ui: fix false positive openpilot unavailable on startup (#38643) --- openpilot/selfdrive/ui/mici/onroad/alert_renderer.py | 2 +- openpilot/selfdrive/ui/onroad/alert_renderer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index fe96bbd032..d2896e1807 100644 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -128,7 +128,7 @@ class AlertRenderer(Widget): # 1. Never received selfdriveState since going onroad waiting_for_startup = recv_frame < ui_state.started_frame - if waiting_for_startup and time_since_onroad > 5: + if waiting_for_startup and time_since_onroad > 10: return ALERT_STARTUP_PENDING # 2. Lost communication with selfdriveState after receiving it diff --git a/openpilot/selfdrive/ui/onroad/alert_renderer.py b/openpilot/selfdrive/ui/onroad/alert_renderer.py index 29ace66287..62511b87db 100644 --- a/openpilot/selfdrive/ui/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/onroad/alert_renderer.py @@ -92,7 +92,7 @@ class AlertRenderer(Widget): # 1. Never received selfdriveState since going onroad waiting_for_startup = recv_frame < ui_state.started_frame - if waiting_for_startup and time_since_onroad > 5: + if waiting_for_startup and time_since_onroad > 10: return ALERT_STARTUP_PENDING # 2. Lost communication with selfdriveState after receiving it From 351701689f4cc2fc7e1430fde4ee33e42f345007 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:22:04 -0400 Subject: [PATCH 251/325] bump teleop (#38645) --- teleoprtc_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teleoprtc_repo b/teleoprtc_repo index 31db236a9e..1aa8fc433b 160000 --- a/teleoprtc_repo +++ b/teleoprtc_repo @@ -1 +1 @@ -Subproject commit 31db236a9ef820d7051ccd53488153cfbc84d3b9 +Subproject commit 1aa8fc433bef1519a95c0700c96258c3be6dfb34 From 047be14df9d90e316b4d63d0575b6ad5da6551c6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 15:25:19 -0700 Subject: [PATCH 252/325] release: speedup builds (#38644) --- .../lib/longitudinal_mpc_lib/SConscript | 1 + tools/release/build_release.sh | 38 ++++++++----------- tools/release/build_stripped.sh | 13 ++----- tools/release/release_files.py | 12 +++--- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 636ef0fb21..fa249765bc 100644 --- a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -35,6 +35,7 @@ build_files = [f'{gen}/acados_solver_long.c'] + casadi_model + casadi_cost_y + c # extra generated files used to trigger a rebuild generated_files = [ + 'acados_ocp_long.json', f'{gen}/Makefile', f'{gen}/main_long.c', diff --git a/tools/release/build_release.sh b/tools/release/build_release.sh index 5dca1626ca..4bc5dd2e68 100755 --- a/tools/release/build_release.sh +++ b/tools/release/build_release.sh @@ -2,15 +2,14 @@ set -e set -x -# git diff --name-status origin/release3-staging | grep "^A" | less - DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" - cd $DIR BUILD_DIR=/data/openpilot SOURCE_DIR="$(git rev-parse --show-toplevel)" +export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" + if [ -z "$RELEASE_BRANCH" ]; then echo "RELEASE_BRANCH is not set" exit 1 @@ -23,29 +22,24 @@ BUILD_BRANCH=release-mici-staging source $DIR/identity.sh echo "[-] Setting up repo T=$SECONDS" -rm -rf $BUILD_DIR -mkdir -p $BUILD_DIR +if ! git -C "$SOURCE_DIR" worktree remove --force "$BUILD_DIR" 2>/dev/null; then + rm -rf $BUILD_DIR +fi +git -C "$SOURCE_DIR" worktree prune +git -C "$SOURCE_DIR" worktree add --detach --no-checkout "$BUILD_DIR" cd $BUILD_DIR -git init -git remote add origin git@github.com:commaai/openpilot.git -git checkout --orphan $BUILD_BRANCH +git update-ref -d "refs/heads/$BUILD_BRANCH" +git symbolic-ref HEAD "refs/heads/$BUILD_BRANCH" +git read-tree --empty # do the files copy echo "[-] copying files T=$SECONDS" cd $SOURCE_DIR -cp -pR --parents $(./tools/release/release_files.py) $BUILD_DIR/ +./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" -- # in the directory cd $BUILD_DIR -rm -f panda/board/obj/panda.bin.signed -rm -f panda/board/obj/panda_h7.bin.signed - -VERSION=$(cat openpilot/common/version.h | awk -F[\"-] '{print $2}') -echo "[-] committing version $VERSION T=$SECONDS" -git add -f . -git commit -a -m "openpilot v$VERSION release" - # use the full CPU available for speeding up the build. # openpilot resets the CPU frequencies when test_onroad.py runs below. for policy in /sys/devices/system/cpu/cpufreq/policy*; do @@ -54,9 +48,6 @@ for policy in /sys/devices/system/cpu/cpufreq/policy*; do echo "$hardware_max" | sudo tee "$policy/scaling_max_freq" >/dev/null done -# Build and test before launch_chffrplus.sh creates the on-device package -# symlinks. SConstruct uses the same package roots for build subprocesses. -export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" scons if [ -n "$INCLUDE_BIG_MODEL" ]; then test -f openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest @@ -83,7 +74,6 @@ find . -name '*.a' -delete find . -name '*.o' -delete find . -name '*.os' -delete find . -name '*.pyc' -delete -find . -name 'moc_*' -delete find . -name '__pycache__' -delete rm -rf .sconsign.dblite Jenkinsfile tools/release/ rm -f openpilot/selfdrive/modeld/models/*.onnx* @@ -91,9 +81,11 @@ rm -f openpilot/selfdrive/modeld/models/*.onnx* # Mark as prebuilt release touch prebuilt +VERSION=$(cat openpilot/common/version.h | awk -F[\"-] '{print $2}') # Add built files to git -git add -f . -git commit --amend -m "openpilot v$VERSION" +# writing larger objects is faster than compressing them on-device +git -c core.compression=0 add -f . +git -c core.compression=0 -c gc.auto=0 commit -m "openpilot v$VERSION" # Run tests cd $BUILD_DIR diff --git a/tools/release/build_stripped.sh b/tools/release/build_stripped.sh index 6c1ba4e097..ba4c847375 100755 --- a/tools/release/build_stripped.sh +++ b/tools/release/build_stripped.sh @@ -30,20 +30,14 @@ git submodule deinit -f --all git rm -rf --cached . find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \; -# cleanup before the copy -cd $SOURCE_DIR -git clean -xdff -git submodule foreach --recursive git clean -xdff - # do the files copy echo "[-] copying files T=$SECONDS" cd $SOURCE_DIR -./tools/release/release_files.py | xargs -d '\n' cp -pR --parents -t "$TARGET_DIR" +./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$TARGET_DIR" -- # in the directory cd $TARGET_DIR rm -rf .git/modules/ -rm -f panda/board/obj/panda.bin.signed find openpilot/selfdrive/modeld/models -name '*.onnx' -size +95M -exec ./openpilot/common/file_chunker.py {} \; @@ -57,9 +51,10 @@ echo -n "$GIT_HASH" > git_src_commit echo -n "$GIT_COMMIT_DATE" > git_src_commit_date echo "[-] committing version $VERSION T=$SECONDS" -git add -f . +# writing larger objects is faster than compressing them on-device +git -c core.compression=0 add -f . git status -git commit -a -m "openpilot v$VERSION release +git -c core.compression=0 commit -a -m "openpilot v$VERSION release date: $DATETIME master commit: $GIT_HASH diff --git a/tools/release/release_files.py b/tools/release/release_files.py index 1558f04a26..dd42125337 100755 --- a/tools/release/release_files.py +++ b/tools/release/release_files.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import os import re -from pathlib import Path +import subprocess +import sys HERE = os.path.abspath(os.path.dirname(__file__)) ROOT = os.path.abspath(os.path.join(HERE, "../..")) @@ -25,11 +26,12 @@ whitelist: list[str] = [ ] if __name__ == "__main__": - for f in Path(ROOT).rglob("**/*"): - if not (f.is_file() or f.is_symlink()): + tracked_files = subprocess.check_output(["git", "ls-files", "-z", "--recurse-submodules"], cwd=ROOT).split(b"\0") + for tracked_file in tracked_files: + if not tracked_file: continue - rf = str(f.relative_to(ROOT)) + rf = os.fsdecode(tracked_file) if not os.getenv("INCLUDE_BIG_MODEL") and rf.startswith("openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx"): continue blacklisted = any(re.search(p, rf) for p in blacklist) @@ -37,4 +39,4 @@ if __name__ == "__main__": if blacklisted and not whitelisted: continue - print(rf) + sys.stdout.buffer.write(tracked_file + b"\0") From 03e6c81821eddfdb80d3ed29e86e950d9cd0f296 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 16:10:35 -0700 Subject: [PATCH 253/325] test_onroad: more precise frame ID check (#38647) --- openpilot/selfdrive/test/test_onroad.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/test/test_onroad.py b/openpilot/selfdrive/test/test_onroad.py index 4426c196d4..f524046abe 100755 --- a/openpilot/selfdrive/test/test_onroad.py +++ b/openpilot/selfdrive/test/test_onroad.py @@ -335,13 +335,14 @@ class TestOnroad(OpenpilotTestCase): assert np.all(eof_sof_diff > 0) assert np.all(eof_sof_diff < 50*1e6) - # TODO: loggerd doesn't start fast enough to be ready before the first frames come out first_fid = {min(self.ts[c]['frameId']) for c in cams} - #assert len(first_fid) == 1, "Cameras don't start on same frame ID" if cams[0].endswith('CameraState'): # camerad guarantees that all cams start on frame ID 0 # (note loggerd also needs to start up fast enough to catch it) assert min(first_fid) < 100, "Cameras start on frame ID too high" + else: + # encoderd synchronizes all camera encoders to the same starting frame + assert len(first_fid) == 1, "Camera encoders don't start on same frame ID" # we don't do a full segment rotation, so these might not match exactly last_fid = {max(self.ts[c]['frameId']) for c in cams} From dfbe0ee7c14973942fd4e463d5d8632ad58309a4 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 18:23:37 -0700 Subject: [PATCH 254/325] jenkins: set big cache dir (#38648) --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index db18d13ddd..af33edb821 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -22,6 +22,7 @@ shopt -s huponexit # kill all child processes when the shell exits export CI=1 export PYTHONWARNINGS=error +export COMMA_CACHE=/data/tmp/comma_download_cache #export LOGPRINT=debug # this has gotten too spammy... export TEST_DIR=${env.TEST_DIR} export SOURCE_DIR=${env.SOURCE_DIR} From 85d364d4de7db5235c23affaccd7211b1aa42bab Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 16 Aug 2026 18:24:01 -0700 Subject: [PATCH 255/325] loggerd: fix ~0.5s startup logging delay (#38649) --- openpilot/common/hardware/base.h | 2 +- openpilot/common/hardware/comma/hardware.h | 16 +++++++++------- openpilot/system/loggerd/logger.cc | 6 +++--- openpilot/system/loggerd/logger.h | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/openpilot/common/hardware/base.h b/openpilot/common/hardware/base.h index f4546adfa8..53db48ff5b 100644 --- a/openpilot/common/hardware/base.h +++ b/openpilot/common/hardware/base.h @@ -15,7 +15,7 @@ public: static std::string get_serial() { return "cccccc"; } - static std::map get_init_logs() { + static std::map get_init_logs(bool route_log = false) { return {}; } diff --git a/openpilot/common/hardware/comma/hardware.h b/openpilot/common/hardware/comma/hardware.h index 6292183d9d..7bb9074f6b 100644 --- a/openpilot/common/hardware/comma/hardware.h +++ b/openpilot/common/hardware/comma/hardware.h @@ -59,7 +59,7 @@ public: std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n"; } - static std::map get_init_logs() { + static std::map get_init_logs(bool route_log = false) { std::map ret = { {"/BUILD", util::read_file("/BUILD")}, {"lsblk", util::check_output("lsblk -o NAME,SIZE,STATE,VENDOR,MODEL,REV,SERIAL")}, @@ -73,12 +73,14 @@ public: temp.erase(temp.find_last_not_of(std::string("\0\r\n", 3))+1); ret["boot temp"] = temp; - // TODO: log something from system and boot - for (std::string part : {"xbl", "abl", "aop", "devcfg", "xbl_config"}) { - for (std::string slot : {"a", "b"}) { - std::string partition = part + "_" + slot; - std::string hash = util::check_output("sha256sum /dev/disk/by-partlabel/" + partition); - ret[partition] = hash.substr(0, hash.find_first_of(" ")); + // TODO: these are too slow to do on route log inits. need to do it async? + if (!route_log) { + for (std::string part : {"xbl", "abl", "aop", "devcfg", "xbl_config"}) { + for (std::string slot : {"a", "b"}) { + std::string partition = part + "_" + slot; + std::string hash = util::check_output("sha256sum /dev/disk/by-partlabel/" + partition); + ret[partition] = hash.substr(0, hash.find_first_of(" ")); + } } } diff --git a/openpilot/system/loggerd/logger.cc b/openpilot/system/loggerd/logger.cc index 0ebe323939..f553760016 100644 --- a/openpilot/system/loggerd/logger.cc +++ b/openpilot/system/loggerd/logger.cc @@ -12,7 +12,7 @@ #include "common/version.h" // ***** log metadata ***** -kj::Array logger_build_init_data() { +kj::Array logger_build_init_data(bool route_log) { uint64_t wall_time = nanos_since_epoch(); MessageBuilder msg; @@ -70,7 +70,7 @@ kj::Array logger_build_init_data() { "df -h", // usage for all filesystems }; - auto hw_logs = Hardware::get_init_logs(); + auto hw_logs = Hardware::get_init_logs(route_log); auto commands = init.initCommands().initEntries(log_commands.size() + hw_logs.size()); for (int i = 0; i < log_commands.size(); i++) { @@ -164,7 +164,7 @@ static void log_sentinel(LoggerState *log, SentinelType type, int exit_signal = LoggerState::LoggerState(const std::string &log_root) { route_name = logger_get_identifier("RouteCount"); route_path = log_root + "/" + route_name; - init_data = logger_build_init_data(); + init_data = logger_build_init_data(true); } LoggerState::~LoggerState() { diff --git a/openpilot/system/loggerd/logger.h b/openpilot/system/loggerd/logger.h index 419becfe5d..17c29d1a02 100644 --- a/openpilot/system/loggerd/logger.h +++ b/openpilot/system/loggerd/logger.h @@ -32,6 +32,6 @@ protected: std::unique_ptr rlog, qlog; }; -kj::Array logger_build_init_data(); +kj::Array logger_build_init_data(bool route_log = false); std::string logger_get_identifier(std::string key); std::string zstd_decompress(const std::string &in); From 053d9c446800df38aca42b69bb99197aefbea77b Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:38:12 -0700 Subject: [PATCH 256/325] [bot] Update Python packages (#38650) * Update Python packages * revert that for now * ignore dashcam only --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- docs/CARS.md | 18 +-- opendbc_repo | 2 +- .../test/process_replay/test_processes.py | 5 +- tinygrad_repo | 2 +- uv.lock | 139 ++++++++++-------- 5 files changed, 93 insertions(+), 73 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 25bb8386dc..1e0bd07b77 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -158,7 +158,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai N connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 Hyundai E connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 FCA connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| -|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 FCA connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 FCA connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai A connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai K connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| |Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 Hyundai E connector
    - 1 OBD-C cable (2 ft)
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    ||| @@ -237,16 +237,16 @@ A supported vehicle is one that just works when you install a comma device. All |Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Rivian A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |SEAT[12](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| |SEAT[12](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| -|Subaru|Ascent 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|Forester 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Ascent 2019-21|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Forester 2019-21|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| |Subaru|Legacy 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru B connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| |Subaru|Outback 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru B connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|XV 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| -|Subaru|XV 2020-21|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|XV 2018-19|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| +|Subaru|XV 2020-21|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 Subaru A connector
    - 1 comma four
    - 1 comma power v3
    - 1 harness box
    - 1 mount
    Buy Here
    Tools- 1 Pry Tool
    - 1 Socket Wrench 8mm or 5/16" (deep)
    ||| |Škoda|Fabia 2022-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    [17](#footnotes)||| |Škoda|Kamiq 2021-23[13,15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    [17](#footnotes)||| |Škoda[12](#footnotes)|Karoq 2019-23[15](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
    Parts- 1 OBD-C cable (2 ft)
    - 1 VW J533 connector
    - 1 comma four
    - 1 harness box
    - 1 long OBD-C cable (9.5 ft)
    - 1 mount
    Buy Here
    ||| diff --git a/opendbc_repo b/opendbc_repo index c536b211b7..b4ef5e1cf4 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit c536b211b762c37c6d869923ae8ba59ca2f18a0c +Subproject commit b4ef5e1cf406ff143fa67bdbfb154739d43279c9 diff --git a/openpilot/selfdrive/test/process_replay/test_processes.py b/openpilot/selfdrive/test/process_replay/test_processes.py index d9d827add5..9447ecae19 100755 --- a/openpilot/selfdrive/test/process_replay/test_processes.py +++ b/openpilot/selfdrive/test/process_replay/test_processes.py @@ -7,7 +7,7 @@ import traceback from collections import defaultdict from tqdm import tqdm from typing import Any -from opendbc.car.car_helpers import interface_names +from opendbc.car.car_helpers import interface_names, interfaces from openpilot.common.git import get_commit from openpilot.tools.lib.openpilotci import get_url from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs, format_diff @@ -64,7 +64,8 @@ segments = [ ] # dashcamOnly makes don't need to be tested until a full port is done -excluded_interfaces = ["mock", "body", "psa"] +excluded_interfaces = {brand for brand, platforms in interface_names.items() + if all(interfaces[platform].get_non_essential_params(platform).dashcamOnly for platform in platforms)} | {"body"} BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/" REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") diff --git a/tinygrad_repo b/tinygrad_repo index 8611fe22a7..138fb4a783 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 8611fe22a7fcc7d1928bbde19ded66277cb12f3e +Subproject commit 138fb4a783d82f4e877ad2fe3692aaf8d1de2e46 diff --git a/uv.lock b/uv.lock index 5717d0a38d..b9af0d6adf 100644 --- a/uv.lock +++ b/uv.lock @@ -39,24 +39,43 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.9" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -879,49 +898,49 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.2" +version = "0.16.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, - { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, - { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, - { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, - { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, - { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, - { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, ] [[package]] name = "scons" -version = "4.10.1" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/82/3c4e089ac8df2eaee8a7f14e489b2a76f94f4c1d8defa4e46c8ad15cae86/scons-4.11.0.tar.gz", hash = "sha256:5ba48f9e2eb6b9178cabdc9893792418e6970c84f43f4b027e4468e20616a89c", size = 3269126, upload-time = "2026-08-11T04:29:45.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ac/a4445bbbd58a5fa6a5c8b3b0458ffbee04e4acaff87677058eab9c6af682/scons-4.11.0-py3-none-any.whl", hash = "sha256:2edc077aaeafc43377ba46ce1fa3e7b40edea59c62db9ef7e39e07dc88b754fa", size = 4123742, upload-time = "2026-08-11T04:29:42.881Z" }, ] [[package]] name = "sentry-sdk" -version = "2.67.1" +version = "2.68.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/8a/b2eec40df8a67bf073e244d29001d04ee365d163bc4f15efdfce35f53090/sentry_sdk-2.67.1.tar.gz", hash = "sha256:f263d8c9aa4137750640de8fb0ed5404df6bb564e20e4b59cb16a6eeba18d4ed", size = 990599, upload-time = "2026-08-10T13:05:55.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/94/23b7dd072acb9628907bd3f4fbf61794a7b12a9db8f33c1276f70ae5ac92/sentry_sdk-2.68.0.tar.gz", hash = "sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3", size = 1008854, upload-time = "2026-08-13T09:06:21.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/e2/70692eba662037cddf93391cbbf98297159f3038612e9b9a8129e16feb7a/sentry_sdk-2.67.1-py3-none-any.whl", hash = "sha256:a66bfbce1cd8a93c51c369d642ad85b46253ea7a6f7938141315b83e2823cda5", size = 515591, upload-time = "2026-08-10T13:05:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9b/e2421d08956d0bc4691d995393d835e563886bff499d8fb10fdefae85a8d/sentry_sdk-2.68.0-py3-none-any.whl", hash = "sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4", size = 518670, upload-time = "2026-08-13T09:06:19.735Z" }, ] [[package]] @@ -1085,27 +1104,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.69" +version = "0.0.72" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5b/7a618632dfe9373b7df572ecd7a08c8f799d772fbc317da82dd3aa363207/ty-0.0.69.tar.gz", hash = "sha256:b65106e9ff24fa76e25e1142fb09c85244e815c40450e3021d2bf652c231bb43", size = 6565094, upload-time = "2026-08-06T10:04:25.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/60/6534092f4d2c15e2491807edd609c2e50d527c1fed957acf40b9f110b64a/ty-0.0.69-py3-none-linux_armv6l.whl", hash = "sha256:98bfd383b273540829af673e7f98b9c1c4bcc8547d12a1a3806cd0bec7f0e087", size = 12364185, upload-time = "2026-08-06T10:03:47.137Z" }, - { url = "https://files.pythonhosted.org/packages/34/2b/5c29689bd4f74c2e3394d983d85e4011b629f2ce3730c9442553b8554bf8/ty-0.0.69-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:964621ddd05771660017c51b4e74078d861d9fc863c21ef2a500db1ab62c9ccf", size = 12042510, upload-time = "2026-08-06T10:03:49.481Z" }, - { url = "https://files.pythonhosted.org/packages/09/46/fa085bde4d23516d7ef14b24736fc5dd7dc498f60f52b3d077e59ffdea20/ty-0.0.69-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3ffea4048dd0da4c9c97393b4be0901098a9065b06fa81be2477cbde65d8a151", size = 11549397, upload-time = "2026-08-06T10:03:51.747Z" }, - { url = "https://files.pythonhosted.org/packages/25/cc/97b9efb2061dcab6fef1e94a4ad99df0bb45bd2cc15d4f5794c787ee0552/ty-0.0.69-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8684d4a70aadd1eab0f41bdba835e3288ef49db8402a8e6ca81bab52ed5d610", size = 12115567, upload-time = "2026-08-06T10:03:53.79Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c1/a5e0404965093835f3e62544e661784ec0aa8ef0b006ed50af50b19c107e/ty-0.0.69-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afaaba240ab4122e2069a796836d10be81b4ddb053ae268b3dff962a0b4ca5c7", size = 12149770, upload-time = "2026-08-06T10:03:55.993Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8cad6b205a4abe8a044ca0c84aea71e8ccda29b07a75a5f090e310605580/ty-0.0.69-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ea63ef07d4e33aeb1a775cf5f2c736b3ed22fa6f8b1b608591612c36795044", size = 12941278, upload-time = "2026-08-06T10:03:58.324Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/8766d96b732c2a060d70dc8ccafcc4d6a54109a2a95f1deb0705de88892b/ty-0.0.69-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb3730b1268e92a2907d7aea3afe8dd1b360ae65862f0557080cf479d481b424", size = 13426509, upload-time = "2026-08-06T10:04:00.621Z" }, - { url = "https://files.pythonhosted.org/packages/02/1f/e991b2cde953ea5b94d6a9a4c45c87937bd916bc09235f764407bf471c0a/ty-0.0.69-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a544ff57a752ef186ed40b5a2f44c17402af4cdefeb74a311ca02ebd57c4fca0", size = 13106582, upload-time = "2026-08-06T10:04:02.818Z" }, - { url = "https://files.pythonhosted.org/packages/ea/bb/73538f1b99e3558fd9db87b98698426f0f60fc8666da0b1efd0e70e275eb/ty-0.0.69-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87ed2cbca20caddfdf8e3e14d213ce91b67e75feed78900f4aaf3ef884954028", size = 12708931, upload-time = "2026-08-06T10:04:05.233Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/484a5208d74c4ad1155933906295ccdce9aa81a257d8df2ab9e41bd60133/ty-0.0.69-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2684efcbce5b6fe45045faf610b377b50781b6d2aa7e61ea23ecf5b3d2bce421", size = 12985322, upload-time = "2026-08-06T10:04:07.587Z" }, - { url = "https://files.pythonhosted.org/packages/6e/81/b75003f0d4da9ab3bc8fd4f4802f836cb9921ff7e70f460604f7b769a0b5/ty-0.0.69-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da9aeb26fdac1d2214937542b59e0d4d1ba94ec7a3f45444f33c846de1eb1d63", size = 12063910, upload-time = "2026-08-06T10:04:09.835Z" }, - { url = "https://files.pythonhosted.org/packages/8a/76/088469f547ef63dceefc4a75826aedee5014f9371dc5171cde931896a82c/ty-0.0.69-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:00e7677cd14ede381f705f71104ea7b8ea0ce217a8634e19a89781953de0e9ad", size = 12166823, upload-time = "2026-08-06T10:04:12.114Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c9/ce88a0bec0d46d8ae180b99c6ec014866fecc4cba1727b5feec8877b2765/ty-0.0.69-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d91965eb799649833d0d6042db09cd03d15289125245337cc46a2606effb7bda", size = 12483136, upload-time = "2026-08-06T10:04:14.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/9e/6fae0ff225a0012642cf72c077e20f8f448c0a80771bc3360e8178fe2f32/ty-0.0.69-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f03359cd8e5c412aa0c181118fa9b9061a4dddaedbb61bac0a424fb0814d402", size = 12799025, upload-time = "2026-08-06T10:04:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/e4/43/78a658d18b2a4ccf35b053392f2213bf12e3c63b2abea512d3b6751d1f4c/ty-0.0.69-py3-none-win32.whl", hash = "sha256:ec460e01586b1eb91894c4a8403bee3e045a47e7a4ada943cc27ce8e348e88cf", size = 11787774, upload-time = "2026-08-06T10:04:18.622Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5e/88db1f674403f2b81316a853a44a81ed220621fa96f8f7ae586fb6ca7513/ty-0.0.69-py3-none-win_amd64.whl", hash = "sha256:18976ca26a4e28fc3249477f79a695d5502e670803f2e080d89ac905baef3c6e", size = 12864038, upload-time = "2026-08-06T10:04:20.748Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7b/6fc6efd00c69103d70f2bdbe824343089cd70b17b3079170057d3e5a3ac0/ty-0.0.69-py3-none-win_arm64.whl", hash = "sha256:7d4ca3bb74d91cb9947ba3f3b4cb131ad6a2b3ecc76d34040c4ec6092d2e411d", size = 12196693, upload-time = "2026-08-06T10:04:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" }, + { url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" }, + { url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" }, + { url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" }, ] [[package]] From 94a32493e3fd9552a42747ea4337fb151a555bdb Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sun, 16 Aug 2026 18:40:16 -0700 Subject: [PATCH 257/325] modeld_v2: chestnut support (#1894) * modeld_v2: Support eGpu * bump tg * egpu * no pkls please * god no onnx either * fix test * done in build model now * lint * rip * egpu ready build all split * manual seed , reuse memory buffers across runs * dont download big when we dont have big lol * whoops * no i and x * cd * who put those there. ?? * reduce flakiness by using artifact-name from build-model to regex, speed up pub b y checking the name before trying to clone and publish again * try hf as a trusted publisher :) * mf its a dataaset. i knew that * fucking validation wants raw to fetch and full to push. grr * smh * dude i am missing so much * pkl name * move build all to hf * tests: migrate sunnypilot tests to unittest and remove pytest * red diff mf * im scared , this may be a bad idea lol * fetch latest commit. * transition to requests * models: use requests instead of aiohttp * tici * fix * gpu fixes from upstream * lint * bump * needed to say * old * how?? * epgu flag reduce * this made me cry * support monolith still * precache warp in legacy * Move jsons to param for sunnylink --------- Co-authored-by: Jason Wen --- .../workflows/build-all-tinygrad-models.yaml | 228 +++------------- .../build-single-tinygrad-model.yaml | 143 +++++----- .github/workflows/sunnypilot-build-model.yaml | 38 +-- openpilot/common/params_keys.h | 3 + openpilot/sunnypilot/SConscript | 1 - openpilot/sunnypilot/modeld_v2/SConscript | 84 ------ .../sunnypilot/modeld_v2/compile_modeld.py | 253 ++++++++++-------- openpilot/sunnypilot/modeld_v2/modeld.py | 153 +++++++---- .../sunnypilot/modeld_v2/tests/helpers.py | 7 +- .../modeld_v2/tests/test_recovery_power.py | 2 +- openpilot/sunnypilot/models/fetcher.py | 34 ++- openpilot/sunnypilot/models/helpers.py | 2 +- .../models/tests/test_tinygrad_ref.py | 5 +- release/ci/model_generator.py | 10 +- tinygrad_repo | 2 +- 15 files changed, 411 insertions(+), 554 deletions(-) delete mode 100644 openpilot/sunnypilot/modeld_v2/SConscript diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 0eedb04703..1ba407b3cf 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -7,6 +7,19 @@ on: description: 'Minimum selector version required for the models (see helpers.py or readme.md)' required: true type: string + target_hardware: + description: 'Hardware target to compile for (qcom or usbgpu)' + required: true + type: choice + default: 'qcom' + options: + - qcom + - usbgpu + hf_repo: + description: 'Hugging Face dataset repository' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' jobs: setup: @@ -46,13 +59,14 @@ jobs: id: get-json run: | cd docs/docs - latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1) + PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v" + latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1) next=$((latest+1)) - json_file="driving_models_v${next}.json" - cp "driving_models_v${latest}.json" "$json_file" + json_file="${PREFIX}${next}.json" + cp "${PREFIX}${latest}.json" "$json_file" echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT echo "json_version=$((next+0))" >> $GITHUB_OUTPUT - echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV + echo "SRC_JSON_FILE=docs/docs/${PREFIX}${latest}.json" >> $GITHUB_ENV - name: Extract tinygrad models id: set-matrix @@ -61,45 +75,23 @@ jobs: jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT - - name: Set up SSH - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - - run: | - mkdir -p ~/.ssh - ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - - - name: Clone GitLab docs repo and create new recompiled dir + - name: Get next recompiled dir number id: create-recompiled-dir env: - GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + HF_REPO: ${{ github.event.inputs.hf_repo }} run: | - git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs - cd gitlab_docs - git checkout main - git sparse-checkout set --no-cone models/ - cd models - latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1) - if [[ -z "$latest_dir" ]]; then - next_dir=1 - else - next_dir=$((latest_dir+1)) - fi - recompiled_dir="${next_dir}" - mkdir -p "recompiled${recompiled_dir}" - touch "recompiled${recompiled_dir}/.gitkeep" - cd ../.. + pip install huggingface_hub + recompiled_dir=$(python3 -c " + from huggingface_hub import HfApi + import re, sys + api = HfApi() + files = api.list_repo_files(repo_id=sys.argv[1], repo_type='dataset') + dirs = [re.search(r'models/recompiled([0-9]+)', f) for f in files] + nums = [int(m.group(1)) for m in dirs if m] + print(max(nums) + 1) + " "$HF_REPO") echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT - - name: Push empty recompiled dir to GitLab - run: | - cd gitlab_docs - git add models/recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} - git config --global user.name "GitHub Action" - git config --global user.email "action@github.com" - git commit -m "Add recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} for build-all" || echo "No changes to commit" - git push origin main - - name: Push new JSON to GitHub docs repo run: | cd docs @@ -123,25 +115,30 @@ jobs: is_20hz: ${{ matrix.model.is_20hz }} recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} json_version: ${{ needs.setup.outputs.json_version }} + target_hardware: ${{ github.event.inputs.target_hardware }} + hf_repo: ${{ github.event.inputs.hf_repo }} + set_min_version: ${{ github.event.inputs.set_min_version }} + tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} secrets: inherit retry_failed_models: needs: [setup, get_and_build] runs-on: ubuntu-latest - if: ${{ needs.setup.result != 'failure' && !cancelled() }} + if: ${{ !cancelled() && needs.setup.result == 'success' && (needs.get_and_build.result == 'success' || needs.get_and_build.result == 'failure') }} outputs: retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }} steps: - uses: actions/download-artifact@v4 with: - pattern: model-* + pattern: artifact-name-* path: output + continue-on-error: true - id: set-retry-matrix run: | echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json - built=(); while IFS= read -r line; do built+=("$line"); done < <( - find output -maxdepth 1 -name 'model-*' -printf "%f\n" | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' + built=(); while IFS= read -r line; do [ -n "$line" ] && built+=("$line"); done < <( + find output -maxdepth 1 -name 'artifact-name-*' -printf "%f\n" 2>/dev/null | sed -E 's/^artifact-name-//' | awk '{gsub(/^ +| +$/, ""); print}' ) jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json @@ -149,7 +146,7 @@ jobs: retry_get_and_build: needs: [setup, get_and_build, retry_failed_models] - if: ${{ needs.get_and_build.result == 'failure' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '') }} + if: ${{ !cancelled() && needs.retry_failed_models.result == 'success' && needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '' }} strategy: matrix: model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }} @@ -161,146 +158,9 @@ jobs: is_20hz: ${{ matrix.model.is_20hz }} recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} json_version: ${{ needs.setup.outputs.json_version }} + target_hardware: ${{ github.event.inputs.target_hardware }} artifact_suffix: -retry + hf_repo: ${{ github.event.inputs.hf_repo }} + set_min_version: ${{ github.event.inputs.set_min_version }} + tinygrad_ref: ${{ needs.setup.outputs.tinygrad_ref }} secrets: inherit - - publish_models: - name: Publish models sequentially - needs: [setup, get_and_build, retry_failed_models, retry_get_and_build] - if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - max-parallel: 1 - matrix: - model: ${{ fromJson(needs.setup.outputs.model_matrix) }} - env: - RECOMPILED_DIR: recompiled${{ needs.setup.outputs.recompiled_dir }} - JSON_FILE: ${{ needs.setup.outputs.json_file }} - ARTIFACT_NAME_INPUT: ${{ matrix.model.display_name }} - steps: - - name: Set up SSH - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - - - name: Add GitLab.com SSH key to known_hosts - run: | - mkdir -p ~/.ssh - ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - - - name: Clone GitLab docs repo - env: - GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' - run: | - echo "Cloning GitLab" - git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs - cd gitlab_docs - echo "checkout models/${RECOMPILED_DIR}" - git sparse-checkout set --no-cone models/${RECOMPILED_DIR} - git checkout main - cd .. - - - name: Checkout docs repo - uses: actions/checkout@v4 - with: - repository: sunnypilot/sunnypilot-models - ref: gh-pages - path: docs - ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} - - - name: Validate recompiled dir and JSON version - run: | - if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then - echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" - exit 1 - fi - if [ ! -f "$JSON_FILE" ]; then - echo "JSON file $JSON_FILE does not exist!" - exit 1 - fi - - - name: Download artifact name file - uses: actions/download-artifact@v4 - with: - name: artifact-name-${{ env.ARTIFACT_NAME_INPUT }} - path: artifact_name - - - name: Read artifact name - id: read-artifact-name - run: | - ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) - echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT - - - name: Download model artifact - uses: actions/download-artifact@v4 - with: - name: ${{ steps.read-artifact-name.outputs.artifact_name }} - path: output - - - name: Remove onnx files bc not needed for recompiled dir since they already exist from single build - run: | - find output -type f -name '*.onnx' -delete - find output -type f -name 'big_*.pkl' -delete - find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete - - - name: Copy model artifacts to gitlab - env: - ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} - run: | - ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" - mkdir -p "$ARTIFACT_DIR" - for path in output/*; do - if [ "$(basename "$path")" = "artifact_name.txt" ]; then - continue - fi - name="$(basename "$path")" - if [ -d "$path" ]; then - mkdir -p "$ARTIFACT_DIR/$name" - cp -r "$path"/* "$ARTIFACT_DIR/$name/" - echo "Copied dir $name -> $ARTIFACT_DIR/$name" - else - cp "$path" "$ARTIFACT_DIR/" - echo "Copied file $name -> $ARTIFACT_DIR/" - fi - done - - - name: Push recompiled dir to GitLab - env: - GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - run: | - cd gitlab_docs - git checkout main - git pull origin main - for d in models/"$RECOMPILED_DIR"/*/; do - git sparse-checkout add "$d" - done - git add models/"$RECOMPILED_DIR" - git config --global user.name "GitHub Action" - git config --global user.email "action@github.com" - git commit -m "Update $RECOMPILED_DIR with model from build-all-tinygrad-models" || echo "No changes to commit" - git push origin main - - run: | - cd docs - git pull origin gh-pages - - - name: update json - run: | - ARGS="" - [ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\"" - ARGS="$ARGS --sort-by-date" - ARGS="$ARGS --tinygrad-ref \"${{ needs.setup.outputs.tinygrad_ref }}\"" - eval python3 docs/json_parser.py \ - --json-path "$JSON_FILE" \ - --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ - $ARGS - - - name: Push updated json to GitHub - run: | - cd docs - git config --global user.name "GitHub Action" - git config --global user.email "action@github.com" - git checkout gh-pages - git add docs/"$(basename $JSON_FILE)" - git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" - git push origin gh-pages diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml index cf2d870802..1ad06a54e4 100644 --- a/.github/workflows/build-single-tinygrad-model.yaml +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -29,11 +29,24 @@ on: required: false type: boolean default: true - bypass_push: - description: 'Bypass pushing to GitLab for build-all' + target_hardware: + description: 'Hardware target to compile for (qcom or usbgpu)' required: false - default: true - type: boolean + type: string + default: 'qcom' + hf_repo: + description: 'Hugging Face dataset repository (e.g. sunnypilot/sunnypilot_models_v1)' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' + set_min_version: + description: 'Minimum selector version' + required: false + type: string + tinygrad_ref: + description: 'Tinygrad reference' + required: false + type: string workflow_dispatch: inputs: upstream_branch: @@ -65,8 +78,8 @@ on: - None - Master Models - Release Models - - 2025 World Models - 2026 World Models + - 2026 Deep RL Models - Custom Merge Models - Other custom_model_folder: @@ -81,9 +94,22 @@ on: description: 'Minimum selector version' required: false type: string + target_hardware: + description: 'Hardware target to compile for' + required: false + type: choice + default: 'qcom' + options: + - qcom + - usbgpu + hf_repo: + description: 'Hugging Face dataset repository' + required: false + type: string + default: 'sunnypilot/sunnypilot_models_v1' env: RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} - JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json + JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json jobs: build_model: @@ -93,38 +119,20 @@ jobs: custom_name: ${{ inputs.custom_name || inputs.upstream_branch }} is_20hz: ${{ inputs.is_20hz }} artifact_suffix: ${{ inputs.artifact_suffix }} + target_hardware: ${{ inputs.target_hardware }} secrets: inherit publish_model: - if: ${{ !inputs.bypass_push && !cancelled() }} + if: ${{ !cancelled() && needs.build_model.result == 'success' }} concurrency: - group: gitlab-push-${{ inputs.recompiled_dir }} + group: hf-push-${{ inputs.recompiled_dir }} cancel-in-progress: false needs: build_model runs-on: ubuntu-latest + permissions: + id-token: write + contents: write steps: - - name: Set up SSH - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - - - name: Add GitLab.com SSH key to known_hosts - run: | - mkdir -p ~/.ssh - ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - - - name: Clone GitLab docs repo - env: - GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' - run: | - echo "Cloning GitLab" - git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/${{ vars.MODELS_GITLAB }} gitlab_docs - cd gitlab_docs - echo "checkout models/${RECOMPILED_DIR}" - git sparse-checkout set --no-cone models/${RECOMPILED_DIR} - git checkout main - cd .. - - name: Checkout docs repo uses: actions/checkout@v4 with: @@ -133,16 +141,28 @@ jobs: path: docs ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} - - name: Validate recompiled dir and JSON version + - name: Install huggingface_hub + run: pip install --upgrade "huggingface_hub>=0.22.0" + + - name: Validate hf_repo and JSON version + env: + HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} run: | - if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then - echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" - exit 1 - fi if [ ! -f "$JSON_FILE" ]; then echo "JSON file $JSON_FILE does not exist!" exit 1 fi + python3 -c " + import sys + from huggingface_hub import HfApi + try: + api = HfApi() + api.repo_info(repo_id=sys.argv[1], repo_type='dataset') + print(f'Success: Repo {sys.argv[1]} exists.') + except Exception as e: + print('HF validation failed:', e) + sys.exit(1) + " "${{ inputs.hf_repo }}" - name: Download artifact name file uses: actions/download-artifact@v4 @@ -162,49 +182,26 @@ jobs: name: ${{ steps.read-artifact-name.outputs.artifact_name }} path: output - - name: Remove unwanted files - run: | - find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete - find output -type f -name 'dmonitoring_model.onnx' -delete - - - name: Copy model artifact(s) to GitLab recompiled dir + - name: Create models folder env: ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} run: | - ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" - mkdir -p "$ARTIFACT_DIR" - for path in output/*; do - if [ "$(basename "$path")" = "artifact_name.txt" ]; then - continue - fi - name="$(basename "$path")" - if [ -d "$path" ]; then - mkdir -p "$ARTIFACT_DIR/$name" - cp -r "$path"/* "$ARTIFACT_DIR/$name/" - echo "Copied dir $name -> $ARTIFACT_DIR/$name" - else - cp "$path" "$ARTIFACT_DIR/" - echo "Copied file $name -> $ARTIFACT_DIR/" - fi - done + mkdir -p "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + cp -r output/* "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" + rm -f "local_models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/artifact_name.txt" - - name: Push recompiled dir to GitLab + - name: Upload to Hugging Face env: - GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} run: | - cd gitlab_docs - git checkout main - git pull origin main - for d in models/"$RECOMPILED_DIR"/*/; do - git sparse-checkout add "$d" - done - git add models/"$RECOMPILED_DIR" - git config --global user.name "GitHub Action" - git config --global user.email "action@github.com" - git commit -m "Create/Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit" - git push origin main + hf upload ${{ inputs.hf_repo }} \ + output/ \ + "models/${RECOMPILED_DIR}/${ARTIFACT_NAME}/" \ + --repo-type=dataset - - run: | + - name: Pull gh-pages + run: | cd docs git pull origin gh-pages @@ -220,9 +217,11 @@ jobs: fi [ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\"" [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" + [ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\"" + [ -n "${{ inputs.tinygrad_ref }}" ] && ARGS="$ARGS --tinygrad-ref \"${{ inputs.tinygrad_ref }}\"" eval python3 docs/json_parser.py \ --json-path "$JSON_FILE" \ - --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ + --recompiled-dir "local_models/$RECOMPILED_DIR" \ --sort-by-date \ $ARGS diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 2c435e58a0..17981d68e5 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -80,6 +80,7 @@ jobs: with: repository: commaai/openpilot ref: ${{ inputs.upstream_branch }} + fetch-depth: 1 submodules: recursive path: openpilot @@ -89,18 +90,25 @@ jobs: with: repository: sunnypilot/sunnypilot ref: ${{ inputs.upstream_branch }} + fetch-depth: 1 submodules: recursive path: openpilot - name: Get commit date id: commit-date run: | - cd ${{ github.workspace }}/openpilot + cd ${{ github.workspace }}/openpilot/openpilot commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') echo "model_date=${commit_date}" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT - run: | - cd ${{ github.workspace }}/openpilot - git lfs pull + cd ${{ github.workspace }}/openpilot/openpilot + if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then + git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx" + rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx + else + git lfs pull -I "selfdrive/modeld/models/big_*.onnx" + find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete + fi - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: @@ -116,24 +124,10 @@ jobs: steps: - uses: actions/checkout@v4 with: + fetch-depth: 1 submodules: recursive - run: git lfs pull - - name: Cache SCons - uses: actions/cache@v4 - with: - path: ${{env.SCONS_CACHE_DIR}} - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} - # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) - # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. - restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model - scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }}-model - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }}-model - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_NEW_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.MASTER_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }} - name: Set environment variables id: set-env @@ -144,7 +138,7 @@ jobs: export UV_PYTHON_PREFERENCE=managed export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT - uv sync + uv sync --frozen printenv >> $GITHUB_ENV if [[ "${{ runner.debug }}" == "1" ]]; then cat $GITHUB_OUTPUT @@ -173,8 +167,6 @@ jobs: with: name: models-${{ env.REF }}${{ inputs.artifact_suffix }} path: ${{ env.MODELS_DIR }} - - run: | - rm -f ${{ env.MODELS_DIR }}/{dmonitoring_model,big_driving_policy,big_driving_vision,big_driving_supercombo}.onnx - name: Build Model run: | @@ -191,7 +183,7 @@ jobs: if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then echo "USBGPU build" export USBGPU=1 - TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" else echo "QCOM build" @@ -254,10 +246,8 @@ jobs: # Copy the model files rsync -avm \ --include='*.dlc' \ - --include='*.pkl' \ --include='*.chunk*' \ --include='*.chunkmanifest' \ - --include='*.onnx' \ --exclude='*' \ --delete-excluded \ --chown=comma:comma \ diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index bb7989381b..964227e785 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -195,11 +195,14 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, + {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, STRING}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, + {"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, + {"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}}, // Neural Network Lateral Control {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/openpilot/sunnypilot/SConscript b/openpilot/sunnypilot/SConscript index 09ad39ab43..587deea5ff 100644 --- a/openpilot/sunnypilot/SConscript +++ b/openpilot/sunnypilot/SConscript @@ -1,3 +1,2 @@ SConscript(['common/transformations/SConscript']) -SConscript(['modeld_v2/SConscript']) SConscript(['selfdrive/locationd/SConscript']) diff --git a/openpilot/sunnypilot/modeld_v2/SConscript b/openpilot/sunnypilot/modeld_v2/SConscript deleted file mode 100644 index daaa199ea9..0000000000 --- a/openpilot/sunnypilot/modeld_v2/SConscript +++ /dev/null @@ -1,84 +0,0 @@ -import os -import glob - -from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye -from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE -from openpilot.common.hardware import HARDWARE, PC - -Import('env', 'arch', 'release') -lenv = env.Clone() -tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x] - - -def get_camera_configs(): - DEVICE_RESOLUTIONS = { - "tici": (_ar_ox_fisheye.width, _ar_ox_fisheye.height), - "tizi": (_ar_ox_fisheye.width, _ar_ox_fisheye.height), - "mici": (_os_fisheye.width, _os_fisheye.height), - } - if release or PC or 'CI' in os.environ: - return set(DEVICE_RESOLUTIONS.values()) - return [DEVICE_RESOLUTIONS[HARDWARE.get_device_type()]] - -CAMERA_CONFIGS = get_camera_configs() - -tg_flags = { - 'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0', - 'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")}', -}.get(arch, 'DEV=CPU:LLVM') - -image_flag = { - 'larch64': 'IMAGE=2', -}.get(arch, 'IMAGE=0') - -model_w, model_h = MEDMODEL_INPUT_SIZE -from openpilot.selfdrive.modeld.constants import ModelConstants -frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ -camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) - -pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"' -compile_modeld_script = File("compile_modeld.py").abspath -upstream_compile_script = File(Dir("#openpilot/selfdrive/modeld").File("compile_modeld.py").abspath) -script_deps = [File("compile_modeld.py"), upstream_compile_script] - -def compile_combined(model_type, onnx_args, output_name): - output_pkl = File(f"models/{output_name}").abspath - cmd = (f'{pythonpath_string} {tg_flags} {image_flag} python3 {compile_modeld_script} ' - f'--model-type {model_type} ' - f'--model-size {model_w}x{model_h} ' - f'--camera-resolutions {camera_res_args} ' - f'{onnx_args} ' - f'--frame-skip {frame_skip} ' - f'--output {output_pkl}') - onnx_files = [f for f in onnx_args.split() if f.endswith('.onnx')] - return lenv.Command(output_pkl, tinygrad_files + script_deps + [File(f) for f in onnx_files if os.path.isfile(f)], cmd) - -# Vision + Policy (stock default model) -vision_onnx = File("models/driving_vision.onnx").abspath -policy_onnx = File("models/driving_policy.onnx").abspath -if os.path.isfile(vision_onnx) and os.path.isfile(policy_onnx): - compile_combined('vision_policy', - f'--vision-onnx {vision_onnx} --policy-onnx {policy_onnx}', - 'driving_combined_tinygrad.pkl') - -# Vision + Off-Policy -off_policy_onnx = File("models/driving_off_policy.onnx").abspath -if os.path.isfile(vision_onnx) and os.path.isfile(off_policy_onnx): - policy_arg = f'--policy-onnx {policy_onnx}' if os.path.isfile(policy_onnx) else '' - compile_combined('vision_multi_policy', - f'--vision-onnx {vision_onnx} {policy_arg} --off-policy-onnx {off_policy_onnx}', - 'driving_combined_multi_tinygrad.pkl') - -# Vision + On-Policy + Off-Policy -on_policy_onnx = File("models/driving_on_policy.onnx").abspath -if os.path.isfile(vision_onnx) and os.path.isfile(on_policy_onnx) and os.path.isfile(off_policy_onnx): - compile_combined('vision_multi_policy', - f'--vision-onnx {vision_onnx} --off-policy-onnx {off_policy_onnx} --on-policy-onnx {on_policy_onnx}', - 'driving_combined_tri_tinygrad.pkl') - -# Supercombo -supercombo_onnx = File("models/supercombo.onnx").abspath -if os.path.isfile(supercombo_onnx): - compile_combined('supercombo', - f'--supercombo-onnx {supercombo_onnx}', - 'driving_combined_supercombo_tinygrad.pkl') diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index def54a4599..4397209005 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -8,10 +8,10 @@ See the LICENSE.md file in the root directory for more details. import argparse import os -import pickle +import tempfile import time -from collections import defaultdict from functools import partial +from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob import numpy as np os.environ['GMMU'] = '0' @@ -38,6 +38,9 @@ from tinygrad.engine.jit import TinyJit from tinygrad.tensor import Tensor MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy') +WARP_INPUTS = ['tfm', 'big_tfm'] +POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] +WARP_DEV = os.getenv('WARP_DEV') def _detect_desire_key(shapes: dict) -> str | None: @@ -76,7 +79,7 @@ def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tu def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT, - is_supercombo: bool = False, use_packed: bool = True) -> tuple[dict, dict]: + is_supercombo: bool = False) -> tuple[dict, dict]: road_key, _ = _detect_vision_keys(input_shapes) if not road_key: raise ValueError("Vision road key missing from input shapes.") @@ -92,74 +95,75 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D desire_shape = input_shapes[desire_key] features_buffer = input_shapes.get('features_buffer') - if use_packed: # remove packed detection block after all models are recompiled - npy_arrays = { - 'tfm': np.zeros((3, 3), dtype=np.float32), - 'big_tfm': np.zeros((3, 3), dtype=np.float32) - } + npy_arrays = { + 'tfm': np.zeros((3, 3), dtype=np.float32), + 'big_tfm': np.zeros((3, 3), dtype=np.float32) + } - shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo) - packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) + shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo) + packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) - split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else [] - split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else [] - for (k, s), v in zip(shapes.items(), split_views, strict=True): - npy_arrays[k] = v.reshape(s) + split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else [] + split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else [] + for (k, s), v in zip(shapes.items(), split_views, strict=True): + npy_arrays[k] = v.reshape(s) - queues = { - 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), - dtype=np.float32), device=device).contiguous().realize(), - 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), - } + queues = { + 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + 'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), + dtype=np.float32), device=device).contiguous().realize(), + 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), + } - if features_buffer: - queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]), - dtype=np.float32), device=device).contiguous().realize() + if features_buffer: + queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]), + dtype=np.float32), device=device).contiguous().realize() - queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')}) - else: - # TODO-SP: Remove legacy queuing fallback else block after all models are recompiled - npy_arrays = { - 'desire': np.zeros(desire_shape[2], dtype=np.float32), - 'tfm': np.zeros((3, 3), dtype=np.float32), - 'big_tfm': np.zeros((3, 3), dtype=np.float32) - } - - for key, shape in input_shapes.items(): - if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key): - npy_arrays[key] = np.zeros(shape, dtype=np.float32) - - queues = { - 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]), - dtype=np.float32), device=device).contiguous().realize() - } - - if features_buffer: - queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]), - dtype=np.float32), device=device).contiguous().realize() - - queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()}) + queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')}) return queues, npy_arrays def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict, - frame_skip: int, device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]: - return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False, use_packed=use_packed) + frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]: + return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False) def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, - device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]: - return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True, use_packed=use_packed) + device: str = Device.DEFAULT) -> tuple[dict, dict]: + return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True) -def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int], - features_slice: slice, frame_skip: int, input_shapes: dict, prepare_only: bool): - frame_prepare = make_frame_prepare(nv12, *model_size) +def make_random_images(keys, shape, device): + return {k: Tensor.randint(shape, low=0, high=256, dtype=dtypes.uint8, device=device).realize() for k in keys} + + +def make_warp_queues(device=Device.DEFAULT): + npy = { + 'tfm': np.zeros((3, 3), dtype=np.float32), + 'big_tfm': np.zeros((3, 3), dtype=np.float32), + } + queues = {k: Tensor(v, device='NPY').realize() for k, v in npy.items()} + return queues, npy + + +def make_warp(nv12: NV12Frame, model_w: int, model_h: int): + frame_prepare = make_frame_prepare(nv12, model_w, model_h) + WARP_DEV = os.getenv('WARP_DEV', Device.DEFAULT) + + def warp(tfm, big_tfm, frame, big_frame): + tfm = tfm.to(WARP_DEV) + big_tfm = big_tfm.to(WARP_DEV) + Tensor.realize(tfm, big_tfm) + + warped_frame = frame_prepare(frame, tfm).unsqueeze(0) + warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) + return Tensor.cat(warped_frame, warped_big_frame) + return warp + + +def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, frame_skip: int, input_shapes: dict): sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) @@ -172,20 +176,14 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode is_supercombo = vision_runner is None npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo) - def runner(img_q, big_img_q, feat_q, packed_npy_inputs, frame, big_frame, tfm, big_tfm, **kwargs): + def run_policy(warped, img_q, big_img_q, feat_q, packed_npy_inputs, **kwargs): desire_q = kwargs['desire_q'] - packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT) - tfm_dev = tfm.to(Device.DEFAULT) - big_tfm_dev = big_tfm.to(Device.DEFAULT) + warped_dev = warped.to(Device.DEFAULT) + Tensor.realize(packed_npy_inputs_dev, warped_dev) - Tensor.realize(packed_npy_inputs_dev, tfm_dev, big_tfm_dev) - - img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn).realize() - big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn).realize() - - if prepare_only: - return img, big_img + img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn).realize() + big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn).realize() unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)] unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True)) @@ -220,42 +218,52 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() return policy_out - return runner + return run_policy -def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_only: bool, frame_skip: int, vision_runner, policy_runners: list, metadata: dict): - print(f"Compiling combined JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...") +def compile_jit(jit, make_random_inputs, input_keys, make_queues): + SEED = 42 + def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): + input_queues, npy = make_queues(Device.DEFAULT) + rng = np.random.default_rng(seed) + Tensor.manual_seed(seed) - all_shapes = {key: value for meta in metadata.values() for key, value in meta['input_shapes'].items()} + testing = test_val is not None or test_buffers is not None + n_runs = 1 if testing else 3 - feat_meta = metadata.get('vision') or metadata.get('model') or metadata.get('policy') - if not feat_meta: - raise ValueError("Could not find vision, model, or policy metadata.") + for i in range(n_runs): + for v in npy.values(): + v[:] = rng.standard_normal(v.shape).astype(v.dtype) + Device.default.synchronize() + random_inputs = make_random_inputs() + st = time.perf_counter() + outs = fn(**{k: input_queues[k] for k in input_keys if k in input_queues}, **random_inputs) + mt = time.perf_counter() + Device.default.synchronize() + et = time.perf_counter() + print(f" [{i+1}/{n_runs}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms") - features_slice = feat_meta['output_slices']['hidden_state'] - WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT + if i == 0: + val = [np.copy(v.numpy()) for v in (outs if isinstance(outs, tuple) else [outs])] if outs is not None else [] + buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()] - is_supercombo = vision_runner is None - run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only) - run_jit = TinyJit(run_func, prune=True) - queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT, is_supercombo=is_supercombo) + if test_val is not None: + match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True)) + assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})" + if test_buffers is not None: + match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True)) + assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})" + return val, buffers - for i in range(3): - rng = np.random.default_rng(42 + i) - frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize() - big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize() - for arr in npy_arrays.values(): - arr[:] = rng.standard_normal(arr.shape).astype(arr.dtype) - - Device.default.synchronize() - start_time = time.perf_counter() - run_jit(**queues, frame=frame, big_frame=big_frame) - mid_time = time.perf_counter() - Device.default.synchronize() - print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms") - - # TODO-SP: switch to dump_oob/load_oob on next full recompile of all models - return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit + print('capture + replay') + test_val, test_buffers = random_inputs_run(jit, SEED) + print('pickle round trip') + with tempfile.TemporaryFile(dir=".") as f: + dump_oob(jit, f) + f.seek(0) + deserialized_jit = load_oob(f) + random_inputs_run(deserialized_jit, SEED, test_val=test_val, test_buffers=test_buffers) + return deserialized_jit def _parse_size(size_str: str) -> tuple[int, int]: @@ -277,19 +285,6 @@ def read_file_chunked_to_shm(path): return shm_path -def _compile_for_resolutions(camera_resolutions: list, model_size: tuple[int, int], frame_skip: int, - vision_runner, policy_runners: list, metadata: dict) -> dict: - from openpilot.system.camerad.cameras.nv12_info import get_nv12_info - return { - (cam_w, cam_h): { - name: compile_and_warmup(NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)), model_size, prepare_only, - frame_skip, vision_runner, policy_runners, metadata) - for name, prepare_only in [('warp_enqueue', True), ('run_policy', False)] - } - for cam_w, cam_h in camera_resolutions - } - - def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: runners, keys = [], [] for name, onnx_arg in [('policy', args.policy_onnx), ('off_policy', args.off_policy_onnx), ('on_policy', args.on_policy_onnx)]: @@ -300,7 +295,18 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: if __name__ == "__main__": + if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'): + from openpilot.system.hardware.chestnut.flash import link_up + for _ in range(10): + if link_up(): + break + time.sleep(1) + else: + raise RuntimeError("Chestnut not ready, skipping big model build") + + from openpilot.common.file_chunker import chunk_file, get_chunk_targets from openpilot.selfdrive.modeld.get_model_metadata import make_metadata_dict + from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from tinygrad.nn.onnx import OnnxRunner parser = argparse.ArgumentParser(description="Compile combined JIT pkl for sunnypilot modeld_v2") @@ -317,7 +323,8 @@ if __name__ == "__main__": parser.add_argument('--supercombo-onnx', help='supercombo ONNX (for supercombo)') args = parser.parse_args() - output_data = defaultdict(dict) + model_w, model_h = args.model_size + output_data = {} args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx) args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx) @@ -348,17 +355,31 @@ if __name__ == "__main__": vision_meta = output_data['metadata'].get('vision', {}) derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {})) - output_data.update(_compile_for_resolutions(args.camera_resolutions, args.model_size, derived_frame_skip, - vision_runner, policy_runners, output_data['metadata'])) + all_shapes = {key: value for meta in output_data['metadata'].values() for key, value in meta['input_shapes'].items()} + feat_meta = output_data['metadata'].get('vision') or output_data['metadata'].get('model') or output_data['metadata'].get('policy') + assert feat_meta is not None + features_slice = feat_meta['output_slices']['hidden_state'] + is_supercombo = vision_runner is None + + print(f"Compiling run_policy JIT (model_size={model_w}x{model_h}, frame_skip={derived_frame_skip})...") + run_policy_func = make_run_policy(vision_runner, policy_runners, features_slice, derived_frame_skip, all_shapes) + run_policy_jit = TinyJit(run_policy_func, prune=True) + make_policy_queues = partial(generate_queues_and_npy, all_shapes, derived_frame_skip, is_supercombo=is_supercombo) + make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=WARP_DEV) + output_data['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, make_policy_queues) + + for cam_w, cam_h in args.camera_resolutions: + print(f"Compiling warp JIT for {cam_w}x{cam_h}...") + nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) + warp = TinyJit(make_warp(nv12, model_w, model_h), prune=True) + output_data[(cam_w, cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) with open(args.output, "wb") as file: - # TODO-SP: switch to dump_oob from openpilot/selfdrive/helpers on next full recompile of all models - pickle.dump(output_data, file) + dump_oob(output_data, file) pkl_size = os.path.getsize(args.output) print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)") - - from openpilot.common.file_chunker import chunk_file, get_chunk_targets chunk_targets = get_chunk_targets(args.output, pkl_size) chunk_file(args.output, chunk_targets) print(f"Chunked into {len(chunk_targets) - 1} file(s)") diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index 66b560802b..9f3d709537 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -9,17 +9,13 @@ See the LICENSE.md file in the root directory for more details. import os os.environ['GMMU'] = '0' from openpilot.common.hardware import COMMA_HARDWARE -os.environ['DEV'] = 'QCOM' if COMMA_HARDWARE else 'CPU' -USBGPU = "USBGPU" in os.environ -if USBGPU: - os.environ['DEV'] = 'AMD' - os.environ['AMD_IFACE'] = 'USB' -import pickle +from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob import time import numpy as np import openpilot.cereal.messaging as messaging from openpilot.cereal import log from opendbc.car.structs import car +from openpilot.cereal.services import SERVICE_LIST from setproctitle import setproctitle from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.visionipc import VisionStreamType @@ -27,7 +23,6 @@ from msgq.visionipc import VisionIpcClient, VisionBuf from opendbc.car.car_helpers import get_demo_car_params from tinygrad.tensor import Tensor -from tinygrad.device import Device from openpilot.common.file_chunker import open_file_chunked from openpilot.common.swaglog import cloudlog @@ -40,12 +35,13 @@ from openpilot.system import sentry from openpilot.system.camerad.cameras.nv12_info import get_nv12_info from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value +from openpilot.selfdrive.modeld.modeld import ChestnutState from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper -from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues +from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase @@ -88,7 +84,7 @@ class ModelState(ModelStateBase): inputs: dict[str, np.ndarray] prev_desire: np.ndarray - def __init__(self, cam_w: int, cam_h: int): + def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False): ModelStateBase.__init__(self) env_pkl = os.environ.get('COMBINED_MODEL_PKL') @@ -103,6 +99,7 @@ class ModelState(ModelStateBase): self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.MIN_LAT_CONTROL_SPEED = 0.3 self.PLANPLUS_CONTROL: float = 1.0 + self.usbgpu = usbgpu pkl_path = _find_driving_pkl(model_bundle) assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" @@ -110,24 +107,20 @@ class ModelState(ModelStateBase): def _init_combined(self, pkl_path, cam_w, cam_h, bundle): cloudlog.warning(f"loading combined pkl: {pkl_path}") - # TODO-SP: switch to load_oob from openpilot/selfdrive/helpers on next full recompile of all models - jits = pickle.load(open_file_chunked(pkl_path)) + jits = load_oob(open_file_chunked(pkl_path)) - self.DEV = Device.DEFAULT - self.WARP_DEV = 'CPU' if USBGPU else self.DEV + self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' + self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV self.QUEUE_DEV = self.DEV - metadata = jits['metadata'] - self._run_policy = jits[(cam_w, cam_h)]['run_policy'] - self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue'] - - # TODO-SP: Remove legacy use_packed detection block after all models are recompiled - captured = getattr(self._run_policy, 'captured', None) - if captured is not None: - use_packed = 'packed_npy_inputs' in getattr(captured, 'expected_names', []) + self.is_legacy_model = 'run_policy' not in jits # remove after next recompile + if self.is_legacy_model: + self.warp = jits[(cam_w, cam_h)]['warp_enqueue'] + self.run_policy = jits[(cam_w, cam_h)]['run_policy'] else: - use_packed = True + self.run_policy = jits['run_policy'] + self.warp = jits[(cam_w, cam_h)] if 'model' in metadata: model_metadata = metadata['model'] @@ -136,10 +129,9 @@ class ModelState(ModelStateBase): self._policy_slices_list = [] self._combined_model_type = 'supercombo' self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key] - from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues frame_skip = derive_frame_skip({}, model_metadata['input_shapes']) self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], - frame_skip, device=self.QUEUE_DEV, use_packed=use_packed) + frame_skip, device=self.QUEUE_DEV) else: vision_metadata = metadata['vision'] policy_keys = [k for k in metadata if k != 'vision'] @@ -152,13 +144,12 @@ class ModelState(ModelStateBase): self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys] self.policy_output_slices = self._policy_slices_list[0] self._has_on_policy = any('on' in k.lower() for k in policy_keys) - first_policy_metadata = metadata[policy_keys[0]] - vision_input_shapes = vision_metadata['input_shapes'] - policy_input_shapes = first_policy_metadata['input_shapes'] - self._vision_input_names = [k for k in vision_input_shapes if 'img' in k] - frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes) - self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, - frame_skip, device=self.QUEUE_DEV, use_packed=use_packed) + self._vision_input_names = [key for key in vision_metadata['input_shapes'] if 'img' in key] + first_policy_meta = metadata[policy_keys[0]] + frame_skip = derive_frame_skip(vision_metadata['input_shapes'], first_policy_meta['input_shapes']) + self.input_queues, self.numpy_inputs = make_split_input_queues(vision_metadata['input_shapes'], + first_policy_meta['input_shapes'], + frame_skip, device=self.QUEUE_DEV) self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire')) self._road_key = next(key for key in self._vision_input_names if 'big' not in key) @@ -186,10 +177,33 @@ class ModelState(ModelStateBase): self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info) yuv_size = self.frame_buf_params[self._road_key][3] - self._warp_enqueue( - **self.input_queues, - frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize(), - big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize()) + frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() + big_frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() + + if self.is_legacy_model: # Remove this conditional hack after recompile + self.warp(**self.input_queues, frame=frame_tensor, big_frame=big_frame_tensor) + else: + self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) + + if self.usbgpu: + self.warmup() + + def warmup(self) -> None: + dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names} + transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k} + + dummy_inputs = {} + for k, v in self.numpy_inputs.items(): + if k not in ['tfm', 'big_tfm', 'prev_feat']: + dummy_inputs[k] = np.zeros(v.shape, dtype=v.dtype) + + self.run(dummy_frames, transforms, dummy_inputs, prepare_only=False) + + for v in self.numpy_inputs.values(): + v[:] = 0 + self.prev_desire[:] = 0 + self.full_frames.clear() + self._blob_cache.clear() @property @@ -227,11 +241,17 @@ class ModelState(ModelStateBase): self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3) self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3) - if prepare_only: - self._warp_enqueue(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - return None - - raw_outputs = self._run_policy(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + if self.is_legacy_model: # remove after next recompile + if prepare_only: + self.warp(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + return None + raw_outputs = self.run_policy(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + else: + if prepare_only: + self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + return None + warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped) if self._combined_model_type == 'supercombo': model_output = raw_outputs.numpy().flatten() @@ -267,10 +287,9 @@ class ModelState(ModelStateBase): buf[0, :-1] = buf[0, 1:] buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 - # TODO-SP: This is a hack to prevent GPU corruption by calculating in CPU space, it can be removed on next recompile - if 'prev_feat' not in self.numpy_inputs and 'feat_q' in self.input_queues: - feat_val = self.input_queues['feat_q'].numpy() - self.input_queues['feat_q'].assign(feat_val).realize() + if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): + cloudlog.error("model output not finite, dropping frame") + return None return outputs @@ -278,8 +297,8 @@ class ModelState(ModelStateBase): lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: if 'action' not in model_output: plan = model_output['plan'][0] - desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, - action_t=long_action_t) + desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS, + action_t=long_action_t) curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0] if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan) @@ -287,8 +306,8 @@ class ModelState(ModelStateBase): else: desired_accel = model_output['action'][0, 1] desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2 - should_stop = (v_ego < 0.3 and desired_accel < 0.1) + stop = v_ego < 0.3 and desired_accel < 0.1 desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models @@ -297,7 +316,7 @@ class ModelState(ModelStateBase): else: desired_curvature = prev_action.desiredCurvature - return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop)) + return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature), desiredAcceleration=float(desired_accel), shouldStop=bool(stop)) def main(demo=False): @@ -308,6 +327,14 @@ def main(demo=False): setproctitle(PROCESS_NAME) config_realtime_process(7, 54) + USBGPU = usbgpu_present() + if USBGPU: + os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' + + params = Params() + params.put_bool("UsbGpuLoading", USBGPU) + params.remove("UsbGpuActive") + # visionipc clients while True: available_streams = VisionIpcClient.available_streams("camerad", block=False) @@ -332,15 +359,34 @@ def main(demo=False): cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") cloudlog.warning("loading model") - model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height) - cloudlog.warning("models loaded, modeld starting") + st = time.monotonic() + + model = None + if USBGPU: + import threading + def load(): + nonlocal model + model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True) + t = threading.Thread(target=load, daemon=True) + t.start() + t.join(60) + if model is None: + params.put_bool("UsbGpuActive", False) + raise RuntimeError("eGPU model load failed or timed out (60s)") + params.put_bool("UsbGpuActive", True) + else: + model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False) + + params.put_bool("UsbGpuLoading", False) + cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) + pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() - params = Params() + chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) @@ -478,6 +524,7 @@ def main(demo=False): fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants) + modelv2_send.modelV2.big = model.usbgpu desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] @@ -498,6 +545,8 @@ def main(demo=False): pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id + if chestnut_state is not None and run_count % round(model.constants.MODEL_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0: + chestnut_state.send() if __name__ == "__main__": try: diff --git a/openpilot/sunnypilot/modeld_v2/tests/helpers.py b/openpilot/sunnypilot/modeld_v2/tests/helpers.py index 6925a61f08..ee59e82785 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/helpers.py +++ b/openpilot/sunnypilot/modeld_v2/tests/helpers.py @@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details. """ import pathlib -import pickle import tempfile import openpilot.sunnypilot.models.helpers as helpers @@ -164,14 +163,16 @@ ARCHETYPES = { def make_pkl_data(archetype): return { 'metadata': archetype.metadata_structure, - (CAM_W, CAM_H): {'run_policy': _noop_jit, 'warp_enqueue': _noop_jit}, + 'run_policy': _noop_jit, + (CAM_W, CAM_H): _noop_jit, } def write_pkl(tmp_path, archetype): + from openpilot.selfdrive.modeld.helpers import dump_oob pkl_path = tmp_path / 'driving_test_tinygrad.pkl' with open(pkl_path, 'wb') as f: - pickle.dump(make_pkl_data(archetype), f) + dump_oob(make_pkl_data(archetype), f) return pkl_path diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py index 85305395ea..fb72022fa5 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_recovery_power.py @@ -33,7 +33,7 @@ class TestRecoveryPower(OpenpilotTestCase): def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0): recorded_vel.append(plan_vel.copy()) - return 0.0, False + return 0.0 def mock_curvature(output, plan, vego, lat_action_t, mlsim): recorded_curv_plans.append(plan.copy()) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index eda1117a2a..e60af2925f 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -13,6 +13,7 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible +from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.cereal import custom @@ -103,11 +104,11 @@ class ModelParser: class ModelCache: """Handles caching of model data to avoid frequent remote fetches""" - def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9)): + def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9), suffix: str = ""): self.params = params self.cache_timeout = cache_timeout - self._LAST_SYNC_KEY = "ModelManager_LastSyncTime" - self._CACHE_KEY = "ModelManager_ModelsCache" + self._LAST_SYNC_KEY = f"ModelManager_LastSyncTime{suffix}" + self._CACHE_KEY = f"ModelManager_ModelsCache{suffix}" def _is_expired(self) -> bool: """Checks if the cache has expired""" @@ -139,24 +140,37 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v19.json" + MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v19.json" def __init__(self, params: Params): self.params = params - self.model_cache = ModelCache(params) self.model_parser = ModelParser() + self._is_usbgpu: bool | None = None + self.model_cache = ModelCache(params) + self.model_url = self.MODEL_URL + self._update_model_source() + + def _update_model_source(self) -> None: + """Updates what json to use based on usbgpu availability""" + is_usbgpu = usbgpu_present() + if is_usbgpu != self._is_usbgpu: + self._is_usbgpu = is_usbgpu + self.model_cache = ModelCache(self.params, suffix="_USBGPU" if is_usbgpu else "") + self.model_url = self.MODEL_URL_USBGPU if is_usbgpu else self.MODEL_URL + self.params.put("ModelManager_ActiveJson", self.model_url, block=True) def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None: """Fetches fresh model data from remote and updates cache. Returns None on transport errors. Raises on 404 and other fatal HTTP errors. """ try: - response = requests.get(self.MODEL_URL, timeout=10) + response = requests.get(self.model_url, timeout=10) # Explicitly handle 404 differently if response.status_code == 404: - cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}") - raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response) + cloudlog.error(f"Models URL returned 404 Not Found: {self.model_url}") + raise HTTPError(f"404 Not Found: {self.model_url}", response=response) # Raise for any other 4xx/5xx response.raise_for_status() @@ -179,6 +193,7 @@ class ModelFetcher: def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: """Gets the list of available models, with smart cache handling""" + self._update_model_source() cached_data, is_expired = self.model_cache.get() if cached_data and not is_expired: @@ -202,10 +217,7 @@ if __name__ == "__main__": for bundle in bundles: for model in bundle.models: model_overrides = {override.key: override.value for override in bundle.overrides} - # Print model details print(f"Bundle: {bundle.internalName}, Type: {model.type}, Status: {bundle.status}, Overrides: {model_overrides}") - # Print artifact details print(f"Artifact: {model.artifact.fileName}, Download URI: {model.artifact.downloadUri.uri}") - # Print metadata details if model.artifact.chunks: print(f"Contains {len(model.artifact.chunks)} chunks.") diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index 101d8d196e..b5c97467d3 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai from openpilot.common.hardware.hw import Paths # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO -REQUIRED_JSON_VERSION = 16 +REQUIRED_JSON_VERSION = 17 CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' diff --git a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py index 1712c60410..fd389f93c0 100644 --- a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py +++ b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py @@ -1,12 +1,13 @@ import requests +from openpilot.common.params import Params from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref from openpilot.sunnypilot.models.fetcher import ModelFetcher from openpilot.common.test import OpenpilotTestCase - def fetch_tinygrad_ref(): - response = requests.get(ModelFetcher.MODEL_URL, timeout=10) + fetcher = ModelFetcher(Params()) + response = requests.get(fetcher.model_url, timeout=10) response.raise_for_status() json_data = response.json() return json_data.get("tinygrad_ref") diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 76935f3627..607260f145 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -48,6 +48,11 @@ def create_short_name(full_name: str) -> str: return result[:8] +def create_pkl_name(full_name: str) -> str: + pkl = re.sub(r'[^a-zA-Z0-9]+', '_', full_name).strip('_').lower() + return pkl + + def _read_pkl_bytes(pkl_path: Path) -> bytes: manifest = Path(f"{pkl_path}.chunkmanifest") if manifest.exists(): @@ -154,14 +159,15 @@ if __name__ == "__main__": _output_dir = Path(args.output_dir) _output_dir.mkdir(exist_ok=True, parents=True) _short_name = create_short_name(args.custom_name) if args.custom_name else None + _pkl = create_pkl_name(args.custom_name) if args.custom_name else None _driving_pkl = _find_driving_pkl(_output_dir) if not _driving_pkl: print(f"No driving_tinygrad.pkl found in {_output_dir}", file=sys.stderr) sys.exit(1) - if _short_name: - new_pkl = _output_dir / f"driving_{_short_name.lower()}_tinygrad.pkl" + if _pkl: + new_pkl = _output_dir / f"driving_{_pkl}_tinygrad.pkl" if not new_pkl.exists(): _driving_pkl = _rename_pkl_with_chunks(_driving_pkl, new_pkl) else: diff --git a/tinygrad_repo b/tinygrad_repo index 2fecac4e4a..66ee3cfb4f 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 2fecac4e4ac32fe369c41f8400b6e7b9adb18683 +Subproject commit 66ee3cfb4f3a3908a6a20ddfbec7774ba7c09b4e From b8e14d85fbb5e3da1af7f3596e0d9eb49965056d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 17 Aug 2026 21:47:14 -0700 Subject: [PATCH 258/325] chestnut: don't compile if big model is LFS pointer (#38655) * use compiled helper for hardwared alert, source doesn't matter. scons skips compile if it's empty/lfs pointer * log it * rmnl * compile failed * out of scope * rmnl --- openpilot/selfdrive/modeld/SConscript | 6 +++++- openpilot/selfdrive/modeld/helpers.py | 4 ++++ openpilot/system/hardware/hardwared.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 30a31aae27..8b2322f94e 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path +from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, big_model_source_available, usbgpu_present, modeld_pkl_path CAMERA_CONFIGS = [ @@ -44,6 +44,10 @@ tg_devices = { # which device to put jit inputs to at runtime } USBGPU = usbgpu_present() +if USBGPU and not big_model_source_available(): + print("Big model source unavailable, skipping big model build") + USBGPU = False + if USBGPU: usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 37ab0b26d7..ffd59eae06 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -21,6 +21,10 @@ def modeld_pkl_path(usbgpu: bool): prefix = 'big_' if usbgpu else '' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' +def big_model_source_available() -> bool: + model_path = MODELS_DIR / 'big_driving_supercombo.onnx' + return model_path.is_file() and model_path.stat().st_size >= 1024 + def dump_oob(obj, f): with tempfile.TemporaryFile(dir=".") as tmp: def buffer_callback(pb: pickle.PickleBuffer): diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index a423d8f97d..2817ef3942 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,6 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW +from openpilot.selfdrive.modeld.helpers import usbgpu_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR @@ -237,8 +238,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) or \ - os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest")) + big_model_available = usbgpu_compiled() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From b7657f6553855a569e30e2a971d6e7043d3badc4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 17 Aug 2026 21:54:29 -0700 Subject: [PATCH 259/325] lfs: exclude big driving model in master clones (#38626) * exclude big * lfs * Revert "lfs" This reverts commit b646d5fb50d2a5be1e6e73275f2ee302687e670f. --- .lfsconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.lfsconfig b/.lfsconfig index 42dfa2d944..e538b9d031 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,4 +1,5 @@ [lfs] url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git + fetchexclude = "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" locksverify = false From 6dd3457f4fd269919cbfb95af4bd930761fbe166 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 18 Aug 2026 02:36:48 -0400 Subject: [PATCH 260/325] ci: refactor prebuilt workflow (#1916) * ci: fix prebuilt file copy for null-separated release_files.py output * ci: drop prebuilt symlinks the launch script recreates and gate the release on no submodules * ci: use the device-local scons cache and prune dead prebuilt config * ci: keep the scons cache in the runner workspace instead of the device's --- .../workflows/sunnypilot-build-prebuilt.yaml | 124 +++++------------- release/ci/publish.sh | 6 + 2 files changed, 41 insertions(+), 89 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 12a3a7bce0..db0d0f55b4 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -4,12 +4,8 @@ env: BUILD_DIR: "/data/openpilot" OUTPUT_DIR: ${{ github.workspace }}/output CI_DIR: ${{ github.workspace }}/release/ci - SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" - # Branch configurations - STAGING_SOURCE_BRANCH: 'master' - # Runtime configuration SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" @@ -109,11 +105,6 @@ jobs: group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: false runs-on: [self-hosted, tici] - outputs: - new_branch: ${{ needs.prepare_strategy.outputs.new_branch }} - version: ${{ needs.prepare_strategy.outputs.version }} - extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }} - commit_sha: ${{ github.sha }} if: ${{ (always() && !cancelled() && !failure()) && needs.prepare_strategy.result == 'success' && @@ -129,26 +120,8 @@ jobs: repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} - run: git lfs pull - - name: Cache SCons - uses: actions/cache@v4 - with: - path: ${{env.SCONS_CACHE_DIR}} - key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }} - # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) - # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. - restore-keys: | - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_SOURCE_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }} - - name: Set environment variables - id: set-env run: | - echo "new_branch=${{ needs.prepare_strategy.outputs.new_branch }}" >> $GITHUB_OUTPUT - echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT - echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT - echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT - # Set up common environment source /etc/profile; export UV_PROJECT_ENVIRONMENT=${HOME}/venv @@ -157,9 +130,6 @@ jobs: export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT uv sync printenv >> $GITHUB_ENV - if [[ "${{ runner.debug }}" == "1" ]]; then - cat $GITHUB_OUTPUT - fi - name: Setup build environment run: | @@ -168,7 +138,7 @@ jobs: echo "Starting build stage..." echo "BUILD_DIR: ${BUILD_DIR}" echo "CI_DIR: ${CI_DIR}" - echo "VERSION: ${{ steps.set-env.outputs.version }}" + echo "VERSION: ${{ needs.prepare_strategy.outputs.version }}" echo "UV_PROJECT_ENVIRONMENT: ${UV_PROJECT_ENVIRONMENT}" echo "VIRTUAL_ENV: ${VIRTUAL_ENV}" echo "-------" @@ -180,61 +150,44 @@ jobs: - name: Build Main Project run: | - export PYTHONPATH="$BUILD_DIR" - ./tools/release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/ + export PYTHONPATH="$BUILD_DIR:$BUILD_DIR/msgq_repo:$BUILD_DIR/opendbc_repo:$BUILD_DIR/rednose_repo:$BUILD_DIR/teleoprtc_repo:$BUILD_DIR/tinygrad_repo" + ./tools/release/release_files.py | xargs -0 cp -pR --parents -t "$BUILD_DIR" -- + # outside the checkout, which is wiped each run. /data/scons_cache is the device's, not ours. + SCONS_CACHE="$RUNNER_WORKSPACE/scons_cache" + mkdir -p "$SCONS_CACHE" cd $BUILD_DIR - ln -sfn msgq_repo/msgq msgq - ln -sfn opendbc_repo/opendbc opendbc - ln -sfn rednose_repo/rednose rednose - ln -sfn teleoprtc_repo/teleoprtc teleoprtc - ln -sfn tinygrad_repo/tinygrad tinygrad - sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py - echo "Building sunnypilot's modeld_v2..." - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/modeld_v2 - echo "Building sunnypilot's locationd..." - scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/sunnypilot/selfdrive/locationd - echo "Building openpilot's locationd..." - scons -j1 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal openpilot/selfdrive/locationd + echo "Building locationd..." + # -j1: parallel rednose generators OOM the device + scons -j1 cache_dir="$SCONS_CACHE" --minimal \ + openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd echo "Building rest of sunnypilot" - scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal + /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal touch ${BUILD_DIR}/prebuilt if [[ "${{ runner.debug }}" == "1" ]]; then ls -la ${BUILD_DIR} fi - - name: Prepare Output + - name: Strip release tree run: | - sudo rm -rf ${OUTPUT_DIR} - mkdir -p ${OUTPUT_DIR} - rsync -am${RUNNER_DEBUG:+v} \ - --exclude='.sconsign.dblite' \ - --exclude='*.a' \ - --exclude='*.o' \ - --exclude='*.os' \ - --exclude='*.pyc' \ - --exclude='moc_*' \ - --exclude='__pycache__' \ - --exclude='Jenkinsfile' \ - --exclude='**/release/' \ - --exclude='**/.github/' \ - --exclude='**/openpilot/selfdrive/ui/replay/' \ - --exclude='**/__pycache__/' \ - --exclude='${{env.SCONS_CACHE_DIR}}' \ - --exclude='**/.git/' \ - --exclude='**/SConstruct' \ - --exclude='**/SConscript' \ - --exclude='**/.venv/' \ - --exclude='openpilot/selfdrive/modeld/models/*.onnx*' \ - --exclude='openpilot/sunnypilot/modeld*/models/*.onnx*' \ - --exclude='openpilot/third_party/*x86*' \ - --exclude='openpilot/third_party/*Darwin*' \ - --delete-excluded \ - --chown=comma:comma \ - ${BUILD_DIR}/ ${OUTPUT_DIR}/ + cd $BUILD_DIR + find . -name '*.a' -delete + find . -name '*.o' -delete + find . -name '*.os' -delete + find . -name '*.pyc' -delete + find . -name 'moc_*' -delete + find . -name '__pycache__' -type d -exec rm -rf {} + + find . -name 'SConstruct' -delete + find . -name 'SConscript' -delete + rm -rf .sconsign.dblite Jenkinsfile tools/release/ release/ + rm -f openpilot/selfdrive/modeld/models/*.onnx* + rm -f openpilot/sunnypilot/modeld*/models/*.onnx* + find openpilot/third_party/ -name '*x86*' -exec rm -r {} + + find openpilot/third_party/ -name '*Darwin*' -exec rm -r {} + + cd - - name: 'Tar.gz files' run: | - tar czf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} . + tar czf prebuilt.tar.gz -C ${{ env.BUILD_DIR }} . ls -la prebuilt.tar.gz - name: 'Upload Artifact' @@ -242,6 +195,7 @@ jobs: with: name: prebuilt path: prebuilt.tar.gz + compression-level: 0 - name: Re-enable powersave if: always() @@ -283,29 +237,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - echo '${{ toJSON(needs.build.outputs) }}' + echo '${{ toJSON(needs.prepare_strategy.outputs) }}' ls -la ${{ env.OUTPUT_DIR }} ${{ env.CI_DIR }}/publish.sh \ "${{ github.workspace }}" \ "${{ env.OUTPUT_DIR }}" \ - "${{ needs.build.outputs.new_branch }}" \ - "${{ needs.build.outputs.version }}" \ + "${{ needs.prepare_strategy.outputs.new_branch }}" \ + "${{ needs.prepare_strategy.outputs.version }}" \ "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ - "${{ needs.build.outputs.extra_version_identifier }}" - - echo "" - echo "---- ℹ️ To update the list of branches that auto deploy prebuilts -----" - echo "" - echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES" - echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}" - echo "3. Update as needed (JSON array with no spaces)" + "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" - name: Tag ${{ needs.prepare_strategy.outputs.environment }} if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} run: | TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}" - git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.build.outputs.build }}." + git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.prepare_strategy.outputs.build }}." git push -f origin ${TAG} notify: @@ -324,7 +271,6 @@ jobs: - name: Prepare notification message id: message run: | - TEMPLATE='${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}' export VERSION="${{ needs.prepare_strategy.outputs.version }}" export branch_name="${{ env.SOURCE_BRANCH }}" export new_branch="${{ needs.prepare_strategy.outputs.new_branch }}" @@ -373,7 +319,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - name: process.env.LABELf + name: process.env.LABEL }); console.log(`Removed '${process.env.LABEL}' label from PR #${prNumber}`); diff --git a/release/ci/publish.sh b/release/ci/publish.sh index 27904caf5d..fd1a61a87c 100755 --- a/release/ci/publish.sh +++ b/release/ci/publish.sh @@ -52,6 +52,12 @@ git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --all echo "[-] committing version $VERSION T=$SECONDS" git add -f . +# gitlinks break the release tree on device +if git ls-files -s | awk '$1 == "160000" { found = 1; print } END { exit !found }'; then + echo "Error: submodules found in release tree." + exit 1 +fi + # include source commit hash and build date in commit GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD) DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') From 2f4744d39be8ef28757f32bd4bc0724a05126064 Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Tue, 18 Aug 2026 13:47:52 -0700 Subject: [PATCH 261/325] modeld_v2: fix features_buffer alignment for supercombo models (#1918) Co-authored-by: Quantizr (Jimmy) --- openpilot/sunnypilot/modeld_v2/compile_modeld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index 4397209005..927e5f3392 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -117,7 +117,8 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D } if features_buffer: - queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]), + feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1 + queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], features_buffer[2]), dtype=np.float32), device=device).contiguous().realize() queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')}) From 3d09a47a4754c6960bceb7d9f2cb0b5a5e537867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 18 Aug 2026 14:15:20 -0700 Subject: [PATCH 262/325] cruise planner: fix decel jerk from cruise (#38653) * cruise planner: fix decel jerk from cruise * dead variable --- .../controls/lib/longitudinal_planner.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_planner.py b/openpilot/selfdrive/controls/lib/longitudinal_planner.py index c2b8b94abb..cc1345a6ae 100755 --- a/openpilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/openpilot/selfdrive/controls/lib/longitudinal_planner.py @@ -47,9 +47,8 @@ def get_cruise_accel(e2e, v_cruise, v_ego, a_cruise_prev, angle_steers, CP, dt, max_accel = min(max_accel, coast_limit) target_accel = np.clip(v_cruise - v_ego, A_CRUISE_MIN, max_accel) - if not e2e: - j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS) - target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt)) + j_cruise = np.interp(v_ego, A_CRUISE_MAX_BP, J_CRUISE_VALS) + target_accel = float(np.clip(target_accel, a_cruise_prev - j_cruise * dt, a_cruise_prev + j_cruise * dt)) return target_accel @@ -62,10 +61,9 @@ class LongitudinalPlanner: self.dt = dt self.allow_throttle = True - self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) - self.a_cruise = 0.0 - self.output_a_target = 0.0 + self.a_cruise = init_a + self.output_a_target = init_a self.output_should_stop = False self.v_desired_trajectory = np.zeros(CONTROL_N) @@ -100,7 +98,8 @@ class LongitudinalPlanner: if reset_state: self.v_desired_filter.x = v_ego - self.a_desired = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX) + self.output_a_target = np.clip(sm['carState'].aEgo, ACCEL_MIN, ACCEL_MAX) + self.a_cruise = self.output_a_target # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) @@ -109,7 +108,7 @@ class LongitudinalPlanner: prev_accel_constraint = not (reset_state or sm['carState'].standstill) self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality) - self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired) + self.mpc.set_cur_state(self.v_desired_filter.x, self.output_a_target) self.mpc.update(sm['radarState'], personality=sm['selfdriveState'].personality) self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution) @@ -122,7 +121,7 @@ class LongitudinalPlanner: cloudlog.info("FCW triggered") # Save starting point for next iteration - a_prev = self.a_desired + a_prev = self.output_a_target action_t = self.CP.longitudinalActuatorDelay + DT_MDL output_a_target_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX, @@ -145,7 +144,6 @@ class LongitudinalPlanner: self.output_should_stop = any(should_stop for _, _, should_stop in candidates) self.output_a_target = np.clip(output_a_target, ACCEL_MIN, ACCEL_MAX) - self.a_desired = float(self.output_a_target) self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.output_a_target + a_prev) / 2.0 def publish(self, sm, pm): From 59833c500a4b204a211e76a377033f77f4da6d09 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:23:47 -0700 Subject: [PATCH 263/325] models: bump json version (#1919) --- openpilot/sunnypilot/models/fetcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index e60af2925f..4d6c9964d7 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -140,8 +140,8 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v19.json" - MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v19.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" + MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json" def __init__(self, params: Params): self.params = params From 20ba774eaa8856f71bd8d3e76c9db04a2b069282 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 18 Aug 2026 19:45:40 -0400 Subject: [PATCH 264/325] [TIZI/TICI] ui: fix missing model download status and rework status row (#1920) * [TIZI/TICI] ui: fix missing model download status and rework the status row * fix lint --- .../ui/sunnypilot/layouts/settings/models.py | 74 ++++---- openpilot/system/ui/sunnypilot/lib/utils.py | 40 +++++ .../ui/sunnypilot/widgets/download_status.py | 166 ++++++++++++++++++ 3 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 openpilot/system/ui/sunnypilot/widgets/download_status.py diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index ae4ff802f3..aa5700fe28 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -22,9 +22,9 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH from openpilot.system.ui.sunnypilot.lib.styles import style -from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction +from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction, ScrollingButtonAction from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp -from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item +from openpilot.system.ui.sunnypilot.widgets.download_status import download_status_item from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder if gui_app.sunnypilot_ui(): @@ -35,9 +35,8 @@ class ModelsLayout(Widget): def __init__(self): super().__init__() self.model_manager = None - self.download_status = None - self.prev_download_status = None self.model_dialog = None + self._downloading = False self.last_cache_calc_time = 0 self._initialize_items() @@ -52,15 +51,11 @@ class ModelsLayout(Widget): self.current_model_item = ListItemSP( title=tr("Current Model"), description="", - action_item=NoElideButtonAction(tr("SELECT")), + action_item=ScrollingButtonAction(tr("SELECT")), callback=self._handle_current_model_clicked ) - self.supercombo_label = progress_item(tr("Driving Model")) - self.vision_label = progress_item(tr("Vision Model")) - self.policy_label = progress_item(tr("Policy Model")) - self.off_policy_label = progress_item(tr("Off-Policy Model")) - self.on_policy_label = progress_item(tr("On-Policy Model")) + self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), @@ -98,8 +93,7 @@ class ModelsLayout(Widget): 1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, lambda v: f"{v / 100:.2f} m") - self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label, - self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, + self.items = [self.current_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset] def _update_lagd_description(self, lagd_toggle: bool): @@ -135,14 +129,9 @@ class ModelsLayout(Widget): gui_app.push_widget(dialog) def _handle_bundle_download_progress(self): - labels = {custom.ModelManagerSP.Model.Type.supercombo: self.supercombo_label, - custom.ModelManagerSP.Model.Type.vision: self.vision_label, - custom.ModelManagerSP.Model.Type.policy: self.policy_label, - custom.ModelManagerSP.Model.Type.offPolicy: self.off_policy_label, - custom.ModelManagerSP.Model.Type.onPolicy: self.on_policy_label} - for label in labels.values(): - label.set_visible(False) + self.download_item.set_visible(False) self.cancel_download_item.set_visible(False) + self._downloading = False if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle): return @@ -153,32 +142,41 @@ class ModelsLayout(Widget): if not bundle: return - self.download_status = bundle.status - status_changed = self.prev_download_status != self.download_status - self.prev_download_status = self.download_status - self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadIndex") is not None) if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: self.last_cache_calc_time = current_time self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") - if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading: + if bundle.status == custom.ModelManagerSP.DownloadStatus.downloading: device._reset_interactive_timeout() - for model in bundle.models: - if label := labels.get(getattr(model.type, 'raw', model.type)): - label.set_visible(True) - p = model.artifact.downloadProgress - text, show, color = f"pending - {bundle.displayName}", False, rl.GRAY - if p.status == custom.ModelManagerSP.DownloadStatus.downloading: - text, show = f"{int(p.progress)}% - {bundle.displayName}", True - elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached): - status_text = tr("from cache" if p.status == custom.ModelManagerSP.DownloadStatus.cached else "downloaded") - text, color = f"{bundle.displayName} - {status_text if status_changed else tr('ready')}", ON_COLOR - elif p.status == custom.ModelManagerSP.DownloadStatus.failed: - text, color = f"download failed - {bundle.displayName}", rl.RED - label.action_item.update(p.progress, text, show, color) + # every bundle is a single chunked artifact now + progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName] + if not progresses: + return + + self.download_item.set_visible(True) + self.download_item.action_item.update(**self._download_row_state(progresses, bundle.internalName)) + self._downloading = self.download_item.action_item.downloading + + @staticmethod + def _download_row_state(progresses, name: str) -> dict: + """Maps a bundle's artifact progress to DownloadStatusAction.update kwargs.""" + # .raw: _DynamicEnum equals its int but does not hash like it + statuses = {getattr(p.status, 'raw', p.status) for p in progresses} + progress = sum(p.progress for p in progresses) / len(progresses) + ds = custom.ModelManagerSP.DownloadStatus + + if ds.failed in statuses: + # close.png is authored black and a tint cannot lift it, hence close2 + return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"} + if ds.downloading in statuses: + return {"name": name, "downloading": True, "progress": progress} + if statuses <= {ds.downloaded, ds.cached}: + return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"} + # circled_slash is authored grey; tinting it again only darkens it + return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE} @staticmethod def _show_reset_params_dialog(): @@ -251,7 +249,7 @@ class ModelsLayout(Widget): self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] self._handle_bundle_download_progress() - active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)" + active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)" self.current_model_item.action_item.set_value(active_name) if not ui_state.is_offroad(): diff --git a/openpilot/system/ui/sunnypilot/lib/utils.py b/openpilot/system/ui/sunnypilot/lib/utils.py index ecaba86f4b..b9ed152aff 100644 --- a/openpilot/system/ui/sunnypilot/lib/utils.py +++ b/openpilot/system/ui/sunnypilot/lib/utils.py @@ -4,7 +4,15 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from collections.abc import Callable + +import pyray as rl + +from openpilot.system.ui.lib.application import FontWeight +from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP +from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.list_view import BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING, _resolve_value class NoElideButtonAction(ButtonActionSP): @@ -12,6 +20,38 @@ class NoElideButtonAction(ButtonActionSP): return super().get_width_hint() + 1 +class ScrollingButtonAction(ButtonActionSP): + """ButtonActionSP whose value scrolls instead of eliding when it doesn't fit.""" + + def __init__(self, text: str | Callable[[], str], width: int = style.BUTTON_ACTION_WIDTH, + enabled: bool | Callable[[], bool] = True): + super().__init__(text=text, width=width, enabled=enabled) + self._value_label = UnifiedLabel("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL, + text_color=self._value_color, scroll=True, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + + def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR): + if self.value != _resolve_value(value, ""): + self._value_label.reset_scroll() + super().set_value(value, color) + self._value_label.set_text(value) + self._value_label.set_text_color(color) + + def _render(self, rect: rl.Rectangle) -> bool: + """Duplicate of ButtonActionSP._render, with the value drawn by a scrolling label""" + self._button.set_text(self.text) + self._button.set_enabled(_resolve_value(self.enabled)) + button_rect = rl.Rectangle(rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT) + self._button.render(button_rect) + + if self.value: + self._value_label.render(rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)) + + pressed = self._pressed + self._pressed = False + return pressed + + class AlertFadeAnimator: def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05): from openpilot.common.filter_simple import FirstOrderFilter diff --git a/openpilot/system/ui/sunnypilot/widgets/download_status.py b/openpilot/system/ui/sunnypilot/widgets/download_status.py new file mode 100644 index 0000000000..b299c464f1 --- /dev/null +++ b/openpilot/system/ui/sunnypilot/widgets/download_status.py @@ -0,0 +1,166 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import math + +import numpy as np +import pyray as rl + +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.lib.styles import style +from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP +from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.list_view import ItemAction + +FONT_SIZE = style.ITEM_TEXT_FONT_SIZE +ICON_SIZE = 56 +ICON_PADDING = 12 + +BAR_WIDTH = 1100 +BAR_HEIGHT = 20 +BAR_GAP = 16 +BAR_RADIUS = BAR_HEIGHT / 2 +CAPSULE_POINTS = 24 + +RAIL_COLOR = rl.Color(60, 60, 60, 255) +FILL_COLOR = rl.Color(30, 121, 232, 255) +# rl.WHITE is a tuple; the shimmer path reads .a off the color +TEXT_COLOR = rl.Color(255, 255, 255, 255) + +SWEEP_SPEED = 550.0 # px/s +SWEEP_BAND = 240.0 # highlight half-width, px +SWEEP_DIM = 0.65 + + +class DownloadStatusAction(ItemAction): + """Model download row: a name + percent over a progress rail while downloading, a name + icon otherwise.""" + + def __init__(self): + super().__init__(width=BAR_WIDTH) + self.name = "" + self.status_text = "" + self.downloading = False + self.text_color = rl.GRAY + self.icon: str | None = None + self.icon_color: rl.Color | None = None + self._font = gui_app.font(FontWeight.NORMAL) + # raw progress arrives in steps, one per 128KB chunk the manager publishes + self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) + # integrated per frame; (t * speed) % span jumps whenever the fill width changes + self._sweep = 0.0 + + self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + + def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None): + if downloading and not self.downloading: + self._name_label.reset_shimmer() + self._progress.x = progress + self._sweep = 0.0 + self.name = name + self.downloading = downloading + self.status_text = status_text + self.text_color = text_color + self.icon = icon + self.icon_color = icon_color + self._name_label._shimmer = downloading + if downloading: + self._progress.update(progress) + self._sweep += SWEEP_SPEED / gui_app.target_fps + + @property + def _idle_text(self) -> str: + return f"{self.name} - {self.status_text}" if self.status_text else self.name + + def get_width_hint(self) -> float: + if self.downloading: + return BAR_WIDTH + width = measure_text_cached(self._font, self._idle_text, FONT_SIZE).x + if self.icon: + width += ICON_SIZE + ICON_PADDING + return width + + def _render(self, rect: rl.Rectangle): + if self.downloading: + self._render_downloading(rect) + else: + self._render_idle(rect) + + def _sweep_gradient(self, width: float) -> Gradient: + # clearance at both ends keeps the wrap offscreen + center = (self._sweep % (width + 2 * SWEEP_BAND)) - SWEEP_BAND + + def band(x: float) -> float: + return max(0.0, 1.0 - abs(x - center) / SWEEP_BAND) + + # sampling the corners is exact for a piecewise linear band + xs = sorted({0.0, width} | {min(max(center + o, 0.0), width) for o in (-SWEEP_BAND, 0.0, SWEEP_BAND)}, reverse=True) + # the gradient axis runs right-to-left in screen space + stops = [1.0 - x / width for x in xs] + # alpha here is the lift over the SWEEP_DIM base, not the final opacity + colors = [rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * band(x))) for x in xs] + return Gradient(start=(0.0, 0.0), end=(1.0, 0.0), colors=colors, stops=stops) + + @staticmethod + def _capsule(rect: rl.Rectangle) -> np.ndarray: + """Rounded-end ribbon so the gradient covers the caps.""" + r = rect.height / 2 + cy = rect.y + r + top, bottom = [], [] + for i in range(CAPSULE_POINTS): + x = rect.x + rect.width * i / (CAPSULE_POINTS - 1) + d = min(x - rect.x, rect.x + rect.width - x, r) + h = math.sqrt(max(r * r - (r - d) ** 2, 0.0)) + top.append((x, cy - h)) + bottom.append((x, cy + h)) + return np.array(top + bottom[::-1], dtype=np.float32) + + def _draw_fill(self, rail: rl.Rectangle, fill_width: float): + if fill_width <= 0: + return + fill = rl.Rectangle(rail.x, rail.y, fill_width, rail.height) + rl.draw_rectangle_rounded(fill, 1.0, 10, rl.Color(FILL_COLOR.r, FILL_COLOR.g, FILL_COLOR.b, int(255 * SWEEP_DIM))) + draw_polygon(fill, self._capsule(fill), gradient=self._sweep_gradient(fill_width)) + + def _render_downloading(self, rect: rl.Rectangle): + percent = f"{int(self._progress.x)}%" + text_height = measure_text_cached(self._font, percent, FONT_SIZE).y + top = rect.y + (rect.height - (text_height + BAR_GAP + BAR_HEIGHT)) / 2 + + text_rect = rl.Rectangle(rect.x, top, rect.width, text_height) + self._name_label.set_text(self.name) + self._name_label.render(text_rect) + self._percent_label.set_text(percent) + self._percent_label.render(text_rect) + + rail = rl.Rectangle(rect.x, top + text_height + BAR_GAP, rect.width, BAR_HEIGHT) + rl.draw_rectangle_rounded(rail, 1.0, 10, RAIL_COLOR) + self._draw_fill(rail, max(0.0, min(rect.width, rect.width * (self._progress.x / 100.0)))) + + def _render_idle(self, rect: rl.Rectangle): + text = self._idle_text + text_size = measure_text_cached(self._font, text, FONT_SIZE) + right = rect.x + rect.width + + if self.icon: + texture = gui_app.texture(self.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True) + rl.draw_texture_v(texture, rl.Vector2(right - texture.width, rect.y + (rect.height - texture.height) / 2), + self.icon_color or self.text_color) + right -= texture.width + ICON_PADDING + + rl.draw_text_ex(self._font, text, rl.Vector2(right - text_size.x, rect.y + (rect.height - text_size.y) / 2), + FONT_SIZE, 0, self.text_color) + + +def download_status_item(title): + return ListItemSP(title=title, action_item=DownloadStatusAction(), title_color=style.ITEM_TEXT_COLOR) From 2b576c5fce0aebe5e1af3110f2ceba720fd9df72 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 18 Aug 2026 19:46:56 -0400 Subject: [PATCH 265/325] ci: tmp disable ui_report --- .github/workflows/ui_preview.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index 8b7b63f344..c6bc63c8ac 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -25,7 +25,8 @@ env: jobs: preview: - if: github.repository == 'sunnypilot/sunnypilot' + if: false # tmp disable due to GH API rate limiting flakiness + #if: github.repository == 'sunnypilot/sunnypilot' name: preview runs-on: ubuntu-latest timeout-minutes: 20 From 9f1709a7e152be90a3328438adceb99ef88f30f1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 18 Aug 2026 18:29:33 -0700 Subject: [PATCH 266/325] Revert "lfs: exclude big driving model in master clones (#38626)" This reverts commit b7657f6553855a569e30e2a971d6e7043d3badc4. --- .lfsconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/.lfsconfig b/.lfsconfig index e538b9d031..42dfa2d944 100644 --- a/.lfsconfig +++ b/.lfsconfig @@ -1,5 +1,4 @@ [lfs] url = https://gitlab.com/commaai/openpilot-lfs.git/info/lfs pushurl = ssh://git@gitlab.com/commaai/openpilot-lfs.git - fetchexclude = "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" locksverify = false From 03711a13b0a7740f28ac480b42da67a86ed1d1dd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 18 Aug 2026 18:31:07 -0700 Subject: [PATCH 267/325] Revert "chestnut: don't compile if big model is LFS pointer" (#38670) Revert "chestnut: don't compile if big model is LFS pointer (#38655)" This reverts commit b8e14d85fbb5e3da1af7f3596e0d9eb49965056d. --- openpilot/selfdrive/modeld/SConscript | 6 +----- openpilot/selfdrive/modeld/helpers.py | 4 ---- openpilot/system/hardware/hardwared.py | 4 ++-- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 8b2322f94e..30a31aae27 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, big_model_source_available, usbgpu_present, modeld_pkl_path +from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path CAMERA_CONFIGS = [ @@ -44,10 +44,6 @@ tg_devices = { # which device to put jit inputs to at runtime } USBGPU = usbgpu_present() -if USBGPU and not big_model_source_available(): - print("Big model source unavailable, skipping big model build") - USBGPU = False - if USBGPU: usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index ffd59eae06..37ab0b26d7 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -21,10 +21,6 @@ def modeld_pkl_path(usbgpu: bool): prefix = 'big_' if usbgpu else '' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' -def big_model_source_available() -> bool: - model_path = MODELS_DIR / 'big_driving_supercombo.onnx' - return model_path.is_file() and model_path.stat().st_size >= 1024 - def dump_oob(obj, f): with tempfile.TemporaryFile(dir=".") as tmp: def buffer_callback(pb: pickle.PickleBuffer): diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 2817ef3942..a423d8f97d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,7 +16,6 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import usbgpu_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR @@ -238,7 +237,8 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = usbgpu_compiled() + big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) or \ + os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest")) while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From 08c83149b0a61f2e18ba3877439bac34683428ac Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 18 Aug 2026 18:31:59 -0700 Subject: [PATCH 268/325] Pin SCons to 4.10.1 (#38669) --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78fb53ed1d..c1dd803640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "tqdm", # cars (fw_versions.py) on start + many one-off uses # core - "scons", + "scons==4.10.1", # 4.11 removed the qt3 tool still used to build Cabana "pycapnp==2.1.0", # 2.2 introduces a memory leak due to cyclic references "numpy >=2.0", diff --git a/uv.lock b/uv.lock index b9af0d6adf..20fe78c1fb 100644 --- a/uv.lock +++ b/uv.lock @@ -675,7 +675,7 @@ requires-dist = [ { name = "rednose", marker = "extra == 'submodules'", editable = "rednose_repo" }, { name = "requests" }, { name = "ruff", marker = "extra == 'testing'" }, - { name = "scons" }, + { name = "scons", specifier = "==4.10.1" }, { name = "sentry-sdk" }, { name = "setproctitle" }, { name = "sounddevice" }, @@ -923,11 +923,11 @@ wheels = [ [[package]] name = "scons" -version = "4.11.0" +version = "4.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/82/3c4e089ac8df2eaee8a7f14e489b2a76f94f4c1d8defa4e46c8ad15cae86/scons-4.11.0.tar.gz", hash = "sha256:5ba48f9e2eb6b9178cabdc9893792418e6970c84f43f4b027e4468e20616a89c", size = 3269126, upload-time = "2026-08-11T04:29:45.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/ac/a4445bbbd58a5fa6a5c8b3b0458ffbee04e4acaff87677058eab9c6af682/scons-4.11.0-py3-none-any.whl", hash = "sha256:2edc077aaeafc43377ba46ce1fa3e7b40edea59c62db9ef7e39e07dc88b754fa", size = 4123742, upload-time = "2026-08-11T04:29:42.881Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, ] [[package]] From 3c90b66b65777a233b6288ecd009a3806f1ab439 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:40:11 -0400 Subject: [PATCH 269/325] webrtcd: bind to localhost (#38664) check content type and bind to localhost --- openpilot/system/webrtc/tests/test_stream_session.py | 7 ++++++- openpilot/system/webrtc/webrtcd.py | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/openpilot/system/webrtc/tests/test_stream_session.py b/openpilot/system/webrtc/tests/test_stream_session.py index d64cf6aed6..03d540c3c2 100644 --- a/openpilot/system/webrtc/tests/test_stream_session.py +++ b/openpilot/system/webrtc/tests/test_stream_session.py @@ -7,7 +7,7 @@ from openpilot.common.test import OpenpilotTestCase from openpilot.cereal import messaging, log from teleoprtc.tracks import VIDEO_CLOCK_RATE -from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy +from openpilot.system.webrtc.webrtcd import CerealOutgoingMessageProxy, CerealIncomingMessageProxy, ServerState, handle_get_stream from openpilot.system.webrtc.device.video import LiveStreamVideoStreamTrack @@ -80,3 +80,8 @@ class TestStreamSession(OpenpilotTestCase): start_pts = packet.pts assert abs(i + packet.pts - (start_pts + (((time.monotonic_ns() - start_ns) * VIDEO_CLOCK_RATE) // 1_000_000_000))) < 450 #5ms assert bytes(packet) == b"" + + def test_stream_rejects_non_json_content_type(self): + response = self.loop.run_until_complete(handle_get_stream(ServerState(), b"{}", "text/plain")) + + assert response == (415, b'{"error": "unsupported media type"}', "application/json; charset=utf-8") diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 8e022eda1c..3c0c3037f3 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -395,7 +395,10 @@ def _text_response(text: str, status: int = 200) -> tuple[int, bytes, str]: return (status, text.encode(), "text/plain; charset=utf-8") -async def handle_get_stream(state: ServerState, raw_body: bytes) -> tuple[int, bytes, str]: +async def handle_get_stream(state: ServerState, raw_body: bytes, content_type: str) -> tuple[int, bytes, str]: + if content_type != "application/json": + return _json_response({"error": "unsupported media type"}, status=415) + stream_dict = state.streams body = StreamRequestBody(**json.loads(raw_body)) @@ -508,7 +511,7 @@ class WebrtcdHandler(BaseHTTPRequestHandler): services = parse_qs(parsed.query).get("services", [""])[0] result = self._run(handle_get_schema(self.server.state, services)) elif parsed.path == "/stream": - result = self._run(handle_get_stream(self.server.state, self._read_body())) + result = self._run(handle_get_stream(self.server.state, self._read_body(), self.headers.get_content_type())) else: # /notify try: payload = json.loads(self._read_body()) @@ -611,7 +614,7 @@ def webrtcd_thread(host: str, port: int): def main(): parser = argparse.ArgumentParser(description="WebRTC daemon") - parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on") + parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to listen on") parser.add_argument("--port", type=int, default=5001, help="Port to listen on") args = parser.parse_args() From 8edce0da4492307df211c710af6c4ece1c4a218e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 18 Aug 2026 18:40:54 -0700 Subject: [PATCH 270/325] Clean up big model detection w/ helpers (#38671) use helpers --- openpilot/system/hardware/hardwared.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index a423d8f97d..16eb18d153 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,6 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW +from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR @@ -237,8 +238,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx")) or \ - os.path.isfile(os.path.join(BASEDIR, "openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest")) + big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From 7bd6cad821890db71c6ebacc2ccc78c2e662a948 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 18 Aug 2026 19:06:48 -0700 Subject: [PATCH 271/325] Re-open agnos updater UI if crash (#38672) loop if crash --- launch_chffrplus.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index 62ebffd27b..f30e03ca62 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -24,7 +24,9 @@ function agnos_init { if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi - $DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST + while true; do + $DIR/openpilot/common/hardware/comma/updater $AGNOS_PY $MANIFEST + done fi } From 20fdc3d824d86b62d83ea5c1b857b031feee98b6 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:13:57 -0400 Subject: [PATCH 272/325] webrtcd: cloud logging (#38674) * logging * remove test * get rid of redudant try except * fix logger context --- openpilot/system/athena/athenad.py | 8 ++++++-- openpilot/system/webrtc/webrtcd.py | 26 +++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index 828ae6b848..1351f45c22 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -795,7 +795,7 @@ def startStream(sdp: str, enabled: bool) -> dict: bridge_services_in = [] # stale car params case taken care of by webrtcd being shut off on ignition - cp_bytes = Params().get("CarParamsPersistent") + cp_bytes = params.get("CarParamsPersistent") if cp_bytes is not None: with car.CarParams.from_bytes(cp_bytes) as CP: if CP.notCar: @@ -808,7 +808,11 @@ def startStream(sdp: str, enabled: bool) -> dict: # webrtcd clears IsLiveStreaming when the session ends params.put_bool("IsLiveStreaming", True) # wait for webrtcd end points to wake up - wait_for_webrtcd() + try: + wait_for_webrtcd() + except TimeoutError: + cloudlog.event("athena.startStream.webrtcd_offroad_start_timeout", error=True) + raise return post_stream_request(StreamRequestBody(sdp, ["wideRoad"], enabled, bridge_services_in, ["carState", "deviceState"])) diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 3c0c3037f3..56b4ac43ce 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -21,10 +21,16 @@ from typing import Any from openpilot.system.webrtc.helpers import StreamRequestBody from openpilot.system.webrtc.schema import generate_field from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog from openpilot.cereal import messaging, log SESSION_TIMEOUT_SECONDS = 300 + +# ice candidate parser for logging +def _ice_candidates(sdp: str) -> list[str]: + return [line.removeprefix("a=") for line in sdp.splitlines() if line.startswith("a=candidate:")] + # socket trick: route lookup for 8.8.8.8 (nothing is sent or actually connected to) # return the source interfaces IP which is the default interface of the device def _default_route_ip() -> str | None: @@ -253,7 +259,7 @@ class StreamSession: self._cleanup_lock = asyncio.Lock() self._cleanup_done = False self.logger = logging.getLogger("webrtcd") - self.logger.info( + cloudlog.warning( "New stream session (%s), video cameras %s, video enabled %s, incoming services %s, outgoing services %s", self.identifier, [t.id for t in self.video_tracks], body.enabled, body.bridge_services_in, body.bridge_services_out, ) @@ -341,14 +347,18 @@ class StreamSession: if self.bitrate_controller is not None: self.bitrate_controller.start() - self.logger.info("Stream session (%s) connected", self.identifier) + with cloudlog.ctx(session_id=self.identifier): + cloudlog.warning("webrtcd.session.connected") if self.is_body: await self.run_body_session() else: await self.run_normal_session() - self.logger.info("Stream session (%s) ended", self.identifier) + with cloudlog.ctx(session_id=self.identifier): + cloudlog.warning("webrtcd.session.ended") except Exception: self.logger.exception("Stream session failure") + with cloudlog.ctx(session_id=self.identifier): + cloudlog.exception("webrtcd.session.exception") finally: await self.post_run_cleanup() @@ -422,15 +432,25 @@ async def handle_get_stream(state: ServerState, raw_body: bytes, content_type: s stream_dict[session.identifier] = session try: answer = await asyncio.wait_for(session.get_answer(), timeout=30) + cloudlog.event( + "webrtcd.session.ice_candidates", + session_id=session.identifier, + offer_candidates=_ice_candidates(body.sdp), + answer_candidates=_ice_candidates(answer.sdp), + ) except TimeoutError: await session.stop() stream_dict.pop(session.identifier, None) logging.getLogger("webrtcd").exception("Timed out creating stream answer") + with cloudlog.ctx(session_id=session.identifier): + cloudlog.warning("webrtcd.session.answer_timeout") raise except Exception: await session.stop() stream_dict.pop(session.identifier, None) logging.getLogger("webrtcd").exception("Failed to create stream answer") + with cloudlog.ctx(session_id=session.identifier): + cloudlog.exception("webrtcd.session.answer_exception") raise session.start() From 5b36799eec73ee9d630ccf8304c43dccf8fe7a28 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:56:33 -0400 Subject: [PATCH 273/325] webrtc: fix message handler race (#38675) open message handler early --- openpilot/system/webrtc/webrtcd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpilot/system/webrtc/webrtcd.py b/openpilot/system/webrtc/webrtcd.py index 56b4ac43ce..9481e077ab 100755 --- a/openpilot/system/webrtc/webrtcd.py +++ b/openpilot/system/webrtc/webrtcd.py @@ -335,9 +335,12 @@ class StreamSession: async def run(self): try: self.params.put("LivestreamRequestKeyframe", True) + + # avoid datachannel race by adding messange_handler immediately + self.stream.set_message_handler(self.message_handler) + await asyncio.wait_for(self.stream.wait_for_connection(), timeout=15) if self.stream.has_messaging_channel(): - self.stream.set_message_handler(self.message_handler) if self.incoming_bridge is not None: await self.shared_pub_master.add_services_if_needed(self.incoming_bridge_services) if self.outgoing_bridge is not None: From a8d1a280c665dcca7923782f8bfdf15c7831bdda Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:49:19 -0400 Subject: [PATCH 274/325] webrtcd/athenad: we don't have to fail on no car params (#38678) we don't have to fail on no car params --- openpilot/system/athena/athenad.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpilot/system/athena/athenad.py b/openpilot/system/athena/athenad.py index 1351f45c22..2b67638776 100755 --- a/openpilot/system/athena/athenad.py +++ b/openpilot/system/athena/athenad.py @@ -800,8 +800,6 @@ def startStream(sdp: str, enabled: bool) -> dict: with car.CarParams.from_bytes(cp_bytes) as CP: if CP.notCar: bridge_services_in.append("testJoystick") - else: - raise Exception("failed to get CarParamsPersistent") if params.get_bool("IsOffroad"): # manager owns camerad/stream_encoderd/webrtcd; flip the param and let it bring them up. From dcf9d25bf36f335bc9d5b1618f20f0e53aa22238 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:02:03 -0400 Subject: [PATCH 275/325] webrtcd: more descriptive errors (#38677) more descriptive errors --- openpilot/system/webrtc/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/system/webrtc/helpers.py b/openpilot/system/webrtc/helpers.py index de45e1c6c9..87fe20eb20 100644 --- a/openpilot/system/webrtc/helpers.py +++ b/openpilot/system/webrtc/helpers.py @@ -23,9 +23,9 @@ def post_stream_request(body: StreamRequestBody) -> dict: ret["time"] = (t_end - t_start) * 1000 return ret except requests.ConnectTimeout as e: - raise Exception("webrtc took too long to respond.") from e + raise Exception("device took too long to respond.") from e except requests.ConnectionError as e: - raise Exception("webrtc server on device is not running.") from e + raise Exception("turn car ignition off to use livestreaming.") from e def wait_for_webrtcd(max_retries: float = 10) -> None: @@ -37,4 +37,4 @@ def wait_for_webrtcd(max_retries: float = 10) -> None: except requests.ConnectionError: attempts += 1 time.sleep(0.5) - raise TimeoutError("webrtcd did not initialize in time.") + raise TimeoutError("livestreaming service did not initialize in time.") From 555f48c5d28709f039b79f3f6105e51305edd4b5 Mon Sep 17 00:00:00 2001 From: stef <19478336+stefpi@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:21:59 -0400 Subject: [PATCH 276/325] params: remove livestream param on ignition (#38679) * remove livestream param on ignition * simplify process config --- openpilot/common/params_keys.h | 2 +- openpilot/system/manager/process_config.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index e44d8f8e34..2a49690b7e 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -59,7 +59,7 @@ inline static std::unordered_map keys = { {"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsEngaged", {PERSISTENT, BOOL}}, {"IsLdwEnabled", {PERSISTENT, BOOL}}, - {"IsLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsLiveStreaming", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, {"IsMetric", {PERSISTENT, BOOL}}, {"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, {"IsRhdDetected", {PERSISTENT, BOOL}}, diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index 32d8508696..b8a1e4a12e 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -67,15 +67,12 @@ def or_(*fns): def and_(*fns): return lambda *args: operator.and_(*(fn(*args) for fn in fns)) -def not_(*fns): - return lambda *args: operator.not_(*(fn(*args) for fn in fns)) - procs = [ DaemonProcess("manage_athenad", "openpilot.system.athena.manage_athenad", "AthenadPid"), NativeProcess("loggerd", "openpilot/system/loggerd", ["./loggerd"], logging), NativeProcess("encoderd", "openpilot/system/loggerd", ["./encoderd"], only_onroad), - NativeProcess("stream_encoderd", "openpilot/system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)), + NativeProcess("stream_encoderd", "openpilot/system/loggerd", ["./encoderd", "--stream"], or_(livestream, notcar)), PythonProcess("logmessaged", "openpilot.system.logmessaged", always_run), NativeProcess("camerad", "openpilot/system/camerad", ["./camerad"], or_(driverview, livestream), enabled=not WEBCAM), @@ -119,7 +116,7 @@ procs = [ # debug procs NativeProcess("bridge", "openpilot/cereal/messaging", ["./bridge"], notcar), - PythonProcess("webrtcd", "openpilot.system.webrtc.webrtcd", or_(and_(livestream, not_(iscar)), notcar)), + PythonProcess("webrtcd", "openpilot.system.webrtc.webrtcd", or_(livestream, notcar)), PythonProcess("joystick", "openpilot.tools.joystick.joystick_control", and_(joystick, iscar)), ] From 53e13a7bc0c38f53b1f11a50cfa346d4f51c39c9 Mon Sep 17 00:00:00 2001 From: Robin Dittrich <43466794+RWL-Dittrich@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:36:37 +0200 Subject: [PATCH 277/325] LagdToggle: fix inverted get_lat_delay branches (#1906) * helpers.py get_lat_delay fix * fix trailing whitespace * lint --------- Co-authored-by: Nayan --- openpilot/sunnypilot/livedelay/helpers.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpilot/sunnypilot/livedelay/helpers.py b/openpilot/sunnypilot/livedelay/helpers.py index 0f7437ceea..7b36b57af1 100644 --- a/openpilot/sunnypilot/livedelay/helpers.py +++ b/openpilot/sunnypilot/livedelay/helpers.py @@ -8,7 +8,10 @@ from openpilot.common.params import Params def get_lat_delay(params: Params, stock_lat_delay: float) -> float: - if params.get_bool("LagdToggle"): - return float(params.get("LagdValueCache", return_default=True)) +# live learning on: use what lagd publishes. +# off: use the fixed steerActuatorDelay + software delay sum that LagdToggle caches. - return stock_lat_delay + if params.get_bool("LagdToggle"): + return stock_lat_delay + + return float(params.get("LagdValueCache", return_default=True)) From c783f2225adc7b1bf9515fb35cf225cf9f90e59b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:44:15 -0400 Subject: [PATCH 278/325] [bot] Update Python packages (#1925) Update Python packages Co-authored-by: github-actions[bot] --- opendbc_repo | 2 +- uv.lock | 110 +++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index abab7a1690..06743dfb39 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit abab7a16903a0da7be42128af124f27c977e2617 +Subproject commit 06743dfb39cff0f0cd5ddae244afef42891f4b93 diff --git a/uv.lock b/uv.lock index a7967e8e11..b12a3283a1 100644 --- a/uv.lock +++ b/uv.lock @@ -381,20 +381,20 @@ wheels = [ [[package]] name = "deepmerge" -version = "2.1.0" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/6c/9f4577a36d5f463a3a3f8322bd65d33e1a1a6b6ba1d692a5ebc3cba19015/deepmerge-3.0.tar.gz", hash = "sha256:14ed69f063de64b7743985c732ccff5d6c34ff4560946e7fbfd99086b853b9ce", size = 22279, upload-time = "2026-08-17T05:50:53.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d7/7f19bedd30b90b72865aeec3a29127bed6dee6c9ef0324bb5b4d424bb0e3/deepmerge-3.0-py3-none-any.whl", hash = "sha256:c8541c3e186dc88d19a5513ad3a0b2d0b22beaa780969fc0c13b995a64265365", size = 14855, upload-time = "2026-08-17T05:50:52.218Z" }, ] [[package]] name = "filelock" -version = "3.32.2" +version = "3.32.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" }, ] [[package]] @@ -478,7 +478,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -491,18 +491,18 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/ae/222a91937ebee7f62c0ca8f5ee0afd97577caf24c0abb927d1f5c7e9f6d2/huggingface_hub-1.28.0.tar.gz", hash = "sha256:46a2e950c09234de54093d587d1675382f0d08dbd600d9fb599b5932f5b2c6cb", size = 959609, upload-time = "2026-08-18T12:27:15.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/eafef18f1a75e125e68395db21131db0cf868a128ecd2fce69b4df6c584b/huggingface_hub-1.28.0-py3-none-any.whl", hash = "sha256:58a8bacb03072edfc38067065e9dc24bbb34805410fcd36a1632de0b329660bb", size = 793202, upload-time = "2026-08-18T12:27:12.719Z" }, ] [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -971,11 +971,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] @@ -1194,18 +1194,18 @@ wheels = [ [[package]] name = "sounddevice" -version = "0.5.5" +version = "0.5.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/db/0c890e2d9aab9ba284021efc02e1d3aebfecab1b611762d7434602209bcf/sounddevice-0.5.6.tar.gz", hash = "sha256:8ec9fbfde2e32f020b167e348f3ab3bac6625a5f15af524d790108ac7147a410", size = 1120094, upload-time = "2026-08-17T07:55:05.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, - { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" }, - { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" }, + { url = "https://files.pythonhosted.org/packages/72/1f/62eef605172bddc1017508469a12f75bc7c4194ece35c734f822795f53b1/sounddevice-0.5.6-py3-none-any.whl", hash = "sha256:de099612311ad81e55d31ccbd83f43ea6bf4d87b48f9b6ea55a1fbcde0eee4e0", size = 32793, upload-time = "2026-08-17T07:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/b6/84/85e719d49cf98b2f406d9ac9c338892286c4448eb42ef0b2625ccf159616/sounddevice-0.5.6-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:e3aef00ad8b1d1740eb66d9a7671eab88a4d2b8fa4ab33498d742e63b65c309c", size = 1009647, upload-time = "2026-08-17T07:54:58.814Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6f/6292145099f72a153a710245f46ae43e5fb6c77bec1b6086cb76c12dc280/sounddevice-0.5.6-py3-none-win32.whl", hash = "sha256:b36b807eb02abd257198bf84b2af05e4fea199a9d2f0019014169c7136d45e9c", size = 1009627, upload-time = "2026-08-17T07:55:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3e/cbc593c31a5f0d817b3fe97e64aa8461bd0f55cb07b67ce1b776296ae336/sounddevice-0.5.6-py3-none-win_amd64.whl", hash = "sha256:7f4162f514f007b0bf25a3ccfed3f1705bc2ec311888a90232729eec4f57a4f4", size = 1009630, upload-time = "2026-08-17T07:55:02.088Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/b0c21c9f215a6fd9606b8f8748c21212dc098e5d5a2d93068c50edcf19b4/sounddevice-0.5.6-py3-none-win_arm64.whl", hash = "sha256:c8ae19173e5f27f8c12d4b5eee2dbfe542cee125d591e663e0fb4dfb75246d45", size = 1009630, upload-time = "2026-08-17T07:55:03.689Z" }, ] [[package]] @@ -1335,27 +1335,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.72" +version = "0.0.73" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/90/c4e1bb4cead3b644c3e258a27f9b05c7dc5eb0ec96a4f5282194edae9e0d/ty-0.0.73.tar.gz", hash = "sha256:823d4ce0d237bfc7eb6bcee70842f2c0706113813a16951077840743712f4b74", size = 6712739, upload-time = "2026-08-19T03:12:43.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" }, - { url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" }, - { url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" }, - { url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" }, - { url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" }, - { url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" }, - { url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" }, - { url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f5e1801e55cc631f2db193276675b30561b963a2403da832bffb5d100267/ty-0.0.73-py3-none-linux_armv6l.whl", hash = "sha256:90a946082bf9bc446b5e72973d9f4ff1222a240b2ca4c9e6eed61eb913e30810", size = 12715452, upload-time = "2026-08-19T03:12:06.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/32/515dd05074c213b433524ab97eb003b0132ae7e358e0d75633ba7a314ed8/ty-0.0.73-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b7d6b5c6a6db7ea95fbbc16af514ef44a27a29a2fe1dc798900790364d170209", size = 12301870, upload-time = "2026-08-19T03:12:08.924Z" }, + { url = "https://files.pythonhosted.org/packages/50/4d/085b4889f0d4bbe4af8b96242d4a1cb209fff95967cfa239ea141983719b/ty-0.0.73-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dd6f657f463e01372d8688f235be164750c8db722c97da27fa4903aa8d40b203", size = 12111741, upload-time = "2026-08-19T03:12:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/95/f6/d6ec277cadfecf03ad4c18551b67c4c6eb7807a0560d801db14be99d7a89/ty-0.0.73-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2de468e33fd44c9ff1c43473a7316f4289480f5cba8995a67b6d22aee39ca9", size = 12196124, upload-time = "2026-08-19T03:12:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/ce78d8707563af9cae9bbd25328bfbc4931035085bd20089adf0c418f70e/ty-0.0.73-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2942fa0ef795a66034cdc8d75a72f453442f3b58ff2f69b4da05b7b954765b55", size = 12488557, upload-time = "2026-08-19T03:12:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e8/329b9851b23502758c5c98e8cc875ea2a1b4c9674b4ca3a86da56a5063d3/ty-0.0.73-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e0f1ef14f642e18ac4e7a616a2796dcf7a5d82e28cd17f9796494acc7c4aabb", size = 13215606, upload-time = "2026-08-19T03:12:17.225Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/67fedfd2cb77516ef0066b1642f487dba0eb3006493cf3475b15f5b8b228/ty-0.0.73-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16981e15fdceedb37d0aff76c5ac25914595dfee2675af95335550064251ad22", size = 13665497, upload-time = "2026-08-19T03:12:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b3/154f4dd48ec5eebc186ab4b822c6e62f982fc5ddfd262d6e3903c2acba44/ty-0.0.73-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644b2bec8a2e2e4957a942ae81d6cff5571c489bb5a8675e4d3886de537a694d", size = 13351231, upload-time = "2026-08-19T03:12:21.353Z" }, + { url = "https://files.pythonhosted.org/packages/35/5f/d462496903fbe453fb76363f8478be929c8e6ff21e6928c57dcd7e5fa21f/ty-0.0.73-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338d565be3186f50ff8e9d10483685549c2d23f0754485d5ede3b54f4319188a", size = 12782586, upload-time = "2026-08-19T03:12:23.667Z" }, + { url = "https://files.pythonhosted.org/packages/87/52/ec6d24b74abe3ec324204c1c71e6d0c6c76a17ffc15fd51d603b0a302abe/ty-0.0.73-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:11c7b6d839309d2c102cb3a4c03d817176bbfab5b2fccc95a75ec5c9597421c9", size = 13247134, upload-time = "2026-08-19T03:12:25.956Z" }, + { url = "https://files.pythonhosted.org/packages/26/20/cc74650fec56a54786c6d7c89e09576fcad3092be34cf21715d39a406a9b/ty-0.0.73-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:488572db7ff97fb50ea36a76250f2d617c9727d143da6c7bf0623276eb0fc507", size = 12309344, upload-time = "2026-08-19T03:12:28.122Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/4b0a9087f4315d7fbadf77a3ce44c816cc9ffabed1ced06cc5be81fbc414/ty-0.0.73-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1b958ebceefbbf594e59eb8d3d55bbd033ce634026fcba3e4bc3179e78e45bb7", size = 12502319, upload-time = "2026-08-19T03:12:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/11/80/0a925074911fe111912ea29d9eed309bcc183f43d2fb3eef07db056a0beb/ty-0.0.73-py3-none-musllinux_1_2_i686.whl", hash = "sha256:91a32993b3c34e42c3f323ad6c0399cb596bd1c27e9b7f20db7cd64c1067b68e", size = 12753688, upload-time = "2026-08-19T03:12:32.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/aeccaf89efbc2e112bd415340a22e2669ec998aa397242503e747b712ca4/ty-0.0.73-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bab8a19fbf51f479bddb2a12c5fabfe52f918a5590362321ed5d89b44eb62c15", size = 13069050, upload-time = "2026-08-19T03:12:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3e/eae485fd86c1585943fd4e1746b0757b2da01e2c43136ebe8c686fe1c7f1/ty-0.0.73-py3-none-win32.whl", hash = "sha256:03347a612f0fa020b19bfd8dbd521db6ecc75d377a3e4d4f6e6c2e62871da4cc", size = 12053187, upload-time = "2026-08-19T03:12:37.565Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/9b8b983786e3ce34924e372e8b76b92b508273ab65c589fc7e88cc03ee17/ty-0.0.73-py3-none-win_amd64.whl", hash = "sha256:cedd05122ded0b5dcc55431a370e974b747f99c41c290a3d2ab8c1867f197519", size = 12693838, upload-time = "2026-08-19T03:12:39.483Z" }, + { url = "https://files.pythonhosted.org/packages/ea/88/25333bbfea6a5dc064371d2002d3d4807db90b84d5448f9106b2712b0fbc/ty-0.0.73-py3-none-win_arm64.whl", hash = "sha256:e47068f8369dea5d641a26a2ad0a947a320b02ff87099b07e95de0323245a4dc", size = 12443573, upload-time = "2026-08-19T03:12:41.449Z" }, ] [[package]] @@ -1387,7 +1387,7 @@ wheels = [ [[package]] name = "zensical" -version = "0.0.54" +version = "0.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1399,20 +1399,20 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/7e/343a78c0c9da1954d2f0a4d47ca778baac48bb18c6b9c0c6260e7974976e/zensical-0.0.54.tar.gz", hash = "sha256:4de205dbb323d0a443e2ebf3fef77e93e3c1493c34a58d205e7f3631dd7745af", size = 3992024, upload-time = "2026-08-13T16:04:49.297Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/7d/18bb725a659352e9af0940a3d879c5edcff5d86fec3ac15ce500d484d9d3/zensical-0.0.56.tar.gz", hash = "sha256:c359163800d1c3a8c39af48f4e2869fcfc2b4fc00d28652bd2a5b0330c36530c", size = 3997416, upload-time = "2026-08-18T15:46:47.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/cf/13e887c303fd5c786c09f83362382a42d10292fca633a95067de6a6591a3/zensical-0.0.54-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7177a3b6647e4ee47864ab02e5141f9e923dd57b9fa96e5dc5da4228daff505", size = 12893082, upload-time = "2026-08-13T16:04:17.337Z" }, - { url = "https://files.pythonhosted.org/packages/82/ee/7fe1418fa31bc120cf9eb0fbce9e021c40752206ce993a7bc652c011f890/zensical-0.0.54-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7b23c3b0c720885891b220c3e562074d93eb940314823d91875c6246976bd9b2", size = 12778626, upload-time = "2026-08-13T16:04:19.906Z" }, - { url = "https://files.pythonhosted.org/packages/61/b8/0420115270c1a22a2d4a1598f89dadc4e933eb7aa85539b72f3532acc5b6/zensical-0.0.54-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c012eb0ec20fda5794e90b4906b7401c77b712e2acc7f9f65026935368539da", size = 13225462, upload-time = "2026-08-13T16:04:22.663Z" }, - { url = "https://files.pythonhosted.org/packages/8d/48/0dfdd3e00fb3b807de702383ef5f4e9cf0d2791fee0e8a56f39bd590f16b/zensical-0.0.54-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0e851d26ba4f7397db3b3532e534c0572a9826bbd451ee0a00e649c030dcb38", size = 13158184, upload-time = "2026-08-13T16:04:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/f1fb1f5387108b206f985cdb394bbdc730557cc339e63067ded9b3565263/zensical-0.0.54-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48583da6f485e2373845d4332882c9e9d504302dbd2731c589543584df5b087", size = 13536772, upload-time = "2026-08-13T16:04:27.373Z" }, - { url = "https://files.pythonhosted.org/packages/a3/97/3224b3dd5d76cebddf9725ae871a5a6a8f2de177be59d802232d00073d8b/zensical-0.0.54-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7781a906623fb7bc278cc874ec6f80691c8753b90fbdf90a451abb046ae204", size = 13191901, upload-time = "2026-08-13T16:04:30.295Z" }, - { url = "https://files.pythonhosted.org/packages/b2/00/b1f55530c8df4331e1f209ec605816a578142f124b5f8078a9d984514d63/zensical-0.0.54-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:75aff1c01f6104dd0e79d08c87bd595da13db3e1f6da55fc9933ebff3e94fdd5", size = 13402956, upload-time = "2026-08-13T16:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/c7/6e/58df496c2742600df3a2f273ba52b8ced1ce357340b825af9f1bb0f2042e/zensical-0.0.54-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1469c5d551a0ea2d0fcf922046a263d2afcff5f03ce3bb443e286970d04ff9f2", size = 13431462, upload-time = "2026-08-13T16:04:35.518Z" }, - { url = "https://files.pythonhosted.org/packages/5b/85/70ae775db7865be2434bc39e9b4ff1c7988978d796a7ff9d49294e7410c7/zensical-0.0.54-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:31c354f98b3374b9bcab65278a278dbec66125c997f7f1a5ee9501f413b87b9c", size = 13587289, upload-time = "2026-08-13T16:04:37.954Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/05e6b3e04323810bbf4da9240b8b710740fac82ec01659b14c86e44aa88e/zensical-0.0.54-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85ef75654e7845aa65f1392af771771566f33faac452a2aed004ba367454f812", size = 13534594, upload-time = "2026-08-13T16:04:41.006Z" }, - { url = "https://files.pythonhosted.org/packages/14/70/be34910c13632f85911f505bd9a4d1bb53d46c8498d20ebb18570bbe4b7b/zensical-0.0.54-cp310-abi3-win32.whl", hash = "sha256:b56c80a8cd234666afb917fb1ed8467104f6857b1acabd66f60ef1b8a0daa66f", size = 12448180, upload-time = "2026-08-13T16:04:43.486Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c6/a965265946555023dc0e159a41038502190882669d177dfa59b1dd3d580b/zensical-0.0.54-cp310-abi3-win_amd64.whl", hash = "sha256:f5a602986c4123a349cfd075c8d494096a2a2db74422169d65f8092872e48dfa", size = 12712264, upload-time = "2026-08-13T16:04:46.155Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4b/6810aec6e670451f39639039b570a43d90bc1d4a3b93cf13316ccf6bad11/zensical-0.0.56-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5135ea3aa5d1358503fc1903e866c191873b138028bc1ab170abac3a4b537ffe", size = 12874712, upload-time = "2026-08-18T15:46:18.378Z" }, + { url = "https://files.pythonhosted.org/packages/97/98/23445d8ed708088dd6d9d51674f8836b77c53aab280360d7aa7a206bea8e/zensical-0.0.56-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a22ae2329ba755c6e58e1fe5967ca429d580d346a73cf467ad5185377e2cf809", size = 12764552, upload-time = "2026-08-18T15:46:20.746Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ab89450728b0a55e6a52a86060b38a362a52a1b9f02eda7fef68b729eb2e/zensical-0.0.56-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03c70ef328e31cce0e31739acdd6679e9272bc7a3016ca5eafaa16dcfda460c9", size = 13212920, upload-time = "2026-08-18T15:46:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5a/969fd9a461204392a9266a544c185fbb223714b306534661dcbb7d290be7/zensical-0.0.56-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ffc50153a50292078357a5052e7a6b3e20a818523792ffcbeace70418b761eb", size = 13146652, upload-time = "2026-08-18T15:46:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/28a7c8dea8fe2edbba75a664efbde797b5922651ea92ffc480947ab22197/zensical-0.0.56-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88bba0e339d36647ce638a42b4ba96f2521b59ea54be74c314340be7e401f94b", size = 13530946, upload-time = "2026-08-18T15:46:28.19Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/d55a18c6e041b788af1d19e4d8c61ee9b8a7de5b9cc79ffa7bc9565e3a28/zensical-0.0.56-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b7a37f6ac38ac218e3e0dd311b58c468603963e38ee23f2e25672dcd6e9c17b", size = 13178741, upload-time = "2026-08-18T15:46:30.378Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/a07da2f761cfd6a27faf9e8d5a9475c396a9a0ca37424a0b67cab7ae4d2c/zensical-0.0.56-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f6d850bce3184422b37b9d98ef4d123047a47446844793251cabc04bd55f8f0", size = 13389836, upload-time = "2026-08-18T15:46:32.847Z" }, + { url = "https://files.pythonhosted.org/packages/34/aa/697ef9846b0e2071de4d03b0472e2658380ea9271bd01235f4ffaeef1978/zensical-0.0.56-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d18944a4a111050b8f571e73d4e2475d7ce62ce78b64f09bcf33bb11cc346e1c", size = 13419551, upload-time = "2026-08-18T15:46:35.112Z" }, + { url = "https://files.pythonhosted.org/packages/97/6b/20e1b2443951d5182b3fc2ee54c200ea00c09bd8901a479be4147c94361b/zensical-0.0.56-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3937029ec5091d577c2a05ebccd38fde97fab28662d165ead66d566689b2a6df", size = 13579878, upload-time = "2026-08-18T15:46:37.381Z" }, + { url = "https://files.pythonhosted.org/packages/53/c7/9c3b400b8a7d78cc169b7a78d4f74a90f9114209034a604f04051f48037c/zensical-0.0.56-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:346a1cdbad157633d185f79039a97c8b4f8c2b40be7d7efd56d72622f40247b0", size = 13526142, upload-time = "2026-08-18T15:46:39.949Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/7a6f2a513054071e44a504d7157684f00ac5e5e5f741eadc78ab6c80642c/zensical-0.0.56-cp310-abi3-win32.whl", hash = "sha256:a06681046f74b5bdc506d22af8fcef505b2450e45538c7f01a5e028f4e50c6f7", size = 12433218, upload-time = "2026-08-18T15:46:42.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/5b497c75fb1fd5845f3ec2f0b11b5e01f18f14c297041cd2f20d30d8b6a4/zensical-0.0.56-cp310-abi3-win_amd64.whl", hash = "sha256:c557985f12d042c15dcb7d577543d81ceee9281f96d591d265310b673437a325", size = 12702823, upload-time = "2026-08-18T15:46:44.836Z" }, ] [[package]] From 049d225d5aec12ff44200b12ca700a504d1ddb52 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:18:17 -0700 Subject: [PATCH 279/325] ci: Dedicated Model Runner (#1922) * ci: Dedicated Model Runner * recurse * not needed * fix wrapper * whoops * bypass * modeld_v2: restore chestnut link check before big model build * modeld_v2: stage onnx to disk instead of shared memory * ci: clear unchunked onnx temps before model build * ci: stream the pkl hash instead of loading it into memory --------- Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 45 ++++++++++++------- .../sunnypilot/modeld_v2/compile_modeld.py | 23 +++++----- .../modeld_v2/tests/test_compile_modeld.py | 37 ++++++++++++++- release/ci/model_generator.py | 24 +++++----- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 17981d68e5..50d216be31 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -103,10 +103,10 @@ jobs: - run: | cd ${{ github.workspace }}/openpilot/openpilot if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then - git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx" + git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx" -X "**/selfdrive/modeld/models/dmonitoring_*.onnx" rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx else - git lfs pull -I "selfdrive/modeld/models/big_*.onnx" + git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete fi - name: 'Upload Artifact' @@ -116,7 +116,7 @@ jobs: path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx build_model: - runs-on: [self-hosted, tici] + runs-on: [self-hosted, usbgpu] needs: get_model env: MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) @@ -127,7 +127,6 @@ jobs: fetch-depth: 1 submodules: recursive - - run: git lfs pull - name: Set environment variables id: set-env @@ -160,7 +159,7 @@ jobs: fi source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable - rm -rf ${{ env.MODELS_DIR }}/*.onnx + rm -rf ${{ env.MODELS_DIR }}/*.onnx* - name: Download model artifacts uses: actions/download-artifact@v4 @@ -180,6 +179,7 @@ jobs: MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then echo "USBGPU build" export USBGPU=1 @@ -187,27 +187,40 @@ jobs: OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" else echo "QCOM build" - TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + TG_FLAGS="$TG_FLAGS_QCOM" OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl" fi # Generate metadata for all ONNX files find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do echo "Generating metadata: $onnx_file" - env ${TG_FLAGS} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true + env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true done # Detect model type and build compile args - VISION_ONNX="${{ env.MODELS_DIR }}/driving_vision.onnx" - POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx" - OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx" - ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx" + VISION_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do + [ -f "$f" ] && VISION_ONNX="$f" && break + done + + POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do + [ -f "$f" ] && POLICY_ONNX="$f" && break + done + + OFF_POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do + [ -f "$f" ] && OFF_POLICY_ONNX="$f" && break + done + + ON_POLICY_ONNX="" + for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do + [ -f "$f" ] && ON_POLICY_ONNX="$f" && break + done + SUPERCOMBO_ONNX="" - for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do - if [ -f "$f" ]; then - SUPERCOMBO_ONNX="$f" - break - fi + for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do + [ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break done MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index 927e5f3392..85ae57c078 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -272,18 +272,17 @@ def _parse_size(size_str: str) -> tuple[int, int]: return int(width), int(height) -def read_file_chunked_to_shm(path): +def read_file_chunked_to_disk(path): if not path: return None import atexit import shutil from openpilot.common.file_chunker import open_file_chunked - from openpilot.common.hardware.hw import Paths - shm_path = os.path.join(Paths.shm_path(), os.path.basename(path)) - atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path)) - with open(shm_path, 'wb') as dst, open_file_chunked(path) as src: - shutil.copyfileobj(src, dst) - return shm_path + tmp_path = f'{path}.unchunked' + with open(tmp_path, 'wb') as f, open_file_chunked(path) as src: + shutil.copyfileobj(src, f) + atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path)) + return tmp_path def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: @@ -327,11 +326,11 @@ if __name__ == "__main__": model_w, model_h = args.model_size output_data = {} - args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx) - args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx) - args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx) - args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx) - args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx) + args.vision_onnx = read_file_chunked_to_disk(args.vision_onnx) + args.policy_onnx = read_file_chunked_to_disk(args.policy_onnx) + args.off_policy_onnx = read_file_chunked_to_disk(args.off_policy_onnx) + args.on_policy_onnx = read_file_chunked_to_disk(args.on_policy_onnx) + args.supercombo_onnx = read_file_chunked_to_disk(args.supercombo_onnx) vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py index 885696b853..96bfb42638 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py @@ -5,10 +5,15 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import os +import tempfile +from pathlib import Path + import numpy as np from openpilot.common.parameterized import parameterized -from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key +from openpilot.common.file_chunker import chunk_file, get_chunk_targets +from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key, read_file_chunked_to_disk from openpilot.common.test import OpenpilotTestCase @@ -160,3 +165,33 @@ class TestOutputSlicePreservation(OpenpilotTestCase): policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)} assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \ "vision and policy slices should not overlap in keys" + + +class TestReadFileChunkedToDisk(OpenpilotTestCase): + def test_none_passthrough(self): + assert read_file_chunked_to_disk(None) is None + + def test_unchunked_source_staged_on_disk(self): + with tempfile.TemporaryDirectory() as d: + src = Path(d) / "driving_supercombo.onnx" + payload = os.urandom(1024) + src.write_bytes(payload) + + out = Path(read_file_chunked_to_disk(str(src))) + + assert out.parent == Path(d) + assert out.name == "driving_supercombo.onnx.unchunked" + assert out.read_bytes() == payload + + def test_chunked_source_reassembled_on_disk(self): + with tempfile.TemporaryDirectory() as d: + src = Path(d) / "driving_supercombo.onnx" + payload = os.urandom(4096) + src.write_bytes(payload) + chunk_file(str(src), get_chunk_targets(str(src), len(payload))) + assert not src.exists() + + out = Path(read_file_chunked_to_disk(str(src))) + + assert out.parent == Path(d) + assert out.read_bytes() == payload diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 607260f145..80b102a6f1 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -53,24 +53,28 @@ def create_pkl_name(full_name: str) -> str: return pkl -def _read_pkl_bytes(pkl_path: Path) -> bytes: +def _hash_pkl(pkl_path: Path) -> str: manifest = Path(f"{pkl_path}.chunkmanifest") if manifest.exists(): num_chunks = int(manifest.read_text().strip()) - parts = [] - for i in range(num_chunks): - chunk = Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") - parts.append(chunk.read_bytes()) - return b''.join(parts) - return pkl_path.read_bytes() + paths = [Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") for i in range(num_chunks)] + else: + paths = [pkl_path] + + digest = hashlib.sha256() + for path in paths: + with path.open('rb') as f: + while block := f.read(1024 * 1024): + digest.update(block) + return digest.hexdigest() def _find_driving_pkl(output_path: Path) -> Path | None: - for pattern in ('driving_tinygrad.pkl', 'driving_*_tinygrad.pkl'): + for pattern in ('*driving_tinygrad.pkl', '*driving_*_tinygrad.pkl'): matches = sorted(output_path.glob(pattern)) if matches: return matches[0] - for pattern in ('driving_tinygrad.pkl.chunkmanifest', 'driving_*_tinygrad.pkl.chunkmanifest'): + for pattern in ('*driving_tinygrad.pkl.chunkmanifest', '*driving_*_tinygrad.pkl.chunkmanifest'): matches = sorted(output_path.glob(pattern)) if matches: return Path(str(matches[0]).removesuffix('.chunkmanifest')) @@ -87,7 +91,7 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path: def generate_chunked_model(driving_pkl: Path) -> dict: - tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest() + tinygrad_hash = _hash_pkl(driving_pkl) chunks_config = [] manifest_file = Path(f"{driving_pkl}.chunkmanifest") From be76a88b805f3c260b58ab515f519660937bd704 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 20 Aug 2026 19:12:59 -0400 Subject: [PATCH 280/325] ci override LFS fetch exclude for real ONNX file retrieval (#1928) ci: override lfs.fetchexclude so the model fetch pulls real ONNX files instead of pointers --- .github/workflows/sunnypilot-build-model.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 50d216be31..bc132ae1bf 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -103,17 +103,22 @@ jobs: - run: | cd ${{ github.workspace }}/openpilot/openpilot if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then - git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx" -X "**/selfdrive/modeld/models/dmonitoring_*.onnx" + git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx" rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx else - git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" + git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X "" find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete fi + if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then + echo "::error::the ONNX files above are still LFS pointers, not real models" + exit 1 + fi - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: name: models-${{ env.REF }}${{ inputs.artifact_suffix }} path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx + if-no-files-found: error build_model: runs-on: [self-hosted, usbgpu] From 5ae100aa1d51c024732b8113237dd41fb6630ac4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 20 Aug 2026 19:19:59 -0400 Subject: [PATCH 281/325] models fetcher: bump big model to v21 --- openpilot/sunnypilot/models/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index 4d6c9964d7..b64e27b1ad 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -141,7 +141,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" - MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json" + MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.json" def __init__(self, params: Params): self.params = params From 5ecd05aedf247ee3a09796546cca5993bc8eb6ce Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 20 Aug 2026 21:31:32 -0400 Subject: [PATCH 282/325] models: add big model to default model resolution (#1929) * device * sunnylink * lint * lfs? * Revert "lfs?" This reverts commit bcdaec6b4c15a00274e38957824f9a9a0dd76032. * update path * Scope the default big model down to the sunnylink schema * Drop the mock-only default model test * Move the default model resolver out to separate PR --------- Co-authored-by: Jason Wen --- openpilot/sunnypilot/models/default_model.py | 47 +++++++------------ openpilot/sunnypilot/models/model_name.py | 1 + .../models/tests/test_default_model.py | 2 +- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index 62b6831402..ad4e8b6532 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -4,7 +4,7 @@ import hashlib from openpilot.common.basedir import BASEDIR from openpilot.sunnypilot import get_file_hash -from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL +from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") @@ -13,7 +13,6 @@ SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", def update_model_hash(): supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) - combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() with open(MODEL_HASH_PATH, "w") as f: @@ -22,40 +21,28 @@ def update_model_hash(): print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") -def get_current_default_model_name(): - print("[GET DEFAULT MODEL NAME]") - name = DEFAULT_MODEL - print(f'Current default model name: "{name}"') - - return name - - -def update_default_model_name(name: str): - print("[CHANGE DEFAULT MODEL NAME]") +def update_default_model_names(default_model_name: str, default_big_model_name: str): + print("[CHANGE DEFAULT MODEL NAMES]") with open(DEFAULT_MODEL_NAME_PATH, "w") as f: - f.write(f'DEFAULT_MODEL = "{name}"\n') - print(f'New default model name: "{name}"') + f.write(f'DEFAULT_MODEL = "{default_model_name}"\n') + f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n') + + print(f'New default small model name: "{default_model_name}"') + print(f'New default big model name: "{default_big_model_name}"') print("[DONE]") if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Update default model name and hash") - parser.add_argument("--new_name", type=str, help="New default model name") + parser = argparse.ArgumentParser(description="Update default model names and hash") + parser.add_argument("--new_small_model_name", type=str, help="New default small model name") + parser.add_argument("--new_big_model_name", type=str, help="New default big model name") args = parser.parse_args() - if not args.new_name: - print("Warning: No new default model name provided. Use --new_name to specify") - print("Default model name and hash will not be updated! (aborted)") - exit(0) + if args.new_small_model_name is None and args.new_big_model_name is None: + new_name = input(f'Enter new default small model name (current: "{DEFAULT_MODEL}", leave empty to keep): ').strip() + new_big_model_name = input(f'Enter new default big model name (current: "{DEFAULT_BIG_MODEL}", leave empty to keep): ').strip() + else: + new_name, new_big_model_name = args.new_small_model_name, args.new_big_model_name - current_name = get_current_default_model_name() - new_name = args.new_name - if current_name == new_name: - print(f'Proposed default model name: "{new_name}"') - confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip() - if confirm != "Y": - print("Default model name and hash will not be updated! (aborted)") - exit(0) - - update_default_model_name(new_name) + update_default_model_names(new_name or DEFAULT_MODEL, new_big_model_name or DEFAULT_BIG_MODEL) update_model_hash() diff --git a/openpilot/sunnypilot/models/model_name.py b/openpilot/sunnypilot/models/model_name.py index 02a6c2bac2..374e8473df 100644 --- a/openpilot/sunnypilot/models/model_name.py +++ b/openpilot/sunnypilot/models/model_name.py @@ -1 +1,2 @@ DEFAULT_MODEL = "CD210" +DEFAULT_BIG_MODEL = "Lebowski" diff --git a/openpilot/sunnypilot/models/tests/test_default_model.py b/openpilot/sunnypilot/models/tests/test_default_model.py index 450237e0e9..b72c2b4c89 100644 --- a/openpilot/sunnypilot/models/tests/test_default_model.py +++ b/openpilot/sunnypilot/models/tests/test_default_model.py @@ -20,4 +20,4 @@ class TestDefaultModel(OpenpilotTestCase): with open(MODEL_HASH_PATH) as f: current_hash = f.read().strip() - assert combined_hash == current_hash, "Run sunnypilot/models/default_model.py to update the default model name and hash" + assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash" From b742557d62175872ea91723eb73a22b0849b8ae2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 20 Aug 2026 21:56:57 -0400 Subject: [PATCH 283/325] sunnylink: add model resolver (#1931) * models: add get_default_model resolver for sunnylink * models: move get_default_model to default_model.py --- openpilot/sunnypilot/models/default_model.py | 6 ++++++ openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index ad4e8b6532..3883b4b23f 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -4,8 +4,14 @@ import hashlib from openpilot.common.basedir import BASEDIR from openpilot.sunnypilot import get_file_hash +from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL + +def get_default_model() -> str: + return DEFAULT_BIG_MODEL if usbgpu_present() else DEFAULT_MODEL + + DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx") diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index ac168db7d8..c0534a76bf 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,7 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import openpilot.cereal.messaging as messaging -from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL +from openpilot.sunnypilot.models.default_model import get_default_model from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string @@ -181,7 +181,7 @@ def getParamsMetadata() -> str: schema = generate_schema() schema["capabilities"] = generate_capabilities() schema["capability_labels"] = CAPABILITY_LABELS - schema["default_model"] = DEFAULT_MODEL + schema["default_model"] = get_default_model() raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") except Exception: From 5ad2bfdb752b9f455ec4f715506bd622a4402ab2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Aug 2026 00:35:38 -0400 Subject: [PATCH 284/325] ci: deprecate GitHub runners (#1933) --- openpilot/system/manager/github_runner.sh | 40 ---- openpilot/system/manager/process_config.py | 8 - release/ci/install_github_runner.sh | 260 --------------------- release/ci/uninstall_github_runner.sh | 66 ------ 4 files changed, 374 deletions(-) delete mode 100755 openpilot/system/manager/github_runner.sh delete mode 100755 release/ci/install_github_runner.sh delete mode 100755 release/ci/uninstall_github_runner.sh diff --git a/openpilot/system/manager/github_runner.sh b/openpilot/system/manager/github_runner.sh deleted file mode 100755 index f2170cfc70..0000000000 --- a/openpilot/system/manager/github_runner.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash - -# Define the service name -SERVICE_NAME="actions.runner.sunnypilot.$(uname -n)" - -# Function to control the service -control_service() { - local action=$1 # Store the function argument in a local variable - sudo systemctl $action ${SERVICE_NAME} -} - -service_exists_and_is_loaded() { - sudo systemctl status ${SERVICE_NAME} &>/dev/null - if [[ $? -ne 4 ]]; then - return 0 # Service is known to systemd (i.e., loaded) - else - return 1 # Service is unknown to systemd (i.e., not loaded) - fi -} - -# Check for required argument -if [[ -z $1 ]] || { [[ $1 != "start" ]] && [[ $1 != "stop" ]]; }; then - echo "Usage: $0 {start|stop}" - exit 1 -fi - -# Store the script argument in a descriptive variable -ACTION=$1 - -# Trap EXIT signal (Ctrl+C) and stop the service -trap 'control_service stop ; exit' SIGINT SIGKILL EXIT - -# Enter the main loop -while true; do - # Check if the service is actually present on the system - if service_exists_and_is_loaded; then - control_service $ACTION # Call the function with the specified action - fi - sleep 1 # Pause before the next iteration -done \ No newline at end of file diff --git a/openpilot/system/manager/process_config.py b/openpilot/system/manager/process_config.py index c9e939c432..ceea3f8847 100644 --- a/openpilot/system/manager/process_config.py +++ b/openpilot/system/manager/process_config.py @@ -68,10 +68,6 @@ def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: def livestream(started: bool, params: Params, CP: car.CarParams) -> bool: return params.get_bool("IsLiveStreaming") -def use_github_runner(started, params, CP: car.CarParams) -> bool: - return not PC and params.get_bool("EnableGithubRunner") and ( - not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage")) - def use_copyparty(started, params, CP: car.CarParams) -> bool: return bool(params.get_bool("EnableCopyparty")) @@ -189,10 +185,6 @@ procs += [ NativeProcess("locationd_llk", "openpilot/sunnypilot/selfdrive/locationd", ["./locationd"], only_onroad), ] -if os.path.exists("./github_runner.sh"): - procs += [NativeProcess("github_runner_start", "openpilot/system/manager", - ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] - if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): procs += [PythonProcess("sunnylink_uploader", "openpilot.sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh deleted file mode 100755 index 9f11e4841c..0000000000 --- a/release/ci/install_github_runner.sh +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Default values -DEFAULT_REPO_URL="https://github.com/sunnypilot" -START_AT_BOOT=false -RESTORE_MODE=false -RUNNER_VERSION="2.325.0" - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --start-at-boot) - START_AT_BOOT=true - shift - ;; - --token) - GITHUB_TOKEN="$2" - shift 2 - ;; - --repo) - REPO_URL="$2" - shift 2 - ;; - --restore) - RESTORE_MODE=true - shift - ;; - *) - if [ -z "$GITHUB_TOKEN" ]; then - GITHUB_TOKEN="$1" - elif [ -z "$REPO_URL" ]; then - REPO_URL="$1" - fi - shift - ;; - esac -done - -# Determine BASE_DIR based on mount point -if mountpoint -q /data/media; then - BASE_DIR="/data/media/0/github" -else - BASE_DIR="/data/github" -fi - -# Constants -RUNNER_USER="github-runner" -USER_GROUPS="comma,gpu,gpio,sudo" -RUNNER_DIR="${BASE_DIR}/runner" -BUILDS_DIR="${BASE_DIR}/builds" -LOGS_DIR="${BASE_DIR}/logs" -CACHE_DIR="${BASE_DIR}/cache" -OPENPILOT_DIR="${BASE_DIR}/openpilot" - -# Basic utility functions (no dependencies) -remount_rw() { - sudo mount -o remount,rw / -} - -remount_ro() { - sync || true # Try to sync but continue even if it fails - sudo mount -o remount,ro / # Always try to remount as read-only -} - -# Always ensure we try to remount as read-only on exit -trap remount_ro EXIT - -setup_runner_user() { - sudo useradd --comment 'GitHub Runner' --create-home --home-dir ${BASE_DIR} ${RUNNER_USER} --shell /bin/bash -G ${USER_GROUPS} || sudo usermod -aG ${USER_GROUPS} ${RUNNER_USER} -} - -create_sudoers_entry() { - sudo grep -qxF "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" /etc/sudoers || echo "${RUNNER_USER} ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers -} - -set_directory_permissions() { - sudo chown -R ${RUNNER_USER}:comma "$BASE_DIR" - sudo chmod -R g+rwx "$BASE_DIR" - sudo find "$BASE_DIR" -type d -exec chmod g+s {} + -} - -setup_directories() { - echo "Creating necessary directories..." - sudo mkdir -p "$RUNNER_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" - mkdir -p "/data/openpilot" - sudo chown -R comma:comma "/data/openpilot" - sync -} - -wipe_bash_logout() { - export BASE_DIR - sudo -u ${RUNNER_USER} bash -c "touch ${BASE_DIR}/.bash_logout" - sudo -u ${RUNNER_USER} bash -c "truncate -s 0 '${BASE_DIR}/.bash_logout'" -} - -# System configuration functions (depends on basic utility functions) -setup_system_configs() { - echo "Setting up system configurations..." - remount_rw - setup_runner_user - create_sudoers_entry - remount_ro - set_directory_permissions - wipe_bash_logout -} - -# Runner setup functions -install_runner() { - echo "Downloading and setting up runner..." - cd "$RUNNER_DIR" - curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz - sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz - sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz - sudo chmod +x ./config.sh -} - -configure_runner() { - remount_rw - echo "Configuring runner..." - cd "$RUNNER_DIR" - sudo -u ${RUNNER_USER} ./config.sh --url "$REPO_URL" --token "$GITHUB_TOKEN" --name $(hostname) --runnergroup "tici-tizi" --labels "tici" --work "$BUILDS_DIR" --unattended - remount_ro -} - -create_service_template() { - echo "Creating service template..." - cat < "$RUNNER_DIR/bin/actions.runner.service.template" -[Unit] -Description={{Description}} -After=network-online.target nss-lookup.target time-sync.target -Wants=network-online.target nss-lookup.target time-sync.target -StartLimitInterval=5 -StartLimitBurst=10 - -[Service] -Type=simple -User=root -ExecStart=/usr/bin/unshare -m -- /bin/bash -c 'mount --bind ${OPENPILOT_DIR} /data/openpilot && setpriv --reuid={{User}} --regid={{User}} --init-groups env HOME=${BASE_DIR} USER={{User}} LOGNAME={{User}} MAIL=/var/mail/{{User}} {{RunnerRoot}}/runsvc.sh' -WorkingDirectory={{RunnerRoot}} -KillMode=process -KillSignal=SIGTERM -TimeoutStopSec=5min -Restart=always -RestartSec=120 - -[Install] -WantedBy=multi-user.target -EOL -} - -install_service() { - local service_name - if [ -f "${RUNNER_DIR}/.service" ]; then - service_name=$(cat "${RUNNER_DIR}/.service") - else - service_name="actions.runner.sunnypilot.$(uname -n)" - fi - - create_service_template - remount_rw - local service_path="/etc/systemd/system/${service_name}" - echo "Installing systemd service..." - if [ -f "${service_path}" ]; then - echo "Service ${service_path} found in systemd, we will delete it" - sudo rm -f "${service_path}" - fi - - cd "$RUNNER_DIR" - sudo ./svc.sh install $RUNNER_USER - - if [ "$START_AT_BOOT" = false ]; then - sudo systemctl disable "${service_name}" - fi - remount_ro -} - -check_restore_prerequisites() { - local can_restore=false - local service_name="" - - # Check if base runner directory exists - if [ ! -d "${RUNNER_DIR}" ]; then - echo "ERROR: Runner directory ${RUNNER_DIR} does not exist" - echo "This directory is required for restore operations" - exit 1 - fi - - # First check if we have the required files for restoration - if [ -f "${RUNNER_DIR}/.credentials" ] && [ -f "${RUNNER_DIR}/.service" ]; then - can_restore=true - service_name=$(cat "${RUNNER_DIR}/.service") - echo "Found required runner configuration files" - else - echo "Missing required runner configuration files" - echo "Required: .credentials and .service files in ${RUNNER_DIR}" - exit 1 - fi - - if ! id "${RUNNER_USER}" &>/dev/null; then - echo "User ${RUNNER_USER} does not exist" - fi - - # Only proceed if we can restore AND need to restore - if [ "$can_restore" = true ]; then - echo "Restoration is possible" - return 0 - else - echo "No restoration possible" - exit 0 - fi -} - -perform_restore() { - echo "Starting runner restoration..." - setup_directories - setup_system_configs - install_service - echo "Runner restoration completed successfully" -} - -perform_install() { - echo "Starting fresh installation..." - setup_directories - setup_system_configs - install_runner - set_directory_permissions - configure_runner - install_service - echo "Installation completed successfully" -} - -main() { - if [ "$RESTORE_MODE" = true ]; then - echo "Running in restore mode - will only restore system configurations..." - check_restore_prerequisites - perform_restore - else - # Check required arguments for normal installation - if [ -z "$GITHUB_TOKEN" ]; then - echo "Usage: $0 [--start-at-boot] [--token ] [--repo ] [--restore]" - echo "Required argument (except for --restore): github_token" - echo "Optional arguments:" - echo " --start-at-boot Enable auto-start at boot (default: false)" - echo " --repo Repository URL (default: ${DEFAULT_REPO_URL})" - echo " --restore Restore existing runner configuration" - exit 1 - fi - - # Set repository URL if not provided - REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" - perform_install - fi - - echo "Starting runner service..." - cd "$RUNNER_DIR" - sudo ./svc.sh start -} - -main diff --git a/release/ci/uninstall_github_runner.sh b/release/ci/uninstall_github_runner.sh deleted file mode 100755 index 5f3acfbafd..0000000000 --- a/release/ci/uninstall_github_runner.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# Determine BASE_DIR based on mount point -if mountpoint -q /data/media; then - GITHUB_BASE_DIR="/data/media/0/github" -else - GITHUB_BASE_DIR="/data/github" -fi - -# Define directories and user -BIN_DIR="$GITHUB_BASE_DIR/bin" -BUILDS_DIR="$GITHUB_BASE_DIR/builds" -OPENPILOT_DIR="$GITHUB_BASE_DIR/openpilot" -LOGS_DIR="$GITHUB_BASE_DIR/logs" -CACHE_DIR="$GITHUB_BASE_DIR/cache" -RUNNER_USERNAME="github-runner" -# Define the systemd service name -SERVICE_NAME="github-runner" -USER_GROUPS="comma,gpu,gpio,sudo" - -# Function to stop and disable the systemd service -stop_and_uninstall_service() { - cd $GITHUB_BASE_DIR/runner - sudo ./svc.sh stop - sudo ./svc.sh uninstall -} - -# Function to remove the systemd service file -remove_runner() { - cd $GITHUB_BASE_DIR/runner - sudo rm .runner - sudo su -c './config.sh remove' github-runner -} - -# Function to delete the Github Runner directories -delete_directories() { - sudo rm -rf "$BIN_DIR/github-runner" - sudo rm -rf "$GITHUB_BASE_DIR" "$BIN_DIR" "$BUILDS_DIR" "$LOGS_DIR" "$CACHE_DIR" "$OPENPILOT_DIR" -} - -# Function to remove the Github Runner user -delete_user() { - for group in ${USER_GROUPS//,/ } - do - sudo gpasswd -d ${RUNNER_USERNAME} ${group} - done - sudo userdel -r ${RUNNER_USERNAME} -} - -# Function to remove sudoers entry -remove_sudoers_entry() { - sudo sed -i.bak "/${RUNNER_USERNAME} ALL=(ALL) NOPASSWD: ALL/d" /etc/sudoers -} - -# Make filesystem writable -sudo mount -o remount rw / - -# Ensure filesystem is remounted as read-only on script exit -trap "sudo mount -o remount ro /" EXIT - -# Call functions -stop_and_uninstall_service -remove_runner -delete_directories -delete_user -remove_sudoers_entry -# End of uninstall script From a49c2609274087d33f469ae1edbe1c5608589f61 Mon Sep 17 00:00:00 2001 From: Marceline Milligan Date: Fri, 21 Aug 2026 11:26:52 -0600 Subject: [PATCH 285/325] ui: show default big model name when eGPU present/active (#1930) * Name the big default model in the device UI Build on the default big-model metadata from #1929 and resolve the displayed model from cached capability and modeld runtime state. Keep model selection behavior unchanged. Assisted-by: GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove get_default_model_label * simplify --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: nayan --- .../selfdrive/ui/sunnypilot/layouts/settings/models.py | 8 +++++--- .../selfdrive/ui/sunnypilot/mici/layouts/models.py | 10 +++++----- openpilot/sunnypilot/models/default_model.py | 7 +++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index aa5700fe28..e083dfb079 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -10,7 +10,7 @@ import time import pyray as rl from openpilot.cereal import custom -from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL +from openpilot.sunnypilot.models.default_model import get_default_model from openpilot.common.constants import CV from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.lib.multilang import tr @@ -211,7 +211,8 @@ class ModelsLayout(Widget): for bundle in bundles: folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle) - folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{DEFAULT_MODEL} (Default)", 'short_name': "Default"})])] + folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{get_default_model()} (Default)", + 'short_name': "Default"})])] for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True): folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True) name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "") @@ -249,7 +250,8 @@ class ModelsLayout(Widget): self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] self._handle_bundle_download_progress() - active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)" + default_label = f"{get_default_model()} (Default)" + active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else default_label self.current_model_item.action_item.set_value(active_name) if not ui_state.is_offroad(): diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index af8f348f72..6eff456559 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl from openpilot.cereal import custom -from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL +from openpilot.sunnypilot.models.default_model import get_default_model from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout from openpilot.selfdrive.ui.ui_state import ui_state, device @@ -27,7 +27,7 @@ class CurrentModelInfo(Widget): subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) - default_text = f"{DEFAULT_MODEL} (Default)".lower() + default_text = f"{get_default_model()} (Default)".lower() self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) @@ -95,7 +95,7 @@ class ModelsLayoutMici(NavScroller): folders = self._get_grouped_bundles(favorites) folder_buttons = [] - default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower()) + default_btn = BigButton(f"{get_default_model()} (Default)".lower()) default_btn.set_click_callback(self._select_default) folder_buttons.append(default_btn) @@ -162,7 +162,8 @@ class ModelsLayoutMici(NavScroller): self._was_downloading = is_downloading self.current_model_info.current_model_header.set_text(tr("active model")) - model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)".lower() + default_model_text = f"{get_default_model()} (Default)".lower() + model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.ref else default_model_text self.current_model_info.current_model_text.set_text(model_text) self.current_model_info.info_header.set_text(tr("cache size")) self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB") @@ -191,4 +192,3 @@ class ModelsLayoutMici(NavScroller): self.current_model_info.info_header.set_text(tr("progress") + self._download_progress) self.current_model_info.info_header._shimmer = True self.current_model_info.info_text.set_text(f"{progress/count:.2f}%") - diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index 3883b4b23f..128426979b 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -3,13 +3,16 @@ import os import hashlib from openpilot.common.basedir import BASEDIR +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot import get_file_hash -from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL def get_default_model() -> str: - return DEFAULT_BIG_MODEL if usbgpu_present() else DEFAULT_MODEL + show_big_model = (ui_state.usbgpu and ui_state.usbgpu_compiled + and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad())) + + return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") From 084747c75d2cbd23af65ab7a9e770bbd7b98bac9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 21 Aug 2026 15:41:12 -0700 Subject: [PATCH 286/325] Fix button label widths (#38680) * Revert "ui: fix text and icon overlap on button (#38628)" This reverts commit d9c4120f891430da60a353e91600483ef0292de1. * simple * can do this * fix eliding * Revert "fix eliding" This reverts commit b271a350182ad87f9942d7363383ee8ec72d0e36. * clean up * clean up --- openpilot/selfdrive/ui/mici/widgets/button.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index cad40d7d01..e2912d00cf 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -149,11 +149,15 @@ class BigButton(Widget): def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: super().set_touch_valid_callback(lambda: touch_callback() and self._grow_animation_until is None) - def _width_hint(self) -> int: - # A value moves the title to the top, where it shares space with the icon. + def _title_width_hint(self) -> int: + # A value moves the title to the top, where it shares space with the icon icon_size = self._txt_icon.width if self._txt_icon and self.value else 0 return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_size) + def _subtitle_width_hint(self) -> int: + # Bottom aligned, so it sits below the icon + return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2) + def _get_label_font_size(self): if len(self.text) <= 18: return 48 @@ -228,14 +232,14 @@ class BigButton(Widget): label_color = LABEL_COLOR if self.enabled else rl.Color(255, 255, 255, int(255 * 0.35)) self._label.set_color(label_color) - label_rect = rl.Rectangle(label_x, btn_y + self.LABEL_VERTICAL_PADDING, self._width_hint(), + label_rect = rl.Rectangle(label_x, btn_y + self.LABEL_VERTICAL_PADDING, self._title_width_hint(), self._rect.height - self.LABEL_VERTICAL_PADDING * 2) self._label.render(label_rect) if self.value: - label_y = btn_y + self.LABEL_VERTICAL_PADDING + self._label.get_content_height(self._width_hint()) + label_y = label_rect.y + self._label.get_content_height(int(label_rect.width)) sub_label_height = btn_y + self._rect.height - self.LABEL_VERTICAL_PADDING - label_y - sub_label_rect = rl.Rectangle(label_x, label_y, self._width_hint(), sub_label_height) + sub_label_rect = rl.Rectangle(label_x, label_y, self._subtitle_width_hint(), sub_label_height) self._sub_label.render(sub_label_rect) # ICON ------------------------------------------------------------------- @@ -312,9 +316,6 @@ class BigMultiToggle(BigToggle): self.set_value(self._options[0]) - def _width_hint(self) -> int: - return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width) - def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) cur_idx = self._options.index(self.value) @@ -363,9 +364,6 @@ class GreyBigButton(BigButton): def LABEL_VERTICAL_PADDING(self): return BigButton.LABEL_VERTICAL_PADDING if self._label.text else 18 - def _width_hint(self) -> int: - return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2) - def _get_label_font_size(self): return 36 From 4667241fe7548a9f7a609e755f1bc4b66108e22d Mon Sep 17 00:00:00 2001 From: granolaFPV <65827214+granolaFPV@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:51:43 -0700 Subject: [PATCH 287/325] [TIZI/TICI] ui: dynamic path width color (#1926) * Fix UI path color and thickness based on lateral steering state (Issue #1441) * Fix UI path color and thickness based on lateral control engagement (Issue #1441) * Fix UI path width and color based on MADS lateral engagement (Issue #1441) * Fix UI path width and color based on MADS lateral engagement (Issue #1441) * move to ModelRendererSP * match torque bar * same behavior across the board * simplify --------- Co-authored-by: Brennan Browne Co-authored-by: Jason Wen --- openpilot/selfdrive/ui/onroad/model_renderer.py | 4 ++-- .../selfdrive/ui/sunnypilot/onroad/model_renderer.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/onroad/model_renderer.py b/openpilot/selfdrive/ui/onroad/model_renderer.py index def1c644af..f5a39a2a5b 100644 --- a/openpilot/selfdrive/ui/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/onroad/model_renderer.py @@ -192,7 +192,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): max_idx = self._get_path_length_idx(path_x_array, max_distance) self._path.projected_points = self._map_line_to_polygon( - self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False + self._path.raw_points, self._get_path_half_width(), self._path_offset_z, max_idx, max_distance, allow_invert=False ) self._update_experimental_gradient() @@ -292,7 +292,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP): allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control self._blend_filter.update(int(allow_throttle)) - if ui_state.rainbow_path: + if ui_state.rainbow_path and self._lateral_active: self.rainbow_path.draw_rainbow_path(self._rect, self._path) return diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py index 5d78997662..3cf639d0a1 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py @@ -4,11 +4,23 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath +from openpilot.system.ui.lib.application import gui_app class ModelRendererSP: def __init__(self): self.rainbow_path = RainbowPath() self.chevron_metrics = ChevronMetrics() + self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps) + + @property + def _lateral_active(self) -> bool: + return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) + + def _get_path_half_width(self) -> float: + target = 0.9 if self._lateral_active else 0.40 + return self._width_filter.update(target) From ca9338812e3622766f9cb62554f7963f13121ba5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Aug 2026 00:16:10 -0400 Subject: [PATCH 288/325] ci: prep for chestnut prebuilts --- .../workflows/build-default-big-model.yaml | 82 +++++++++++++++++++ release/ci/upload_default_model.py | 74 +++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 .github/workflows/build-default-big-model.yaml create mode 100644 release/ci/upload_default_model.py diff --git a/.github/workflows/build-default-big-model.yaml b/.github/workflows/build-default-big-model.yaml new file mode 100644 index 0000000000..1a734a3af8 --- /dev/null +++ b/.github/workflows/build-default-big-model.yaml @@ -0,0 +1,82 @@ +name: Build default big model + +on: + push: + branches: [ master, master-dev ] + paths: + - 'openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx' + workflow_dispatch: + +env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/big + +jobs: + build_model: + uses: ./.github/workflows/sunnypilot-build-model.yaml + with: + upstream_branch: ${{ github.sha }} + custom_name: default-big-model + target_hardware: usbgpu + secrets: inherit + + upload_defaults: + needs: build_model + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" + + - name: Install huggingface_hub + run: pip install --upgrade "huggingface_hub>=0.22.0" + + - name: Download artifact name + uses: actions/download-artifact@v4 + with: + name: artifact-name-default-big-model + path: artifact_name + + - name: Read artifact name + id: artifact + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact_name }} + path: output + + - name: Upload model to HF defaults + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} + run: | + rm -f output/artifact_name.txt + hf upload ${{ env.HF_REPO }} \ + output/ \ + "${HF_DEFAULTS_PATH}/${ARTIFACT_NAME}/" \ + --repo-type=dataset + + - name: Get tinygrad ref and ONNX hash + id: meta + run: | + export PYTHONPATH=$(pwd) + echo "tinygrad_ref=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" >> $GITHUB_OUTPUT + echo "onnx_sha256=$(sha256sum openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: Update default_models.json on HF + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} + run: | + python3 release/ci/upload_default_model.py \ + --hf-repo "${{ env.HF_REPO }}" \ + --hf-defaults-path "${{ env.HF_DEFAULTS_PATH }}" \ + --artifact-name "$ARTIFACT_NAME" \ + --metadata-path "output/metadata.json" \ + --onnx-sha256 "${{ steps.meta.outputs.onnx_sha256 }}" \ + --tinygrad-ref "${{ steps.meta.outputs.tinygrad_ref }}" diff --git a/release/ci/upload_default_model.py b/release/ci/upload_default_model.py new file mode 100644 index 0000000000..ab31dcabce --- /dev/null +++ b/release/ci/upload_default_model.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import argparse +import json +import sys +import tempfile + +from huggingface_hub import HfApi, hf_hub_download + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--hf-repo", required=True) + parser.add_argument("--hf-defaults-path", required=True) + parser.add_argument("--artifact-name", required=True) + parser.add_argument("--metadata-path", required=True) + parser.add_argument("--onnx-sha256", required=True) + parser.add_argument("--tinygrad-ref", required=True) + args = parser.parse_args() + + with open(args.metadata_path) as f: + metadata = json.load(f) + + bundle = metadata['bundles'][0] + bundle['onnx_sha256'] = args.onnx_sha256 + + artifact = bundle['models'][0]['artifact'] + hf_base = f"https://huggingface.co/datasets/{args.hf_repo}/resolve/main/{args.hf_defaults_path}/{args.artifact_name}" + artifact['download_uri']['url'] = f"{hf_base}/{artifact['file_name']}" + for chunk in artifact.get('chunks', []): + chunk['url'] = f"{hf_base}/{chunk['file_name']}" + + json_filename = f"{args.hf_defaults_path}/default_models.json" + try: + local_path = hf_hub_download(repo_id=args.hf_repo, repo_type='dataset', filename=json_filename) + with open(local_path) as f: + defaults_json = json.load(f) + except Exception: + defaults_json = {"tinygrad_ref": args.tinygrad_ref, "bundles": []} + + defaults_json['tinygrad_ref'] = args.tinygrad_ref + + existing_idx = next((i for i, b in enumerate(defaults_json['bundles']) + if b.get('display_name') == bundle.get('display_name')), None) + if existing_idx is not None: + defaults_json['bundles'][existing_idx] = bundle + else: + defaults_json['bundles'].append(bundle) + + print(json.dumps(defaults_json, indent=2)) + + api = HfApi() + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump(defaults_json, f, indent=2) + tmp_path = f.name + + api.upload_file( + path_or_fileobj=tmp_path, + path_in_repo=json_filename, + repo_id=args.hf_repo, + repo_type="dataset", + ) + + print(f"Updated {json_filename}") + + +if __name__ == "__main__": + main() From 07558166c81c1b8ba091adfbf607449c449ecdb6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Aug 2026 00:32:35 -0400 Subject: [PATCH 289/325] ci: only check default model on dispatch --- .github/workflows/build-default-big-model.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-default-big-model.yaml b/.github/workflows/build-default-big-model.yaml index 1a734a3af8..008659086d 100644 --- a/.github/workflows/build-default-big-model.yaml +++ b/.github/workflows/build-default-big-model.yaml @@ -1,10 +1,6 @@ name: Build default big model on: - push: - branches: [ master, master-dev ] - paths: - - 'openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx' workflow_dispatch: env: From 5a8567e3e7fc0de99575e253264f5cbd6886a951 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Aug 2026 03:41:48 -0400 Subject: [PATCH 290/325] ci: chestnut prebuilt branches (#1935) * ci: chestnut prebuilt branches * fix * nope * big * try again * diff * malformed * auth * more --- .../workflows/build-default-big-model.yaml | 53 ++++--- .../workflows/sunnypilot-build-prebuilt.yaml | 147 +++++++++++++++++- release/ci/model_generator.py | 22 ++- release/ci/upload_default_model.py | 46 +++++- 4 files changed, 231 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build-default-big-model.yaml b/.github/workflows/build-default-big-model.yaml index 008659086d..4b05a7977d 100644 --- a/.github/workflows/build-default-big-model.yaml +++ b/.github/workflows/build-default-big-model.yaml @@ -8,17 +8,35 @@ env: HF_DEFAULTS_PATH: models/defaults/big jobs: + resolve_name: + runs-on: ubuntu-24.04 + outputs: + model_name: ${{ steps.name.outputs.model_name }} + onnx_ref: ${{ steps.name.outputs.onnx_ref }} + steps: + - uses: actions/checkout@v4 + - id: name + run: | + NAME=$(PYTHONPATH=${{ github.workspace }} python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") + ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx) + echo "model_name=${NAME}" >> $GITHUB_OUTPUT + echo "onnx_ref=$ONNX_REF" >> $GITHUB_OUTPUT + build_model: + needs: resolve_name uses: ./.github/workflows/sunnypilot-build-model.yaml with: - upstream_branch: ${{ github.sha }} - custom_name: default-big-model + upstream_branch: ${{ needs.resolve_name.outputs.onnx_ref }} + custom_name: ${{ needs.resolve_name.outputs.model_name }} target_hardware: usbgpu secrets: inherit upload_defaults: - needs: build_model + needs: [ resolve_name, build_model ] runs-on: ubuntu-24.04 + permissions: + id-token: write + contents: write steps: - uses: actions/checkout@v4 with: @@ -31,7 +49,7 @@ jobs: - name: Download artifact name uses: actions/download-artifact@v4 with: - name: artifact-name-default-big-model + name: artifact-name-${{ needs.resolve_name.outputs.model_name }} path: artifact_name - name: Read artifact name @@ -46,33 +64,20 @@ jobs: name: ${{ steps.artifact.outputs.artifact_name }} path: output - - name: Upload model to HF defaults + - name: Upload to HF and update default_models.json env: HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} run: | rm -f output/artifact_name.txt - hf upload ${{ env.HF_REPO }} \ - output/ \ - "${HF_DEFAULTS_PATH}/${ARTIFACT_NAME}/" \ - --repo-type=dataset - - - name: Get tinygrad ref and ONNX hash - id: meta - run: | export PYTHONPATH=$(pwd) - echo "tinygrad_ref=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" >> $GITHUB_OUTPUT - echo "onnx_sha256=$(sha256sum openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | cut -d' ' -f1)" >> $GITHUB_OUTPUT - - - name: Update default_models.json on HF - env: - HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} - ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} - run: | python3 release/ci/upload_default_model.py \ --hf-repo "${{ env.HF_REPO }}" \ --hf-defaults-path "${{ env.HF_DEFAULTS_PATH }}" \ --artifact-name "$ARTIFACT_NAME" \ - --metadata-path "output/metadata.json" \ - --onnx-sha256 "${{ steps.meta.outputs.onnx_sha256 }}" \ - --tinygrad-ref "${{ steps.meta.outputs.tinygrad_ref }}" + --model-dir output \ + --onnx-path "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" \ + --onnx-ref "${{ needs.resolve_name.outputs.onnx_ref }}" \ + --model-name "${{ needs.resolve_name.outputs.model_name }}" \ + --tinygrad-ref "$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" \ + --run-number "${{ github.run_number }}" diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index db0d0f55b4..e155a68d4a 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -36,6 +36,7 @@ jobs: publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }} is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }} build: ${{ steps.strategy.outputs.build }} + include_big_model: ${{ steps.strategy.outputs.include_big_model }} steps: - uses: actions/checkout@v4 - name: Extract deploy strategy @@ -78,6 +79,9 @@ jobs: stable_version=$(cat openpilot/sunnypilot/common/version.h | grep SUNNYPILOT_VERSION | sed -e 's/[^0-9|.]//g'); echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT + + include_big_model="$(echo "$CONFIG" | jq -r '.include_big_model // false')"; + echo "include_big_model=$include_big_model" >> $GITHUB_OUTPUT fi echo "build=$BUILD" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT @@ -203,6 +207,101 @@ jobs: source ${UV_PROJECT_ENVIRONMENT}/bin/activate PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + prepare_chestnut: + needs: [ prepare_strategy ] + runs-on: ubuntu-24.04 + if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/big + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + + - run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" + + - name: Check HF defaults and build if needed + id: resolve + run: | + ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) + echo "Repo ONNX hash: $ACTUAL_ONNX_HASH" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_hash() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ] + } + + if check_hash; then + echo "HF defaults match repo ONNX" + else + echo "No matching model on HF — triggering build" + gh workflow run build-default-big-model.yaml --ref "${{ github.head_ref || github.ref_name }}" + + echo "Waiting for build to start..." + sleep 120 + + RUN_ID=$(gh run list --workflow=build-default-big-model.yaml --branch="${{ github.head_ref || github.ref_name }}" --limit=1 --json databaseId --jq '.[0].databaseId') + if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then + echo "::error::Failed to find build-default-big-model run" + exit 1 + fi + + echo "Waiting for run $RUN_ID..." + gh run watch "$RUN_ID" + + CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') + if [ "$CONCLUSION" != "success" ]; then + echo "::error::build-default-big-model failed: $CONCLUSION" + exit 1 + fi + + if ! check_hash; then + echo "::error::HF defaults still don't match after build" + exit 1 + fi + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download big model chunks + run: | + ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + + mkdir -p big_model_chunks + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + CANONICAL="big_driving_tinygrad.pkl" + echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') + CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" + curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" + done + + echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" + + - name: Upload big model chunks + uses: actions/upload-artifact@v4 + with: + name: big-model-chunks + path: big_model_chunks/ + compression-level: 0 + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish: concurrency: @@ -211,14 +310,20 @@ jobs: # Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time. group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }} cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} - if: ${{ (always() && !cancelled() && !failure()) && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} - needs: [ build, prepare_strategy ] + if: ${{ + always() && !cancelled() && + needs.build.result == 'success' && + needs.prepare_strategy.result == 'success' && + (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) && + (needs.prepare_strategy.outputs.include_big_model != 'true' || needs.prepare_chestnut.result == 'success') + }} + needs: [ build, prepare_strategy, prepare_chestnut ] runs-on: ubuntu-24.04 environment: ${{ needs.prepare_strategy.outputs.environment }} steps: - uses: actions/checkout@v4 - - name: Download build artifacts + - name: Download prebuilt artifact uses: actions/download-artifact@v4 with: name: prebuilt @@ -228,6 +333,24 @@ jobs: mkdir -p ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + - name: Prepare chestnut output + if: ${{ needs.prepare_chestnut.result == 'success' }} + run: | + mkdir -p "${{ github.workspace }}/chestnut_output" + tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output" + + - name: Download big model chunks + if: ${{ needs.prepare_chestnut.result == 'success' }} + uses: actions/download-artifact@v4 + with: + name: big-model-chunks + path: big_model_chunks + + - name: Inject big model into chestnut + if: ${{ needs.prepare_chestnut.result == 'success' }} + run: | + cp big_model_chunks/* "${{ github.workspace }}/chestnut_output/openpilot/selfdrive/modeld/models/" + - name: Configure Git run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" @@ -248,6 +371,22 @@ jobs: "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" + - name: Publish chestnut branch + if: ${{ needs.prepare_chestnut.result == 'success' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut" + CHESTNUT_DIR="${{ github.workspace }}/chestnut_output" + + ${{ env.CI_DIR }}/publish.sh \ + "${{ github.workspace }}" \ + "$CHESTNUT_DIR" \ + "$CHESTNUT_BRANCH" \ + "${{ needs.prepare_strategy.outputs.version }}" \ + "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ + "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" + - name: Tag ${{ needs.prepare_strategy.outputs.environment }} if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} run: | @@ -260,6 +399,7 @@ jobs: - prepare_strategy - build - publish + - prepare_chestnut runs-on: ubuntu-24.04 if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' @@ -279,6 +419,7 @@ jobs: export commit_short_sha="${commit_short_sha:0:7}" export extra_version_identifier="${{ needs.prepare_strategy.outputs.extra_version_identifier || github.run_number }}" export PUBLIC_REPO_URL="${{ env.PUBLIC_REPO_URL }}" + export chestnut_branch="${{ needs.prepare_chestnut.result == 'success' && format('{0}-chestnut', needs.prepare_strategy.outputs.new_branch) || '' }}" MESSAGE=$(cat << 'EOF' | envsubst ${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }} diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 80b102a6f1..ff9be64783 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -90,6 +90,18 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path: return old_pkl.rename(new_pkl) +def _hash_onnx_files(model_dir: Path) -> str | None: + onnx_files = sorted(model_dir.glob("*.onnx")) + if not onnx_files: + return None + digest = hashlib.sha256() + for f in onnx_files: + with f.open('rb') as fh: + while block := fh.read(1024 * 1024): + digest.update(block) + return digest.hexdigest() + + def generate_chunked_model(driving_pkl: Path) -> dict: tinygrad_hash = _hash_pkl(driving_pkl) @@ -123,7 +135,8 @@ def generate_chunked_model(driving_pkl: Path) -> dict: } -def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown") -> None: +def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown", + onnx_sha256=None) -> None: bundle_json = { "short_name": short_name, "display_name": custom_name or upstream_branch, @@ -139,6 +152,9 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short "models": models, } + if onnx_sha256: + bundle_json["onnx_sha256"] = onnx_sha256 + # Write metadata to output_dir metadata_json = { "bundles": [bundle_json] @@ -178,4 +194,6 @@ if __name__ == "__main__": _driving_pkl = new_pkl _model_metadata = generate_chunked_model(_driving_pkl) - create_metadata_json([_model_metadata], _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch) + _onnx_sha256 = _hash_onnx_files(Path(args.model_dir)) + create_metadata_json([_model_metadata], _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch, + onnx_sha256=_onnx_sha256) diff --git a/release/ci/upload_default_model.py b/release/ci/upload_default_model.py index ab31dcabce..eac48e3be4 100644 --- a/release/ci/upload_default_model.py +++ b/release/ci/upload_default_model.py @@ -7,35 +7,66 @@ See the LICENSE.md file in the root directory for more details. """ import argparse +import hashlib import json -import sys import tempfile from huggingface_hub import HfApi, hf_hub_download +def hash_file(path: str) -> str: + digest = hashlib.sha256() + with open(path, 'rb') as f: + while block := f.read(1024 * 1024): + digest.update(block) + return digest.hexdigest() + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--hf-repo", required=True) parser.add_argument("--hf-defaults-path", required=True) parser.add_argument("--artifact-name", required=True) - parser.add_argument("--metadata-path", required=True) - parser.add_argument("--onnx-sha256", required=True) + parser.add_argument("--model-dir", required=True) + parser.add_argument("--onnx-path", required=True) + parser.add_argument("--onnx-ref", required=True) + parser.add_argument("--model-name", required=True) parser.add_argument("--tinygrad-ref", required=True) + parser.add_argument("--run-number", required=True) args = parser.parse_args() - with open(args.metadata_path) as f: + api = HfApi() + onnx_sha256 = hash_file(args.onnx_path) + short_ref = args.onnx_ref[:8] + folder_name = f"model-{args.model_name}-{short_ref}-{args.run_number}" + + print(f"ONNX hash: {onnx_sha256}") + print(f"ONNX ref: {args.onnx_ref} (short: {short_ref})") + print(f"Folder: {folder_name}") + + metadata_path = f"{args.model_dir}/metadata.json" + with open(metadata_path) as f: metadata = json.load(f) bundle = metadata['bundles'][0] - bundle['onnx_sha256'] = args.onnx_sha256 + bundle['display_name'] = args.model_name + bundle['onnx_sha256'] = onnx_sha256 + bundle['onnx_ref'] = args.onnx_ref artifact = bundle['models'][0]['artifact'] - hf_base = f"https://huggingface.co/datasets/{args.hf_repo}/resolve/main/{args.hf_defaults_path}/{args.artifact_name}" + hf_base = f"https://huggingface.co/datasets/{args.hf_repo}/resolve/main/{args.hf_defaults_path}/{folder_name}" artifact['download_uri']['url'] = f"{hf_base}/{artifact['file_name']}" for chunk in artifact.get('chunks', []): chunk['url'] = f"{hf_base}/{chunk['file_name']}" + print(f"Uploading model to {args.hf_defaults_path}/{folder_name}/") + api.upload_folder( + folder_path=args.model_dir, + path_in_repo=f"{args.hf_defaults_path}/{folder_name}", + repo_id=args.hf_repo, + repo_type="dataset", + ) + json_filename = f"{args.hf_defaults_path}/default_models.json" try: local_path = hf_hub_download(repo_id=args.hf_repo, repo_type='dataset', filename=json_filename) @@ -47,7 +78,7 @@ def main(): defaults_json['tinygrad_ref'] = args.tinygrad_ref existing_idx = next((i for i, b in enumerate(defaults_json['bundles']) - if b.get('display_name') == bundle.get('display_name')), None) + if b.get('onnx_sha256') == onnx_sha256), None) if existing_idx is not None: defaults_json['bundles'][existing_idx] = bundle else: @@ -55,7 +86,6 @@ def main(): print(json.dumps(defaults_json, indent=2)) - api = HfApi() with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: json.dump(defaults_json, f, indent=2) tmp_path = f.name From 4f46433e2bc432e85eeb2bb78aaa93dfc723f885 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Aug 2026 10:20:06 -0400 Subject: [PATCH 291/325] alerts: add branch metadata to chestnut offroad warning (#1936) * alerts: add branch metadata to chestnut offroad warning * all branches --- openpilot/common/version.py | 9 +++++++++ openpilot/selfdrive/selfdrived/alerts_offroad.json | 2 +- openpilot/system/hardware/hardwared.py | 8 ++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/openpilot/common/version.py b/openpilot/common/version.py index f1514aa3cd..0456782c05 100755 --- a/openpilot/common/version.py +++ b/openpilot/common/version.py @@ -16,6 +16,15 @@ MASTER_SP_BRANCHES = ['master'] RELEASE_BRANCHES = ['release-tizi-staging', 'release-mici-staging', 'release-tizi', 'release-mici', 'nightly'] TESTED_BRANCHES = RELEASE_BRANCHES + ['devel-staging', 'nightly-dev'] + RELEASE_SP_BRANCHES + TESTED_SP_BRANCHES +CHESTNUT_BRANCHES = { + "staging": "staging-chestnut", + "dev": "dev-chestnut", + "release-mici": "release-chestnut", + "release-tizi": "release-chestnut", + "release-mici-staging": "release-chestnut-staging", + "release-tizi-staging": "release-chestnut-staging", +} + SP_BRANCH_MIGRATIONS = { ("tici", "staging-c3-new"): "staging-tici", ("tici", "dev-c3-new"): "staging-tici", diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index 226be06683..91a7ec8ab3 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -18,7 +18,7 @@ "_comment": "Set extra field to the failed reason." }, "Offroad_ChestnutBranch": { - "text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", + "text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.", "severity": 0 }, "Offroad_UnregisteredHardware": { diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 3c22f1cbd7..8aed8be5b9 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -27,7 +27,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.system.statsd import statlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController -from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp +from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp, CHESTNUT_BRANCHES ThermalStatus = log.DeviceState.ThermalStatus @@ -301,7 +301,11 @@ def hardware_thread(end_event, hw_queue) -> None: set_usb_state(msg.deviceState, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state) - set_offroad_alert_if_changed("Offroad_ChestnutBranch", msg.deviceState.chestnutPresent and not big_model_available) + current_channel = get_build_metadata().channel + chestnut_target = CHESTNUT_BRANCHES.get(current_channel) + chestnut_needs_switch = msg.deviceState.chestnutPresent and not big_model_available and chestnut_target is not None + set_offroad_alert_if_changed("Offroad_ChestnutBranch", chestnut_needs_switch, + extra_text=chestnut_target if chestnut_needs_switch else None) # this subset is only used for offroad temp_sources = [ From 086530b7c600b9e2503ad78121c5a8b1d5ed0dae Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 22 Aug 2026 21:47:38 -0400 Subject: [PATCH 292/325] [TIZI/TICI] ui: fix path width during gas and steering override (#1938) --- openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py index 3cf639d0a1..4be82bef4a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/model_renderer.py @@ -8,6 +8,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath +from openpilot.selfdrive.ui.sunnypilot.ui_state import MADSState from openpilot.system.ui.lib.application import gui_app @@ -19,6 +20,11 @@ class ModelRendererSP: @property def _lateral_active(self) -> bool: + sm = ui_state.sm + if sm.valid["selfdriveStateSP"]: + mads = sm["selfdriveStateSP"].mads + if mads.available: + return mads.enabled and mads.state != MADSState.paused return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) def _get_path_half_width(self) -> float: From 34621cf81647b39040db0ad0dfa40507f1349f35 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 02:48:10 -0400 Subject: [PATCH 293/325] ci: refactor big model chunk handling (#1939) --- .../workflows/sunnypilot-build-prebuilt.yaml | 63 +++++++++---------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index e155a68d4a..84fe6b3cc1 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -211,6 +211,8 @@ jobs: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} + outputs: + onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }} env: HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/big @@ -226,6 +228,7 @@ jobs: run: | ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) echo "Repo ONNX hash: $ACTUAL_ONNX_HASH" + echo "onnx_sha256=$ACTUAL_ONNX_HASH" >> $GITHUB_OUTPUT JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" @@ -267,36 +270,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Download big model chunks - run: | - ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) - JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" - DEFAULTS=$(curl -fsSL "$JSON_URL") - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') - - mkdir -p big_model_chunks - ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') - BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') - NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') - - CANONICAL="big_driving_tinygrad.pkl" - echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do - CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') - CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" - ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") - echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" - curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" - done - - echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - - - name: Upload big model chunks - uses: actions/upload-artifact@v4 - with: - name: big-model-chunks - path: big_model_chunks/ - compression-level: 0 - - name: Cancel run on failure if: failure() run: gh run cancel ${{ github.run_id }} @@ -339,12 +312,32 @@ jobs: mkdir -p "${{ github.workspace }}/chestnut_output" tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output" - - name: Download big model chunks + - name: Download big model chunks from HF if: ${{ needs.prepare_chestnut.result == 'success' }} - uses: actions/download-artifact@v4 - with: - name: big-model-chunks - path: big_model_chunks + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/big + run: | + ONNX_HASH="${{ needs.prepare_chestnut.outputs.onnx_sha256 }}" + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + + mkdir -p big_model_chunks + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + CANONICAL="big_driving_tinygrad.pkl" + echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') + CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" + curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" + done + + echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - name: Inject big model into chestnut if: ${{ needs.prepare_chestnut.result == 'success' }} From 6c6fba9a14fff1837077ca1948fd6a09110c1c9e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 02:57:02 -0400 Subject: [PATCH 294/325] ci: fix flaky LLK test (#1940) --- .../selfdrive/locationd/tests/test_locationd.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py index ac6ab6aaea..2e99122bc7 100644 --- a/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py +++ b/openpilot/sunnypilot/selfdrive/locationd/tests/test_locationd.py @@ -83,9 +83,14 @@ class TestLocationdProc(OpenpilotTestCase): self.pm.send(msg.which(), msg) if msg.which() == "cameraOdometry": self.pm.wait_for_readers_to_update(msg.which(), timeout=1, dt=0.005) - time.sleep(1) # wait for async params write + for _ in range(50): + val = self.params.get('LastGPSPositionLLK') + if val is not None: + break + time.sleep(0.1) - lastGPS = json.loads(self.params.get('LastGPSPositionLLK')) + self.assertIsNotNone(val, "LastGPSPositionLLK not written within 5s") + lastGPS = json.loads(val) self.assertAlmostEqual(lastGPS['latitude'], self.lat, delta=0.001) self.assertAlmostEqual(lastGPS['longitude'], self.lon, delta=0.001) self.assertAlmostEqual(lastGPS['altitude'], self.alt, delta=0.001) From 97468e4fa4a643a976681df4fa5e8c86857423fd Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 03:48:52 -0400 Subject: [PATCH 295/325] [TIZI/TICI] ui: remove calibration reset dialog on model change (#1942) --- .../ui/sunnypilot/layouts/settings/models.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index e083dfb079..becdceaaf0 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -178,27 +178,14 @@ class ModelsLayout(Widget): # circled_slash is authored grey; tinting it again only darkens it return {"name": name, "text_color": rl.GRAY, "icon": "icons/circled_slash.png", "icon_color": rl.WHITE} - @staticmethod - def _show_reset_params_dialog(): - def _callback(response): - if response == DialogResult.CONFIRM: - ui_state.params.remove("CalibrationParams") - ui_state.params.remove("LiveTorqueParameters") - msg = tr("Model download has started in the background. We suggest resetting calibration. Would you like to do that now?") - dialog = ConfirmDialog(msg, tr("Reset Calibration"), callback=_callback) - gui_app.push_widget(dialog) - def _on_model_selected(self, result): if result != DialogResult.CONFIRM: return selected_ref = self.model_dialog.selection_ref if selected_ref == "Default": ui_state.params.remove("ModelManager_ActiveBundle") - self._show_reset_params_dialog() elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None): ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index) - if self.model_manager.activeBundle and selected_bundle.generation != self.model_manager.activeBundle.generation: - self._show_reset_params_dialog() self.model_dialog = None @staticmethod From 211f990f6bac22f8b1c3d777ac4b67f17565c77f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 04:04:46 -0400 Subject: [PATCH 296/325] models: fix sunnylink default model display and false big model re-downloading (#1941) * big needs small * no download * actually * send it --- openpilot/sunnypilot/models/manager.py | 15 ++++++++++++--- .../sunnypilot/sunnylink/athena/sunnylinkd.py | 8 ++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 7ba805ff95..8ca8775875 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -143,13 +143,17 @@ class ModelManagerSP: is_cached = False if len(artifact.chunks) > 0: from openpilot.common.file_chunker import get_chunk_name + num_chunks = len(artifact.chunks) chunks_valid = True for i, chunk in enumerate(artifact.chunks): - chunk_path = get_chunk_name(full_path, i, len(artifact.chunks)) + chunk_path = get_chunk_name(full_path, i, num_chunks) if not await verify_file(chunk_path, chunk.sha256): chunks_valid = False break - if chunks_valid and len(artifact.chunks) > 0: + artifact.downloadProgress.progress = ((i + 1) / num_chunks) * 100 + self._sync_artifact_progress(artifact) + self._report_status() + if chunks_valid and num_chunks > 0: is_cached = True else: if await verify_file(full_path, expected_hash): @@ -216,6 +220,9 @@ class ModelManagerSP: """Downloads all models in a bundle""" self.selected_bundle = model_bundle self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading + for model in self.selected_bundle.models: + model.artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading + self._report_status() os.makedirs(destination_path, exist_ok=True) try: @@ -260,7 +267,9 @@ class ModelManagerSP: self.active_bundle = get_active_bundle(self.params) if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: - if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): + if self.active_bundle and self.active_bundle.index == index_to_download: + self.params.remove("ModelManager_DownloadIndex") + elif model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) except Exception as e: diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index c0534a76bf..27be6e24ed 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,7 +28,8 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import openpilot.cereal.messaging as messaging -from openpilot.sunnypilot.models.default_model import get_default_model +from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled +from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string @@ -181,7 +182,10 @@ def getParamsMetadata() -> str: schema = generate_schema() schema["capabilities"] = generate_capabilities() schema["capability_labels"] = CAPABILITY_LABELS - schema["default_model"] = get_default_model() + # mirrors get_default_model() — ui_state unavailable in sunnylinkd process + show_big = (usbgpu_present() and usbgpu_compiled() + and (params.get_bool("UsbGpuActive") or params.get_bool("UsbGpuLoading") or params.get_bool("IsOffroad"))) + schema["default_model"] = DEFAULT_BIG_MODEL if show_big else DEFAULT_MODEL raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") except Exception: From 0de7fbf33d65b052efec8d5d1e2d810c3e5b48b2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 15:06:53 -0400 Subject: [PATCH 297/325] new --- openpilot/sunnypilot/models/default_model.py | 2 +- openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index 128426979b..a8e55f8e69 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -9,7 +9,7 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MO def get_default_model() -> str: - show_big_model = (ui_state.usbgpu and ui_state.usbgpu_compiled + show_big_model = (ui_state.usbgpu and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad())) return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index 27be6e24ed..3425a85624 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,7 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import openpilot.cereal.messaging as messaging -from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi @@ -183,9 +183,10 @@ def getParamsMetadata() -> str: schema["capabilities"] = generate_capabilities() schema["capability_labels"] = CAPABILITY_LABELS # mirrors get_default_model() — ui_state unavailable in sunnylinkd process - show_big = (usbgpu_present() and usbgpu_compiled() + show_big = (usbgpu_present() and (params.get_bool("UsbGpuActive") or params.get_bool("UsbGpuLoading") or params.get_bool("IsOffroad"))) schema["default_model"] = DEFAULT_BIG_MODEL if show_big else DEFAULT_MODEL + schema["usbgpu_active"] = params.get_bool("UsbGpuActive") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") except Exception: From 699eaf79575754bb6c3cf8360f5c1e9d7026e669 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 19:16:15 -0400 Subject: [PATCH 298/325] include them! --- .../selfdrive/ui/sunnypilot/mici/layouts/home.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py index d29e579c52..623002e8c2 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py @@ -5,11 +5,22 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.widgets.label import UnifiedLabel +RUNNER_TINYGRAD = 1 + class MiciHomeLayoutSP(MiciHomeLayout): def __init__(self): super().__init__() self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) + + def _render(self, rect): + super()._render(rect) + chestnut = ui_state.sm["deviceState"].chestnutPresent + if chestnut: + gpu_ready = ui_state.usbgpu_compiled or (ui_state.params.get("ModelRunnerTypeCache") == RUNNER_TINYGRAD) + self._egpu_icon.set_visible(gpu_ready) + self._egpu_icon_gray.set_visible(not gpu_ready) From dcddb2a0bdf4ca202f5925ff01a6acd0443c67a4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 23 Aug 2026 19:53:46 -0400 Subject: [PATCH 299/325] models: revert icon override from this branch scope --- .../selfdrive/ui/sunnypilot/mici/layouts/home.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py index 623002e8c2..d29e579c52 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py @@ -5,22 +5,11 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout -from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.widgets.label import UnifiedLabel -RUNNER_TINYGRAD = 1 - class MiciHomeLayoutSP(MiciHomeLayout): def __init__(self): super().__init__() self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) - - def _render(self, rect): - super()._render(rect) - chestnut = ui_state.sm["deviceState"].chestnutPresent - if chestnut: - gpu_ready = ui_state.usbgpu_compiled or (ui_state.params.get("ModelRunnerTypeCache") == RUNNER_TINYGRAD) - self._egpu_icon.set_visible(gpu_ready) - self._egpu_icon_gray.set_visible(not gpu_ready) From 94ed0608e6c62f33f7cf17aaa0498869e065324c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 01:40:31 -0400 Subject: [PATCH 300/325] models: use less strict chestnut detection state (#1948) --- openpilot/sunnypilot/models/fetcher.py | 17 ++++++++--------- openpilot/sunnypilot/models/manager.py | 4 +++- .../sunnypilot/sunnylink/athena/sunnylinkd.py | 7 ++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index b64e27b1ad..773eb5b95c 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -13,8 +13,6 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.common.hardware.hw import Paths from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible -from openpilot.selfdrive.modeld.helpers import usbgpu_present - from openpilot.cereal import custom @@ -149,11 +147,10 @@ class ModelFetcher: self._is_usbgpu: bool | None = None self.model_cache = ModelCache(params) self.model_url = self.MODEL_URL - self._update_model_source() - def _update_model_source(self) -> None: - """Updates what json to use based on usbgpu availability""" - is_usbgpu = usbgpu_present() + def _update_model_source(self, chestnut_present: bool) -> None: + """Updates what json to use based on chestnut hardware presence via deviceState""" + is_usbgpu = chestnut_present if is_usbgpu != self._is_usbgpu: self._is_usbgpu = is_usbgpu self.model_cache = ModelCache(self.params, suffix="_USBGPU" if is_usbgpu else "") @@ -191,9 +188,9 @@ class ModelFetcher: return None - def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: + def get_available_bundles(self, chestnut_present: bool = False) -> list[custom.ModelManagerSP.ModelBundle]: """Gets the list of available models, with smart cache handling""" - self._update_model_source() + self._update_model_source(chestnut_present) cached_data, is_expired = self.model_cache.get() if cached_data and not is_expired: @@ -210,10 +207,12 @@ class ModelFetcher: cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback") return self.model_parser.parse_models(cached_data) + if __name__ == "__main__": + from openpilot.selfdrive.modeld.helpers import usbgpu_present params = Params() model_fetcher = ModelFetcher(params) - bundles = model_fetcher.get_available_bundles() + bundles = model_fetcher.get_available_bundles(chestnut_present=usbgpu_present()) for bundle in bundles: for model in bundle.models: model_overrides = {override.key: override.value for override in bundle.overrides} diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 8ca8775875..37bcb781cf 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -30,6 +30,7 @@ class ModelManagerSP: self.params = Params() self.model_fetcher = ModelFetcher(self.params) self.pm = messaging.PubMaster(["modelManagerSP"]) + self.sm = messaging.SubMaster(["deviceState"]) self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] self.selected_bundle: custom.ModelManagerSP.ModelBundle = None self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params) @@ -262,7 +263,8 @@ class ModelManagerSP: while True: try: - self.available_models = self.model_fetcher.get_available_bundles() + self.sm.update(0) + self.available_models = self.model_fetcher.get_available_bundles(self.sm['deviceState'].chestnutPresent) validate_active_bundle(self.params, self.available_models) self.active_bundle = get_active_bundle(self.params) diff --git a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py index 3425a85624..1ab2f373ed 100755 --- a/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/openpilot/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,7 +28,6 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import openpilot.cereal.messaging as messaging -from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi @@ -182,10 +181,8 @@ def getParamsMetadata() -> str: schema = generate_schema() schema["capabilities"] = generate_capabilities() schema["capability_labels"] = CAPABILITY_LABELS - # mirrors get_default_model() — ui_state unavailable in sunnylinkd process - show_big = (usbgpu_present() - and (params.get_bool("UsbGpuActive") or params.get_bool("UsbGpuLoading") or params.get_bool("IsOffroad"))) - schema["default_model"] = DEFAULT_BIG_MODEL if show_big else DEFAULT_MODEL + schema["default_model"] = DEFAULT_MODEL + schema["default_big_model"] = DEFAULT_BIG_MODEL schema["usbgpu_active"] = params.get_bool("UsbGpuActive") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8") From 66cf334067cac6a412302b2afa3a18c29b3acbe7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 12:35:37 -0400 Subject: [PATCH 301/325] ci: unify default model build into single workflow (#1951) * ci: unify default model build into single workflow * ci: consolidate upload jobs and add tinygrad ref validation --- .../workflows/build-default-big-model.yaml | 83 ------ .github/workflows/build-default-models.yaml | 279 ++++++++++++++++++ .../workflows/sunnypilot-build-prebuilt.yaml | 8 +- 3 files changed, 283 insertions(+), 87 deletions(-) delete mode 100644 .github/workflows/build-default-big-model.yaml create mode 100644 .github/workflows/build-default-models.yaml diff --git a/.github/workflows/build-default-big-model.yaml b/.github/workflows/build-default-big-model.yaml deleted file mode 100644 index 4b05a7977d..0000000000 --- a/.github/workflows/build-default-big-model.yaml +++ /dev/null @@ -1,83 +0,0 @@ -name: Build default big model - -on: - workflow_dispatch: - -env: - HF_REPO: sunnypilot/sunnypilot_models_v1 - HF_DEFAULTS_PATH: models/defaults/big - -jobs: - resolve_name: - runs-on: ubuntu-24.04 - outputs: - model_name: ${{ steps.name.outputs.model_name }} - onnx_ref: ${{ steps.name.outputs.onnx_ref }} - steps: - - uses: actions/checkout@v4 - - id: name - run: | - NAME=$(PYTHONPATH=${{ github.workspace }} python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") - ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx) - echo "model_name=${NAME}" >> $GITHUB_OUTPUT - echo "onnx_ref=$ONNX_REF" >> $GITHUB_OUTPUT - - build_model: - needs: resolve_name - uses: ./.github/workflows/sunnypilot-build-model.yaml - with: - upstream_branch: ${{ needs.resolve_name.outputs.onnx_ref }} - custom_name: ${{ needs.resolve_name.outputs.model_name }} - target_hardware: usbgpu - secrets: inherit - - upload_defaults: - needs: [ resolve_name, build_model ] - runs-on: ubuntu-24.04 - permissions: - id-token: write - contents: write - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" - - - name: Install huggingface_hub - run: pip install --upgrade "huggingface_hub>=0.22.0" - - - name: Download artifact name - uses: actions/download-artifact@v4 - with: - name: artifact-name-${{ needs.resolve_name.outputs.model_name }} - path: artifact_name - - - name: Read artifact name - id: artifact - run: | - ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) - echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT - - - name: Download model artifact - uses: actions/download-artifact@v4 - with: - name: ${{ steps.artifact.outputs.artifact_name }} - path: output - - - name: Upload to HF and update default_models.json - env: - HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} - ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} - run: | - rm -f output/artifact_name.txt - export PYTHONPATH=$(pwd) - python3 release/ci/upload_default_model.py \ - --hf-repo "${{ env.HF_REPO }}" \ - --hf-defaults-path "${{ env.HF_DEFAULTS_PATH }}" \ - --artifact-name "$ARTIFACT_NAME" \ - --model-dir output \ - --onnx-path "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" \ - --onnx-ref "${{ needs.resolve_name.outputs.onnx_ref }}" \ - --model-name "${{ needs.resolve_name.outputs.model_name }}" \ - --tinygrad-ref "$(python3 openpilot/sunnypilot/models/tinygrad_ref.py)" \ - --run-number "${{ github.run_number }}" diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml new file mode 100644 index 0000000000..55efda4e4d --- /dev/null +++ b/.github/workflows/build-default-models.yaml @@ -0,0 +1,279 @@ +name: Build default models + +on: + workflow_dispatch: + inputs: + target: + description: 'Model target to build' + required: true + type: choice + options: + - small + - big + workflow_call: + inputs: + target: + description: 'Model target to build (small or big)' + required: true + type: string + +env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + +jobs: + resolve: + runs-on: ubuntu-24.04 + outputs: + model_name: ${{ steps.resolve.outputs.model_name }} + onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} + onnx_path: ${{ steps.resolve.outputs.onnx_path }} + hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} + target_hardware: ${{ steps.resolve.outputs.target_hardware }} + tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }} + dm_onnx_ref: ${{ steps.resolve.outputs.dm_onnx_ref }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - id: resolve + run: | + export PYTHONPATH=${{ github.workspace }} + + if [ "${{ inputs.target }}" = "big" ]; then + NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") + ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" + HF_DEFAULTS_PATH="models/defaults/big" + TARGET_HW="usbgpu" + else + NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL; print(DEFAULT_MODEL)") + ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx" + HF_DEFAULTS_PATH="models/defaults/small" + TARGET_HW="qcom" + fi + + ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH") + TINYGRAD_REF=$(python3 openpilot/sunnypilot/models/tinygrad_ref.py) + if [ -z "$TINYGRAD_REF" ]; then + echo "::error::Failed to resolve tinygrad ref" + exit 1 + fi + + DM_ONNX_REF="" + if [ "${{ inputs.target }}" = "small" ]; then + DM_ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) + fi + + echo "model_name=${NAME}" >> $GITHUB_OUTPUT + echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT + echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT + echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT + echo "target_hardware=${TARGET_HW}" >> $GITHUB_OUTPUT + echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT + echo "dm_onnx_ref=${DM_ONNX_REF}" >> $GITHUB_OUTPUT + + build_driving_model: + needs: resolve + uses: ./.github/workflows/sunnypilot-build-model.yaml + with: + upstream_branch: ${{ needs.resolve.outputs.onnx_ref }} + custom_name: ${{ needs.resolve.outputs.model_name }} + target_hardware: ${{ needs.resolve.outputs.target_hardware }} + secrets: inherit + + upload_defaults: + needs: [ resolve, build_driving_model, build_dm_model ] + if: ${{ !cancelled() && needs.build_driving_model.result == 'success' && (inputs.target != 'small' || needs.build_dm_model.result == 'success') }} + runs-on: ubuntu-24.04 + permissions: + id-token: write + contents: write + env: + DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx + steps: + - uses: actions/checkout@v4 + + - name: Pull ONNX via LFS + run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}${{ inputs.target == 'small' && ',openpilot/selfdrive/modeld/models/dmonitoring_model.onnx' || '' }}" + + - name: Install huggingface_hub + run: pip install --upgrade "huggingface_hub>=0.22.0" + + - name: Download driving artifact name + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: artifact_name + + - name: Read driving artifact name + id: artifact + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download driving model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.artifact.outputs.artifact_name }} + path: output + + - name: Upload driving model to HF + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} + run: | + rm -f output/artifact_name.txt + export PYTHONPATH=$(pwd) + python3 release/ci/upload_default_model.py \ + --hf-repo "${{ env.HF_REPO }}" \ + --hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \ + --artifact-name "$ARTIFACT_NAME" \ + --model-dir output \ + --onnx-path "${{ needs.resolve.outputs.onnx_path }}" \ + --onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \ + --model-name "${{ needs.resolve.outputs.model_name }}" \ + --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ + --run-number "${{ github.run_number }}" + + - name: Download DM artifact + if: ${{ inputs.target == 'small' }} + uses: actions/download-artifact@v4 + with: + name: dm-model-${{ github.run_number }} + path: dm_output + + - name: Generate DM metadata and upload to HF + if: ${{ inputs.target == 'small' }} + env: + HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + run: | + export PYTHONPATH=$(pwd) + python3 -c " + import json, hashlib + from pathlib import Path + from datetime import datetime, UTC + + dm_dir = Path('dm_output') + manifest = list(dm_dir.glob('*.chunkmanifest')) + assert manifest, 'No chunkmanifest found' + pkl_name = manifest[0].name.removesuffix('.chunkmanifest') + num_chunks = int(manifest[0].read_text().strip()) + + chunks = [] + for i in range(num_chunks): + chunk = dm_dir / f'{pkl_name}.chunk{i+1:02d}of{num_chunks:02d}' + chunks.append({ + 'file_name': chunk.name, + 'sha256': hashlib.sha256(chunk.read_bytes()).hexdigest() + }) + + digest = hashlib.sha256() + for c in chunks: + with open(dm_dir / c['file_name'], 'rb') as f: + while block := f.read(1024*1024): + digest.update(block) + + metadata = { + 'bundles': [{ + 'short_name': 'DMMODEL', + 'display_name': 'dmonitoring_model', + 'ref': '${{ needs.resolve.outputs.dm_onnx_ref }}', + 'runner': 'tinygrad', + 'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'models': [{ + 'type': 'chunked', + 'artifact': { + 'file_name': pkl_name, + 'download_uri': {'url': '', 'sha256': digest.hexdigest()}, + 'chunks': chunks + } + }] + }] + } + with open(dm_dir / 'metadata.json', 'w') as f: + json.dump(metadata, f, indent=2) + print('Generated DM metadata.json') + " + + python3 release/ci/upload_default_model.py \ + --hf-repo "${{ env.HF_REPO }}" \ + --hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \ + --artifact-name "dm-model-${{ github.run_number }}" \ + --model-dir dm_output \ + --onnx-path "${{ env.DM_ONNX }}" \ + --onnx-ref "${{ needs.resolve.outputs.dm_onnx_ref }}" \ + --model-name "dmonitoring_model" \ + --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ + --run-number "${{ github.run_number }}" + + build_dm_model: + needs: resolve + if: ${{ inputs.target == 'small' }} + runs-on: [self-hosted, tici] + env: + DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx + DM_PKL: openpilot/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Compile DM model + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + + taskset -c 7 env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/tinygrad_repo/examples/openpilot/compile3.py \ + ${{ github.workspace }}/${{ env.DM_ONNX }} \ + ${{ github.workspace }}/${{ env.DM_PKL }} + + - name: Chunk DM pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.DM_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked {pkl} into {len(targets)} chunks') + " + + - name: Prepare DM output + run: | + mkdir -p dm_output + cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/ + cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/ + + - name: Upload DM artifact + uses: actions/upload-artifact@v4 + with: + name: dm-model-${{ github.run_number }} + path: dm_output/ + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 84fe6b3cc1..c7232505ee 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -242,14 +242,14 @@ jobs: echo "HF defaults match repo ONNX" else echo "No matching model on HF — triggering build" - gh workflow run build-default-big-model.yaml --ref "${{ github.head_ref || github.ref_name }}" + gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=big echo "Waiting for build to start..." sleep 120 - RUN_ID=$(gh run list --workflow=build-default-big-model.yaml --branch="${{ github.head_ref || github.ref_name }}" --limit=1 --json databaseId --jq '.[0].databaseId') + RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --limit=1 --json databaseId --jq '.[0].databaseId') if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then - echo "::error::Failed to find build-default-big-model run" + echo "::error::Failed to find build-default-models run" exit 1 fi @@ -258,7 +258,7 @@ jobs: CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') if [ "$CONCLUSION" != "success" ]; then - echo "::error::build-default-big-model failed: $CONCLUSION" + echo "::error::build-default-models failed: $CONCLUSION" exit 1 fi From 2bcfed5c7120763c76f6824d0ed0d5f6423c5ed9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 15:37:46 -0400 Subject: [PATCH 302/325] ci: compile default models with stock modeld (#1954) * ci: compile default big model with stock modeld * Revert "Revert big RL model (#38627)" This reverts commit 516ec1e68203439a73f340f1d0b3b91eabc626ee. * ci: compile default small model with stock modeld compiler * Reapply "Revert big RL model (#38627)" This reverts commit d06cfabb625e16bfd8f74984b359162be4b71417. --- .github/workflows/build-default-models.yaml | 237 +++++++++++++++++++- 1 file changed, 228 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index 55efda4e4d..00a2df0efe 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -72,18 +72,237 @@ jobs: echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT echo "dm_onnx_ref=${DM_ONNX_REF}" >> $GITHUB_OUTPUT - build_driving_model: + build_small_model: needs: resolve - uses: ./.github/workflows/sunnypilot-build-model.yaml - with: - upstream_branch: ${{ needs.resolve.outputs.onnx_ref }} - custom_name: ${{ needs.resolve.outputs.model_name }} - target_hardware: ${{ needs.resolve.outputs.target_hardware }} - secrets: inherit + if: ${{ inputs.target == 'small' }} + runs-on: [self-hosted, tici] + env: + SMALL_ONNX: openpilot/selfdrive/modeld/models/driving_supercombo.onnx + SMALL_PKL: openpilot/selfdrive/modeld/models/driving_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Pull ONNX via LFS + run: git lfs pull -I "${{ env.SMALL_ONNX }}" + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Compile small model with stock compiler + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)") + + TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + + env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \ + --onnx ${{ github.workspace }}/${{ env.SMALL_ONNX }} \ + --model-size $MODEL_SIZE \ + --camera-resolutions $CAMERA_RES \ + --frame-skip $FRAME_SKIP \ + --output ${{ github.workspace }}/${{ env.SMALL_PKL }} + + - name: Chunk small pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.SMALL_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked into {len(targets)} files') + " + + - name: Prepare output + env: + MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models" + OUTPUT_DIR="${{ github.workspace }}/small_output" + PKL_BASE="driving_tinygrad.pkl" + mkdir -p "$OUTPUT_DIR" + + cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/" + cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/" + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "$MODELS_DIR" \ + --output-dir "$OUTPUT_DIR" \ + --custom-name "$MODEL_NAME" \ + --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" + + echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" + + - name: Upload small model artifact + uses: actions/upload-artifact@v4 + with: + name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + path: ${{ github.workspace }}/small_output/ + + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: ${{ github.workspace }}/small_output/artifact_name.txt + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + + build_big_model: + needs: resolve + if: ${{ inputs.target == 'big' }} + runs-on: [self-hosted, usbgpu] + env: + BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx + BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Pull big ONNX via LFS + run: git lfs pull -I "${{ env.BIG_ONNX }}" + + - name: Set environment variables + run: | + source /etc/profile + export UV_PROJECT_ENVIRONMENT=${HOME}/venv + export UV_PYTHON_PREFERENCE=managed + export UV_PYTHON_INSTALL_DIR=${HOME}/uv/python + export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT + uv sync --frozen + printenv >> $GITHUB_ENV + + - name: Disable powersave + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable + + - name: Wait for chestnut PCIe link + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + python3 -c " + import time + from openpilot.system.hardware.chestnut.flash import link_up + for i in range(10): + if link_up(): + print(f'PCIe link up after {i+1} attempt(s)') + break + time.sleep(1) + else: + raise RuntimeError('Chestnut PCIe link not ready after 10 attempts') + " + + - name: Compile big model with stock compiler + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") + FRAME_SKIP=$(python3 -c "from openpilot.selfdrive.modeld.constants import ModelConstants as MC; print(MC.MODEL_RUN_FREQ // MC.MODEL_CONTEXT_FREQ)") + + TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" + + env ${TG_FLAGS} python3 \ + ${{ github.workspace }}/openpilot/selfdrive/modeld/compile_modeld.py \ + --onnx ${{ github.workspace }}/${{ env.BIG_ONNX }} \ + --model-size $MODEL_SIZE \ + --camera-resolutions $CAMERA_RES \ + --frame-skip $FRAME_SKIP \ + --output ${{ github.workspace }}/${{ env.BIG_PKL }} + + - name: Chunk big pkl + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + python3 -c " + from openpilot.common.file_chunker import chunk_file, get_chunk_targets + import os + pkl = '${{ github.workspace }}/${{ env.BIG_PKL }}' + size = os.path.getsize(pkl) + targets = get_chunk_targets(pkl, size) + chunk_file(pkl, targets) + print(f'Chunked into {len(targets)} files') + " + + - name: Prepare output + env: + MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH=${{ github.workspace }} + MODELS_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld/models" + OUTPUT_DIR="${{ github.workspace }}/big_output" + PKL_BASE="big_driving_tinygrad.pkl" + mkdir -p "$OUTPUT_DIR" + + cp "$MODELS_DIR/${PKL_BASE}".chunk* "$OUTPUT_DIR/" + cp "$MODELS_DIR/${PKL_BASE}.chunkmanifest" "$OUTPUT_DIR/" + + python3 "${{ github.workspace }}/release/ci/model_generator.py" \ + --model-dir "$MODELS_DIR" \ + --output-dir "$OUTPUT_DIR" \ + --custom-name "$MODEL_NAME" \ + --upstream-branch "${{ needs.resolve.outputs.onnx_ref }}" + + echo "model-${MODEL_NAME}-${{ github.run_number }}" > "$OUTPUT_DIR/artifact_name.txt" + + - name: Upload big model artifact + uses: actions/upload-artifact@v4 + with: + name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + path: ${{ github.workspace }}/big_output/ + + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ needs.resolve.outputs.model_name }} + path: ${{ github.workspace }}/big_output/artifact_name.txt + + - name: Re-enable powersave + if: always() + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable upload_defaults: - needs: [ resolve, build_driving_model, build_dm_model ] - if: ${{ !cancelled() && needs.build_driving_model.result == 'success' && (inputs.target != 'small' || needs.build_dm_model.result == 'success') }} + needs: [ resolve, build_small_model, build_big_model, build_dm_model ] + if: | + ${{ + !cancelled() && + (inputs.target == 'big' && needs.build_big_model.result == 'success' || + inputs.target == 'small' && needs.build_small_model.result == 'success') && + (inputs.target != 'small' || needs.build_dm_model.result == 'success') + }} runs-on: ubuntu-24.04 permissions: id-token: write From 8e16c9babb96661eca53766d89e47f0e75d2fe08 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 16:24:19 -0400 Subject: [PATCH 303/325] ci: offload small model compilation (#1952) * ci: compile default big model with stock modeld * Revert "Revert big RL model (#38627)" This reverts commit 516ec1e68203439a73f340f1d0b3b91eabc626ee. * ci: compile default small model with stock modeld compiler * ci: offload small model compilation * Reapply "Revert big RL model (#38627)" This reverts commit d06cfabb625e16bfd8f74984b359162be4b71417. * Reapply "Revert big RL model (#38627)" This reverts commit d06cfabb625e16bfd8f74984b359162be4b71417. --- .github/workflows/build-default-models.yaml | 8 +- .../workflows/sunnypilot-build-prebuilt.yaml | 127 +++++++++++++++++- openpilot/selfdrive/modeld/SConscript | 80 +++++------ 3 files changed, 168 insertions(+), 47 deletions(-) diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index 00a2df0efe..996e89fc3e 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -31,6 +31,7 @@ jobs: target_hardware: ${{ steps.resolve.outputs.target_hardware }} tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }} dm_onnx_ref: ${{ steps.resolve.outputs.dm_onnx_ref }} + dm_onnx_date: ${{ steps.resolve.outputs.dm_onnx_date }} steps: - uses: actions/checkout@v4 with: @@ -60,8 +61,10 @@ jobs: fi DM_ONNX_REF="" + DM_ONNX_DATE="" if [ "${{ inputs.target }}" = "small" ]; then DM_ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) + DM_ONNX_DATE=$(git log -1 --format=%cd --date=format:'%B %d, %Y' -- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) fi echo "model_name=${NAME}" >> $GITHUB_OUTPUT @@ -71,6 +74,7 @@ jobs: echo "target_hardware=${TARGET_HW}" >> $GITHUB_OUTPUT echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT echo "dm_onnx_ref=${DM_ONNX_REF}" >> $GITHUB_OUTPUT + echo "dm_onnx_date=${DM_ONNX_DATE}" >> $GITHUB_OUTPUT build_small_model: needs: resolve @@ -395,7 +399,7 @@ jobs: metadata = { 'bundles': [{ 'short_name': 'DMMODEL', - 'display_name': 'dmonitoring_model', + 'display_name': 'dmonitoring_model (${{ needs.resolve.outputs.dm_onnx_date }})', 'ref': '${{ needs.resolve.outputs.dm_onnx_ref }}', 'runner': 'tinygrad', 'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), @@ -421,7 +425,7 @@ jobs: --model-dir dm_output \ --onnx-path "${{ env.DM_ONNX }}" \ --onnx-ref "${{ needs.resolve.outputs.dm_onnx_ref }}" \ - --model-name "dmonitoring_model" \ + --model-name "dmonitoring_model (${{ needs.resolve.outputs.dm_onnx_date }})" \ --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ --run-number "${{ github.run_number }}" diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index c7232505ee..2f824be964 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -165,7 +165,7 @@ jobs: scons -j1 cache_dir="$SCONS_CACHE" --minimal \ openpilot/selfdrive/locationd openpilot/sunnypilot/selfdrive/locationd echo "Building rest of sunnypilot" - /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal + SKIP_TINYGRAD_COMPILE=1 /usr/bin/time -v scons -j$(nproc) cache_dir="$SCONS_CACHE" --minimal touch ${BUILD_DIR}/prebuilt if [[ "${{ runner.debug }}" == "1" ]]; then ls -la ${BUILD_DIR} @@ -242,12 +242,13 @@ jobs: echo "HF defaults match repo ONNX" else echo "No matching model on HF — triggering build" + TRIGGER_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=big echo "Waiting for build to start..." sleep 120 - RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --limit=1 --json databaseId --jq '.[0].databaseId') + RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --created=">$TRIGGER_TIME" --limit=1 --json databaseId --jq '.[0].databaseId') if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then echo "::error::Failed to find build-default-models run" exit 1 @@ -276,21 +277,99 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + prepare_small_models: + needs: [ prepare_strategy ] + runs-on: ubuntu-24.04 + outputs: + driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} + dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/small + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + submodules: recursive + + - run: git lfs pull -I "openpilot/selfdrive/modeld/models/driving_supercombo.onnx,openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" + + - name: Check HF defaults and build if needed + id: resolve + run: | + DRIVING_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/driving_supercombo.onnx" | cut -d' ' -f1) + DM_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" | cut -d' ' -f1) + TINYGRAD_REF=$(PYTHONPATH=${{ github.workspace }} python3 openpilot/sunnypilot/models/tinygrad_ref.py) + echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT + echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT + echo "Driving ONNX hash: $DRIVING_HASH" + echo "DM ONNX hash: $DM_HASH" + echo "tinygrad ref: $TINYGRAD_REF" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_defaults() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 + DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1 + DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + [ -n "$DM" ] && [ "$DM" != "null" ] || return 1 + } + + if check_defaults; then + echo "HF defaults match repo ONNX hashes and tinygrad ref" + else + echo "No matching models on HF — triggering build" + TRIGGER_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) + gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=small + + echo "Waiting for build to start..." + sleep 120 + + RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --created=">$TRIGGER_TIME" --limit=1 --json databaseId --jq '.[0].databaseId') + if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then + echo "::error::Failed to find build-default-models run" + exit 1 + fi + + echo "Waiting for run $RUN_ID..." + gh run watch "$RUN_ID" + + CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') + if [ "$CONCLUSION" != "success" ]; then + echo "::error::build-default-models failed: $CONCLUSION" + exit 1 + fi + + if ! check_defaults; then + echo "::error::HF defaults still don't match after build" + exit 1 + fi + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish: concurrency: - # We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name. - # This means that if multiple commits come in while we're publishing, they will be queued up and publish one after the other. - # Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time. group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }} cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} if: ${{ always() && !cancelled() && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && + needs.prepare_small_models.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) && (needs.prepare_strategy.outputs.include_big_model != 'true' || needs.prepare_chestnut.result == 'success') }} - needs: [ build, prepare_strategy, prepare_chestnut ] + needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_models ] runs-on: ubuntu-24.04 environment: ${{ needs.prepare_strategy.outputs.environment }} steps: @@ -306,6 +385,41 @@ jobs: mkdir -p ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + - name: Download small model chunks from HF + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/small + run: | + set -o pipefail + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + DEFAULTS=$(curl -fsSL "$JSON_URL") + MODELS_DIR="${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models" + + download_model_chunks() { + local ONNX_HASH="$1" + local CANONICAL="$2" + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) + if [ -z "$CHUNK_IDX" ]; then + echo "::error::Failed to parse chunk index from: $CHUNK_NAME" + exit 1 + fi + CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" + curl -fsSL -o "${MODELS_DIR}/${CANONICAL_CHUNK}" "$ENCODED_URL" + done + echo "$NUM_CHUNKS" > "${MODELS_DIR}/${CANONICAL}.chunkmanifest" + } + + download_model_chunks "${{ needs.prepare_small_models.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" + download_model_chunks "${{ needs.prepare_small_models.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" + - name: Prepare chestnut output if: ${{ needs.prepare_chestnut.result == 'success' }} run: | @@ -393,6 +507,7 @@ jobs: - build - publish - prepare_chestnut + - prepare_small_models runs-on: ubuntu-24.04 if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 30a31aae27..19ce7d5e00 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -73,44 +73,45 @@ compile_modeld_script = [ model_w, model_h = MEDMODEL_INPUT_SIZE frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ -for usbgpu in [False, True] if USBGPU else [False]: - target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath - # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU - file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) - driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) - camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) - # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. - taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' - cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' - f'--model-size {model_w}x{model_h} ' - f'--camera-resolutions {camera_res_args} ' - f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' - f'--output {target_pkl_path} --frame-skip {frame_skip}') - onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) - chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum)) - def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets): - from openpilot.system.hardware.chestnut.flash import link_up - # chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars - for _ in range(10): - if link_up(): - break - time.sleep(1) - else: - print("Chestnut not ready, skipping big model build") - return - if ret := env.Execute(command): - return ret - chunk_file(pkl, chunks) - def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): - chunk_file(pkl, chunks) - actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] - node = lenv.Command( - chunk_targets, - tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], - actions, - ) - if usbgpu: - lenv.SideEffect(usbgpu_lock, node) +if not os.getenv('SKIP_TINYGRAD_COMPILE'): + for usbgpu in [False, True] if USBGPU else [False]: + target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath + # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU + file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) + driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) + camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) + # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. + taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' + cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' + f'--model-size {model_w}x{model_h} ' + f'--camera-resolutions {camera_res_args} ' + f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' + f'--output {target_pkl_path} --frame-skip {frame_skip}') + onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) + chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum)) + def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets): + from openpilot.system.hardware.chestnut.flash import link_up + # chestnut can enumerate before its PCIe link is up due to varying 12V power behavior across cars + for _ in range(10): + if link_up(): + break + time.sleep(1) + else: + print("Chestnut not ready, skipping big model build") + return + if ret := env.Execute(command): + return ret + chunk_file(pkl, chunks) + def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): + chunk_file(pkl, chunks) + actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] + node = lenv.Command( + chunk_targets, + tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], + actions, + ) + if usbgpu: + lenv.SideEffect(usbgpu_lock, node) # get model metadata fn = File(f"models/dmonitoring_model").abspath @@ -142,4 +143,5 @@ def tg_compile(flags, model_name): Action(do_chunk, " [CHUNK] $TARGET")], ) -tg_compile(tg_flags, 'dmonitoring_model') +if not os.getenv('SKIP_TINYGRAD_COMPILE'): + tg_compile(tg_flags, 'dmonitoring_model') From 6cc5f3aad890527bee2ca85d71a43c205a69a4dc Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 20:02:48 -0400 Subject: [PATCH 304/325] ci: fix DM model build, separate HF defaults paths, nuke build races (#1956) * ci: fix DM model build, separate HF defaults paths, nuke build races * more split! * name * ci: download driving and DM model chunks into chestnut prebuilt output --- .github/workflows/build-default-models.yaml | 65 +++--- .../workflows/sunnypilot-build-prebuilt.yaml | 219 ++++++++++++------ 2 files changed, 177 insertions(+), 107 deletions(-) diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index 996e89fc3e..bf00845e27 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -10,13 +10,18 @@ on: options: - small - big + - dm workflow_call: inputs: target: - description: 'Model target to build (small or big)' + description: 'Model target to build (small, big, or dm)' required: true type: string +concurrency: + group: build-default-models-${{ inputs.target }} + cancel-in-progress: false + env: HF_REPO: sunnypilot/sunnypilot_models_v1 @@ -28,10 +33,7 @@ jobs: onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} onnx_path: ${{ steps.resolve.outputs.onnx_path }} hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} - target_hardware: ${{ steps.resolve.outputs.target_hardware }} tinygrad_ref: ${{ steps.resolve.outputs.tinygrad_ref }} - dm_onnx_ref: ${{ steps.resolve.outputs.dm_onnx_ref }} - dm_onnx_date: ${{ steps.resolve.outputs.dm_onnx_date }} steps: - uses: actions/checkout@v4 with: @@ -45,12 +47,14 @@ jobs: NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL; print(DEFAULT_BIG_MODEL)") ONNX_PATH="openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" HF_DEFAULTS_PATH="models/defaults/big" - TARGET_HW="usbgpu" + elif [ "${{ inputs.target }}" = "dm" ]; then + ONNX_PATH="openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" + HF_DEFAULTS_PATH="models/defaults/dm" + NAME="dmonitoring_model ($(git log -1 --format=%cd --date=format:'%B %d, %Y' -- "$ONNX_PATH"))" else NAME=$(python3 -c "from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL; print(DEFAULT_MODEL)") ONNX_PATH="openpilot/selfdrive/modeld/models/driving_supercombo.onnx" HF_DEFAULTS_PATH="models/defaults/small" - TARGET_HW="qcom" fi ONNX_REF=$(git log -1 --format='%H' -- "$ONNX_PATH") @@ -60,21 +64,11 @@ jobs: exit 1 fi - DM_ONNX_REF="" - DM_ONNX_DATE="" - if [ "${{ inputs.target }}" = "small" ]; then - DM_ONNX_REF=$(git log -1 --format='%H' -- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) - DM_ONNX_DATE=$(git log -1 --format=%cd --date=format:'%B %d, %Y' -- openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) - fi - echo "model_name=${NAME}" >> $GITHUB_OUTPUT echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT - echo "target_hardware=${TARGET_HW}" >> $GITHUB_OUTPUT echo "tinygrad_ref=${TINYGRAD_REF}" >> $GITHUB_OUTPUT - echo "dm_onnx_ref=${DM_ONNX_REF}" >> $GITHUB_OUTPUT - echo "dm_onnx_date=${DM_ONNX_DATE}" >> $GITHUB_OUTPUT build_small_model: needs: resolve @@ -304,43 +298,45 @@ jobs: ${{ !cancelled() && (inputs.target == 'big' && needs.build_big_model.result == 'success' || - inputs.target == 'small' && needs.build_small_model.result == 'success') && - (inputs.target != 'small' || needs.build_dm_model.result == 'success') + inputs.target == 'small' && needs.build_small_model.result == 'success' || + inputs.target == 'dm' && needs.build_dm_model.result == 'success') }} runs-on: ubuntu-24.04 permissions: id-token: write contents: write - env: - DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx steps: - uses: actions/checkout@v4 - name: Pull ONNX via LFS - run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}${{ inputs.target == 'small' && ',openpilot/selfdrive/modeld/models/dmonitoring_model.onnx' || '' }}" + run: git lfs pull -I "${{ needs.resolve.outputs.onnx_path }}" - name: Install huggingface_hub run: pip install --upgrade "huggingface_hub>=0.22.0" - - name: Download driving artifact name + - name: Download artifact name + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} uses: actions/download-artifact@v4 with: name: artifact-name-${{ needs.resolve.outputs.model_name }} path: artifact_name - - name: Read driving artifact name + - name: Read artifact name + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} id: artifact run: | ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT - - name: Download driving model artifact + - name: Download model artifact + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} uses: actions/download-artifact@v4 with: name: ${{ steps.artifact.outputs.artifact_name }} path: output - - name: Upload driving model to HF + - name: Upload model to HF + if: ${{ inputs.target == 'small' || inputs.target == 'big' }} env: HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} @@ -359,14 +355,14 @@ jobs: --run-number "${{ github.run_number }}" - name: Download DM artifact - if: ${{ inputs.target == 'small' }} + if: ${{ inputs.target == 'dm' }} uses: actions/download-artifact@v4 with: name: dm-model-${{ github.run_number }} path: dm_output - name: Generate DM metadata and upload to HF - if: ${{ inputs.target == 'small' }} + if: ${{ inputs.target == 'dm' }} env: HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} run: | @@ -399,8 +395,8 @@ jobs: metadata = { 'bundles': [{ 'short_name': 'DMMODEL', - 'display_name': 'dmonitoring_model (${{ needs.resolve.outputs.dm_onnx_date }})', - 'ref': '${{ needs.resolve.outputs.dm_onnx_ref }}', + 'display_name': '${{ needs.resolve.outputs.model_name }}', + 'ref': '${{ needs.resolve.outputs.onnx_ref }}', 'runner': 'tinygrad', 'build_time': datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ'), 'models': [{ @@ -423,15 +419,15 @@ jobs: --hf-defaults-path "${{ needs.resolve.outputs.hf_defaults_path }}" \ --artifact-name "dm-model-${{ github.run_number }}" \ --model-dir dm_output \ - --onnx-path "${{ env.DM_ONNX }}" \ - --onnx-ref "${{ needs.resolve.outputs.dm_onnx_ref }}" \ - --model-name "dmonitoring_model (${{ needs.resolve.outputs.dm_onnx_date }})" \ + --onnx-path "${{ needs.resolve.outputs.onnx_path }}" \ + --onnx-ref "${{ needs.resolve.outputs.onnx_ref }}" \ + --model-name "${{ needs.resolve.outputs.model_name }}" \ --tinygrad-ref "${{ needs.resolve.outputs.tinygrad_ref }}" \ --run-number "${{ github.run_number }}" build_dm_model: needs: resolve - if: ${{ inputs.target == 'small' }} + if: ${{ inputs.target == 'dm' }} runs-on: [self-hosted, tici] env: DM_ONNX: openpilot/selfdrive/modeld/models/dmonitoring_model.onnx @@ -441,6 +437,9 @@ jobs: with: submodules: recursive + - name: Pull DM ONNX via LFS + run: git lfs pull -I "${{ env.DM_ONNX }}" + - name: Set environment variables run: | source /etc/profile diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 2f824be964..8c954a7273 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -240,34 +240,24 @@ jobs: if check_hash; then echo "HF defaults match repo ONNX" - else - echo "No matching model on HF — triggering build" - TRIGGER_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) - gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=big - - echo "Waiting for build to start..." - sleep 120 - - RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --created=">$TRIGGER_TIME" --limit=1 --json databaseId --jq '.[0].databaseId') - if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then - echo "::error::Failed to find build-default-models run" - exit 1 - fi - - echo "Waiting for run $RUN_ID..." - gh run watch "$RUN_ID" - - CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') - if [ "$CONCLUSION" != "success" ]; then - echo "::error::build-default-models failed: $CONCLUSION" - exit 1 - fi - - if ! check_hash; then - echo "::error::HF defaults still don't match after build" - exit 1 - fi + exit 0 fi + + echo "No matching model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=big + + echo "Polling HF for big model availability..." + for i in $(seq 1 90); do + sleep 30 + if check_hash; then + echo "Big model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/90: not yet available" + done + + echo "::error::Big model not available on HF after 45 minutes" + exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -277,12 +267,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - prepare_small_models: + prepare_small_model: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 outputs: driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} - dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} env: HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/small @@ -292,18 +281,15 @@ jobs: ref: ${{ github.head_ref || github.ref_name }} submodules: recursive - - run: git lfs pull -I "openpilot/selfdrive/modeld/models/driving_supercombo.onnx,openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" + - run: git lfs pull -I "openpilot/selfdrive/modeld/models/driving_supercombo.onnx" - name: Check HF defaults and build if needed id: resolve run: | DRIVING_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/driving_supercombo.onnx" | cut -d' ' -f1) - DM_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" | cut -d' ' -f1) TINYGRAD_REF=$(PYTHONPATH=${{ github.workspace }} python3 openpilot/sunnypilot/models/tinygrad_ref.py) echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT - echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT echo "Driving ONNX hash: $DRIVING_HASH" - echo "DM ONNX hash: $DM_HASH" echo "tinygrad ref: $TINYGRAD_REF" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" @@ -314,40 +300,92 @@ jobs: [ "$TINYGRAD_MATCH" = "true" ] || return 1 DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) [ -n "$DRIVING" ] && [ "$DRIVING" != "null" ] || return 1 + } + + if check_defaults; then + echo "HF defaults match repo ONNX hash and tinygrad ref" + exit 0 + fi + + echo "No matching model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=small + + echo "Polling HF for model availability..." + for i in $(seq 1 60); do + sleep 30 + if check_defaults; then + echo "Model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/60: not yet available" + done + + echo "::error::Small driving model not available on HF after 30 minutes" + exit 1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Cancel run on failure + if: failure() + run: gh run cancel ${{ github.run_id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + prepare_dm_model: + needs: [ prepare_strategy ] + runs-on: ubuntu-24.04 + outputs: + dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 + HF_DEFAULTS_PATH: models/defaults/dm + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + submodules: recursive + + - run: git lfs pull -I "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" + + - name: Check HF defaults and build if needed + id: resolve + run: | + DM_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" | cut -d' ' -f1) + TINYGRAD_REF=$(PYTHONPATH=${{ github.workspace }} python3 openpilot/sunnypilot/models/tinygrad_ref.py) + echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT + echo "DM ONNX hash: $DM_HASH" + echo "tinygrad ref: $TINYGRAD_REF" + + JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" + + check_defaults() { + DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) [ -n "$DM" ] && [ "$DM" != "null" ] || return 1 } if check_defaults; then - echo "HF defaults match repo ONNX hashes and tinygrad ref" - else - echo "No matching models on HF — triggering build" - TRIGGER_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) - gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=small - - echo "Waiting for build to start..." - sleep 120 - - RUN_ID=$(gh run list --workflow=build-default-models.yaml --branch="${{ github.head_ref || github.ref_name }}" --created=">$TRIGGER_TIME" --limit=1 --json databaseId --jq '.[0].databaseId') - if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then - echo "::error::Failed to find build-default-models run" - exit 1 - fi - - echo "Waiting for run $RUN_ID..." - gh run watch "$RUN_ID" - - CONCLUSION=$(gh run view "$RUN_ID" --json conclusion --jq '.conclusion') - if [ "$CONCLUSION" != "success" ]; then - echo "::error::build-default-models failed: $CONCLUSION" - exit 1 - fi - - if ! check_defaults; then - echo "::error::HF defaults still don't match after build" - exit 1 - fi + echo "HF defaults match DM ONNX hash and tinygrad ref" + exit 0 fi + + echo "No matching DM model on HF — dispatching build" + gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=dm + + echo "Polling HF for DM model availability..." + for i in $(seq 1 60); do + sleep 30 + if check_defaults; then + echo "DM model available on HF after $((i * 30))s" + exit 0 + fi + echo "Poll $i/60: not yet available" + done + + echo "::error::DM model not available on HF after 30 minutes" + exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -365,11 +403,12 @@ jobs: always() && !cancelled() && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && - needs.prepare_small_models.result == 'success' && + needs.prepare_small_model.result == 'success' && + needs.prepare_dm_model.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) && (needs.prepare_strategy.outputs.include_big_model != 'true' || needs.prepare_chestnut.result == 'success') }} - needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_models ] + needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ] runs-on: ubuntu-24.04 environment: ${{ needs.prepare_strategy.outputs.environment }} steps: @@ -385,19 +424,19 @@ jobs: mkdir -p ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} - - name: Download small model chunks from HF + - name: Download default model chunks from HF env: HF_REPO: sunnypilot/sunnypilot_models_v1 - HF_DEFAULTS_PATH: models/defaults/small run: | set -o pipefail - JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" - DEFAULTS=$(curl -fsSL "$JSON_URL") MODELS_DIR="${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models" download_model_chunks() { - local ONNX_HASH="$1" - local CANONICAL="$2" + local DEFAULTS_PATH="$1" + local ONNX_HASH="$2" + local CANONICAL="$3" + local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${DEFAULTS_PATH}/default_models.json" + local DEFAULTS=$(curl -fsSL "$JSON_URL") BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') @@ -417,8 +456,8 @@ jobs: echo "$NUM_CHUNKS" > "${MODELS_DIR}/${CANONICAL}.chunkmanifest" } - download_model_chunks "${{ needs.prepare_small_models.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" - download_model_chunks "${{ needs.prepare_small_models.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" + download_model_chunks "models/defaults/small" "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" + download_model_chunks "models/defaults/dm" "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" - name: Prepare chestnut output if: ${{ needs.prepare_chestnut.result == 'success' }} @@ -453,10 +492,41 @@ jobs: echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - - name: Inject big model into chestnut + - name: Inject models into chestnut if: ${{ needs.prepare_chestnut.result == 'success' }} + env: + HF_REPO: sunnypilot/sunnypilot_models_v1 run: | - cp big_model_chunks/* "${{ github.workspace }}/chestnut_output/openpilot/selfdrive/modeld/models/" + CHESTNUT_MODELS="${{ github.workspace }}/chestnut_output/openpilot/selfdrive/modeld/models" + cp big_model_chunks/* "$CHESTNUT_MODELS/" + + download_model_chunks() { + local DEFAULTS_PATH="$1" + local ONNX_HASH="$2" + local CANONICAL="$3" + local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${DEFAULTS_PATH}/default_models.json" + local DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) + if [ -z "$CHUNK_IDX" ]; then + echo "::error::Failed to parse chunk index from: $CHUNK_NAME" + exit 1 + fi + CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" + curl -fsSL -o "${CHESTNUT_MODELS}/${CANONICAL_CHUNK}" "$ENCODED_URL" + done + echo "$NUM_CHUNKS" > "${CHESTNUT_MODELS}/${CANONICAL}.chunkmanifest" + } + + download_model_chunks "models/defaults/small" "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" + download_model_chunks "models/defaults/dm" "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" - name: Configure Git run: | @@ -507,7 +577,8 @@ jobs: - build - publish - prepare_chestnut - - prepare_small_models + - prepare_small_model + - prepare_dm_model runs-on: ubuntu-24.04 if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' From d14d0b1dd04d2320e49e80e6ecbcb6906752cb66 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 21:48:53 -0400 Subject: [PATCH 305/325] ci: parallelize models chunk downloads and split branch publishing (#1955) * ci: parallelize model chunk downloads and better publish * ci: download all model chunks in parallel with xargs -P8 * split split * ew * must require --- .../download-hf-model-chunks/action.yml | 66 ++++++ .../workflows/sunnypilot-build-prebuilt.yaml | 189 +++++++----------- 2 files changed, 136 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/download-hf-model-chunks/action.yml diff --git a/.github/workflows/download-hf-model-chunks/action.yml b/.github/workflows/download-hf-model-chunks/action.yml new file mode 100644 index 0000000000..01ab5385da --- /dev/null +++ b/.github/workflows/download-hf-model-chunks/action.yml @@ -0,0 +1,66 @@ +name: Download HF model chunks +description: Resolve and download model chunks from HuggingFace in parallel + +inputs: + hf_repo: + description: HuggingFace dataset repo + required: true + models: + description: 'JSON array of {hf_path, onnx_hash, canonical} objects' + required: true + dest_dir: + description: Destination directory for downloaded chunks + required: true + +runs: + using: composite + steps: + - name: Download model chunks + shell: bash + env: + HF_REPO: ${{ inputs.hf_repo }} + MODELS_JSON: ${{ inputs.models }} + DEST_DIR: ${{ inputs.dest_dir }} + run: | + set -eo pipefail + DOWNLOAD_LIST=$(mktemp) + + resolve_chunks() { + local HF_PATH="$1" ONNX_HASH="$2" CANONICAL="$3" DEST_DIR="$4" + local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_PATH}/default_models.json" + local DEFAULTS BUNDLE ARTIFACT BASE_URL NUM_CHUNKS + DEFAULTS=$(curl -fsSL "$JSON_URL") + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') + ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') + BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') + NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') + + mkdir -p "$DEST_DIR" + while IFS= read -r CHUNK_NAME; do + CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) + if [ -z "$CHUNK_IDX" ]; then + echo "::error::Failed to parse chunk index from: $CHUNK_NAME" + return 1 + fi + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") + printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST" + done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name') + echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest" + } + + echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do + HF_PATH=$(echo "$model" | jq -r '.hf_path') + ONNX_HASH=$(echo "$model" | jq -r '.onnx_hash') + CANONICAL=$(echo "$model" | jq -r '.canonical') + resolve_chunks "$HF_PATH" "$ONNX_HASH" "$CANONICAL" "$DEST_DIR" + done + + TOTAL=$(wc -l < "$DOWNLOAD_LIST") + echo "Downloading $TOTAL chunks with 8 parallel connections..." + xargs -P8 -d'\n' -I{} bash -c ' + URL="${1%% *}" + DEST="${1#* }" + echo "Downloading $(basename "$DEST")" + curl -fsSL --retry 3 --retry-delay 5 -o "$DEST" "$URL" + ' _ {} < "$DOWNLOAD_LIST" + rm -f "$DOWNLOAD_LIST" diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 8c954a7273..3288963ab4 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -424,109 +424,16 @@ jobs: mkdir -p ${{ env.OUTPUT_DIR }} tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} - - name: Download default model chunks from HF - env: - HF_REPO: sunnypilot/sunnypilot_models_v1 - run: | - set -o pipefail - MODELS_DIR="${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models" - - download_model_chunks() { - local DEFAULTS_PATH="$1" - local ONNX_HASH="$2" - local CANONICAL="$3" - local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${DEFAULTS_PATH}/default_models.json" - local DEFAULTS=$(curl -fsSL "$JSON_URL") - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') - ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') - BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') - NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') - - echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do - CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) - if [ -z "$CHUNK_IDX" ]; then - echo "::error::Failed to parse chunk index from: $CHUNK_NAME" - exit 1 - fi - CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" - ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") - echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" - curl -fsSL -o "${MODELS_DIR}/${CANONICAL_CHUNK}" "$ENCODED_URL" - done - echo "$NUM_CHUNKS" > "${MODELS_DIR}/${CANONICAL}.chunkmanifest" - } - - download_model_chunks "models/defaults/small" "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" - download_model_chunks "models/defaults/dm" "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" - - - name: Prepare chestnut output - if: ${{ needs.prepare_chestnut.result == 'success' }} - run: | - mkdir -p "${{ github.workspace }}/chestnut_output" - tar xzf prebuilt.tar.gz -C "${{ github.workspace }}/chestnut_output" - - - name: Download big model chunks from HF - if: ${{ needs.prepare_chestnut.result == 'success' }} - env: - HF_REPO: sunnypilot/sunnypilot_models_v1 - HF_DEFAULTS_PATH: models/defaults/big - run: | - ONNX_HASH="${{ needs.prepare_chestnut.outputs.onnx_sha256 }}" - JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" - DEFAULTS=$(curl -fsSL "$JSON_URL") - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') - - mkdir -p big_model_chunks - ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') - BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') - NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') - - CANONICAL="big_driving_tinygrad.pkl" - echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do - CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+') - CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" - ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") - echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" - curl -fsSL -o "big_model_chunks/${CANONICAL_CHUNK}" "$ENCODED_URL" - done - - echo "$NUM_CHUNKS" > "big_model_chunks/${CANONICAL}.chunkmanifest" - - - name: Inject models into chestnut - if: ${{ needs.prepare_chestnut.result == 'success' }} - env: - HF_REPO: sunnypilot/sunnypilot_models_v1 - run: | - CHESTNUT_MODELS="${{ github.workspace }}/chestnut_output/openpilot/selfdrive/modeld/models" - cp big_model_chunks/* "$CHESTNUT_MODELS/" - - download_model_chunks() { - local DEFAULTS_PATH="$1" - local ONNX_HASH="$2" - local CANONICAL="$3" - local JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${DEFAULTS_PATH}/default_models.json" - local DEFAULTS=$(curl -fsSL "$JSON_URL") - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)') - ARTIFACT=$(echo "$BUNDLE" | jq -r '.models[0].artifact') - BASE_URL=$(echo "$ARTIFACT" | jq -r '.download_uri.url' | sed 's|/[^/]*$||') - NUM_CHUNKS=$(echo "$ARTIFACT" | jq -r '.chunks | length') - - echo "$ARTIFACT" | jq -r '.chunks[].file_name' | while read CHUNK_NAME; do - CHUNK_IDX=$(echo "$CHUNK_NAME" | grep -oP 'chunk\K[0-9]+of[0-9]+' || true) - if [ -z "$CHUNK_IDX" ]; then - echo "::error::Failed to parse chunk index from: $CHUNK_NAME" - exit 1 - fi - CANONICAL_CHUNK="${CANONICAL}.chunk${CHUNK_IDX}" - ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${CHUNK_NAME}', safe=':/'))") - echo "Downloading $CHUNK_NAME -> $CANONICAL_CHUNK" - curl -fsSL -o "${CHESTNUT_MODELS}/${CANONICAL_CHUNK}" "$ENCODED_URL" - done - echo "$NUM_CHUNKS" > "${CHESTNUT_MODELS}/${CANONICAL}.chunkmanifest" - } - - download_model_chunks "models/defaults/small" "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}" "driving_tinygrad.pkl" - download_model_chunks "models/defaults/dm" "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}" "dmonitoring_model_tinygrad.pkl" + - name: Download model chunks from HF + uses: ./.github/workflows/download-hf-model-chunks + with: + hf_repo: sunnypilot/sunnypilot_models_v1 + dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models + models: | + [ + {"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"}, + {"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"} + ] - name: Configure Git run: | @@ -548,22 +455,6 @@ jobs: "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" - - name: Publish chestnut branch - if: ${{ needs.prepare_chestnut.result == 'success' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut" - CHESTNUT_DIR="${{ github.workspace }}/chestnut_output" - - ${{ env.CI_DIR }}/publish.sh \ - "${{ github.workspace }}" \ - "$CHESTNUT_DIR" \ - "$CHESTNUT_BRANCH" \ - "${{ needs.prepare_strategy.outputs.version }}" \ - "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ - "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" - - name: Tag ${{ needs.prepare_strategy.outputs.environment }} if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} run: | @@ -571,11 +462,71 @@ jobs: git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.prepare_strategy.outputs.build }}." git push -f origin ${TAG} + publish_chestnut: + concurrency: + group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}-chestnut + cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} + if: ${{ + always() && !cancelled() && + needs.build.result == 'success' && + needs.prepare_strategy.result == 'success' && + needs.prepare_small_model.result == 'success' && + needs.prepare_dm_model.result == 'success' && + needs.prepare_chestnut.result == 'success' && + (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} + needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Download prebuilt artifact + uses: actions/download-artifact@v4 + with: + name: prebuilt + + - name: Untar prebuilt + run: | + mkdir -p ${{ env.OUTPUT_DIR }} + tar xzf prebuilt.tar.gz -C ${{ env.OUTPUT_DIR }} + + - name: Download model chunks from HF + uses: ./.github/workflows/download-hf-model-chunks + with: + hf_repo: sunnypilot/sunnypilot_models_v1 + dest_dir: ${{ env.OUTPUT_DIR }}/openpilot/selfdrive/modeld/models + models: | + [ + {"hf_path": "models/defaults/small", "onnx_hash": "${{ needs.prepare_small_model.outputs.driving_onnx_sha256 }}", "canonical": "driving_tinygrad.pkl"}, + {"hf_path": "models/defaults/dm", "onnx_hash": "${{ needs.prepare_dm_model.outputs.dm_onnx_sha256 }}", "canonical": "dmonitoring_model_tinygrad.pkl"}, + {"hf_path": "models/defaults/big", "onnx_hash": "${{ needs.prepare_chestnut.outputs.onnx_sha256 }}", "canonical": "big_driving_tinygrad.pkl"} + ] + + - name: Configure Git + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Publish chestnut branch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CHESTNUT_BRANCH="${{ needs.prepare_strategy.outputs.new_branch }}-chestnut" + + ${{ env.CI_DIR }}/publish.sh \ + "${{ github.workspace }}" \ + "${{ env.OUTPUT_DIR }}" \ + "$CHESTNUT_BRANCH" \ + "${{ needs.prepare_strategy.outputs.version }}" \ + "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ + "${{ needs.prepare_strategy.outputs.extra_version_identifier }}" + notify: needs: - prepare_strategy - build - publish + - publish_chestnut - prepare_chestnut - prepare_small_model - prepare_dm_model From 19f83b274fceeaa56ec5090d0497046d604be027 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 22:06:20 -0400 Subject: [PATCH 306/325] ci: identical environment for publish_chestnut prebuilt --- .github/workflows/sunnypilot-build-prebuilt.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 3288963ab4..ac1dd3eca9 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -477,6 +477,7 @@ jobs: }} needs: [ build, prepare_strategy, prepare_chestnut, prepare_small_model, prepare_dm_model ] runs-on: ubuntu-24.04 + environment: ${{ needs.prepare_strategy.outputs.environment }} steps: - uses: actions/checkout@v4 From 2ba91d2be5cc91813762ffe6af3243728e7799ed Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 22:42:45 -0400 Subject: [PATCH 307/325] ci: add tinygrad ref check to prepare_chestnut and even faster prebuilt stages (#1957) * ci: faster prebuilt stages * tg check chestnut * zoomer! --- .../workflows/sunnypilot-build-prebuilt.yaml | 87 ++++++++++--------- release/ci/publish.sh | 2 +- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index ac1dd3eca9..c93dbe02c1 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -39,6 +39,8 @@ jobs: include_big_model: ${{ steps.strategy.outputs.include_big_model }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Extract deploy strategy id: strategy run: | @@ -96,6 +98,8 @@ jobs: }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Wait for Tests uses: ./.github/workflows/wait-for-action # Path to where you place the action with: @@ -119,6 +123,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + fetch-depth: 1 submodules: recursive ref: ${{ env.SOURCE_BRANCH }} repository: ${{ github.event.pull_request.head.repo.fork && github.event.pull_request.head.repo.full_name || github.repository }} @@ -214,42 +219,44 @@ jobs: outputs: onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }} env: + GH_REPO: ${{ github.repository }} HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/big steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref || github.ref_name }} - - - run: git lfs pull -I "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" - - - name: Check HF defaults and build if needed + - name: Resolve ONNX hash and tinygrad ref via API id: resolve run: | - ACTUAL_ONNX_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx" | cut -d' ' -f1) - echo "Repo ONNX hash: $ACTUAL_ONNX_HASH" - echo "onnx_sha256=$ACTUAL_ONNX_HASH" >> $GITHUB_OUTPUT + REF="${{ github.head_ref || github.ref_name }}" + + ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + echo "ONNX hash: $ONNX_HASH" + echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') + echo "tinygrad ref: $TINYGRAD_REF" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" - check_hash() { + check_defaults() { DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 - BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ACTUAL_ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) + TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) + [ "$TINYGRAD_MATCH" = "true" ] || return 1 + BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) [ -n "$BUNDLE" ] && [ "$BUNDLE" != "null" ] } - if check_hash; then - echo "HF defaults match repo ONNX" + if check_defaults; then + echo "HF defaults match repo ONNX hash and tinygrad ref" exit 0 fi echo "No matching model on HF — dispatching build" - gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=big + gh workflow run build-default-models.yaml --ref "$REF" -f target=big echo "Polling HF for big model availability..." for i in $(seq 1 90); do sleep 30 - if check_hash; then + if check_defaults; then echo "Big model available on HF after $((i * 30))s" exit 0 fi @@ -273,23 +280,20 @@ jobs: outputs: driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} env: + GH_REPO: ${{ github.repository }} HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/small steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref || github.ref_name }} - submodules: recursive - - - run: git lfs pull -I "openpilot/selfdrive/modeld/models/driving_supercombo.onnx" - - - name: Check HF defaults and build if needed + - name: Resolve ONNX hash and tinygrad ref via API id: resolve run: | - DRIVING_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/driving_supercombo.onnx" | cut -d' ' -f1) - TINYGRAD_REF=$(PYTHONPATH=${{ github.workspace }} python3 openpilot/sunnypilot/models/tinygrad_ref.py) - echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT + REF="${{ github.head_ref || github.ref_name }}" + + DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "Driving ONNX hash: $DRIVING_HASH" + echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') echo "tinygrad ref: $TINYGRAD_REF" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" @@ -308,7 +312,7 @@ jobs: fi echo "No matching model on HF — dispatching build" - gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=small + gh workflow run build-default-models.yaml --ref "$REF" -f target=small echo "Polling HF for model availability..." for i in $(seq 1 60); do @@ -337,23 +341,20 @@ jobs: outputs: dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} env: + GH_REPO: ${{ github.repository }} HF_REPO: sunnypilot/sunnypilot_models_v1 HF_DEFAULTS_PATH: models/defaults/dm steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref || github.ref_name }} - submodules: recursive - - - run: git lfs pull -I "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" - - - name: Check HF defaults and build if needed + - name: Resolve ONNX hash and tinygrad ref via API id: resolve run: | - DM_HASH=$(sha256sum "openpilot/selfdrive/modeld/models/dmonitoring_model.onnx" | cut -d' ' -f1) - TINYGRAD_REF=$(PYTHONPATH=${{ github.workspace }} python3 openpilot/sunnypilot/models/tinygrad_ref.py) - echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT + REF="${{ github.head_ref || github.ref_name }}" + + DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "DM ONNX hash: $DM_HASH" + echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT + + TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') echo "tinygrad ref: $TINYGRAD_REF" JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" @@ -372,7 +373,7 @@ jobs: fi echo "No matching DM model on HF — dispatching build" - gh workflow run build-default-models.yaml --ref "${{ github.head_ref || github.ref_name }}" -f target=dm + gh workflow run build-default-models.yaml --ref "$REF" -f target=dm echo "Polling HF for DM model availability..." for i in $(seq 1 60); do @@ -413,6 +414,8 @@ jobs: environment: ${{ needs.prepare_strategy.outputs.environment }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Download prebuilt artifact uses: actions/download-artifact@v4 @@ -480,6 +483,8 @@ jobs: environment: ${{ needs.prepare_strategy.outputs.environment }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Download prebuilt artifact uses: actions/download-artifact@v4 @@ -538,6 +543,8 @@ jobs: && (fromJSON(vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES_V2)[github.head_ref || github.ref_name] != null) }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Prepare notification message id: message diff --git a/release/ci/publish.sh b/release/ci/publish.sh index fd1a61a87c..4b328a035c 100755 --- a/release/ci/publish.sh +++ b/release/ci/publish.sh @@ -47,7 +47,7 @@ git rm -rf $OUTPUT_DIR/.git || true # Doing cleanup, but it might fail if the .g git remote remove origin || true # ensure cleanup git remote add origin $GIT_ORIGIN #git push origin -d $DEV_BRANCH || true # Ensuring we delete the remote branch if it exists as we are wiping it out -git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --allow-empty -m "sunnypilot v$VERSION release" && git push -u origin $DEV_BRANCH) +git fetch --depth 1 origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --allow-empty -m "sunnypilot v$VERSION release" && git push -u origin $DEV_BRANCH) echo "[-] committing version $VERSION T=$SECONDS" git add -f . From 45814e331381c2c132912fae0807cfb783bec0a2 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:52:29 -0700 Subject: [PATCH 308/325] modeld_v2: spatial features (#1934) * modeld_v2: spatial features * Update fetcher.py * dont reshape non 4 dim arrays * realize for non compiled * Update compile_modeld.py * god dammit it was realize() * it was fucking frozen tinygrad. just need to recompile * bump * ci: add is_big flag to metadata.json to support backward compat * Update model_generator.py * Update sunnypilot-build-model.yaml * Update helpers.py * Revert "Update helpers.py" This reverts commit 3a955ca11a84486fc143219e5824d8d9b3927895. * Reapply "Update helpers.py" This reverts commit ca9c6e193326dbe99089d099e61979f7b80e0981. * models: use less strict chestnut detection state --------- Co-authored-by: Jason Wen --- .github/workflows/sunnypilot-build-model.yaml | 2 +- .../sunnypilot/modeld_v2/compile_modeld.py | 23 +++--- .../modeld_v2/tests/test_compile_modeld.py | 82 +++++++++++++++++++ openpilot/sunnypilot/models/fetcher.py | 4 +- openpilot/sunnypilot/models/helpers.py | 2 +- release/ci/model_generator.py | 7 +- 6 files changed, 104 insertions(+), 16 deletions(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index bc132ae1bf..5c2e3bf204 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -188,7 +188,7 @@ jobs: if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then echo "USBGPU build" export USBGPU=1 - TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" + TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" else echo "QCOM build" diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index 85ae57c078..17687908c8 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details. """ import argparse +import math import os import tempfile import time @@ -66,14 +67,15 @@ def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tu if desire_key: shapes['desire'] = (input_shapes[desire_key][2],) - if is_supercombo and 'features_buffer' in input_shapes: - fb = input_shapes['features_buffer'] - shapes['prev_feat'] = (fb[0], fb[2]) - for key, shape in input_shapes.items(): if key not in (desire_key, 'features_buffer') and 'img' not in key: shapes[key] = tuple(shape) + if is_supercombo and 'features_buffer' in input_shapes: + fb = input_shapes['features_buffer'] + feat_dim = math.prod(fb[2:]) + shapes['prev_feat'] = (fb[0], feat_dim) + sizes = [int(np.prod(size)) for size in shapes.values()] return shapes, sizes @@ -117,8 +119,9 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D } if features_buffer: + feat_dim = math.prod(features_buffer[2:]) feat_q_len = frame_skip * features_buffer[1] if is_supercombo else frame_skip * (features_buffer[1] - 1) + 1 - queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], features_buffer[2]), + queues['feat_q'] = Tensor(np.zeros((feat_q_len, features_buffer[0], feat_dim), dtype=np.float32), device=device).contiguous().realize() queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')}) @@ -183,14 +186,14 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, warped_dev = warped.to(Device.DEFAULT) Tensor.realize(packed_npy_inputs_dev, warped_dev) - img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn).realize() - big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn).realize() + img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn) + big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn) unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)] unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True)) desire_dev = unpacked_dict['desire'] - desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize() + desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn) inputs = {desire_key: desire_buf} for key, tensor_val in unpacked_dict.items(): @@ -199,7 +202,7 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, if 'prev_feat' in unpacked_dict: prev_feat_dev = unpacked_dict['prev_feat'] - inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).realize() + inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).reshape(input_shapes['features_buffer']) if vision_runner: vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize() @@ -211,7 +214,7 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, inputs.update({road_key: img, wide_key: big_img}) if 'features_buffer' not in inputs: - inputs['features_buffer'] = sample_skip_fn(feat_q) + inputs['features_buffer'] = sample_skip_fn(feat_q).reshape(input_shapes['features_buffer']) policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize() if 'features_buffer' not in inputs and features_slice is not None: diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py index 96bfb42638..86974b14f1 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_compile_modeld.py @@ -195,3 +195,85 @@ class TestReadFileChunkedToDisk(OpenpilotTestCase): assert out.parent == Path(d) assert out.read_bytes() == payload + + +class Test4DFeaturesBuffer(OpenpilotTestCase): + def test_get_policy_npy_shapes_4d(self): + from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes + input_shapes = { + 'desire_pulse': (1, 25, 8), + 'features_buffer': (1, 24, 32, 512), # compare 4d to 3d for regression + 'traffic_convention': (1, 2), + 'action_t': (1, 2) + } + shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=True) + assert shapes['prev_feat'] == (1, 16384) + assert sizes == [8, 2, 2, 16384] + + def test_get_policy_npy_shapes_3d(self): + from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes + input_shapes = { + 'desire_pulse': (1, 25, 8), + 'features_buffer': (1, 24, 512), + 'traffic_convention': (1, 2), + 'action_t': (1, 2) + } + shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=True) + assert shapes['prev_feat'] == (1, 512) + assert sizes == [8, 2, 2, 512] + + +class TestStockCompileModeldEquivalence(OpenpilotTestCase): + def test_get_policy_npy_shapes_matches_stock(self): + from openpilot.selfdrive.modeld.compile_modeld import get_policy_npy_shapes as stock_get_policy_npy_shapes + from openpilot.sunnypilot.modeld_v2.compile_modeld import get_policy_npy_shapes as sunny_get_policy_npy_shapes + + stock_input_shapes = { + 'desire_pulse': (1, 25, 8), + 'features_buffer': (1, 24, 512), # see below comment + 'traffic_convention': (1, 2), + 'action_t': (1, 2), + } + + stock_shapes, stock_sizes = stock_get_policy_npy_shapes(stock_input_shapes) + sunny_shapes, sunny_sizes = sunny_get_policy_npy_shapes(stock_input_shapes, is_supercombo=True) + + assert sunny_shapes == stock_shapes + assert sunny_sizes == stock_sizes + assert sunny_shapes['prev_feat'] == (1, 512) + + def test_make_input_queues_full_stock_equivalence(self): + from openpilot.selfdrive.modeld.compile_modeld import make_input_queues as stock_make_input_queues + from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues as sunny_make_supercombo_input_queues + input_shapes = { + 'img': (1, 12, 128, 256), + 'desire_pulse': (1, 25, 8), + 'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512 + 'traffic_convention': (1, 2), + 'action_t': (1, 2), + } + frame_skip = 4 + + stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY') + sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY') + assert set(sunny_queues.keys()) == set(stock_queues.keys()) + for key in stock_queues: + assert sunny_queues[key].shape == stock_queues[key].shape, \ + f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}" + assert set(sunny_npy.keys()) == set(stock_npy.keys()) + for key in stock_npy: + assert sunny_npy[key].shape == stock_npy[key].shape, \ + f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}" + + def test_make_warp_queues_stock_equivalence(self): + from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues + from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues + stock_vision_shapes = {'img': (1, 12, 128, 256)} # for now? + stock_queues, stock_npy = stock_make_warp_queues(stock_vision_shapes, frame_skip=4, device='NPY') + sunny_queues, sunny_npy = sunny_make_warp_queues(device='NPY') + + assert set(sunny_npy.keys()) == set(stock_npy.keys()) == {'tfm', 'big_tfm'} + for key in sunny_npy: + assert sunny_npy[key].shape == stock_npy[key].shape == (3, 3) + + diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index 773eb5b95c..c9e86edd0c 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -138,8 +138,8 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" - MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json" + MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json" def __init__(self, params: Params): self.params = params diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index b5c97467d3..d0fb2e37ec 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai from openpilot.common.hardware.hw import Paths # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO -REQUIRED_JSON_VERSION = 17 +REQUIRED_JSON_VERSION = 18 CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index ff9be64783..2d35d319c2 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -136,7 +136,7 @@ def generate_chunked_model(driving_pkl: Path) -> dict: def create_metadata_json(models: list, output_dir: Path, custom_name=None, short_name=None, is_20hz=False, upstream_branch="unknown", - onnx_sha256=None) -> None: + onnx_sha256=None, is_big=False) -> None: bundle_json = { "short_name": short_name, "display_name": custom_name or upstream_branch, @@ -149,6 +149,7 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short "generation": "-1", "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), "overrides": {}, + "is_big": is_big, "models": models, } @@ -186,6 +187,8 @@ if __name__ == "__main__": print(f"No driving_tinygrad.pkl found in {_output_dir}", file=sys.stderr) sys.exit(1) + is_big = _driving_pkl.name.startswith('big_') + if _pkl: new_pkl = _output_dir / f"driving_{_pkl}_tinygrad.pkl" if not new_pkl.exists(): @@ -196,4 +199,4 @@ if __name__ == "__main__": _model_metadata = generate_chunked_model(_driving_pkl) _onnx_sha256 = _hash_onnx_files(Path(args.model_dir)) create_metadata_json([_model_metadata], _output_dir, args.custom_name, _short_name, args.is_20hz, args.upstream_branch, - onnx_sha256=_onnx_sha256) + onnx_sha256=_onnx_sha256, is_big=is_big) From 760c19d3f91f79e404f36b020c5df027a5291e48 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 24 Aug 2026 23:31:52 -0400 Subject: [PATCH 309/325] ui/models: handle missing files during cache size calculation (#1958) --- .../selfdrive/ui/sunnypilot/layouts/settings/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index becdceaaf0..e4a6bea6e0 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -115,8 +115,12 @@ class ModelsLayout(Widget): def calculate_cache_size(): cache_size = 0.0 if os.path.exists(CUSTOM_MODEL_PATH): - cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2) - return cache_size + for file in os.listdir(CUSTOM_MODEL_PATH): + try: + cache_size += os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) + except OSError: + continue + return cache_size / (1024**2) def _clear_cache(self): def _callback(response): From cefe5737b9b201af26eafc0c4d426e7e15ecdd4c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 00:41:31 -0400 Subject: [PATCH 310/325] models: fix current model not updating on chestnut status (#1959) * models: preserve user model selection across reboots and power cycles * no * again * idk * over --- openpilot/sunnypilot/models/helpers.py | 24 ++++++------------------ openpilot/sunnypilot/models/manager.py | 7 ++++++- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index d0fb2e37ec..d97d655d5f 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -23,7 +23,6 @@ REQUIRED_JSON_VERSION = 18 CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' ModelManager = custom.ModelManagerSP -_LAST_VALIDATED_RAW = None def _compute_hash(file_path: str) -> str | None: @@ -86,11 +85,11 @@ def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, availa if available_bundles is not None: matching_bundle = None for bundle in available_bundles: - if getattr(active_bundle, 'ref', None) and getattr(bundle, 'ref', None): + if active_bundle.ref and bundle.ref: if active_bundle.ref == bundle.ref: matching_bundle = bundle break - elif getattr(active_bundle, 'internalName', None) == getattr(bundle, 'internalName', None): + elif active_bundle.internalName == bundle.internalName: matching_bundle = bundle break @@ -98,36 +97,25 @@ def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, availa return True if active_bundle.minimumSelectorVersion != matching_bundle.minimumSelectorVersion: return True - - active_runner = getattr(active_bundle, 'runner', None) - matching_runner = getattr(matching_bundle, 'runner', None) - if active_runner is not None and matching_runner is not None: - if getattr(active_runner, 'raw', active_runner) != getattr(matching_runner, 'raw', matching_runner): - return True + if active_bundle.runner.raw != matching_bundle.runner.raw: + return True if set(_bundle_artifacts(active_bundle)) != set(_bundle_artifacts(matching_bundle)): return True - return not _bundle_is_valid_locally(active_bundle) + # missing files trigger re-download, not selection reset + return False def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None: - global _LAST_VALIDATED_RAW - raw_bundle = params.get("ModelManager_ActiveBundle") if not raw_bundle: return - if raw_bundle == _LAST_VALIDATED_RAW: - return - active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle) if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles): cloudlog.warning("Active model bundle invalid; resetting to default") params.remove("ModelManager_ActiveBundle") params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True) - _LAST_VALIDATED_RAW = None - else: - _LAST_VALIDATED_RAW = raw_bundle def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | bytes | None = None) -> "custom.ModelManagerSP.ModelBundle | None": diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 37bcb781cf..60f01d0c81 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -257,15 +257,20 @@ class ModelManagerSP: """Main entry point for downloading a model bundle""" asyncio.run(self._download_bundle(model_bundle, destination_path)) + BOOT_SETTLE_TICKS = 10 # seconds at 1 Hz before validating active bundle + def main_thread(self) -> None: """Main thread for model management""" rk = Ratekeeper(1, print_delay_threshold=None) + boot_ticks = 0 while True: try: self.sm.update(0) self.available_models = self.model_fetcher.get_available_bundles(self.sm['deviceState'].chestnutPresent) - validate_active_bundle(self.params, self.available_models) + if boot_ticks >= self.BOOT_SETTLE_TICKS: + validate_active_bundle(self.params, self.available_models) + boot_ticks = min(boot_ticks + 1, self.BOOT_SETTLE_TICKS) self.active_bundle = get_active_bundle(self.params) if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: From 25c25047b890337059f3a6727c1525c49dca2b10 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 01:12:12 -0400 Subject: [PATCH 311/325] models: persist model selection per catalog across chestnut state changes (#1960) --- openpilot/common/params_keys.h | 2 ++ openpilot/sunnypilot/models/helpers.py | 19 ++++++++++++++++++- openpilot/sunnypilot/models/manager.py | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 01c14fb539..111099fce9 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -196,6 +196,8 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, STRING}}, + {"ModelManager_PrevBundle", {PERSISTENT, JSON}}, + {"ModelManager_PrevBundle_USBGPU", {PERSISTENT, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index d97d655d5f..e33cc445d1 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -106,14 +106,31 @@ def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, availa return False -def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None: +def _prev_bundle_key(is_usbgpu: bool) -> str: + return "ModelManager_PrevBundle_USBGPU" if is_usbgpu else "ModelManager_PrevBundle" + + +def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None, + is_usbgpu: bool = False) -> None: raw_bundle = params.get("ModelManager_ActiveBundle") if not raw_bundle: + prev = params.get(_prev_bundle_key(is_usbgpu)) + if prev and (prev_bundle := get_active_bundle(params, raw_bundle_dict=prev)) is not None: + if not _bundle_needs_reset(prev_bundle, available_bundles): + params.put("ModelManager_ActiveBundle", prev, block=True) return active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle) if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles): cloudlog.warning("Active model bundle invalid; resetting to default") + params.put(_prev_bundle_key(not is_usbgpu), raw_bundle, block=True) + + prev = params.get(_prev_bundle_key(is_usbgpu)) + if prev and (prev_bundle := get_active_bundle(params, raw_bundle_dict=prev)) is not None: + if not _bundle_needs_reset(prev_bundle, available_bundles): + params.put("ModelManager_ActiveBundle", prev, block=True) + return + params.remove("ModelManager_ActiveBundle") params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True) diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 60f01d0c81..e47cf7536c 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -267,9 +267,10 @@ class ModelManagerSP: while True: try: self.sm.update(0) - self.available_models = self.model_fetcher.get_available_bundles(self.sm['deviceState'].chestnutPresent) + chestnut_present = self.sm['deviceState'].chestnutPresent + self.available_models = self.model_fetcher.get_available_bundles(chestnut_present) if boot_ticks >= self.BOOT_SETTLE_TICKS: - validate_active_bundle(self.params, self.available_models) + validate_active_bundle(self.params, self.available_models, is_usbgpu=chestnut_present) boot_ticks = min(boot_ticks + 1, self.BOOT_SETTLE_TICKS) self.active_bundle = get_active_bundle(self.params) From b742b96c4482aced861525c33c55e1374aa8bb0c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 12:04:39 -0400 Subject: [PATCH 312/325] [MICI] ui: four-state eGPU icon for non-default big models (#1945) * ui: four-state eGPU icon for non-default big models * oops * try this out * align --- openpilot/selfdrive/ui/mici/layouts/home.py | 7 +++- .../ui/sunnypilot/mici/layouts/home.py | 38 +++++++++++++++++++ .../ui/sunnypilot/mici/onroad/hud_renderer.py | 3 ++ openpilot/selfdrive/ui/sunnypilot/ui_state.py | 2 + 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index ff8d350e08..fd74979404 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -248,8 +248,11 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) - self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) + if gui_app.sunnypilot_ui(): + self._set_egpu_visibility() + else: + self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) + self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py index d29e579c52..e2f1b4fb67 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py @@ -4,8 +4,14 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import math + +import pyray as rl + from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import FontWeight +from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel @@ -13,3 +19,35 @@ class MiciHomeLayoutSP(MiciHomeLayout): def __init__(self): super().__init__() self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) + self._egpu_icon_default = IconWidget("icons_mici/egpu.png", (50, 37)) + self._egpu_icon_default.set_visible(False) + self._egpu_icon_orange = IconWidget("icons_mici/egpu_orange.png", (50, 37)) + self._egpu_icon_orange.set_visible(False) + gray_idx = self._status_bar_layout.widgets.index(self._egpu_icon_gray) + self._status_bar_layout.widgets.insert(gray_idx + 1, self._egpu_icon_default) + self._status_bar_layout.widgets.insert(gray_idx + 2, self._egpu_icon_orange) + + def _set_egpu_visibility(self): + chestnut = ui_state.sm["deviceState"].chestnutPresent + if not chestnut: + self._egpu_icon.set_visible(False) + self._egpu_icon_default.set_visible(False) + self._egpu_icon_orange.set_visible(False) + self._egpu_icon_gray.set_visible(False) + return + + big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad + big_model_failed = ui_state.started and (ui_state.usbgpu_active is False) + loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) + + if loading: + self._egpu_icon_default._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + self._egpu_icon_default.set_visible(True) + self._egpu_icon.set_visible(False) + self._egpu_icon_orange.set_visible(False) + self._egpu_icon_gray.set_visible(False) + else: + self._egpu_icon_default.set_visible(False) + self._egpu_icon.set_visible(big_model_selected and not big_model_failed) + self._egpu_icon_orange.set_visible(big_model_selected and big_model_failed) + self._egpu_icon_gray.set_visible(not big_model_selected) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py index 9d39d01727..ad75f7e969 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py @@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators @@ -21,6 +22,8 @@ class HudRendererSP(HudRenderer): def _render(self, rect: rl.Rectangle) -> None: super()._render(rect) + if ui_state.usbgpu and not ui_state.usbgpu_compiled and ui_state.model_runner_tinygrad: + self._draw_model_source(rect) self.blind_spot_indicators.render(rect) def _has_blind_spot_detected(self) -> bool: diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 602830a4db..948253d47d 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -43,6 +43,7 @@ class UIStateSP: self.screensaver_enabled: bool = False self.active_bundle = None + self.model_runner_tinygrad: bool = False self.blindspot: bool = False self.chevron_metrics = None self.custom_interactive_timeout: int = 0 @@ -151,6 +152,7 @@ class UIStateSP: self._enforce_constraints() self.active_bundle = self.params.get("ModelManager_ActiveBundle") + self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad" self.blindspot = self.params.get_bool("BlindSpot") self.chevron_metrics = self.params.get("ChevronInfo") self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) From 78a766eb6145a416d8d95e323a13b6a914b6f9ff Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 20:38:02 -0400 Subject: [PATCH 313/325] ui: fix scrolling label speed at non-60fps refresh rates (#1967) * ui: fix scrolling label speed at non-60fps refresh rates * send it * nope * more --- openpilot/system/ui/sunnypilot/lib/utils.py | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openpilot/system/ui/sunnypilot/lib/utils.py b/openpilot/system/ui/sunnypilot/lib/utils.py index b9ed152aff..6ae30d13ae 100644 --- a/openpilot/system/ui/sunnypilot/lib/utils.py +++ b/openpilot/system/ui/sunnypilot/lib/utils.py @@ -8,12 +8,26 @@ from collections.abc import Callable import pyray as rl -from openpilot.system.ui.lib.application import FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP -from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel from openpilot.system.ui.widgets.list_view import BUTTON_WIDTH, BUTTON_HEIGHT, TEXT_PADDING, _resolve_value +SCROLL_SPEED = 1.2 # stock is 0.8, boosted 50% to compensate for larger font (50 vs 32) +SCROLL_REFERENCE_FPS = 60. + + +class UnifiedLabelSP(UnifiedLabel): + # stock scroll formula (0.8 / 60 * fps) is inverted — pre-correct so speed is constant px/sec + def _render(self, _): + if self._needs_scroll and self._scroll_state == ScrollState.SCROLLING: + fps = gui_app.target_fps + wrong_step = 0.8 / SCROLL_REFERENCE_FPS * fps + correct_step = SCROLL_SPEED * SCROLL_REFERENCE_FPS / fps + self._scroll_offset -= (correct_step - wrong_step) + super()._render(_) + class NoElideButtonAction(ButtonActionSP): def get_width_hint(self): @@ -21,14 +35,12 @@ class NoElideButtonAction(ButtonActionSP): class ScrollingButtonAction(ButtonActionSP): - """ButtonActionSP whose value scrolls instead of eliding when it doesn't fit.""" - def __init__(self, text: str | Callable[[], str], width: int = style.BUTTON_ACTION_WIDTH, enabled: bool | Callable[[], bool] = True): super().__init__(text=text, width=width, enabled=enabled) - self._value_label = UnifiedLabel("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL, - text_color=self._value_color, scroll=True, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL, + text_color=self._value_color, scroll=True, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR): if self.value != _resolve_value(value, ""): From 1d4558c067bde1cfab37c6f865eb9ddf8f1098d7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 20:47:52 -0400 Subject: [PATCH 314/325] [TIZI/TICI] sidebar: show eGPU icon when chestnut is present (#1968) * [tizi/tici] sidebar: show eGPU icon when chestnut is present * matchy match * fix --- openpilot/selfdrive/ui/layouts/sidebar.py | 9 ++++- .../ui/sunnypilot/layouts/sidebar.py | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/layouts/sidebar.py b/openpilot/selfdrive/ui/layouts/sidebar.py index f950edaa46..5429a35851 100644 --- a/openpilot/selfdrive/ui/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/layouts/sidebar.py @@ -168,9 +168,16 @@ class Sidebar(Widget, SidebarSP): # Home/Flag button flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN) button_img = self._flag_img if ui_state.started else self._home_img + button_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y) + icon_opacity = 1.0 + + if gui_app.sunnypilot_ui(): + button_img, button_pos, icon_opacity = SidebarSP._get_home_icon(self, button_img) tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL - rl.draw_texture_ex(button_img, rl.Vector2(HOME_BTN.x, HOME_BTN.y), 0.0, 1.0, tint) + if icon_opacity < 1.0: + tint = rl.Color(tint[0], tint[1], tint[2], int(255 * icon_opacity)) + rl.draw_texture_ex(button_img, button_pos, 0.0, 1.0, tint) # Microphone button if self._recording_audio: diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py index 79bb15dbb8..2c670fa221 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py @@ -4,11 +4,14 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import math + import pyray as rl import time from dataclasses import dataclass from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID +from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr_noop @@ -18,6 +21,9 @@ METRIC_MARGIN = 30 METRIC_START_Y = 300 HOME_BTN = rl.Rectangle(60, 860, 180, 180) +EGPU_ICON_WIDTH = 180 +EGPU_ICON_HEIGHT = 133 + # Color scheme class Colors: @@ -53,6 +59,10 @@ class MetricData: class SidebarSP: def __init__(self): self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING) + self._egpu_green_img = gui_app.texture("icons_mici/egpu_green.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) + self._egpu_default_img = gui_app.texture("icons_mici/egpu.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) + self._egpu_orange_img = gui_app.texture("icons_mici/egpu_orange.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) + self._egpu_gray_img = gui_app.texture("icons_mici/egpu_gray.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) def _update_sunnylink_status(self): if not ui_state.params.get_bool("SunnylinkEnabled"): @@ -78,6 +88,29 @@ class SidebarSP: self._sunnylink_status.update(tr_noop("SUNNYLINK"), status, color) + def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]: + default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y) + if not ui_state.sm["deviceState"].chestnutPresent: + return default_img, default_pos, 1.0 + + big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad + big_model_failed = ui_state.started and (ui_state.usbgpu_active is False) + loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) + + if loading: + icon = self._egpu_default_img + opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + elif big_model_selected and big_model_failed: + icon, opacity = self._egpu_orange_img, 1.0 + elif big_model_selected: + icon, opacity = self._egpu_green_img, 1.0 + else: + icon, opacity = self._egpu_gray_img, 1.0 + + x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2 + y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2 + return icon, rl.Vector2(x, y), opacity + def _draw_metrics_w_sunnylink(self, rect: rl.Rectangle, _temp, _panda, _connect): metrics = [_temp, _panda, _connect, self._sunnylink_status] start_y = int(rect.y) + METRIC_START_Y From 15f201caeddcf331ab4ff5f2b909ba0f28c3eeee Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 25 Aug 2026 21:07:44 -0400 Subject: [PATCH 315/325] ui: use full big model failure detection for sidebar and home eGPU icons (#1969) --- openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py | 2 +- openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py | 2 +- openpilot/selfdrive/ui/ui_state.py | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py index 2c670fa221..7c74c48469 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/sidebar.py @@ -94,7 +94,7 @@ class SidebarSP: return default_img, default_pos, 1.0 big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad - big_model_failed = ui_state.started and (ui_state.usbgpu_active is False) + big_model_failed = ui_state.started and ui_state.big_model_failed loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) if loading: diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py index e2f1b4fb67..b261373947 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py @@ -37,7 +37,7 @@ class MiciHomeLayoutSP(MiciHomeLayout): return big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad - big_model_failed = ui_state.started and (ui_state.usbgpu_active is False) + big_model_failed = ui_state.started and ui_state.big_model_failed loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) if loading: diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index e0aca74ff2..7e2a2494ea 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -112,6 +112,15 @@ class UIState(UIStateSP): def add_on_body_changed_callbacks(self, callback: Callable[[], None]): self._on_body_changed_callbacks.append(callback) + @property + def big_model_failed(self) -> bool: + # Mirrors the onroad HUD's four-condition check so sidebar and home icons reflect the same failure states + return (self.usbgpu_active is False or + not self.sm['deviceState'].chestnutPresent or + (self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and + not self.sm.alive['modelV2']) or + (self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame)) + @property def engaged(self) -> bool: return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled) From da28afca91a1ec0e5919a483d99508512886105c Mon Sep 17 00:00:00 2001 From: Nayan Date: Wed, 26 Aug 2026 02:34:02 -0400 Subject: [PATCH 316/325] models: dual-slot backend (qcom/usbgpu) with ref-based downloads (#1966) * models: dual-slot backend (qcom/usbgpu) with ref-based downloads * models: restore get_active_source and the usbgpu-to-qcom fallback * models: fix per-slot validation and cap mismatched-source refetches * ui/models: select models by ref and seed the usbgpu slot on migration * models: drop defensive attribute guards on capnp bundles * models: remove vestigial fetcher state and dead fallbacks * models: resolve the active bundle from the active source slot only * models: pass the usbgpu kwarg through the modeld test stubs * models: resolve the displayed model from the active slot in ui_state * models: correct the validation memo type hint * models: drop docstrings that restate the function name --------- Co-authored-by: Jason Wen --- openpilot/common/params_keys.h | 7 +- .../ui/sunnypilot/layouts/settings/models.py | 18 +- .../ui/sunnypilot/mici/layouts/models.py | 14 +- openpilot/selfdrive/ui/sunnypilot/ui_state.py | 5 +- openpilot/sunnypilot/modeld_v2/modeld.py | 2 +- .../sunnypilot/modeld_v2/tests/helpers.py | 4 +- .../tests/test_combined_pkl_loader.py | 4 +- openpilot/sunnypilot/models/fetcher.py | 106 +++-- openpilot/sunnypilot/models/helpers.py | 107 +++-- openpilot/sunnypilot/models/manager.py | 62 ++- .../models/tests/test_manager_download.py | 431 ++++++++++++++++++ .../models/tests/test_tinygrad_ref.py | 4 +- openpilot/sunnypilot/sunnylink/statsd.py | 1 + .../sunnypilot/system/params_migration.py | 18 + openpilot/sunnypilot/system/tests/__init__.py | 0 .../system/tests/test_params_migration.py | 36 ++ 16 files changed, 698 insertions(+), 121 deletions(-) create mode 100644 openpilot/sunnypilot/system/tests/__init__.py create mode 100644 openpilot/sunnypilot/system/tests/test_params_migration.py diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 111099fce9..4d8ffb64eb 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -195,11 +195,10 @@ inline static std::unordered_map keys = { // Model Manager params {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, - {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, STRING}}, - {"ModelManager_PrevBundle", {PERSISTENT, JSON}}, - {"ModelManager_PrevBundle_USBGPU", {PERSISTENT, JSON}}, + {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, + {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, - {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}}, + {"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index e4a6bea6e0..93668014f6 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -11,6 +11,8 @@ import pyray as rl from openpilot.cereal import custom from openpilot.sunnypilot.models.default_model import get_default_model +from openpilot.sunnypilot.models.fetcher import ModelFetcher +from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS from openpilot.common.constants import CV from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.lib.multilang import tr @@ -68,7 +70,7 @@ class ModelsLayout(Widget): callback=self._clear_cache ) - self.cancel_download_item = button_item(tr("Cancel Download"), tr("Cancel"), "", lambda: ui_state.params.remove("ModelManager_DownloadIndex")) + self.cancel_download_item = button_item(tr("Cancel Download"), tr("Cancel"), "", lambda: ui_state.params.remove("ModelManager_DownloadRef")) self.lane_turn_value_control = option_item_sp(tr("Adjust Lane Turn Speed"), "LaneTurnValue", 500, 2000, tr("Set the maximum speed for lane turn desires. Default is 19 mph."), @@ -146,7 +148,7 @@ class ModelsLayout(Widget): if not bundle: return - self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadIndex") is not None) + self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadRef") is not None) if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: self.last_cache_calc_time = current_time @@ -187,9 +189,10 @@ class ModelsLayout(Widget): return selected_ref = self.model_dialog.selection_ref if selected_ref == "Default": - ui_state.params.remove("ModelManager_ActiveBundle") + source = ModelFetcher.active_source(ui_state.sm["deviceState"].chestnutPresent) + ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None): - ui_state.params.put("ModelManager_DownloadIndex", selected_bundle.index) + ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) self.model_dialog = None @staticmethod @@ -227,7 +230,7 @@ class ModelsLayout(Widget): advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") live_delay: bool = ui_state.params.get_bool("LagdToggle") - camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None + camera_offset: bool = ui_state.active_bundle is not None self.lane_turn_desire_toggle.action_item.set_state(turn_desire) self.lane_turn_value_control.set_visible(turn_desire and advanced_controls) @@ -241,8 +244,9 @@ class ModelsLayout(Widget): self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] self._handle_bundle_download_progress() - default_label = f"{get_default_model()} (Default)" - active_name = self.model_manager.activeBundle.displayName if self.model_manager and self.model_manager.activeBundle.ref else default_label + # read the slot through ui_state, not modelManagerSP: the manager republishes a + # tick after a chestnut change, and the stale bundle flashes the wrong model + active_name = (ui_state.active_bundle or {}).get("displayName") or f"{get_default_model()} (Default)" self.current_model_item.action_item.set_value(active_name) if not ui_state.is_offroad(): diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index 6eff456559..87073d531f 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -8,6 +8,8 @@ import pyray as rl from openpilot.cereal import custom from openpilot.sunnypilot.models.default_model import get_default_model +from openpilot.sunnypilot.models.fetcher import ModelFetcher +from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS from openpilot.selfdrive.ui.mici.widgets.button import BigButton from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout from openpilot.selfdrive.ui.ui_state import ui_state, device @@ -60,7 +62,7 @@ class ModelsLayoutMici(NavScroller): self.select_model_btn.set_click_callback(self._show_folders) self.cancel_download_btn = BigButton(tr("cancel download")) - self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex")) + self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadRef")) self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn] self._scroller.add_widgets(self.main_items) @@ -113,11 +115,12 @@ class ModelsLayoutMici(NavScroller): gui_app.pop_widgets_to(self) def _select_model(self, bundle): - ui_state.params.put("ModelManager_DownloadIndex", bundle.index) + ui_state.params.put("ModelManager_DownloadRef", bundle.ref) self._pop_to_main() def _select_default(self): - ui_state.params.remove("ModelManager_ActiveBundle") + source = ModelFetcher.active_source(ui_state.sm["deviceState"].chestnutPresent) + ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) self._pop_to_main() def _select_folder(self, folder_name): @@ -162,8 +165,9 @@ class ModelsLayoutMici(NavScroller): self._was_downloading = is_downloading self.current_model_info.current_model_header.set_text(tr("active model")) - default_model_text = f"{get_default_model()} (Default)".lower() - model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.ref else default_model_text + # read the slot through ui_state, not modelManagerSP: the manager republishes a + # tick after a chestnut change, and the stale bundle flashes the wrong model + model_text = ((ui_state.active_bundle or {}).get("displayName") or f"{get_default_model()} (Default)").lower() self.current_model_info.current_model_text.set_text(model_text) self.current_model_info.info_header.set_text(tr("cache size")) self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB") diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 948253d47d..9bed533d3f 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -10,6 +10,7 @@ from openpilot.cereal import messaging, log, custom from opendbc.car.structs import car from openpilot.common.params import Params from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness +from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_active_source from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP @@ -151,7 +152,9 @@ class UIStateSP: self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self._enforce_constraints() - self.active_bundle = self.params.get("ModelManager_ActiveBundle") + source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active, + usbgpu_loading=self.usbgpu_loading, offroad=self.is_offroad()) + self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source]) self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad" self.blindspot = self.params.get_bool("BlindSpot") self.chevron_metrics = self.params.get("ChevronInfo") diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index 9f3d709537..d180012279 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -91,7 +91,7 @@ class ModelState(ModelStateBase): if env_pkl and os.path.exists(env_pkl): model_bundle = None else: - model_bundle = get_active_bundle() + model_bundle = get_active_bundle(usbgpu=usbgpu) self.generation = model_bundle.generation if model_bundle is not None else None overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {} diff --git a/openpilot/sunnypilot/modeld_v2/tests/helpers.py b/openpilot/sunnypilot/modeld_v2/tests/helpers.py index ee59e82785..6e66bf771a 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/helpers.py +++ b/openpilot/sunnypilot/modeld_v2/tests/helpers.py @@ -190,8 +190,8 @@ def tmp_path(): def patch_modeld(monkeypatch): def _patch(bundle): - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle) + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) return _patch diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index 3396649a1d..ccd8cbc7f3 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -59,8 +59,8 @@ class TestFindDrivingPkl(OpenpilotTestCase): class TestModelStateCombinedInit(OpenpilotTestCase): def test_asserts_when_no_pkl(self, monkeypatch): bundle = DummyBundle(models=[], is_20hz=True) - monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle) - monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle) + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) with self.assertRaisesRegex(AssertionError, "No driving pkl found"): ModelState(cam_w=CAM_W, cam_h=CAM_H) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index c9e86edd0c..1bbfb02f70 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -141,41 +141,50 @@ class ModelFetcher: MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json" MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json" + MODEL_SOURCES = { + "qcom": (MODEL_URL, ""), + "usbgpu": (MODEL_URL_USBGPU, "_USBGPU"), + } + def __init__(self, params: Params): self.params = params self.model_parser = ModelParser() - self._is_usbgpu: bool | None = None - self.model_cache = ModelCache(params) - self.model_url = self.MODEL_URL + self.model_caches = { + source: ModelCache(params, suffix=suffix) + for source, (_, suffix) in self.MODEL_SOURCES.items() + } + self._refetched: set[str] = set() + self.params.put("ModelManager_ActiveJson", { + "qcom": self.MODEL_URL, + "usbgpu": self.MODEL_URL_USBGPU, + }, block=True) - def _update_model_source(self, chestnut_present: bool) -> None: - """Updates what json to use based on chestnut hardware presence via deviceState""" - is_usbgpu = chestnut_present - if is_usbgpu != self._is_usbgpu: - self._is_usbgpu = is_usbgpu - self.model_cache = ModelCache(self.params, suffix="_USBGPU" if is_usbgpu else "") - self.model_url = self.MODEL_URL_USBGPU if is_usbgpu else self.MODEL_URL - self.params.put("ModelManager_ActiveJson", self.model_url, block=True) + @staticmethod + def active_source(chestnut_present: bool) -> str: + return "usbgpu" if chestnut_present else "qcom" - def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None: + def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None: """Fetches fresh model data from remote and updates cache. Returns None on transport errors. Raises on 404 and other fatal HTTP errors. """ + model_url, _ = self.MODEL_SOURCES[source] try: - response = requests.get(self.model_url, timeout=10) + response = requests.get(model_url, timeout=10) # Explicitly handle 404 differently if response.status_code == 404: - cloudlog.error(f"Models URL returned 404 Not Found: {self.model_url}") - raise HTTPError(f"404 Not Found: {self.model_url}", response=response) + cloudlog.error(f"Models URL returned 404 Not Found: {model_url}") + raise HTTPError(f"404 Not Found: {model_url}", response=response) # Raise for any other 4xx/5xx response.raise_for_status() json_data = response.json() - self.model_cache.set(json_data) - cloudlog.debug("Successfully updated models cache") - return self.model_parser.parse_models(json_data) + parsed = self.model_parser.parse_models(json_data) + if parsed: + self.model_caches[source].set(json_data) + cloudlog.debug(f"Successfully updated models cache for {source}") + return parsed except ConnectionError as e: cloudlog.warning(f"DNS/connection error while fetching models: {e}") @@ -188,16 +197,40 @@ class ModelFetcher: return None - def get_available_bundles(self, chestnut_present: bool = False) -> list[custom.ModelManagerSP.ModelBundle]: - """Gets the list of available models, with smart cache handling""" - self._update_model_source(chestnut_present) - cached_data, is_expired = self.model_cache.get() + @staticmethod + def _cache_matches_source(source: str, cached_data: dict) -> bool: + bundles = cached_data.get("bundles", []) + if source == "usbgpu": + return any(bundle.get("is_big") is True for bundle in bundles) + return not any(bundle.get("is_big") is True for bundle in bundles) + + def get_bundles_for_source(self, source: str) -> list[custom.ModelManagerSP.ModelBundle]: + if source not in self.MODEL_SOURCES: + cloudlog.warning(f"Unknown model source: {source}") + return [] + + cached_data, is_expired = self.model_caches[source].get() if cached_data and not is_expired: - cloudlog.debug("Using valid cached models data") - return self.model_parser.parse_models(cached_data) + # a source is refetched over a mismatch at most once per process: if the fresh + # manifest still mismatches, the URL is authoritative and the cache is trusted + if self._cache_matches_source(source, cached_data) or source in self._refetched: + try: + parsed = self.model_parser.parse_models(cached_data) + except Exception: + cloudlog.warning(f"Failed to parse cached models for {source}; refetching", exc_info=True) + else: + if parsed: + cloudlog.debug(f"Using valid cached models data for source {source}") + return parsed + # a source-matching cache that yields no valid bundles is stale (e.g. an old + # manifest version) - do not trust it, refetch so the source is repopulated + cloudlog.warning(f"Cached models for {source} have no valid bundles; refetching") + else: + self._refetched.add(source) + cloudlog.warning(f"Cached models for {source} not valid; refetching once") - fetched_bundles = self._fetch_and_cache_models() + fetched_bundles = self._fetch_and_cache_models(source) if fetched_bundles is not None: return fetched_bundles @@ -205,14 +238,33 @@ class ModelFetcher: cloudlog.warning("Failed to fetch fresh data and no cache available") cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback") - return self.model_parser.parse_models(cached_data) + try: + return self.model_parser.parse_models(cached_data) + except Exception: + return [] + + +def get_cached_bundles(params: Params, source: str) -> list[custom.ModelManagerSP.ModelBundle]: + + if source not in ModelFetcher.MODEL_SOURCES: + cloudlog.warning(f"Unknown model source: {source}") + return [] + _, suffix = ModelFetcher.MODEL_SOURCES[source] + cached_data = params.get(f"ModelManager_ModelsCache{suffix}") + if not cached_data: + return [] + try: + return ModelParser.parse_models(cached_data) + except Exception as e: + cloudlog.warning(f"Failed to parse cached models for source {source}: {e}") + return [] if __name__ == "__main__": from openpilot.selfdrive.modeld.helpers import usbgpu_present params = Params() model_fetcher = ModelFetcher(params) - bundles = model_fetcher.get_available_bundles(chestnut_present=usbgpu_present()) + bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present())) for bundle in bundles: for model in bundle.models: model_overrides = {override.key: override.value for override in bundle.overrides} diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index e33cc445d1..707b86f722 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -16,6 +16,7 @@ from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider from openpilot.common.hardware.hw import Paths +from openpilot.selfdrive.modeld.helpers import usbgpu_present # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO REQUIRED_JSON_VERSION = 18 @@ -24,6 +25,12 @@ CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' ModelManager = custom.ModelManagerSP +ACTIVE_BUNDLE_KEYS = { + "qcom": "ModelManager_ActiveBundle", + "usbgpu": "ModelManager_ActiveBundleUSBGPU", +} +_LAST_VALIDATED_RAW: dict[str, dict | None] = {} + def _compute_hash(file_path: str) -> str | None: from openpilot.common.file_chunker import open_file_chunked @@ -97,55 +104,81 @@ def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, availa return True if active_bundle.minimumSelectorVersion != matching_bundle.minimumSelectorVersion: return True - if active_bundle.runner.raw != matching_bundle.runner.raw: + if active_bundle.runner != matching_bundle.runner: return True if set(_bundle_artifacts(active_bundle)) != set(_bundle_artifacts(matching_bundle)): return True - # missing files trigger re-download, not selection reset - return False + return not _bundle_is_valid_locally(active_bundle) -def _prev_bundle_key(is_usbgpu: bool) -> str: - return "ModelManager_PrevBundle_USBGPU" if is_usbgpu else "ModelManager_PrevBundle" - - -def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None, - is_usbgpu: bool = False) -> None: - raw_bundle = params.get("ModelManager_ActiveBundle") - if not raw_bundle: - prev = params.get(_prev_bundle_key(is_usbgpu)) - if prev and (prev_bundle := get_active_bundle(params, raw_bundle_dict=prev)) is not None: - if not _bundle_needs_reset(prev_bundle, available_bundles): - params.put("ModelManager_ActiveBundle", prev, block=True) - return - - active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle) - if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles): - cloudlog.warning("Active model bundle invalid; resetting to default") - params.put(_prev_bundle_key(not is_usbgpu), raw_bundle, block=True) - - prev = params.get(_prev_bundle_key(is_usbgpu)) - if prev and (prev_bundle := get_active_bundle(params, raw_bundle_dict=prev)) is not None: - if not _bundle_needs_reset(prev_bundle, available_bundles): - params.put("ModelManager_ActiveBundle", prev, block=True) - return - - params.remove("ModelManager_ActiveBundle") - params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True) - - -def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | bytes | None = None) -> "custom.ModelManagerSP.ModelBundle | None": - params = params or Params() +def _parse_active_bundle(raw_bundle) -> "custom.ModelManagerSP.ModelBundle | None": try: - active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {}) - if isinstance(active_bundle_dict, dict) and active_bundle_dict and is_bundle_version_compatible(active_bundle_dict): - return custom.ModelManagerSP.ModelBundle(**active_bundle_dict) + if isinstance(raw_bundle, dict) and raw_bundle and is_bundle_version_compatible(raw_bundle): + return custom.ModelManagerSP.ModelBundle(**raw_bundle) except Exception: pass return None +def get_selected_bundle(params: Params | None = None, source: str = "qcom") -> "custom.ModelManagerSP.ModelBundle | None": + params = params or Params() + return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source])) + + +def get_active_source(usbgpu: bool | None = None, usbgpu_active: bool | None = None, + usbgpu_loading: bool | None = None, offroad: bool | None = None) -> str: + if usbgpu is None: + usbgpu = usbgpu_present() + state_valid = usbgpu_active is not None or usbgpu_loading is not None or offroad is not None + big_active = usbgpu and (not state_valid or usbgpu_active or usbgpu_loading or offroad) + return "usbgpu" if big_active else "qcom" + + +def get_active_bundle(params: Params | None = None, *, usbgpu: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None": + # no cross-slot fallback: an empty active slot means the hardware default, which + # only stock modeld can run - modeld_v2 requires a real bundle + params = params or Params() + return get_selected_bundle(params, get_active_source(usbgpu=usbgpu)) + + +def resolve_bundle_by_ref( + ref: str, source_bundles: dict[str, list[custom.ModelManagerSP.ModelBundle]], +) -> "tuple[custom.ModelManagerSP.ModelBundle, str] | None": + for source, bundles in source_bundles.items(): + for bundle in bundles: + if bundle.ref == ref: + return bundle, source + return None + + +def _validate_active_bundle(params: Params, source: str, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None: + global _LAST_VALIDATED_RAW + + key = ACTIVE_BUNDLE_KEYS[source] + raw_bundle = params.get(key) + if not raw_bundle: + return + + if _LAST_VALIDATED_RAW.get(key) == raw_bundle: + return + + active_bundle = _parse_active_bundle(raw_bundle) + if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles): + cloudlog.warning(f"Active model bundle invalid for {source}; resetting to default") + params.remove(key) + _LAST_VALIDATED_RAW[key] = None + else: + _LAST_VALIDATED_RAW[key] = raw_bundle + + +def validate_active_bundles(params: Params, source_bundles: dict[str, list[custom.ModelManagerSP.ModelBundle]]) -> None: + # an empty list means the fetch failed, not that the catalog dropped the bundle + for source, bundles in source_bundles.items(): + _validate_active_bundle(params, source, bundles or None) + get_active_model_runner(params, force_check=True) + + def get_active_model_runner(params: Params | None = None, force_check: bool = False) -> int: params = params or Params() cached_runner_type = params.get("ModelRunnerTypeCache") diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index e47cf7536c..2405566d55 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -17,7 +17,8 @@ from openpilot.common.hardware.hw import Paths from openpilot.cereal import messaging, custom from openpilot.sunnypilot.models.fetcher import ModelFetcher -from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file +from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_bundle, get_selected_bundle, + resolve_bundle_by_ref, validate_active_bundles, verify_file) # (connect, read) seconds. read is per-request inactivity, not a total cap DOWNLOAD_TIMEOUT = (30, 30) @@ -31,9 +32,11 @@ class ModelManagerSP: self.model_fetcher = ModelFetcher(self.params) self.pm = messaging.PubMaster(["modelManagerSP"]) self.sm = messaging.SubMaster(["deviceState"]) + self.chestnut_present = False self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] + self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {} self.selected_bundle: custom.ModelManagerSP.ModelBundle = None - self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params) + self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) self._chunk_size = 128 * 1000 # 128 KB chunks self._download_start_times: dict[str, float] = {} # Track start time per model @@ -77,7 +80,7 @@ class ModelManagerSP: f.write(chunk) bytes_downloaded += len(chunk) - if self.params.get("ModelManager_DownloadIndex") is None: + if self.params.get("ModelManager_DownloadRef") is None: raise Exception("Download cancelled") if total_size > 0: @@ -115,7 +118,7 @@ class ModelManagerSP: for data in response.iter_content(chunk_size=self._chunk_size): f.write(data) chunk_downloaded += len(data) - if self.params.get("ModelManager_DownloadIndex") is None: + if self.params.get("ModelManager_DownloadRef") is None: raise Exception("Download cancelled") intra = chunk_downloaded / max(chunk_size, 1) progress = min(99.0, ((i + intra) / num_chunks) * 100) @@ -217,8 +220,7 @@ class ModelManagerSP: model_manager_state.availableBundles = self.available_models self.pm.send('modelManagerSP', msg) - async def _download_bundle(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str) -> None: - """Downloads all models in a bundle""" + async def _download_bundle(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str, source: str) -> None: self.selected_bundle = model_bundle self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloading for model in self.selected_bundle.models: @@ -240,10 +242,9 @@ class ModelManagerSP: seen_artifacts.add(artifact.fileName) await self._process_artifact(artifact, destination_path) - self.active_bundle = self.selected_bundle - self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded - self.params.put("ModelManager_ActiveBundle", self.active_bundle.to_dict(), block=True) - self.selected_bundle = None + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded + self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True) + self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) except Exception: if self.selected_bundle is not None: @@ -253,37 +254,32 @@ class ModelManagerSP: finally: self._report_status() - def download(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str) -> None: + def download(self, model_bundle: custom.ModelManagerSP.ModelBundle, destination_path: str, source: str) -> None: """Main entry point for downloading a model bundle""" - asyncio.run(self._download_bundle(model_bundle, destination_path)) - - BOOT_SETTLE_TICKS = 10 # seconds at 1 Hz before validating active bundle + asyncio.run(self._download_bundle(model_bundle, destination_path, source)) def main_thread(self) -> None: """Main thread for model management""" rk = Ratekeeper(1, print_delay_threshold=None) - boot_ticks = 0 while True: try: self.sm.update(0) - chestnut_present = self.sm['deviceState'].chestnutPresent - self.available_models = self.model_fetcher.get_available_bundles(chestnut_present) - if boot_ticks >= self.BOOT_SETTLE_TICKS: - validate_active_bundle(self.params, self.available_models, is_usbgpu=chestnut_present) - boot_ticks = min(boot_ticks + 1, self.BOOT_SETTLE_TICKS) - self.active_bundle = get_active_bundle(self.params) + self.chestnut_present = self.sm['deviceState'].chestnutPresent + self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES} + self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)] + validate_active_bundles(self.params, self.source_models) + self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) - if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None: - if self.active_bundle and self.active_bundle.index == index_to_download: - self.params.remove("ModelManager_DownloadIndex") - elif model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): + if (ref_to_download := self.params.get("ModelManager_DownloadRef")) is not None: + if resolved := resolve_bundle_by_ref(ref_to_download, self.source_models): + model_to_download, source = resolved try: - self.download(model_to_download, Paths.model_root()) + self.download(model_to_download, Paths.model_root(), source) except Exception as e: cloudlog.exception(e) finally: - self.params.remove("ModelManager_DownloadIndex") + self.params.remove("ModelManager_DownloadRef") self.selected_bundle = None if self.params.get("ModelManager_ClearCache"): @@ -302,12 +298,14 @@ class ModelManagerSP: Clears the model cache directory of all files except those in the active model bundle. """ - # Get list of files used by active model bundle + # Get list of files used by both slots' selected bundles (either may become + # the truly active bundle depending on hardware availability) active_files = [] - if self.active_bundle is not None: # When the default model is active - for model in self.active_bundle.models: - if hasattr(model, 'artifact') and model.artifact.fileName: - active_files.append(model.artifact.fileName) + for source in ACTIVE_BUNDLE_KEYS: + if selected_bundle := get_selected_bundle(self.params, source): + for model in selected_bundle.models: + if model.artifact.fileName: + active_files.append(model.artifact.fileName) # Remove all files except active ones (including their chunk files) model_dir = Paths.model_root() diff --git a/openpilot/sunnypilot/models/tests/test_manager_download.py b/openpilot/sunnypilot/models/tests/test_manager_download.py index 67fb9023af..d74deb03e6 100644 --- a/openpilot/sunnypilot/models/tests/test_manager_download.py +++ b/openpilot/sunnypilot/models/tests/test_manager_download.py @@ -11,6 +11,7 @@ import http.server import os import tempfile import threading +import time import unittest from typing import Any from unittest import mock @@ -23,6 +24,10 @@ from openpilot.common.test import OpenpilotTestCase from openpilot.common.file_chunker import get_chunk_name, get_manifest_path from openpilot.selfdrive.test.helpers import http_server_context from openpilot.sunnypilot.models import manager as manager_module +from openpilot.sunnypilot.models.fetcher import ModelFetcher, get_cached_bundles +from openpilot.sunnypilot.models import helpers +from openpilot.sunnypilot.models.helpers import (get_active_bundle, get_active_source, get_selected_bundle, + resolve_bundle_by_ref, validate_active_bundles) from openpilot.sunnypilot.models.manager import ModelManagerSP CHUNK_BODIES = [b'A' * 5000, b'B' * 5000, b'C' * 3000] @@ -103,6 +108,7 @@ class ManagerDownloadTestBase(OpenpilotTestCase): self.manager.selected_bundle = None self.manager.active_bundle = None self.manager.available_models = [] + self.manager.chestnut_present = False self.manager._chunk_size = 1024 self.manager._download_start_times = {} @@ -249,6 +255,85 @@ class TestManagerDownload(ManagerDownloadTestBase): assert self.manager._download_start_times == {} self.run_with_server(body) + def test_download_ref_present_keeps_download_alive(self): + """A pending download request (DownloadRef set) must not be cancelled mid-transfer.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + self.manager.params.get.side_effect = lambda key: b"ref" if key == "ModelManager_DownloadRef" else None + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + assert os.path.isfile(get_manifest_path(base_path)) + self.run_with_server(body) + + def test_cancellation_via_download_ref(self): + """Removing DownloadRef mid-transfer cancels the download.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + checks = {"n": 0} + + def get(key): + if key == "ModelManager_DownloadRef": + checks["n"] += 1 + return b"ref" if checks["n"] <= 2 else None + return b"0" + + self.manager.params.get.side_effect = get + with self.assertRaises(Exception) as ctx: + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + assert 'cancelled' in str(ctx.exception).lower() + assert not os.path.isfile(get_manifest_path(base_path)) + self.run_with_server(body) + + def _make_params_with_store(self): + params = mock.MagicMock() + store = {} + + def get(key, *args, **kwargs): + return store.get(key, b"0") # b"0" -> download not cancelled + + def put(key, value, *args, **kwargs): + store[key] = value + + params.get.side_effect = get + params.put.side_effect = put + return params, store + + def test_download_writes_qcom_slot(self): + """A download resolved to the qcom source writes the qcom active bundle slot only.""" + def body(): + artifact = self.make_artifact(chunked=True) + self._bundle.ref = "test-ref" + self._bundle.minimumSelectorVersion = 18 + params, store = self._make_params_with_store() + self.manager.params = params + asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) + + assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot" + assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu slot" + assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded + assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref" + assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded + chunk_names = [get_chunk_name(artifact.fileName, i, len(artifact.chunks)) for i in range(len(artifact.chunks))] + missing = [c for c in chunk_names if not os.path.isfile(os.path.join(self.dest, c))] + assert missing == [], f"chunks missing from the cache: {missing}" + self.run_with_server(body) + + def test_download_writes_usbgpu_slot(self): + """A download resolved to the usbgpu source writes the usbgpu active bundle slot only.""" + def body(): + self.make_artifact(chunked=True) + self._bundle.ref = "big-ref" + self._bundle.minimumSelectorVersion = 18 + params, store = self._make_params_with_store() + self.manager.params = params + asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu")) + + assert "ModelManager_ActiveBundleUSBGPU" in store, "usbgpu download must write the usbgpu slot" + assert "ModelManager_ActiveBundle" not in store, "usbgpu download must not touch the qcom slot" + assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded + self.run_with_server(body) + class TestManagerImports(OpenpilotTestCase): """Catches undeclared dependencies. aiohttp lived only in the AGNOS venv; 19.6 dropped @@ -267,6 +352,352 @@ class TestManagerImports(OpenpilotTestCase): assert connect > 0 and read > 0, "requests defaults to no timeout; downloads would hang forever" +class TestResolveBundleByRef(OpenpilotTestCase): + """A ref resolves to (bundle, source) across both hardware manifests. Refs are + unique per manifest and never overlap across sources, so a ref maps to exactly + one slot. Shared by the manager's download flow and the settings UI.""" + + @staticmethod + def _bundle(ref: str): + bundle = custom.ModelManagerSP.ModelBundle.new_message() + bundle.ref = ref + return bundle + + def test_qcom_ref_resolves_to_qcom_slot(self): + small = self._bundle("small") + assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom") + + def test_usbgpu_ref_resolves_to_usbgpu_slot(self): + big = self._bundle("big") + assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu") + + def test_unknown_ref_returns_none(self): + source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []} + assert resolve_bundle_by_ref("nope", source_bundles) is None + + +def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict: + """Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects). + Big (usbgpu) bundles carry `is_big: true` in the manifest JSON.""" + return { + "index": index, + "short_name": short_name, + "display_name": short_name.upper(), + "generation": 1, + "environment": "release", + "runner": "tinygrad", + "is_big": is_big, + "minimum_selector_version": "18", + "ref": ref, + "models": [{ + "type": "supercombo", + "artifact": { + "file_name": f"{short_name}.pkl", + "download_uri": {"url": f"https://example.com/{short_name}.pkl", "sha256": "s"}, + }, + }], + } + + +def fresh_sync_time() -> int: + return int(time.monotonic() * 1e9) + + +class TestModelFetcherSources(OpenpilotTestCase): + """Both manifests are always maintained: get_bundles_for_source exposes either + source by name, and active_source picks which one matches the attached hardware.""" + + def _make_params(self, qcom_manifest, usbgpu_manifest): + params = mock.MagicMock() + + def get(key): + if key == "ModelManager_ModelsCache": + return qcom_manifest + if key == "ModelManager_ModelsCache_USBGPU": + return usbgpu_manifest + if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): + return fresh_sync_time() + return None + + params.get.side_effect = get + return params + + def test_active_source_follows_chestnut_presence(self): + assert ModelFetcher.active_source(False) == "qcom" + assert ModelFetcher.active_source(True) == "usbgpu" + + def test_get_bundles_for_source_returns_each_source(self): + params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, + {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) + fetcher = ModelFetcher(params) + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] + + def test_get_bundles_for_source_unknown(self): + assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == [] + + def test_get_cached_bundles_parses_source(self): + params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, + {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) + qcom_bundles = get_cached_bundles(params, "qcom") + usbgpu_bundles = get_cached_bundles(params, "usbgpu") + assert [b.ref for b in qcom_bundles] == ["aaa"] + assert [b.ref for b in usbgpu_bundles] == ["bbb"] + assert qcom_bundles[0].displayName == "SMALL" + + def test_get_cached_bundles_empty_when_missing(self): + params = mock.MagicMock() + params.get.return_value = None + assert get_cached_bundles(params, "qcom") == [] + assert get_cached_bundles(params, "usbgpu") == [] + + def test_get_cached_bundles_unknown_source(self): + assert get_cached_bundles(mock.MagicMock(), "bogus") == [] + + def test_active_json_has_both_urls(self): + params = mock.MagicMock() + ModelFetcher(params) + active_json_calls = [call for call in params.put.call_args_list if call.args[0] == "ModelManager_ActiveJson"] + assert active_json_calls, "expected ModelManager_ActiveJson to be written" + assert active_json_calls[-1].args[1] == { + "qcom": ModelFetcher.MODEL_URL, + "usbgpu": ModelFetcher.MODEL_URL_USBGPU, + } + + + +class TestSourceCacheIntegrity(OpenpilotTestCase): + """Each source's cached manifest must contain only that source's models; the + `is_big` flag in the JSON marks the big (usbgpu) models. A mismatched cache is + legacy data from before the per-source split (the active manifest was cached + under the unsuffixed key regardless of hardware) and is refetched. This + replaces the old one-time bundle migration.""" + + def _make_params(self, qcom_manifest, usbgpu_manifest): + params = mock.MagicMock() + + def get(key): + if key == "ModelManager_ModelsCache": + return qcom_manifest + if key == "ModelManager_ModelsCache_USBGPU": + return usbgpu_manifest + if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): + return fresh_sync_time() + return None + + params.get.side_effect = get + return params + + def _fetched(self, *bundles): + return ModelFetcher(mock.MagicMock()).model_parser.parse_models({"bundles": list(bundles)}) + + def test_qcom_cache_with_big_models_is_refetched(self): + """Legacy: the unsuffixed cache holds the big manifest. is_big confirms it is + the wrong set for qcom, so a fresh fetch replaces it.""" + params = self._make_params({"bundles": [manifest_bundle("big", "bbb", is_big=True)]}, + {"bundles": [manifest_bundle("big2", "ccc", is_big=True)]}) + fetcher = ModelFetcher(params) + fetched = self._fetched(manifest_bundle("small", "aaa")) + with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched): + bundles = fetcher.get_bundles_for_source("qcom") + assert [bundle.ref for bundle in bundles] == ["aaa"] + + def test_usbgpu_cache_without_big_models_is_refetched(self): + params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, + {"bundles": [manifest_bundle("big2", "ccc")]}) + fetcher = ModelFetcher(params) + fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True)) + with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched): + bundles = fetcher.get_bundles_for_source("usbgpu") + assert [bundle.ref for bundle in bundles] == ["bbb"] + + def test_matching_caches_are_used_without_fetch(self): + params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, + {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) + fetcher = ModelFetcher(params) + with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")): + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] + assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] + + def test_stale_version_cache_is_refetched(self): + """A source-matching cache whose bundles are all filtered by the selector version + check parses to zero valid bundles; it is stale (e.g. an old manifest) and must be + refetched instead of silently returning an empty list forever.""" + stale = manifest_bundle("small", "aaa") + stale["minimum_selector_version"] = "16" + params = self._make_params({"bundles": [stale]}, + {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) + fetcher = ModelFetcher(params) + fetched = self._fetched(manifest_bundle("small2", "ddd")) + with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched) as fetch: + bundles = fetcher.get_bundles_for_source("qcom") + fetch.assert_called_once_with("qcom") + assert [bundle.ref for bundle in bundles] == ["ddd"] + + def test_mismatched_refetch_happens_once(self): + """If the fresh manifest still fails the source check, the URL is authoritative: + trust it instead of refetching at 1 Hz forever.""" + params = self._make_params({"bundles": [manifest_bundle("big", "bbb", is_big=True)]}, + {"bundles": [manifest_bundle("big2", "ccc", is_big=True)]}) + fetcher = ModelFetcher(params) + fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True)) + with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched) as fetch: + first = fetcher.get_bundles_for_source("qcom") + second = fetcher.get_bundles_for_source("qcom") + fetch.assert_called_once_with("qcom") + assert [bundle.ref for bundle in first] == ["bbb"] + assert [bundle.ref for bundle in second] == ["bbb"] + + def test_corrupt_cache_is_refetched(self): + """A cache that fails to parse (e.g. truncated/foreign JSON) must trigger a + refetch instead of raising every loop and never recovering.""" + corrupt = {"bundles": [{"short_name": "broken"}]} # missing required fields + params = self._make_params(corrupt, {"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) + fetcher = ModelFetcher(params) + fetched = self._fetched(manifest_bundle("small", "aaa")) + with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched) as fetch: + bundles = fetcher.get_bundles_for_source("qcom") + fetch.assert_called_once_with("qcom") + assert [bundle.ref for bundle in bundles] == ["aaa"] + + +class TestActiveBundleValidation(OpenpilotTestCase): + """Validation is per-slot: a failed fetch (empty bundle list) must not reset a slot, + and resetting one slot must not stomp the runner cache derived from the other.""" + + def setUp(self): + super().setUp() + helpers._LAST_VALIDATED_RAW.clear() + + @staticmethod + def _raw_bundle(ref: str, runner: int | None = None) -> dict: + bundle = custom.ModelManagerSP.ModelBundle.new_message() + bundle.ref = ref + bundle.minimumSelectorVersion = 18 + if runner is not None: + bundle.runner = runner + return bundle.to_dict() + + def _params(self, qcom=None, usbgpu=None): + params = mock.MagicMock() + + def get(key, *args, **kwargs): + return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.get(key) + + params.get.side_effect = get + return params + + def test_empty_catalog_does_not_reset_slot(self): + params = self._params(qcom=self._raw_bundle("small")) + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + validate_active_bundles(params, {"qcom": [], "usbgpu": []}) + params.remove.assert_not_called() + + def test_reset_recomputes_runner_from_surviving_slot(self): + tinygrad = int(custom.ModelManagerSP.Runner.tinygrad) + big_raw = self._raw_bundle("big", runner=tinygrad) + params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw) + catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))], + "usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]} + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + validate_active_bundles(params, catalog) + params.remove.assert_called_once_with("ModelManager_ActiveBundle") + runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"] + assert [call.args[1] for call in runner_puts] == [tinygrad] + + +class TestActiveBundleSelection(OpenpilotTestCase): + """The effective active bundle is the active source's slot: usbgpu when a GPU is + present, qcom otherwise. An empty active slot means the hardware default (stock + runner), never the other slot's pick - modeld_v2 requires a real bundle.""" + + @staticmethod + def _raw_bundle(ref: str) -> dict: + bundle = custom.ModelManagerSP.ModelBundle.new_message() + bundle.ref = ref + bundle.minimumSelectorVersion = 18 + return bundle.to_dict() + + def _params(self, qcom=None, usbgpu=None): + params = mock.MagicMock() + + def get(key, *args, **kwargs): + if key == "ModelManager_ActiveBundle": + return qcom + if key == "ModelManager_ActiveBundleUSBGPU": + return usbgpu + return None + + params.get.side_effect = get + return params + + def test_selected_bundle_is_per_slot(self): + params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) + assert get_selected_bundle(params, "qcom").ref == "small" + assert get_selected_bundle(params, "usbgpu").ref == "big" + + def test_no_gpu_uses_qcom_slot(self): + params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + assert get_active_bundle(params).ref == "small" + + def test_gpu_uses_usbgpu_slot(self): + params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + assert get_active_bundle(params).ref == "big" + + def test_gpu_without_big_selection_is_hardware_default(self): + params = self._params(qcom=self._raw_bundle("small"), usbgpu=None) + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + assert get_active_bundle(params) is None + + +class TestEffectiveSource(OpenpilotTestCase): + """One gate decides the active source. With no flags it is runtime truth (GPU + attached); display callers (mici) pass the ui_state flags, which additionally + require the big model to be loading, active, or the device offroad. The active + bundle is simply the selected bundle of that source.""" + + @staticmethod + def _raw_bundle(ref: str) -> dict: + bundle = custom.ModelManagerSP.ModelBundle.new_message() + bundle.ref = ref + bundle.minimumSelectorVersion = 18 + return bundle.to_dict() + + def test_runtime_no_gpu(self): + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + assert get_active_source() == "qcom" + + def test_runtime_gpu_present(self): + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): + assert get_active_source() == "usbgpu" + + def test_display_offroad_gpu_present_shows_big(self): + assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu" + + def test_display_onroad_gpu_loading_shows_big(self): + assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu" + + def test_display_onroad_gpu_active_shows_big(self): + assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu" + + def test_display_onroad_gpu_idle_shows_small(self): + assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom" + + def test_display_active_none_is_idle(self): + assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_loading=False, offroad=False) == "qcom" + + def test_active_bundle_follows_source(self): + params = mock.MagicMock() + params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"), + "ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key) + with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): + assert get_active_bundle(params).ref == "small" + assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False, + usbgpu_loading=False, offroad=True)).ref == "big" + + @unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network') class TestLiveModelManifest(OpenpilotTestCase): """Every artifact and chunk URL in the published manifest must resolve.""" diff --git a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py index fd389f93c0..d6d82dfb32 100644 --- a/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py +++ b/openpilot/sunnypilot/models/tests/test_tinygrad_ref.py @@ -1,13 +1,11 @@ import requests -from openpilot.common.params import Params from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref from openpilot.sunnypilot.models.fetcher import ModelFetcher from openpilot.common.test import OpenpilotTestCase def fetch_tinygrad_ref(): - fetcher = ModelFetcher(Params()) - response = requests.get(fetcher.model_url, timeout=10) + response = requests.get(ModelFetcher.MODEL_URL, timeout=10) response.raise_for_status() json_data = response.json() return json_data.get("tinygrad_ref") diff --git a/openpilot/sunnypilot/sunnylink/statsd.py b/openpilot/sunnypilot/sunnylink/statsd.py index 7e8faf6327..a221fc084f 100755 --- a/openpilot/sunnypilot/sunnylink/statsd.py +++ b/openpilot/sunnypilot/sunnylink/statsd.py @@ -65,6 +65,7 @@ def sp_stats(end_event): 'MadsSteeringMode', 'MadsUnifiedEngagementMode', 'ModelManager_ActiveBundle', + 'ModelManager_ActiveBundleUSBGPU', 'ModelManager_Favs', 'EnableSunnylinkUploader', 'SunnylinkEnabled', diff --git a/openpilot/sunnypilot/system/params_migration.py b/openpilot/sunnypilot/system/params_migration.py index 130fd64310..f0f0d7248a 100644 --- a/openpilot/sunnypilot/system/params_migration.py +++ b/openpilot/sunnypilot/system/params_migration.py @@ -84,6 +84,21 @@ def _migrate_tesla_mads_screen_button(_params): cloudlog.exception(f"Error migrating TeslaMadsScreenButton: {e}") +def _migrate_model_bundle_slots(_params): + # Pre-split, a chestnut user's big-model selection lived in the single + # ActiveBundle. Seed both slots; validation drops whichever does not match + # its own manifest. + try: + if _params.get("ModelManager_ActiveBundleUSBGPU") is not None: + return + if (bundle := _params.get("ModelManager_ActiveBundle")) is None: + return + _params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True) + cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle") + except Exception as e: + cloudlog.exception(f"Error migrating model bundle slots: {e}") + + def run_migration(_params): # migrate OnroadScreenOffBrightness if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION: @@ -120,3 +135,6 @@ def run_migration(_params): # seed TeslaMadsScreenButton for existing Tesla installs _migrate_tesla_mads_screen_button(_params) + + # seed the usbgpu model slot from the pre-split single slot + _migrate_model_bundle_slots(_params) diff --git a/openpilot/sunnypilot/system/tests/__init__.py b/openpilot/sunnypilot/system/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpilot/sunnypilot/system/tests/test_params_migration.py b/openpilot/sunnypilot/system/tests/test_params_migration.py new file mode 100644 index 0000000000..328a7a65af --- /dev/null +++ b/openpilot/sunnypilot/system/tests/test_params_migration.py @@ -0,0 +1,36 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +from openpilot.common.params import Params +from openpilot.common.test import OpenpilotTestCase +from openpilot.sunnypilot.system.params_migration import _migrate_model_bundle_slots + + +class TestModelBundleSlotMigration(OpenpilotTestCase): + """Pre-split, a chestnut user's big-model selection lived in the single ActiveBundle. + The migration seeds both slots; per-source validation later drops whichever does not + match its own manifest.""" + + def test_seeds_usbgpu_slot_from_active_bundle(self): + params = Params() + bundle = {"ref": "big", "minimumSelectorVersion": 18} + params.put("ModelManager_ActiveBundle", bundle, block=True) + _migrate_model_bundle_slots(params) + assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle + assert params.get("ModelManager_ActiveBundle") == bundle + + def test_noop_when_usbgpu_slot_already_set(self): + params = Params() + params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True) + params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True) + _migrate_model_bundle_slots(params) + assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"} + + def test_noop_when_no_selection(self): + params = Params() + _migrate_model_bundle_slots(params) + assert params.get("ModelManager_ActiveBundleUSBGPU") is None From d40df6f829c18bf17ae2e4a825220778e2780e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 26 Aug 2026 12:06:53 -0700 Subject: [PATCH 317/325] modeld: fall back on invalid big model outputs (#38700) --- openpilot/selfdrive/modeld/modeld.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 7f049912dd..783231b7b8 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -163,7 +163,7 @@ class ModelState: return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: + inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -189,9 +189,7 @@ class ModelState: ) model_output = outs.numpy()[0] if self.usbgpu and not np.all(np.isfinite(model_output)): - # TODO remove with prev_feat - cloudlog.error("model output not finite, dropping frame") - return None + raise RuntimeError("model output not finite") outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] From 980fb79c1af4980a45c291107fd7f71f558fd556 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 12:21:14 -0700 Subject: [PATCH 318/325] update orange GPU icon (#38701) mici: update failed eGPU icon --- openpilot/selfdrive/assets/icons_mici/egpu_orange.png | 4 ++-- openpilot/selfdrive/ui/mici/onroad/hud_renderer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png b/openpilot/selfdrive/assets/icons_mici/egpu_orange.png index c2fc06e34e..d3982abf87 100644 --- a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png +++ b/openpilot/selfdrive/assets/icons_mici/egpu_orange.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1 -size 10876 +oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820 +size 13079 diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index 0d1532057e..ad8f67809c 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -126,7 +126,7 @@ class HudRenderer(Widget): self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) - self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44) + self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 75, 44) self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) self._egpu_icon: rl.Texture | None = None From 63548ce10d10d5725cbcce559d23b2e47d82a281 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 15:20:04 -0700 Subject: [PATCH 319/325] bump tinygrad (#38702) --- tinygrad_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinygrad_repo b/tinygrad_repo index 138fb4a783..c015351ac5 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 138fb4a783d82f4e877ad2fe3692aaf8d1de2e46 +Subproject commit c015351ac5c00c10c58dbdaf530ef5b2883ab948 From 5cfdb2f4dac82ba89ebf310db13ff08d49cf4916 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 15:42:59 -0700 Subject: [PATCH 320/325] rename usbgpu to chestnut (#38703) chestnut: rename eGPU interfaces --- openpilot/common/params_keys.h | 4 +- .../icons_mici/{egpu.png => chestnut.png} | 0 ...{egpu_crossed.png => chestnut_crossed.png} | 0 .../{egpu_gray.png => chestnut_gray.png} | 0 .../{egpu_green.png => chestnut_green.png} | 0 .../{egpu_orange.png => chestnut_orange.png} | 0 openpilot/selfdrive/modeld/SConscript | 26 +++++------ .../selfdrive/modeld/dmonitoringmodeld.py | 2 +- openpilot/selfdrive/modeld/helpers.py | 14 +++--- openpilot/selfdrive/modeld/modeld.py | 40 ++++++++--------- openpilot/selfdrive/selfdrived/selfdrived.py | 8 ++-- openpilot/selfdrive/ui/mici/layouts/home.py | 12 ++--- .../selfdrive/ui/mici/onroad/hud_renderer.py | 44 +++++++++---------- openpilot/selfdrive/ui/ui_state.py | 22 +++++----- openpilot/system/hardware/hardwared.py | 4 +- 15 files changed, 88 insertions(+), 88 deletions(-) rename openpilot/selfdrive/assets/icons_mici/{egpu.png => chestnut.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_crossed.png => chestnut_crossed.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_gray.png => chestnut_gray.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_green.png => chestnut_green.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_orange.png => chestnut_orange.png} (100%) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 2a49690b7e..21b7463d92 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -127,7 +127,7 @@ inline static std::unordered_map keys = { {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, - {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, - {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/assets/icons_mici/egpu.png b/openpilot/selfdrive/assets/icons_mici/chestnut.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu.png rename to openpilot/selfdrive/assets/icons_mici/chestnut.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_crossed.png b/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_crossed.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_gray.png b/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_gray.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_gray.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_green.png b/openpilot/selfdrive/assets/icons_mici/chestnut_green.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_green.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_green.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png b/openpilot/selfdrive/assets/icons_mici/chestnut_orange.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_orange.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_orange.png diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 30a31aae27..f046be9915 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path +from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path CAMERA_CONFIGS = [ @@ -36,18 +36,18 @@ else: tg_devices = { # which device to put jit inputs to at runtime 'openpilot.selfdrive.modeld.modeld': { 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, - 'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} + 'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} }, 'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'default': {'DEV': tg_backend} }, } -USBGPU = usbgpu_present() -if USBGPU: - usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' +CHESTNUT = chestnut_present() +if CHESTNUT: + chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it - usbgpu_lock = File("models/.usb_gpu.lock").abspath + chestnut_lock = File("models/.chestnut.lock").abspath def write_tg_devices(target, source, env): with open(str(target[0]), "w") as f: @@ -73,10 +73,10 @@ compile_modeld_script = [ model_w, model_h = MEDMODEL_INPUT_SIZE frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ -for usbgpu in [False, True] if USBGPU else [False]: - target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath - # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU - file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) +for chestnut in [False, True] if CHESTNUT else [False]: + target_pkl_path = File(modeld_pkl_path(chestnut)).abspath + # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut + file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. @@ -103,14 +103,14 @@ for usbgpu in [False, True] if USBGPU else [False]: chunk_file(pkl, chunks) def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): chunk_file(pkl, chunks) - actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] + actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] node = lenv.Command( chunk_targets, tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], actions, ) - if usbgpu: - lenv.SideEffect(usbgpu_lock, node) + if chestnut: + lenv.SideEffect(chestnut_lock, node) # get model metadata fn = File(f"models/dmonitoring_model").abspath diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 67161c35bd..4010725b89 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -29,7 +29,7 @@ class ModelState: output: np.ndarray def __init__(self, cam_w: int, cam_h: int): - self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV'] + self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV'] with open(METADATA_PATH, 'rb') as f: model_metadata = pickle.load(f) self.input_shapes = model_metadata['input_shapes'] diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 37ab0b26d7..84236f3fd0 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -13,12 +13,12 @@ MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' -def get_tg_input_devices(process_name: str, usbgpu: bool): +def get_tg_input_devices(process_name: str, chestnut: bool): with open(TG_INPUT_DEVICES_PATH) as f: - return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu'] + return json.load(f)[process_name]['default' if not chestnut else 'chestnut'] -def modeld_pkl_path(usbgpu: bool): - prefix = 'big_' if usbgpu else '' +def modeld_pkl_path(chestnut: bool): + prefix = 'big_' if chestnut else '' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' def dump_oob(obj, f): @@ -45,7 +45,7 @@ def load_oob(f): yield pb return pickle.load(io.BytesIO(opcodes), buffers=buffers()) -def usbgpu_present() -> bool: +def chestnut_present() -> bool: for d in USB_DEVICES_PATH.glob("*"): try: usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) @@ -56,5 +56,5 @@ def usbgpu_present() -> bool: pass return False -def usbgpu_compiled() -> bool: - return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file() +def chestnut_compiled() -> bool: + return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 783231b7b8..7a76841516 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from functools import cached_property import os -os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom +os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device import struct @@ -30,7 +30,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -137,17 +137,17 @@ class FrameMeta: class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse - def __init__(self, cam_w: int, cam_h: int, usbgpu: bool): - input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) + def __init__(self, cam_w: int, cam_h: int, chestnut: bool): + input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] - jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu))) + jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut))) metadata = jits['metadata'] self.input_shapes = metadata['input_shapes'] self.vision_input_names = [k for k in self.input_shapes if 'img' in k] self.output_slices = metadata['output_slices'] self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.usbgpu = usbgpu + self.chestnut = chestnut self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) @@ -188,7 +188,7 @@ class ModelState: **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) model_output = outs.numpy()[0] - if self.usbgpu and not np.all(np.isfinite(model_output)): + if self.chestnut and not np.all(np.isfinite(model_output)): raise RuntimeError("model output not finite") outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] @@ -211,12 +211,12 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - USBGPU = usbgpu_present() and usbgpu_compiled() - if USBGPU: + CHESTNUT = chestnut_present() and chestnut_compiled() + if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() - params.put_bool("UsbGpuLoading", USBGPU) - params.remove("UsbGpuActive") + params.put_bool("ChestnutLoading", CHESTNUT) + params.remove("ChestnutActive") config_realtime_process(7, 54) @@ -246,7 +246,7 @@ def main(demo=False): st = time.monotonic() cloudlog.warning("loading model") model = None - if USBGPU: + if CHESTNUT: big_model = None def load_big(): nonlocal big_model @@ -260,22 +260,22 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model - params.put_bool("UsbGpuActive", model is not None) + params.put_bool("ChestnutActive", model is not None) - small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None + small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: model = small_model - params.put_bool("UsbGpuLoading", False) + params.put_bool("ChestnutLoading", False) cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if USBGPU else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -385,11 +385,11 @@ def main(demo=False): try: model_output = model.run(bufs, transforms, inputs) except Exception: - if not params.get_bool("UsbGpuActive"): + if not params.get_bool("ChestnutActive"): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") - params.put_bool("UsbGpuActive", False) + params.put_bool("ChestnutActive", False) model = small_model if chestnut_state is not None: chestnut_state.big = False @@ -408,7 +408,7 @@ def main(demo=False): fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) - modelv2_send.modelV2.big = model.usbgpu + modelv2_send.modelV2.big = model.chestnut desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 1f1f6f7349..638448d897 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -159,17 +159,17 @@ class SelfdriveD: self.events.add(EventName.joystickDebug) self.startup_event = None - loading = self.params.get_bool("UsbGpuLoading") + loading = self.params.get_bool("ChestnutLoading") if self.big_model_loading and not loading: self.big_model_ready_t = time.monotonic() self.big_model_loading = loading if self.big_model_loading: self.events.add(EventName.bigModelLoading) - big_active = self.params.get("UsbGpuActive") - usbgpu_present = self.sm['deviceState'].chestnutPresent + big_active = self.params.get("ChestnutActive") + chestnut_present = self.sm['deviceState'].chestnutPresent model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] - big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present) + big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present) if big_failed and not self.big_model_failed: self.events.add(EventName.bigModelFailed) self.big_model_failed = big_failed diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 8b21a1a98c..50dc95901f 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37)) - self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (50, 37)) + self._chestnut_icon_gray = IconWidget("icons_mici/chestnut_gray.png", (50, 37)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -150,8 +150,8 @@ class MiciHomeLayout(Widget): IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), self._experimental_icon, - self._egpu_icon, - self._egpu_icon_gray, + self._chestnut_icon, + self._chestnut_icon_gray, self._body_icon, self._mic_icon, ], spacing=18) @@ -248,8 +248,8 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) - self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) + self._chestnut_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.chestnut_compiled) + self._chestnut_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.chestnut_compiled) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index ad8f67809c..e844f5e9e7 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -108,7 +108,7 @@ class HudRenderer(Widget): self.v_ego_cluster_seen: bool = False self._engaged: bool = False self._small_model_engaged: bool = False - self._egpu_fade_time: float = 0 + self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True self._show_wheel_critical = False @@ -124,17 +124,17 @@ class HudRenderer(Widget): self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) - self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) - self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) - self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 75, 44) - self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) - self._egpu_icon: rl.Texture | None = None + self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) + self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) + self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) + self._txt_chestnut_crossed: rl.Texture = gui_app.texture('icons_mici/chestnut_crossed.png', 60, 52) + self._chestnut_icon: rl.Texture | None = None self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) def set_wheel_critical_icon(self, critical: bool): """Set the wheel icon to critical or normal state.""" @@ -165,11 +165,11 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and + if (engaged and not self._engaged and not ui_state.chestnut_loading and ui_state.chestnut_active is not True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): self._small_model_engaged = True if engaged != self._engaged: - self._egpu_fade_time = rl.get_time() if engaged else 0 + self._chestnut_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() self._engaged = engaged @@ -191,7 +191,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.usbgpu and ui_state.usbgpu_compiled: + if ui_state.chestnut and ui_state.chestnut_compiled: self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +200,30 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and + big_failed = (ui_state.chestnut_active is False or not ui_state.sm['deviceState'].chestnutPresent or + (ui_state.chestnut_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and not ui_state.sm.alive['modelV2']) or - (ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) + (ui_state.chestnut_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) self._small_model_engaged &= big_failed - loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed) + loading = ui_state.chestnut_loading or (ui_state.chestnut_active is None and not big_failed) if loading: pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) - icon = self._txt_egpu + icon = self._txt_chestnut opacity = 0.35 + 0.65 * pulse elif self._small_model_engaged: - icon = self._txt_egpu_crossed + icon = self._txt_chestnut_crossed opacity = 0.65 elif big_failed: - icon = self._txt_egpu_orange + icon = self._txt_chestnut_orange opacity = 1.0 else: - icon = self._txt_egpu_green + icon = self._txt_chestnut_green opacity = 1.0 - if icon is not self._egpu_icon: - self._egpu_fade_time = rl.get_time() - self._egpu_icon = icon - alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE) + if icon is not self._chestnut_icon: + self._chestnut_fade_time = rl.get_time() + self._chestnut_icon = icon + alpha = self._chestnut_alpha_filter.update(loading or 0 < rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 42e8086351..63a71758fa 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC -from openpilot.selfdrive.modeld.helpers import usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import chestnut_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 PARAM_UPDATE_TIME = 1 / 5.0 @@ -77,10 +77,10 @@ class UIState: self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.usbgpu: bool = False - self.usbgpu_compiled: bool = usbgpu_compiled() - self.usbgpu_active: bool | None = self.params.get("UsbGpuActive") - self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") + self.chestnut: bool = False + self.chestnut_compiled: bool = chestnut_compiled() + self.chestnut_active: bool | None = None + self.chestnut_loading: bool = False self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -208,12 +208,12 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep usbgpu UI active until offroad transition when gpu disappears - self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started) - if not self.usbgpu_compiled: - self.usbgpu_compiled = usbgpu_compiled() - self.usbgpu_active = self.params.get("UsbGpuActive") - self.usbgpu_loading = self.params.get_bool("UsbGpuLoading") + # keep chestnut UI active until offroad transition when gpu disappears + self.chestnut = self.sm["deviceState"].chestnutPresent or (self.chestnut and self.started) + if not self.chestnut_compiled: + self.chestnut_compiled = chestnut_compiled() + self.chestnut_active = self.params.get("ChestnutActive") + self.chestnut_loading = self.params.get_bool("ChestnutLoading") class Device: diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 16eb18d153..64ca4415d1 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,7 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR @@ -238,7 +238,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled() + big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From fa75fdd852d29eedf454afdcfa76c34451ada8e9 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Wed, 26 Aug 2026 17:57:55 -0700 Subject: [PATCH 321/325] chestnut stats: overlap with gpu work (#38704) * chestnut stats: overlap with gpu work * ci --------- Co-authored-by: elkoled --- openpilot/selfdrive/modeld/modeld.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 7a76841516..0db66ed880 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from collections.abc import Callable +import ctypes from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom @@ -90,8 +92,10 @@ class ChestnutState: if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: smu = Device["AMD"].iface.dev_impl.smu + metrics_t = smu.smu_mod.SmuMetricsExternal_t smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) - metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics + metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:]) + metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], 'powerDrawW': metrics.AverageSocketPower, @@ -163,7 +167,7 @@ class ModelState: return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -187,6 +191,8 @@ class ModelState: outs, = self.run_policy( **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) + if after_enqueue is not None: + after_enqueue() model_output = outs.numpy()[0] if self.chestnut and not np.all(np.isfinite(model_output)): raise RuntimeError("model output not finite") @@ -383,7 +389,9 @@ def main(demo=False): mt1 = time.perf_counter() try: - model_output = model.run(bufs, transforms, inputs) + send_chestnut = (chestnut_state is not None and + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): raise @@ -425,10 +433,6 @@ def main(demo=False): pm.send('cameraOdometry', posenet_send) last_vipc_frame_id = meta_main.frame_id - if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0: - chestnut_state.send() - - if __name__ == "__main__": try: import argparse From 4a13639cfd122ccb9113a4d6ce225dcbd8e61914 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 19:12:49 -0700 Subject: [PATCH 322/325] reduce chestnut states (#38705) ui: unify chestnut status presentation --- .../assets/icons_mici/chestnut_crossed.png | 3 -- .../assets/icons_mici/chestnut_gray.png | 3 -- openpilot/selfdrive/ui/mici/layouts/home.py | 12 +++--- .../selfdrive/ui/mici/onroad/hud_renderer.py | 37 ++++++------------ openpilot/selfdrive/ui/ui_state.py | 38 +++++++++++++++++-- 5 files changed, 53 insertions(+), 40 deletions(-) delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_gray.png diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png b/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png deleted file mode 100644 index 4fc5decb59..0000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842 -size 2034 diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png b/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png deleted file mode 100644 index a6aeb84689..0000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472 -size 7279 diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 50dc95901f..519580925f 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (50, 37)) - self._chestnut_icon_gray = IconWidget("icons_mici/chestnut_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -151,7 +151,7 @@ class MiciHomeLayout(Widget): NetworkIcon(), self._experimental_icon, self._chestnut_icon, - self._chestnut_icon_gray, + self._chestnut_failed_icon, self._body_icon, self._mic_icon, ], spacing=18) @@ -248,8 +248,8 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.chestnut_compiled) - self._chestnut_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.chestnut_compiled) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index e844f5e9e7..3bb2f70b84 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -107,7 +107,6 @@ class HudRenderer(Widget): self.speed: float = 0.0 self.v_ego_cluster_seen: bool = False self._engaged: bool = False - self._small_model_engaged: bool = False self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True @@ -127,9 +126,7 @@ class HudRenderer(Widget): self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) - self._txt_chestnut_crossed: rl.Texture = gui_app.texture('icons_mici/chestnut_crossed.png', 60, 52) self._chestnut_icon: rl.Texture | None = None - self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) @@ -165,13 +162,10 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.chestnut_loading and ui_state.chestnut_active is not True and - ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): - self._small_model_engaged = True - if engaged != self._engaged: - self._chestnut_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() + if engaged != self._engaged: + self._chestnut_fade_time = rl.get_time() if engaged else 0 self._engaged = engaged self.set_speed = set_speed self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA @@ -191,8 +185,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.chestnut and ui_state.chestnut_compiled: - self._draw_model_source(rect) + self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +193,24 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.chestnut_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.chestnut_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and - not ui_state.sm.alive['modelV2']) or - (ui_state.chestnut_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) - self._small_model_engaged &= big_failed - loading = ui_state.chestnut_loading or (ui_state.chestnut_active is None and not big_failed) + loading = ui_state.chestnut_state == ChestnutState.LOADING if loading: - pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) icon = self._txt_chestnut - opacity = 0.35 + 0.65 * pulse - elif self._small_model_engaged: - icon = self._txt_chestnut_crossed - opacity = 0.65 - elif big_failed: + opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): icon = self._txt_chestnut_orange opacity = 1.0 - else: + elif ui_state.chestnut_state == ChestnutState.ACTIVE: icon = self._txt_chestnut_green opacity = 1.0 + else: + return if icon is not self._chestnut_icon: self._chestnut_fade_time = rl.get_time() self._chestnut_icon = icon - alpha = self._chestnut_alpha_filter.update(loading or 0 < rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE) + visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE + alpha = self._chestnut_alpha_filter.update(visible) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 63a71758fa..c169cbeaff 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -24,6 +24,15 @@ class UIStatus(Enum): OVERRIDE = "override" +class ChestnutState(Enum): + DISCONNECTED = "disconnected" + UNCOMPILED = "uncompiled" + READY = "ready" + LOADING = "loading" + ACTIVE = "active" + FAILED = "failed" + + class UIState: _instance: 'UIState | None' = None @@ -77,10 +86,11 @@ class UIState: self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.chestnut: bool = False + self.chestnut_present: bool = False self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None self.chestnut_loading: bool = False + self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -126,6 +136,7 @@ class UIState: self.sm.update(0) self._update_state() self._update_status() + self._update_chestnut_state() device.update() def _params_refresh_worker(self): @@ -186,12 +197,35 @@ class UIState: self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame self.started_time = time.monotonic() + self.chestnut_present = self.sm["deviceState"].chestnutPresent for callback in self._offroad_transition_callbacks: callback() self._started_prev = self.started + def _update_chestnut_state(self) -> None: + detected = self.sm["deviceState"].chestnutPresent + if not self.started: + self.chestnut_present = detected + self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else + ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED) + return + + model_seen = self.sm.recv_frame["modelV2"] > self.started_frame + if not self.chestnut_present: + self.chestnut_state = ChestnutState.DISCONNECTED + elif not self.chestnut_compiled: + self.chestnut_state = ChestnutState.UNCOMPILED + elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): + self.chestnut_state = ChestnutState.FAILED + elif self.chestnut_loading or not model_seen: + self.chestnut_state = ChestnutState.LOADING + elif self.chestnut_active is False: + self.chestnut_state = ChestnutState.FAILED + else: + self.chestnut_state = ChestnutState.ACTIVE + def update_params(self) -> None: # For slower operations # Update longitudinal control state @@ -208,8 +242,6 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep chestnut UI active until offroad transition when gpu disappears - self.chestnut = self.sm["deviceState"].chestnutPresent or (self.chestnut and self.started) if not self.chestnut_compiled: self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive") From 2d6cc4c065c4d1833dc267fff60ebae48b444817 Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 27 Aug 2026 02:03:53 -0400 Subject: [PATCH 323/325] models: Model Selector upgrades (#1953) * uh, i did not commit anything all this time * slideee to the left, cha cha * lint lint * ui: unify model source predicate and per-source bundle lookup in model_info * [TIZI/TICI] ui: disable the other-model row onroad like the active row * [TIZI/TICI] ui: drop docstring that restates the function name * [TIZI/TICI] ui: keep Favorites as the first model folder in the picker * ui: record why model names read the params slots and not modelManagerSP * ui: show the default model's name on the picker Default entries * models: bind a download to its ref so cancel and reselect work everywhere * models: resume partial chunked downloads and verify silently * models: publish a verifying status so cached checks read as verification, not a stuck download * [TIZI/TICI] ui: move download status onto each model's own row * [TIZI/TICI] ui: show the row status description while it has text * [TIZI/TICI] ui: restore the Model Status bar row * models: a cancel interrupts verification immediately and keeps on-disk chunks * models: a selection made mid-download queues instead of cancelling the transfer * [TIZI/TICI] ui: Model Status shows both slots idle and the queued pick while busy * [TIZI/TICI] ui: label the Model Status slots small and big and scroll long names * models: start a queued download in the same tick and label empty slots (Default) * ui: scroll Model Status names at the corrected speed * [TIZI/TICI] ui: Model Status shows the big model failing over to small * [TIZI/TICI] ui: stable model rows and a runner-matched failover note on Model Status * [TIZI/TICI] ui: model rows show full names and the failover note reopens with the page * ui: name the actually driving model runner-matched and bring mici to state parity * fix ugly --------- Co-authored-by: Jason Wen Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> --- openpilot/cereal/custom.capnp | 1 + .../ui/sunnypilot/layouts/settings/models.py | 204 +++++++++++++----- .../ui/sunnypilot/mici/layouts/models.py | 118 +++++++--- .../selfdrive/ui/sunnypilot/model_info.py | 88 ++++++++ openpilot/sunnypilot/models/manager.py | 98 ++++++--- .../models/tests/test_manager_download.py | 82 +++++++ .../ui/sunnypilot/widgets/download_status.py | 47 +++- 7 files changed, 521 insertions(+), 117 deletions(-) create mode 100644 openpilot/selfdrive/ui/sunnypilot/model_info.py diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index c20bf923be..086b10c01c 100644 --- a/openpilot/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -131,6 +131,7 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { downloaded @2; cached @3; failed @4; + verifying @5; } struct DownloadProgress { diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py index 93668014f6..3aa115139f 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -10,11 +10,10 @@ import time import pyray as rl from openpilot.cereal import custom -from openpilot.sunnypilot.models.default_model import get_default_model -from openpilot.sunnypilot.models.fetcher import ModelFetcher -from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS +from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle, resolve_bundle_by_ref from openpilot.common.constants import CV from openpilot.selfdrive.ui.ui_state import device, ui_state +from openpilot.selfdrive.ui.sunnypilot.model_info import big_model_state, bundles_for_source, carrying_model, default_model_name, queued_name from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import DialogResult, Widget @@ -38,7 +37,10 @@ class ModelsLayout(Widget): super().__init__() self.model_manager = None self.model_dialog = None + self._selection_source = None self._downloading = False + self._verifying = False + self._last_note = None self.last_cache_calc_time = 0 self._initialize_items() @@ -50,17 +52,24 @@ class ModelsLayout(Widget): self._scroller = Scroller(self.items, line_separator=True, spacing=0) def _initialize_items(self): - self.current_model_item = ListItemSP( - title=tr("Current Model"), + self.small_model_item = ListItemSP( + title=tr("Small Model"), description="", action_item=ScrollingButtonAction(tr("SELECT")), - callback=self._handle_current_model_clicked + callback=lambda: self._open_source_dialog("qcom") + ) + + self.big_model_item = ListItemSP( + title=tr("Big Model"), + action_item=ScrollingButtonAction(tr("SELECT")), + callback=lambda: self._open_source_dialog("usbgpu") ) self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), + ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 0), gui_app.push_widget(alert_dialog(tr("Fetching Latest Models"))))) self.clear_cache_item = ListItemSP( @@ -70,7 +79,9 @@ class ModelsLayout(Widget): callback=self._clear_cache ) - self.cancel_download_item = button_item(tr("Cancel Download"), tr("Cancel"), "", lambda: ui_state.params.remove("ModelManager_DownloadRef")) + self.cancel_download_item = button_item(lambda: tr("Cancel Verification") if self._verifying else tr("Cancel Download"), + tr("Cancel"), "", + lambda: ui_state.params.remove("ModelManager_DownloadRef")) self.lane_turn_value_control = option_item_sp(tr("Adjust Lane Turn Speed"), "LaneTurnValue", 500, 2000, tr("Set the maximum speed for lane turn desires. Default is 19 mph."), @@ -95,7 +106,7 @@ class ModelsLayout(Widget): 1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, lambda v: f"{v / 100:.2f} m") - self.items = [self.current_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item, + self.items = [self.small_model_item, self.big_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset] def _update_lagd_description(self, lagd_toggle: bool): @@ -109,10 +120,6 @@ class ModelsLayout(Widget): desc += f"
    {tr('Actuator Delay:')} {cp:.2f} s + {tr('Software Delay:')} {sw:.2f} s = {tr('Total Delay:')} {cp + sw:.2f} s" self.lagd_toggle.set_description(desc) - def _is_downloading(self): - return (self.model_manager and self.model_manager.selectedBundle and - self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading) - @staticmethod def calculate_cache_size(): cache_size = 0.0 @@ -135,36 +142,90 @@ class ModelsLayout(Widget): gui_app.push_widget(dialog) def _handle_bundle_download_progress(self): - self.download_item.set_visible(False) self.cancel_download_item.set_visible(False) self._downloading = False - - if not self.model_manager or (not self.model_manager.selectedBundle and not self.model_manager.activeBundle): - return - - bundle = self.model_manager.selectedBundle if self._is_downloading() or ( - self.model_manager.selectedBundle and self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed - ) else self.model_manager.activeBundle - if not bundle: - return - - self.cancel_download_item.set_visible(bool(self.model_manager.selectedBundle) and ui_state.params.get("ModelManager_DownloadRef") is not None) + self._verifying = False + self.download_item.set_visible(True) if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5: self.last_cache_calc_time = current_time self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB") + bundle = self.model_manager.selectedBundle if self.model_manager else None + progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName] if bundle else [] + if not progresses or bundle.status not in (custom.ModelManagerSP.DownloadStatus.downloading, + custom.ModelManagerSP.DownloadStatus.failed): + self.download_item.action_item.update(name="", segments=self._slot_segments()) + return + + self.cancel_download_item.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None) if bundle.status == custom.ModelManagerSP.DownloadStatus.downloading: device._reset_interactive_timeout() - # every bundle is a single chunked artifact now - progresses = [model.artifact.downloadProgress for model in bundle.models if model.artifact.fileName] - if not progresses: - return - - self.download_item.set_visible(True) - self.download_item.action_item.update(**self._download_row_state(progresses, bundle.internalName)) + state = self._download_row_state(progresses, bundle.internalName) + if queued := queued_name(bundle.ref): + state["name"] += f" | {queued} {tr('queued')}" + self.download_item.action_item.update(**state) self._downloading = self.download_item.action_item.downloading + ds = custom.ModelManagerSP.DownloadStatus + self._verifying = any(getattr(p.status, 'raw', p.status) == ds.verifying for p in progresses) + + def _slot_segments(self): + """small and big slots side by side; green marks the slot whose pick is actually + driving (runner-matched, so a failed Default big greens neither slot), an empty + slot shows its default.""" + big_state = big_model_state() + carry_source, carry_internal, _ = carrying_model() + segments = [] + for source, label in (("qcom", tr("small")), ("usbgpu", tr("big"))): + if segments: + segments.append(("|", rl.GRAY, None, None)) + bundle = get_selected_bundle(ui_state.params, source) + name = bundle.internalName if bundle else default_model_name(source) + color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY + name = "● " + name + if source == "usbgpu": + if big_state == 'failed': + color = rl.RED + elif big_state == 'loading': + color = rl.GOLD + segments.append((label, rl.GRAY, None, None)) + segments.append((name, color, None, None)) + return segments + + @staticmethod + def _set_item_note(item, text): + # a description renders only while shown; hide before clearing or the + # empty description keeps its visible state + if text: + item.set_description(text) + item.show_description(True) + else: + item.show_description(False) + item.set_description("") + + def _status_note(self) -> str: + """The failover story for the Model Status row. One-way big -> small, and the + fallback is runner-matched: a Default big can only fall back to the Default + small (stock modeld), a custom big has no automatic fallback yet.""" + if not ui_state.usbgpu: + return "" + big_bundle = get_selected_bundle(ui_state.params, "usbgpu") + big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu") + big_is_default = big_bundle is None + fallback_name = default_model_name("qcom") + state = big_model_state() + if state == 'failed': + if big_is_default: + return tr("Big model unavailable, {} is driving until the next drive.").format(fallback_name) + return tr("Big model unavailable until the next drive.") + if state == 'loading': + if big_is_default: + return tr("{} drives until the big model is ready.").format(fallback_name) + return tr("Getting the big model ready.") + if big_is_default: + return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name) + return tr("{} will drive when the eGPU is ready.").format(big_name) @staticmethod def _download_row_state(progresses, name: str) -> dict: @@ -177,6 +238,8 @@ class ModelsLayout(Widget): if ds.failed in statuses: # close.png is authored black and a tint cannot lift it, hence close2 return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"} + if ds.verifying in statuses: + return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")} if ds.downloading in statuses: return {"name": name, "downloading": True, "progress": progress} if statuses <= {ds.downloaded, ds.cached}: @@ -186,46 +249,66 @@ class ModelsLayout(Widget): def _on_model_selected(self, result): if result != DialogResult.CONFIRM: + self.model_dialog = None return selected_ref = self.model_dialog.selection_ref - if selected_ref == "Default": - source = ModelFetcher.active_source(ui_state.sm["deviceState"].chestnutPresent) - ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) - elif selected_bundle := next((bundle for bundle in self.model_manager.availableBundles if bundle.ref == selected_ref), None): - ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) self.model_dialog = None + if selected_ref == "Default": + if self._selection_source in ACTIVE_BUNDLE_KEYS: + ui_state.params.remove(ACTIVE_BUNDLE_KEYS[self._selection_source]) + return + if selected_bundle := self._resolve_selected_bundle(selected_ref): + ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) + + def _resolve_selected_bundle(self, ref): + source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} + resolved = resolve_bundle_by_ref(ref, source_bundles) + return resolved[0] if resolved else None @staticmethod def _bundle_to_node(bundle): return TreeNode(bundle.ref, {'display_name': bundle.displayName, 'short_name': bundle.internalName}) - def _get_folders(self, favorites): - bundles = self.model_manager.availableBundles + def _get_folders(self, favorites, bundles): folders = {} for bundle in bundles: folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle) - folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{get_default_model()} (Default)", - 'short_name': "Default"})])] + folders_list = [] for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True): folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True) name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "") folders_list.append(TreeFolder(name, [self._bundle_to_node(bundle) for bundle in folder_bundles])) if favorites and (fav_bundles := [bundle for bundle in bundles if bundle.ref in favorites]): - folders_list.insert(1, TreeFolder("Favorites", [self._bundle_to_node(bundle) for bundle in fav_bundles])) + folders_list.insert(0, TreeFolder("Favorites", [self._bundle_to_node(bundle) for bundle in fav_bundles])) return folders_list - def _handle_current_model_clicked(self): + def _open_source_dialog(self, source): + self._selection_source = source favs = ui_state.params.get("ModelManager_Favs") favorites = set(favs.split(';')) if favs else set() - folders_list = self._get_folders(favorites) - - active_ref = self.model_manager.activeBundle.ref if self.model_manager.activeBundle else "Default" - self.model_dialog = TreeOptionDialog(tr("Select a Model"), folders_list, active_ref, "ModelManager_Favs", - get_folders_fn=self._get_folders, on_exit=self._on_model_selected) + folders_list = self._source_folders(favorites, source) + if not folders_list: + gui_app.push_widget(alert_dialog(tr("No models are available for this hardware yet. Connect to the internet and refresh the model list."))) + return + self.model_dialog = TreeOptionDialog(tr("Select a Model"), folders_list, self._slot_active_ref(source), "ModelManager_Favs", + get_folders_fn=lambda favs: self._source_folders(favs, source), on_exit=self._on_model_selected) gui_app.push_widget(self.model_dialog) + def _source_folders(self, favorites, source): + bundles = bundles_for_source(source) + if not bundles: + return [] + folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': default_model_name(source)})])] + folders_list.extend(self._get_folders(favorites, bundles)) + return folders_list + + @staticmethod + def _slot_active_ref(source: str) -> str: + bundle = get_selected_bundle(ui_state.params, source) + return bundle.ref if bundle else "Default" + def _update_state(self): advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls") turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire") @@ -244,20 +327,27 @@ class ModelsLayout(Widget): self._update_lagd_description(live_delay) self.model_manager = ui_state.sm["modelManagerSP"] self._handle_bundle_download_progress() - # read the slot through ui_state, not modelManagerSP: the manager republishes a - # tick after a chestnut change, and the stale bundle flashes the wrong model - active_name = (ui_state.active_bundle or {}).get("displayName") or f"{get_default_model()} (Default)" - self.current_model_item.action_item.set_value(active_name) - if not ui_state.is_offroad(): - self.current_model_item.action_item.set_enabled(False) - self.current_model_item.set_description(tr("Only available when vehicle is off, or always offroad mode is on")) - else: - self.current_model_item.action_item.set_enabled(True) - self.current_model_item.set_description("") + carry_source, _, carry_display = carrying_model() + for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")): + bundle = get_selected_bundle(ui_state.params, item_source) + name = bundle.displayName if bundle else default_model_name(item_source) + color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR + item.action_item.set_value(name, color) + + note = self._status_note() + if note != self._last_note: + self._last_note = note + self._set_item_note(self.download_item, note) + + offroad = ui_state.is_offroad() + self.small_model_item.action_item.set_enabled(offroad) + self.big_model_item.action_item.set_enabled(offroad) + self.small_model_item.set_description("" if offroad else tr("Only available when vehicle is off, or always offroad mode is on")) def _render(self, rect): self._scroller.render(rect) def show_event(self): self._scroller.show_event() + self._last_note = None # re-expand the failover note every time the page opens diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py index 87073d531f..183b47fa58 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py @@ -7,18 +7,37 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl from openpilot.cereal import custom -from openpilot.sunnypilot.models.default_model import get_default_model -from openpilot.sunnypilot.models.fetcher import ModelFetcher -from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS +from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog +from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle from openpilot.selfdrive.ui.mici.widgets.button import BigButton -from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout from openpilot.selfdrive.ui.ui_state import ui_state, device +from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model, + default_model_name, model_info, queued_name) from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import NavScroller +def _model_info() -> tuple[str, str, str]: + """(active model, info header, info text) for the panel. Runner-matched: the + active line names what actually drives, and a notable big-model state takes + the info pair.""" + source, active_name, other_name = model_info() + state = big_model_state() + _, _, carry_display = carrying_model() + if carry_display is None: + big = get_selected_bundle(ui_state.params, "usbgpu") + carry_display = big.displayName if big else default_model_name("usbgpu") + active_text = (carry_display or active_name).lower() + if state == 'failed': + return active_text, tr("big model"), tr("unavailable") + if state == 'loading': + return active_text, tr("big model"), tr("getting ready") + header = tr("small model") if source == "usbgpu" else tr("big model") + return active_text, header, other_name.lower() + + class CurrentModelInfo(Widget): def __init__(self): super().__init__() @@ -28,12 +47,12 @@ class CurrentModelInfo(Widget): header_color = rl.Color(255, 255, 255, int(255 * 0.9)) subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) max_width = int(self._rect.width - 20) + active_text, info_header, info_text = _model_info() self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) - default_text = f"{get_default_model()} (Default)".lower() - self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) + self.current_model_text = UnifiedLabel(active_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) - self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) - self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN) + self.info_header = UnifiedLabel(info_header, 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) + self.info_text = UnifiedLabel(info_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) def _render(self, _): self.current_model_header.set_position(self._rect.x + 20, self._rect.y - 10) @@ -57,6 +76,7 @@ class ModelsLayoutMici(NavScroller): self._download_progress = "." self._download_frame = 0 self._was_downloading = False + self._selection_source: str | None = None self.select_model_btn = BigButton(tr("select model")) self.select_model_btn.set_click_callback(self._show_folders) @@ -71,8 +91,7 @@ class ModelsLayoutMici(NavScroller): def model_manager(self): return ui_state.sm["modelManagerSP"] - def _get_grouped_bundles(self, favorites = None): - bundles = self.model_manager.availableBundles + def _get_grouped_bundles(self, bundles, favorites = None): folders = {} for bundle in bundles: folder = next((override.value for override in bundle.overrides if override.key == "folder"), "") @@ -92,48 +111,70 @@ class ModelsLayoutMici(NavScroller): def _show_folders(self): self.focused_widget = self.select_model_btn + hardware_btns = [] + active = active_source() + for source, label in (("qcom", tr("small models")), ("usbgpu", tr("big models"))): + bundle = get_selected_bundle(ui_state.params, source) + value = (bundle.internalName if bundle else default_model_name(source)).lower() + if source == active: + value += f" ({tr('active')})" + btn = BigButton(label.lower(), value=value) + btn.set_click_callback(lambda s=source: self._select_hardware(s)) + hardware_btns.append(btn) + self._push_selection_view(hardware_btns) + + def _select_hardware(self, source): + self._selection_source = source + favs = ui_state.params.get("ModelManager_Favs") favorites = set(favs.split(';')) if favs else set() - folders = self._get_grouped_bundles(favorites) + bundles = bundles_for_source(source) + if not bundles: + gui_app.push_widget(BigDialog(title=tr("No models available"), + description=tr("No models are available for this hardware yet. Connect to the internet and refresh the model list."))) + return + folders = self._get_grouped_bundles(bundles, favorites) + folder_buttons = [] - default_btn = BigButton(f"{get_default_model()} (Default)".lower()) - default_btn.set_click_callback(self._select_default) + default_btn = BigButton(default_model_name(source).lower()) + default_btn.set_click_callback(lambda s=source: self._select_default(s)) folder_buttons.append(default_btn) for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True): - if folder.lower() in ["release models", "master models", "favorites"]: - btn = BigButton(folder.lower()) - btn.set_click_callback(lambda f=folder: self._select_folder(f)) - if folder.lower() == "favorites": - folder_buttons.insert(0, btn) - else: - folder_buttons.append(btn) + btn = BigButton(folder.lower()) + btn.set_click_callback(lambda f=folder: self._select_folder(f)) + if folder.lower() == "favorites": + folder_buttons.insert(0, btn) + else: + folder_buttons.append(btn) self._push_selection_view(folder_buttons) def _pop_to_main(self): gui_app.pop_widgets_to(self) + self._scroller.scroll_panel.set_offset(0.0) def _select_model(self, bundle): ui_state.params.put("ModelManager_DownloadRef", bundle.ref) self._pop_to_main() - def _select_default(self): - source = ModelFetcher.active_source(ui_state.sm["deviceState"].chestnutPresent) + def _select_default(self, source): ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) self._pop_to_main() def _select_folder(self, folder_name): + source = self._selection_source + if source is None: # folders are only reachable after picking a hardware + return favs = ui_state.params.get("ModelManager_Favs") favorites = set(favs.split(';')) if favs else set() - folders = self._get_grouped_bundles(favorites) + folders = self._get_grouped_bundles(bundles_for_source(source), favorites) bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True) btns = [] for bundle in bundles: - txt = bundle.displayName.lower() - btn = BigButton(txt) + btn = BigButton(bundle.displayName.lower()) btn.set_click_callback(lambda b=bundle: self._select_model(b)) btns.append(btn) self._push_selection_view(btns) @@ -165,12 +206,10 @@ class ModelsLayoutMici(NavScroller): self._was_downloading = is_downloading self.current_model_info.current_model_header.set_text(tr("active model")) - # read the slot through ui_state, not modelManagerSP: the manager republishes a - # tick after a chestnut change, and the stale bundle flashes the wrong model - model_text = ((ui_state.active_bundle or {}).get("displayName") or f"{get_default_model()} (Default)").lower() - self.current_model_info.current_model_text.set_text(model_text) - self.current_model_info.info_header.set_text(tr("cache size")) - self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB") + active_text, info_header, info_text = _model_info() + self.current_model_info.current_model_text.set_text(active_text) + self.current_model_info.info_header.set_text(info_header) + self.current_model_info.info_text.set_text(info_text) if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed: self.current_model_info.info_header.set_text(tr("error") + self._download_progress) @@ -181,18 +220,29 @@ class ModelsLayoutMici(NavScroller): device.set_override_interactive_timeout(5) progress = 0.0 count = 0 + verifying = False for model in manager.selectedBundle.models: count += 1 p = model.artifact.downloadProgress - if p.status == custom.ModelManagerSP.DownloadStatus.downloading: + if p.status in (custom.ModelManagerSP.DownloadStatus.downloading, + custom.ModelManagerSP.DownloadStatus.verifying): progress += p.progress + verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, custom.ModelManagerSP.DownloadStatus.cached): progress += 100.0 - self.current_model_info.current_model_header.set_text(tr("downloading")) + self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading")) + self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download")) self.current_model_info.current_model_header._shimmer = True - self.current_model_info.current_model_text.set_text(f"{manager.selectedBundle.internalName.lower()}") + name_text = manager.selectedBundle.internalName.lower() + if queued := queued_name(manager.selectedBundle.ref): + name_text += f" | {queued.lower()} {tr('queued')}" + self.current_model_info.current_model_text.set_text(name_text) self.current_model_info.info_header.set_text(tr("progress") + self._download_progress) self.current_model_info.info_header._shimmer = True self.current_model_info.info_text.set_text(f"{progress/count:.2f}%") + + elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded: + self.current_model_info.info_header.set_text(tr("downloaded")) + self.current_model_info.info_text.set_text(tr("downloaded")) diff --git a/openpilot/selfdrive/ui/sunnypilot/model_info.py b/openpilot/selfdrive/ui/sunnypilot/model_info.py new file mode 100644 index 0000000000..a93a06f187 --- /dev/null +++ b/openpilot/selfdrive/ui/sunnypilot/model_info.py @@ -0,0 +1,88 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.sunnypilot.models.fetcher import get_cached_bundles +from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref +from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL + + +def active_source() -> str: + return get_active_source(usbgpu=ui_state.usbgpu, + usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_loading, + offroad=ui_state.is_offroad()) + + +def bundles_for_source(source: str): + if source == active_source(): + return ui_state.sm["modelManagerSP"].availableBundles + return get_cached_bundles(ui_state.params, source) + + +def default_model(source: str) -> str: + return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL + + +def default_model_name(source: str) -> str: + return f"{default_model(source)} (Default)" + + +def big_model_state() -> str | None: + """'failed' | 'loading' | None, mirroring the sidebar's detection (#1969).""" + if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed: + return 'failed' + big_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad + if ui_state.usbgpu_loading or (big_selected and ui_state.started and ui_state.usbgpu_active is None): + return 'loading' + return None + + +def carrying_model() -> tuple[str | None, str | None, str | None]: + """(source, internal name, display name) of what actually drives. Runner-matched: + when a Default big cannot carry, stock modeld runs the Default small, never the + small slot's pick; a custom big has no automatic fallback yet -> (None, None, None).""" + source = active_source() + if source == "usbgpu": + bundle = get_selected_bundle(ui_state.params, "usbgpu") + if bundle: + return "usbgpu", bundle.internalName, bundle.displayName + name = default_model_name("usbgpu") + return "usbgpu", name, name + if ui_state.usbgpu: + if get_selected_bundle(ui_state.params, "usbgpu") is None: + name = default_model_name("qcom") + return "qcom", name, name + return None, None, None + bundle = get_selected_bundle(ui_state.params, "qcom") + if bundle: + return "qcom", bundle.internalName, bundle.displayName + name = default_model_name("qcom") + return "qcom", name, name + + +def queued_name(current_ref) -> str | None: + ref = ui_state.params.get("ModelManager_DownloadRef") + if ref and ref != current_ref: + source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} + if resolved := resolve_bundle_by_ref(ref, source_bundles): + return resolved[0].internalName + return None + + +def model_info() -> tuple[str, str, str]: + """returns (active source, active model name, other model name) + + Names come from the params slots, never modelManagerSP.activeBundle — the + manager republishes a tick after a chestnut change, so the stale bundle + would flash the wrong model.""" + source = active_source() + other = "qcom" if source == "usbgpu" else "usbgpu" + active_bundle = get_selected_bundle(ui_state.params, source) + other_bundle = get_selected_bundle(ui_state.params, other) + + active_name = active_bundle.displayName if active_bundle else default_model_name(source) + other_name = other_bundle.displayName if other_bundle else default_model_name(other) + return source, active_name, other_name diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 2405566d55..178d6c04e5 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -24,6 +24,10 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_ DOWNLOAD_TIMEOUT = (30, 30) +class DownloadCancelled(Exception): + pass + + class ModelManagerSP: """Manages model downloads and status reporting""" @@ -39,6 +43,17 @@ class ModelManagerSP: self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) self._chunk_size = 128 * 1000 # 128 KB chunks self._download_start_times: dict[str, float] = {} # Track start time per model + self._download_ref: bytes | str | None = None + + def _download_interrupted(self) -> bool: + # only removal cancels: a different ref is a queued selection that + # _release_download_ref leaves in place for the next tick + return self.params.get("ModelManager_DownloadRef") is None + + def _release_download_ref(self) -> None: + if self.params.get("ModelManager_DownloadRef") == self._download_ref: + self.params.remove("ModelManager_DownloadRef") + self._download_ref = None def _sync_artifact_progress(self, source_artifact) -> None: """Mirror download progress to all artifacts sharing the same filename in the selected bundle.""" @@ -80,8 +95,8 @@ class ModelManagerSP: f.write(chunk) bytes_downloaded += len(chunk) - if self.params.get("ModelManager_DownloadRef") is None: - raise Exception("Download cancelled") + if self._download_interrupted(): + raise DownloadCancelled("Download cancelled") if total_size > 0: progress = (bytes_downloaded / total_size) * 100 @@ -94,7 +109,7 @@ class ModelManagerSP: # Clean up start time after download completes del self._download_start_times[model.fileName] - async def _download_chunked(self, base_url: str, base_path: str, artifact) -> None: + async def _download_chunked(self, base_url: str, base_path: str, artifact, skip: frozenset[int] | set[int] = frozenset()) -> None: from openpilot.common.file_chunker import get_chunk_name, get_manifest_path num_chunks = len(artifact.chunks) @@ -106,8 +121,11 @@ class ModelManagerSP: # Shared connection saves a TCP+TLS handshake per chunk. # Keep sequential: the link saturates on one stream and Session is not thread-safe. + completed = len(skip) with requests.Session() as session: for i, _ in enumerate(artifact.chunks): + if i in skip: + continue chunk_url = get_chunk_name(base_url, i, num_chunks) chunk_path = get_chunk_name(base_path, i, num_chunks) chunk_downloaded = 0 @@ -118,15 +136,16 @@ class ModelManagerSP: for data in response.iter_content(chunk_size=self._chunk_size): f.write(data) chunk_downloaded += len(data) - if self.params.get("ModelManager_DownloadRef") is None: - raise Exception("Download cancelled") + if self._download_interrupted(): + raise DownloadCancelled("Download cancelled") intra = chunk_downloaded / max(chunk_size, 1) - progress = min(99.0, ((i + intra) / num_chunks) * 100) + progress = min(99.0, ((completed + intra) / num_chunks) * 100) artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading artifact.downloadProgress.progress = progress artifact.downloadProgress.eta = self._calculate_eta(artifact.fileName, progress) self._sync_artifact_progress(artifact) self._report_status() + completed += 1 with open(manifest_path, 'w') as f: # noqa: ASYNC230 f.write(str(num_chunks)) @@ -137,6 +156,8 @@ class ModelManagerSP: async def _process_artifact(self, artifact, destination_path: str) -> None: if not artifact.downloadUri.uri: return None + if self._download_interrupted(): + raise DownloadCancelled("Download cancelled") url = artifact.downloadUri.uri expected_hash = artifact.downloadUri.sha256 @@ -144,21 +165,23 @@ class ModelManagerSP: full_path = os.path.join(destination_path, filename) try: + # progress counts only valid chunks so a resumed download continues the + # bar from where verification left it, instead of falling back to zero is_cached = False + valid_chunks: set[int] = set() if len(artifact.chunks) > 0: from openpilot.common.file_chunker import get_chunk_name num_chunks = len(artifact.chunks) - chunks_valid = True for i, chunk in enumerate(artifact.chunks): - chunk_path = get_chunk_name(full_path, i, num_chunks) - if not await verify_file(chunk_path, chunk.sha256): - chunks_valid = False - break - artifact.downloadProgress.progress = ((i + 1) / num_chunks) * 100 + if self._download_interrupted(): + raise DownloadCancelled("Download cancelled") + if await verify_file(get_chunk_name(full_path, i, num_chunks), chunk.sha256): + valid_chunks.add(i) + artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.verifying + artifact.downloadProgress.progress = (len(valid_chunks) / num_chunks) * 100 self._sync_artifact_progress(artifact) self._report_status() - if chunks_valid and num_chunks > 0: - is_cached = True + is_cached = len(valid_chunks) == num_chunks else: if await verify_file(full_path, expected_hash): is_cached = True @@ -172,7 +195,7 @@ class ModelManagerSP: return if len(artifact.chunks) > 0: - await self._download_chunked(url, full_path, artifact) + await self._download_chunked(url, full_path, artifact, skip=valid_chunks) from openpilot.common.file_chunker import get_chunk_name for i, chunk in enumerate(artifact.chunks): chunk_path = get_chunk_name(full_path, i, len(artifact.chunks)) @@ -189,6 +212,17 @@ class ModelManagerSP: self._sync_artifact_progress(artifact) self._report_status() + except DownloadCancelled: + # a cancel keeps whatever is on disk: complete chunks resume the next attempt + self._download_start_times.pop(artifact.fileName, None) + artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed + artifact.downloadProgress.eta = 0 + self._sync_artifact_progress(artifact) + if self.selected_bundle: + self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed + self._report_status() + raise + except Exception as e: cloudlog.error(f"Error downloading {filename}: {str(e)}") for f in [full_path] + [p for p in (os.path.join(destination_path, f) for f in os.listdir(destination_path)) if filename in p]: @@ -242,6 +276,8 @@ class ModelManagerSP: seen_artifacts.add(artifact.fileName) await self._process_artifact(artifact, destination_path) + if self._download_interrupted(): + raise DownloadCancelled("Download cancelled") self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True) self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) @@ -258,6 +294,27 @@ class ModelManagerSP: """Main entry point for downloading a model bundle""" asyncio.run(self._download_bundle(model_bundle, destination_path, source)) + def _process_download_requests(self) -> None: + # loops so a ref queued during a download starts in the same tick, without + # the bar dropping to idle for a tick between the two transfers + last_ref = None + while (ref_to_download := self.params.get("ModelManager_DownloadRef")) is not None: + if ref_to_download == last_ref: # a repeating ref falls back to the next tick instead of spinning + return + last_ref = ref_to_download + resolved = resolve_bundle_by_ref(ref_to_download, self.source_models) + if not resolved: + return + model_to_download, source = resolved + self._download_ref = ref_to_download + try: + self.download(model_to_download, Paths.model_root(), source) + except Exception as e: + cloudlog.exception(e) + finally: + self._release_download_ref() + self.selected_bundle = None + def main_thread(self) -> None: """Main thread for model management""" rk = Ratekeeper(1, print_delay_threshold=None) @@ -271,16 +328,7 @@ class ModelManagerSP: validate_active_bundles(self.params, self.source_models) self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) - if (ref_to_download := self.params.get("ModelManager_DownloadRef")) is not None: - if resolved := resolve_bundle_by_ref(ref_to_download, self.source_models): - model_to_download, source = resolved - try: - self.download(model_to_download, Paths.model_root(), source) - except Exception as e: - cloudlog.exception(e) - finally: - self.params.remove("ModelManager_DownloadRef") - self.selected_bundle = None + self._process_download_requests() if self.params.get("ModelManager_ClearCache"): self.clear_model_cache() diff --git a/openpilot/sunnypilot/models/tests/test_manager_download.py b/openpilot/sunnypilot/models/tests/test_manager_download.py index d74deb03e6..4d3b7989fb 100644 --- a/openpilot/sunnypilot/models/tests/test_manager_download.py +++ b/openpilot/sunnypilot/models/tests/test_manager_download.py @@ -103,6 +103,7 @@ class ManagerDownloadTestBase(OpenpilotTestCase): self.manager = ModelManagerSP.__new__(ModelManagerSP) self.manager.params = mock.MagicMock() self.manager.params.get.return_value = b'0' # not cancelled + self.manager._download_ref = b'0' self.manager.pm = mock.MagicMock() self.manager.pm.send.side_effect = self._record_progress self.manager.selected_bundle = None @@ -261,6 +262,7 @@ class TestManagerDownload(ManagerDownloadTestBase): artifact = self.make_artifact(chunked=True) base_path = os.path.join(self.dest, artifact.fileName) self.manager.params.get.side_effect = lambda key: b"ref" if key == "ModelManager_DownloadRef" else None + self.manager._download_ref = b"ref" asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) assert os.path.isfile(get_manifest_path(base_path)) self.run_with_server(body) @@ -279,12 +281,92 @@ class TestManagerDownload(ManagerDownloadTestBase): return b"0" self.manager.params.get.side_effect = get + self.manager._download_ref = b"ref" with self.assertRaises(Exception) as ctx: asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) assert 'cancelled' in str(ctx.exception).lower() assert not os.path.isfile(get_manifest_path(base_path)) self.run_with_server(body) + def test_replaced_download_ref_queues_instead_of_cancelling(self): + """Selecting another model mid-transfer lets the running download finish.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + self.manager.params.get.side_effect = lambda key: b"other-ref" if key == "ModelManager_DownloadRef" else None + self.manager._download_ref = b"ref" + asyncio.run(self.manager._download_chunked(artifact.downloadUri.uri, base_path, artifact)) + assert os.path.isfile(get_manifest_path(base_path)) + self.run_with_server(body) + + def test_replaced_download_ref_is_kept(self): + """A selection made during a download must survive that download's cleanup.""" + self.manager.params.get.return_value = b"new-ref" + self.manager._download_ref = b"old-ref" + self.manager._release_download_ref() + self.manager.params.remove.assert_not_called() + + def test_own_download_ref_is_released(self): + self.manager.params.get.return_value = b"ref" + self.manager._download_ref = b"ref" + self.manager._release_download_ref() + self.manager.params.remove.assert_called_once_with("ModelManager_DownloadRef") + + def test_cached_bundle_cancel_skips_slot_write(self): + """A cancel must stop an already-on-disk bundle before it is applied to the slot.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + for i, data in enumerate(CHUNK_BODIES): + with open(get_chunk_name(base_path, i, len(CHUNK_BODIES)), 'wb') as f: + f.write(data) + self._bundle.ref = "test-ref" + params, store = self._make_params_with_store() + store["ModelManager_DownloadRef"] = None # removed -> cancelled + self.manager.params = params + self.manager._download_ref = b"ref" + with self.assertRaises(Exception) as ctx: + asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) + assert 'cancelled' in str(ctx.exception).lower() + assert "ModelManager_ActiveBundle" not in store + assert all(os.path.isfile(p) for p in self.chunk_paths(base_path)), "cancel must not delete cached chunks" + self.run_with_server(body) + + def test_resume_skips_valid_chunks(self): + """A chunk already on disk is kept and not re-downloaded; progress starts above its share.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + with open(get_chunk_name(base_path, 0, len(CHUNK_BODIES)), 'wb') as f: + f.write(CHUNK_BODIES[0]) + + asyncio.run(self.manager._process_artifact(artifact, self.dest)) + + chunk0_suffix = get_chunk_name('', 0, len(CHUNK_BODIES)) + assert not any(p.endswith(chunk0_suffix) for p in DownloadHandler.request_paths), "valid chunk was re-downloaded" + for i, expected in enumerate(CHUNK_BODIES): + with open(get_chunk_name(base_path, i, len(CHUNK_BODIES)), 'rb') as f: + assert f.read() == expected + assert os.path.isfile(get_manifest_path(base_path)) + assert min(self.reported) >= (1 / len(CHUNK_BODIES)) * 100 - 1, "progress must not restart below the resumed share" + self.run_with_server(body) + + def test_verify_reports_valid_fraction_then_cached(self): + """A fully cached bundle publishes climbing verify progress and ends cached.""" + def body(): + artifact = self.make_artifact(chunked=True) + base_path = os.path.join(self.dest, artifact.fileName) + for i, data in enumerate(CHUNK_BODIES): + with open(get_chunk_name(base_path, i, len(CHUNK_BODIES)), 'wb') as f: + f.write(data) + + asyncio.run(self.manager._process_artifact(artifact, self.dest)) + + assert DownloadHandler.request_paths == [], "cached bundle must not hit the network" + assert [round(p) for p in self.reported[:3]] == [33, 67, 100] + assert artifact.downloadProgress.status == custom.ModelManagerSP.DownloadStatus.cached + self.run_with_server(body) + def _make_params_with_store(self): params = mock.MagicMock() store = {} diff --git a/openpilot/system/ui/sunnypilot/widgets/download_status.py b/openpilot/system/ui/sunnypilot/widgets/download_status.py index b299c464f1..135bd151a5 100644 --- a/openpilot/system/ui/sunnypilot/widgets/download_status.py +++ b/openpilot/system/ui/sunnypilot/widgets/download_status.py @@ -16,6 +16,7 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP from openpilot.system.ui.widgets.label import UnifiedLabel +from openpilot.system.ui.sunnypilot.lib.utils import UnifiedLabelSP from openpilot.system.ui.widgets.list_view import ItemAction FONT_SIZE = style.ITEM_TEXT_FONT_SIZE @@ -24,6 +25,8 @@ ICON_PADDING = 12 BAR_WIDTH = 1100 BAR_HEIGHT = 20 +SEGMENT_GAP = 24 +SEGMENT_NAME_MAX_WIDTH = 380 BAR_GAP = 16 BAR_RADIUS = BAR_HEIGHT / 2 CAPSULE_POINTS = 24 @@ -45,6 +48,8 @@ class DownloadStatusAction(ItemAction): super().__init__(width=BAR_WIDTH) self.name = "" self.status_text = "" + self.segments: list[tuple[str, rl.Color, str | None, rl.Color | None]] | None = None + self._segment_labels: list[UnifiedLabelSP] = [] self.downloading = False self.text_color = rl.GRAY self.icon: str | None = None @@ -62,7 +67,8 @@ class DownloadStatusAction(ItemAction): alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None): + def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None): + self.segments = segments if downloading and not self.downloading: self._name_label.reset_shimmer() self._progress.x = progress @@ -85,11 +91,22 @@ class DownloadStatusAction(ItemAction): def get_width_hint(self) -> float: if self.downloading: return BAR_WIDTH + if self.segments: + return sum(total for _, _, total in self._measured_segments()) width = measure_text_cached(self._font, self._idle_text, FONT_SIZE).x if self.icon: width += ICON_SIZE + ICON_PADDING return width + def _measured_segments(self): + """[(segment, text width, total width incl. icon and gap)]""" + out = [] + for i, seg in enumerate(self.segments or []): + text_width = min(measure_text_cached(self._font, seg[0], FONT_SIZE).x, SEGMENT_NAME_MAX_WIDTH) + total = text_width + (ICON_PADDING + ICON_SIZE if seg[2] else 0) + (SEGMENT_GAP if i else 0) + out.append((seg, text_width, total)) + return out + def _render(self, rect: rl.Rectangle): if self.downloading: self._render_downloading(rect) @@ -134,6 +151,8 @@ class DownloadStatusAction(ItemAction): def _render_downloading(self, rect: rl.Rectangle): percent = f"{int(self._progress.x)}%" + if self.status_text: + percent = f"{self.status_text} {percent}" text_height = measure_text_cached(self._font, percent, FONT_SIZE).y top = rect.y + (rect.height - (text_height + BAR_GAP + BAR_HEIGHT)) / 2 @@ -148,6 +167,9 @@ class DownloadStatusAction(ItemAction): self._draw_fill(rail, max(0.0, min(rect.width, rect.width * (self._progress.x / 100.0)))) def _render_idle(self, rect: rl.Rectangle): + if self.segments: + self._render_segments(rect) + return text = self._idle_text text_size = measure_text_cached(self._font, text, FONT_SIZE) right = rect.x + rect.width @@ -161,6 +183,29 @@ class DownloadStatusAction(ItemAction): rl.draw_text_ex(self._font, text, rl.Vector2(right - text_size.x, rect.y + (rect.height - text_size.y) / 2), FONT_SIZE, 0, self.text_color) + def _render_segments(self, rect: rl.Rectangle): + measured = self._measured_segments() + while len(self._segment_labels) < len(measured): + self._segment_labels.append(UnifiedLabelSP("", font_size=FONT_SIZE, max_width=SEGMENT_NAME_MAX_WIDTH, + scroll=True, wrap_text=False)) + x = rect.x + rect.width - sum(total for _, _, total in measured) + for i, ((text, color, icon, icon_color), text_width, _) in enumerate(measured): + if i: + x += SEGMENT_GAP + label = self._segment_labels[i] + if label.text != text: + label.set_text(text) + label.set_text_color(color) + text_height = measure_text_cached(self._font, text, FONT_SIZE).y + label.set_position(x, rect.y + (rect.height - text_height) / 2) + label.render() + x += text_width + if icon: + texture = gui_app.texture(icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True) + rl.draw_texture_v(texture, rl.Vector2(x + ICON_PADDING, rect.y + (rect.height - texture.height) / 2), + icon_color or color) + x += ICON_PADDING + ICON_SIZE + def download_status_item(title): return ListItemSP(title=title, action_item=DownloadStatusAction(), title_color=style.ITEM_TEXT_COLOR) From 9f43d2477d29198f21e528c50cc2a7b20ba82f23 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 27 Aug 2026 03:52:57 -0400 Subject: [PATCH 324/325] [MICI] ui: move and restyle the sunnylink pill in settings (#1972) --- .../ui/sunnypilot/mici/layouts/settings.py | 18 ++++++++++++++---- .../selfdrive/assets/icons_mici/sunnylink.png | 3 +++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 openpilot/sunnypilot/selfdrive/assets/icons_mici/sunnylink.png diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py index f14efe51a3..4c0eba41ea 100644 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py @@ -12,13 +12,23 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, Bi from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr ICON_SIZE = 70 BIG_ICON_SIZE = 110 +class SunnylinkBigButton(SettingsBigButton): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._label.set_font_weight(FontWeight.AUDIOWIDE) + + def _get_label_font_size(self): + # Audiowide runs wider than Inter: "sunnylink" wraps to two lines at 64 + return 56 + + class SettingsLayoutSP(OP.SettingsLayout): def __init__(self): OP.SettingsLayout.__init__(self) @@ -33,7 +43,7 @@ class SettingsLayoutSP(OP.SettingsLayout): self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE) sunnylink_panel = SunnylinkLayoutMici() - sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55)) + sunnylink_btn = SunnylinkBigButton(tr("sunnylink"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/sunnylink.png", 76, 44)) sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel)) models_panel = ModelsLayoutMici() @@ -56,8 +66,8 @@ class SettingsLayoutSP(OP.SettingsLayout): items = self._scroller._items.copy() - items.insert(1, sunnylink_btn) - items.insert(2, models_btn) + items.insert(1, models_btn) + items.insert(5, sunnylink_btn) # front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad items.insert(0, self._enable_offroad_btn_onroad) diff --git a/openpilot/sunnypilot/selfdrive/assets/icons_mici/sunnylink.png b/openpilot/sunnypilot/selfdrive/assets/icons_mici/sunnylink.png new file mode 100644 index 0000000000..6639536f9a --- /dev/null +++ b/openpilot/sunnypilot/selfdrive/assets/icons_mici/sunnylink.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:447099e93e303b29e7b3eac237bb0f27f8c5e12786991139aee2432532a75f58 +size 12310 From 4075befc5e5fea9a4fc1e68eaa23ca5bbd01824f Mon Sep 17 00:00:00 2001 From: Nayan Date: Thu, 27 Aug 2026 11:23:26 -0400 Subject: [PATCH 325/325] osm: support map deletion via sunnylink (#1971) delete delete --- openpilot/common/params_keys.h | 1 + .../ui/sunnypilot/layouts/settings/osm.py | 15 ++------------- openpilot/sunnypilot/mapd/mapd_manager.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 4d8ffb64eb..5461424c13 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -246,6 +246,7 @@ inline static std::unordered_map keys = { // mapd {"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}}, + {"Mapd_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"MapdVersion", {PERSISTENT, STRING}}, {"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}}, {"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py index 7b30e880f9..8e1c4afe72 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -8,7 +8,6 @@ import datetime import os import platform import requests -import shutil import threading from pathlib import Path from time import monotonic @@ -75,22 +74,12 @@ class OSMLayout(Widget): def _update_map_size(self): threading.Thread(target=self.calculate_size, daemon=True).start() - def _do_delete_maps(self): - if MAP_PATH.exists(): - shutil.rmtree(MAP_PATH) - - for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"): - ui_state.params.remove(param) - + def _on_confirm_delete_maps(self): + ui_state.params.put_bool("Mapd_ClearCache", True) self._delete_maps_btn.action_item.set_enabled(True) self._delete_maps_btn.action_item.set_text(tr("DELETE")) self._update_map_size() - def _on_confirm_delete_maps(self): - self._delete_maps_btn.action_item.set_enabled(False) - self._delete_maps_btn.action_item.set_text("DELETING...") - threading.Thread(target=self._do_delete_maps).start() - def _delete_maps(self): self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"), tr("Yes, delete all maps"), self._on_confirm_delete_maps) diff --git a/openpilot/sunnypilot/mapd/mapd_manager.py b/openpilot/sunnypilot/mapd/mapd_manager.py index 2251289bbe..899b0c2bd9 100755 --- a/openpilot/sunnypilot/mapd/mapd_manager.py +++ b/openpilot/sunnypilot/mapd/mapd_manager.py @@ -55,6 +55,19 @@ def cleanup_old_osm_data(files_to_remove: list[str]) -> None: shutil.rmtree(file, ignore_errors=False) +def clear_downloaded_maps() -> None: + """Deletes downloaded OSM map data and resets params.""" + path = f"{Paths.mapd_root()}/offline" + if os.path.exists(path): + shutil.rmtree(path, ignore_errors=True) + + for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", + "OsmStateName", "OsmStateTitle"): + params.remove(param) + + cloudlog.info("mapd: downloaded maps cleared") + + def request_refresh_osm_location_data(nations: list[str], states: list[str] | None = None) -> None: params.put("OsmDownloadedDate", str(datetime.now().timestamp()), block=True) params.put_bool("OsmDbUpdatesCheck", False, block=True) @@ -131,6 +144,10 @@ def main_thread(): show_alert = bool(get_files_for_cleanup() and params.get_bool("OsmLocal")) set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.") + if params.get("Mapd_ClearCache"): + clear_downloaded_maps() + params.remove("Mapd_ClearCache") + update_osm_db() live_map_sp.tick() rk.keep_time()