From 0cfd6de2b3c0077678209e5946268745206e885c Mon Sep 17 00:00:00 2001 From: Rick Lan Date: Tue, 11 Nov 2025 09:25:56 +0800 Subject: [PATCH] feat: Squash all core features into min --- .vscode/extensions.json | 3 + .vscode/settings.json | 17 +- LICENSE.md | 30 ++ README.md | 127 +++----- README_EN.md | 74 +++++ README_OPENPILOT.md | 107 +++++++ common/params_keys.h | 4 +- dragonpilot/.gitignore | 0 dragonpilot/selfdrive/assets/.gitignore | 0 dragonpilot/selfdrive/assets/dragonpilot.png | Bin 0 -> 21118 bytes .../selfdrive/assets/icons/icon_empty.svg | 35 ++ .../selfdrive/assets/images/spinner_comma.png | Bin 0 -> 88317 bytes dragonpilot/selfdrive/controls/.gitignore | 0 dragonpilot/selfdrive/controls/lib/.gitignore | 0 dragonpilot/selfdrive/ui/.gitignore | 0 .../ui/layouts/settings/dragonpilot.py | 87 +++++ opendbc_repo/opendbc/car/body/interface.py | 2 +- opendbc_repo/opendbc/car/car_helpers.py | 4 +- .../opendbc/car/chrysler/interface.py | 2 +- opendbc_repo/opendbc/car/docs.py | 2 +- opendbc_repo/opendbc/car/ford/interface.py | 2 +- opendbc_repo/opendbc/car/gm/interface.py | 2 +- opendbc_repo/opendbc/car/honda/interface.py | 2 +- opendbc_repo/opendbc/car/hyundai/interface.py | 2 +- .../opendbc/car/hyundai/tests/test_hyundai.py | 6 +- opendbc_repo/opendbc/car/interfaces.py | 8 +- opendbc_repo/opendbc/car/mazda/interface.py | 2 +- opendbc_repo/opendbc/car/mock/interface.py | 2 +- opendbc_repo/opendbc/car/nissan/interface.py | 2 +- opendbc_repo/opendbc/car/psa/interface.py | 2 +- opendbc_repo/opendbc/car/rivian/interface.py | 2 +- opendbc_repo/opendbc/car/structs.py | 3 + opendbc_repo/opendbc/car/subaru/interface.py | 2 +- opendbc_repo/opendbc/car/tesla/interface.py | 2 +- .../opendbc/car/tests/test_car_interfaces.py | 2 +- opendbc_repo/opendbc/car/toyota/interface.py | 2 +- .../opendbc/car/volkswagen/interface.py | 2 +- selfdrive/assets/icons/plus.png | Bin 0 -> 2833 bytes selfdrive/car/card.py | 4 +- selfdrive/car/cruise.py | 2 +- selfdrive/car/tests/test_models.py | 2 +- .../controls/lib/longitudinal_planner.py | 4 +- selfdrive/controls/plannerd.py | 6 +- selfdrive/selfdrived/selfdrived.py | 2 +- selfdrive/ui/layouts/settings/developer.py | 14 +- selfdrive/ui/layouts/settings/settings.py | 3 + selfdrive/ui/layouts/settings/software.py | 2 +- selfdrive/ui/layouts/settings/toggles.py | 14 + system/manager/manager.py | 9 +- system/manager/process_config.py | 2 +- system/sentry.py | 9 +- system/ui/spinner.py | 2 +- system/ui/widgets/list_view.py | 298 +++++++++++++++++- 53 files changed, 787 insertions(+), 125 deletions(-) create mode 100644 LICENSE.md create mode 100644 README_EN.md create mode 100644 README_OPENPILOT.md create mode 100644 dragonpilot/.gitignore create mode 100644 dragonpilot/selfdrive/assets/.gitignore create mode 100644 dragonpilot/selfdrive/assets/dragonpilot.png create mode 100644 dragonpilot/selfdrive/assets/icons/icon_empty.svg create mode 100644 dragonpilot/selfdrive/assets/images/spinner_comma.png create mode 100644 dragonpilot/selfdrive/controls/.gitignore create mode 100644 dragonpilot/selfdrive/controls/lib/.gitignore create mode 100644 dragonpilot/selfdrive/ui/.gitignore create mode 100644 dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py create mode 100644 selfdrive/assets/icons/plus.png diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 458312f..03ba8b0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,5 +4,8 @@ "ms-vscode.cpptools", "elagil.pre-commit-helper", "charliermarsh.ruff", + "JamiTech.simply-blame", + "k--kato.intellij-idea-keybindings", + "trinm1709.dracula-theme-from-intellij" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f0731c3..c62cc3d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,10 +3,25 @@ "editor.insertSpaces": true, "editor.renderWhitespace": "trailing", "files.trimTrailingWhitespace": true, + "terminal.integrated.defaultProfile.linux": "dragonpilot", + "terminal.integrated.profiles.linux": { + "dragonpilot": { + "path": "bash", + "args": ["-c", "distrobox enter dp"] + } + }, "search.exclude": { "**/.git": true, "**/.venv": true, - "**/__pycache__": true + "**/__pycache__": true, + "msgq_repo/": true, + "opendbc/": true, + "rednose/": true, + "rednose_repo/": true, + "openpilot/": true, + "teleoprtc_repo/": true, + "tinygrad/": true, + "tinygrad_repo/": true }, "files.exclude": { "**/.git": true, diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..abeb9f0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,30 @@ +Copyright (c) 2019, Rick Lan + +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, and/or sublicense, +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 + the copyright holder. + +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. + +--- +Copyright (c) 2018, Comma.ai, Inc. + +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, and to permit persons to whom the +Software is furnished to do so, 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. + +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. diff --git a/README.md b/README.md index 9f1819a..e0c7fe4 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,74 @@ -
+![](dragonpilot/selfdrive/assets/dragonpilot.png) -

openpilot

+[Read this in English](README_EN.md) -

- openpilot is an operating system for robotics. -
- Currently, it upgrades the driver assistance system in 300+ supported cars. -

+# **🐲 dragonpilot - 賦予您的愛車「龍」之魂** -

- Docs - · - Roadmap - · - Contribute - · - Community - · - Try it on a comma 3X -

+**我們與您一同翱翔於更智慧、更貼心的駕駛旅程。** -Quick start: `bash <(curl -fsSL openpilot.comma.ai)` +## **👋 嘿, 朋友,歡迎您的到來!** -[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai) -[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai) +`dragonpilot` 誕生於 2019 年,由三位早期的 openpilot 華人玩家共同創立。初衷很簡單:為廣大的華人用戶、玩家們提供一個友善的交流環境、更簡便的設定協助,並加入更多適合在地使用的貼心功能。 -
+我們深知在地化的重要性,特別是語言的親切感。因此,我們率先導入了完整的中文介面,讓 `dragonpilot` 迅速在華語地區累積了口碑,也讓華人的使用者數量在全球名列前茅。這份來自在地的支持,是我們持續前進的最大動力。 - - - - - - -
+我們以功能強大的 [openpilot](https://github.com/commaai/openpilot) 為基礎——這套據美國消費者報告評測優於市售車方案的開源輔助駕駛系統——融入了更多在地化的巧思與客製化的溫度,希望能打造出最符合您需求的駕駛夥伴。(您也可以參考我們 repo 中保留的 [openpilot 原始說明檔案](README_OPENPILOT.md)) +取名 `dragonpilot`,是因為我們希望它能像神話中的「龍」一樣,既強大又充滿智慧,為您的行車安全保駕護航。龍,在我們華人文化中,更是吉祥與力量的象徵,也代表著我們的根源與驕傲。 -Using openpilot in a car ------- +## **✨ dragonpilot 的里程碑** -To use openpilot in a car, you need four things: -1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x). -2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version. -3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md). -4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car. +我們不僅保留了 openpilot 的核心優勢,更達成了許多從社群回饋中誕生的里程碑,這些是我們引以為傲的足跡: -We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play. +* **🚘 全時置中車道維持 (ALKA)** -### Branches -| branch | URL | description | -|------------------|----------------------------------------|-------------------------------------------------------------------------------------| -| `release3` | openpilot.comma.ai | This is openpilot's release branch. | -| `release3-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | -| `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | -| `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | + 這不只是一個功能,更是 `dragonpilot` 的哲學。我們最早於 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 時便實現了這個功能,其開發歷程始於 2017 Lexus IS300h,接著擴展至 Toyota 全車系,並逐步延伸到其他支援的品牌。它能溫柔地輔助您,讓車輛始終穩定地保持在車道中央,提供一份額外的安心與從容。 -To start developing openpilot ------- +* **🌐 率先導入多國語言介面** -openpilot is developed by [comma](https://comma.ai/) and by users like you. We welcome both pull requests and issues on [GitHub](http://github.com/commaai/openpilot). + 在官方 openpilot 還未支援前,我們便已將多國語言介面實現。`dragonpilot` 完整支援繁體中文、簡體中文與英文,讓操作毫無隔閡。 -* Join the [community Discord](https://discord.comma.ai) -* Check out [the contributing docs](docs/CONTRIBUTING.md) -* Check out the [openpilot tools](tools/) -* Code documentation lives at https://docs.comma.ai -* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki) +* **💻 唯一同時支援多硬體平台** -Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors. + 我們是唯一曾致力於讓專案同時兼容 EON、comma two、comma 3 與 Jetson 平台的社群分支,這份努力是為了服務最廣大的玩家社群。 + 此外,在 comma.ai 團隊於 0.10.0 版本宣布停止支持 comma 3 後,我們仍是唯一一個完整同時支援 comma 3、comma 3X 以及 O3、O3L、O3XL(O3 系列為副廠硬體)的社群分支。 -Safety and Testing ----- +* **📜 曾榮獲官方認證第一大分支** -* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details. -* openpilot has software-in-the-loop [tests](.github/workflows/selfdrive_tests.yaml) that run on every commit. -* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details. -* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety). -* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes. -* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile). -* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes. + 基於活躍的社群與功能創新,`dragonpilot` 曾一度成長為 comma ai 官方認證的第一大 openpilot 分支,這份榮耀屬於每一位參與者。 -
-MIT Licensed +## **🧑‍💻 設計理念 - 少即是多 (Less is More)** -openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. +隨著 openpilot 的 AI 模型日益強大,許多過去需要手動微調的功能,現在都已能透過更先進的模型來實現。因此,我們現在的開發重心回歸到 **「最小化修改」(minimal changes)** 的核心原則上。 -Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. +我們的目標是為您提供最純粹、最接近官方的 openpilot 駕駛感受,同時保留 `dragonpilot` 那些經過時間考驗、最受社群喜愛的經典功能。我們相信,在強大的 AI 基礎上,簡潔即是力量。 -**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. -YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. -NO WARRANTY EXPRESSED OR IMPLIED.** -
+## **🛠️ 硬件的足跡 - 一路走來的夥伴們** -
-User Data and comma Account +從最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社群中各式各樣充滿智慧的**副廠機 (如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我們也曾探索過在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。 -By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone. +目前最新版本主要支援: comma3 / 3X 以及 O3 / O3L / O3XL 等社群硬體。 +針對 EON / C1.5 / C2 等舊款硬體,最後支援的版本位於 [d2 分支](https://github.com/dragonpilot-community/dragonpilot/tree/d2)。 +無論您手上是哪一款設備,都代表著您對開源駕駛輔助的一份熱情。 -openpilot is open source software: the user is free to disable data collection if they wish to do so. +## **🫂 加入我們,成為「尋龍者」的一份子** -openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. -The driver-facing camera and microphone are only logged if you explicitly opt-in in settings. +`dragonpilot` 的成長,離不開每一位使用者的貢獻與回饋。我們是一個以**公開、透明**為原則的溫暖社群,希望在這裡能與所有對 openpilot / dragonpilot 有興趣的用戶分享、交流開發與使用上的經驗。 -By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. -
+[**歡迎加入我們的 Facebook 社團進行交流!**](https://www.facebook.com/groups/930190251238639) + +## **❤️ 特別感謝** + +`dragonpilot` 從創立至今,從未打算透過 Patreon 等平台進行任何形式的募資。我們的初衷是建立一個讓大家能一起學習、一起成長的社群。It's all about fun, not money. + +然而,我們仍要對那些自發性支持本專案的朋友們,致上最誠摯的感謝。正是因為有您們的鼓勵,我們才有更大的動力持續前進。 + +[**我們的贊助者名單**](SPONSORS.md) + +### **安全聲明** + +`dragonpilot` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。 + +**最後,再次感謝您的到來。** + +**期待與您一同在智慧駕駛的道路上,乘「龍」而行!** \ No newline at end of file diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..2d43abe --- /dev/null +++ b/README_EN.md @@ -0,0 +1,74 @@ +![](dragonpilot/selfdrive/assets/dragonpilot.png) + +[Read this in Chinese](README.md) + +# **🐲 dragonpilot - Bringing the Spirit of the Dragon to Your Car** + +**Join us on a smarter, more thoughtful driving journey.** + +## **👋 Welcome, friend!** + +`dragonpilot` was launched in 2019 by three early openpilot enthusiasts from the Chinese community. Our mission was simple: create a friendly space for users to share experiences, provide easier setup help, and add features tailored for local needs. + +Localization has always been at the heart of what we do—starting with a fully Chinese interface. This made `dragonpilot` quickly popular in Chinese-speaking regions and helped our user base grow into one of the largest worldwide. That community support is what keeps us moving forward. + +Built on top of the powerful [openpilot](https://github.com/commaai/openpilot)—an open-source driver assistance system rated by Consumer Reports as outperforming commercial offerings—we add localized refinements and user-focused features to create a driving companion that truly fits your needs. (You can also see the [original openpilot README](README_OPENPILOT.md) preserved in our repo.) + +The name `dragonpilot` reflects our vision: like the dragon of mythology, it is strong and wise, guarding your safety on the road. In Chinese culture, the dragon is also a symbol of luck and strength, representing our roots and pride. + +## **✨ Milestones** + +Beyond carrying forward openpilot's core strengths, we've reached several milestones inspired by community feedback: + +* **🚘 Always Lane Keep Assist (ALKA)** + + More than a feature—it's part of the `dragonpilot` philosophy. Introduced as early as [version 0.6.2](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199), first tested on a 2017 Lexus IS300h, then expanded to Toyota's lineup and beyond. ALKA helps keep your vehicle steadily centered, giving you extra confidence on the road. + +* **🌐 First to add multilingual support** + + Before openpilot officially supported it, we had already introduced multiple languages. `dragonpilot` fully supports Traditional Chinese, Simplified Chinese, and English. + +* **💻 Only community fork to support multiple hardware platforms at once** + + We uniquely worked to make the project run on EON, comma two, comma 3, and Jetson—serving the widest range of users possible. + Additionally, after the comma.ai team deprecated the comma 3 in version 0.10.0, we remain the only community fork to offer full, simultaneous support for the comma 3, comma 3X, and the O3, O3L, and O3XL (the O3 series being third-party hardware). + +* **📜 Once recognized as the #1 openpilot fork** + + Thanks to an active community and continuous innovation, `dragonpilot` was once the largest openpilot fork officially recognized by comma ai. This honor belongs to everyone who contributed. + +## **🧑‍💻 Design Philosophy - Less is More** + +As openpilot's AI grows stronger, many features that once required manual tuning are now handled by advanced models. That's why our focus has returned to **“minimal changes.”** + +We aim to give you the purest, most official-like openpilot driving experience—while preserving `dragonpilot`'s classic, community-loved features. With a solid AI foundation, simplicity is strength. + +## **🛠️ Hardware Journey** + +From the early **EON**, to official devices like **comma two / three (C2/C3/C3X)**, to creative community builds (**C1.5, O2, O3, O3L, O3XL, etc.**), and even experiments with [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot). + +Currently, the latest versions support: **comma3 / 3X** and community hardware like **O3 / O3L / O3XL**. +Older devices such as **EON / C1.5 / C2** are supported in the [d2 branch](https://github.com/dragonpilot-community/dragonpilot/tree/d2). +Whatever device you're on, it represents your passion for open-source driver assistance. + +## **🫂 Join Us – Become a “Dragon Seeker”** + +`dragonpilot` thrives thanks to every user's contributions and feedback. We're an open, transparent, and welcoming community where enthusiasts can share experiences with openpilot and `dragonpilot`. + +[**Join our Facebook group here!**](https://www.facebook.com/groups/930190251238639) + +## **❤️ Special Thanks** + +Since day one, `dragonpilot` has never asked for funding through Patreon or similar platforms. Our vision is a community where everyone learns and grows together. It's about fun, not money. + +That said, we're deeply grateful to those who voluntarily supported the project. Your encouragement keeps us motivated to keep building. + +[**See our sponsors**](SPONSORS.md) + +### **Safety Notice** + +`dragonpilot` is a driver **assistance** system, not full self-driving. It reduces fatigue and improves safety, but you must remain alert and ready to take control at all times. Always follow your local traffic laws. + +**Thanks again for being here.** + +**We look forward to riding the “dragon” with you on the road to smarter driving!** diff --git a/README_OPENPILOT.md b/README_OPENPILOT.md new file mode 100644 index 0000000..9f1819a --- /dev/null +++ b/README_OPENPILOT.md @@ -0,0 +1,107 @@ +
+ +

openpilot

+ +

+ openpilot is an operating system for robotics. +
+ Currently, it upgrades the driver assistance system in 300+ supported cars. +

+ +

+ Docs + · + Roadmap + · + Contribute + · + Community + · + Try it on a comma 3X +

+ +Quick start: `bash <(curl -fsSL openpilot.comma.ai)` + +[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai) +[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai) + +
+ + + + + + + +
+ + +Using openpilot in a car +------ + +To use openpilot in a car, you need four things: +1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x). +2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version. +3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md). +4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car. + +We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play. + +### Branches +| branch | URL | description | +|------------------|----------------------------------------|-------------------------------------------------------------------------------------| +| `release3` | openpilot.comma.ai | This is openpilot's release branch. | +| `release3-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. | +| `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. | +| `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. | + +To start developing openpilot +------ + +openpilot is developed by [comma](https://comma.ai/) and by users like you. We welcome both pull requests and issues on [GitHub](http://github.com/commaai/openpilot). + +* Join the [community Discord](https://discord.comma.ai) +* Check out [the contributing docs](docs/CONTRIBUTING.md) +* Check out the [openpilot tools](tools/) +* Code documentation lives at https://docs.comma.ai +* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki) + +Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors. + +Safety and Testing +---- + +* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details. +* openpilot has software-in-the-loop [tests](.github/workflows/selfdrive_tests.yaml) that run on every commit. +* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details. +* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety). +* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes. +* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile). +* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes. + +
+MIT Licensed + +openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified. + +Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user. + +**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT. +YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS. +NO WARRANTY EXPRESSED OR IMPLIED.** +
+ +
+User Data and comma Account + +By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone. + +openpilot is open source software: the user is free to disable data collection if they wish to do so. + +openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. +The driver-facing camera and microphone are only logged if you explicitly opt-in in settings. + +By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. +
diff --git a/common/params_keys.h b/common/params_keys.h index f6e8d78..65eadb2 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -28,7 +28,7 @@ inline static std::unordered_map keys = { {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"CurrentBootlog", {PERSISTENT, STRING}}, {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, - {"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"DisableLogging", {PERSISTENT, BOOL, "0"}}, {"DisablePowerDown", {PERSISTENT, BOOL}}, {"DisableUpdates", {PERSISTENT, BOOL}}, {"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}}, @@ -128,4 +128,6 @@ inline static std::unordered_map keys = { {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"Version", {PERSISTENT, STRING}}, + {"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}}, + {"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}}, }; diff --git a/dragonpilot/.gitignore b/dragonpilot/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/dragonpilot/selfdrive/assets/.gitignore b/dragonpilot/selfdrive/assets/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/dragonpilot/selfdrive/assets/dragonpilot.png b/dragonpilot/selfdrive/assets/dragonpilot.png new file mode 100644 index 0000000000000000000000000000000000000000..1001f19426b496dacb62c591ac430500dbb8087e GIT binary patch literal 21118 zcmZUaRa6{Ju*YX{hd^+L#T^nXSb}61cY?dSJAvTA7Y{CrLvXjCSzLolV39!Z4~OLD zKHR7K&@s_p~3+G03QmG(*ytz_x~0wjDIcu!HM4gZXi$1 z_wPU}2=0&nDrmNERo((XLlW+jIr_gkgC#^$1poqA0U$I203QA|g&qQcH$MOznE`-! z766dD<#uXF{Ck1z2GREf09=y)Eg;}aF2%q8095X+w%_WBv2DAy{9MT6CT^zE6a%H4 z`b)XD%2=O2f7Zv#8m7@T-aYEH*)aC}&{StHKO8DnY(a(^^I7f%7&3KH=HEwed?k>` zHl3aAY15f+OZVSYuGGfj;$k{3zgd4M2o(d5;{QEFpo|sv%e01;qRim+8vxhN z4IV|5H3K(>R&iPtzI}HY5CT#sFvAWVzSKwCR)sGphkK)j6iyTt+GT4|hB|}r&}PW{ zGXMjCow);=H3kMOK|z&iRXFEgYyc{pK?kft!!h4lDBUle9i|mR84^GVRD)Kp$k8(O zyA*^PIaxuK+MR{ehQ0QRMnn7ptFh^i9(4J5QM5%NW#}z?7^W9tMrt1Dhz@X)NS8yu z45%s)Ovoq}{q3hEEHAkG3dp<|N#VW_{ZTG#N#p3#=2tjWSje8GgB@B8bmJ9zV#wlS zoMR)LCH{oqW}ab!MA-)$$R7G-*on^k9r+vg&OVN%Bt^tu}%z2kl3*h$rRI0 zrBS;ihHcoNhBJ zM=Q~ctZdliQkuBSUDDA{MJ5Q|UO`%g7wr4{ZLn<$mvws?J5&igFPqf0zxKKuWlr8r zuhM*<4@&gxnG{VuDSZXACKfkG_cdHzAX`}(h7hz-9&jaHeO2->k(oZEg5BO= z3^TI!+n1>-*<7|bKf3rK7bUL(uwqlEGaB2KDib{9zw|??U3z7kIlJSs+CB=yd z{v?untSN{a3=L}+thNE0x{KnO{B-B5^MjqRSTWLOq&m^`_X;Cyq~5PeG{Gm&^pc&x z#M??_ThdknKR!B-bdC(JAE!QT?B7#!hiru+%^UU7g{PmVAqE&YdHTV7ZjOH;m%Ok8 z#fgPcg814Dq#NOxlRdM!PO1}$8UEMhi35+EEyCve=kByECpL^z?PWQSwdRJ+M#^J6 z!YiqMP*RU^{)UP~mag09A}7?y?=_0hUI*F#c3D)b5{HG#V;<7RmP1(mE6JzRD+=PA zHrM$2#ZT)gj~R2e=&i3F&*nmIV}@N#6dxziavBJ3_|Mp!^Pe~4(&QR5Rq&R-f{!V2 zMAYr@97_|L|Le}-i9k|X{0@#nnuIJ8!M0Xuew3ir>`LS`jI2KW3Ch{(OS$RdC4-rf zA~yCOJr=;bsZ))du6|1g)jUI~5q|WtKReJ#JKAP=?Uj{+W+hh_mybKhf*b@C6!xu!Afh4H8)!!#RspYTTg$R0KA12kZY?b9JezpPrnuG$ChGqF zeAsd^R-dLh4y@ZSep|;X?3wJMrOfbq7rhenEG414Mw;}ySJ<=*>Ng1bojW8J$_9#O z6PrNtP{n)^uWIU+t5r^4y~9U5YL z^~mfr;%fCXMJbuaT4-vT_eye=@m1+(WG0x8X=Tznm)Y4Zi$QGRX9rH~RpTd}xAy&^ zVNL=y#MEcH2?a=l3yM>Y2I*>^5vuKTC(oSiNi;-oaNWU+oWWa>9I@#p|FctqGttF; zO0K(p>t4$sBz_2|YGk6|t(D1vy>JoZOUquZftwD3vu;E#5voR7eeXGmQ}#{Jn@Sq~ z6_(}y-e!u$H~F6$(=STBS(fY6GY~Kp`%V2U=y-wkXFFx~Y1%wDL-@)&+yT}XgX|na zKNAt}?yHnN-FXM&nZOnxk}tcC$Dd8~@Uy{%l^Ek3!mE8JX-3|BV|2STB6O^9!&;}Y z)>5htSCNn6Z1dnJwmxN{-fjgP>pWsqu2AQV+(KFgo5-_M%^9lj#(910Ta24Kh6wH@ zwV7%6)u)SCf3RZ9)mk#=U4!q#CYzLgb5cMkcMPca4ffxn{}~$mk~cQnZGJWJ-h%6m zj74<@Iwbu|DqXCr`_Gzsf1fgJSAHVvsKrG_`jepOgHx!MR&PqFeOJsGpHci-7vvXJ z*Fqr&*p;m95z55aeZYU4psKM?@QXiHa{n#C<;?7XXpZfgD5TsL{@lul7=%h4sDBcB zZU0CKJ2Be-Lysv@K+#`g)*XxClz?Q065y^RUMJ#%)ap}(OLNEH!3dMrIVydi$S&iz z-Mze?Vk0AUNb(;rnxB_-DZ@S@~>-A_yk7#3KJ{%XD=`WCLoW& z^ep}ah&sCcjd|9exjV0~*H}-iphT|rsuf{g%qMmlmR0Z{{?*ANQgc6NPImeasa7aF zPZkmaB`2)e5$Kfs)v-Q%R&$%4Xo%@jrbCBs_O+=eQx@^P!gN!m&(3eCyL)V}CIa=# z825-ir7-x1PG3AgFZxR-o!)zD;>&paSL3m`kNC!gaC^capeq?giMQ2Tvrox<*e@2` zIM}Xh6V(35hFIQ{B6O=n>u6=@z(?=|*Ii>{x|(psZMAutc`!FAzkh>WSF;@~T}6w~ zoy%tLx%>q!8&|s!tm*{2y4BBvT`v#Tf0NN|dRK>E^wdr_L@md^UUHPhpe(PgP*cN| z{Q~u9UEeo_Y}9%}Tynn3GHs4j5Aps7n{fPsmSrG@Gv%$0X=vk~&qB;D``zyx#whjBU^XrrSG=O_m}daQMyp@L`AIy|sWUdaa==%hM10HuLT4Vv{UYa4UxJ zRM*EjP}J@Bc;CTr6}f?j_Fa+4O$0Z z4Q&gpjE?Wvf%OT-zZ@`cy!x-mTNx*vIsTlP_(oX6 z;2(y~Iq~K>_pJW?QXox?eAZ}BY*us6sq45kz94*p~{=*`7luMYuq z)}`L0iZ!b&|GqrOhG4^w%r8A=s5{vW3rp&o*_sUJHzRxIPnq9u6~T4-yc1S=?c2xx zezw7|zsVVfCh^Z#&_Cl!%`IWMvy%w~fj+&X0zTf5Y$oq$C+m7=hIBihhW4LT5Esj$ zDajKvQMG|Klj3jI6Z>E_^6(=**mSEhii$K@$%Rx%+zI7(@ky_j)aj(k<J~iD!Zz>H5jY z`cC$CKDB;s=!~->3IDY`9}Cl5^uf3+q8kw%U=jV#BHSL;MXtFg!TD$=Z7VIN$6iN< zaLcPQ!z{kyqAY#W!n3A8$-2Hx_E1y3@$|ELkc_FJ669-Kvr$R|AN zUk<+Ed!I_OHDQ>k-J8%(;$KhpnKIkX_zL)UjJ@-;u+-YIAPFbyOqhD)-!WHWaKV_b z0XkyiDWgGvi1QDh8#aODm>D9}wD{mfnc`&W}_Cb509`D*+g*3UgyRU_i(NL31Q`vJ7&-aXl_M}|e9R7zN6Ofy5p z5pw%!0p|^rLSL^^!N8$Ax7U616LMTwxCUX8x{uo1m9CxUr+2BL2l;d41p5jhXVq$D z#f@?OJrlf)V}hrN2_-1g;i@6W#VdFe^OLImMCpje=gDGsqXQ#^bt~kOVnSM`g&XzC zX4dI1j1?}=gw6S?<&5U}!>@p(0NI;S@U2fe{mS&&B=B-PFRT?;ImMZ$s zSJRL!j4du|>28^y(}Yz!xwaT4d2g6HbOiL?r^oI1Qg?4qDl|5g5(JZ*ow43=960s= zv3W4OJigX~CzPW>DKV^OXz|R|gub_ez*szS&fU&kcG;!6^UpysMd@v8NvQpKU@>t;%+Z#Y7B3~2wUSVL8VtTLrf%#X3L(X=aSYqh ziLL4$8434GArBxKd|0Dfyq=D1$YUPpHza6wiNBB%Zi<$ltmN#9F%$StRW->y>UaxBFqTu=;poXso+fU-ev-f6f9T08Vh0V^S-rQPSu z?HHN+qtd^&RF`5p(-*<0554Ys)lzN%ZrXsCPWxp>T>QuT>VSL)&Zs$eEbMSg2<|eJ zm`_IP(K$tlbPN-IMw488$0X$4VML^-cu9#nl;mznK_;Nx*Fix}=t&QatlJ)fsqpK<9*{xf2)AAUs z1>5YL=BD^$qtB||6svWm)wXm6*EMxEz?sK*hS#suz;NRGw6XGupO)0xoOAsrWUW^iPb z03P5~YM%)D43>;6fDdSY7uo}k0CuI40t4VR!1)gw@Dui10j9v+KYGAqO)v;r2QjRa z)nN3a00&&~O-T9#;Dm3+2XuiBaSLlFO7H;C6OLJhrh^Gcu=W&C0Qnhmc=rt7E`i#A z>|e|RQ|gMf_~)B>pdf69>zxi24(|^04e;`0CRhv zH3Y9;|1BJ>KYj~Hf!q=_k8bN1U#0MiqaLD~y#dxx4msj0!)OjG-lFvT0Ut5XD>XlZ zZ!_-8=PXcUUWZrR@CcaS8l9FQ3seBle;NUOyMB~Y`Tq7HRQ zy2709TqK#JowmvLce_1}G+_&n20{zLF;$EA>(}08S%MImf$`>W(rDK*@Z+8gj z(|xFwM}XFS`!L}m4(}4Qdudd&s3@oE^J4JJ;pr%VR zS0&IWh-9TXxhZ-W;$mn+J+cM^TN^5?dl+iO^#-h_)ZCZ8l3CiQB~6Q@;$Sbqp5*fR zUS)AV>M@!VWGl>mWffDgJLEOD>4G_Fh1$Y%&<71H7k_f}FZ-^7-{%b3;aahbkGAhM z@}w%}OB+DU_53u}z@Gw)g&+G7Ck32hF3HIW!KI#V)F}iQ(22QV;>_mH!yqN_a&tVg zuPJS?U3Y-x53_Z#63%w+s}`)_tS|Ptg+}|j8n;b&z8)E>iC1oRwJykmr_w2`c~O<_ z)D;6YW{$+RiDDzAmZzK;vE9lP$PpZrZ_HSPr-%ZrZWNt1fStvLvrXBSrhRmAmljIh zff2Vq^x$tQP){Kf`)T3x@p^YsV*$bl9>``YJhzLzpS6K&L+;qeUX@=fzK20ey8r3iws(P)6hRGo_wyQeb8 ztDE75kdGMIgy?<^lNIDn*+K8Mj(4ooYRgxOrbe+7if)ID^ie*hOPBs&x#u%zTUFHK zNsN(gER#zle9<*V4nDw|ts&aI5O1i~8Pzs6Qn?g9I+eI)aXlw)*iuPuu2^Jpb|LrP*(5Q|Y#GAK`hj9I@X_y2K&EFpgTLm&k2;ISx1)d`miXZ;fdlY_+n?Yb zNBQDWR!sPP>eBFj#eW&Vs_dwV_vrp|b%hdE`poR9>sw6pzg1}ltWjrWGhrogAOrCw zwZZz03}(hndW$WtiKZegsb!frgO?4YYQu*r^f^t_lQ!QGm)6yg?^aZ z`MM|=6SiRZvxVQN`$gtV=d5YMW035-(kg#q-OFr>qdpRBPaV1iN^^> z*-ol6ry~OoxP70i%!ZdmFXno@dA|a$!oUOboFU*x)6d8&orhs2H}f(lrnZAd{8LW@ zgeIZL^VtB@uJvb4hTo4^{H%_3eC{d|sEbw=3gj4S$N}9cgHN>~Ko&~gKofzuykp@^ zJjnt|J9$5;$$VhMFA+_}>~lP`*K1h1%3YuK>fEDpj+aIUhGzWX#F=>6(eq4I6pe4u$-W`4~ zmu<@#>;_ez+O?vqhbrdoe2m8YRM2k}V~%g3_??Y1ea9eWlzOzB)WOV{(mz6>zW^JQ zWwb*2P*mVIc3~!@-J9pP@39<}stV<*DaW;m@B|C6Nd)D@QU$x6RXBhx(EhS>eLM)*dZ(B8 zffuchIyCr2Fmqrl1ISo$U-H|e5kOs-a6=mz_8T=1F!?epwtzM05F?#odNF8stE-qW zPR!#?OzNvfe;^LpmIn^l;kplgdL)P7U)qKjJc^ZEIEfEn^aC>l<6~?m*SH1L(mw|| z4DPz8_$r3Tq>qKm7N{8J{YCZ+HQ~YT_8bWPc zL@owU$2fRnfVu=BvX8<2kgKGDSe9F<;J2hO2+v?HW*C~x59WvKnt-h8c-y3X6G;5(c27k4`?IjRQa#vGcHTlERX zo!}5rSAkCWxj~e1HuRz6n+L#$dPmVIXybS&em?l~x8x138arH- zMeJoim&G|DjuPOMYH%zyf>+JhjQP67d9z2c5_h+=P>a^3Bb|6pV#_u+rMy$`MT=IR zqVW!r3=#ZW(U?$C|0#rj*HHhJ>5)aN8He|ATRLNcuPz9x-&R)ZEU^2bNPFm#ML7F1 zx|CNjktyL_oE3#kbcCqoDJy5!ZVjA2kwFZzxNc)SGi|l1Uv*^G@`^+LB-ermp&p-Vj~q?d{{* z;R^35n+c$=u?tdtpyx#!b5H<7-3CJz^>G@{{Rwk!#DwIv8l(5zC40_)2?hvX zBd}+Zqxh5o;-o9N^G^UXX3(qB)8V^-D0gdL#7~t(!vf@6!BYk5@`7^lIZ80I3y;#_&to>x%1??QM2j8-;yjo;sg0}UcO&nC$H?^qiwTPx6Nx^@im#)Nz z`qB}S&X7wcb2?Q_Up_9VA;GK2pY8B;+HJ<0x-ZCTaND$*N-a3rTeClOtz{ut=2nrE z0{nx&M{{Ui!&*0CkF|bn<_wxYj(1e6)t0E(x2P@mqmR_P*yj`SgP4qAyM&g4htUw_ zA~!5^zKFA&W`dfh^mq>zu`YE|-+k;!U^;J_x~>4eO_TK}@un=mt*j2qKwsZt1@pCF zEfqn`y`9ftzh&K8Qp0kPV(Ug!wy55Ea7OUsXRyDofzytHeolcpqrCAHyWP+q?G8L~ z(eB;P&9=H)BE2y*gW`zY;!Lf~#hbijh-$EWUf{R#BloIke=lN;jv+=VJ7VvxAsFyj0|~JdI1MM@T*CXd zmqD;gaIilIsjew)d8rU&@^3I~s(3>d@zE%9cS$c|7vmJyHklSrcPa#`iD9M>a&id_ zk01CrxE8J(<_T}7UVL~YfVp^K1UKVm=6I^eWiO5{EE@GF3YloeI6$RRtU?!FPd{s4 zd{AwSLuD2V>uf90!??pi2!jsStCTK51bD~fu(mhxD=9fjOp7pysgh=2kjPTTV*y z(6Lah{Of%3+2)kKd#mn)$DmO)@M9TGH(!v%V^sQoikH7bF`o59m>MIe$_*e%2{a+n zQ?XJIsdSqi_6vamaf{o4)J|su+9b#NL4OwS#jU}oiZ5hm6tIz(B;ebGVuKI7`Ja|3 zYqU=k7{|$uJl?!5@>HXXoxcu+qe6BiRgI}23LJkS2=9zrb-*P|n_;FQ!$SrJa8xYJK&- zc|{KVlr#+1lEyu|tFCb*Ku~|0(d#8}k&T!;P5wVEfP3UeDs?AQ<_7l4bH#|IoEl+) z@Qcu|kXD@bA6)MOKKdTPWKH0gwuiMJO=vaLMrL{x?Gk<)X5$Vj!iNt8)81*Gm>=wA zskVK;yyK~Obkt=?WZ3zSU_h%im;l9<*E88Eeb)(_-n^#e6gC z)CzL@R-~Yu|7Z|jB!>R#NNyw3wH1}I_LBNdsx|4YZcfL)C<&ix`0lX18<)_TGdY>j z3u1yYq(5y7IB9abQNkpCKo#Rp@Law<8OkOGydvD6a9SXF#U?{b#E!zzJmT3k0$gqA z)U7hDb%2#oD%nI|GMj!^T~C1J?Y|y0=wUN6r-EYo)hj0PYg*hZ?%c&)`YRPRq4Hk0 ztfkh44z)O>!m1e4Ri~gz<0`wxdXH1cR?ck2w8=2h8vJVdWb6ukYYskJ7v0hf+uqX= zKD(1hC5n1k`6O+xniQ~KN=~=y@Gei;h&Zf+Cexy+is`V%3Zi|R>(aOq<2BGT8EvfAcaI&T`lty9P>R2FW|Moe%_gsy(Xd^M2y;i$uj& zTcA7A&J6#`8r#4_*tlW&eOk$3s6FcyJts7-f?R`k&g zbgK6!E@WG0CrHYdvJbpqJ~E6ON(6$qKsc=P>3R>`W+}(HOLhg~iT$+1%hbLs>#|lc z(1IRwC3y5@$I_T$$pBlHLoF~w-N0x)o9xU_1tr_?L4w?RUt4qsvbPIwQ+gN(x4=Bq zjepSvd*3d+Qz<%#^Ry!o)tQ`6q6|^gV4_q}e*BZ)>>lXpnegxiF9%_l^d$xyz~K#F zI`)tZdw8;Z`&8QCN#^~cr4P3LI{sA@`xY^Hx;e(gRK@tO15_?cM~5|KTVOS*7V_u! zr#MB}ZF2XfqVxX#mn+)2;@M1fUwV=46O>=?jCl{YG#QyCSAwLP)WgT^H8BI@oAl+4 zoBx_%xBq6OJgWk-7EQTf`KBejLR#-OTeVOL(GbSouAi@@EXB6Vdy-T7cMMb~A(dmzs!lljJA$KS;j8OY@iuFtz8Vitd z!G%ZcN)iEns?HOM!rj<9Ot)&yD%L$;LzFv~>3L(kLgNnt(hDuctH3fwzb+s?_E#h_ zQH(46^r;(LoYh_+)E}?`vNBG3Sj7Nw6W-s{PZID2RQZYAm!YgP4Ia;XO{iX(hXEiz zgr{QV?$cBWcmGfh2+Kg`>q{9&Kz0xHkZkh`s!FPGBN>VS_SxY--n;FJqx!L<`yKu&kn!LR5y^e^M!#$56+T-`9jQVKwwX7L5&;CwxH!>>5tMfKPHn zP8sfiTG;j5_p&LSzn0%j8aA1x$<;Jg4``#@;fCgc@In)nU6Q~3*saSOQhyMsf6PygNXuiR@{iayA(S#F@x88q57yU>`1g`>aD<=YQFa0&=xDg21(omF&rsCuhc@% z>>uccoVTzyc?z1dLFj>bouOjTqO9uN=LkWGA-ijapvj{$<`v~5Asu;%!*9vbAS3l>+8Tzfwn*u-96BAkTd!u7pmSX~Ac1jm9gF*fvi%CaP!76nK3GtDWHFKc;R=_7L@#i{$I6w)QXerW?Q4PX67ln$(dsjCCs{hgQa~|RHb!Aj_ z{TR3z{NeBbSe3Ukphpn}>s&H)pog}}ixb3&1>X}2s0H1e$8oQ=r0h!dTMA@DiIEC;TL+Sh9Sk9TPUEO3T%VTjp6I_EwxOsxRn8d&j z+_!&2C+$UqKs7i*w9JG#ngnLtp^|gGRWDg@2tu2Z4NGpg0n~KOIL-YP;{L#Q{Mr4i zhuU9kfo@zg`Q#Wc_lj4j_0nE1pNS47nw7Ub0Zh^yFM7t7I?_<78UM)Oy}mbbX(yxq z)rV_3Xnr7hREmk3zt&85Zyw^JWzwPHv}o4QGooaY&?Y zjH?k}EQ!*!QC|9gMo`K;Bay=PSH|3#u%7JSqs-tpdsyYPa3gnC0rT50IPFDe<8DVj zK0;s=sT>J2?JN@4w;ms3Z$eCs?pDOpY~@${*kRhl!YLAhdyn~K!()y3yK~|CEO^Z9 zPYjgUoTKeJmz(s0SbG%q%gPmj9=aJVpR$?o@{}5Vg~>JFdIZ93lY`=sxAvOvS+P?M z<^527)yx-Kzs`T%-QT`+Dvf1y6bS%BarSPn38lzdV zo_L|xf74J3^DZ+AIAHH_D<%8{5C>3I@)ui46!%Kn;VAMbev;O8d}UP(3mY0Wa}vE7 zIbiB@zPQ0ZTK#LM%@q~e@6^iCt-5y;B)OV{+3{t%T8gDY-Wn%9pv_894VgFaKnYSL zN;SacooV_0;seJktZ?KQ6UWzNtQBlN5|w?jN2RCRkUr5QwzF4Ek4lxls?y_M_(EnZ5N^i!cU&4rfnXtB=N_;S7KKZAWkiIJ5!K37OE?U?$f;P0ns@k zqt!nf3A`Tbe$2?v0eMOvu+78~2p}YF`Xw-Ku@GG^6iNs5qMU0~%~JN815+A`Dv-c^ zgKzSOuRz;~=Ds|hyW5ASJ;h2Ex)@hqz*3^71VjAZpDGl61~E%IlYM76ex>U5gSvkd zoTRQ8{PRC1_<{v2^R4glMX~A+R5vPM_w~f51vF!LDVNvm*h$e9R-o)b90tDH(HBJN zi1kl}+H}J|mMQ8}u+WJcjkFQ4@8@3_4(=UW$u7~19pQf3jiA2LD7=`?)(Knfpvvgi zXUeyU(tR<0TccAHTy4(u_j!h~J9}W?n+lWoD-I1|es0zhA$3!WN`jQ9t=ZW)Jw5p0!?oqU$34u91= zKksT0;M!rB5nLMPCjJAH8K1{e9l{y#rdOT;ds53k|qtz zAUg$#3nX6N{T2ltMHg99-4Yr&9D>EWQMbLyacbz?76>}?yN$zB)Ih(h)2ciuO3*rQ zmh$OLAKNvLUJYjaq4&j?*V$s-o7a+3AyS=RFRUk%vF#d&SH-I)@p}7*eNeL@XW}KR zurb9-5TN8?>=}nd1rz|g&*$SrU(se>>)%4(DUpqJb@#i8lyTh@89MidVD(3;#t&iU zAEC6r|Lv;l)}eVMoGFk9oHvf@{UIn+>ZSgI{5Gl7=AY)W`q1Ib*muO*~wdOi!* zY!tkNy*q*p71KnLmfc`&U2InRsye* zr^x^xCFXSlh$9=UNh@oaJB57-_&{G;(*&W{86YAD?p77ReBW%Q9bjQBl&fSMe_cR@ zFPhsNgI3kUgCOp%0qoL&vH!&Qgxh@PA~YI`i^)&8UiU~Z%RnK3e0=ou{u;c6vdxlL zjn*k~HAEo@N1`vkknz2D;=rrSnNhSSn%a#wk|84G0!iQO( zg>)Sl+W&4_$KM(MPB7vHYvCX$#8azlxP1e!bKQ&czuSvd5u?pnlzu3gsWafF+uAwL z4i|>}In*PA^&`NpK9x&#4;NX%%#wE zd`X{%^|SfY=}`&+HOPsfLz`mK=-xN%pGj97Q1>ET=*378*+>e(8$TKgo={h#5YMax zm0frELXtjdboxUIk_4DjLJCKp&i&UuxVr^>z;xyL@qmO{bCCudF%+nB6ij)z3%`tV z<}9=6uV1C;e#YiR&ZCI4&yDHAoO%UPktpUiS0HZKV7ispA|Q;G@?&UoVQzQYmII#A zNmDb((_4k&rk8jgz!oY5ET}v_{sb4)H@)VBXC&vMyKc0;fjN;W81iczA0x`Ky$(um zIR2J}w1%G2mEy@>;?;)p*N}B|#sl#g)HYllkyOJ*drnN8IjO6yFZwl8e*7qfI3722 z1V1>W)MdbFn={T`l=Jp4gC!fzN(V*l%H?^@gqu+rC6k;AC#GX7)o#AmQ{g(-dYQ?2 z@A;|}p7__+uZ(0h3%M|UbY(4gK%bI;tqz?51PU%+&Rfri>yba4VX{%zYVZZM9WjT9 zNfq)yL^)e?QA2mAOXcU6i~ZPD^7tEA8!nDL{Gwm^xAq>Lop@u~nn<^77&$cX4rO_= zCVsfN2(*1ab_8f-FBwvhRO8?5vMn&|z*~z=$u~HZ4yguVMsCvOK^ZYNGm6cTjWiSG z*idWvkJbb^Ib2ccL~=3`8eDrQj!zY&Vx!lYFj%>G{$|42w7j z#-g{Hxs9<^0t@=o%zANG?TtT=7d*jefYsmCGj2^d^_rB5D2MiqXW_}mCZv$;LMJH} zh0-C)aY~N1q~Yc+*gX9pS-r`e9IEKzxMy=Onvk9TE|9tNd%59QIhXKz>`c--5POT* z%ZWiFVqpjKhPRp}khxQ*L#G7&nW?Omnbxi^-25zT3fa|4H4-rq>0D7d>5dK%>`mn; z+Mtv&8_tejhH;8>VkfCb9G}J8Y5l6P?jF7e7h2F#3k^NsEs^Ui11DjIOh@OrX&(iQ^#>^8> zwb9R5YEiWf*p3v&t4^pO>-Z#G?~}_t<`&$cy96Uu(0v-8lBPOTESAHc%4#7pCM?1td~4GlSo83lXW$=sIZ>zn71%iLc_Qd{ zzV!ij#kC(B&J>tu2;b(R5Sy+`D;|chW|E`6=*nktKRH`N)ff_dPX=r z594@F*i}@1E!7FzS>!*IxO7cI3eGVskHw_s5x&t0cv$Vg%v*+Y1NovCp|r*!S12`u zO1!FPZCU8Qs_<)$O?1Y(bIsoSO}}}bO;#S^G_RX-;BlgHS7kD(lSATUao;eU*CuVz z{reRfz}4WXhbMHcad8qd(xb9@#r6y56tRSP@3$cmWrcF0!GV~Xf%kcmDw|MR!8|<| zSV|12OeoIV-iHT!W#sd`-p(62W;3p-U0p3GfWOXlZHiKaJsaoOvK?QdV8a5bsxF$@ zK3h53M?56IdQoTNVqUj2g{;Km(&OnRo)dVW`la?^D>M2C(NAK8wtX|fdYp`zZshxm*^0IsD4=i9dq`ICjz)W4=o|Ei!CBk5s7V~!A zl<}zlm#B7IS(a;8vG><~DG`Yd?qk&-v>S--{S}8Ce_<&18u1lk!mS`98)U$ASwhyk zS1mj{;+r1b$}Erf0kU_eKNUd*iWCcD#`j)T30c{mtr(}81CE$ zE$@v;LJ^fL4y*@t%AW56n`63qnOQ`;TJ@)Xmgp@2!9VoiomvmdMcw$lz@eKo!@;pz zgvA- z?a}+ zBdP4MjGS60^TB81-!`prT?iV_uPj@?D++KErmvn4%M}*f4=ox?jhx9|u5q8Nh?x9# z;5hCcKe#I(n`cq*VEdO4qkSgYfB%;3qm?qbq0(g_jICpsj+Cf~Zkw5vi^Cu9Rv<(Y zMxJ6sJV(dFdaoz_$o${odzf-EjV_q{2QPRlKC7IML{t!q$tU5ym;`K_B9UoTg2J{+ z45PVlJ9zY)$@#6AwqA<^-lU-yaOietHSac`AZ*s3$HW4 z*sg|1?q~~G2W?}+7dlZ`wv9nSC?BzpuYiB%?w8XmV{72yZDzy@ZKrC8vs6b_llD+0bKCjmUqFB+VZiad0+9F5Qha8D3n2E; zM>_rubYqf~ewX?-1rW_vY1E54<7Hckw=%@OapwAKn*X!C2Qr0SUlxr!>%&5PS_@vX zxzBY<`%t0bFIqAV6(;zUD|jSPY&3wkTJ9KuEe(?>ok7unwp!HF%tV4Q<(+uKtxQ79 zc@etktzW$>g`~->E|hE7?(@6L6O2NakYVO3_`hE<->W9Fy@^2T630HeXY|lwuM~!$ zE39J*eP}!ifE^$W1yjO3c*FOabhLbZcuSwxv&Gk#4GF3)h+oWn+u11i_nDql?8ein z3oBO$^SUaS&&p>WOE{&wPu<1(+u8{w|3xOWb$ot(4e>|imvo^9vR5_I|#F-+tgwd#xFJa~UN-Q5CKQb@MBg}5?3>k#Kv%M=jrV?HefRg}#?*AUx#wu9}Lkud{S4U510dQa>E2i4WP{!xh|?b^-k5n(uF z{4bunizJRr6&2YG^O4nbU@Q{o3sh)iLU!CWl(bH#wTFyfl#Zfh*|KSn>sh^(KcT%E ztnaDlGOr|+PVLSV|NB)4!Rq6~EEc1J^nE|>sdXrQxByK!Be{CEXK$^lFRkJ<*M5W6 za*(pltTwl3B(i_R^7B6x0CV4sP!Duimu5f*%sKIgl)Dt{k?4W%$^|Ewcv-o$7dD^x-EFE?eh+JWJSCYZi9kS##u%z@9XEqNhIjZz{k|`R?$>T1KiN!T$Jv*Z?2E~FEX8#ox~P-V zFUY>#qj%*~X6w$g8=4jpsd}K*07p&11gC!l9 zgQTr@h0~V{s%0DLCzw0l#}jIFKnyihJWP47xRL}Cl`55vtPH9hS|C~8O`=QHm+ebQ z(H+TJS5{w_xyEjTIM$g)-*Q@T&69`~*(EO$Tq;e>GB=}|}*Ad(lgD~Lrpy6B(uD!x4!ZdVd4(Y;Y1X- z@+)aon)`v;blEyQ-U(l4$h0z%|4DFLp(1!Z>-F=Xy`%#wcQ(0_A2{yp6pclG&l5@3 z_|$7`dWs{;*a3zPc-AYyu-Jbpmf0XBNJ##_>}yG{GUdw#37-8Mntl?e86}o`wJl+<4)vQjHcC&Dr$yh6l$h!F z01Ua{8GCkD0kZGuZT}|-68G)mm6`+KFs0NF1$G!40O%yR=>=UHxdVV!0Ga}*21;+< z000rsNklrr4JHrE#c1Z~{90y<0MMYsNp zAG5y3M=?Fu&b))-qd_Hkh9ZgcMbzaUKrh2xgX0y@uo=Fi_~sY6LT@ z81i(k={tqS$K=@~{7E(!cxfINc4?l7u@`j>_)HuJBh0ZbSY~x1gI5Kulo+kjJk5Oo zbDzbMyK_xlUnyTb&=%y&11Day7c=l8R+2o$9Pff-Rwprd)07n?56$D;OT;stPpHYN z8Z3*Pn8yYFBaa(gM)LX$Zlp?*rYT)U!_~Q1?AFY0YwWusK(K>I zF3NyuonVfAY?;+C2CsDSKFQ`0Si=d-e~FP47kB_%&#B z_i%IkW5=xa$RM!z1&L$J_Zk{Azd3_R_&SKQ!MFaXl&S?VI*&N70PwGT$M-0urT`cM zU}AvB>j8L*!Kpvtb93XI7mVtWRJ<0+8!OWF1@OG59+b){*R(>&3sG=S5GBf`~1+XhQPs z41!{N8nzHwX4Nd9XFNgOIlVL@>nMr9nN7m$`9Q!C5is2SJwk-tFav@ph(@3V$gIi( z#wsP5Rau1Aa#RGu7?QX-A|D8-$K3ZUw8}&386l#mPx7^(DGE`M7!CZtL~?Jyy?iiY z%&J8M&T0~VW4`(!NFZgt;k}3vM}rI`t^Y+ug)j_@=LVEWo{WmkO4gyMJ4ZzYuFoej zx-$C(-n@vIr$>y~8<89va4lCEc9Hk}8xoMNUuML*DUsYa0{Mce;XGaQjgS{2fQY3e zJ4cLAnvnb=U|{LthHd2a5)M+Ra2#`3bkB#wU}l7*s~4hxls)ns`JRL~qoz)gY{Ouc z9#)Xt*07Jf*dT+H3jUE<=n)u#D)?}Om{kp<&|yl9lTpY^N{~D!qBr)OfQO)|VIw&` z;QtA*PxBbTGS4Iri-;hRgz?OFqO%X=9K~p~KVL<%R|JWtFN5{|d&U^ybGAn#0vOp> zliVeOYw5u7>hFgoOdJ&@1l1+^b3~By70LP$B%=KT7;+;iH5MSV>Ji{Vsb&NVGg6nQ zeuvS5pP7SJWquKLYJ?DUDqp2VQBCsTh}uc+324u1Y}iXy_TXmYq0gXq6%=oFGIBDj`z5sGF{g)^f}08x^%C~;6xv}d)Y|C&TKQIr~E z%&eM_oaS>6){yKQ(M$0C2r{eaN>?e!J2_%H^!<@>r~VO7yU>4$o_Qo&8+H?lYVJ2r zFu@4c|RvOI#g@>^T1isWvF z4SB|ISpZMkHzX?}Xd`}=(S=9jOeAedE{PygA7Jz;qWcL>lJ8Do# z@|cL>s)FQqKG$)FVN167>_lC5$SYKn91=lwwkpERY6Hpsg+@5HMiklWNgfz6TKA&rN-ms@X;c#1@ z;UddPc8JjYhYD>*8K#hIRHy_LBs)G=u!`j12;r&~$pt>u+X046{lQ56yiXdqG(z*= zi@Ks|l%bLl%peJSBW{oI2CR%2(>lYaSLh8!FyAdtxZBaET0J!?F;~K@Rx>Xo9!_!?qnXTX<>cA1*of+)h;Pa|MxZK!*ka!BDP;{P za)$M^PkarDz?_eYfj2M-7ZoE>)nT-AqDY;{c}heiQa=*Ii$%<=w)7eE_ijEVQn`>$ zWAq9I*ukreU_JX>ZB`qSvm?Gi)g&*;r!iZI+E3lAjU=}&QkuHmr*yS@9_Ly@^74pB zu&2Uol5L9JI68>Y>-3QhwG#MRmPb5o=kpf6w+Lye8U6Ja%$sy28pd7p-}WtEu(9An z7<_Er1&nr5XA(w{Y>-DhJ?AqBvSpFc(O-PvCy}oZ7V(siW_%z)@&d!DHg905PeW-w z;bZ2hga*kSL`C%q7Zec7Pg2|qJOZOlnFCArwg z+)gfFI#DFY`oP7LIht30lG9?ytP&)zFdWGLhL_NPJMnzO8j*fVa)2K{^|JCB&7EaD zbnux4FeuCODJ6Lpnv*J}ssNk;;Jp}~AqC(>0ADe?9fKzU_|fBU>jM~8 zp!b@e1oan$rn14T1_Kxm;8>x7xWRh>&d3&iih^MY(^?PUXaJwai~xE7_<*`&OGOs& zQVZZ&ACDYbXlkM4we9Tj=hXmS%)%V%le`(g`vA6#DJnk&aI~B?10fMcix>r#ro;Gb z804ud6IA)IuQe_(nmF3W9!w0IVT)S+9pepO$H|Kv8{?W%YB_*I;5`N@O0YA4<> z)(dgm**@^m5I~_NukP>Tx34Ot7@|~l8+1%uzZm0h0)Rv0Y#D}?1*WxFY%y_US`7f) zD2!{DknoWJ@VeKZ0~lIB(p9++2J8nQLwM3ZK z_?Qw(KLDSQyeA}4{2c(b9)G)c0ok!S1L)@Qx4!|HoFTJn1K=S5V*&JzIqt>_vsx0j ziN`YDLJ)l*fKOsdFm(Z30N{I)CuW%1Ga0}uoMY#Ej?trgHVt#@NgJRJc%1>i%HI|q?|PhkeXvf8c(4i-V0&4-Bq?Ec%!e0!- ziYViH9y*|Ket|5hC4j4iaotO@yNz*vqK`4RjX6%X@$uX5EhO<>NWK7_k9iJsP;DW> zd;t5)Stuwd$TAX2sV||uNM9GwvYG-oAHdJhl}`6evrGKwW30IMEbQU&ck7|E0YI`5 z$rGWyeqR7M3P8OAB>%4g43slaa8R&yZ4!kBAuLjV86k6X`XumMeH3}T4+Y_A!;FbctQ30S5z*GR+ z0r27xdtX!r#&ACy0GJDH<8y;rImm_HIcO7rzX7;k?3o1%1<$Zj2z@YsCjoSUfhhob z$I%!|vZF{#6=oX&0CNDGqLlii7#3SJrBG!ec{@nzKnK)KmMKxlc_VqsQA zC1Iu9&~*U(6~OgHd`lC+Oz8Z$AD|8SGoTIBRZ&qZBz~wII?=hOsJy!aIH1TG)kf%$ zm%Eix)y1@MRjfNsasYrQ0Q52!H2__LWfQc0XpS(ixzH~1`2ZFISP5+}s{&A+K|*mE zbP{o0VMt8@v;)vyK3#-Kb(Me10hm_lo7MS|N~v#(aq+5{cb;Su05=0T3x#i(4FhTc ztbuN_xdA$CV4d9f8-Gy8f#Et7?vB;Y6Rdi0GB}9S_~M-6>I=-2Y}m@ zQfm!548!unxV8mw7l6Hu`fMP}kO1%@fQyt;KO1ydgE@pw+*}jR0RTn-_{CtvK$hSa zp|JxDvoaD^m}=sMq9!&q7%>n6tP=I_9;5ylhLtVG)gHP&)yXK_WNHK1g-y_v6>d~Y zEimXX3@aCmYdZk{0B{82-}=cwZlD^#7yvgZr6wD67>1Q=rlp`8qh15u+|{Iy7_fp` z0B-}h0l)+kPh}WZ6d2b|0Iq^I(i@4(KvJT52Ri-hTf?Xf!-^vF>JQ*z0EffaxQZ1v z0eA(#ol2?i3_1+MiUZ@?3c4NriO@Bi3=}@B2Jk-s_bR1k8gv+j6-UO^62M64iV)ol z&I@mg0sIPGN#O~l)G~t-!>|g0an*xvKXxW`X zl?Ek-VHG>`Y6;*#0LKE@1ExwuzF`A^&j36J;4NdDG7QUry~@}YIuLa@fWd}&<&;T% z0^nunK+i=+Nz}Ya z06rHl(OCr$+LEr9U=z6Y@0Fet;Y z48((pwTJGS+($m!0q6*zb&U6)0G0rl2VgRQALTP0zyiaZ48t-|c#>Fi=m6Fp&>fb$ zLic5C51<8fshpBL&YwAz<_}e;r{>_l` + + + + diff --git a/dragonpilot/selfdrive/assets/images/spinner_comma.png b/dragonpilot/selfdrive/assets/images/spinner_comma.png new file mode 100644 index 0000000000000000000000000000000000000000..aa90a78c471f413653616f97d72a019f459dc173 GIT binary patch literal 88317 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 C?W-mL literal 0 HcmV?d00001 diff --git a/dragonpilot/selfdrive/controls/.gitignore b/dragonpilot/selfdrive/controls/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/dragonpilot/selfdrive/controls/lib/.gitignore b/dragonpilot/selfdrive/controls/lib/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/dragonpilot/selfdrive/ui/.gitignore b/dragonpilot/selfdrive/ui/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py b/dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py new file mode 100644 index 0000000..5e4802f --- /dev/null +++ b/dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py @@ -0,0 +1,87 @@ +import os +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.common.params import Params, UnknownKeyName +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item, simple_item, button_item, spin_button_item, double_spin_button_item, text_spin_button_item +from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog + + +class DragonpilotLayout(Widget): + def __init__(self): + super().__init__() + self._params = Params() + self._scroller: Scroller | None = None + self._has_long_ctrl = False + self._has_radar_unavailable = False + + self._toggles = {} + + if ui_state.CP is not None: + self._has_long_ctrl = ui_state.CP.openpilotLongitudinalControl + self._has_radar_unavailable = ui_state.CP.radarUnavailable + match ui_state.CP.brand: + case "toyota": + self._toyota_toggles() + case "volkswagen": + self._vag_toggles() + case "mazda": + self._mazda_toggles() + + self._lat_toggles() + self._lon_toggles() + self._ui_toggles() + self._device_toggles() + + self._reset_dp_conf_btn = button_item( + lambda: tr("Reset DP Settings"), + lambda: tr("RESET"), + lambda: tr("Reset dragonpilot settings to default and restart the device."), + callback=self._reset_dp_conf) + + self._toggles['btn_reset_dp_conf'] = self._reset_dp_conf_btn + + self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0) + + def _toyota_toggles(self): + self._toggles["title_toyota"] = simple_item(title=lambda: tr("### Toyota / Lexus ###")) + + def _vag_toggles(self): + self._toggles["title_vag"] = simple_item(title=lambda: tr("### VAG ###")) + + def _mazda_toggles(self): + self._toggles["title_mazda"] = simple_item(title=lambda: tr("### Mazda ###")) + + def _lat_toggles(self): + self._toggles["title_lat"] = simple_item(title=lambda: tr("### Lateral ###")) + + def _lon_toggles(self): + self._toggles["title_lon"] = simple_item(title=lambda: tr("### Longitudinal ###")) + + def _ui_toggles(self): + self._toggles["title_ui"] = simple_item(title=lambda: tr("### UI ###")) + + def _device_toggles(self): + self._toggles["title_dev"] = simple_item(title=lambda: tr("### Device ###")) + + def _reset_dp_conf(self): + def reset_dp_conf(result: int): + # Check engaged again in case it changed while the dialog was open + if result != DialogResult.CONFIRM: + return + self._params.put_bool_nonblocking("dp_dev_reset_conf", True) + + dialog = ConfirmDialog(tr("Are you sure you want to reset ALL DP SETTINGS to default?"), tr("Reset")) + gui_app.set_modal_overlay(dialog, callback=reset_dp_conf) + + def show_event(self): + self._scroller.show_event() + self._update_toggles() + + def _update_toggles(self): + ui_state.update_params() + + def _render(self, rect): + self._scroller.render(rect) diff --git a/opendbc_repo/opendbc/car/body/interface.py b/opendbc_repo/opendbc/car/body/interface.py index 24e571e..daa9dc0 100644 --- a/opendbc_repo/opendbc/car/body/interface.py +++ b/opendbc_repo/opendbc/car/body/interface.py @@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.notCar = True ret.brand = "body" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.body)] diff --git a/opendbc_repo/opendbc/car/car_helpers.py b/opendbc_repo/opendbc/car/car_helpers.py index dfda015..eaa6a2f 100644 --- a/opendbc_repo/opendbc/car/car_helpers.py +++ b/opendbc_repo/opendbc/car/car_helpers.py @@ -149,7 +149,7 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, alpha_long_allowed: bool, - is_release: bool, num_pandas: int = 1, cached_params: CarParamsT | None = None): + is_release: bool, num_pandas: int = 1, dp_params: int = 0, cached_params: CarParamsT | None = None): candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params) if candidate is None: @@ -157,7 +157,7 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip candidate = "MOCK" CarInterface = interfaces[candidate] - CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, docs=False) + CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, dp_params, docs=False) CP.carVin = vin CP.carFw = car_fw CP.fingerprintSource = source diff --git a/opendbc_repo/opendbc/car/chrysler/interface.py b/opendbc_repo/opendbc/car/chrysler/interface.py index 0be2cca..d69f192 100755 --- a/opendbc_repo/opendbc/car/chrysler/interface.py +++ b/opendbc_repo/opendbc/car/chrysler/interface.py @@ -13,7 +13,7 @@ class CarInterface(CarInterfaceBase): RadarInterface = RadarInterface @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "chrysler" ret.dashcamOnly = candidate in RAM_HD diff --git a/opendbc_repo/opendbc/car/docs.py b/opendbc_repo/opendbc/car/docs.py index 901b889..a66a081 100755 --- a/opendbc_repo/opendbc/car/docs.py +++ b/opendbc_repo/opendbc/car/docs.py @@ -32,7 +32,7 @@ def get_params_for_docs(platform) -> CarParams: cp_platform = platform if platform in interfaces else MOCK.MOCK CP: CarParams = interfaces[cp_platform].get_params(cp_platform, fingerprint=gen_empty_fingerprint(), car_fw=[CarParams.CarFw(ecu=CarParams.Ecu.unknown)], - alpha_long=True, is_release=False, docs=True) + alpha_long=True, is_release=False, dp_params=0, docs=True) return CP diff --git a/opendbc_repo/opendbc/car/ford/interface.py b/opendbc_repo/opendbc/car/ford/interface.py index 97685df..9d3872e 100644 --- a/opendbc_repo/opendbc/car/ford/interface.py +++ b/opendbc_repo/opendbc/car/ford/interface.py @@ -26,7 +26,7 @@ class CarInterface(CarInterfaceBase): return CarControllerParams.ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS) @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "ford" ret.radarUnavailable = Bus.radar not in DBC[candidate] diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index 38adf80..e292d9d 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -82,7 +82,7 @@ class CarInterface(CarInterfaceBase): return self.lateral_accel_from_torque_linear @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "gm" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)] ret.autoResumeSng = False diff --git a/opendbc_repo/opendbc/car/honda/interface.py b/opendbc_repo/opendbc/car/honda/interface.py index 4b1c680..cde21d3 100755 --- a/opendbc_repo/opendbc/car/honda/interface.py +++ b/opendbc_repo/opendbc/car/honda/interface.py @@ -31,7 +31,7 @@ class CarInterface(CarInterfaceBase): return CarControllerParams.NIDEC_ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS) @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "honda" CAN = CanBus(ret, fingerprint) diff --git a/opendbc_repo/opendbc/car/hyundai/interface.py b/opendbc_repo/opendbc/car/hyundai/interface.py index 1e058c4..3744450 100644 --- a/opendbc_repo/opendbc/car/hyundai/interface.py +++ b/opendbc_repo/opendbc/car/hyundai/interface.py @@ -23,7 +23,7 @@ class CarInterface(CarInterfaceBase): RadarInterface = RadarInterface @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "hyundai" # "LKA steering" if LKAS or LKAS_ALT messages are seen coming from the camera. diff --git a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py index b798aca..4f3a20c 100644 --- a/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py +++ b/opendbc_repo/opendbc/car/hyundai/tests/test_hyundai.py @@ -51,7 +51,7 @@ class TestHyundaiFingerprint: if lka_steering: cam_can = CanBus(None, fingerprint).CAM fingerprint[cam_can] = [0x50, 0x110] # LKA steering messages - CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], False, False, False) + CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], False, False, 0, False) assert bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING) == lka_steering # radar available @@ -59,14 +59,14 @@ class TestHyundaiFingerprint: fingerprint = gen_empty_fingerprint() if radar: fingerprint[1][RADAR_START_ADDR] = 8 - CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False) + CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, 0, False) assert CP.radarUnavailable != radar def test_alternate_limits(self): # Alternate lateral control limits, for high torque cars, verify Panda safety mode flag is set fingerprint = gen_empty_fingerprint() for car_model in CAR: - CP = CarInterface.get_params(car_model, fingerprint, [], False, False, False) + CP = CarInterface.get_params(car_model, fingerprint, [], False, False, 0, False) assert bool(CP.flags & HyundaiFlags.ALT_LIMITS) == bool(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.ALT_LIMITS) def test_can_features(self): diff --git a/opendbc_repo/opendbc/car/interfaces.py b/opendbc_repo/opendbc/car/interfaces.py index af93dea..0848b35 100644 --- a/opendbc_repo/opendbc/car/interfaces.py +++ b/opendbc_repo/opendbc/car/interfaces.py @@ -122,11 +122,11 @@ class CarInterfaceBase(ABC): """ Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints. """ - return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False, False) + return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False, 0, False) @classmethod def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw], - alpha_long: bool, is_release: bool, docs: bool) -> structs.CarParams: + alpha_long: bool, is_release: bool, dp_params: int, docs: bool) -> structs.CarParams: ret = CarInterfaceBase.get_std_params(candidate) platform = PLATFORMS[candidate] @@ -139,7 +139,7 @@ class CarInterfaceBase(ABC): ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor ret.flags |= int(platform.config.flags) - ret = cls._get_params(ret, candidate, fingerprint, car_fw, alpha_long, is_release, docs) + ret = cls._get_params(ret, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) # Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload if not ret.notCar: @@ -154,7 +154,7 @@ class CarInterfaceBase(ABC): @staticmethod @abstractmethod def _get_params(ret: structs.CarParams, candidate, fingerprint: dict[int, dict[int, int]], - car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release: bool, docs: bool) -> structs.CarParams: + car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release: bool, dp_params: int, docs: bool) -> structs.CarParams: raise NotImplementedError @staticmethod diff --git a/opendbc_repo/opendbc/car/mazda/interface.py b/opendbc_repo/opendbc/car/mazda/interface.py index 814846e..1381e47 100755 --- a/opendbc_repo/opendbc/car/mazda/interface.py +++ b/opendbc_repo/opendbc/car/mazda/interface.py @@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "mazda" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)] ret.radarUnavailable = True diff --git a/opendbc_repo/opendbc/car/mock/interface.py b/opendbc_repo/opendbc/car/mock/interface.py index 3bcc2f8..3b6d501 100755 --- a/opendbc_repo/opendbc/car/mock/interface.py +++ b/opendbc_repo/opendbc/car/mock/interface.py @@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "mock" ret.mass = 1700. ret.wheelbase = 2.70 diff --git a/opendbc_repo/opendbc/car/nissan/interface.py b/opendbc_repo/opendbc/car/nissan/interface.py index bac7b3f..f4ca54f 100644 --- a/opendbc_repo/opendbc/car/nissan/interface.py +++ b/opendbc_repo/opendbc/car/nissan/interface.py @@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "nissan" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.nissan)] ret.autoResumeSng = False diff --git a/opendbc_repo/opendbc/car/psa/interface.py b/opendbc_repo/opendbc/car/psa/interface.py index f719a3d..d644b34 100644 --- a/opendbc_repo/opendbc/car/psa/interface.py +++ b/opendbc_repo/opendbc/car/psa/interface.py @@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = 'psa' ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.psa)] diff --git a/opendbc_repo/opendbc/car/rivian/interface.py b/opendbc_repo/opendbc/car/rivian/interface.py index f1108e0..2731627 100644 --- a/opendbc_repo/opendbc/car/rivian/interface.py +++ b/opendbc_repo/opendbc/car/rivian/interface.py @@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase): RadarInterface = RadarInterface @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "rivian" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.rivian)] diff --git a/opendbc_repo/opendbc/car/structs.py b/opendbc_repo/opendbc/car/structs.py index a5628c7..0959d5c 100644 --- a/opendbc_repo/opendbc/car/structs.py +++ b/opendbc_repo/opendbc/car/structs.py @@ -18,3 +18,6 @@ CarStateT = capnp.lib.capnp._StructModule RadarDataT = capnp.lib.capnp._StructModule CarControlT = capnp.lib.capnp._StructModule CarParamsT = capnp.lib.capnp._StructModule + +class DPFlags: + pass diff --git a/opendbc_repo/opendbc/car/subaru/interface.py b/opendbc_repo/opendbc/car/subaru/interface.py index b11a987..09b9b3a 100644 --- a/opendbc_repo/opendbc/car/subaru/interface.py +++ b/opendbc_repo/opendbc/car/subaru/interface.py @@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "subaru" ret.radarUnavailable = True # for HYBRID CARS to be upstreamed, we need: diff --git a/opendbc_repo/opendbc/car/tesla/interface.py b/opendbc_repo/opendbc/car/tesla/interface.py index baf1593..2019f7c 100644 --- a/opendbc_repo/opendbc/car/tesla/interface.py +++ b/opendbc_repo/opendbc/car/tesla/interface.py @@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "tesla" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)] diff --git a/opendbc_repo/opendbc/car/tests/test_car_interfaces.py b/opendbc_repo/opendbc/car/tests/test_car_interfaces.py index 63bedf0..081b07d 100644 --- a/opendbc_repo/opendbc/car/tests/test_car_interfaces.py +++ b/opendbc_repo/opendbc/car/tests/test_car_interfaces.py @@ -52,7 +52,7 @@ def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase: # initialize car interface CarInterface = interfaces[car_name] car_params = CarInterface.get_params(car_name, params['fingerprints'], params['car_fw'], - alpha_long=params['alpha_long'], is_release=False, docs=False) + alpha_long=params['alpha_long'], is_release=False, dp_params=0, docs=False) return CarInterface(car_params) diff --git a/opendbc_repo/opendbc/car/toyota/interface.py b/opendbc_repo/opendbc/car/toyota/interface.py index b2f82f2..545e3d2 100644 --- a/opendbc_repo/opendbc/car/toyota/interface.py +++ b/opendbc_repo/opendbc/car/toyota/interface.py @@ -21,7 +21,7 @@ class CarInterface(CarInterfaceBase): return CarControllerParams(CP).ACCEL_MIN, CarControllerParams(CP).ACCEL_MAX @staticmethod - def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "toyota" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)] ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate] diff --git a/opendbc_repo/opendbc/car/volkswagen/interface.py b/opendbc_repo/opendbc/car/volkswagen/interface.py index 4c40f87..61dd797 100644 --- a/opendbc_repo/opendbc/car/volkswagen/interface.py +++ b/opendbc_repo/opendbc/car/volkswagen/interface.py @@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase): CarController = CarController @staticmethod - def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams: + def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams: ret.brand = "volkswagen" ret.radarUnavailable = True diff --git a/selfdrive/assets/icons/plus.png b/selfdrive/assets/icons/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..92b448b0bdcfcb2b1f1abede5f5b751b314353d1 GIT binary patch literal 2833 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 literal 0 HcmV?d00001 diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 27b04ae..00be613 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -82,6 +82,8 @@ class Car: is_release = self.params.get_bool("IsReleaseBranch") + dp_params = 0 + if CI is None: # wait for one pandaState and one CAN packet print("Waiting for CAN messages...") @@ -99,7 +101,7 @@ class Car: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params) + self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, dp_params, cached_params) self.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP) self.CP = self.CI.CP diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 0d76184..0474a7a 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -12,7 +12,7 @@ V_CRUISE_MIN = 8 V_CRUISE_MAX = 145 V_CRUISE_UNSET = 255 V_CRUISE_INITIAL = 40 -V_CRUISE_INITIAL_EXPERIMENTAL_MODE = 105 +V_CRUISE_INITIAL_EXPERIMENTAL_MODE = 50 IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding errors incrementing set speed ButtonEvent = car.CarState.ButtonEvent diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 8996ad6..49532f5 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -149,7 +149,7 @@ class TestCarModelBase(unittest.TestCase): 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) + cls.CP = cls.CarInterface.get_params(cls.platform, cls.fingerprint, car_fw, alpha_long, False, dp_params=0, docs=False) assert cls.CP assert cls.CP.carFingerprint == cls.platform diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 34fc85f..94b6329 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -26,6 +26,8 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5 _A_TOTAL_MAX_V = [1.7, 3.2] _A_TOTAL_MAX_BP = [20., 40.] +class DPFlags: + pass def get_max_accel(v_ego): return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) @@ -89,7 +91,7 @@ class LongitudinalPlanner: throttle_prob = 1.0 return x, v, a, j, throttle_prob - def update(self, sm): + def update(self, sm, dp_flags = 0): mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' if len(sm['carControl'].orientationNED) == 3: diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index bec7eed..dc3d5d2 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -4,7 +4,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import Priority, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.controls.lib.ldw import LaneDepartureWarning -from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner +from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner, DPFlags import cereal.messaging as messaging @@ -22,10 +22,12 @@ def main(): sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], poll='modelV2') + dp_flags = 0 + while True: sm.update() if sm.updated['modelV2']: - longitudinal_planner.update(sm) + longitudinal_planner.update(sm, dp_flags) longitudinal_planner.publish(sm, pm) ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl']) diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index e9c62fe..2747205 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -122,7 +122,7 @@ class SelfdriveD: self.rk = Ratekeeper(100, print_delay_threshold=None) # Determine startup event - self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster + self.startup_event = EventName.startup #if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster if not car_recognized: self.startup_event = EventName.startupNoCar elif car_recognized and self.CP.passive: diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index 4b8f087..0c5f019 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -2,13 +2,18 @@ from openpilot.common.params import Params from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.list_view import toggle_item +from openpilot.system.ui.widgets.list_view import toggle_item, button_item from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult +# rick - for show error logs +from openpilot.system.ui.widgets.html_render import HtmlModal +import os +from openpilot.common.basedir import BASEDIR + # Description constants DESCRIPTIONS = { 'enable_adb': tr_noop( @@ -33,6 +38,7 @@ class DeveloperLayout(Widget): super().__init__() self._params = Params() self._is_release = self._params.get_bool("IsReleaseBranch") + self._last_error_log_dialog: HtmlModal | None = None # Build items and keep references for callbacks/state updates self._adb_toggle = toggle_item( @@ -82,6 +88,7 @@ class DeveloperLayout(Widget): self._joystick_toggle, self._long_maneuver_toggle, self._alpha_long_toggle, + button_item(lambda: tr("Show Last Errors"), lambda: tr("Show"), callback=self._on_show_last_errors), ] self._scroller = Scroller(items, line_separator=True, spacing=0) @@ -170,3 +177,8 @@ class DeveloperLayout(Widget): self._params.put_bool("AlphaLongitudinalEnabled", False) self._params.put_bool("OnroadCycleRequested", True) self._update_toggles() + + def _on_show_last_errors(self): + if not self._last_error_log_dialog: + self._last_error_log_dialog = HtmlModal(text=(self._params.get("dp_dev_last_log") or "")) + gui_app.set_modal_overlay(self._last_error_log_dialog) diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index f13383f..655f3eb 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -13,6 +13,7 @@ from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import NetworkUI +from dragonpilot.selfdrive.ui.layouts.settings.dragonpilot import DragonpilotLayout # Settings close button SETTINGS_CLOSE_TEXT = "×" @@ -40,6 +41,7 @@ class PanelType(IntEnum): SOFTWARE = 3 FIREHOSE = 4 DEVELOPER = 5 + DRAGONPILOT = 6 @dataclass @@ -65,6 +67,7 @@ class SettingsLayout(Widget): PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()), PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()), PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()), + PanelType.DRAGONPILOT: PanelInfo("dp", DragonpilotLayout()), } self._font_medium = gui_app.font(FontWeight.MEDIUM) diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index 8166a8a..cd207f3 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -68,7 +68,7 @@ class SoftwareLayout(Widget): self._version_item, self._download_btn, self._install_btn, - self._branch_btn, + # self._branch_btn, # rick - disable this for now button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), ], line_separator=True, spacing=0) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index e4441ed..aee93b5 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -31,6 +31,8 @@ DESCRIPTIONS = { 'RecordFront': tr_noop("Upload data from the driver facing 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."), + "DisableLogging": tr_noop("Disable logging service"), + "DisableUpdates": tr_noop("Disable update service"), } @@ -90,6 +92,18 @@ class TogglesLayout(Widget): "metric.png", False, ), + "DisableLogging": ( + lambda: tr("Disable Logging"), + DESCRIPTIONS["DisableLogging"], + "", + False, + ), + "DisableUpdates": ( + lambda: tr("Disable Updates"), + DESCRIPTIONS["DisableUpdates"], + "", + False, + ), } self._long_personality_setting = multiple_button_item( diff --git a/system/manager/manager.py b/system/manager/manager.py index 36055d8..b3d898e 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -11,7 +11,7 @@ import cereal.messaging as messaging import openpilot.system.sentry as sentry from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow -from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware import HARDWARE, TICI from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process_config import managed_processes @@ -171,8 +171,10 @@ def manager_thread() -> None: # Exit main loop when uninstall/shutdown/reboot is needed shutdown = False - for param in ("DoUninstall", "DoShutdown", "DoReboot"): + for param in ("DoUninstall", "DoShutdown", "DoReboot", "dp_dev_reset_conf"): if params.get_bool(param): + if TICI and param == "dp_dev_reset_conf": + os.system("rm -fr /data/params/d/dp_*") shutdown = True params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}") cloudlog.warning(f"Shutting down manager - {param} set") @@ -207,6 +209,9 @@ def main() -> None: elif params.get_bool("DoShutdown"): cloudlog.warning("shutdown") HARDWARE.shutdown() + elif params.get_bool("dp_dev_reset_conf"): + cloudlog.warning("reboot") + HARDWARE.reboot() if __name__ == "__main__": diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 940e7f9..ee2cfe5 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -19,7 +19,7 @@ def iscar(started: bool, params: Params, CP: car.CarParams) -> bool: return started and not CP.notCar def logging(started: bool, params: Params, CP: car.CarParams) -> bool: - run = (not CP.notCar) or not params.get_bool("DisableLogging") + run = not params.get_bool("DisableLogging") return started and run def ublox_available() -> bool: diff --git a/system/sentry.py b/system/sentry.py index 47d64ba..9f7d19e 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -12,9 +12,9 @@ from openpilot.system.version import get_build_metadata, get_version class SentryProject(Enum): # python project - SELFDRIVE = "https://6f3c7076c1e14b2aa10f5dde6dda0cc4@o33823.ingest.sentry.io/77924" + SELFDRIVE = "https://980a0cba712a4c3593c33c78a12446e1@o273754.ingest.sentry.io/1488600" # native project - SELFDRIVE_NATIVE = "https://3e4b586ed21a4479ad5d85083b639bc6@o33823.ingest.sentry.io/157615" + SELFDRIVE_NATIVE = "https://980a0cba712a4c3593c33c78a12446e1@o273754.ingest.sentry.io/1488600" def report_tombstone(fn: str, message: str, contents: str) -> None: @@ -26,11 +26,16 @@ def report_tombstone(fn: str, message: str, contents: str) -> None: sentry_sdk.capture_message(message=message) sentry_sdk.flush() +def save_exception(exc_text): + log = "\n".join(exc_text.splitlines()) + "\n" + Params().put("dp_dev_last_log", log) def capture_exception(*args, **kwargs) -> None: cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1)) try: + import traceback + save_exception(traceback.format_exc()) sentry_sdk.capture_exception(*args, **kwargs) sentry_sdk.flush() # https://github.com/getsentry/sentry-python/issues/291 except Exception: diff --git a/system/ui/spinner.py b/system/ui/spinner.py index eb33f08..29a5dba 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -26,7 +26,7 @@ def clamp(value, min_value, max_value): class Spinner(Widget): def __init__(self): super().__init__() - self._comma_texture = gui_app.texture("images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) + self._comma_texture = gui_app.texture("../../dragonpilot/selfdrive/assets/images/spinner_comma.png", TEXTURE_SIZE, TEXTURE_SIZE) self._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True) self._rotation = 0.0 self._progress: int | None = None diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index e5f234e..af8d63d 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -296,6 +296,12 @@ class ListItem(Widget): # Cached properties for performance self._prev_description: str | None = self.description + @property + def enabled(self) -> bool: + if self.action_item: + return self.action_item.enabled + return True + def show_event(self): self._set_description_visible(False) @@ -352,17 +358,20 @@ class ListItem(Widget): content_x = self._rect.x + ITEM_PADDING text_x = content_x + color = ITEM_TEXT_COLOR if self.enabled else ITEM_TEXT_VALUE_COLOR + icon_tint = rl.WHITE if self.enabled else ITEM_TEXT_VALUE_COLOR + # Only draw title and icon for items that have them if self.title: # Draw icon if present if self.icon: - rl.draw_texture(self._icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - self._icon_texture.width) // 2), rl.WHITE) + rl.draw_texture(self._icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - self._icon_texture.width) // 2), icon_tint) text_x += ICON_SIZE + ITEM_PADDING # Draw main text text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) item_y = self._rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 - rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) + rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), ITEM_TEXT_FONT_SIZE, 0, color) # Draw description if visible if self.description_visible: @@ -466,3 +475,288 @@ def multiple_button_item(title: str | Callable[[], str], description: str | Call button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""): action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback) return ListItem(title=title, description=description, icon=icon, action_item=action) + + +# Copyright (c) 2019, Rick Lan +# +# 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, and/or sublicense, +# 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 +# the copyright holder. +# +# 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. + +from abc import ABC, abstractmethod + +class BaseSpinBoxAction(ItemAction, ABC): + def __init__(self, callback: Callable | None, enabled: bool | Callable[[], bool], width: int): + super().__init__(width=width, enabled=enabled) + self._callback = callback + + icon_size = 60 + self._minus_icon = gui_app.texture("icons/minus.png", icon_size, icon_size) + self._plus_icon = gui_app.texture("icons/plus.png", icon_size, icon_size) + + self._minus_button = Button("", self._on_minus, icon=self._minus_icon, button_style=ButtonStyle.LIST_ACTION, multi_touch=True) + self._plus_button = Button("", self._on_plus, icon=self._plus_icon, button_style=ButtonStyle.LIST_ACTION, multi_touch=True) + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + super().set_touch_valid_callback(touch_callback) + self._minus_button.set_touch_valid_callback(touch_callback) + self._plus_button.set_touch_valid_callback(touch_callback) + + def _render(self, rect: rl.Rectangle) -> bool: + is_enabled = _resolve_value(self._enabled_source, False) + + button_width = 110 + button_height = BUTTON_HEIGHT + spacing = 10 + button_y = rect.y + (rect.height - button_height) / 2 + + minus_rect = rl.Rectangle(rect.x, button_y, button_width, button_height) + plus_rect = rl.Rectangle(rect.x + rect.width - button_width, button_y, button_width, button_height) + + label_x = rect.x + button_width + spacing + label_width = (plus_rect.x) - (label_x) - spacing + + self._minus_button.set_enabled(is_enabled and self._get_minus_enabled()) + self._plus_button.set_enabled(is_enabled and self._get_plus_enabled()) + + self._minus_button.render(minus_rect) + self._plus_button.render(plus_rect) + + if label_width > 0: + label_rect = rl.Rectangle(label_x, rect.y, label_width, rect.height) + display_text = self._get_display_text() + color = ITEM_TEXT_VALUE_COLOR if is_enabled else ITEM_DESC_TEXT_COLOR + gui_label(label_rect, display_text, font_size=ITEM_TEXT_FONT_SIZE, color=color, + font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + + return False + + @abstractmethod + def _on_minus(self): + """Called when the minus button is pressed.""" + pass + + @abstractmethod + def _on_plus(self): + """Called when the plus button is pressed.""" + pass + + @abstractmethod + def _get_minus_enabled(self) -> bool: + """Return True if the minus button should be enabled.""" + pass + + @abstractmethod + def _get_plus_enabled(self) -> bool: + """Return True if the plus button should be enabled.""" + pass + + @abstractmethod + def _get_display_text(self) -> str: + """Return the string to display in the center.""" + pass + + +class SpinBoxAction(BaseSpinBoxAction): + def __init__(self, initial_value: int, min_val: int, max_val: int, step: int = 1, + suffix: str = "", special_value_text: str | None = None, + callback: Callable[[int], None] | None = None, enabled: bool | Callable[[], bool] = True, + width: int = 320): + super().__init__(callback, enabled, width) + self._value = initial_value + self._min_val = min_val + self._max_val = max_val + self._step = step + self._suffix = suffix + self._special_value_text = special_value_text + + def set_value(self, value: int): + self._value = max(self._min_val, min(self._max_val, value)) + + def get_value(self) -> int: + return self._value + + def _on_minus(self): + new_val = max(self._min_val, self._value - self._step) + if new_val != self._value: + self._value = new_val + if self._callback: + self._callback(self._value) + + def _on_plus(self): + new_val = min(self._max_val, self._value + self._step) + if new_val != self._value: + self._value = new_val + if self._callback: + self._callback(self._value) + + def _get_minus_enabled(self) -> bool: + return self._value > self._min_val + + def _get_plus_enabled(self) -> bool: + return self._value < self._max_val + + def _get_display_text(self) -> str: + if self._special_value_text and self._value == self._min_val: + return self._special_value_text + return f"{self._value}{self._suffix}" + + +class DoubleSpinBoxAction(BaseSpinBoxAction): + def __init__(self, initial_value: float, min_val: float, max_val: float, step: float = 0.1, + decimals: int = 1, suffix: str = "", special_value_text: str | None = None, # <-- 1. This is correct + callback: Callable[[float], None] | None = None, enabled: bool | Callable[[], bool] = True, + width: int = 320): + super().__init__(callback, enabled, width) + self._value = initial_value + self._min_val = min_val + self._max_val = max_val + self._step = step + self._decimals = decimals + self._suffix = suffix + self._special_value_text = special_value_text # <-- 2. Store the variable + + def set_value(self, value: float): + self._value = max(self._min_val, min(self._max_val, value)) + + def get_value(self) -> float: + return self._value + + def _on_minus(self): + new_val = max(self._min_val, self._value - self._step) + if new_val < self._value: + self._value = new_val + if self._callback: + self._callback(self._value) + + def _on_plus(self): + new_val = min(self._max_val, self._value + self._step) + if new_val > self._value: + self._value = new_val + if self._callback: + self._callback(self._value) + + def _get_minus_enabled(self) -> bool: + return self._value > self._min_val + + def _get_plus_enabled(self) -> bool: + return self._value < self._max_val + + def _get_display_text(self) -> str: + is_min_val = abs(self._value - self._min_val) < 1e-9 + if self._special_value_text and is_min_val: + return self._special_value_text + + return f"{self._value:.{self._decimals}f}{self._suffix}" + + +class TextSpinBoxAction(BaseSpinBoxAction): + def __init__(self, options: list[str], initial_index: int = 0, + callback: Callable[[int], None] | None = None, enabled: bool | Callable[[], bool] = True, + width: int = 320): + super().__init__(callback, enabled, width) + self._options = options if options else [""] + self._current_index = max(0, min(len(self._options) - 1, initial_index)) + self._initial_index = initial_index + + def set_index(self, index: int): + self._current_index = max(0, min(len(self._options) - 1, index)) + + def get_index(self) -> int: + return self._current_index + + def _on_minus(self): + new_idx = max(0, self._current_index - 1) + if new_idx != self._current_index: + self._current_index = new_idx + if self._callback: + self._callback(self._current_index) + + def _on_plus(self): + new_idx = min(len(self._options) - 1, self._current_index + 1) + if new_idx != self._current_index: + self._current_index = new_idx + if self._callback: + self._callback(self._current_index) + + def _get_minus_enabled(self) -> bool: + return self._current_index > 0 + + def _get_plus_enabled(self) -> bool: + return self._current_index < len(self._options) - 1 + + def _get_display_text(self) -> str: + return self._options[self._current_index] + + +def spin_button_item(title: str | Callable[[], str], callback: Callable[[int], None] | None, + initial_value: int, min_val: int, max_val: int, step: int = 1, + suffix: str = "", special_value_text: str | None = None, + description: str | Callable[[], str] | None = None, + icon: str = "", enabled: bool | Callable[[], bool] = True, + width: int = 500) -> ListItem: + """ + Creates a ListItem with a spinbox-style control (minus, value, plus). + + :param title: The main title of the list item. + :param callback: Function to call with the new integer value when it changes. + :param initial_value: The starting value. + :param min_val: The minimum allowed value. + :param max_val: The maximum allowed value. + :param step: The increment/decrement amount on each button press. + :param suffix: A string to append to the value (e.g., " s"). + :param special_value_text: Text to display when the value is at min_val (e.g., "Auto"). + :param description: Optional description text shown when the item is expanded. + :param icon: Optional icon for the list item. + :param enabled: Whether the control is enabled. + :return: A ListItem widget. + """ + action = SpinBoxAction(initial_value=initial_value, min_val=min_val, max_val=max_val, step=step, + suffix=suffix, special_value_text=special_value_text, + callback=callback, enabled=enabled, width=width) + return ListItem(title=title, description=description, action_item=action, icon=icon) + +def double_spin_button_item(title: str | Callable[[], str], callback: Callable[[float], None] | None, + initial_value: float, min_val: float, max_val: float, step: float = 0.1, + decimals: int = 1, suffix: str = "", special_value_text: str | None = None, + description: str | Callable[[], str] | None = None, + icon: str = "", enabled: bool | Callable[[], bool] = True, + width: int = 500) -> ListItem: + """ + Creates a ListItem with a spinbox-style control for float values. + + :param decimals: Number of decimal places to display. + :return: A ListItem widget. + """ + action = DoubleSpinBoxAction(initial_value=initial_value, min_val=min_val, max_val=max_val, step=step, + decimals=decimals, suffix=suffix, special_value_text=special_value_text, + callback=callback, enabled=enabled, width=width) + return ListItem(title=title, description=description, action_item=action, icon=icon) + +def text_spin_button_item(title: str | Callable[[], str], callback: Callable[[int], None] | None, + options: list[str], initial_index: int = 0, + description: str | Callable[[], str] | None = None, + icon: str = "", enabled: bool | Callable[[], bool] = True, + width: int = 500) -> ListItem: + """ + Creates a ListItem with a spinbox control for a list of text options. + + :param options: A list of strings to cycle through (e.g., ['Low', 'Mid', 'High']). + :param initial_index: The starting index in the options list. + :param callback: Function to call with the new *index* (int) when it changes. + :return: A ListItem widget. + """ + action = TextSpinBoxAction(options=options, initial_index=initial_index, + callback=callback, enabled=enabled, width=width) + return ListItem(title=title, description=description, action_item=action, icon=icon)