Merge branch 'min-feat/dev/model-selector' into full

This commit is contained in:
Rick Lan
2025-06-17 19:01:48 +08:00
8 changed files with 488 additions and 1 deletions

View File

@@ -127,4 +127,6 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
{"dp_device_beep", PERSISTENT},
{"dp_lat_alka", PERSISTENT},
{"dp_ui_display_mode", PERSISTENT},
{"dp_device_model_selected", PERSISTENT},
{"dp_device_model_list", PERSISTENT},
};

View File

@@ -5,6 +5,9 @@ export MKL_NUM_THREADS=1
export NUMEXPR_NUM_THREADS=1
export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1
if [ -s /data/params/d/dp_device_model_selected ]; then
export FINGERPRINT="$(cat /data/params/d/dp_device_model_selected)"
fi
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="12.3"

View File

@@ -26,6 +26,7 @@ Export('widgets')
qt_libs = [widgets, qt_util] + base_libs
qt_src = ["main.cc", "ui.cc", "qt/sidebar.cc", "qt/body.cc",
"qt/offroad/model_selector.cc",
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
"qt/offroad/software_settings.cc", "qt/offroad/developer_panel.cc", "qt/offroad/onboarding.cc", "qt/offroad/dp_panel.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", "qt/offroad/firehose.cc",

View File

@@ -0,0 +1,226 @@
// MIT Non-Commercial License
//
// Copyright (c) 2019, dragonpilot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, for non-commercial purposes only, subject to the following conditions:
//
// - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// - Commercial use (e.g., use in a product, service, or activity intended to generate revenue) is prohibited without explicit written permission from dragonpilot. Contact ricklan@gmail.com for inquiries.
// - Any project that uses the Software must visibly mention the following acknowledgment: "This project uses software from dragonpilot and is licensed under a custom license requiring permission for use."
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "selfdrive/ui/qt/offroad/model_selector.h"
// Define style constants to improve maintainability
namespace {
const QString SELECTOR_BTN_STYLE = "background-color: #00309a; font-size: 48px;";
const QString MODEL_LIST_STYLE = "font-size: 64px;";
const QString SCROLLBAR_STYLE = "width: 96px;";
const QString GROUP_HEADER_BG_COLOR = "#c8c8c8"; // Light gray
const QString GROUP_HEADER_TEXT_COLOR = "#000000"; // Black
// Role for storing the actual model name without indentation
const int ModelNameRole = Qt::UserRole;
}
ModelSelector::ModelSelector(QWidget *parent) : QWidget(parent) {
setupUI();
setupModelListPanel();
connectSignals();
}
QWidget* ModelSelector::setupUI() {
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->addSpacing(10);
// Selector button
QWidget* model_selector_btn_widget = new QWidget;
QHBoxLayout* model_selector_btn_layout = new QHBoxLayout();
QLabel* vehicle_model_label = new QLabel(tr("Vehicle Model:"));
vehicle_model_label->setStyleSheet("margin-right: 2px; font-size: 48px;");
model_selector_btn_layout->addWidget(vehicle_model_label);
QString model_selected = QString::fromUtf8(Params().get("dp_device_model_selected").c_str());
model_selector_btn = new QPushButton(model_selected.isEmpty() ? tr("[AUTO DETECT]") : model_selected);
model_selector_btn->setObjectName("ModelSelectorBtn");
model_selector_btn->setStyleSheet(SELECTOR_BTN_STYLE);
model_selector_btn_layout->addWidget(model_selector_btn);
model_selector_btn_layout->setAlignment(Qt::AlignCenter);
model_selector_btn_layout->setStretch(1, 1);
model_selector_btn_widget->setLayout(model_selector_btn_layout);
main_layout->addWidget(model_selector_btn_widget);
main_layout->addSpacing(10);
main_layout->addStretch(); // Add stretch to push everything to the top
setLayout(main_layout);
return model_selector_btn_widget;
}
void ModelSelector::setupModelListPanel() {
// Create model list panel
model_list_panel = new QWidget();
QVBoxLayout* model_list_layout = new QVBoxLayout(model_list_panel);
model_list_layout->setContentsMargins(50, 25, 50, 25);
model_list = new QListWidget(model_list_panel);
// Set styles using the constants
QString listStyle = QString("QListWidget { %1 } QScrollBar:vertical { %2 }")
.arg(MODEL_LIST_STYLE)
.arg(SCROLLBAR_STYLE);
model_list->setStyleSheet(listStyle);
model_list->setFixedHeight(750);
model_list_layout->addWidget(model_list);
model_list_frame = new ScrollView(model_list_panel, nullptr);
}
void ModelSelector::loadModelList() {
if (model_list->count() > 0) {
// If list is already populated, just update the selection
updateCurrentSelection();
return;
}
// Add auto-detect option
QListWidgetItem* autoDetectItem = new QListWidgetItem(tr("[AUTO DETECT]"));
autoDetectItem->setData(ModelNameRole, tr("[AUTO DETECT]"));
model_list->addItem(autoDetectItem);
Params params;
QString model_list_str = QString::fromStdString(params.get("dp_device_model_list"));
QJsonDocument document = QJsonDocument::fromJson(model_list_str.toUtf8());
if (document.isArray()) {
QJsonArray models = document.array();
for (const auto& groupValue : models) {
QJsonObject group = groupValue.toObject();
QString groupName = group["group"].toString();
// Add group header item
QListWidgetItem* groupHeader = new QListWidgetItem(groupName);
groupHeader->setFlags(Qt::NoItemFlags); // Make non-selectable
groupHeader->setBackground(QColor(GROUP_HEADER_BG_COLOR));
groupHeader->setForeground(QColor(GROUP_HEADER_TEXT_COLOR));
groupHeader->setTextAlignment(Qt::AlignCenter);
model_list->addItem(groupHeader);
// Add models in this group
QJsonArray groupModels = group["models"].toArray();
for (const auto& model : groupModels) {
QString modelName = model.toString();
// Create item with visual indentation
QListWidgetItem* modelItem = new QListWidgetItem(" " + modelName);
// Store actual model name without indentation in user role
modelItem->setData(ModelNameRole, modelName);
model_list->addItem(modelItem);
}
}
}
// Set the current selection after loading
updateCurrentSelection();
}
// New helper method to update the selection
void ModelSelector::updateCurrentSelection() {
// Get the currently selected model from params
Params params;
QString currentModel = QString::fromStdString(params.get("dp_device_model_selected"));
// If empty, select the AUTO DETECT option
if (currentModel.isEmpty()) {
model_list->setCurrentRow(0); // AUTO DETECT is the first item
return;
}
// Otherwise, find and select the matching model
for (int i = 0; i < model_list->count(); i++) {
QListWidgetItem* item = model_list->item(i);
// Only check selectable items (not group headers)
if (item->flags() & Qt::ItemIsSelectable) {
QString modelName = item->data(ModelNameRole).toString();
if (modelName == currentModel) {
model_list->setCurrentItem(item);
break;
}
}
}
}
void ModelSelector::clearModelList() {
model_list->clear();
}
void ModelSelector::updateButtonText(const QString& text) {
model_selector_btn->setText(text);
}
QWidget* ModelSelector::getModelListPanel() {
return model_list_frame;
}
void ModelSelector::setPanelWidget(QStackedWidget* panel) {
panel_widget = panel;
if (panel_widget && model_list_frame->parent() != panel_widget) {
model_list_frame->setParent(panel_widget);
panel_widget->addWidget(model_list_frame);
}
}
void ModelSelector::setNavButtonGroup(QButtonGroup* buttons) {
nav_btns = buttons;
}
void ModelSelector::connectSignals() {
connect(model_selector_btn, &QPushButton::clicked, [this]() {
if (panel_widget) {
// Load the model list when needed
loadModelList();
panel_widget->setCurrentWidget(model_list_frame);
}
emit buttonClicked();
});
connect(model_list, &QListWidget::itemClicked, [this](QListWidgetItem* item) {
// Only process clicks on selectable items (not group headers)
if (item->flags() & Qt::ItemIsSelectable) {
// Get model name from the data role rather than trimming text
QString model_name = item->data(ModelNameRole).toString();
QString param_value = (model_name == tr("[AUTO DETECT]")) ? QString() : model_name;
// Update param and button text
Params().put("dp_device_model_selected", param_value.toStdString());
updateButtonText(param_value.isEmpty() ? tr("[AUTO DETECT]") : model_name);
// Emit signal that model was selected
emit modelSelected(model_name);
// Go back to the previous panel
if (nav_btns && nav_btns->checkedButton()) {
nav_btns->checkedButton()->click();
} else if (nav_btns && nav_btns->buttons().size() > 0) {
// Default to first panel if none selected
nav_btns->buttons().first()->click();
}
}
});
if (model_list_frame) {
model_list_frame->installEventFilter(this);
}
}
bool ModelSelector::eventFilter(QObject *obj, QEvent *event) {
if (obj == model_list_frame) {
if (event->type() == QEvent::Hide) {
clearModelList();
}
}
return QWidget::eventFilter(obj, event);
}

View File

@@ -0,0 +1,69 @@
// MIT Non-Commercial License
//
// Copyright (c) 2019, dragonpilot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, for non-commercial purposes only, subject to the following conditions:
//
// - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// - Commercial use (e.g., use in a product, service, or activity intended to generate revenue) is prohibited without explicit written permission from dragonpilot. Contact ricklan@gmail.com for inquiries.
// - Any project that uses the Software must visibly mention the following acknowledgment: "This project uses software from dragonpilot and is licensed under a custom license requiring permission for use."
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include <QWidget>
#include <QPushButton>
#include <QListWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QListWidgetItem>
#include <QDebug>
#include <QStackedWidget>
#include <QButtonGroup> // Add this include for QButtonGroup
#include <QEvent>
#include "common/params.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
class ModelSelector : public QWidget {
Q_OBJECT
public:
explicit ModelSelector(QWidget *parent = nullptr);
// Get the model list panel widget
QWidget* getModelListPanel();
// Set the panel widget to switch to when selecting models
void setPanelWidget(QStackedWidget* panel);
// Set the button group to return to after model selection
void setNavButtonGroup(QButtonGroup* nav_btns);
signals:
void buttonClicked();
void modelSelected(const QString& model_name);
private:
QPushButton* model_selector_btn;
QListWidget* model_list;
QWidget* model_list_panel;
ScrollView* model_list_frame;
QStackedWidget* panel_widget = nullptr;
QButtonGroup* nav_btns = nullptr;
QWidget* setupUI();
void setupModelListPanel();
void loadModelList();
void updateCurrentSelection();
void clearModelList();
void connectSignals();
void updateButtonText(const QString& text);
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
};

View File

@@ -16,6 +16,7 @@
#include "selfdrive/ui/qt/offroad/developer_panel.h"
#include "selfdrive/ui/qt/offroad/firehose.h"
#include "selfdrive/ui/qt/offroad/dp_panel.h"
#include "selfdrive/ui/qt/offroad/model_selector.h"
TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
// param, title, desc, icon, restart needed
@@ -482,7 +483,26 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
sidebar_widget->setFixedWidth(500);
main_layout->addWidget(sidebar_widget);
main_layout->addWidget(panel_widget);
// Create right column with model selector on top and panel_widget below
QWidget* right_column = new QWidget(this);
QVBoxLayout* right_layout = new QVBoxLayout(right_column);
right_layout->setContentsMargins(0, 0, 0, 0);
right_layout->setSpacing(20); // Space between model selector and panel
// Create the ModelSelector button at the top of right column
ModelSelector* model_selector = new ModelSelector(this);
right_layout->addWidget(model_selector);
// Set up panel widget and nav button references
model_selector->setPanelWidget(panel_widget);
model_selector->setNavButtonGroup(nav_btns);
// Add panel_widget below the model selector
right_layout->addWidget(panel_widget, 1); // Give panel_widget stretch priority
// Add right column to main layout
main_layout->addWidget(right_column);
setStyleSheet(R"(
* {

View File

@@ -17,6 +17,7 @@ from openpilot.system.athena.registration import register, UNREGISTERED_DONGLE_I
from openpilot.common.swaglog import cloudlog, add_file_handler
from openpilot.system.version import get_build_metadata, terms_version, training_version
from openpilot.system.hardware.hw import Paths
from openpilot.system.manager.vehicle_model_collector import VehicleModelCollector
def manager_init() -> None:
@@ -45,6 +46,8 @@ def manager_init() -> None:
("dp_device_beep", "0"),
("dp_lat_alka", "0"),
("dp_ui_display_mode", "0"),
("dp_device_model_selected", ""),
("dp_device_model_list", ""),
]
if params.get_bool("RecordFrontLock"):
@@ -55,6 +58,8 @@ def manager_init() -> None:
if params.get(k) is None:
params.put(k, v)
params.put("dp_device_model_list", VehicleModelCollector().get())
# Create folders needed for msgq
try:
os.mkdir(Paths.shm_path())

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
# MIT Non-Commercial License
#
# Copyright (c) 2019, dragonpilot
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, for non-commercial purposes only, subject to the following conditions:
#
# - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# - Commercial use (e.g., use in a product, service, or activity intended to generate revenue) is prohibited without explicit written permission from dragonpilot. Contact ricklan@gmail.com for inquiries.
# - Any project that uses the Software must visibly mention the following acknowledgment: "This project uses software from dragonpilot and is licensed under a custom license requiring permission for use."
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import os
import importlib
import json
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
class VehicleModelCollector:
def __init__(self):
self.base_package = "opendbc.car"
self.base_path = f"{BASEDIR}/opendbc/car"
self.exclude_brands = ['body', 'mock']
# Define the lookup dictionary for brand-to-group mappings
self.brand_to_group_map = {
"chrysler": [
{"prefix": "DODGE_", "group": "Dodge"},
{"prefix": "RAM_", "group": "Ram"},
{"prefix": "JEEP_", "group": "Jeep"},
],
"gm": [
{"prefix": "BUICK_", "group": "Buick"},
{"prefix": "CADILLAC_", "group": "Cadillac"},
{"prefix": "CHEVROLET_", "group": "Chevrolet"},
{"prefix": "HOLDEN_", "group": "Holden"},
],
"honda": {"prefix": "ACURA_", "group": "Acura"},
"toyota": {"prefix": "LEXUS_", "group": "Lexus"},
"hyundai": [
{"prefix": "KIA_", "group": "Kia"},
{"prefix": "GENESIS_", "group": "Genesis"}
],
"volkswagen": [
{"prefix": "AUDI_", "group": "Audi"},
{"prefix": "SKODA_", "group": "Skoda"},
{"prefix": "SEAT_", "group": "Seat"}
]
}
# Define exceptions for group names
self.group_name_exceptions = {
"gm": "GM",
}
@staticmethod
def is_car_model(car_class, attr):
"""Check if the attribute is a car model (not callable and not a dunder attribute)"""
return not callable(getattr(car_class, attr)) and not attr.startswith("__")
@staticmethod
def move_to_proper_group(models, prefix):
"""
Moves models with a certain prefix to their respective group.
Example: Models starting with 'LEXUS_' should go to 'Lexus' group.
"""
moved_models = []
for model in models[:]: # Iterate over a copy to avoid modifying during iteration
if model.startswith(prefix):
moved_models.append(model)
models.remove(model) # Remove from the original group
return moved_models
def format_group_name(self, group_name):
"""
Formats group names according to the exceptions dictionary.
Groups in the exceptions dictionary are returned in all caps, others are title cased.
"""
return self.group_name_exceptions.get(group_name, group_name.title())
def collect_models(self):
"""Collect all car models and organize them by brand/group"""
# List all subdirectories (car brands)
car_brands = sorted([
name for name in os.listdir(self.base_path)
if os.path.isdir(os.path.join(self.base_path, name)) and not name.startswith("__")
])
grouped_models = {}
# Import CAR from each subdirectory and group models by brand
for brand in car_brands:
if brand in self.exclude_brands:
continue
module_name = f"{self.base_package}.{brand}.values"
try:
module = importlib.import_module(module_name)
if hasattr(module, "CAR"):
car_class = getattr(module, "CAR")
models = sorted([attr for attr in dir(car_class) if self.is_car_model(car_class, attr)])
# Check if the brand has a special group in the lookup map
if brand in self.brand_to_group_map:
group_info = self.brand_to_group_map[brand]
if isinstance(group_info, list): # If multiple prefixes for the brand
for prefix_info in group_info:
moved_models = self.move_to_proper_group(models, prefix_info["prefix"])
if moved_models:
if prefix_info["group"] not in grouped_models:
grouped_models[prefix_info["group"]] = []
grouped_models[prefix_info["group"]].extend(moved_models)
else: # Single prefix for the brand
moved_models = self.move_to_proper_group(models, group_info["prefix"])
if moved_models:
if group_info["group"] not in grouped_models:
grouped_models[group_info["group"]] = []
grouped_models[group_info["group"]].extend(moved_models)
# Add remaining models to the respective brand
if models:
grouped_models[brand] = models
except ModuleNotFoundError:
pass
# Sort the groups alphabetically
sorted_grouped_models = sorted(grouped_models.items(), key=lambda x: x[0])
# Convert to the desired output structure, ensuring models are sorted within each group
output = [{"group": self.format_group_name(group), "models": sorted(models)}
for group, models in sorted_grouped_models]
return output
# def save_to_params(self, output=None):
# """Save the collected model list to Params"""
# if output is None:
# output = self.collect_models()
# Params().put("dp_device_model_list", json.dumps(output))
# return output
#
# def run(self):
# """Collect models and save to params"""
# models = self.collect_models()
# self.save_to_params(models)
# return models
def get_json(self):
return self.collect_models()
def get(self):
return json.dumps(self.collect_models())
# Allow running as a script
if __name__ == "__main__":
collector = VehicleModelCollector()
print(collector.get())