1 Commits

Author SHA1 Message Date
github-actions[bot] 3dcc048299 sunnypilot v2026.002.000 release
date: 2026-06-19T21:43:27
master commit: 5d90689776fdc7a3be31fc1335003aee20a2ba62
2026-06-19 21:43:47 +08:00
1654 changed files with 73454 additions and 131697 deletions
+6
View File
@@ -0,0 +1,6 @@
Wen
REGIST
PullRequest
cancelled
FOF
NoO
+11
View File
@@ -0,0 +1,11 @@
* @sunnypilot/dev-internal
/.github/ @devtekve @sunnyhaibin
/release/ci/ @devtekve @sunnyhaibin
/tinygrad_repo @devtekve @Discountchubbs
/tinygrad/ @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
/selfdrive/modeld/ @devtekve @Discountchubbs
/sunnypilot/model* @devtekve @Discountchubbs
/sunnypilot/sunnylink/ @devtekve
/system/athena/ @devtekve
+6 -2
View File
@@ -1,7 +1,11 @@
CI / testing:
ci:
- changed-files:
- any-glob-to-all-files: "{.github/**,**/test_*,**/test/**,Jenkinsfile}"
chore:
- changed-files:
- any-glob-to-all-files: "{.github/**}"
car:
- changed-files:
- any-glob-to-all-files: '{selfdrive/car/**,opendbc_repo}'
@@ -24,4 +28,4 @@ multilanguage:
autonomy:
- changed-files:
- any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit}"
- any-glob-to-all-files: "{selfdrive/modeld/models/**,selfdrive/test/process_replay/model_replay_ref_commit,sunnypilot/modeld*/models/**}"
+43
View File
@@ -0,0 +1,43 @@
exclude-labels:
- 'no-changelog'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
collapse-after: 5
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
collapse-after: 5
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&'
replacers:
- search: '/[Ss][Uu][Nn][Nn][Yy][Pp][Ii][Ll][Oo][Tt]/g'
replace: 'sunnypilot'
- search: '/\b[Ss][Pp]\b/g'
replace: 'SP'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
name-template: 'v$RESOLVED_VERSION 🚀'
tag-template: 'v$RESOLVED_VERSION'
version-template: "0.$MAJOR.$MINOR.$PATCH" # The day OP becomes v1, we need to bump this
tag-prefix: "v0." # The day OP becomes v1, we need to bump this
prerelease-identifier: "staging"
template: |
## Changes
$CHANGES
+6 -2
View File
@@ -88,6 +88,10 @@ build/
.context/
PLAN.md
TASK.md
CLAUDE.md
SKILL.md
# rick - keep panda_tici standalone
panda_tici/
### JetBrains ###
!.idea/customTargets.xml
!.idea/tools/*
!.run/*
-3
View File
@@ -4,8 +4,5 @@
"ms-vscode.cpptools",
"elagil.pre-commit-helper",
"charliermarsh.ruff",
"JamiTech.simply-blame",
"k--kato.intellij-idea-keybindings",
"trinm1709.dracula-theme-from-intellij"
]
}
+1 -15
View File
@@ -3,24 +3,10 @@
"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,
"msgq_repo/": true,
"rednose/": true,
"rednose_repo/": true,
"openpilot/": true,
"teleoprtc_repo/": true,
"tinygrad/": true,
"tinygrad_repo/": true
"**/__pycache__": true
},
"files.exclude": {
"**/.git": true,
-140
View File
@@ -1,140 +0,0 @@
# ALKA (Always-on Lane Keeping Assist) Design v3
## Overview
ALKA enables lateral control (steering) when ACC Main is ON, without requiring cruise to be engaged. This allows lane keeping assist to function independently of longitudinal control.
**Simplified Behavior (v3):**
- All brands use direct tracking: `lkas_on = acc_main_on`
- No button/toggle tracking (removed TJA, LKAS button, LKAS HUD)
- ACC Main ON = ALKA enabled, ACC Main OFF = ALKA disabled
---
## Per-Brand Summary
| Brand | Status | ACC Main Source | Notes |
|-------|--------|-----------------|-------|
| Body | Disabled | - | No steering capability |
| Chrysler | Disabled | - | Needs special handling |
| Ford | Enabled | EngBrakeData (0x165) CcStat | |
| GM | Disabled | - | No ACC Main signal |
| Honda Nidec | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Honda Bosch | Enabled | SCM_FEEDBACK (0x326) MAIN_ON | |
| Hyundai | Enabled | SCC11 (0x420) bit 0 | |
| Hyundai CAN-FD | Enabled | SCC_CONTROL (0x1A0) bit 66 | |
| Hyundai Legacy | Enabled | SCC11 (0x420) bit 0 | |
| Mazda | Enabled | CRZ_CTRL (0x21C) bit 17 | |
| Nissan | Enabled | CRUISE_THROTTLE (0x239) bit 17 | |
| PSA | Disabled | - | Not implemented |
| Rivian | Disabled | - | Different architecture |
| Subaru | Enabled | CruiseControl (0x240) bit 40 | |
| Subaru Preglobal | Enabled | CruiseControl (0x144) bit 48 | |
| Tesla | Disabled | - | Different architecture |
| Toyota | Enabled | PCM_CRUISE_2 (0x1D3) bit 15 | |
| Toyota (UNSUPPORTED_DSU) | Enabled | DSU_CRUISE (0x365) bit 0 | |
| VW MQB | Enabled | TSK_06 TSK_Status (>=2) | |
| VW PQ | Enabled | Motor_5 (0x480) bit 50 (long) | |
---
## Permission Model
Lateral control requires checks at both layers. Normal path uses `controls_allowed`, ALKA path uses additional checks.
| Check | Panda | openpilot | Notes |
|-------|:-----:|:---------:|-------|
| **Normal Path** |
| `controls_allowed` (cruise engaged) | ✓ | ✓ | Either this OR ALKA path |
| **ALKA Path** |
| `alka_allowed` (brand supports) | ✓ | ✓ | Set per brand in safety init |
| `ALT_EXP_ALKA` (user enabled) | ✓ | ✓ | alternativeExperience flag |
| `lkas_on` (ACC Main ON) | ✓ | ✓ | Tracked via CAN messages |
| `vehicle_moving` / `!standstill` | ✓ | ✓ | |
| **openpilot Additional** |
| `gear_ok` (not P/N/R) | ✗ | ✓ | Python layer only |
| `calibrated` | ✗ | ✓ | Python layer only |
| `seatbelt latched` | ✗ | ✓ | Python layer only |
| `doors closed` | ✗ | ✓ | Python layer only |
| `!steerFaultTemporary` | ✗ | ✓ | Python layer only |
| `!steerFaultPermanent` | ✗ | ✓ | Python layer only |
---
## Data Flow
```
┌─────────────────────────────────────────────────────────────────────┐
│ CAN Bus │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Safety Layer (panda C code) │ │ Python Layer │
│ │ │ │
│ rx_hook: │ │ carstate.py: │
│ - Parse ACC Main signal │ │ - Parse cruiseState.available │
│ - Set lkas_on = acc_main_on │ │ - Set self.lkas_on │
│ │ │ │
│ lat_control_allowed(): │ └─────────────┬───────────────────┘
│ - Check lkas_on + other flags │ │
│ - Gate steering commands │ ▼
└─────────────────────────────────┘ ┌─────────────────────────────────┐
│ card.py: │
│ - Publish carStateExt.lkasOn │
└─────────────┬───────────────────┘
┌─────────────────────────────────┐
│ controlsd.py: │
│ - Read carStateExt.lkasOn │
│ - Check ALKA conditions │
│ - Set CC.latActive │
└─────────────────────────────────┘
```
### Key Files
| File | Purpose |
|------|---------|
| `custom.capnp` | Defines `CarStateExt` struct with `lkasOn` field |
| `log.capnp` | Includes `carStateExt` in event union |
| `interfaces.py` | Defines `self.lkas_on = False` default in `CarStateBase` |
| `carstate.py` (per brand) | Tracks `lkas_on` based on ACC Main |
| `card.py` | Publishes `carStateExt.lkasOn` from `CI.CS.lkas_on` |
| `controlsd.py` | Reads `carStateExt.lkasOn` to determine `alka_active` |
---
## ACC Main Tracking
All brands use simple direct tracking:
```c
// Panda (C code)
if (alka_allowed && (alternative_experience & ALT_EXP_ALKA)) {
lkas_on = acc_main_on; // or GET_BIT(msg, bit_position)
}
```
```python
# Python carstate.py
self.lkas_on = ret.cruiseState.available
```
This guard ensures:
1. Brand supports ALKA (`alka_allowed`)
2. User enabled ALKA (`ALT_EXP_ALKA`)
Without both conditions, no ACC Main tracking occurs, and ALKA remains disabled.
---
## Testing
Safety tests verify:
- `alka_allowed` flag set correctly per brand
- ACC Main tracking updates `lkas_on` directly
- `lat_control_allowed()` returns true only when all conditions met
- Steering TX blocked when ALKA conditions not met
- Bus routing variants (camera_scc, unsupported_dsu)
+1291
View File
File diff suppressed because it is too large Load Diff
+15 -24
View File
@@ -1,30 +1,21 @@
Copyright (c) 2019, Rick Lan
# Custom MIT License
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:
Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
- 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.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
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.
1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
"This software is licensed under a custom license requiring permission for use."
3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
"This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
4. **No Warranty**: 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.
Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
---
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.
Haibin Wen, SUNNYPILOT LLC
+48 -46
View File
@@ -1,74 +1,76 @@
![](dragonpilot/selfdrive/assets/dragonpilot.png)
## ✍ To install this fork use installer.comma.ai/infiniteCable2/master (Comma Four compatible)
[Read this in English](README_EN.md)
![](https://user-images.githubusercontent.com/47793918/233812617-beab2e71-57b9-479e-8bff-c3931347ca40.png)
# **🐲 dragonpilot - 赋予您的爱车「龙」之魂**
## 🌞 What is sunnypilot?
[sunnypilot](https://github.com/sunnyhaibin/sunnypilot) is a fork of comma.ai's openpilot, an open source driver assistance system. sunnypilot offers the user a unique driving experience for over 300+ supported car makes and models with modified behaviors of driving assist engagements. sunnypilot complies with comma.ai's safety rules as accurately as possible.
**我们与您一同翱翔于更智慧、更贴心的驾驶旅程。**
## 💭 Join our Community Forum
Join the official sunnypilot community forum to stay up to date with all the latest features and be a part of shaping the future of sunnypilot!
* https://community.sunnypilot.ai/
## **👋 嘿,朋友,欢迎您到来!**
## Documentation
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
`dragonpilot` 诞生于 2019 年,由三位早期的 openpilot 华人玩家共同创立。初衷很简单:为广大的华人用户、玩家们提供一个友善的交流环境、更简便的设定协助,并加入更多适合在地使用的贴心功能。
## 🚘 Running on a dedicated device in a car
First, check out this list of items you'll need to [get started](https://community.sunnypilot.ai/t/getting-started-using-sunnypilot-in-your-supported-car/251).
我们深知本地化的重要性,特别是语言的亲切感。因此,我们率先导入了完整的中文界面,让 `dragonpilot` 迅速在中文地区积累了口碑,也让华人的使用者数量在全球名列前茅。这份来自社区的的支持,是我们持续前进的最大动力。
## Installation
Next, refer to the sunnypilot community forum for [installation instructions](https://community.sunnypilot.ai/t/read-before-installing-sunnypilot/254), as well as a complete list of [Recommended Branch Installations](https://community.sunnypilot.ai/t/recommended-branch-installations/235).
我们以功能强大的 [openpilot](https://github.com/commaai/openpilot) 为基础——这套据美国消费者报告评测优于市售车方案的开源辅助驾驶系统——融入了更多本地化的巧思与定制化的温度,希望能打造出最符合您需求的驾驶伙伴。(您也可以参考我们 repo 中保留的 [openpilot 原始说明文件](README_OPENPILOT.md)
## 🎆 Pull Requests
We welcome both pull requests and issues on GitHub. Bug fixes are encouraged.
取名 `dragonpilot`,是因为我们希望它能像神话中的「龙」一样,既强大又充满智慧,为您的行车安全保驾护航。龙,在我们华人文化中,更是吉祥与力量的象征,也代表着我们的根源与骄傲。
Pull requests should be against the most current `master` branch.
## **✨ dragonpilot 的里程碑**
## 📊 User Data
我们不仅保留了 openpilot 的核心优势,更达成了许多从社区反馈中诞生的里程碑,这些是我们引以为傲的足迹:
By default, sunnypilot uploads the driving data to comma servers. You can also access your data through [comma connect](https://connect.comma.ai/).
* **🚘 全时居中车道保持 (ALKA)**
sunnypilot is open source software. The user is free to disable data collection if they wish to do so.
这不只是一个功能,更是 `dragonpilot` 的哲学。我们最早于 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 时便实现了这个功能,其开发历程始于 2017 Lexus IS300h,接着扩展至 Toyota 全车系,并逐步延伸到其他支持的品牌。它能温柔地辅助您,让车辆始终稳定地保持在车道中央,提供一份额外的安心与从容。
sunnypilot logs the road-facing camera, 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 this software, 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.
在官方 openpilot 还未支持前,我们便已将多语言界面实现。`dragonpilot` 完整支持繁体中文、简体中文与英文,让操作毫无隔阂。
## Licensing
* **💻 唯一同时支持多硬件平台**
sunnypilot is released under the [MIT License](LICENSE). This repository includes original work as well as significant portions of code derived from [openpilot by comma.ai](https://github.com/commaai/openpilot), which is also released under the MIT license with additional disclaimers.
我们是唯一曾致力于让项目同时兼容 EON、comma two、comma 3 与 Jetson 平台的社区分支,这份努力是为了服务最广大的玩家社群。
此外,在 comma.ai 团队于 0.10.0 版本宣布停止支持 comma 3 后,我们仍是唯一一个完整同时支持 comma 3、comma 3X 以及 O3、O3L、O3XL(O3 系列为第三方硬件)的社区分支。
The original openpilot license notice, including comma.ais indemnification and alpha software disclaimer, is reproduced below as required:
* **📜 曾荣获官方认证第一大分支**
> 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.**
基于活跃的社区与功能创新,`dragonpilot` 曾一度成长为 comma ai 官方认证的第一大 openpilot 分支,这份荣耀属于每一位参与者。
For full license terms, please see the [`LICENSE`](LICENSE) file.
## **🧑‍💻 设计理念 - 少即是多 (Less is More)**
## 💰 Support sunnypilot
If you find any of the features useful, consider becoming a [sponsor on GitHub](https://github.com/sponsors/sunnyhaibin) to support future feature development and improvements.
随着 openpilot 的 AI 模型日益强大,许多过去需要手动微调的功能,现在都已能通过更先进的模型来实现。因此,我们现在的开发重心回归到 **「最小化修改」(minimal changes)** 的核心原则上。
我们的目标是为您提供最纯粹、最接近官方的 openpilot 驾驶感受,同时保留 `dragonpilot` 那些经过时间考验、最受社区喜爱的经典功能。我们相信,在强大的 AI 基础上,简洁即是力量。
By becoming a sponsor, you will gain access to exclusive content, early access to new features, and the opportunity to directly influence the project's development.
## **🛠️ 硬件的足迹 - 一路走来的伙伴们**
从最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社区中各式各样充满智慧的**第三方硬件(如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我们也曾探索过在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。
<h3>GitHub Sponsor</h3>
目前最新版本主要支持: **comma3 / 3X** 以及 **O3 / O3L / O3XL** 等社区硬件。
针对 EON / C1.5 / C2 等旧款硬件,最后支持的版本位于 [d2 分支](https://github.com/dragonpilot-community/dragonpilot/tree/d2)。
无论您手上是哪一款设备,都代表着您对开源驾驶辅助的一份热情。
<a href="https://github.com/sponsors/sunnyhaibin">
<img src="https://user-images.githubusercontent.com/47793918/244135584-9800acbd-69fd-4b2b-bec9-e5fa2d85c817.png" alt="Become a Sponsor" width="300" style="max-width: 100%; height: auto;">
</a>
<br>
## **🫂 加入我们,成为「寻龙者」的一份子**
<h3>PayPal</h3>
`dragonpilot` 的成长,离不开每一位用户的贡献与反馈。我们是一个以**公开、透明**为原则的温暖社区,希望在这里能与所有对 openpilot / dragonpilot 感兴趣的用户分享、交流开发与使用上的经验。
<a href="https://paypal.me/sunnyhaibin0850" target="_blank">
<img src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" alt="PayPal this" title="PayPal - The safer, easier way to pay online!" border="0" />
</a>
<br></br>
[**欢迎加入我们的 Facebook 社团进行交流!**](https://www.facebook.com/groups/930190251238639)
Your continuous love and support are greatly appreciated! Enjoy 🥰
## **❤️ 特别感谢**
`dragonpilot` 从创立至今,从未打算通过 Patreon 等平台进行任何形式的募资。我们的初衷是建立一个让大家能一起学习、一起成长的社区。It's all about fun, not money.
然而,我们仍要对那些自发性支持本项目的朋友们,致上最诚挚的感谢。正是因为有了您们的鼓励,我们才有更大的动力持续前进。
[**我们的赞助者名单**](SPONSORS.md)
### **安全声明**
`dragonpilot` 是一种驾驶**辅助**系统,并非全自动驾驶。它旨在减轻您的驾驶疲劳,提升行车安全,但驾驶人仍需时刻保持专注,并随时准备接管车辆。请务必遵守您所在地的交通法规。
**最后,再次感谢您的到来。**
**期待与您一同在智慧驾驶的道路上,乘「龙」而行!**
<span>-</span> Jason, Founder of sunnypilot
-74
View File
@@ -1,74 +0,0 @@
![](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!**
-111
View File
@@ -1,111 +0,0 @@
<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 four</a>
</h3>
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/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 four, available at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
2. **Software:** The setup procedure for the comma four 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 300+ 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 four 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
Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches:
| comma four branch | comma 3X branch | URL | description |
|------------------------|------------------------|----------------------------------------|-------------------------------------------------------------------------------------|
| `release-mici` | `release-tizi` | openpilot.comma.ai | This is openpilot's release branch. |
| `release-mici-staging` | `release-tizi-staging` | openpilot-test.comma.ai | This is the staging branch for releases. Use it to get new releases slightly early. |
| `nightly` | `nightly` | openpilot-nightly.comma.ai | This is the bleeding edge development branch. Do not expect this to be stable. |
| `nightly-dev` | `nightly-dev` | installer.comma.ai/commaai/nightly-dev | Same as nightly, but includes experimental development features for some cars. |
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/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 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, and users can disable data collection if they wish.
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>
+4
View File
@@ -1,3 +1,7 @@
Version 0.11.2 (2026-06-15)
========================
Version 0.11.1 (2026-05-18)
========================
* New driver monitoring model
+7 -16
View File
@@ -59,9 +59,6 @@ allowed_system_libs = {
"EGL", "GLESv2", "GL",
"Qt5Charts", "Qt5Core", "Qt5Gui", "Qt5Widgets",
"dl", "drm", "gbm", "m", "pthread",
# dragonpilot: comma3 multi-panda USB (selfdrive/pandad_tici) + cabana/jotpluggler need libusb.
# Upstream removed USB and dropped libusb from this whitelist; we keep aux-panda so re-allow it.
"usb-1.0",
}
def _resolve_lib(env, name):
@@ -123,7 +120,7 @@ env = Environment(
LIBPATH=[
"#common",
"#msgq_repo",
"#selfdrive/pandad_tici" if "TICI_DOS" in os.environ else "#selfdrive/pandad",
"#selfdrive/pandad",
"#rednose/helpers",
[x.LIB_DIR for x in pkgs],
],
@@ -144,7 +141,7 @@ if arch == "larch64":
env.Append(LIBPATH=[
"/usr/lib/aarch64-linux-gnu",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57"]
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
env.Append(CCFLAGS=arch_flags)
env.Append(CXXFLAGS=arch_flags)
elif arch == "Darwin":
@@ -193,10 +190,11 @@ else:
np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version')
Export('env', 'arch', 'acados')
Export('env', 'arch', 'acados', 'release')
# Setup cache dir
cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
default_cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
cache_size_limit = 4e9 if "CI" in os.environ else 2e9
CacheDir(cache_dir)
Clean(["."], cache_dir)
@@ -210,13 +208,6 @@ def prune_cache_dir(target=None, source=None, env=None):
cache_size -= os.path.getsize(f)
os.unlink(f)
# dragonpilot settings generation — runs every scons invocation, idempotent.
# Writes common/params_keys.h in place; we don't declare a target so scons
# treats it purely as a pre-build side effect.
if env.Execute('./generate_settings.py') != 0:
Exit('generate_settings.py failed')
# ********** start building stuff **********
# Build common module
@@ -240,7 +231,6 @@ Export('messaging')
# Build other submodules
SConscript(['panda/SConscript'])
SConscript(['panda_tici/SConscript'])
# Build rednose library
SConscript(['rednose/SConscript'])
@@ -256,7 +246,6 @@ if arch == "larch64":
# Build selfdrive
SConscript([
'selfdrive/pandad/SConscript',
'selfdrive/pandad_tici/SConscript',
'selfdrive/controls/lib/lateral_mpc_lib/SConscript',
'selfdrive/controls/lib/longitudinal_mpc_lib/SConscript',
'selfdrive/locationd/SConscript',
@@ -264,6 +253,8 @@ SConscript([
'selfdrive/ui/SConscript',
])
SConscript(['sunnypilot/SConscript'])
# Build desktop-only tools
if GetOption('extras') and arch != "larch64":
SConscript([
+443 -19
View File
@@ -10,42 +10,450 @@ $Cxx.namespace("cereal");
# DO rename the structs
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
struct ControlsStateExt @0x81c2f05a394cf4af {
alkaActive @0 :Bool;
struct ModularAssistiveDrivingSystem {
state @0 :ModularAssistiveDrivingSystemState;
enabled @1 :Bool;
active @2 :Bool;
available @3 :Bool;
enum ModularAssistiveDrivingSystemState {
disabled @0;
paused @1;
enabled @2;
softDisabling @3;
overriding @4;
}
}
struct CarStateExt @0xaedffd8f31e7b55d {
# dp - ALKA: lkasOn state from carstate (mirrors panda's lkas_on)
lkasOn @0 :Bool;
struct IntelligentCruiseButtonManagement {
state @0 :IntelligentCruiseButtonManagementState;
sendButton @1 :SendButtonState;
vTarget @2 :Float32;
enum IntelligentCruiseButtonManagementState {
inactive @0; # No button press or default state
preActive @1; # Pre-active state before transitioning to increasing or decreasing
increasing @2; # Increasing speed
decreasing @3; # Decreasing speed
holding @4; # Holding steady speed
}
enum SendButtonState {
none @0;
increase @1;
decrease @2;
}
}
struct ModelExt @0xf35cc4560bbf6ec2 {
leftEdgeDetected @0 :Bool;
rightEdgeDetected @1 :Bool;
# Same struct as Log.RadarState.LeadData
struct LeadData {
dRel @0 :Float32;
yRel @1 :Float32;
vRel @2 :Float32;
aRel @3 :Float32;
vLead @4 :Float32;
dPath @6 :Float32;
vLat @7 :Float32;
vLeadK @8 :Float32;
aLeadK @9 :Float32;
fcw @10 :Bool;
status @11 :Bool;
aLeadTau @12 :Float32;
modelProb @13 :Float32;
radar @14 :Bool;
radarTrackId @15 :Int32 = -1;
aLeadDEPRECATED @5 :Float32;
}
struct DashyState @0xda96579883444c35 {
# Pre-serialized JSON bytes for dashy UI
# Aggregates all topics needed by dashy into single message
json @0 :Data;
struct SelfdriveStateSP @0x81c2f05a394cf4af {
mads @0 :ModularAssistiveDrivingSystem;
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
enum AudibleAlert {
none @0;
engage @1;
disengage @2;
refuse @3;
warningSoft @4;
warningImmediate @5;
prompt @6;
promptRepeat @7;
promptDistracted @8;
# unused, these are reserved for upstream events so we don't collide
reserved9 @9;
reserved10 @10;
reserved11 @11;
reserved12 @12;
reserved13 @13;
reserved14 @14;
reserved15 @15;
reserved16 @16;
reserved17 @17;
reserved18 @18;
reserved19 @19;
reserved20 @20;
reserved21 @21;
reserved22 @22;
reserved23 @23;
reserved24 @24;
reserved25 @25;
reserved26 @26;
reserved27 @27;
reserved28 @28;
reserved29 @29;
reserved30 @30;
promptSingleLow @31;
promptSingleHigh @32;
}
}
struct CustomReserved4 @0x80ae746ee2596b11 {
struct ModelManagerSP @0xaedffd8f31e7b55d {
activeBundle @0 :ModelBundle;
selectedBundle @1 :ModelBundle;
availableBundles @2 :List(ModelBundle);
struct DownloadUri {
uri @0 :Text;
sha256 @1 :Text;
}
enum DownloadStatus {
notDownloading @0;
downloading @1;
downloaded @2;
cached @3;
failed @4;
}
struct DownloadProgress {
status @0 :DownloadStatus;
progress @1 :Float32;
eta @2 :UInt32;
}
struct Artifact {
fileName @0 :Text;
downloadUri @1 :DownloadUri;
downloadProgress @2 :DownloadProgress;
}
struct Model {
type @0 :Type;
artifact @1 :Artifact; # Main artifact
metadata @2 :Artifact; # Metadata artifact
enum Type {
supercombo @0;
navigation @1;
vision @2;
policy @3;
offPolicy @4;
onPolicy @5;
}
}
enum Runner {
snpe @0;
tinygrad @1;
stock @2;
}
struct Override {
key @0 :Text;
value @1 :Text;
}
struct ModelBundle {
index @0 :UInt32;
internalName @1 :Text;
displayName @2 :Text;
models @3 :List(Model);
status @4 :DownloadStatus;
generation @5 :UInt32;
environment @6 :Text;
runner @7 :Runner;
is20hz @8 :Bool;
ref @9 :Text;
minimumSelectorVersion @10 :UInt32;
overrides @11 :List(Override);
}
}
struct CustomReserved5 @0xa5cd762cd951a455 {
struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
dec @0 :DynamicExperimentalControl;
longitudinalPlanSource @1 :LongitudinalPlanSource;
smartCruiseControl @2 :SmartCruiseControl;
speedLimit @3 :SpeedLimit;
vTarget @4 :Float32;
aTarget @5 :Float32;
events @6 :List(OnroadEventSP.Event);
e2eAlerts @7 :E2eAlerts;
struct DynamicExperimentalControl {
state @0 :DynamicExperimentalControlState;
enabled @1 :Bool;
active @2 :Bool;
enum DynamicExperimentalControlState {
acc @0;
blended @1;
}
}
struct SmartCruiseControl {
vision @0 :Vision;
map @1 :Map;
struct Vision {
state @0 :VisionState;
vTarget @1 :Float32;
aTarget @2 :Float32;
currentLateralAccel @3 :Float32;
maxPredictedLateralAccel @4 :Float32;
enabled @5 :Bool;
active @6 :Bool;
}
struct Map {
state @0 :MapState;
vTarget @1 :Float32;
aTarget @2 :Float32;
enabled @3 :Bool;
active @4 :Bool;
}
enum VisionState {
disabled @0; # System disabled or inactive.
enabled @1; # No predicted substantial turn on vision range.
entering @2; # A substantial turn is predicted ahead, adapting speed to turn comfort levels.
turning @3; # Actively turning. Managing acceleration to provide a roll on turn feeling.
leaving @4; # Road ahead straightens. Start to allow positive acceleration.
overriding @5; # System overriding with manual control.
}
enum MapState {
disabled @0; # System disabled or inactive.
enabled @1; # No predicted substantial turn on map range.
turning @2; # Actively turning. Managing acceleration to provide a roll on turn feeling.
overriding @3; # System overriding with manual control.
}
}
struct SpeedLimit {
resolver @0 :Resolver;
assist @1 :Assist;
struct Resolver {
speedLimit @0 :Float32;
distToSpeedLimit @1 :Float32;
source @2 :Source;
speedLimitOffset @3 :Float32;
speedLimitLast @4 :Float32;
speedLimitFinal @5 :Float32;
speedLimitFinalLast @6 :Float32;
speedLimitValid @7 :Bool;
speedLimitLastValid @8 :Bool;
}
struct Assist {
state @0 :AssistState;
enabled @1 :Bool;
active @2 :Bool;
vTarget @3 :Float32;
aTarget @4 :Float32;
}
enum Source {
none @0;
car @1;
map @2;
}
enum AssistState {
disabled @0;
inactive @1; # No speed limit set or not enabled by parameter.
preActive @2;
pending @3; # Awaiting new speed limit.
adapting @4; # Reducing speed to match new speed limit.
active @5; # Cruising at speed limit.
}
}
enum LongitudinalPlanSource {
cruise @0;
sccVision @1;
sccMap @2;
speedLimitAssist @3;
}
struct E2eAlerts {
greenLightAlert @0 :Bool;
leadDepartAlert @1 :Bool;
}
}
struct CustomReserved6 @0xf98d843bfd7004a3 {
struct OnroadEventSP @0xda96579883444c35 {
events @0 :List(Event);
struct Event {
name @0 :EventName;
# event types
enable @1 :Bool;
noEntry @2 :Bool;
warning @3 :Bool; # alerts presented only when enabled or soft disabling
userDisable @4 :Bool;
softDisable @5 :Bool;
immediateDisable @6 :Bool;
preEnable @7 :Bool;
permanent @8 :Bool; # alerts presented regardless of openpilot state
overrideLateral @10 :Bool;
overrideLongitudinal @9 :Bool;
}
enum EventName {
lkasEnable @0;
lkasDisable @1;
manualSteeringRequired @2;
manualLongitudinalRequired @3;
silentLkasEnable @4;
silentLkasDisable @5;
silentBrakeHold @6;
silentWrongGear @7;
silentReverseGear @8;
silentDoorOpen @9;
silentSeatbeltNotLatched @10;
silentParkBrake @11;
controlsMismatchLateral @12;
hyundaiRadarTracksConfirmed @13;
experimentalModeSwitched @14;
wrongCarModeAlertOnly @15;
pedalPressedAlertOnly @16;
laneTurnLeft @17;
laneTurnRight @18;
speedLimitPreActive @19;
speedLimitActive @20;
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
}
}
struct CustomReserved7 @0xb86e6369214c01c8 {
struct CarParamsSP @0x80ae746ee2596b11 {
flags @0 :UInt32; # flags for car specific quirks in sunnypilot
safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags
pcmCruiseSpeed @3 :Bool;
intelligentCruiseButtonManagementAvailable @4 :Bool;
enableGasInterceptor @5 :Bool;
neuralNetworkLateralControl @2 :NeuralNetworkLateralControl;
struct NeuralNetworkLateralControl {
model @0 :Model;
fuzzyFingerprint @1 :Bool;
struct Model {
path @0 :Text;
name @1 :Text;
}
}
}
struct CustomReserved8 @0xf416ec09499d9d19 {
struct CarControlSP @0xa5cd762cd951a455 {
mads @0 :ModularAssistiveDrivingSystem;
params @1 :List(Param);
leadOne @2 :LeadData;
leadTwo @3 :LeadData;
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
struct Param {
key @0 :Text;
type @2 :ParamType;
value @3 :Data;
valueDEPRECATED @1 :Text; # The data type change may cause issues with backwards compatibility.
}
enum ParamType {
string @0;
bool @1;
int @2;
float @3;
time @4;
json @5;
bytes @6;
}
}
struct CustomReserved9 @0xa1680744031fdb2d {
struct BackupManagerSP @0xf98d843bfd7004a3 {
backupStatus @0 :Status;
restoreStatus @1 :Status;
backupProgress @2 :Float32;
restoreProgress @3 :Float32;
lastError @4 :Text;
currentBackup @5 :BackupInfo;
backupHistory @6 :List(BackupInfo);
enum Status {
idle @0;
inProgress @1;
completed @2;
failed @3;
}
struct Version {
major @0 :UInt16;
minor @1 :UInt16;
patch @2 :UInt16;
build @3 :UInt16;
branch @4 :Text;
}
struct MetadataEntry {
key @0 :Text;
value @1 :Text;
tags @2 :List(Text);
}
struct BackupInfo {
deviceId @0 :Text;
version @1 :UInt32;
config @2 :Text;
isEncrypted @3 :Bool;
createdAt @4 :Text; # ISO timestamp
updatedAt @5 :Text; # ISO timestamp
sunnypilotVersion @6 :Version;
backupMetadata @7 :List(MetadataEntry);
}
}
struct CarStateSP @0xb86e6369214c01c8 {
speedLimit @0 :Float32;
}
struct LiveMapDataSP @0xf416ec09499d9d19 {
speedLimitValid @0 :Bool;
speedLimit @1 :Float32;
speedLimitAheadValid @2 :Bool;
speedLimitAhead @3 :Float32;
speedLimitAheadDistance @4 :Float32;
roadName @5 :Text;
}
struct ModelDataV2SP @0xa1680744031fdb2d {
laneTurnDirection @0 :TurnDirection;
enum TurnDirection {
none @0;
turnLeft @1;
turnRight @2;
}
}
struct CustomReserved10 @0xcb9fd56c7057593a {
@@ -75,5 +483,21 @@ struct CustomReserved17 @0xa30662f84033036c {
struct CustomReserved18 @0xc86a3d38d13eb3ef {
}
struct CustomReserved19 @0xa4f1eb3323f5f582 {
struct LiveCurvatureParameters @0xa4f1eb3323f5f582 {
liveValid @0 :Bool;
version @1 :Int32;
useParams @2 :Bool;
currentCorrection @3 :Float32;
currentBias @4 :Float32;
currentBucketPoints @5 :UInt16;
totalBucketPoints @6 :UInt16;
calPerc @7 :Int8;
bucketSpeed @8 :Int8;
corrections @9 :List(Float32);
counts @10 :List(UInt16);
biases @11 :List(Float32);
bucketCurvature @12 :Int8;
fitValid @13 :List(Bool);
previewCorrections @14 :List(Float32);
previewValid @15 :List(Bool);
}
-12
View File
@@ -760,18 +760,6 @@ struct LateralLQRState @0x9024e2d790c82ade {
steeringAngleDesiredDeg @6 :Float32;
}
struct LateralCurvatureState @0xad9d8095c06f7c61 {
active @0 :Bool;
actualCurvature @1 :Float32;
desiredCurvature @2 :Float32;
error @3 :Float32;
p @4 :Float32;
i @5 :Float32;
f @6 :Float32;
output @7 :Float32;
saturated @8 :Bool;
}
struct LateralPlannerSolution @0x84caeca5a6b4acfe {
x @0 :List(Float32);
y @1 :List(Float32);
+33 -15
View File
@@ -132,6 +132,9 @@ struct OnroadEvent @0xc4fa6047f024e718 {
userBookmark @95;
excessiveActuation @96;
audioFeedback @97;
dashcamModeRadDisEngOn @100;
radarDisableFailed @101;
steerFaultWarning @102;
soundsUnavailableDEPRECATED @47;
}
@@ -561,8 +564,8 @@ struct PandaState @0xa7649e2575e4591e {
# these fields are not used by openpilot, but they're
# reserved for forks building alternate experiences.
controlsAllowedRESERVED1 @38 :Bool;
controlsAllowedRESERVED2 @39 :Bool;
controlsAllowedLateral @38 :Bool;
controlsAllowedLongitudinal @39 :Bool;
enum FaultStatus {
none @0;
@@ -809,6 +812,7 @@ struct ControlsState @0x97ff69c53601abf1 {
uiAccelCmd @5 :Float32;
ufAccelCmd @33 :Float32;
curvature @37 :Float32; # path curvature from vehicle model
modelDesiredCurvature @67 :Float32; # raw desired curvature from modelV2 before smoothing/adaptation
desiredCurvature @61 :Float32; # lag adjusted curvatures used by lateral controllers
forceDecel @51 :Bool;
@@ -818,10 +822,22 @@ struct ControlsState @0x97ff69c53601abf1 {
debugState @59 :LateralDebugState;
torqueState @60 :LateralTorqueState;
curvatureStateDEPRECATED @65 :Deprecated.LateralCurvatureState;
curvatureStateDEPRECATED @65 :LateralCurvatureState;
lqrStateDEPRECATED @55 :Deprecated.LateralLQRState;
indiStateDEPRECATED @52 :Deprecated.LateralINDIState;
}
struct LateralCurvatureState @0xad9d8095c06f7c61 {
active @0 :Bool;
actualCurvature @1 :Float32;
desiredCurvature @2 :Float32;
error @3 :Float32;
p @4 :Float32;
i @5 :Float32;
f @6 :Float32;
output @7 :Float32;
saturated @8 :Bool;
}
struct LateralPIDState {
active @0 :Bool;
@@ -1145,6 +1161,7 @@ struct LateralManeuverPlan {
struct LongitudinalPlan @0xe00b5b3eba12876c {
modelMonoTime @9 :UInt64;
hasLead @7 :Bool;
leadDistance @40 :Float32;
fcw @8 :Bool;
longitudinalPlanSource @15 :LongitudinalPlanSource;
processingDelay @29 :Float32;
@@ -1154,6 +1171,7 @@ struct LongitudinalPlan @0xe00b5b3eba12876c {
speeds @33 :List(Float32);
jerks @34 :List(Float32);
aTarget @18 :Float32;
vTarget @41 :Float32;
shouldStop @37: Bool;
allowThrottle @38: Bool;
allowBrake @39: Bool;
@@ -2548,16 +2566,16 @@ struct Event {
# DO change the name of the field and struct
# DON'T change the ID (e.g. @107)
# DON'T change which struct it points to
controlsStateExt @107 :Custom.ControlsStateExt;
carStateExt @108 :Custom.CarStateExt;
modelExt @109 :Custom.ModelExt;
dashyState @110 :Custom.DashyState;
customReserved4 @111 :Custom.CustomReserved4;
customReserved5 @112 :Custom.CustomReserved5;
customReserved6 @113 :Custom.CustomReserved6;
customReserved7 @114 :Custom.CustomReserved7;
customReserved8 @115 :Custom.CustomReserved8;
customReserved9 @116 :Custom.CustomReserved9;
selfdriveStateSP @107 :Custom.SelfdriveStateSP;
modelManagerSP @108 :Custom.ModelManagerSP;
longitudinalPlanSP @109 :Custom.LongitudinalPlanSP;
onroadEventsSP @110 :Custom.OnroadEventSP;
carParamsSP @111 :Custom.CarParamsSP;
carControlSP @112 :Custom.CarControlSP;
backupManagerSP @113 :Custom.BackupManagerSP;
carStateSP @114 :Custom.CarStateSP;
liveMapDataSP @115 :Custom.LiveMapDataSP;
modelDataV2SP @116 :Custom.ModelDataV2SP;
customReserved10 @136 :Custom.CustomReserved10;
customReserved11 @137 :Custom.CustomReserved11;
customReserved12 @138 :Custom.CustomReserved12;
@@ -2567,7 +2585,7 @@ struct Event {
customReserved16 @142 :Custom.CustomReserved16;
customReserved17 @143 :Custom.CustomReserved17;
customReserved18 @144 :Custom.CustomReserved18;
customReserved19 @145 :Custom.CustomReserved19;
liveCurvatureParameters @145 :Custom.LiveCurvatureParameters;
# *********** legacy + deprecated ***********
model @9 :Deprecated.ModelData; # TODO: rename modelV2 and mark this as deprecated
@@ -2610,7 +2628,7 @@ struct Event {
lateralPlanDEPRECATED @64 :LateralPlan;
navModelDEPRECATED @104 :Deprecated.NavModelData;
uiPlanDEPRECATED @106 :UiPlan;
liveLocationKalmanDEPRECATED @72 :LiveLocationKalman;
liveLocationKalman @72 :LiveLocationKalman;
liveTracksDEPRECATED @16 :List(Deprecated.LiveTracksDEPRECATED);
onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED);
gyroscope2DEPRECATED @100 :SensorEventData;
+6 -10
View File
@@ -40,16 +40,12 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event
def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
valid = kwargs.pop('valid', False)
log_mono_time = kwargs.pop('logMonoTime', int(time.monotonic() * 1e9))
# pycapnp 2.2.x's kwargs/from_dict path creates cyclic garbage here. Realtime processes disable GC.
dat = log.Event.new_message()
dat.valid = valid
dat.logMonoTime = log_mono_time
for field, value in kwargs.items():
setattr(dat, field, value)
args = {
'valid': False,
'logMonoTime': int(time.monotonic() * 1e9),
**kwargs
}
dat = log.Event.new_message(**args)
if service is not None:
if size is None:
dat.init(service)
+1 -1
View File
@@ -30,7 +30,7 @@ def zmq_sleep(t=1):
# TODO: this should take any capnp struct and returrn a msg with random populated data
def random_carstate():
fields = ["vEgo", "aEgo", "brake", "steeringAngleDeg"]
fields = ["vEgo", "aEgo", "steeringTorque", "steeringAngleDeg"]
msg = messaging.new_message("carState")
cs = msg.carState
for f in fields:
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Schema-level cereal compat check between sunnypilot and upstream openpilot.
Rules (per struct matched across sides by typeId):
R1 shared ordinal must reference the same type.
R2 sunnypilot-only ordinal in a union -> FAIL (unknown discriminant upstream).
R3 sunnypilot-only ordinal on a regular field -> OK (additive struct evolution).
R4 upstream-only ordinal -> OK.
R5 sunnypilot-only struct referenced via an upstream-shared field -> FAIL.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any
NO_DISCRIMINANT = 0xFFFF
def hex_id(value: int) -> str:
return f"0x{value:016x}"
def encode_type(type_node: Any) -> dict:
which = type_node.which()
if which == "struct":
return {"kind": "struct", "typeId": hex_id(type_node.struct.typeId)}
if which == "enum":
return {"kind": "enum", "typeId": hex_id(type_node.enum.typeId)}
if which == "interface":
return {"kind": "interface", "typeId": hex_id(type_node.interface.typeId)}
if which == "list":
return {"kind": "list", "element": encode_type(type_node.list.elementType)}
if which == "anyPointer":
return {"kind": "anyPointer"}
return {"kind": which}
def encode_field(name: str, field: Any) -> dict:
proto = field.proto
ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None
discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None
if proto.which() == "group":
type_desc = {"kind": "group", "typeId": hex_id(proto.group.typeId)}
else:
type_desc = encode_type(proto.slot.type)
return {
"name": name,
"ordinal": ordinal,
"discriminant": discriminant,
"type": type_desc,
}
def encode_struct(schema: Any) -> dict:
node = schema.node
return {
"typeId": hex_id(node.id),
"displayName": node.displayName,
"hasUnion": node.struct.discriminantCount > 0,
"fields": [encode_field(name, field) for name, field in schema.fields.items()],
}
def _child_struct_schema(field: Any) -> Any:
proto = field.proto
if proto.which() == "group":
return field.schema
type_node = proto.slot.type
which = type_node.which()
if which == "struct":
return field.schema
if which == "list":
container = field.schema
element_type = type_node.list.elementType
while element_type.which() == "list":
container = container.elementType
element_type = element_type.list.elementType
if element_type.which() == "struct":
return container.elementType
return None
def collect_schema(root: Any) -> dict[str, dict]:
structs: dict[str, dict] = {}
stack = [root]
while stack:
schema = stack.pop()
type_id = hex_id(schema.node.id)
if type_id in structs:
continue
structs[type_id] = encode_struct(schema)
for _name, field in schema.fields.items():
try:
child = _child_struct_schema(field)
except Exception:
child = None
if child is not None:
stack.append(child)
return structs
def load_log(cereal_dir: str) -> Any:
import capnp
cereal_dir = os.path.abspath(cereal_dir)
capnp.remove_import_hook()
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
def dump_schema(cereal_dir: str, path: str) -> None:
log = load_log(cereal_dir)
payload = {
"root": hex_id(log.Event.schema.node.id),
"structs": collect_schema(log.Event.schema),
}
with open(path, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2, sort_keys=True)
print(f"wrote schema dump with {len(payload['structs'])} structs to {path}")
def types_equal(a: dict, b: dict) -> bool:
if a.get("kind") != b.get("kind"):
return False
kind = a["kind"]
if kind in ("struct", "enum", "interface", "group"):
return a.get("typeId") == b.get("typeId")
if kind == "list":
return types_equal(a["element"], b["element"])
return True
def type_repr(t: dict) -> str:
kind = t.get("kind", "?")
if kind in ("struct", "enum", "interface", "group"):
return f"{kind}({t.get('typeId')})"
if kind == "list":
return f"list<{type_repr(t['element'])}>"
return kind
def field_is_union_variant(field: dict) -> bool:
return field.get("discriminant") is not None
def index_fields_by_ordinal(struct: dict) -> dict[int, dict]:
indexed: dict[int, dict] = {}
for field in struct["fields"]:
ordinal = field.get("ordinal")
if ordinal is None:
continue
indexed[ordinal] = field
return indexed
def compare(sunnypilot_dump: dict, upstream_dump: dict) -> list[str]:
violations: list[str] = []
sunnypilot_structs: dict[str, dict] = sunnypilot_dump["structs"]
upstream_structs: dict[str, dict] = upstream_dump["structs"]
sunnypilot_struct_referenced_from_shared: set[str] = set()
for type_id, sunnypilot_struct in sunnypilot_structs.items():
upstream_struct = upstream_structs.get(type_id)
if upstream_struct is None:
continue
sunnypilot_fields = index_fields_by_ordinal(sunnypilot_struct)
upstream_fields = index_fields_by_ordinal(upstream_struct)
display = sunnypilot_struct["displayName"]
for ordinal, sunnypilot_field in sunnypilot_fields.items():
upstream_field = upstream_fields.get(ordinal)
if upstream_field is None:
if field_is_union_variant(sunnypilot_field):
violations.append(
f"[R2] {display} @{ordinal} ('{sunnypilot_field['name']}', {type_repr(sunnypilot_field['type'])}): "
f"union variant not present upstream. upstream cannot parse this discriminant."
)
continue
if not types_equal(sunnypilot_field["type"], upstream_field["type"]):
violations.append(
f"[R1] {display} @{ordinal}: type mismatch. "
f"sunnypilot='{sunnypilot_field['name']}' {type_repr(sunnypilot_field['type'])} vs "
f"upstream='{upstream_field['name']}' {type_repr(upstream_field['type'])}."
)
continue
cursor = sunnypilot_field["type"]
while cursor.get("kind") == "list":
cursor = cursor["element"]
if cursor.get("kind") in ("struct", "group", "interface") and cursor.get("typeId"):
sunnypilot_struct_referenced_from_shared.add(cursor["typeId"])
for type_id, sunnypilot_struct in sunnypilot_structs.items():
if type_id in upstream_structs:
continue
if type_id in sunnypilot_struct_referenced_from_shared:
violations.append(
f"[R5] struct {sunnypilot_struct['displayName']} ({type_id}) exists only on sunnypilot "
f"but is referenced from an upstream-shared field. upstream cannot resolve this type."
)
return violations
def load_peer(path: str) -> dict:
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def run_read(cereal_dir: str, peer_path: str) -> int:
log = load_log(cereal_dir)
peer_dump = load_peer(peer_path)
local_dump = {
"root": hex_id(log.Event.schema.node.id),
"structs": collect_schema(log.Event.schema),
}
violations = compare(sunnypilot_dump=peer_dump, upstream_dump=local_dump)
if not violations:
print("cereal compat OK: upstream openpilot can parse sunnypilot routes "
"(no leaked structs, no ordinal collisions).")
return 0
print(f"cereal compat FAIL: upstream openpilot would misparse sunnypilot routes "
f"({len(violations)} violation(s)):")
for v in violations:
print(f" {v}")
return 1
def main() -> int:
parser = argparse.ArgumentParser(
description="sunnypilot <-> upstream cereal compatibility validator (schema-level)."
)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
args = parser.parse_args()
if args.generate:
dump_schema(args.cereal_dir, args.file)
return 0
return run_read(args.cereal_dir, args.file)
if __name__ == "__main__":
sys.exit(main())
+16 -4
View File
@@ -82,6 +82,22 @@ _services: dict[str, tuple] = {
"wideRoadEncodeData": (False, 20., None, QueueSize.BIG),
"qRoadEncodeData": (False, 20., None, QueueSize.BIG),
# sunnypilot
"modelManagerSP": (False, 1., 1, QueueSize.BIG),
"backupManagerSP": (False, 1., 1, QueueSize.BIG),
"selfdriveStateSP": (True, 100., 10),
"longitudinalPlanSP": (True, 20., 10),
"onroadEventsSP": (True, 1., 1),
"carParamsSP": (True, 0.02, 1),
"carControlSP": (True, 100., 10),
"carStateSP": (True, 100., 10),
"liveMapDataSP": (True, 1., 1),
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
"liveLocationKalman": (True, 20.),
# infiniteCable
"liveCurvatureParameters": (True, 4., 1),
# debug
"uiDebug": (True, 0., 1),
"testJoystick": (True, 0.),
@@ -93,10 +109,6 @@ _services: dict[str, tuple] = {
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
"customReservedRawData0": (True, 0.),
"controlsStateExt": (True, 100.),
"carStateExt": (True, 100.),
"modelExt": (True, 20.),
"dashyState": (True, 0.), # Aggregated dashy UI state (optional)
}
SERVICE_LIST = {name: Service(*vals) for
idx, (name, vals) in enumerate(_services.items())}
-62
View File
@@ -1,62 +0,0 @@
import jwt
import os
import requests
from datetime import datetime, timedelta, UTC
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
# name: jwt signature algorithm
KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"}
class Api:
def __init__(self, dongle_id):
self.dongle_id = dongle_id
self.jwt_algorithm, self.private_key, _ = get_key_pair()
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self.request('POST', *args, **kwargs)
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
def get_token(self, payload_extra=None, expiry_hours=1):
now = datetime.now(UTC).replace(tzinfo=None)
payload = {
'identity': self.dongle_id,
'nbf': now,
'iat': now,
'exp': now + timedelta(hours=expiry_hours)
}
if payload_extra is not None:
payload.update(payload_extra)
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
return token
def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params):
headers = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
headers['User-Agent'] = "openpilot-" + get_version()
# TODO: add session to Api
req = requests if session is None else session
return req.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params)
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
for key in KEYS:
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
return KEYS[key], private.read(), public.read()
return None, None, None
+26
View File
@@ -0,0 +1,26 @@
from openpilot.common.api.comma_connect import CommaConnectApi
class Api:
def __init__(self, dongle_id):
self.service = CommaConnectApi(dongle_id)
def request(self, method, endpoint, **params):
return self.service.request(method, endpoint, **params)
def get(self, *args, **kwargs):
return self.service.get(*args, **kwargs)
def post(self, *args, **kwargs):
return self.service.post(*args, **kwargs)
def get_token(self, payload_extra=None, expiry_hours=1):
return self.service.get_token(payload_extra, expiry_hours)
def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params):
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, session, **params)
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
return CommaConnectApi(None).get_key_pair()
+72
View File
@@ -0,0 +1,72 @@
import jwt
import os
import requests
import unicodedata
from datetime import datetime, timedelta, UTC
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
# name: jwt signature algorithm
KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"}
class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id
self.api_host = api_host
self.user_agent = user_agent
self.jwt_algorithm, self.private_key, _ = self.get_key_pair()
def get(self, *args, **kwargs):
return self.request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self.request('POST', *args, **kwargs)
def request(self, method, endpoint, timeout=None, access_token=None, **params):
return self.api_get(endpoint, method=method, timeout=timeout, access_token=access_token, **params)
def _get_token(self, payload_extra=None, expiry_hours=1, **extra_payload):
now = datetime.now(UTC).replace(tzinfo=None)
payload = {
'identity': self.dongle_id,
'nbf': now,
'iat': now,
'exp': now + timedelta(hours=expiry_hours),
**extra_payload
}
if payload_extra is not None:
payload.update(payload_extra)
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
return token
def get_token(self, payload_extra=None, expiry_hours=1):
return self._get_token(payload_extra, expiry_hours)
def remove_non_ascii_chars(self, text):
normalized_text = unicodedata.normalize('NFD', text)
ascii_encoded_text = normalized_text.encode('ascii', 'ignore')
return ascii_encoded_text.decode()
def api_get(self, endpoint, method='GET', timeout=None, access_token=None, session=None, json=None, **params):
headers = {}
if access_token is not None:
headers['Authorization'] = "JWT " + access_token
version = self.remove_non_ascii_chars(get_version())
headers['User-Agent'] = self.user_agent + version
# TODO: add session to Api
req = requests if session is None else session
return req.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
@staticmethod
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
for key in KEYS:
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
return KEYS[key], private.read(), public.read()
return None, None, None
+11
View File
@@ -0,0 +1,11 @@
import os
from openpilot.common.api.base import BaseApi
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
class CommaConnectApi(BaseApi):
def __init__(self, dongle_id):
super().__init__(dongle_id, API_HOST)
self.user_agent = "openpilot-"
+8 -1
View File
@@ -1,8 +1,13 @@
import re
import sys
import pytest
import inspect
def _to_safe_name(s):
return re.sub(r"[^a-zA-Z0-9_]+", "_", str(s)).strip("_")
class parameterized:
@staticmethod
def expand(cases):
@@ -34,7 +39,9 @@ def parameterized_class(attrs, input_list=None):
def decorator(cls):
globs = sys._getframe(1).f_globals
for i, params in enumerate(params_list):
name = f"{cls.__name__}_{i}"
# append sanitized string param values so pytest -k can filter by them
suffix = "_".join(filter(None, (_to_safe_name(v) for v in params.values() if isinstance(v, str))))
name = f"{cls.__name__}_{i}" + (f"_{suffix}" if suffix else "")
new_cls = type(name, (cls,), dict(params))
new_cls.__module__ = cls.__module__
new_cls.__test__ = True # override inherited False so pytest collects this subclass
+4 -2
View File
@@ -103,10 +103,12 @@ Params::~Params() {
assert(queue.empty());
}
std::vector<std::string> Params::allKeys() const {
std::vector<std::string> Params::allKeys(ParamKeyFlag flag) const {
std::vector<std::string> ret;
for (auto &p : keys) {
ret.push_back(p.first);
if (flag == ALL || (p.second.flags & flag)) {
ret.push_back(p.first);
}
}
return ret;
}
+2 -1
View File
@@ -18,6 +18,7 @@ enum ParamKeyFlag {
DONT_LOG = 0x20,
DEVELOPMENT_ONLY = 0x40,
CLEAR_ON_IGNITION_ON = 0x80,
BACKUP = 0x100,
ALL = 0xFFFFFFFF
};
@@ -45,7 +46,7 @@ public:
Params(const Params&) = delete;
Params& operator=(const Params&) = delete;
std::vector<std::string> allKeys() const;
std::vector<std::string> allKeys(ParamKeyFlag flag = ALL) const;
bool checkKey(const std::string &key);
ParamKeyFlag getKeyFlag(const std::string &key);
ParamKeyType getKeyType(const std::string &key);
+193 -30
View File
@@ -2,13 +2,12 @@
#include <string>
#include <unordered_map>
#include "cereal/gen/cpp/log.capnp.h"
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
{"AdbEnabled", {PERSISTENT, BOOL}},
{"AlwaysOnDM", {PERSISTENT, BOOL}},
{"AdbEnabled", {PERSISTENT | BACKUP, BOOL}},
{"AlwaysOnDM", {PERSISTENT | BACKUP, BOOL}},
{"ApiCache_Device", {PERSISTENT, STRING}},
{"ApiCache_FirehoseStats", {PERSISTENT, JSON}},
{"AssistNowToken", {PERSISTENT, STRING}},
@@ -28,37 +27,37 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> 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", {PERSISTENT, BOOL, "0"}},
{"DisablePowerDown", {PERSISTENT, BOOL}},
{"DisableUpdates", {PERSISTENT, BOOL}},
{"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}},
{"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}},
{"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}},
{"DisengageOnAccelerator", {PERSISTENT | BACKUP, BOOL, "0"}},
{"DongleId", {PERSISTENT, STRING}},
{"DoReboot", {CLEAR_ON_MANAGER_START, BOOL}},
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY, BOOL}},
{"ExperimentalMode", {PERSISTENT, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT, BOOL}},
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}},
{"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}},
{"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ForcePowerDown", {PERSISTENT, BOOL}},
{"GitBranch", {PERSISTENT, STRING}},
{"GitCommit", {PERSISTENT, STRING}},
{"GitCommitDate", {PERSISTENT, STRING}},
{"GitDiff", {PERSISTENT, STRING}},
{"GithubSshKeys", {PERSISTENT, STRING}},
{"GithubUsername", {PERSISTENT, STRING}},
{"GithubSshKeys", {PERSISTENT | BACKUP, STRING}},
{"GithubUsername", {PERSISTENT | BACKUP, STRING}},
{"GitRemote", {PERSISTENT, STRING}},
{"GsmApn", {PERSISTENT, STRING}},
{"GsmMetered", {PERSISTENT, BOOL, "1"}},
{"GsmRoaming", {PERSISTENT, BOOL}},
{"GsmApn", {PERSISTENT | BACKUP, STRING}},
{"GsmMetered", {PERSISTENT | BACKUP, BOOL, "1"}},
{"GsmRoaming", {PERSISTENT | BACKUP, BOOL}},
{"HardwareSerial", {PERSISTENT, STRING}},
{"HasAcceptedTerms", {PERSISTENT, STRING, "0"}},
{"InstallDate", {PERSISTENT, TIME}},
{"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsEngaged", {PERSISTENT, BOOL}},
{"IsLdwEnabled", {PERSISTENT, BOOL}},
{"IsMetric", {PERSISTENT, BOOL}},
{"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}},
{"IsMetric", {PERSISTENT | BACKUP, BOOL}},
{"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsOnroad", {PERSISTENT, BOOL}},
{"IsRhdDetected", {PERSISTENT, BOOL}},
@@ -66,7 +65,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LanguageSetting", {PERSISTENT, STRING, "en"}},
{"LanguageSetting", {PERSISTENT | BACKUP, STRING, "en"}},
{"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"LastGPSPosition", {PERSISTENT, STRING}},
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
@@ -77,15 +76,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
{"LastUpdateTime", {PERSISTENT, TIME}},
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"LiveDelay", {PERSISTENT, BYTES}},
{"LiveDelay", {PERSISTENT | BACKUP, BYTES}},
{"LiveParameters", {PERSISTENT, JSON}},
{"LiveParametersV2", {PERSISTENT, BYTES}},
{"LivestreamEncoderBitrate", {CLEAR_ON_MANAGER_START | DONT_LOG, INT}},
{"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}},
{"LocationFilterInitialState", {PERSISTENT, BYTES}},
{"LateralManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"LongitudinalPersonality", {PERSISTENT, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
{"NetworkMetered", {PERSISTENT, BOOL}},
{"LongitudinalPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPersonality::STANDARD))}},
{"NetworkMetered", {PERSISTENT | BACKUP, BOOL}},
{"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
@@ -101,18 +101,21 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_DriverMonitoringUncertain", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}},
{"OpenpilotEnabledToggle", {PERSISTENT, BOOL, "1"}},
{"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"PrimeType", {PERSISTENT, INT}},
{"RecordAudio", {PERSISTENT, BOOL}},
{"RecordAudioFeedback", {PERSISTENT, BOOL, "0"}},
{"RecordFront", {PERSISTENT, BOOL}},
{"RecordAudio", {PERSISTENT | BACKUP, BOOL}},
{"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RecordFront", {PERSISTENT | BACKUP, BOOL}},
{"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet
{"SecOCKey", {PERSISTENT | DONT_LOG, STRING}},
{"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}},
{"ShowDebugInfo", {PERSISTENT, BOOL}},
{"RouteCount", {PERSISTENT, INT, "0"}},
{"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"SshEnabled", {PERSISTENT, BOOL}},
{"SshEnabled", {PERSISTENT | BACKUP, BOOL}},
{"TermsVersion", {PERSISTENT, STRING}},
{"TorqueBar", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrainingVersion", {PERSISTENT, STRING}},
{"UbloxAvailable", {PERSISTENT, BOOL}},
{"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}},
@@ -130,7 +133,167 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
{"Version", {PERSISTENT, STRING}},
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
{"dp_dev_go_off_road", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
// --- infiniteCable params --- //
{"DisableScreenTimer", {PERSISTENT, BOOL}},
{"DarkMode", {PERSISTENT, BOOL}},
{"EnableCurvatureController", {PERSISTENT, BOOL, "1"}},
{"EnableCurvatureD", {PERSISTENT, BOOL, "0"}},
{"CurvatureDDebugData", {PERSISTENT, BOOL, "0"}},
{"EnableLongComfortMode", {PERSISTENT, BOOL}},
{"EnableSmoothSteer", {PERSISTENT, BOOL}},
{"EnableSpeedLimitControl", {PERSISTENT, BOOL}},
{"EnableSpeedLimitPredicative", {PERSISTENT, BOOL}},
{"EnableSLPredReactToSL", {PERSISTENT, BOOL}},
{"EnableSLPredReactToCurves", {PERSISTENT, BOOL}},
{"BatteryDetails", {PERSISTENT, BOOL}},
{"ShowDynamicSteeringLearnerGraph", {PERSISTENT, BOOL}},
{"ForceRHDForBSM", {PERSISTENT, BOOL}},
{"DisableCarSteerAlerts", {PERSISTENT, BOOL}},
{"ShowAccelBar", {PERSISTENT, BOOL}},
{"LiveCurvatureParameters", {PERSISTENT | DONT_LOG, BYTES}},
// --- sunnypilot params --- //
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
{"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h
{"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}},
{"Brightness", {PERSISTENT | BACKUP, INT, "0"}},
{"CarList", {PERSISTENT, JSON}},
{"CarParamsSP", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}},
{"CarParamsSPCache", {CLEAR_ON_MANAGER_START, BYTES}},
{"CarParamsSPPersistent", {PERSISTENT, BYTES}},
{"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}},
{"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}},
{"CompletedSunnylinkConsentVersion", {PERSISTENT, STRING, "0"}},
{"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}},
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
{"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}},
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
{"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}},
{"OnroadScreenOffBrightness", {PERSISTENT | BACKUP, INT, "0"}},
{"OnroadScreenOffBrightnessMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadScreenOffTimer", {PERSISTENT | BACKUP, INT, "15"}},
{"OnroadScreenOffTimerMigrated", {PERSISTENT | BACKUP, STRING, "0.0"}},
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
// MADS params
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
{"MadsSteeringMode", {PERSISTENT | BACKUP, INT, "0"}},
{"MadsUnifiedEngagementMode", {PERSISTENT | BACKUP, BOOL, "1"}},
// Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
// sunnylink params
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
{"ParamsVersion", {PERSISTENT, INT}},
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
{"SunnylinkDongleId", {PERSISTENT, STRING}},
{"SunnylinkdPid", {PERSISTENT, INT}},
{"SunnylinkEnabled", {PERSISTENT, BOOL, "1"}},
{"SunnylinkTempFault", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL, "0"}},
// Backup Manager params
{"BackupManager_CreateBackup", {PERSISTENT, BOOL}},
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
// sunnypilot car specific params
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
// sunnypilot model params
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
// mapd
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
{"MapdVersion", {PERSISTENT, STRING}},
{"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}},
{"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}},
{"OsmDbUpdatesCheck", {CLEAR_ON_MANAGER_START, BOOL}}, // mapd database update happens with device ON, reset on boot
{"OSMDownloadBounds", {PERSISTENT, STRING}},
{"OsmDownloadedDate", {PERSISTENT, STRING, "0.0"}},
{"OSMDownloadLocations", {PERSISTENT, JSON}},
{"OSMDownloadProgress", {CLEAR_ON_MANAGER_START, JSON}},
{"OsmLocal", {PERSISTENT, BOOL}},
{"OsmLocationName", {PERSISTENT, STRING}},
{"OsmLocationTitle", {PERSISTENT, STRING}},
{"OsmLocationUrl", {PERSISTENT, STRING}},
{"OsmStateName", {PERSISTENT, STRING, "All"}},
{"OsmStateTitle", {PERSISTENT, STRING}},
{"OsmWayTest", {PERSISTENT, STRING}},
{"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"RoadNameToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
// Speed Limit
{"SpeedLimitMode", {PERSISTENT | BACKUP, INT, "1"}},
{"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}},
{"SpeedLimitPolicy", {PERSISTENT | BACKUP, INT, "3"}},
{"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}},
// Smart Cruise Control
{"MapTargetVelocities", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"SmartCruiseControlMap", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SmartCruiseControlVision", {PERSISTENT | BACKUP, BOOL, "0"}},
// Torque lateral control custom params
{"CustomTorqueParams", {PERSISTENT | BACKUP , BOOL}},
{"EnforceTorqueControl", {PERSISTENT | BACKUP, BOOL}},
{"LiveTorqueParamsToggle", {PERSISTENT | BACKUP , BOOL}},
{"LiveTorqueParamsRelaxedToggle", {PERSISTENT | BACKUP , BOOL}},
{"TorqueControlTune", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
{"TorqueParamsOverrideEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TorqueParamsOverrideFriction", {PERSISTENT | BACKUP, FLOAT, "0.1"}},
{"TorqueParamsOverrideLatAccelFactor", {PERSISTENT | BACKUP, FLOAT, "2.5"}},
};
+4 -3
View File
@@ -18,6 +18,7 @@ cdef extern from "common/params.h":
CLEAR_ON_OFFROAD_TRANSITION
DEVELOPMENT_ONLY
CLEAR_ON_IGNITION_ON
BACKUP
ALL
cpdef enum ParamKeyType:
@@ -43,7 +44,7 @@ cdef extern from "common/params.h":
optional[string] getKeyDefaultValue(string) nogil
string getParamPath(string) nogil
void clearAll(ParamKeyFlag)
vector[string] allKeys()
vector[string] allKeys(ParamKeyFlag)
PYTHON_2_CPP = {
(str, STRING): lambda v: v,
@@ -176,8 +177,8 @@ cdef class Params:
def get_type(self, key):
return self.p.getKeyType(self.check_key(key))
def all_keys(self):
return self.p.allKeys()
def all_keys(self, flag=ParamKeyFlag.ALL):
return self.p.allKeys(flag)
def get_default_value(self, key):
cdef string k = self.check_key(key)
+83
View File
@@ -55,3 +55,86 @@ class PIDController:
control = self.p + self.i + self.d + self.f
self.control = np.clip(control, self.neg_limit, self.pos_limit)
return self.control
class MultiplicativeUnwindPID:
def __init__(self, k_p, k_i, k_f=0., k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100, min_cmd=1e-10, ki_red_time=1.0):
if isinstance(k_p, Number):
k_p = [[0], [k_p]]
if isinstance(k_i, Number):
k_i = [[0], [k_i]]
if isinstance(k_d, Number):
k_d = [[0], [k_d]]
self._k_p = k_p
self._k_i = k_i
self._k_d = k_d
self.k_f = float(k_f)
self.pos_limit = pos_limit
self.neg_limit = neg_limit
self.rate = float(rate)
self.i_dt = 1.0 / rate
self.min_cmd = abs(min_cmd)
self.ki_red_time = float(ki_red_time)
self.override_prev = False
self.i_unwind_factor = 1.0
self.speed = 0.0
self.reset()
@property
def k_p(self):
return np.interp(self.speed, self._k_p[0], self._k_p[1])
@property
def k_i(self):
return np.interp(self.speed, self._k_i[0], self._k_i[1])
@property
def k_d(self):
return np.interp(self.speed, self._k_d[0], self._k_d[1])
def reset(self):
self.p = 0.0
self.i = 0.0
self.d = 0.0
self.f = 0.0
self.control = 0
def _calc_unwind_factor(self, override):
if not override or self.override_prev:
return
if self.ki_red_time <= 0.0:
self.i_unwind_factor = 1.0
return
if abs(self.i) <= self.min_cmd:
self.i_unwind_factor = 0.0
return
steps = max(int(self.ki_red_time * self.rate), 1)
factor = (self.min_cmd / abs(self.i)) ** (1.0 / steps)
self.i_unwind_factor = min(factor, 1.0)
def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False):
self.speed = speed
self.p = float(error) * self.k_p
self.f = feedforward * self.k_f
self.d = error_rate * self.k_d
if override:
self._calc_unwind_factor(override)
self.i *= self.i_unwind_factor
if abs(self.i) < self.min_cmd:
self.i = 0.0
else:
if not freeze_integrator:
self.i = self.i + error * self.k_i * self.i_dt
# Clip i to prevent exceeding control limits
control_no_i = self.p + self.d + self.f
control_no_i = np.clip(control_no_i, self.neg_limit, self.pos_limit)
self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i)
control = self.p + self.i + self.d + self.f
self.control = np.clip(control, self.neg_limit, self.pos_limit)
self.override_prev = override
return self.control
+112
View File
@@ -0,0 +1,112 @@
import math
class PT2Filter:
"""
Diskreter PT2-Filter mittels Tustin (bilinear transform)
Übertragungsfunktion (kontinuierlich):
H(s) = (w0^2) / (s^2 + 2*zeta*w0*s + w0^2)
Abtastzeit dt.
"""
def __init__(self, w0: float, zeta: float, dt: float):
"""
w0: Eigenkreisfrequenz [rad/s], bestimmt u.a. die Anstiegszeit
zeta: Dämpfungsgrad (zeta=1 => kritisch gedämpft)
dt: Abtastzeit [s]
"""
self.w0 = w0
self.zeta = zeta
self.dt = dt
self.a1, self.a2, self.b0, self.b1, self.b2 = self._design_pt2(self.w0, self.zeta, self.dt)
self.y1 = 0.0
self.y2 = 0.0
self.u1 = 0.0
self.u2 = 0.0
def _design_pt2(self, w0, zeta, dt):
"""
Erzeuge (a1, a2, b0, b1, b2) aus:
H(s) = w0^2 / [s^2 + 2*zeta*w0 s + w0^2]
via Tustin (s = (2/dt)*(1-z^-1)/(1+z^-1)).
Wir bringen G(z) in die Normalform:
Y(z)/U(z) = (b0 + b1 z^-1 + b2 z^-2) / (1 + a1 z^-1 + a2 z^-2).
=> Zeitbereich: y[k] = -a1*y[k-1] - a2*y[k-2] + b0*u[k] + b1*u[k-1] + b2*u[k-2].
"""
Ts = dt
wd = w0
alpha = 2.0 / Ts
b2_ = w0**2
b1_ = 2.0 * w0**2
b0_ = w0**2
A2_f1 = alpha**2
A1_f1 = -2.0 * alpha**2
A0_f1 = alpha**2
factor2 = 2.0*zeta*wd*alpha
A2_f2 = factor2
A1_f2 = 0.0
A0_f2 = -factor2
A2_f3 = wd**2
A1_f3 = 2.0*(wd**2)
A0_f3 = wd**2
A2 = A2_f1 + A2_f2 + A2_f3
A1 = A1_f1 + A1_f2 + A1_f3
A0 = A0_f1 + A0_f2 + A0_f3
B2 = b2_
B1 = b1_
B0 = b0_
b2d = B2 / A2
b1d = B1 / A2
b0d = B0 / A2
a2d = A0 / A2
a1d = A1 / A2
return (a1d, a2d, b0d, b1d, b2d)
def sync(self, target: float):
steps = compute_saturation_steps(self.w0, self.zeta, self.dt)
for i in range(1, steps + 1):
update(target)
def compute_saturation_steps(self, w0: float, zeta: float, dt: float) -> int:
"""
Berechnet eine Abschätzung der Schritte, bis der Filter (95% des Endwerts) erreicht ist.
Wir nutzen hier die Abschätzung:
T_s = 4 / (zeta * w0)
und setzen N = T_s / dt.
"""
Ts = 4.0 / (zeta * w0)
N = Ts / dt
return math.ceil(N)
def reset(self):
"""
Setzt interne Zustände zurück
"""
self.y1 = 0.0
self.y2 = 0.0
self.u1 = 0.0
self.u2 = 0.0
def update(self, u: float) -> float:
"""
Ein Schritt des Filters mit aktuellem Eingang u.
Gibt Ausgang y zurück.
"""
y = (
- self.a1 * self.y1
- self.a2 * self.y2
+ self.b0 * u
+ self.b1 * self.u1
+ self.b2 * self.u2
)
self.y2 = self.y1
self.y1 = y
self.u2 = self.u1
self.u1 = u
return y
+3 -1
View File
@@ -15,6 +15,8 @@
#include "common/version.h"
#include "system/hardware/hw.h"
#include "sunnypilot/common/version.h"
class SwaglogState {
public:
SwaglogState() {
@@ -56,7 +58,7 @@ public:
if (char* daemon_name = getenv("MANAGER_DAEMON")) {
ctx_j["daemon"] = daemon_name;
}
ctx_j["version"] = COMMA_VERSION;
ctx_j["version"] = SUNNYPILOT_VERSION;
ctx_j["dirty"] = !getenv("CLEAN");
ctx_j["device"] = Hardware::get_name();
}
+1 -1
View File
@@ -6,7 +6,7 @@ from openpilot.common.markdown import parse_markdown
class TestMarkdown:
def test_all_release_notes(self):
with open(os.path.join(BASEDIR, "RELEASES.md")) as f:
with open(os.path.join(BASEDIR, "CHANGELOG.md")) as f:
release_notes = f.read().split("\n\n")
assert len(release_notes) > 10
+3 -1
View File
@@ -9,6 +9,8 @@
#include "system/hardware/hw.h"
#include "json11/json11.hpp"
#include "sunnypilot/common/version.h"
std::string daemon_name = "testy";
std::string dongle_id = "test_dongle_id";
int LINE_NO = 0;
@@ -53,7 +55,7 @@ void recv_log(int thread_cnt, int thread_msg_cnt) {
REQUIRE(ctx["dongle_id"].string_value() == dongle_id);
REQUIRE(ctx["dirty"].bool_value() == true);
REQUIRE(ctx["version"].string_value() == COMMA_VERSION);
REQUIRE(ctx["version"].string_value() == SUNNYPILOT_VERSION);
std::string device = Hardware::get_name();
REQUIRE(ctx["device"].string_value() == device);
+1
View File
@@ -36,6 +36,7 @@ const double MS_TO_KPH = 3.6;
const double MS_TO_MPH = MS_TO_KPH * KM_TO_MILE;
const double METER_TO_MILE = KM_TO_MILE / 1000.0;
const double METER_TO_FOOT = 3.28084;
const double METER_TO_KM = 1. / 1000.0;
#define ALIGNED_SIZE(x, align) (((x) + (align)-1) & ~((align)-1))
+1 -1
View File
@@ -1 +1 @@
#define COMMA_VERSION "0.11.1"
#define COMMA_VERSION "0.11.2"
+3
View File
@@ -11,6 +11,9 @@ from openpilot.system.hardware import TICI, HARDWARE
collect_ignore = [
"selfdrive/test/process_replay/test_processes.py",
"selfdrive/test/process_replay/test_regen.py",
# tinygrad JIT has process-global state. Other test files import modeld → tinygrad,
# which corrupts JIT captures for test_warp.py in the same process. Run separately in CI.
"sunnypilot/modeld_v2/tests/test_warp.py",
]
collect_ignore_glob = [
"selfdrive/debug/*.py",
+19 -12
View File
@@ -4,7 +4,7 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 334 Supported Cars
# 341 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
@@ -47,8 +47,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Ford|Focus 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018">Buy Here</a></sub></details>|||
|Ford|Focus Hybrid 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|Ford|Focus 2018-22[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018-22">Buy Here</a></sub></details>|||
|Ford|Focus Hybrid 2018-22[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018-22">Buy Here</a></sub></details>|||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga 2020-23">Buy Here</a></sub></details>|||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2020-23">Buy Here</a></sub></details>|||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
@@ -134,6 +134,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2020">Buy Here</a></sub></details>|||
|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2022-23">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2018-21">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2022-23">Buy Here</a></sub></details>|||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
@@ -231,23 +232,29 @@ A supported vehicle is one that just works when you install a comma device. All
|Nissan[<sup>6</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Nissan[<sup>6</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|Nissan[<sup>6</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2025">Buy Here</a></sub></details>|||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Škoda|Fabia 2022-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
-543
View File
@@ -1,543 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, 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.
Dashy State Aggregation Daemon
Aggregates all cereal topics needed by dashy UI into a single dashyState message.
serverd then forwards that one message over WebSocket, avoiding per-topic
serialization for every connected client.
All display formatting (units, distances, times) is done here so the frontend
can be a pure display layer with no conversion logic.
Publishes: dashyState (pre-serialized JSON at 15Hz)
"""
import json
import cereal.messaging as messaging
from openpilot.common.params import Params
from openpilot.common.realtime import Ratekeeper
from openpilot.common.swaglog import cloudlog
from opendbc.car.common.conversions import Conversions
# Main loop rate
LOOP_RATE = 15 # Hz
# Downsample factor for modelV2 arrays (33 points -> 17 points)
DOWNSAMPLE_FACTOR = 2
# Unit conversion constants
M_TO_FT = 3.28084
# Global state (refreshed periodically)
_is_metric = True
_params = None
_car_params_cache = None
def _ensure_params():
"""Ensure Params instance exists."""
global _params
if _params is None:
_params = Params()
return _params
def refresh_metric_preference():
"""Refresh metric preference from params (called periodically)."""
global _is_metric
try:
_is_metric = _ensure_params().get_bool("IsMetric")
except Exception:
_is_metric = True
def get_car_params_from_params():
"""Read carParams from Params storage (for immediate availability at startup)."""
global _car_params_cache
if _car_params_cache is not None:
return _car_params_cache
try:
from cereal import car
cp_bytes = _ensure_params().get("CarParams")
if cp_bytes:
with car.CarParams.from_bytes(cp_bytes) as cp:
_car_params_cache = {
'openpilotLongitudinalControl': bool(cp.openpilotLongitudinalControl),
}
return _car_params_cache
except Exception:
pass
return {'openpilotLongitudinalControl': False}
def format_speed(speed_ms: float) -> str:
"""Format speed for display (m/s -> km/h or mph)."""
if _is_metric:
return f"{max(0, speed_ms * Conversions.MS_TO_KPH):.0f}"
return f"{max(0, speed_ms * Conversions.MS_TO_MPH):.0f}"
def get_speed_unit() -> str:
"""Get current speed unit string."""
return "km/h" if _is_metric else "mph"
def get_distance_unit() -> str:
"""Get current distance unit string."""
return "km" if _is_metric else "mi"
SET_SPEED_NA = 255
def get_cruise_speed(v_cruise_cluster: float) -> int:
"""Get cruise speed value for display.
Returns the set speed in display units (km/h or mph), or 255 if not set.
"""
if not (0 < v_cruise_cluster < SET_SPEED_NA):
return SET_SPEED_NA
set_speed = v_cruise_cluster
if not _is_metric:
set_speed *= Conversions.KPH_TO_MPH
return round(set_speed)
def downsample(arr):
"""Downsample list by factor."""
if not arr:
return []
return list(arr[::DOWNSAMPLE_FACTOR])
def safe_get(obj, attr, default=None):
"""Safely get attribute from object."""
try:
return getattr(obj, attr, default)
except Exception:
return default
def extract_car_state(sm):
"""Extract carState fields used by dashy."""
cs = sm['carState']
v_ego = float(cs.vEgo)
v_ego_cluster = float(cs.vEgoCluster)
# Set speed: prefer carState.vCruiseCluster, fall back to carState.vCruise
# if the cluster value isn't populated. Both are live on carState in 0.11.1;
# the old controlsState set-speed fields moved into its deprecated group.
v_cruise = float(cs.vCruiseCluster)
if not (0 < v_cruise < SET_SPEED_NA):
v_cruise = float(cs.vCruise)
set_speed = get_cruise_speed(v_cruise)
return {
'vEgo': v_ego,
'vEgoCluster': v_ego_cluster,
'gearShifter': str(cs.gearShifter),
'aEgo': float(cs.aEgo),
'steeringAngleDeg': float(cs.steeringAngleDeg),
'steeringPressed': bool(cs.steeringPressed),
'gasPressed': bool(cs.gasPressed),
'leftBlinker': bool(cs.leftBlinker),
'rightBlinker': bool(cs.rightBlinker),
'leftBlindspot': bool(cs.leftBlindspot),
'rightBlindspot': bool(cs.rightBlindspot),
'cruiseEnabled': bool(cs.cruiseState.enabled),
# Pre-formatted display values
'speedDisplay': format_speed(v_ego),
'speedClusterDisplay': format_speed(v_ego_cluster) if v_ego_cluster > 0 else format_speed(v_ego),
'setSpeed': set_speed, # 255 = not set, otherwise display value
'speedUnit': get_speed_unit(),
}
def extract_selfdrive_state(sm):
"""Extract selfdriveState fields used by dashy."""
ss = sm['selfdriveState']
return {
'enabled': bool(ss.enabled),
'activeOverride': int(safe_get(ss, 'activeOverride', 0)),
'experimentalMode': bool(ss.experimentalMode),
'alertText1': str(ss.alertText1),
'alertText2': str(ss.alertText2),
'alertSize': str(ss.alertSize),
'alertStatus': str(ss.alertStatus),
}
def extract_device_state(sm):
"""Extract deviceState fields used by dashy."""
ds = sm['deviceState']
temp_c = float(safe_get(ds, 'maxTempC', 0))
# Pre-format temperature for display
if _is_metric:
temp_display = f"{temp_c:.0f}°" if temp_c > 0 else "--"
else:
temp_f = temp_c * 9 / 5 + 32
temp_display = f"{temp_f:.0f}°" if temp_c > 0 else "--"
return {
'cpuUsagePercent': list(ds.cpuUsagePercent) if ds.cpuUsagePercent else [],
'gpuUsagePercent': int(ds.gpuUsagePercent),
'memoryUsagePercent': int(ds.memoryUsagePercent),
'freeSpacePercent': float(ds.freeSpacePercent),
'maxTempC': temp_c,
'thermalStatus': str(ds.thermalStatus), # 'green' | 'yellow' | 'red' | 'danger'
'fanSpeedPercentDesired': int(ds.fanSpeedPercentDesired),
'powerDrawW': float(safe_get(ds, 'powerDrawW', 0)),
'deviceType': str(ds.deviceType),
'tempDisplay': temp_display,
}
def extract_lead(lead, sm):
"""Extract lead vehicle data."""
d_rel = float(lead.dRel)
v_rel = float(lead.vRel)
y_rel = float(lead.yRel)
has_lead = bool(lead.status)
# Pre-format lead display values. Each metric ships as a
# (value, unit) pair so the HUD can tabular-align numbers without
# regex-parsing on the JS side.
dist_value = "--"
dist_unit = ""
speed_value = "--"
speed_unit_str = ""
ttc_value = ""
ttc_unit = "s"
ttc_urgent = False
if has_lead:
speed_unit_str = "km/h" if _is_metric else "mph"
dist_unit = "m" if _is_metric else "ft"
conv = Conversions.MS_TO_KPH if _is_metric else Conversions.MS_TO_MPH
dist_value = f"{d_rel:.1f}" if _is_metric else f"{d_rel * M_TO_FT:.1f}"
v_ego = float(sm['carState'].vEgo) if sm.valid['carState'] else 0
# Lead's absolute speed = ego + relative (clamped to 0).
lead_speed_disp = max(0.0, v_ego + v_rel) * conv
speed_value = f"{lead_speed_disp:.1f}"
if v_ego > 0:
ttc = d_rel / v_ego
if ttc < 5.0:
ttc_value = f"{ttc:.1f}"
ttc_urgent = True
return {
'status': has_lead,
'dRel': d_rel,
'yRel': y_rel,
'vRel': v_rel,
'distValue': dist_value,
'distUnit': dist_unit,
'speedValue': speed_value,
'speedUnit': speed_unit_str,
'ttcValue': ttc_value,
'ttcUnit': ttc_unit,
'ttcUrgent': ttc_urgent,
}
def extract_radar_state(sm):
"""Extract radarState fields used by dashy."""
rs = sm['radarState']
return {
'leadOne': extract_lead(rs.leadOne, sm),
'leadTwo': extract_lead(rs.leadTwo, sm),
}
def extract_live_tracks(sm):
"""Extract liveTracks radar points for bird's eye view.
Filters out tracks that are already shown as leadOne or leadTwo.
Uses radarTrackId matching: when radarState matches a liveTrack to a lead,
radarTrackId changes from -1 (vision-only) to the track's ID.
"""
try:
lt = sm['liveTracks']
points = []
# Get lead vehicle radar track IDs to filter them out
# radarTrackId = -1 means vision-only (no radar match)
# radarTrackId >= 0 means matched to a radar track
lead_track_ids = set()
if sm.valid.get('radarState', False):
rs = sm['radarState']
if rs.leadOne.status and rs.leadOne.radarTrackId >= 0:
lead_track_ids.add(rs.leadOne.radarTrackId)
if rs.leadTwo.status and rs.leadTwo.radarTrackId >= 0:
lead_track_ids.add(rs.leadTwo.radarTrackId)
if hasattr(lt, 'points'):
for pt in lt.points:
# Skip if this track is already shown as a lead vehicle
if pt.trackId in lead_track_ids:
continue
# Drop stale tracks — radar's predicting, not measuring
if not pt.measured:
continue
# Drop stationary clutter (sign posts, guardrails,
# parked cars). |vRel| < 0.5 m/s ≈ standing still
# relative to ego; not relevant traffic.
if abs(pt.vRel) < 0.5:
continue
points.append({
'd': float(pt.dRel),
'y': float(pt.yRel),
'v': float(pt.vRel),
'm': bool(pt.measured),
})
return {'points': points}
except Exception as e:
cloudlog.warning(f"extract_live_tracks error: {e}")
return {'points': []}
def extract_model_v2(sm):
"""Extract modelV2 fields used by dashy (downsampled)."""
model = sm['modelV2']
# Position
pos = model.position
position = {
'x': downsample(list(pos.x)),
'y': downsample(list(pos.y)),
'z': downsample(list(pos.z)),
}
# Lane lines (4 lines)
lane_lines = []
for line in model.laneLines:
lane_lines.append({
'x': downsample(list(line.x)),
'y': downsample(list(line.y)),
'z': downsample(list(line.z)),
})
# Road edges (2 edges)
road_edges = []
for edge in model.roadEdges:
road_edges.append({
'x': downsample(list(edge.x)),
'y': downsample(list(edge.y)),
'z': downsample(list(edge.z)),
})
return {
'position': position,
'laneLines': lane_lines,
'laneLineProbs': list(model.laneLineProbs) if hasattr(model, 'laneLineProbs') else [0, 0, 0, 0],
'roadEdges': road_edges,
'roadEdgeStds': list(model.roadEdgeStds) if hasattr(model, 'roadEdgeStds') else [1, 1],
}
def extract_live_calibration(sm):
"""Extract liveCalibration fields used by dashy."""
cal = sm['liveCalibration']
return {
'rpyCalib': list(cal.rpyCalib) if hasattr(cal, 'rpyCalib') and cal.rpyCalib else [],
'calStatus': str(cal.calStatus) if hasattr(cal, 'calStatus') else 'uncalibrated',
'height': list(cal.height) if hasattr(cal, 'height') else [],
}
def extract_longitudinal_plan(sm):
"""Extract longitudinalPlan fields used by dashy."""
lp = sm['longitudinalPlan']
return {
'allowThrottle': bool(safe_get(lp, 'allowThrottle', True)),
}
def extract_controls_state_ext(sm):
"""Extract controlsStateExt fields used by dashy."""
cse = sm['controlsStateExt']
return {
'alkaActive': bool(safe_get(cse, 'alkaActive', False)),
}
def extract_car_params(sm):
"""Extract carParams fields used by dashy."""
cp = sm['carParams']
return {
'openpilotLongitudinalControl': bool(safe_get(cp, 'openpilotLongitudinalControl', False)),
}
# =============================================================================
# TOPIC CONFIGURATION
# =============================================================================
# Single source of truth for all subscribed topics.
# Comment out a line to disable that topic entirely.
#
# Fields:
# extractor: function(sm) -> dict, extracts data from message
# rate: 'fast' = every frame when updated
# number = slow poll divider (e.g., LOOP_RATE = 1Hz)
# 'valid' = just track valid state, no extraction
# 'subscribe' = subscribed but extracted within other extractors
# default: initial cache value (None if not specified)
# =============================================================================
TOPICS = {
# Fast topics - extract every frame when updated
'carState': {'extractor': extract_car_state, 'rate': 'fast'},
'selfdriveState': {'extractor': extract_selfdrive_state, 'rate': 'fast'},
'radarState': {'extractor': extract_radar_state, 'rate': 'fast'},
'liveTracks': {'extractor': extract_live_tracks, 'rate': 'fast'},
'modelV2': {'extractor': extract_model_v2, 'rate': 'fast'},
'longitudinalPlan': {'extractor': extract_longitudinal_plan, 'rate': 'fast'},
# Slow topics - poll at fixed intervals
'deviceState': {'extractor': extract_device_state, 'rate': LOOP_RATE // 2},
'liveCalibration': {'extractor': extract_live_calibration, 'rate': LOOP_RATE},
'carParams': {'extractor': extract_car_params, 'rate': LOOP_RATE * 2},
# Valid-only topics - just track valid state
'roadCameraState': {'rate': 'valid', 'default': False},
# Subscribe-only topics - subscribed but extracted within other extractors
'controlsState': {'rate': 'subscribe'},
# Optional/dragonpilot-specific topics - comment out to disable
'controlsStateExt': {'extractor': extract_controls_state_ext, 'rate': 'fast', 'default': {'alkaActive': False}},
}
def _available_topics(topics_cfg):
"""Filter TOPICS to only services this cereal schema knows about.
Lets dragonpilot-specific topics like controlsStateExt drop out
cleanly on a vanilla openpilot schema instead of crashing SubMaster.
Topics that drop out keep their default cache value (see 'default'
in the TOPICS entry), which the frontend already null-checks.
"""
try:
from cereal.services import SERVICE_LIST as _services
except ImportError:
try:
from cereal.services import services as _services
except ImportError:
return topics_cfg
out = {}
for name, cfg in topics_cfg.items():
if name in _services:
out[name] = cfg
else:
cloudlog.info(f"dashyd: cereal service '{name}' not available, skipping")
return out
def main():
cloudlog.info("dashyd: starting")
# Initialize metric preference
refresh_metric_preference()
topics = _available_topics(TOPICS)
# Derive services list from filtered topics
services = list(topics.keys())
sm = messaging.SubMaster(services)
pm = messaging.PubMaster(['dashyState'])
rk = Ratekeeper(LOOP_RATE)
# Initialize cache from TOPICS defaults (always include all topics so
# the frontend gets default values for dropped optional ones too).
cache = {t: cfg.get('default') for t, cfg in TOPICS.items() if cfg.get('rate') != 'subscribe'}
cache['carParams'] = get_car_params_from_params() # special: init from Params
# Build topic lists from the filtered topics (only subscribed ones run their extractors)
fast_topics = {t: cfg['extractor'] for t, cfg in topics.items() if cfg.get('rate') == 'fast'}
slow_topics = {t: (cfg['extractor'], cfg['rate']) for t, cfg in topics.items()
if isinstance(cfg.get('rate'), int)}
valid_topics = [t for t, cfg in topics.items() if cfg.get('rate') == 'valid']
cache_dirty = True
frame_count = 0
while True:
sm.update(0)
frame_count += 1
# Refresh metric preference every ~2 seconds
if frame_count % (LOOP_RATE * 2) == 0:
refresh_metric_preference()
cache_dirty = True # Force re-format with new units
# Fast topics - extract when updated
for topic, extractor in fast_topics.items():
if sm.updated[topic]:
cache[topic] = extractor(sm)
cache_dirty = True
# Slow topics - extract at fixed intervals (ignore sm.updated)
for topic, (extractor, divider) in slow_topics.items():
if frame_count % divider == 0:
cache[topic] = extractor(sm)
cache_dirty = True
# Valid-only topics - just track valid state
for topic in valid_topics:
if sm.updated[topic]:
new_val = sm.valid[topic]
if cache[topic] != new_val:
cache[topic] = new_val
cache_dirty = True
# Only serialize and publish if something changed
if cache_dirty:
# Only publish when critical openpilot data exists
critical_ready = (
cache.get('carState') is not None and
cache.get('modelV2') is not None and
cache.get('selfdriveState') is not None
)
if critical_ready:
state = {
'ts': sm.logMonoTime['carState'],
'display': {
'isMetric': _is_metric,
'speedUnit': get_speed_unit(),
'distanceUnit': get_distance_unit(),
},
**cache, # include all cached topics
}
msg = messaging.new_message('dashyState')
msg.dashyState.json = json.dumps(state).encode()
pm.send('dashyState', msg)
cache_dirty = False
rk.keep_time()
if __name__ == "__main__":
main()
-856
View File
@@ -1,856 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2026, 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.
Dashy HTTP Server
Provides REST API and static file serving for the dashy web UI.
- Settings management (read/write params)
- File browser for drive logs
- Static file serving for web UI
"""
import argparse
import ast
import asyncio
import json
import operator
import os
import logging
import time
from datetime import datetime
from functools import wraps
from urllib.parse import quote
from aiohttp import web
from cereal import messaging
from openpilot.common.params import Params
from openpilot.system.hardware import PC, HARDWARE
from openpilot.system.ui.lib.multilang import multilang as base_multilang
from dragonpilot.settings import SETTINGS
try:
from openpilot.system.version import get_build_metadata as _get_build_metadata
except Exception:
_get_build_metadata = None
# --- Configuration ---
DEFAULT_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), '..') if PC else '/data/media/0/realdata')
WEB_DIST_PATH = os.path.join(os.path.dirname(__file__), "web", "dist")
CAR_PARAMS_CACHE_TTL = 30 # seconds
logger = logging.getLogger("dashy")
class MockParams:
"""In-memory params mock for dev mode."""
_store = {}
def get(self, key, default=None): return self._store.get(key, default)
def get_bool(self, key, default=False): return bool(self._store.get(key)) if key in self._store else default
def put(self, key, value): self._store[key] = value
def put_bool(self, key, value): self._store[key] = value
def remove(self, key): self._store.pop(key, None)
def check_key(self, key): return True
# --- Caching Layer ---
class AppCache:
"""Centralized cache for expensive operations."""
def __init__(self):
self._params = None
self._car_params = None
self._car_params_time = 0
self._context = None
self._context_time = 0
self._settings_cache = None
self._settings_cache_time = 0
@property
def params(self):
"""Get shared Params instance (or mock if unavailable)."""
if self._params is None:
try:
self._params = Params()
except Exception as e:
logger.warning(f"Params unavailable, using mock: {e}")
self._params = MockParams()
return self._params
def get_car_params(self):
"""Get cached CarParams data (brand, longitudinal control)."""
now = time.time()
if self._car_params is None or (now - self._car_params_time) > CAR_PARAMS_CACHE_TTL:
self._car_params = self._parse_car_params()
self._car_params_time = now
return self._car_params
def _parse_car_params(self):
"""Parse CarParams from Params store."""
result = {'brand': '', 'openpilot_longitudinal_control': False}
try:
# CarParams is cleared offroad/at boot; CarParamsPersistent keeps the last car's
# params so brand/longitudinal-gated settings still show when configuring parked.
car_params_bytes = self.params.get("CarParamsPersistent") or self.params.get("CarParams")
if car_params_bytes:
from cereal import car
with car.CarParams.from_bytes(car_params_bytes) as cp:
result['brand'] = cp.brand
result['openpilot_longitudinal_control'] = cp.openpilotLongitudinalControl
except Exception as e:
logger.debug(f"Could not parse CarParams: {e}")
return result
def get_settings_context(self):
"""Get context dict for settings condition evaluation."""
now = time.time()
if self._context is None or (now - self._context_time) > CAR_PARAMS_CACHE_TTL:
car_params = self.get_car_params()
self._context = {
'brand': car_params['brand'],
'openpilotLongitudinalControl': car_params['openpilot_longitudinal_control'],
'LITE': os.getenv("LITE") is not None,
'MICI': self._check_mici(),
# Upstream-mirror items gate on these.
'DASHY': True,
'IS_RELEASE': self._is_release_channel(),
}
self._context_time = now
return self._context
def _check_mici(self):
"""Check if device is MICI type."""
try:
return HARDWARE.get_device_type() == "mici"
except Exception:
return False
def _is_release_channel(self):
if _get_build_metadata is None:
return False
try:
return bool(_get_build_metadata().release_channel)
except Exception:
return False
def get_bool_safe(self, key, default=False):
"""Safely get a boolean param with default."""
try:
return self.params.get_bool(key)
except Exception:
return default
def invalidate(self):
"""Invalidate all caches."""
self._car_params = None
self._context = None
self._settings_cache = None
# --- Helper Functions ---
def api_handler(func):
"""Decorator for API handlers with consistent error handling."""
@wraps(func)
async def wrapper(request):
try:
return await func(request)
except web.HTTPException:
raise
except Exception as e:
logger.error(f"{func.__name__} error: {e}", exc_info=True)
return web.json_response({'error': str(e)}, status=500)
return wrapper
def get_safe_path(requested_path):
"""Ensures the requested path is within DEFAULT_DIR."""
combined_path = os.path.join(DEFAULT_DIR, requested_path.lstrip('/'))
safe_path = os.path.realpath(combined_path)
if os.path.commonpath((safe_path, DEFAULT_DIR)) == DEFAULT_DIR:
return safe_path
return None
_CMP_OPS = {
ast.Eq: operator.eq,
ast.NotEq: operator.ne,
ast.Lt: operator.lt,
ast.LtE: operator.le,
ast.Gt: operator.gt,
ast.GtE: operator.ge,
}
def _eval_node(node, context):
"""Evaluate a tightly restricted AST node against a context dict.
Only the operators that SETTINGS conditions actually use are supported:
Name lookup, literal Constants, and / or / not, and the six numeric
comparisons. No function calls, attribute access, subscripts, or
arithmetic those would re-open the eval-sandbox escape paths.
"""
if isinstance(node, ast.Expression):
return _eval_node(node.body, context)
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.Name):
return context.get(node.id, False)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return not _eval_node(node.operand, context)
if isinstance(node, ast.BoolOp):
values = [_eval_node(v, context) for v in node.values]
if isinstance(node.op, ast.And):
return all(values)
if isinstance(node.op, ast.Or):
return any(values)
if isinstance(node, ast.Compare) and len(node.ops) == 1 and len(node.comparators) == 1:
op_type = type(node.ops[0])
if op_type in _CMP_OPS:
left = _eval_node(node.left, context)
right = _eval_node(node.comparators[0], context)
return _CMP_OPS[op_type](left, right)
raise ValueError(f"Unsupported node: {type(node).__name__}")
def eval_condition(condition, context):
"""Evaluate a SETTINGS condition expression in a sandboxed AST walker."""
if not condition:
return True
try:
tree = ast.parse(condition, mode='eval')
return bool(_eval_node(tree, context))
except Exception as e:
logger.debug(f"Condition evaluation failed: {condition}, error: {e}")
return False
def resolve_value(value):
"""Resolve callable values (lambdas) for JSON serialization."""
return value() if callable(value) else value
# Map of settings-declared param keys to their setting dict.
# Used as an allowlist for /api/settings/params/{name} read/write so
# LAN clients can only touch keys that the UI knowingly exposes.
def _build_param_setting_map():
out = {}
for section in SETTINGS:
for setting in section.get('settings', []):
key = setting.get('key')
if not key:
continue
# action_item entries use `key` as the action name, not a real
# param — skip so they don't leak into the param read/write
# allowlist.
if setting.get('type') == 'action_item':
continue
out[key] = setting
return out
_PARAM_SETTINGS = _build_param_setting_map()
# Control-tab / one-off params the UI legitimately reads or writes that
# are not part of the SETTINGS schema. Kept as an explicit allowlist so
# the broader 'unknown param' guard still blocks arbitrary writes.
_CONTROL_PARAMS = {
'dp_dev_go_off_road', # Controls tab: force-offroad toggle
'DoReboot', # Controls tab: reboot button
'ExperimentalMode', # Tesla HUD: tap set-speed circle to toggle
}
def _param_allowed(key):
return key in _PARAM_SETTINGS or key in _CONTROL_PARAMS
# --- API Endpoints ---
@api_handler
async def init_api(request):
"""Provide initial data to the client."""
cache: AppCache = request.app['cache']
return web.json_response({
'dp_dev_dashy': cache.get_bool_safe("dp_dev_dashy", True),
'isOffroad': cache.get_bool_safe("IsOffroad", False),
})
@api_handler
async def list_files_api(request):
"""List files and folders."""
path_param = request.query.get('path', '/')
safe_path = get_safe_path(path_param)
if not safe_path or not os.path.isdir(safe_path):
return web.json_response({'error': 'Invalid or Not Found Path'}, status=404)
items = []
for entry in os.listdir(safe_path):
full_path = os.path.join(safe_path, entry)
# Skip entries whose real target escapes DEFAULT_DIR (e.g., symlinks).
# get_safe_path only validates the requested directory itself; each
# child has to be re-checked to prevent listing files outside the tree.
real_full = os.path.realpath(full_path)
if os.path.commonpath((real_full, DEFAULT_DIR)) != DEFAULT_DIR:
continue
try:
stat = os.stat(full_path)
is_dir = os.path.isdir(full_path)
items.append({
'name': entry,
'is_dir': is_dir,
'mtime': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
'size': stat.st_size if not is_dir else 0
})
except FileNotFoundError:
continue
# Sort: directories first (by mtime desc), then files (by mtime desc)
dirs = sorted([i for i in items if i['is_dir']], key=lambda x: x['mtime'], reverse=True)
files = sorted([i for i in items if not i['is_dir']], key=lambda x: x['mtime'], reverse=True)
relative_path = os.path.relpath(safe_path, DEFAULT_DIR)
return web.json_response({
'path': '' if relative_path == '.' else relative_path,
'files': dirs + files
})
@api_handler
async def serve_player_api(request):
"""Serve the HLS player page."""
file_path = request.query.get('file')
if not file_path:
return web.Response(text="File parameter is required.", status=400)
if get_safe_path(file_path) is None:
return web.Response(text="Invalid file path.", status=400)
player_html_path = os.path.join(WEB_DIST_PATH, 'pages', 'player.html')
try:
with open(player_html_path, 'r') as f:
html_template = f.read()
except FileNotFoundError:
return web.Response(text="Player HTML not found.", status=500)
html = html_template.replace('{{FILE_PATH}}', quote(file_path, safe=''))
return web.Response(text=html, content_type='text/html')
@api_handler
async def serve_manifest_api(request):
"""Dynamically generate m3u8 playlist."""
file_path = request.query.get('file', '').lstrip('/')
if not file_path:
return web.Response(text="File parameter is required.", status=400)
if get_safe_path(file_path) is None:
return web.Response(text="Invalid file path.", status=400)
encoded_path = quote(file_path)
manifest = f"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:60\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:60.0,\n/media/{encoded_path}\n#EXT-X-ENDLIST\n"
return web.Response(text=manifest, content_type='application/vnd.apple.mpegurl')
@api_handler
async def get_settings_config_api(request):
"""Get the settings configuration from settings.py."""
cache: AppCache = request.app['cache']
# Return cached settings if fresh (2 second TTL)
now = time.time()
if cache._settings_cache is not None and (now - cache._settings_cache_time) < 2:
return web.json_response(cache._settings_cache)
params = cache.params
# Update language if changed
current_lang = params.get("LanguageSetting")
if current_lang:
lang_str = current_lang.decode() if isinstance(current_lang, bytes) else str(current_lang)
lang_str = lang_str.removeprefix("main_")
if lang_str != base_multilang.language and lang_str in base_multilang.languages.values():
base_multilang._language = lang_str
base_multilang.setup()
context = cache.get_settings_context()
settings_with_values = []
for section in SETTINGS:
if not eval_condition(section.get('condition'), context):
continue
section_copy = section.copy()
settings_list = []
for setting in section.get('settings', []):
if not eval_condition(setting.get('condition'), context):
continue
setting_copy = setting.copy()
key = setting['key']
# Resolve callable values
for field in ['title', 'description', 'suffix', 'special_value_text']:
if field in setting_copy:
setting_copy[field] = resolve_value(setting_copy[field])
if 'options' in setting_copy:
setting_copy['options'] = [resolve_value(opt) for opt in setting_copy['options']]
# Get current value based on type
setting_copy['current_value'] = _get_setting_value(params, setting)
settings_list.append(setting_copy)
if settings_list:
section_copy['settings'] = settings_list
settings_with_values.append(section_copy)
response_data = {'settings': settings_with_values}
cache._settings_cache = response_data
cache._settings_cache_time = now
return web.json_response(response_data)
def _get_setting_value(params, setting):
"""Get current value for a setting from Params."""
key = setting['key']
setting_type = setting['type']
default = setting.get('default', 0)
try:
if setting_type == 'toggle_item':
return params.get_bool(key)
elif setting_type == 'double_spin_button_item':
value = params.get(key)
return float(value) if value is not None else float(default)
elif setting_type in ('text_input_item', 'text_display_item'):
value = params.get(key)
if value is None:
return ''
return value.decode('utf-8', errors='replace') if isinstance(value, bytes) else str(value)
elif setting_type == 'action_item':
# Pure action buttons have no stored value; return None so the
# UI treats it as display-only.
return None
else: # spin_button_item, text_spin_button_item
value = params.get(key)
return int(value) if value is not None else int(default)
except Exception as e:
logger.warning(f"Error getting value for {key}: {e}")
if setting_type == 'toggle_item':
return False
elif setting_type == 'double_spin_button_item':
return float(default)
elif setting_type in ('text_input_item', 'text_display_item'):
return ''
elif setting_type == 'action_item':
return None
return int(default)
@api_handler
async def save_param_api(request):
"""Save a single param value.
Usage: POST /api/settings/params/{name}
Body: { "value": <value> }
"""
param_name = request.match_info.get('param_name')
if not param_name:
return web.json_response({'error': 'param_name is required'}, status=400)
if not _param_allowed(param_name):
return web.json_response({'error': 'Unknown param'}, status=403)
setting = _PARAM_SETTINGS.get(param_name)
if setting is not None and setting.get('type') == 'text_display_item':
return web.json_response({'error': 'Read-only param'}, status=403)
cache: AppCache = request.app['cache']
params = cache.params
data = await request.json()
if 'value' not in data:
return web.json_response({'error': 'value is required in body'}, status=400)
_save_param(params, param_name, data['value'])
cache.invalidate()
logger.info(f"Param saved: {param_name}={data['value']}")
return web.json_response({'status': 'success', 'key': param_name, 'value': data['value']})
def _save_param(params, key, value):
"""Save a single param value with proper type handling."""
try:
param_type = params.get_type(key)
if param_type == 1: # BOOL
params.put_bool(key, bool(value))
elif param_type == 2: # INT
params.put(key, int(value))
elif param_type == 3: # FLOAT
params.put(key, float(value))
elif isinstance(value, bool):
params.put_bool(key, value)
else:
params.put(key, str(value) if not isinstance(value, str) else value)
logger.debug(f"Saved {key}={value} (type={param_type})")
except Exception as e:
logger.error(f"Error saving param {key}={value}: {e}")
raise
def _get_param_value(params, key):
"""Get a single param value via its declared setting type, or as a
bool for control-only params that have no SETTINGS entry."""
setting = _PARAM_SETTINGS.get(key)
if setting is not None:
return _get_setting_value(params, setting)
if key in _CONTROL_PARAMS:
try:
return params.get_bool(key)
except Exception:
return False
return None
@api_handler
async def get_param_api(request):
"""Get a single param value."""
param_name = request.match_info.get('param_name')
if not param_name:
return web.json_response({'error': 'param_name is required'}, status=400)
if not _param_allowed(param_name):
return web.json_response({'error': 'Unknown param'}, status=403)
cache: AppCache = request.app['cache']
try:
value = _get_param_value(cache.params, param_name)
except Exception:
value = None
return web.json_response({'key': param_name, 'value': value})
# --- Action endpoints ---
# Named side-effectful operations declared by settings items via the
# `action` field (text_input_item / action_item). Each handler receives
# the parsed JSON body and the AppCache; it returns a dict that is
# serialized as the JSON response. Errors should be raised — the wrapper
# converts them to 502/500 responses.
SSH_KEY_FETCH_TIMEOUT_S = 10
SSH_KEY_MAX_BYTES = 16 * 1024 # plenty for any realistic ~/.ssh/authorized_keys
GITHUB_USERNAME_MAX_LEN = 39 # github's own limit
def _validate_github_username(username):
"""GitHub username: 1-39 chars, alnum or single hyphen, no leading/trailing hyphen."""
if not username or len(username) > GITHUB_USERNAME_MAX_LEN:
return False
if username.startswith('-') or username.endswith('-'):
return False
if '--' in username:
return False
return all(c.isalnum() or c == '-' for c in username)
async def _fetch_github_ssh_keys(username):
"""Fetch https://github.com/{username}.keys. Returns the body text on
HTTP 200; raises web.HTTPException with an upstream-derived status on
failure so the action endpoint surfaces the real reason."""
import aiohttp
url = f"https://github.com/{quote(username, safe='')}.keys"
timeout = aiohttp.ClientTimeout(total=SSH_KEY_FETCH_TIMEOUT_S)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url) as resp:
if resp.status == 404:
raise web.HTTPNotFound(reason=f"GitHub user '{username}' not found")
if resp.status != 200:
raise web.HTTPBadGateway(reason=f"github.com returned HTTP {resp.status}")
body = await resp.content.read(SSH_KEY_MAX_BYTES + 1)
if len(body) > SSH_KEY_MAX_BYTES:
raise web.HTTPBadGateway(reason="SSH key response too large")
return body.decode('utf-8', errors='replace')
async def _action_ssh_key_set(request, payload, cache):
"""Fetch the user's GitHub SSH keys and write both params atomically.
Body: { "value": "<github-username>" }. On success the device's
sshd_config drop-in is updated by openpilot's own SSH manager."""
username = (payload.get('value') or '').strip()
if not _validate_github_username(username):
raise web.HTTPBadRequest(reason="Invalid GitHub username")
keys_body = await _fetch_github_ssh_keys(username)
if not keys_body.strip():
raise web.HTTPBadRequest(reason=f"GitHub user '{username}' has no public SSH keys")
params = cache.params
# Write keys first; only commit the username if keys were stored
# successfully — keeps the two params consistent.
params.put('GithubSshKeys', keys_body)
params.put('GithubUsername', username)
cache.invalidate()
logger.info(f"SSH keys set from github.com/{username} ({len(keys_body)} bytes)")
return {'status': 'ok', 'username': username, 'key_bytes': len(keys_body)}
async def _action_ssh_key_clear(request, payload, cache):
params = cache.params
params.put('GithubSshKeys', '')
params.put('GithubUsername', '')
cache.invalidate()
logger.info("SSH keys cleared")
return {'status': 'ok'}
_ACTION_HANDLERS = {
'ssh_key_set': _action_ssh_key_set,
'ssh_key_clear': _action_ssh_key_clear,
}
@api_handler
async def run_action_api(request):
"""Dispatch /api/action/{name} → registered handler."""
name = request.match_info.get('name', '')
handler = _ACTION_HANDLERS.get(name)
if handler is None:
return web.json_response({'error': f'Unknown action: {name}'}, status=404)
try:
payload = await request.json()
except Exception:
payload = {}
cache: AppCache = request.app['cache']
result = await handler(request, payload, cache)
return web.json_response(result)
@api_handler
async def get_model_list_api(request):
"""Get the model list and current selection."""
cache: AppCache = request.app['cache']
params = cache.params
# Get model list. JSON-typed params come back already-parsed in
# newer dragonpilot; older builds returned bytes/str — handle both.
model_list = {}
try:
raw = params.get("dp_dev_model_list")
if raw:
if isinstance(raw, (bytes, str)):
model_list = json.loads(raw)
elif isinstance(raw, dict):
model_list = raw
except Exception as e:
logger.debug(f"Could not parse dp_dev_model_list: {e}")
# Get current selection
selected_model = ""
try:
selected_raw = params.get("dp_dev_model_selected")
if selected_raw:
selected_model = selected_raw.decode('utf-8') if isinstance(selected_raw, bytes) else str(selected_raw)
except Exception as e:
logger.debug(f"Could not get dp_dev_model_selected: {e}")
return web.json_response({
'model_list': model_list,
'selected_model': selected_model
})
@api_handler
async def save_model_selection_api(request):
"""Save the selected model."""
cache: AppCache = request.app['cache']
params = cache.params
data = await request.json()
selected_model = data.get('selected_model', '')
if not selected_model or selected_model == "[AUTO]":
params.put("dp_dev_model_selected", "")
logger.info("Model selection cleared (AUTO mode)")
else:
params.put("dp_dev_model_selected", selected_model)
logger.info(f"Model selection saved: {selected_model}")
return web.json_response({'status': 'success'})
# --- WebSocket endpoint for data streaming ---
# One shared publisher task polls the dashyState SubMaster and fans out
# to every connected client. The previous per-connection design ran
# blocking ZMQ I/O on the event loop, which starved every other request
# under multi-client load.
async def _publisher_loop(app):
# IMPORTANT: ZMQ sockets are thread-affined. Construct the SubMaster on
# the asyncio main thread and call update() on the same thread — using
# asyncio.to_thread bounces between worker threads and silently breaks
# the receive. The 0-timeout update is cheap enough on the event loop;
# the per-client send is what we actually need to be async for.
try:
sm = messaging.SubMaster(['dashyState'])
except Exception as e:
logger.warning(f"Publisher disabled (SubMaster init failed): {e}")
return
logger.info("dashyState publisher loop started")
while True:
try:
sm.update(0)
if sm.updated['dashyState']:
json_data = sm['dashyState'].json
if isinstance(json_data, bytes):
json_data = json_data.decode('utf-8')
clients = list(app['ws_clients'])
for ws in clients:
if ws.closed:
app['ws_clients'].discard(ws)
continue
try:
await ws.send_str(json_data)
except Exception as e:
logger.debug(f"WebSocket send failed, dropping client: {e}")
app['ws_clients'].discard(ws)
await asyncio.sleep(0.01)
except asyncio.CancelledError:
raise
except Exception as e:
# Don't let a transient error tear down the loop silently.
logger.exception(f"Publisher loop error: {e}")
await asyncio.sleep(0.1)
async def websocket_handler(request):
"""WebSocket endpoint for data-only connections - streams dashyState directly."""
ws = web.WebSocketResponse()
await ws.prepare(request)
logger.info("WebSocket client connected")
request.app['ws_clients'].add(ws)
try:
# Wait until the client disconnects; no inbound traffic expected.
async for _ in ws:
pass
except Exception as e:
logger.warning(f"WebSocket error: {e}")
finally:
request.app['ws_clients'].discard(ws)
logger.info("WebSocket client disconnected")
return ws
# --- No-cache middleware for web assets ---
# Dashy is a same-origin LAN app; no CORS headers are emitted so that
# browsers will block cross-origin JS from mutating settings via the
# JSON endpoints (the preflight will fail for non-simple requests).
@web.middleware
async def no_cache_middleware(request, handler):
response = await handler(request)
path = request.path.lower()
if path.endswith(('.html', '.js', '.css')) or path == '/':
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# --- Application Setup ---
async def on_startup(app):
"""Initialize app-level resources."""
app['cache'] = AppCache()
app['ws_clients'] = set()
app['publisher_task'] = asyncio.create_task(_publisher_loop(app))
logger.info("Dashy server started")
async def on_cleanup(app):
"""Cleanup app-level resources."""
task = app.get('publisher_task')
if task and not task.done():
task.cancel()
try:
await task
except (asyncio.CancelledError, Exception):
pass
logger.info("Dashy server stopped")
def setup_aiohttp_app(host: str, port: int, debug: bool):
logging.basicConfig(
level=logging.DEBUG if debug else logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
app = web.Application(middlewares=[no_cache_middleware])
# API routes
app.router.add_get("/api/init", init_api)
app.router.add_get("/api/files", list_files_api)
app.router.add_get("/api/play", serve_player_api)
app.router.add_get("/api/manifest.m3u8", serve_manifest_api)
app.router.add_get("/api/settings", get_settings_config_api)
app.router.add_get("/api/settings/params/{param_name}", get_param_api)
app.router.add_post("/api/settings/params/{param_name}", save_param_api)
app.router.add_get("/api/models", get_model_list_api)
app.router.add_post("/api/models/select", save_model_selection_api)
app.router.add_post("/api/action/{name}", run_action_api)
app.router.add_get("/api/ws", websocket_handler) # WebSocket for data streaming
# Static files
app.router.add_static('/media', path=DEFAULT_DIR, name='media', show_index=False, follow_symlinks=False)
app.router.add_static('/download', path=DEFAULT_DIR, name='download', show_index=False, follow_symlinks=False)
app.router.add_get("/", lambda r: web.FileResponse(os.path.join(WEB_DIST_PATH, "index.html")))
app.router.add_static("/", path=WEB_DIST_PATH)
app.on_startup.append(on_startup)
app.on_cleanup.append(on_cleanup)
return app
def main():
parser = argparse.ArgumentParser(description="Dashy Server")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to listen on")
parser.add_argument("--port", type=int, default=5088, help="Port to listen on")
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
args = parser.parse_args()
app = setup_aiohttp_app(args.host, args.port, args.debug)
web.run_app(app, host=args.host, port=args.port)
if __name__ == "__main__":
main()
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

-37
View File
@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Dashy by dragonpilot</title>
<link rel="icon" href="/icons/icon-192x192.png">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div id="app-container" class="w-full h-full">
<!-- Loading Overlay (shown when server is unreachable, hidden by JS when connected) -->
<div id="loading-screen" style="display: flex" class="fixed inset-0 z-[400] flex-col items-center justify-center bg-black/95">
<img src="/icons/dashy.png" alt="Dashy" class="w-24 h-24 mb-6">
<span class="loading loading-spinner loading-lg text-primary mb-4"></span>
<div id="loading-status" class="text-white/70 text-sm">Connecting to device...</div>
</div>
<!-- HUD Page (full-screen, default when enabled) -->
<div id="hud-page" class="hud-page">
<div id="hud-page-content" class="relative w-full h-full">
<!-- Video element created dynamically by theme if needed -->
<canvas id="uiCanvas" class="absolute inset-0 w-full h-full pointer-events-none z-10"></canvas>
</div>
</div>
</div>
<!-- Main app -->
<script src="/js/app.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-25
View File
@@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>HLS Player</title>
<style>
body, html { margin: 0; padding: 0; height: 100%; background-color: #000; }
#video { width: 100%; height: 100%; }
</style>
<script src="/lib/hls.js"></script>
</head>
<body>
<video id="video" controls autoplay></video>
<script>
var v = document.getElementById('video');
var s = '/api/manifest.m3u8?file={{FILE_PATH}}';
if (Hls.isSupported()) {
var h = new Hls();
h.loadSource(s);
h.attachMedia(v);
} else if (v.canPlayType('application/vnd.apple.mpegurl')) {
v.src = s;
}
</script>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

@@ -1,35 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

-161
View File
@@ -1,161 +0,0 @@
"""
Copyright (c) 2025, 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.
"""
import time
import numpy as np
from openpilot.common.swaglog import cloudlog
# Configuration parameters
SPEED_RATIO = 0.98 # Must be within 2% over cruise speed
TTC_THRESHOLD = 3.0 # seconds - disable ACM when lead is within this time
# Emergency thresholds - IMMEDIATELY disable ACM
EMERGENCY_TTC = 2.0 # seconds - emergency situation
EMERGENCY_RELATIVE_SPEED = 10.0 # m/s (~36 km/h closing speed - only for rapid closing)
EMERGENCY_DECEL_THRESHOLD = -1.5 # m/s² - if MPC wants this much braking, emergency disable
# Safety cooldown after lead detection
LEAD_COOLDOWN_TIME = 0.5 # seconds - brief cooldown to handle sensor glitches
# Speed-based distance scaling - more practical for real traffic
SPEED_BP = [0., 10., 20., 30.] # m/s (0, 36, 72, 108 km/h)
MIN_DIST_V = [15., 20., 25., 30.] # meters - closer to original 25m baseline
class ACM:
def __init__(self):
self.enabled = False
self._is_speed_over_cruise = False
self._has_lead = False
self._active_prev = False
self._last_lead_time = 0.0 # Track when we last saw a lead
self.active = False
self.just_disabled = False
def _check_emergency_conditions(self, lead, v_ego, current_time):
"""Check for emergency conditions that require immediate ACM disable."""
if not lead or not lead.status:
return False
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
relative_speed = v_ego - lead.vLead # Positive = closing
# Speed-adaptive minimum distance
min_dist_for_speed = np.interp(v_ego, SPEED_BP, MIN_DIST_V)
# Emergency disable conditions - only for truly dangerous situations
# Require BOTH close distance AND (fast closing OR very short TTC)
if lead.dRel < min_dist_for_speed and (
self.lead_ttc < EMERGENCY_TTC or
relative_speed > EMERGENCY_RELATIVE_SPEED):
self._last_lead_time = current_time
if self.active: # Only log if we're actually disabling
cloudlog.warning(f"ACM emergency disable: dRel={lead.dRel:.1f}m, TTC={self.lead_ttc:.1f}s, relSpeed={relative_speed:.1f}m/s")
return True
return False
def _update_lead_status(self, lead, v_ego, current_time):
"""Update lead vehicle detection status."""
if lead and lead.status:
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
if self.lead_ttc < TTC_THRESHOLD:
self._has_lead = True
self._last_lead_time = current_time
else:
self._has_lead = False
else:
self._has_lead = False
self.lead_ttc = float('inf')
def _check_cooldown(self, current_time):
"""Check if we're still in cooldown period after lead detection."""
time_since_lead = current_time - self._last_lead_time
return time_since_lead < LEAD_COOLDOWN_TIME
def _should_activate(self, user_ctrl_lon, v_ego, v_cruise, in_cooldown):
"""Determine if ACM should be active based on all conditions."""
self._is_speed_over_cruise = v_ego > (v_cruise * SPEED_RATIO)
return (not user_ctrl_lon and
not self._has_lead and
not in_cooldown and
self._is_speed_over_cruise)
def update_states(self, cc, rs, user_ctrl_lon, v_ego, v_cruise):
"""Update ACM state with multiple safety checks."""
# Basic validation
if not self.enabled or len(cc.orientationNED) != 3:
self.active = False
return
current_time = time.monotonic()
lead = rs.leadOne
# Check emergency conditions first (highest priority)
if self._check_emergency_conditions(lead, v_ego, current_time):
self.active = False
self._active_prev = self.active
return
# Update normal lead status
self._update_lead_status(lead, v_ego, current_time)
# Check cooldown period
in_cooldown = self._check_cooldown(current_time)
# Determine if ACM should be active
self.active = self._should_activate(user_ctrl_lon, v_ego, v_cruise, in_cooldown)
# Track state changes for logging
self.just_disabled = self._active_prev and not self.active
if self.active and not self._active_prev:
cloudlog.info(f"ACM activated: v_ego={v_ego*3.6:.1f} km/h, v_cruise={v_cruise*3.6:.1f} km/h")
elif self.just_disabled:
cloudlog.info("ACM deactivated")
self._active_prev = self.active
def update_a_desired_trajectory(self, a_desired_trajectory):
"""
Modify acceleration trajectory to allow coasting.
SAFETY: Check for any strong braking request and abort.
"""
if not self.active:
return a_desired_trajectory
# SAFETY CHECK: If MPC wants significant braking, DON'T suppress it
min_accel = np.min(a_desired_trajectory)
if min_accel < EMERGENCY_DECEL_THRESHOLD:
cloudlog.warning(f"ACM aborting: MPC requested {min_accel:.2f} m/s² braking")
self.active = False # Immediately deactivate
return a_desired_trajectory # Return unmodified trajectory
# Only suppress very mild braking (> -1.0 m/s²)
# This allows coasting but preserves any meaningful braking
modified_trajectory = np.copy(a_desired_trajectory)
for i in range(len(modified_trajectory)):
if -1.0 < modified_trajectory[i] < 0:
# Only suppress very gentle braking for cruise control
modified_trajectory[i] = 0.0
# Any braking stronger than -1.0 m/s² is preserved!
return modified_trajectory
-74
View File
@@ -1,74 +0,0 @@
"""
Copyright (c) 2025, 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.
"""
import time
import numpy as np
from openpilot.selfdrive.modeld.constants import ModelConstants
# Cooldown times (how long to stay in experimental mode after trigger)
AEM_COOLDOWN_TTC = 3.0 # seconds - for lead TTC events
AEM_DECEL_FOR_STOP = 2.5 # m/s² - assumed deceleration for stop cooldown calc
AEM_STOP_BUFFER = 2.0 # seconds - extra buffer for model latency
# Stop sign/light detection thresholds
SLOW_DOWN_BP = [0., 2.78, 5.56, 8.34, 11.12, 13.89, 15.28]
SLOW_DOWN_DIST = [10, 30., 50., 70., 80., 90., 120.]
# TTC-based triggering thresholds
TTC_THRESHOLD = 1.8 # seconds - trigger when TTC drops below this
MIN_SPEED_FOR_TTC = 5.0 # m/s (~18 km/h) - TTC meaningless at low speeds
MIN_CLOSING_SPEED = 0.5 # m/s - must be closing at least this fast
class AEM:
def __init__(self):
self._active = False
self._cooldown_end_time = 0.0
def _perform_experimental_mode(self, cooldown: float = AEM_COOLDOWN_TTC):
self._active = True
# Extend cooldown if new trigger comes in
new_end = time.monotonic() + cooldown
self._cooldown_end_time = max(self._cooldown_end_time, new_end)
def get_mode(self, mode):
# override mode
if time.monotonic() < self._cooldown_end_time:
mode = 'blended'
else:
self._active = False
return mode
def update_states(self, model_msg, radar_msg, v_ego):
# Stop sign/light detection - model predicts stopping ahead
# Uses max() so it can't shorten an existing longer cooldown
if len(model_msg.orientation.x) == len(model_msg.position.x) == ModelConstants.IDX_N and \
model_msg.position.x[ModelConstants.IDX_N - 1] < np.interp(v_ego, SLOW_DOWN_BP, SLOW_DOWN_DIST):
self._perform_experimental_mode(v_ego / AEM_DECEL_FOR_STOP + AEM_STOP_BUFFER)
# TTC-based triggering - lead car braking hard
if v_ego > MIN_SPEED_FOR_TTC and radar_msg.leadOne.status:
# vRel is negative when closing in on lead
closing_speed = -radar_msg.leadOne.vRel
if closing_speed > MIN_CLOSING_SPEED:
d_rel = radar_msg.leadOne.dRel
if d_rel > 0:
ttc = d_rel / closing_speed
if ttc < TTC_THRESHOLD:
self._perform_experimental_mode(AEM_COOLDOWN_TTC)
-41
View File
@@ -1,41 +0,0 @@
"""
Copyright (c) 2026, 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 cereal import log
# Hysteresis thresholds (km/h -> m/s)
APM_ACTIVATE_SPEED = 60 * 1000 / 3600 # 60 km/h — switch to aggressive below this
APM_DEACTIVATE_SPEED = 70 * 1000 / 3600 # 70 km/h — restore user personality above this
class APM:
def __init__(self):
self._active = False
def get_personality(self, v_ego, personality):
if self._active:
if v_ego > APM_DEACTIVATE_SPEED:
self._active = False
else:
if v_ego < APM_ACTIVATE_SPEED:
self._active = True
if self._active:
return log.LongitudinalPersonality.aggressive
return personality
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2019, rav4kumar, Rick Lan, dragonpilot community, and a number of other of contributors.
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.
"""
import numpy as np
NEARSIDE_PROB = 0.2
EDGE_PROB = 0.35
class RoadEdgeDetector:
def __init__(self, enabled = False):
self._is_enabled = enabled
self.left_edge_detected = False
self.right_edge_detected = False
def update(self, road_edge_stds, lane_line_probs):
if not self._is_enabled:
return
left_road_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
left_lane_nearside_prob = lane_line_probs[0]
right_road_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
right_lane_nearside_prob = lane_line_probs[3]
self.left_edge_detected = bool(
left_road_edge_prob > EDGE_PROB and
left_lane_nearside_prob < NEARSIDE_PROB and
right_lane_nearside_prob >= left_lane_nearside_prob
)
self.right_edge_detected = bool(
right_road_edge_prob > EDGE_PROB and
right_lane_nearside_prob < NEARSIDE_PROB and
left_lane_nearside_prob >= right_lane_nearside_prob
)
def set_enabled(self, enabled):
self._is_enabled = enabled
def is_enabled(self):
return self._is_enabled
-152
View File
@@ -1,152 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2025, 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.
"""
import subprocess
import time
from cereal import car, messaging
from openpilot.common.realtime import Ratekeeper
import threading
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
class Beepd:
def __init__(self, test=False):
self.current_alert = AudibleAlert.none
self._test = test
self.enable_gpio()
def enable_gpio(self):
try:
if self._test:
print("enabling GPIO")
subprocess.run("echo 42 | sudo tee /sys/class/gpio/export",
shell=True,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
encoding='utf8')
except Exception:
if self._test:
print("GPIO failed to enable")
pass
subprocess.run("echo \"out\" | sudo tee /sys/class/gpio/gpio42/direction",
shell=True,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
encoding='utf8')
def _beep(self, on):
val = "1" if on else "0"
subprocess.run(f"echo \"{val}\" | sudo tee /sys/class/gpio/gpio42/value",
shell=True,
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
encoding='utf8')
def engage(self):
if self._test:
print("beepd: engage")
self._beep(True)
time.sleep(0.05)
self._beep(False)
def disengage(self):
if self._test:
print("beepd: disengage")
for _ in range(2):
self._beep(True)
time.sleep(0.01)
self._beep(False)
time.sleep(0.01)
def prompt(self):
if self._test:
print("beepd: prompt")
for _ in range(3):
self._beep(True)
time.sleep(0.01)
self._beep(False)
time.sleep(0.01)
def warning_immediate(self):
if self._test:
print("beepd: warning_immediate")
for _ in range(5):
self._beep(True)
time.sleep(0.01)
self._beep(False)
time.sleep(0.01)
def dispatch_beep(self, func):
threading.Thread(target=func, daemon=True).start()
def update_alert(self, new_alert):
current_alert_played_once = self.current_alert == AudibleAlert.none
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
self.current_alert = new_alert
if new_alert == AudibleAlert.engage:
self.dispatch_beep(self.engage)
if new_alert == AudibleAlert.disengage:
self.dispatch_beep(self.disengage)
if new_alert == AudibleAlert.prompt:
self.dispatch_beep(self.prompt)
if new_alert == AudibleAlert.warningImmediate:
self.dispatch_beep(self.warning_immediate)
def get_audible_alert(self, sm):
if sm.updated['selfdriveState']:
new_alert = sm['selfdriveState'].alertSound.raw
self.update_alert(new_alert)
def test_beepd_thread(self):
frame = 0
rk = Ratekeeper(20)
pm = messaging.PubMaster(['selfdriveState'])
while True:
cs = messaging.new_message('selfdriveState')
if frame == 20:
cs.selfdriveState.alertSound = AudibleAlert.engage
if frame == 40:
cs.selfdriveState.alertSound = AudibleAlert.disengage
if frame == 60:
cs.selfdriveState.alertSound = AudibleAlert.prompt
if frame == 80:
cs.selfdriveState.alertSound = AudibleAlert.warningImmediate
pm.send("selfdriveState", cs)
frame += 1
rk.keep_time()
def beepd_thread(self):
if self._test:
threading.Thread(target=self.test_beepd_thread).start()
sm = messaging.SubMaster(['selfdriveState'])
rk = Ratekeeper(20)
while True:
sm.update(0)
self.get_audible_alert(sm)
rk.keep_time()
def main():
s = Beepd(test=False)
s.beepd_thread()
if __name__ == "__main__":
main()
-93
View File
@@ -1,93 +0,0 @@
import socket
import time
import pyray as rl
import qrcode
import numpy as np
from openpilot.common.swaglog import cloudlog
IP_REFRESH_INTERVAL = 5 # seconds
class DashyQR:
"""Shared QR code generator for dashy web UI."""
def __init__(self):
self._qr_texture: rl.Texture | None = None
self._last_qr_url: str | None = None
self._last_ip_check: float = 0
@property
def texture(self):
return self._qr_texture
@property
def url(self) -> str | None:
return self._last_qr_url
@staticmethod
def get_local_ip() -> str | None:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return None
@staticmethod
def get_web_ui_url() -> str:
ip = DashyQR.get_local_ip()
return f"http://{ip if ip else 'localhost'}:5088"
def _generate_qr_code(self, url: str) -> None:
try:
qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=0)
qr.add_data(url)
qr.make(fit=True)
pil_img = qr.make_image(fill_color="white", back_color="black").convert('RGBA')
img_array = np.array(pil_img, dtype=np.uint8)
if self._qr_texture and self._qr_texture.id != 0:
rl.unload_texture(self._qr_texture)
rl_image = rl.Image()
rl_image.data = rl.ffi.cast("void *", img_array.ctypes.data)
rl_image.width = pil_img.width
rl_image.height = pil_img.height
rl_image.mipmaps = 1
rl_image.format = rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
self._qr_texture = rl.load_texture_from_image(rl_image)
self._last_qr_url = url
except Exception as e:
cloudlog.warning(f"QR code generation failed: {e}")
self._qr_texture = None
def update(self, force: bool = False) -> bool:
"""Update QR code if needed. Returns True if updated."""
now = time.monotonic()
if not force and now - self._last_ip_check < IP_REFRESH_INTERVAL and self._qr_texture:
return False
self._last_ip_check = now
url = self.get_web_ui_url()
if url != self._last_qr_url:
self._generate_qr_code(url)
return True
return False
def force_update(self):
"""Force immediate IP check and QR regeneration."""
self._last_ip_check = 0
def cleanup(self):
"""Unload texture resources."""
if self._qr_texture and self._qr_texture.id != 0:
rl.unload_texture(self._qr_texture)
self._qr_texture = None
def __del__(self):
self.cleanup()
@@ -1,300 +0,0 @@
# Copyright (c) 2026, 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.
import ast
import os
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.widgets.scroller_tici import Scroller
from dragonpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.list_view import 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
from openpilot.system.hardware import HARDWARE
from dragonpilot.settings import SETTINGS, extract_depends_on_refs
LITE = os.getenv("LITE") is not None
MICI = HARDWARE.get_device_type() == "mici"
class DragonpilotLayout(Widget):
def __init__(self):
super().__init__()
self._scroller: Scroller | None = None
self._brand = ""
self._toggles = {}
self._toggle_metadata = {}
self._defaults: dict[str, str] = {} # key -> default value (fallback when param unset)
self._reverse_deps: dict[str, list[tuple[str, str]]] = {} # parent_key -> [(child_key, expr), ...]
self._item_factories = {
"toggle_item": toggle_item,
"spin_button_item": spin_button_item,
"double_spin_button_item": double_spin_button_item,
"text_spin_button_item": text_spin_button_item,
}
self._openpilot_longitudinal_control = False
if ui_state.CP is not None:
self._brand = ui_state.CP.brand
self._openpilot_longitudinal_control = ui_state.CP.openpilotLongitudinalControl
self._load_settings()
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 _load_settings(self):
settings_data = SETTINGS
self._build_dependency_maps(settings_data)
for i, section in enumerate(settings_data):
if not self._check_condition(section.get("condition")):
continue
title_key = f"title_{i}"
self._toggles[title_key] = simple_item(title=f"### {tr(section['title'])} ###")
count_after_title = len(self._toggles)
for setting in section.get("settings", []):
if self._check_condition(setting.get("condition")) and self._check_brands(setting.get("brands")):
self._create_item(setting)
# Drop the header if nothing rendered under it: all items filtered out
# (brand/condition) or no device widget factory for the item type
# (e.g. dashy-only text_display/text_input/action items). Avoids an
# orphan "### Section ###" with no controls.
if len(self._toggles) == count_after_title:
del self._toggles[title_key]
def _check_condition(self, condition):
if not condition:
return True
context = {"LITE": LITE, "MICI": MICI, "brand": self._brand, "openpilotLongitudinalControl": self._openpilot_longitudinal_control}
try:
return eval(condition, context)
except Exception:
return False
def _check_brands(self, brands):
"""Check if current brand is in the allowed brands list."""
if not brands:
return True # No brand restriction, show for all
return self._brand in brands
def _resolve(self, value):
"""Resolve callable values (lambdas) to their actual values."""
return value() if callable(value) else value
def _build_dependency_maps(self, settings_data):
"""Collect every UI item's default and invert depends_on into a reverse map."""
for section in settings_data:
for item in section.get("settings", []):
if "key" in item and "default" in item:
self._defaults[item["key"]] = str(item["default"])
for section in settings_data:
for item in section.get("settings", []):
expr = item.get("depends_on")
if not expr:
continue
refs = extract_depends_on_refs(expr)
if not refs:
continue
for parent_key in refs:
self._reverse_deps.setdefault(parent_key, []).append((item["key"], expr))
def _eval_depends_on(self, expr):
"""Evaluate a depends_on expression against current param-store values.
Returns True on any eval error so we fail open (item stays enabled)."""
refs = extract_depends_on_refs(expr)
if refs is None:
return True
bindings: dict = {}
for ref in refs:
raw = ui_state.params.get(ref)
val = raw.decode() if isinstance(raw, bytes) else raw
if val is None or val == "":
val = self._defaults.get(ref, "0")
try:
bindings[ref] = ast.literal_eval(val)
except (ValueError, SyntaxError):
bindings[ref] = val
try:
return bool(eval(expr, bindings))
except Exception:
return True
def _create_item(self, setting):
key = setting["key"]
item_type = setting["type"]
factory = self._item_factories.get(item_type)
if not factory:
return
# title and description support callables natively in ListItem
args = {"title": setting["title"]}
if setting.get("description"):
args["description"] = setting["description"]
param_name = setting.get("param_name") or key
# Handle initial values
if item_type == "toggle_item":
args["initial_state"] = ui_state.params.get_bool(param_name)
else:
raw_val = ui_state.params.get(param_name)
initial_val = raw_val.decode() if isinstance(raw_val, bytes) else raw_val
if initial_val is None:
initial_val = setting.get("default")
if item_type == "double_spin_button_item":
args["initial_value"] = float(initial_val)
elif item_type == "text_spin_button_item":
args["initial_index"] = int(initial_val)
else: # spin_button_item
args["initial_value"] = int(initial_val)
# Initial enabled state from depends_on
if "depends_on" in setting:
args["enabled"] = self._eval_depends_on(setting["depends_on"])
# Handle callback creation
primary_action = None
if param_name:
if item_type == "toggle_item":
primary_action = lambda val, p=param_name: ui_state.params.put_bool(p, bool(val))
elif item_type == "double_spin_button_item":
primary_action = lambda val, p=param_name: ui_state.params.put(p, float(val))
else: # spin_button_item, text_spin_button_item
primary_action = lambda val, p=param_name: ui_state.params.put(p, int(val))
# When this item changes, re-evaluate every child that depends on it
parent_deps = self._reverse_deps.get(key, [])
def combined_callback(val, deps=parent_deps):
if primary_action:
primary_action(val)
for child_key, expr in deps:
widget = self._toggles.get(child_key)
if widget is not None:
widget.action_item.set_enabled(self._eval_depends_on(expr))
if "callback" in setting and setting["callback"]:
args["callback"] = getattr(self, setting["callback"])
else:
args["callback"] = combined_callback
# D. Add other properties from JSON
for prop in ["min_val", "max_val", "step"]:
if prop in setting:
args[prop] = setting[prop]
# These properties don't support callables in the widgets, so resolve them
if "special_value_text" in setting:
args["special_value_text"] = self._resolve(setting["special_value_text"])
if "suffix" in setting:
args["suffix"] = self._resolve(setting["suffix"])
if "options" in setting:
args["options"] = [self._resolve(opt) for opt in setting["options"]]
widget = factory(**args)
self._toggles[key] = widget
if param_name:
self._toggle_metadata[key] = {
"widget": widget,
"param_name": param_name,
"item_type": item_type,
"default": setting.get("default")
}
def _reset_dp_conf(self):
def reset_dp_conf(result: int):
if result != DialogResult.CONFIRM:
return
ui_state.params.put_bool("dp_dev_reset_conf", True)
ui_state.params.put_bool("DoReboot", True)
dialog = ConfirmDialog(tr("Are you sure you want to reset ALL DP SETTINGS to default?"), tr("Reset"), callback=reset_dp_conf)
gui_app.push_widget(dialog)
def _refresh_visibility(self):
# ui_state.CP is None when this panel is first built (the UI starts before the car is up),
# so brand/longitudinal-gated sections would be filtered out and never come back. Re-read CP
# when the menu is shown and rebuild the section list if it changed. CP comes from
# CarParamsPersistent, so this also works offroad once the car has been identified once.
brand = ui_state.CP.brand if ui_state.CP is not None else ""
oplong = ui_state.CP.openpilotLongitudinalControl if ui_state.CP is not None else False
if brand == self._brand and oplong == self._openpilot_longitudinal_control:
return
self._brand = brand
self._openpilot_longitudinal_control = oplong
self._toggles = {}
self._toggle_metadata = {}
self._defaults = {}
self._reverse_deps = {}
self._load_settings()
self._toggles['btn_reset_dp_conf'] = self._reset_dp_conf_btn
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
def show_event(self):
self._refresh_visibility()
self._scroller.show_event()
self._update_toggles()
def _update_toggles(self):
ui_state.update_params()
# Refresh toggles from params to mirror external changes
for _, meta in self._toggle_metadata.items():
widget = meta["widget"]
param_name = meta["param_name"]
item_type = meta["item_type"]
default = meta.get("default")
if item_type == "toggle_item":
widget.action_item.set_state(ui_state.params.get_bool(param_name))
else: # Spinners
raw_val = ui_state.params.get(param_name)
val_str = None
if raw_val is not None:
if isinstance(raw_val, bytes):
val_str = raw_val.decode()
else:
val_str = str(raw_val)
elif default is not None:
val_str = str(default)
if val_str is None:
continue
if item_type == "double_spin_button_item":
widget.action_item.set_value(float(val_str))
elif item_type == "spin_button_item":
widget.action_item.set_value(int(val_str))
elif item_type == "text_spin_button_item":
widget.action_item.set_index(int(val_str))
def _render(self, rect):
self._scroller.render(rect)
@@ -1,60 +0,0 @@
import pyray as rl
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from dragonpilot.selfdrive.ui.dashy_qr import DashyQR
class DashyQRCode(Widget):
def __init__(self):
super().__init__()
self._qr = DashyQR()
self._title_label = UnifiedLabel(tr("scan to access"), font_size=32, font_weight=FontWeight.BOLD,
text_color=rl.WHITE, wrap_text=True)
self._subtitle_label = UnifiedLabel("dashy", font_size=48, font_weight=FontWeight.DISPLAY,
text_color=rl.WHITE)
self._or_label = UnifiedLabel(tr("or open browser"), font_size=24, font_weight=FontWeight.NORMAL,
text_color=rl.GRAY)
self._url_label = UnifiedLabel("", font_size=20, font_weight=FontWeight.NORMAL,
text_color=rl.GRAY, wrap_text=True)
def show_event(self):
self._qr.force_update()
def _render(self, rect: rl.Rectangle):
# Skip if off-screen (scroller renders all items, add small buffer for float precision)
if rect.x + rect.width < 1 or rect.x > gui_app.width - 1:
return
if self._qr.update():
self._url_label.set_text(self._qr.url or "")
# Left side: QR code (square, full height)
if self._qr.texture:
scale = rect.height / self._qr.texture.height
pos = rl.Vector2(rect.x, rect.y)
rl.draw_texture_ex(self._qr.texture, pos, 0.0, scale, rl.WHITE)
# Right side: Text
text_x = rect.x + rect.height + 16
text_width = int(rect.width - text_x)
# Title: "scan to access"
self._title_label.set_max_width(text_width)
self._title_label.set_position(text_x, rect.y)
self._title_label.render()
# Subtitle: "dashy"
self._subtitle_label.set_position(text_x, rect.y + 32)
self._subtitle_label.render()
# "or open browser"
self._or_label.set_position(text_x, rect.y + rect.height - 24 - 28)
self._or_label.render()
# URL
self._url_label.set_max_width(text_width)
self._url_label.set_position(text_x, rect.y + rect.height - 24)
self._url_label.render()
@@ -1,46 +0,0 @@
#!/usr/bin/env python3
import os
from openpilot.common.basedir import BASEDIR
from dragonpilot.system.ui.lib.multilang import TRANSLATIONS_DIR, multilang
LANGUAGES_FILE = os.path.join(str(TRANSLATIONS_DIR), "languages.json")
POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "dragonpilot.pot")
def update_translations():
files = []
for root, _, filenames in os.walk(os.path.join(BASEDIR, "dragonpilot")):
for filename in filenames:
if filename.endswith(".py"):
files.append(os.path.relpath(os.path.join(root, filename), BASEDIR))
# Create main translation file
cmd = ("xgettext -L Python --keyword=tr --keyword=trn:1,2 --keyword=tr_noop --from-code=UTF-8 " +
"--flag=tr:1:python-brace-format --flag=trn:1:python-brace-format --flag=trn:2:python-brace-format " +
f"-D {BASEDIR} -o {POT_FILE} {' '.join(files)}")
ret = os.system(cmd)
assert ret == 0
# Generate/update translation files for each language
for name in multilang.languages.values():
po_file = os.path.join(TRANSLATIONS_DIR, f"dragonpilot_{name}.po")
mo_file = os.path.join(TRANSLATIONS_DIR, f"dragonpilot_{name}.mo")
if os.path.exists(po_file):
cmd = f"msgmerge --update --no-fuzzy-matching --backup=none --sort-output {po_file} {POT_FILE}"
ret = os.system(cmd)
assert ret == 0
else:
cmd = f"msginit -l {name} --no-translator --input {POT_FILE} --output-file {po_file}"
ret = os.system(cmd)
assert ret == 0
# Compile .po to .mo
cmd = f"msgfmt {po_file} -o {mo_file}"
ret = os.system(cmd)
assert ret == 0
if __name__ == "__main__":
update_translations()
-190
View File
@@ -1,190 +0,0 @@
"""
Copyright (c) 2026, 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.
Dragonpilot settings aggregator.
Each feature branch drops a single `<branch>.py` in this directory. The module
exposes ITEMS - a list of dicts where each dict carries both UI fields (for the
dp settings panel) and param-storage fields (consumed at build time by
generate_settings.py to produce common/params_keys.h).
Param-only entries (no UI) just omit the UI fields - they still get picked up
by the C++ generator but the aggregator skips them.
At import time this module:
1. Globs every sibling *.py (except __init__.py)
2. Loads each via spec_from_file_location, collects ITEMS, validates shape
3. Groups UI items by their "section" field, orders sections per SECTION_ORDER
4. Exposes the result as SETTINGS, in the shape the UI panel expects
A failing import is logged and skipped - other features still load.
"""
import ast
import importlib.util
import sys
from pathlib import Path
try:
from dragonpilot.system.ui.lib.multilang import tr # noqa: F401 (re-export for feature files)
except ImportError:
from openpilot.system.ui.lib.multilang import tr # noqa: F401
SECTION_ORDER = [
"Toyota / Lexus",
"Honda",
"HKG",
"VAG",
"Mazda",
"Lateral",
"Longitudinal",
"UI",
"Device",
# Upstream openpilot toggle mirrors (dashy-only, gated by `condition: "DASHY"`).
"Openpilot",
"Developer",
]
# Brand-gated sections: the whole header + its items are hidden when the
# current car's brand doesn't match. Generic sections (Lateral/UI/...) are
# unconditional.
SECTION_CONDITIONS = {
"Toyota / Lexus": "brand == 'toyota'",
"Honda": "brand == 'honda'",
"HKG": "brand == 'hyundai'",
"VAG": "brand == 'volkswagen'",
"Mazda": "brand == 'mazda'",
}
_UI_REQUIRED_KEYS = {"section", "key", "type", "title"}
_KNOWN_ITEM_KEYS = _UI_REQUIRED_KEYS | {
# UI-side optional fields
"description", "default", "min_val", "max_val", "step", "suffix",
"special_value_text", "options", "brands", "condition",
"depends_on", "param_name", "callback",
# Dashy-only fields (no factory on the device dp panel; web UI consumes them).
# text_display_item: read-only render of a param's value.
# text_input_item: text field that POSTs typed value to the named action endpoint.
# action_item: button that POSTs to the named action endpoint with no payload.
"action",
# Param-storage fields (consumed by generate_settings.py, ignored by UI)
"flags", "param_type",
}
def extract_depends_on_refs(expr):
"""Pull referenced param keys out of a depends_on expression like 'dp_x > 0 and dp_y == 1'."""
try:
tree = ast.parse(expr, mode="eval")
except SyntaxError:
return None # caller handles
return {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
def _validate_item(item, source):
"""Validate an item for UI rendering. Returns True if the item should be rendered."""
key = item.get("key", "?")
unknown = item.keys() - _KNOWN_ITEM_KEYS
if unknown:
print(f"[dragonpilot.settings] {source}: item {key} has unknown keys {unknown}")
# Param-only entries (no "title") aren't rendered - skip silently.
if "title" not in item:
return False
missing = _UI_REQUIRED_KEYS - item.keys()
if missing:
print(f"[dragonpilot.settings] {source}: item {key} missing UI keys {missing}")
return False
if not callable(item["title"]):
print(f"[dragonpilot.settings] {source}: item {key} title must be callable, e.g. lambda: tr(...)")
return False
return True
def _load_feature(py_file):
# Filenames mirror branch names (e.g. "min-feat.lat.alka-v2.py"); sanitize for sys.modules.
safe = py_file.stem.replace("-", "_").replace(".", "_")
module_name = f"_dp_feature_{safe}"
spec = importlib.util.spec_from_file_location(module_name, py_file)
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
return getattr(mod, "ITEMS", [])
def _check_dangling_refs(ui_items, all_keys):
"""Warn loudly when a depends_on expression names a key that isn't declared anywhere.
The UI silently no-ops on missing refs, which lets typos hide forever."""
for source, item in ui_items:
expr = item.get("depends_on")
if not expr:
continue
key = item.get("key", "?")
refs = extract_depends_on_refs(expr)
if refs is None:
print(f"[dragonpilot.settings] {source}: {key}.depends_on {expr!r} is not valid Python")
continue
for ref in refs:
if ref not in all_keys:
print(f"[dragonpilot.settings] {source}: {key}.depends_on references {ref!r} "
f"which is not defined in any feature file")
def _build_settings():
settings_dir = Path(__file__).parent
by_section: dict[str, list] = {}
all_keys: set[str] = set()
ui_items: list[tuple[str, dict]] = [] # (source filename, item) for cross-ref check
for py_file in sorted(settings_dir.glob("*.py")):
if py_file.name == "__init__.py":
continue
try:
items = _load_feature(py_file)
except Exception as e:
print(f"[dragonpilot.settings] Failed to load {py_file.name}: {e}")
continue
for item in items:
if "key" in item:
all_keys.add(item["key"])
if not _validate_item(item, py_file.name):
continue
ui_items.append((py_file.name, item))
by_section.setdefault(item["section"], []).append(item)
_check_dangling_refs(ui_items, all_keys)
def _section_entry(title, items):
entry = {"title": title, "settings": items}
cond = SECTION_CONDITIONS.get(title)
if cond:
entry["condition"] = cond
return entry
result = []
for section in SECTION_ORDER:
if section in by_section:
result.append(_section_entry(section, by_section[section]))
for section, items in by_section.items():
if section not in SECTION_ORDER:
result.append(_section_entry(section, items))
return result
SETTINGS = _build_settings()
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Honda",
"key": "dp_honda_nidec_stock_long",
"type": "toggle_item",
"title": lambda: tr("Use Stock Longitudinal (Nidec)"),
"description": lambda: tr("Let the Honda Nidec ACC handle gas and brake instead of openpilot. Lateral control (steering) still runs through openpilot."),
"brands": ["honda"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Toyota / Lexus",
"key": "dp_toyota_stock_lon",
"type": "toggle_item",
"title": lambda: tr("Use Stock Longitudinal Control"),
"description": lambda: tr("Let the car's built-in ACC handle gas and brake instead of openpilot. Lateral control (steering) still runs through openpilot."),
"brands": ["toyota"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Toyota / Lexus",
"key": "dp_toyota_tss1_sng",
"type": "toggle_item",
"title": lambda: tr("Enable Stop-and-Go on TSS1"),
"description": lambda: tr("Restores stop-and-go behavior on Toyota Safety Sense 1.0 vehicles, allowing openpilot to resume from a full stop without driver intervention."),
"brands": ["toyota"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
-15
View File
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "VAG",
"key": "dp_vag_a0_sng",
"type": "toggle_item",
"title": lambda: tr("Enable Stop-and-Go on A0 Platform"),
"description": lambda: tr("Restores stop-and-go behavior on VAG A0 platform vehicles (Polo, Fabia, Ibiza, etc.), allowing openpilot to resume from a full stop without driver intervention."),
"brands": ["volkswagen"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "VAG",
"key": "dp_vag_avoid_eps_lockout",
"type": "toggle_item",
"title": lambda: tr("Avoid EPS Lockout"),
"description": lambda: tr("Scale steering torque down at low speeds to avoid EPS lockout."),
"brands": ["volkswagen"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Toyota / Lexus",
"key": "dp_toyota_door_auto_lock_unlock",
"type": "toggle_item",
"title": lambda: tr("Door Auto Lock/Unlock"),
"description": lambda: tr("Enable openpilot to auto-lock doors above 20 km/h and auto-unlock when shifting to Park."),
"brands": ["toyota"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,16 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_audible_alert_mode",
"type": "text_spin_button_item",
"title": lambda: tr("Audible Alert"),
"description": lambda: tr("Std.: Stock behaviour.<br>Warning: Only emits sound when there is a warning.<br>Off: Does not emit any sound at all."),
"options": [lambda: tr("Std."), lambda: tr("Warning"), lambda: tr("Off")],
"default": "0",
"condition": "not LITE",
"flags": "PERSISTENT",
"param_type": "INT",
},
]
@@ -1,14 +0,0 @@
settings:
- key: dp_dev_audible_alert_mode
type: text_spin_button_item
title: "声音提示"
description: "标准:默认行为。<br>警告:仅在出现警告时发出声音。<br>关闭:完全静音。"
category: "Device"
condition: "not LITE"
default: 0
options:
- "标准"
- "警告"
- "关闭"
flags: PERSISTENT
param_type: INT
@@ -1,20 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_auto_shutdown_in",
"type": "spin_button_item",
"title": lambda: tr("Auto Shutdown After"),
"description": lambda: tr("0 min = Immediately"),
"default": "-5",
"min_val": -5,
"max_val": 300,
"step": 5,
"suffix": lambda: tr("min"),
"special_value_text": lambda: tr("Off"),
"condition": "not MICI",
"flags": "PERSISTENT",
"param_type": "INT",
},
]
@@ -1,15 +0,0 @@
settings:
- key: dp_dev_auto_shutdown_in
type: spin_button_item
title: "自动关机时间"
description: "0 分钟 = 立即关机"
category: "Device"
condition: "not MICI"
default: -5
min_val: -5
max_val: 300
step: 5
suffix: "分钟"
special_value_text: "关闭"
flags: PERSISTENT
param_type: INT
@@ -1,14 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_dashy",
"type": "toggle_item",
"title": lambda: tr("Enable dashy Visual"),
"description": lambda: tr("dashy - dragonpilot's all-in-one system hub.<br><br>Visit http://&lt;device_ip&gt;:5088 to access.<br><br>Enable this to use Tesla Visual/HUD."),
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,21 +0,0 @@
settings:
- key: dp_dev_dashy
type: toggle_item
title: "dashy HUD 仪表盘"
description: "dashy - dragonpilot 的一体化系统中心。<br><br>访问 http://<设备IP>:5088 打开。<br><br>启用此选项以使用特斯拉风格 HUD。"
category: "Device"
flags: PERSISTENT
param_type: BOOL
default: "0"
- key: dp_maa_route
category: "Device"
flags: CLEAR_ON_MANAGER_START
param_type: JSON
- key: dp_maa_destination
category: "Device"
flags: PERSISTENT
param_type: JSON
- key: dp_maa_places
category: "Device"
flags: PERSISTENT
param_type: JSON
@@ -1,19 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_delay_loggerd",
"type": "spin_button_item",
"title": lambda: tr("Delay Starting Loggerd for:"),
"description": lambda: tr("Delays the startup of loggerd and its related processes when the device goes on-road.<br>This prevents the initial moments of a drive from being recorded, protecting location privacy at the start of a trip."),
"default": "0",
"min_val": 0,
"max_val": 300,
"step": 5,
"suffix": lambda: tr("sec"),
"special_value_text": lambda: tr("Off"),
"flags": "PERSISTENT",
"param_type": "INT",
},
]
@@ -1,14 +0,0 @@
settings:
- key: dp_dev_delay_loggerd
type: spin_button_item
title: "延迟启动 Loggerd"
description: "当设备上路时延迟启动 loggerd 及其相关进程。<br>防止行程刚开始时的数据被录制,保护行程起始位置隐私。"
category: "Device"
default: 0
min_val: 0
max_val: 300
step: 5
suffix: "秒"
special_value_text: "关闭"
flags: PERSISTENT
param_type: INT
@@ -1,14 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_disable_connect",
"type": "toggle_item",
"title": lambda: tr("Disable Comma Connect"),
"description": lambda: tr("Disable Comma connect service if you do not wish to upload / being tracked by the service."),
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,9 +0,0 @@
settings:
- key: dp_dev_disable_connect
type: toggle_item
title: "禁用 Comma Connect"
description: "如果您不希望上传数据或被服务追踪,可禁用 Comma connect 服务。"
category: "Device"
flags: PERSISTENT
param_type: BOOL
default: "0"
@@ -1,4 +0,0 @@
ITEMS = [
{"key": "dp_dev_model_selected", "flags": "PERSISTENT", "param_type": "STRING"},
{"key": "dp_dev_model_list", "flags": "PERSISTENT", "param_type": "JSON"},
]
@@ -1,9 +0,0 @@
settings:
- key: dp_dev_model_selected
category: "Device"
flags: PERSISTENT
param_type: STRING
- key: dp_dev_model_list
category: "Device"
flags: PERSISTENT
param_type: STRING
@@ -1,26 +0,0 @@
{
"settings": [
{
"key": "dp_dev_is_rhd",
"type": "toggle_item",
"title": "Enable Right-Hand Drive Mode",
"description": "Allow openpilot to obey right-hand traffic conventions on right driver seat.",
"category": "Device",
"condition": "LITE",
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0"
},
{
"key": "dp_dev_beep",
"type": "toggle_item",
"title": "Enable Beep (Warning)",
"description": "Use Buzzer for audiable alerts.",
"category": "Device",
"condition": "LITE",
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0"
}
]
}
@@ -1,26 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_is_rhd",
"type": "toggle_item",
"title": lambda: tr("Enable Right-Hand Drive Mode"),
"description": lambda: tr("Allow openpilot to obey right-hand traffic conventions on right driver seat."),
"condition": "LITE",
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
{
"section": "Device",
"key": "dp_dev_beep",
"type": "toggle_item",
"title": lambda: tr("Enable Beep (Warning)"),
"description": lambda: tr("Use Buzzer for audiable alerts."),
"condition": "LITE",
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,20 +0,0 @@
settings:
- key: dp_dev_is_rhd
type: toggle_item
title: "启用右舵驾驶模式"
description: "允许 openpilot 在右驾驶座位上遵守右侧交通规则。"
category: "Device"
condition: "LITE"
flags: PERSISTENT
param_type: BOOL
default: "0"
- key: dp_dev_beep
type: toggle_item
title: "启用蜂鸣器 (警告)"
description: "使用蜂鸣器进行声音提示。"
category: "Device"
condition: "LITE"
flags: PERSISTENT
param_type: BOOL
default: "0"
@@ -1,14 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Device",
"key": "dp_dev_opview",
"type": "toggle_item",
"title": lambda: tr("Enable opview"),
"description": lambda: tr("Broadcasts telemetry to the opview App (available on Android). Requires the companion App to be running on an external display."),
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,9 +0,0 @@
settings:
- key: dp_dev_opview
type: toggle_item
title: "启用 opview"
description: "向 opview AppAndroid 可用)广播遥测数据。需要在外接显示器上运行配套 App。"
category: "Device"
flags: PERSISTENT
param_type: BOOL
default: "0"
@@ -1,3 +0,0 @@
ITEMS = [
{"key": "dp_dev_tethering", "flags": "PERSISTENT", "param_type": "BOOL", "default": "0"},
]
@@ -1,6 +0,0 @@
settings:
- key: dp_dev_tethering
category: "Device"
flags: PERSISTENT
param_type: BOOL
default: "0"
@@ -1,15 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Lateral",
"key": "dp_lat_alka",
"type": "toggle_item",
"title": lambda: tr("Always-on Lane Keeping Assist (ALKA)"),
"description": lambda: tr("Enable lateral control even when ACC/cruise is disengaged, using ACC Main or LKAS button to toggle. Vehicle must be moving."),
"brands": ["toyota", "hyundai", "honda", "volkswagen", "subaru", "mazda", "nissan", "ford"],
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,10 +0,0 @@
settings:
- key: dp_lat_alka
type: toggle_item
title: "全时车道保持辅助 (ALKA)"
description: "即使在 ACC/巡航未启用时也能启用横向控制,通过 ACC Main 或 LKAS 按钮切换。车辆必须在行驶中。"
category: "Lateral"
brands: ["toyota", "hyundai", "honda", "volkswagen", "subaru", "mazda", "nissan", "ford"]
flags: PERSISTENT
param_type: BOOL
default: "0"
@@ -1,18 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Lateral",
"key": "dp_lat_offset_cm",
"type": "spin_button_item",
"title": lambda: tr("Position Offset"),
"description": lambda: tr("Fine-tune where the car drives within the lane. Positive values move the car left, negative values move right.<br>Recommended to start with small values (±5cm) and adjust based on preference."),
"default": "0",
"min_val": -15,
"max_val": 15,
"step": 1,
"suffix": lambda: tr("cm"),
"flags": "PERSISTENT",
"param_type": "INT",
},
]
@@ -1,13 +0,0 @@
settings:
- key: dp_lat_offset_cm
type: spin_button_item
title: "位置偏移"
description: "微调车辆在车道内的行驶位置。正值向左偏移,负值向右偏移。<br>建议从较小的数值(±5cm)开始,根据偏好调整。"
category: "Lateral"
default: 0
min_val: -15
max_val: 15
step: 1
suffix: "cm"
flags: PERSISTENT
param_type: INT
-35
View File
@@ -1,35 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Lateral",
"key": "dp_lat_lca_speed",
"type": "spin_button_item",
"title": lambda: tr("Lane Change Assist At:"),
"description": lambda: tr("Off = Disable LCA.<br>1 mph = 1.2 km/h."),
"default": "20",
"min_val": 0,
"max_val": 100,
"step": 5,
"suffix": lambda: tr("mph"),
"special_value_text": lambda: tr("Off"),
"flags": "PERSISTENT",
"param_type": "INT",
},
{
"section": "Lateral",
"key": "dp_lat_lca_auto_sec",
"type": "double_spin_button_item",
"title": lambda: tr("+ Auto Lane Change after:"),
"description": lambda: tr("Off = Disable Auto Lane Change."),
"default": "0.0",
"min_val": 0.0,
"max_val": 5.0,
"step": 0.5,
"suffix": lambda: tr("sec"),
"special_value_text": lambda: tr("Off"),
"depends_on": "dp_lat_lca_speed > 0",
"flags": "PERSISTENT",
"param_type": "FLOAT",
},
]
@@ -1,38 +0,0 @@
settings:
- key: dp_lat_lca_speed
type: spin_button_item
title: "变道辅助速度:"
description: "关闭 = 禁用变道辅助。<br>1 mph = 1.2 km/h。"
category: "Lateral"
default: 20
min_val: 0
max_val: 100
step: 5
suffix: "mph"
special_value_text: "关闭"
on_change:
- target: dp_lat_lca_auto_sec
action: set_enabled
condition: "value > 0"
flags: PERSISTENT
param_type: INT
default: "20"
- key: dp_lat_lca_auto_sec
type: double_spin_button_item
title: "+ 自动变道延迟:"
description: "关闭 = 禁用自动变道。"
category: "Lateral"
default: 0.0
min_val: 0.0
max_val: 5.0
step: 0.5
suffix: "秒"
special_value_text: "关闭"
initially_enabled_by:
param: dp_lat_lca_speed
condition: "value > 0"
default: 20
flags: PERSISTENT
param_type: FLOAT
default: "0.0"
@@ -1,14 +0,0 @@
from dragonpilot.settings import tr
ITEMS = [
{
"section": "Lateral",
"key": "dp_lat_road_edge_detection",
"type": "toggle_item",
"title": lambda: tr("Road Edge Detection (RED)"),
"description": lambda: tr("Block lane change assist when the system detects the road edge.<br>NOTE: This will show 'Car Detected in Blindspot' warning."),
"flags": "PERSISTENT",
"param_type": "BOOL",
"default": "0",
},
]
@@ -1,9 +0,0 @@
settings:
- key: dp_lat_road_edge_detection
type: toggle_item
title: "道路边缘检测 (RED)"
description: "当系统检测到道路边缘时阻止变道辅助。<br>注意:这将显示「检测到盲区有车辆」警告。"
category: "Lateral"
flags: PERSISTENT
param_type: BOOL
default: "0"

Some files were not shown because too many files have changed in this diff Show More