Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dcc048299 |
@@ -0,0 +1,6 @@
|
||||
Wen
|
||||
REGIST
|
||||
PullRequest
|
||||
cancelled
|
||||
FOF
|
||||
NoO
|
||||
@@ -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
|
||||
@@ -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/**}"
|
||||
|
||||
@@ -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
|
||||
@@ -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/*
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||

|
||||
## ✍ To install this fork use installer.comma.ai/infiniteCable2/master (Comma Four compatible)
|
||||
|
||||
[Read this in English](README_EN.md)
|
||||

|
||||
|
||||
# **🐲 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.ai’s 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
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||

|
||||
|
||||
[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!**
|
||||
@@ -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)`
|
||||
|
||||
[](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
|
||||
[](LICENSE)
|
||||
[](https://x.com/comma_ai)
|
||||
[](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>
|
||||
@@ -1,3 +1,7 @@
|
||||
Version 0.11.2 (2026-06-15)
|
||||
========================
|
||||
|
||||
|
||||
Version 0.11.1 (2026-05-18)
|
||||
========================
|
||||
* New driver monitoring model
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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())}
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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-"
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"}},
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 @@
|
||||
#define COMMA_VERSION "0.11.1"
|
||||
#define COMMA_VERSION "0.11.2"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> |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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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|[](##)|[](##)|<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>|||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 145 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
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 |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
@@ -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://<device_ip>: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 App(Android 可用)广播遥测数据。需要在外接显示器上运行配套 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
|
||||
@@ -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"
|
||||