feat: Squash all core features into min

This commit is contained in:
Rick Lan 2025-11-11 09:25:56 +08:00
parent c5d5c5d1f3
commit 0cfd6de2b3
53 changed files with 787 additions and 125 deletions

View File

@ -4,5 +4,8 @@
"ms-vscode.cpptools", "ms-vscode.cpptools",
"elagil.pre-commit-helper", "elagil.pre-commit-helper",
"charliermarsh.ruff", "charliermarsh.ruff",
"JamiTech.simply-blame",
"k--kato.intellij-idea-keybindings",
"trinm1709.dracula-theme-from-intellij"
] ]
} }

17
.vscode/settings.json vendored
View File

@ -3,10 +3,25 @@
"editor.insertSpaces": true, "editor.insertSpaces": true,
"editor.renderWhitespace": "trailing", "editor.renderWhitespace": "trailing",
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"terminal.integrated.defaultProfile.linux": "dragonpilot",
"terminal.integrated.profiles.linux": {
"dragonpilot": {
"path": "bash",
"args": ["-c", "distrobox enter dp"]
}
},
"search.exclude": { "search.exclude": {
"**/.git": true, "**/.git": true,
"**/.venv": 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": { "files.exclude": {
"**/.git": true, "**/.git": true,

30
LICENSE.md Normal file
View File

@ -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.

127
README.md
View File

@ -1,107 +1,74 @@
<div align="center" style="text-align: center;"> ![](dragonpilot/selfdrive/assets/dragonpilot.png)
<h1>openpilot</h1> [Read this in English](README_EN.md)
<p> # **🐲 dragonpilot - 賦予您的愛車「龍」之魂**
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
<h3> **我們與您一同翱翔於更智慧、更貼心的駕駛旅程。**
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
</h3>
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) `dragonpilot` 誕生於 2019 年,由三位早期的 openpilot 華人玩家共同創立。初衷很簡單:為廣大的華人用戶、玩家們提供一個友善的交流環境、更簡便的設定協助,並加入更多適合在地使用的貼心功能。
[![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)
</div> 我們深知在地化的重要性,特別是語言的親切感。因此,我們率先導入了完整的中文介面,讓 `dragonpilot` 迅速在華語地區累積了口碑,也讓華人的使用者數量在全球名列前茅。這份來自在地的支持,是我們持續前進的最大動力。
<table> 我們以功能強大的 [openpilot](https://github.com/commaai/openpilot) 為基礎——這套據美國消費者報告評測優於市售車方案的開源輔助駕駛系統——融入了更多在地化的巧思與客製化的溫度,希望能打造出最符合您需求的駕駛夥伴。(您也可以參考我們 repo 中保留的 [openpilot 原始說明檔案](README_OPENPILOT.md))
<tr>
<td><a href="https://youtu.be/NmBfgOanCyk" title="Video By Greer Viau"><img src="https://github.com/commaai/openpilot/assets/8762862/2f7112ae-f748-4f39-b617-fabd689c3772"></a></td>
<td><a href="https://youtu.be/VHKyqZ7t8Gw" title="Video By Logan LeGrand"><img src="https://github.com/commaai/openpilot/assets/8762862/92351544-2833-40d7-9e0b-7ef7ae37ec4c"></a></td>
<td><a href="https://youtu.be/SUIZYzxtMQs" title="A drive to Taco Bell"><img src="https://github.com/commaai/openpilot/assets/8762862/05ceefc5-2628-439c-a9b2-89ce77dc6f63"></a></td>
</tr>
</table>
取名 `dragonpilot`,是因為我們希望它能像神話中的「龍」一樣,既強大又充滿智慧,為您的行車安全保駕護航。龍,在我們華人文化中,更是吉祥與力量的象徵,也代表著我們的根源與驕傲。
Using openpilot in a car ## **✨ dragonpilot 的里程碑**
------
To use openpilot in a car, you need four things: 我們不僅保留了 openpilot 的核心優勢,更達成了許多從社群回饋中誕生的里程碑,這些是我們引以為傲的足跡:
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. * **🚘 全時置中車道維持 (ALKA)**
### Branches 這不只是一個功能,更是 `dragonpilot` 的哲學。我們最早於 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 時便實現了這個功能,其開發歷程始於 2017 Lexus IS300h接著擴展至 Toyota 全車系,並逐步延伸到其他支援的品牌。它能溫柔地輔助您,讓車輛始終穩定地保持在車道中央,提供一份額外的安心與從容。
| 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). 在官方 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、O3XLO3 系列為副廠硬體)的社群分支。
Safety and Testing * **📜 曾榮獲官方認證第一大分支**
----
* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details. 基於活躍的社群與功能創新,`dragonpilot` 曾一度成長為 comma ai 官方認證的第一大 openpilot 分支,這份榮耀屬於每一位參與者。
* 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.
<details> ## **🧑‍💻 設計理念 - 少即是多 (Less is More)**
<summary>MIT Licensed</summary>
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.**
</details>
<details> 從最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社群中各式各樣充滿智慧的**副廠機 (如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我們也曾探索過在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。
<summary>User Data and comma Account</summary>
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. `dragonpilot` 的成長,離不開每一位使用者的貢獻與回饋。我們是一個以**公開、透明**為原則的溫暖社群,希望在這裡能與所有對 openpilot / dragonpilot 有興趣的用戶分享、交流開發與使用上的經驗。
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. [**歡迎加入我們的 Facebook 社團進行交流!**](https://www.facebook.com/groups/930190251238639)
</details>
## **❤️ 特別感謝**
`dragonpilot` 從創立至今,從未打算透過 Patreon 等平台進行任何形式的募資。我們的初衷是建立一個讓大家能一起學習、一起成長的社群。It's all about fun, not money.
然而,我們仍要對那些自發性支持本專案的朋友們,致上最誠摯的感謝。正是因為有您們的鼓勵,我們才有更大的動力持續前進。
[**我們的贊助者名單**](SPONSORS.md)
### **安全聲明**
`dragonpilot` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。
**最後,再次感謝您的到來。**
**期待與您一同在智慧駕駛的道路上,乘「龍」而行!**

74
README_EN.md Normal file
View File

@ -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!**

107
README_OPENPILOT.md Normal file
View File

@ -0,0 +1,107 @@
<div align="center" style="text-align: center;">
<h1>openpilot</h1>
<p>
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
<h3>
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
</h3>
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)
</div>
<table>
<tr>
<td><a href="https://youtu.be/NmBfgOanCyk" title="Video By Greer Viau"><img src="https://github.com/commaai/openpilot/assets/8762862/2f7112ae-f748-4f39-b617-fabd689c3772"></a></td>
<td><a href="https://youtu.be/VHKyqZ7t8Gw" title="Video By Logan LeGrand"><img src="https://github.com/commaai/openpilot/assets/8762862/92351544-2833-40d7-9e0b-7ef7ae37ec4c"></a></td>
<td><a href="https://youtu.be/SUIZYzxtMQs" title="A drive to Taco Bell"><img src="https://github.com/commaai/openpilot/assets/8762862/05ceefc5-2628-439c-a9b2-89ce77dc6f63"></a></td>
</tr>
</table>
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.
<details>
<summary>MIT Licensed</summary>
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.**
</details>
<details>
<summary>User Data and comma Account</summary>
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.
</details>

View File

@ -28,7 +28,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"CurrentBootlog", {PERSISTENT, STRING}}, {"CurrentBootlog", {PERSISTENT, STRING}},
{"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, 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}}, {"DisablePowerDown", {PERSISTENT, BOOL}},
{"DisableUpdates", {PERSISTENT, BOOL}}, {"DisableUpdates", {PERSISTENT, BOOL}},
{"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}}, {"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}},
@ -128,4 +128,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
}; };

0
dragonpilot/.gitignore vendored Normal file
View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="256"
height="256"
viewBox="0 0 64 64"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="icon_empty.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.0820312"
inkscape:cx="128"
inkscape:cy="114.37262"
inkscape:window-width="2560"
inkscape:window-height="1021"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

View File

0
dragonpilot/selfdrive/ui/.gitignore vendored Normal file
View File

View File

@ -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)

View File

@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.notCar = True
ret.brand = "body" ret.brand = "body"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.body)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.body)]

View File

@ -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, 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) candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params)
if candidate is None: if candidate is None:
@ -157,7 +157,7 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip
candidate = "MOCK" candidate = "MOCK"
CarInterface = interfaces[candidate] 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.carVin = vin
CP.carFw = car_fw CP.carFw = car_fw
CP.fingerprintSource = source CP.fingerprintSource = source

View File

@ -13,7 +13,7 @@ class CarInterface(CarInterfaceBase):
RadarInterface = RadarInterface RadarInterface = RadarInterface
@staticmethod @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.brand = "chrysler"
ret.dashcamOnly = candidate in RAM_HD ret.dashcamOnly = candidate in RAM_HD

View File

@ -32,7 +32,7 @@ def get_params_for_docs(platform) -> CarParams:
cp_platform = platform if platform in interfaces else MOCK.MOCK cp_platform = platform if platform in interfaces else MOCK.MOCK
CP: CarParams = interfaces[cp_platform].get_params(cp_platform, fingerprint=gen_empty_fingerprint(), CP: CarParams = interfaces[cp_platform].get_params(cp_platform, fingerprint=gen_empty_fingerprint(),
car_fw=[CarParams.CarFw(ecu=CarParams.Ecu.unknown)], 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 return CP

View File

@ -26,7 +26,7 @@ class CarInterface(CarInterfaceBase):
return CarControllerParams.ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS) return CarControllerParams.ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
@staticmethod @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.brand = "ford"
ret.radarUnavailable = Bus.radar not in DBC[candidate] ret.radarUnavailable = Bus.radar not in DBC[candidate]

View File

@ -82,7 +82,7 @@ class CarInterface(CarInterfaceBase):
return self.lateral_accel_from_torque_linear return self.lateral_accel_from_torque_linear
@staticmethod @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.brand = "gm"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)]
ret.autoResumeSng = False ret.autoResumeSng = False

View File

@ -31,7 +31,7 @@ class CarInterface(CarInterfaceBase):
return CarControllerParams.NIDEC_ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS) return CarControllerParams.NIDEC_ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
@staticmethod @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" ret.brand = "honda"
CAN = CanBus(ret, fingerprint) CAN = CanBus(ret, fingerprint)

View File

@ -23,7 +23,7 @@ class CarInterface(CarInterfaceBase):
RadarInterface = RadarInterface RadarInterface = RadarInterface
@staticmethod @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" ret.brand = "hyundai"
# "LKA steering" if LKAS or LKAS_ALT messages are seen coming from the camera. # "LKA steering" if LKAS or LKAS_ALT messages are seen coming from the camera.

View File

@ -51,7 +51,7 @@ class TestHyundaiFingerprint:
if lka_steering: if lka_steering:
cam_can = CanBus(None, fingerprint).CAM cam_can = CanBus(None, fingerprint).CAM
fingerprint[cam_can] = [0x50, 0x110] # LKA steering messages 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 assert bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING) == lka_steering
# radar available # radar available
@ -59,14 +59,14 @@ class TestHyundaiFingerprint:
fingerprint = gen_empty_fingerprint() fingerprint = gen_empty_fingerprint()
if radar: if radar:
fingerprint[1][RADAR_START_ADDR] = 8 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 assert CP.radarUnavailable != radar
def test_alternate_limits(self): def test_alternate_limits(self):
# Alternate lateral control limits, for high torque cars, verify Panda safety mode flag is set # Alternate lateral control limits, for high torque cars, verify Panda safety mode flag is set
fingerprint = gen_empty_fingerprint() fingerprint = gen_empty_fingerprint()
for car_model in CAR: 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) assert bool(CP.flags & HyundaiFlags.ALT_LIMITS) == bool(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.ALT_LIMITS)
def test_can_features(self): def test_can_features(self):

View File

@ -122,11 +122,11 @@ class CarInterfaceBase(ABC):
""" """
Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints. 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 @classmethod
def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw], 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) ret = CarInterfaceBase.get_std_params(candidate)
platform = PLATFORMS[candidate] platform = PLATFORMS[candidate]
@ -139,7 +139,7 @@ class CarInterfaceBase(ABC):
ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor
ret.flags |= int(platform.config.flags) 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 # Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
if not ret.notCar: if not ret.notCar:
@ -154,7 +154,7 @@ class CarInterfaceBase(ABC):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def _get_params(ret: structs.CarParams, candidate, fingerprint: dict[int, dict[int, int]], 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 raise NotImplementedError
@staticmethod @staticmethod

View File

@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "mazda"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)]
ret.radarUnavailable = True ret.radarUnavailable = True

View File

@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "mock"
ret.mass = 1700. ret.mass = 1700.
ret.wheelbase = 2.70 ret.wheelbase = 2.70

View File

@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "nissan"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.nissan)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.nissan)]
ret.autoResumeSng = False ret.autoResumeSng = False

View File

@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = 'psa'
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.psa)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.psa)]

View File

@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase):
RadarInterface = RadarInterface RadarInterface = RadarInterface
@staticmethod @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.brand = "rivian"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.rivian)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.rivian)]

View File

@ -18,3 +18,6 @@ CarStateT = capnp.lib.capnp._StructModule
RadarDataT = capnp.lib.capnp._StructModule RadarDataT = capnp.lib.capnp._StructModule
CarControlT = capnp.lib.capnp._StructModule CarControlT = capnp.lib.capnp._StructModule
CarParamsT = capnp.lib.capnp._StructModule CarParamsT = capnp.lib.capnp._StructModule
class DPFlags:
pass

View File

@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "subaru"
ret.radarUnavailable = True ret.radarUnavailable = True
# for HYBRID CARS to be upstreamed, we need: # for HYBRID CARS to be upstreamed, we need:

View File

@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "tesla"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)]

View File

@ -52,7 +52,7 @@ def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase:
# initialize car interface # initialize car interface
CarInterface = interfaces[car_name] CarInterface = interfaces[car_name]
car_params = CarInterface.get_params(car_name, params['fingerprints'], params['car_fw'], 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) return CarInterface(car_params)

View File

@ -21,7 +21,7 @@ class CarInterface(CarInterfaceBase):
return CarControllerParams(CP).ACCEL_MIN, CarControllerParams(CP).ACCEL_MAX return CarControllerParams(CP).ACCEL_MIN, CarControllerParams(CP).ACCEL_MAX
@staticmethod @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.brand = "toyota"
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)] ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)]
ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate] ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate]

View File

@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
CarController = CarController CarController = CarController
@staticmethod @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.brand = "volkswagen"
ret.radarUnavailable = True ret.radarUnavailable = True

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -82,6 +82,8 @@ class Car:
is_release = self.params.get_bool("IsReleaseBranch") is_release = self.params.get_bool("IsReleaseBranch")
dp_params = 0
if CI is None: if CI is None:
# wait for one pandaState and one CAN packet # wait for one pandaState and one CAN packet
print("Waiting for CAN messages...") print("Waiting for CAN messages...")
@ -99,7 +101,7 @@ class Car:
with car.CarParams.from_bytes(cached_params_raw) as _cached_params: with car.CarParams.from_bytes(cached_params_raw) as _cached_params:
cached_params = _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.RI = interfaces[self.CI.CP.carFingerprint].RadarInterface(self.CI.CP)
self.CP = self.CI.CP self.CP = self.CI.CP

View File

@ -12,7 +12,7 @@ V_CRUISE_MIN = 8
V_CRUISE_MAX = 145 V_CRUISE_MAX = 145
V_CRUISE_UNSET = 255 V_CRUISE_UNSET = 255
V_CRUISE_INITIAL = 40 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 IMPERIAL_INCREMENT = round(CV.MPH_TO_KPH, 1) # round here to avoid rounding errors incrementing set speed
ButtonEvent = car.CarState.ButtonEvent ButtonEvent = car.CarState.ButtonEvent

View File

@ -149,7 +149,7 @@ class TestCarModelBase(unittest.TestCase):
cls.openpilot_enabled = cls.car_safety_mode_frame is not None cls.openpilot_enabled = cls.car_safety_mode_frame is not None
cls.CarInterface = interfaces[cls.platform] 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
assert cls.CP.carFingerprint == cls.platform assert cls.CP.carFingerprint == cls.platform

View File

@ -26,6 +26,8 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5
_A_TOTAL_MAX_V = [1.7, 3.2] _A_TOTAL_MAX_V = [1.7, 3.2]
_A_TOTAL_MAX_BP = [20., 40.] _A_TOTAL_MAX_BP = [20., 40.]
class DPFlags:
pass
def get_max_accel(v_ego): def get_max_accel(v_ego):
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS) return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
@ -89,7 +91,7 @@ class LongitudinalPlanner:
throttle_prob = 1.0 throttle_prob = 1.0
return x, v, a, j, throttle_prob 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' mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if len(sm['carControl'].orientationNED) == 3: if len(sm['carControl'].orientationNED) == 3:

View File

@ -4,7 +4,7 @@ from openpilot.common.params import Params
from openpilot.common.realtime import Priority, config_realtime_process from openpilot.common.realtime import Priority, config_realtime_process
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.controls.lib.ldw import LaneDepartureWarning 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 import cereal.messaging as messaging
@ -22,10 +22,12 @@ def main():
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'], sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState'],
poll='modelV2') poll='modelV2')
dp_flags = 0
while True: while True:
sm.update() sm.update()
if sm.updated['modelV2']: if sm.updated['modelV2']:
longitudinal_planner.update(sm) longitudinal_planner.update(sm, dp_flags)
longitudinal_planner.publish(sm, pm) longitudinal_planner.publish(sm, pm)
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl']) ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])

View File

@ -122,7 +122,7 @@ class SelfdriveD:
self.rk = Ratekeeper(100, print_delay_threshold=None) self.rk = Ratekeeper(100, print_delay_threshold=None)
# Determine startup event # 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: if not car_recognized:
self.startup_event = EventName.startupNoCar self.startup_event = EventName.startupNoCar
elif car_recognized and self.CP.passive: elif car_recognized and self.CP.passive:

View File

@ -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.widgets.ssh_key import ssh_key_item
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets import Widget 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.scroller import Scroller
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult 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 # Description constants
DESCRIPTIONS = { DESCRIPTIONS = {
'enable_adb': tr_noop( 'enable_adb': tr_noop(
@ -33,6 +38,7 @@ class DeveloperLayout(Widget):
super().__init__() super().__init__()
self._params = Params() self._params = Params()
self._is_release = self._params.get_bool("IsReleaseBranch") 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 # Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item( self._adb_toggle = toggle_item(
@ -82,6 +88,7 @@ class DeveloperLayout(Widget):
self._joystick_toggle, self._joystick_toggle,
self._long_maneuver_toggle, self._long_maneuver_toggle,
self._alpha_long_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) 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("AlphaLongitudinalEnabled", False)
self._params.put_bool("OnroadCycleRequested", True) self._params.put_bool("OnroadCycleRequested", True)
self._update_toggles() 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)

View File

@ -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.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.network import NetworkUI from openpilot.system.ui.widgets.network import NetworkUI
from dragonpilot.selfdrive.ui.layouts.settings.dragonpilot import DragonpilotLayout
# Settings close button # Settings close button
SETTINGS_CLOSE_TEXT = "×" SETTINGS_CLOSE_TEXT = "×"
@ -40,6 +41,7 @@ class PanelType(IntEnum):
SOFTWARE = 3 SOFTWARE = 3
FIREHOSE = 4 FIREHOSE = 4
DEVELOPER = 5 DEVELOPER = 5
DRAGONPILOT = 6
@dataclass @dataclass
@ -65,6 +67,7 @@ class SettingsLayout(Widget):
PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()), PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()),
PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()), PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout()),
PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()), PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()),
PanelType.DRAGONPILOT: PanelInfo("dp", DragonpilotLayout()),
} }
self._font_medium = gui_app.font(FontWeight.MEDIUM) self._font_medium = gui_app.font(FontWeight.MEDIUM)

View File

@ -68,7 +68,7 @@ class SoftwareLayout(Widget):
self._version_item, self._version_item,
self._download_btn, self._download_btn,
self._install_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), button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
], line_separator=True, spacing=0) ], line_separator=True, spacing=0)

View File

@ -31,6 +31,8 @@ DESCRIPTIONS = {
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), '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."), "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."), "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", "metric.png",
False, False,
), ),
"DisableLogging": (
lambda: tr("Disable Logging"),
DESCRIPTIONS["DisableLogging"],
"",
False,
),
"DisableUpdates": (
lambda: tr("Disable Updates"),
DESCRIPTIONS["DisableUpdates"],
"",
False,
),
} }
self._long_personality_setting = multiple_button_item( self._long_personality_setting = multiple_button_item(

View File

@ -11,7 +11,7 @@ import cereal.messaging as messaging
import openpilot.system.sentry as sentry import openpilot.system.sentry as sentry
from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.params import Params, ParamKeyFlag
from openpilot.common.text_window import TextWindow 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.helpers import unblock_stdout, write_onroad_params, save_bootlog
from openpilot.system.manager.process import ensure_running from openpilot.system.manager.process import ensure_running
from openpilot.system.manager.process_config import managed_processes 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 # Exit main loop when uninstall/shutdown/reboot is needed
shutdown = False shutdown = False
for param in ("DoUninstall", "DoShutdown", "DoReboot"): for param in ("DoUninstall", "DoShutdown", "DoReboot", "dp_dev_reset_conf"):
if params.get_bool(param): if params.get_bool(param):
if TICI and param == "dp_dev_reset_conf":
os.system("rm -fr /data/params/d/dp_*")
shutdown = True shutdown = True
params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}") params.put("LastManagerExitReason", f"{param} {datetime.datetime.now()}")
cloudlog.warning(f"Shutting down manager - {param} set") cloudlog.warning(f"Shutting down manager - {param} set")
@ -207,6 +209,9 @@ def main() -> None:
elif params.get_bool("DoShutdown"): elif params.get_bool("DoShutdown"):
cloudlog.warning("shutdown") cloudlog.warning("shutdown")
HARDWARE.shutdown() HARDWARE.shutdown()
elif params.get_bool("dp_dev_reset_conf"):
cloudlog.warning("reboot")
HARDWARE.reboot()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -19,7 +19,7 @@ def iscar(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and not CP.notCar return started and not CP.notCar
def logging(started: bool, params: Params, CP: car.CarParams) -> bool: 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 return started and run
def ublox_available() -> bool: def ublox_available() -> bool:

View File

@ -12,9 +12,9 @@ from openpilot.system.version import get_build_metadata, get_version
class SentryProject(Enum): class SentryProject(Enum):
# python project # python project
SELFDRIVE = "https://6f3c7076c1e14b2aa10f5dde6dda0cc4@o33823.ingest.sentry.io/77924" SELFDRIVE = "https://980a0cba712a4c3593c33c78a12446e1@o273754.ingest.sentry.io/1488600"
# native project # 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: 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.capture_message(message=message)
sentry_sdk.flush() 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: def capture_exception(*args, **kwargs) -> None:
cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1)) cloudlog.error("crash", exc_info=kwargs.get('exc_info', 1))
try: try:
import traceback
save_exception(traceback.format_exc())
sentry_sdk.capture_exception(*args, **kwargs) sentry_sdk.capture_exception(*args, **kwargs)
sentry_sdk.flush() # https://github.com/getsentry/sentry-python/issues/291 sentry_sdk.flush() # https://github.com/getsentry/sentry-python/issues/291
except Exception: except Exception:

View File

@ -26,7 +26,7 @@ def clamp(value, min_value, max_value):
class Spinner(Widget): class Spinner(Widget):
def __init__(self): def __init__(self):
super().__init__() 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._spinner_texture = gui_app.texture("images/spinner_track.png", TEXTURE_SIZE, TEXTURE_SIZE, alpha_premultiply=True)
self._rotation = 0.0 self._rotation = 0.0
self._progress: int | None = None self._progress: int | None = None

View File

@ -296,6 +296,12 @@ class ListItem(Widget):
# Cached properties for performance # Cached properties for performance
self._prev_description: str | None = self.description 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): def show_event(self):
self._set_description_visible(False) self._set_description_visible(False)
@ -352,17 +358,20 @@ class ListItem(Widget):
content_x = self._rect.x + ITEM_PADDING content_x = self._rect.x + ITEM_PADDING
text_x = content_x 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 # Only draw title and icon for items that have them
if self.title: if self.title:
# Draw icon if present # Draw icon if present
if self.icon: 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 text_x += ICON_SIZE + ITEM_PADDING
# Draw main text # Draw main text
text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) 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 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 # Draw description if visible
if self.description_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 = ""): button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""):
action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback) action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback)
return ListItem(title=title, description=description, icon=icon, action_item=action) 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)