mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-06-12 04:34:30 +08:00
Compare commits
7 Commits
pre-build
...
0.10.3-ody
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84968fe6b6 | ||
|
|
857d58fcf8 | ||
|
|
dab1c5b7e0 | ||
|
|
e975fdcd6c | ||
|
|
29beffdd30 | ||
|
|
ef7cd06332 | ||
|
|
7950dee9a1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -95,3 +95,6 @@ Pipfile
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# rick - keep panda_tici standalone
|
||||
panda_tici/
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -4,5 +4,8 @@
|
||||
"ms-vscode.cpptools",
|
||||
"elagil.pre-commit-helper",
|
||||
"charliermarsh.ruff",
|
||||
"JamiTech.simply-blame",
|
||||
"k--kato.intellij-idea-keybindings",
|
||||
"trinm1709.dracula-theme-from-intellij"
|
||||
]
|
||||
}
|
||||
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -3,10 +3,24 @@
|
||||
"editor.insertSpaces": true,
|
||||
"editor.renderWhitespace": "trailing",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.defaultProfile.linux": "dragonpilot",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"dragonpilot": {
|
||||
"path": "bash",
|
||||
"args": ["-c", "distrobox enter dp"]
|
||||
}
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.venv": true,
|
||||
"**/__pycache__": true
|
||||
"**/__pycache__": true,
|
||||
"msgq_repo/": true,
|
||||
"rednose/": true,
|
||||
"rednose_repo/": true,
|
||||
"openpilot/": true,
|
||||
"teleoprtc_repo/": true,
|
||||
"tinygrad/": true,
|
||||
"tinygrad_repo/": true
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
|
||||
140
ALKA_DESIGN.md
Normal file
140
ALKA_DESIGN.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 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)
|
||||
30
LICENSE.md
Normal file
30
LICENSE.md
Normal file
@@ -0,0 +1,30 @@
|
||||
Copyright (c) 2019, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
Copyright (c) 2018, Comma.ai, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
125
README.md
125
README.md
@@ -1,111 +1,74 @@
|
||||
<div align="center" style="text-align: center;">
|
||||

|
||||
|
||||
<h1>openpilot</h1>
|
||||
[Read this in English](README_EN.md)
|
||||
|
||||
<p>
|
||||
<b>openpilot is an operating system for robotics.</b>
|
||||
<br>
|
||||
Currently, it upgrades the driver assistance system in 300+ supported cars.
|
||||
</p>
|
||||
# **🐲 dragonpilot - 賦予您的愛車「龍」之魂**
|
||||
|
||||
<h3>
|
||||
<a href="https://docs.comma.ai">Docs</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
|
||||
<span> · </span>
|
||||
<a href="https://discord.comma.ai">Community</a>
|
||||
<span> · </span>
|
||||
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
|
||||
</h3>
|
||||
**我們與您一同翱翔於更智慧、更貼心的駕駛旅程。**
|
||||
|
||||
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
|
||||
## **👋 嘿, 朋友,歡迎您的到來!**
|
||||
|
||||
[](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
|
||||
[](LICENSE)
|
||||
[](https://x.com/comma_ai)
|
||||
[](https://discord.comma.ai)
|
||||
`dragonpilot` 誕生於 2019 年,由三位早期的 openpilot 華人玩家共同創立。初衷很簡單:為廣大的華人用戶、玩家們提供一個友善的交流環境、更簡便的設定協助,並加入更多適合在地使用的貼心功能。
|
||||
|
||||
</div>
|
||||
我們深知在地化的重要性,特別是語言的親切感。因此,我們率先導入了完整的中文介面,讓 `dragonpilot` 迅速在華語地區累積了口碑,也讓華人的使用者數量在全球名列前茅。這份來自在地的支持,是我們持續前進的最大動力。
|
||||
|
||||
<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>
|
||||
我們以功能強大的 [openpilot](https://github.com/commaai/openpilot) 為基礎——這套據美國消費者報告評測優於市售車方案的開源輔助駕駛系統——融入了更多在地化的巧思與客製化的溫度,希望能打造出最符合您需求的駕駛夥伴。(您也可以參考我們 repo 中保留的 [openpilot 原始說明檔案](README_OPENPILOT.md))
|
||||
|
||||
取名 `dragonpilot`,是因為我們希望它能像神話中的「龍」一樣,既強大又充滿智慧,為您的行車安全保駕護航。龍,在我們華人文化中,更是吉祥與力量的象徵,也代表著我們的根源與驕傲。
|
||||
|
||||
Using openpilot in a car
|
||||
------
|
||||
## **✨ dragonpilot 的里程碑**
|
||||
|
||||
To use openpilot in a car, you need four things:
|
||||
1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
|
||||
2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
|
||||
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
|
||||
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car.
|
||||
我們不僅保留了 openpilot 的核心優勢,更達成了許多從社群回饋中誕生的里程碑,這些是我們引以為傲的足跡:
|
||||
|
||||
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
|
||||
* **🚘 全時置中車道維持 (ALKA)**
|
||||
|
||||
這不只是一個功能,更是 `dragonpilot` 的哲學。我們最早於 [0.6.2 版本](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199) 時便實現了這個功能,其開發歷程始於 2017 Lexus IS300h,接著擴展至 Toyota 全車系,並逐步延伸到其他支援的品牌。它能溫柔地輔助您,讓車輛始終穩定地保持在車道中央,提供一份額外的安心與從容。
|
||||
|
||||
### Branches
|
||||
* **🌐 率先導入多國語言介面**
|
||||
|
||||
Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches:
|
||||
在官方 openpilot 還未支援前,我們便已將多國語言介面實現。`dragonpilot` 完整支援繁體中文、簡體中文與英文,讓操作毫無隔閡。
|
||||
|
||||
| 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
|
||||
------
|
||||
我們是唯一曾致力於讓專案同時兼容 EON、comma two、comma 3 與 Jetson 平台的社群分支,這份努力是為了服務最廣大的玩家社群。
|
||||
此外,在 comma.ai 團隊於 0.10.0 版本宣布停止支持 comma 3 後,我們仍是唯一一個完整同時支援 comma 3、comma 3X 以及 O3、O3L、O3XL(O3 系列為副廠硬體)的社群分支。
|
||||
|
||||
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)
|
||||
基於活躍的社群與功能創新,`dragonpilot` 曾一度成長為 comma ai 官方認證的第一大 openpilot 分支,這份榮耀屬於每一位參與者。
|
||||
|
||||
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.
|
||||
## **🧑💻 設計理念 - 少即是多 (Less is More)**
|
||||
|
||||
Safety and Testing
|
||||
----
|
||||
隨著 openpilot 的 AI 模型日益強大,許多過去需要手動微調的功能,現在都已能透過更先進的模型來實現。因此,我們現在的開發重心回歸到 **「最小化修改」(minimal changes)** 的核心原則上。
|
||||
|
||||
* 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.
|
||||
我們的目標是為您提供最純粹、最接近官方的 openpilot 駕駛感受,同時保留 `dragonpilot` 那些經過時間考驗、最受社群喜愛的經典功能。我們相信,在強大的 AI 基礎上,簡潔即是力量。
|
||||
|
||||
<details>
|
||||
<summary>MIT Licensed</summary>
|
||||
## **🛠️ 硬件的足跡 - 一路走來的夥伴們**
|
||||
|
||||
openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified.
|
||||
從最早的 **EON**,到官方的 **comma two / three (C2/C3/C3X)**,再到社群中各式各樣充滿智慧的**副廠機 (如 C1.5, O2, O3, O3L, O3XL 等)**,甚至我們也曾探索過在 [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot) 上的可能性。
|
||||
|
||||
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.
|
||||
目前最新版本主要支援: comma3 / 3X 以及 O3 / O3L / O3XL 等社群硬體。
|
||||
針對 EON / C1.5 / C2 等舊款硬體,最後支援的版本位於 [d2 分支](https://github.com/dragonpilot-community/dragonpilot/tree/d2)。
|
||||
無論您手上是哪一款設備,都代表著您對開源駕駛輔助的一份熱情。
|
||||
|
||||
**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>
|
||||
`dragonpilot` 的成長,離不開每一位使用者的貢獻與回饋。我們是一個以**公開、透明**為原則的溫暖社群,希望在這裡能與所有對 openpilot / dragonpilot 有興趣的用戶分享、交流開發與使用上的經驗。
|
||||
|
||||
By default, openpilot uploads the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone.
|
||||
[**歡迎加入我們的 Facebook 社團進行交流!**](https://www.facebook.com/groups/930190251238639)
|
||||
|
||||
openpilot is open source software: the user is free to disable data collection if they wish to do so.
|
||||
## **❤️ 特別感謝**
|
||||
|
||||
openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs.
|
||||
The driver-facing camera and microphone are only logged if you explicitly opt-in in settings.
|
||||
`dragonpilot` 從創立至今,從未打算透過 Patreon 等平台進行任何形式的募資。我們的初衷是建立一個讓大家能一起學習、一起成長的社群。It's all about fun, not money.
|
||||
|
||||
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>
|
||||
然而,我們仍要對那些自發性支持本專案的朋友們,致上最誠摯的感謝。正是因為有您們的鼓勵,我們才有更大的動力持續前進。
|
||||
|
||||
[**我們的贊助者名單**](SPONSORS.md)
|
||||
|
||||
### **安全聲明**
|
||||
|
||||
`dragonpilot` 是一種駕駛**輔助**系統,並非全自動駕駛。它旨在減輕您的駕駛疲勞,提升行車安全,但駕駛人仍需時刻保持專注,並隨時準備接管車輛。請務必遵守您所在地區的交通法規。
|
||||
|
||||
**最後,再次感謝您的到來。**
|
||||
|
||||
**期待與您一同在智慧駕駛的道路上,乘「龍」而行!**
|
||||
74
README_EN.md
Normal file
74
README_EN.md
Normal file
@@ -0,0 +1,74 @@
|
||||

|
||||
|
||||
[Read this in Chinese](README.md)
|
||||
|
||||
# **🐲 dragonpilot - Bringing the Spirit of the Dragon to Your Car**
|
||||
|
||||
**Join us on a smarter, more thoughtful driving journey.**
|
||||
|
||||
## **👋 Welcome, friend!**
|
||||
|
||||
`dragonpilot` was launched in 2019 by three early openpilot enthusiasts from the Chinese community. Our mission was simple: create a friendly space for users to share experiences, provide easier setup help, and add features tailored for local needs.
|
||||
|
||||
Localization has always been at the heart of what we do—starting with a fully Chinese interface. This made `dragonpilot` quickly popular in Chinese-speaking regions and helped our user base grow into one of the largest worldwide. That community support is what keeps us moving forward.
|
||||
|
||||
Built on top of the powerful [openpilot](https://github.com/commaai/openpilot)—an open-source driver assistance system rated by Consumer Reports as outperforming commercial offerings—we add localized refinements and user-focused features to create a driving companion that truly fits your needs. (You can also see the [original openpilot README](README_OPENPILOT.md) preserved in our repo.)
|
||||
|
||||
The name `dragonpilot` reflects our vision: like the dragon of mythology, it is strong and wise, guarding your safety on the road. In Chinese culture, the dragon is also a symbol of luck and strength, representing our roots and pride.
|
||||
|
||||
## **✨ Milestones**
|
||||
|
||||
Beyond carrying forward openpilot's core strengths, we've reached several milestones inspired by community feedback:
|
||||
|
||||
* **🚘 Always Lane Keep Assist (ALKA)**
|
||||
|
||||
More than a feature—it's part of the `dragonpilot` philosophy. Introduced as early as [version 0.6.2](https://github.com/dragonpilot-community/dragonpilot/blob/2861467183d62151024320447ba04d18fc3fe1e6/selfdrive/car/toyota/carstate.py#L199), first tested on a 2017 Lexus IS300h, then expanded to Toyota's lineup and beyond. ALKA helps keep your vehicle steadily centered, giving you extra confidence on the road.
|
||||
|
||||
* **🌐 First to add multilingual support**
|
||||
|
||||
Before openpilot officially supported it, we had already introduced multiple languages. `dragonpilot` fully supports Traditional Chinese, Simplified Chinese, and English.
|
||||
|
||||
* **💻 Only community fork to support multiple hardware platforms at once**
|
||||
|
||||
We uniquely worked to make the project run on EON, comma two, comma 3, and Jetson—serving the widest range of users possible.
|
||||
Additionally, after the comma.ai team deprecated the comma 3 in version 0.10.0, we remain the only community fork to offer full, simultaneous support for the comma 3, comma 3X, and the O3, O3L, and O3XL (the O3 series being third-party hardware).
|
||||
|
||||
* **📜 Once recognized as the #1 openpilot fork**
|
||||
|
||||
Thanks to an active community and continuous innovation, `dragonpilot` was once the largest openpilot fork officially recognized by comma ai. This honor belongs to everyone who contributed.
|
||||
|
||||
## **🧑💻 Design Philosophy - Less is More**
|
||||
|
||||
As openpilot's AI grows stronger, many features that once required manual tuning are now handled by advanced models. That's why our focus has returned to **“minimal changes.”**
|
||||
|
||||
We aim to give you the purest, most official-like openpilot driving experience—while preserving `dragonpilot`'s classic, community-loved features. With a solid AI foundation, simplicity is strength.
|
||||
|
||||
## **🛠️ Hardware Journey**
|
||||
|
||||
From the early **EON**, to official devices like **comma two / three (C2/C3/C3X)**, to creative community builds (**C1.5, O2, O3, O3L, O3XL, etc.**), and even experiments with [**Jetson Xavier NX**](https://github.com/eFiniLan/xnxpilot).
|
||||
|
||||
Currently, the latest versions support: **comma3 / 3X** and community hardware like **O3 / O3L / O3XL**.
|
||||
Older devices such as **EON / C1.5 / C2** are supported in the [d2 branch](https://github.com/dragonpilot-community/dragonpilot/tree/d2).
|
||||
Whatever device you're on, it represents your passion for open-source driver assistance.
|
||||
|
||||
## **🫂 Join Us – Become a “Dragon Seeker”**
|
||||
|
||||
`dragonpilot` thrives thanks to every user's contributions and feedback. We're an open, transparent, and welcoming community where enthusiasts can share experiences with openpilot and `dragonpilot`.
|
||||
|
||||
[**Join our Facebook group here!**](https://www.facebook.com/groups/930190251238639)
|
||||
|
||||
## **❤️ Special Thanks**
|
||||
|
||||
Since day one, `dragonpilot` has never asked for funding through Patreon or similar platforms. Our vision is a community where everyone learns and grows together. It's about fun, not money.
|
||||
|
||||
That said, we're deeply grateful to those who voluntarily supported the project. Your encouragement keeps us motivated to keep building.
|
||||
|
||||
[**See our sponsors**](SPONSORS.md)
|
||||
|
||||
### **Safety Notice**
|
||||
|
||||
`dragonpilot` is a driver **assistance** system, not full self-driving. It reduces fatigue and improves safety, but you must remain alert and ready to take control at all times. Always follow your local traffic laws.
|
||||
|
||||
**Thanks again for being here.**
|
||||
|
||||
**We look forward to riding the “dragon” with you on the road to smarter driving!**
|
||||
111
README_OPENPILOT.md
Normal file
111
README_OPENPILOT.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<div align="center" style="text-align: center;">
|
||||
|
||||
<h1>openpilot</h1>
|
||||
|
||||
<p>
|
||||
<b>openpilot is an operating system for robotics.</b>
|
||||
<br>
|
||||
Currently, it upgrades the driver assistance system in 300+ supported cars.
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<a href="https://docs.comma.ai">Docs</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
|
||||
<span> · </span>
|
||||
<a href="https://discord.comma.ai">Community</a>
|
||||
<span> · </span>
|
||||
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
|
||||
</h3>
|
||||
|
||||
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
|
||||
|
||||
[](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 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
|
||||
2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
|
||||
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
|
||||
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car.
|
||||
|
||||
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
|
||||
|
||||
|
||||
### Branches
|
||||
|
||||
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 the driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone.
|
||||
|
||||
openpilot is open source software: the user is free to disable data collection if they wish to do so.
|
||||
|
||||
openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs.
|
||||
The driver-facing camera and microphone are only logged if you explicitly opt-in in settings.
|
||||
|
||||
By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data.
|
||||
</details>
|
||||
@@ -196,6 +196,7 @@ Export('messaging')
|
||||
|
||||
# Build other submodules
|
||||
SConscript(['panda/SConscript'])
|
||||
SConscript(['panda_tici/SConscript'])
|
||||
|
||||
# Build rednose library
|
||||
SConscript(['rednose/SConscript'])
|
||||
|
||||
@@ -10,16 +10,50 @@ $Cxx.namespace("cereal");
|
||||
# DO rename the structs
|
||||
# DON'T change the identifier (e.g. @0x81c2f05a394cf4af)
|
||||
|
||||
struct CustomReserved0 @0x81c2f05a394cf4af {
|
||||
struct ControlsStateExt @0x81c2f05a394cf4af {
|
||||
alkaActive @0 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved1 @0xaedffd8f31e7b55d {
|
||||
struct CarStateExt @0xaedffd8f31e7b55d {
|
||||
# dp - ALKA: lkasOn state from carstate (mirrors panda's lkas_on)
|
||||
lkasOn @0 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved2 @0xf35cc4560bbf6ec2 {
|
||||
struct ModelExt @0xf35cc4560bbf6ec2 {
|
||||
leftEdgeDetected @0 :Bool;
|
||||
rightEdgeDetected @1 :Bool;
|
||||
}
|
||||
|
||||
struct CustomReserved3 @0xda96579883444c35 {
|
||||
struct LiveGPS @0xda96579883444c35 {
|
||||
# Position
|
||||
latitude @0 :Float64; # degrees
|
||||
longitude @1 :Float64; # degrees
|
||||
altitude @2 :Float64; # meters (WGS84)
|
||||
|
||||
# Motion
|
||||
speed @3 :Float32; # m/s (horizontal speed)
|
||||
bearingDeg @4 :Float32; # degrees (heading)
|
||||
|
||||
# Accuracy
|
||||
horizontalAccuracy @5 :Float32; # meters
|
||||
verticalAccuracy @6 :Float32; # meters
|
||||
|
||||
# Status
|
||||
gpsOK @7 :Bool; # livePose valid + GPS fresh
|
||||
status @8 :Status;
|
||||
|
||||
enum Status {
|
||||
noGps @0;
|
||||
initializing @1;
|
||||
calibrating @2;
|
||||
valid @3;
|
||||
recalibrating @4;
|
||||
gpsStale @5;
|
||||
}
|
||||
|
||||
# Metadata
|
||||
unixTimestampMillis @9 :Int64;
|
||||
lastGpsTimestamp @10 :UInt64; # logMonoTime of last GPS
|
||||
}
|
||||
|
||||
struct CustomReserved4 @0x80ae746ee2596b11 {
|
||||
|
||||
@@ -2625,10 +2625,10 @@ 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
|
||||
customReserved0 @107 :Custom.CustomReserved0;
|
||||
customReserved1 @108 :Custom.CustomReserved1;
|
||||
customReserved2 @109 :Custom.CustomReserved2;
|
||||
customReserved3 @110 :Custom.CustomReserved3;
|
||||
controlsStateExt @107 :Custom.ControlsStateExt;
|
||||
carStateExt @108 :Custom.CarStateExt;
|
||||
modelExt @109 :Custom.ModelExt;
|
||||
liveGPS @110 :Custom.LiveGPS;
|
||||
customReserved4 @111 :Custom.CustomReserved4;
|
||||
customReserved5 @112 :Custom.CustomReserved5;
|
||||
customReserved6 @113 :Custom.CustomReserved6;
|
||||
|
||||
@@ -102,6 +102,10 @@ _services: dict[str, tuple] = {
|
||||
"customReservedRawData0": (True, 0.),
|
||||
"customReservedRawData1": (True, 0.),
|
||||
"customReservedRawData2": (True, 0.),
|
||||
"controlsStateExt": (True, 100.),
|
||||
"carStateExt": (True, 100.),
|
||||
"modelExt": (True, 20.),
|
||||
"liveGPS": (True, 20.),
|
||||
}
|
||||
SERVICE_LIST = {name: Service(*vals) for
|
||||
idx, (name, vals) in enumerate(_services.items())}
|
||||
|
||||
@@ -28,7 +28,7 @@ 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", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||
{"DisableLogging", {PERSISTENT, BOOL, "0"}},
|
||||
{"DisablePowerDown", {PERSISTENT, BOOL}},
|
||||
{"DisableUpdates", {PERSISTENT, BOOL}},
|
||||
{"DisengageOnAccelerator", {PERSISTENT, BOOL, "0"}},
|
||||
@@ -129,4 +129,37 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
{"dp_dev_last_log", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
{"dp_dev_reset_conf", {CLEAR_ON_MANAGER_START, BOOL, "0"}},
|
||||
{"dp_dev_beep", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_is_rhd", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_alka", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_display_mode", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_model_selected", {PERSISTENT, STRING}},
|
||||
{"dp_dev_model_list", {PERSISTENT, STRING}},
|
||||
{"dp_lat_lca_speed", {PERSISTENT, INT, "20"}},
|
||||
{"dp_lat_lca_auto_sec", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"dp_dev_go_off_road", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"dp_ui_hide_hud_speed_kph", {PERSISTENT, INT, "0"}},
|
||||
{"dp_lon_ext_radar", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_road_edge_detection", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_rainbow", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_acm", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_aem", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lon_dtsc", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_audible_alert_mode", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_auto_shutdown_in", {PERSISTENT, INT, "-5"}},
|
||||
{"dp_ui_lead", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_dashy", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_delay_loggerd", {PERSISTENT, INT, "0"}},
|
||||
{"dp_dev_disable_connect", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_dev_tethering", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_ui_mici", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_lat_offset_cm", {PERSISTENT, INT, "0"}},
|
||||
{"dp_toyota_door_auto_lock_unlock", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_toyota_tss1_sng", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_toyota_stock_lon", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_vag_a0_sng", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_vag_pq_steering_patch", {PERSISTENT, BOOL, "0"}},
|
||||
{"dp_vag_avoid_eps_lockout", {PERSISTENT, BOOL, "0"}},
|
||||
};
|
||||
|
||||
0
dragonpilot/.gitignore
vendored
Normal file
0
dragonpilot/.gitignore
vendored
Normal file
0
dragonpilot/dashy/.nojekyll
Normal file
0
dragonpilot/dashy/.nojekyll
Normal file
15
dragonpilot/dashy/LICENSE.md
Normal file
15
dragonpilot/dashy/LICENSE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
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.
|
||||
46
dragonpilot/dashy/README.md
Normal file
46
dragonpilot/dashy/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dashy Release Branch
|
||||
|
||||
This is the production-ready release branch of Dashy - Dragonpilot's All-in-one System Hub for You.
|
||||
|
||||
## 🚀 Quick Installation
|
||||
|
||||
```bash
|
||||
git clone -b release https://github.com/efinilan/dashy
|
||||
cd dashy
|
||||
python3 backend/server.py
|
||||
```
|
||||
|
||||
## 📁 What's Included
|
||||
|
||||
- `backend/` - Python server with all dependencies included
|
||||
- `web/` - Pre-built web interface (minified and optimized)
|
||||
|
||||
## 🌐 Access
|
||||
|
||||
After starting the server, open Chrome browser and navigate to:
|
||||
```
|
||||
http://<device-ip>:5088
|
||||
```
|
||||
|
||||
## 🔧 Requirements
|
||||
|
||||
- Network connection
|
||||
- Port 5088 available
|
||||
|
||||
## 📄 License
|
||||
|
||||
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.
|
||||
1
dragonpilot/dashy/backend/.gitignore
vendored
Normal file
1
dragonpilot/dashy/backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
544
dragonpilot/dashy/backend/server.py
Executable file
544
dragonpilot/dashy/backend/server.py
Executable file
@@ -0,0 +1,544 @@
|
||||
#!/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 argparse
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from urllib.parse import quote
|
||||
|
||||
from aiohttp import web, ClientSession, ClientTimeout
|
||||
|
||||
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
|
||||
|
||||
# --- 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")
|
||||
WEBRTC_TIMEOUT = ClientTimeout(total=10)
|
||||
CAR_PARAMS_CACHE_TTL = 30 # seconds
|
||||
|
||||
logger = logging.getLogger("dashy")
|
||||
|
||||
|
||||
# --- 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
|
||||
|
||||
@property
|
||||
def params(self) -> Params:
|
||||
"""Get shared Params instance."""
|
||||
if self._params is None:
|
||||
self._params = Params()
|
||||
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:
|
||||
car_params_bytes = 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()
|
||||
}
|
||||
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 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
|
||||
|
||||
|
||||
# --- 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
|
||||
|
||||
|
||||
def eval_condition(condition, context):
|
||||
"""Safely evaluate a condition string."""
|
||||
if not condition:
|
||||
return True
|
||||
try:
|
||||
return eval(condition, {"__builtins__": {}}, 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
|
||||
|
||||
|
||||
# --- API Endpoints ---
|
||||
@api_handler
|
||||
async def init_api(request):
|
||||
"""Provide initial data to the client."""
|
||||
cache: AppCache = request.app['cache']
|
||||
car_params = cache.get_car_params()
|
||||
|
||||
return web.json_response({
|
||||
'is_metric': cache.get_bool_safe("IsMetric"),
|
||||
'dp_dev_dashy': cache.get_bool_safe("dp_dev_dashy", True),
|
||||
'openpilot_longitudinal_control': car_params['openpilot_longitudinal_control'],
|
||||
'ublox_available': cache.get_bool_safe("UbloxAvailable", True),
|
||||
'dp_lat_alka': cache.get_bool_safe("dp_lat_alka", 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)
|
||||
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)
|
||||
|
||||
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))
|
||||
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)
|
||||
|
||||
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']
|
||||
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)
|
||||
|
||||
return web.json_response({'settings': settings_with_values})
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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'])
|
||||
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 with proper type handling."""
|
||||
try:
|
||||
return params.get_bool(key)
|
||||
except Exception:
|
||||
raw_value = params.get(key)
|
||||
if raw_value is None:
|
||||
return None
|
||||
elif isinstance(raw_value, bytes):
|
||||
return raw_value.decode('utf-8')
|
||||
return raw_value
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
cache: AppCache = request.app['cache']
|
||||
params = cache.params
|
||||
value = _get_param_value(params, param_name)
|
||||
|
||||
return web.json_response({'key': param_name, 'value': value})
|
||||
|
||||
|
||||
@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
|
||||
model_list = {}
|
||||
try:
|
||||
model_list_raw = params.get("dp_dev_model_list")
|
||||
if model_list_raw:
|
||||
model_list = json.loads(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'})
|
||||
|
||||
|
||||
@api_handler
|
||||
async def webrtc_stream_proxy(request):
|
||||
"""Proxy WebRTC stream requests to webrtcd."""
|
||||
host = request.host.split(':')[0]
|
||||
body = await request.read()
|
||||
session: ClientSession = request.app['http_session']
|
||||
|
||||
async with session.post(
|
||||
f'http://{host}:5001/stream',
|
||||
data=body,
|
||||
headers={'Content-Type': 'application/json'}
|
||||
) as resp:
|
||||
response_body = await resp.read()
|
||||
return web.Response(
|
||||
body=response_body,
|
||||
status=resp.status,
|
||||
content_type=resp.content_type
|
||||
)
|
||||
|
||||
|
||||
# --- CORS Middleware ---
|
||||
@web.middleware
|
||||
async def cors_middleware(request, handler):
|
||||
response = await handler(request)
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
||||
|
||||
# Disable caching for web assets
|
||||
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
|
||||
|
||||
|
||||
async def handle_cors_preflight(request):
|
||||
return web.Response(status=200, headers={
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
})
|
||||
|
||||
|
||||
# --- Application Setup ---
|
||||
async def on_startup(app):
|
||||
"""Initialize app-level resources."""
|
||||
app['cache'] = AppCache()
|
||||
app['http_session'] = ClientSession(timeout=WEBRTC_TIMEOUT)
|
||||
logger.info("Dashy server started")
|
||||
|
||||
|
||||
async def on_cleanup(app):
|
||||
"""Cleanup app-level resources."""
|
||||
await app['http_session'].close()
|
||||
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=[cors_middleware])
|
||||
app['port'] = port
|
||||
|
||||
# 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/stream", webrtc_stream_proxy)
|
||||
app.router.add_route('OPTIONS', '/{tail:.*}', handle_cors_preflight)
|
||||
|
||||
# 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()
|
||||
2
dragonpilot/dashy/web/dist/css/styles.css
vendored
Normal file
2
dragonpilot/dashy/web/dist/css/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
dragonpilot/dashy/web/dist/icons/dashy.png
vendored
Normal file
BIN
dragonpilot/dashy/web/dist/icons/dashy.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
dragonpilot/dashy/web/dist/icons/icon-192x192.png
vendored
Normal file
BIN
dragonpilot/dashy/web/dist/icons/icon-192x192.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
90
dragonpilot/dashy/web/dist/index.html
vendored
Normal file
90
dragonpilot/dashy/web/dist/index.html
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
<!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">
|
||||
<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">
|
||||
|
||||
<!-- 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 id="videoPlayer" class="absolute inset-0 w-full h-full object-cover" autoplay playsinline muted></video>
|
||||
<canvas id="uiCanvas" class="absolute inset-0 w-full h-full pointer-events-none z-10"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Backdrop -->
|
||||
<div id="panel-backdrop" class="panel-backdrop"></div>
|
||||
|
||||
<!-- Slide-up Panel -->
|
||||
<div id="panel" class="panel">
|
||||
<!-- Panel Header with Tabs -->
|
||||
<div class="panel-header">
|
||||
<div class="panel-handle"></div>
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<div class="join flex-1 max-w-sm">
|
||||
<button id="panel-tab-controls" class="join-item btn btn-primary btn-sm sm:btn-md flex-1">Controls</button>
|
||||
<button id="panel-tab-settings" class="join-item btn btn-ghost btn-sm sm:btn-md flex-1">Settings</button>
|
||||
<button id="panel-tab-files" class="join-item btn btn-ghost btn-sm sm:btn-md flex-1">Files</button>
|
||||
</div>
|
||||
<button id="panel-close" class="btn btn-circle btn-ghost btn-sm sm:btn-md shrink-0" aria-label="Close panel">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Content -->
|
||||
<div class="panel-content">
|
||||
<!-- Controls Tab -->
|
||||
<div id="controls-content" class="panel-page active">
|
||||
<div class="max-w-2xl landscape:max-w-5xl mx-auto space-y-4">
|
||||
<div id="controls-content-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div id="settings-content" class="panel-page">
|
||||
<div class="max-w-2xl landscape:max-w-5xl mx-auto">
|
||||
<div id="local-settings-content" class="space-y-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Tab -->
|
||||
<div id="files-content" class="panel-page">
|
||||
<div id="files-breadcrumbs" class="breadcrumbs text-sm mb-4">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table id="files-table" class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-12"></th>
|
||||
<th>Name</th>
|
||||
<th>Last Modified</th>
|
||||
<th class="text-right">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main app -->
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
160
dragonpilot/dashy/web/dist/js/app.js
vendored
Normal file
160
dragonpilot/dashy/web/dist/js/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dragonpilot/dashy/web/dist/js/library-loader.js
vendored
Normal file
15
dragonpilot/dashy/web/dist/js/library-loader.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
// Dynamic Library Loader
|
||||
window.loadLibrary = function(name) {
|
||||
if (name === 'hls') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = '/lib/' + name + '.min.js';
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.loadHls = function() { return window.loadLibrary('hls'); };
|
||||
24
dragonpilot/dashy/web/dist/js/themes/flight.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/flight.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
var C=Object.defineProperty;var B=(w,p,d)=>p in w?C(w,p,{enumerable:!0,configurable:!0,writable:!0,value:d}):w[p]=d;var _=(w,p,d)=>B(w,typeof p!="symbol"?p+"":p,d);(function(){"use strict";class w extends ModelRenderer{static getTopics(){return["modelV2","liveCalibration","carParams","longitudinalPlan","radarState"]}constructor(){super(),this._tunnelOffset=0}_draw_lane_lines(){}_draw_path(t){const h=this._path;if(!h||!h.raw_points||h.raw_points.length<4)return;const e=this.ctx,i=this._car_space_transform,R=this._path_offset_z,b=6,l=100,r=t.longitudinalPlan&&t.longitudinalPlan.allowThrottle||!this._longitudinal_control?{r:13,g:248,b:122}:{r:242,g:242,b:242};e.lineCap="round",e.lineJoin="round";const a=h.raw_points,f=1.2,v=.8,M=a[a.length-1][0];let g=Math.max(b,Math.min(l,M));const P=t.radarState,T=P?P.leadOne:null;if(T&&T.status){const c=T.dRel*2;g=Math.max(0,Math.min(c-Math.min(c*.35,10),g))}const S=10,k=b;if(g<=k)return;const F=(g-k)/S,W=t.carState?t.carState.vEgo:0;for(this._tunnelOffset+=W*.015;this._tunnelOffset>=F;)this._tunnelOffset-=F;const $=[];for(let c=0;c<S;c++){let o=k+c*F-this._tunnelOffset;o<k-1&&(o+=F*S);let m=null;for(let s=0;s<a.length-1;s++)if(a[s][0]<=o&&a[s+1][0]>=o){const D=(o-a[s][0])/(a[s+1][0]-a[s][0]);m=[o,a[s][1]+D*(a[s+1][1]-a[s][1]),a[s][2]+D*(a[s+1][2]-a[s][2])];break}if(!m)continue;const L=m[2]+R,u=L-v*3,A=[[o,m[1]-f,L],[o,m[1]+f,L],[o,m[1]-f,u],[o,m[1]+f,u]].map(s=>{const D=i[0][0]*s[0]+i[0][1]*s[1]+i[0][2]*s[2],O=i[1][0]*s[0]+i[1][1]*s[1]+i[1][2]*s[2],E=i[2][0]*s[0]+i[2][1]*s[1]+i[2][2]*s[2];return Math.abs(E)<1e-6?null:[D/E,O/E]});if(A.some(s=>!s))continue;const y=Math.max(0,1-(o-k)/(g-k));$.push({bottomLeft:A[0],bottomRight:A[1],topLeft:A[2],topRight:A[3],distFactor:y})}for(let c=$.length-1;c>=0;c--){const o=$[c],m=.2+o.distFactor*.5,L=1.5+o.distFactor*2;if(e.strokeStyle=`rgba(${r.r}, ${r.g}, ${r.b}, ${m})`,e.lineWidth=L,e.beginPath(),e.moveTo(o.bottomLeft[0],o.bottomLeft[1]),e.lineTo(o.bottomRight[0],o.bottomRight[1]),e.lineTo(o.topRight[0],o.topRight[1]),e.lineTo(o.topLeft[0],o.topLeft[1]),e.closePath(),e.stroke(),c<$.length-1){const u=$[c+1],I=m*.4;e.strokeStyle=`rgba(${r.r}, ${r.g}, ${r.b}, ${I})`,e.lineWidth=L*.5,e.beginPath(),e.moveTo(o.bottomLeft[0],o.bottomLeft[1]),e.lineTo(u.bottomLeft[0],u.bottomLeft[1]),e.stroke(),e.beginPath(),e.moveTo(o.bottomRight[0],o.bottomRight[1]),e.lineTo(u.bottomRight[0],u.bottomRight[1]),e.stroke(),e.beginPath(),e.moveTo(o.topLeft[0],o.topLeft[1]),e.lineTo(u.topLeft[0],u.topLeft[1]),e.stroke(),e.beginPath(),e.moveTo(o.topRight[0],o.topRight[1]),e.lineTo(u.topRight[0],u.topRight[1]),e.stroke()}}}_draw_lead_indicator(){const t=this.ctx,h=Date.now(),e=this._sm&&this._sm.radarState;this._lead_vehicles.forEach((i,R)=>{if(!i.chevron||i.chevron.length<3)return;const b=i.chevron[1][0],l=Math.abs(i.chevron[0][0]-i.chevron[2][0]),n=Math.abs(i.chevron[0][1]-i.chevron[1][1]),r=Math.max(l,n)*.8,a=i.chevron[1][1]-r*.6,f=e?R===0?e.leadOne:e.leadTwo:null,v=f?f.vRel:0,M=f?f.dRel:100;let g;v<-5?g="#ff3333":v<-2?g="#ffaa00":g="#00ff88";const P=1e3+M/100*3e3,T=.7+.3*Math.sin(h/P*Math.PI*2);this._drawTargetBrackets(t,b,a,r,g,T),this._drawTargetInfo(t,b,a+r*.7,M,v,g)})}_drawTargetInfo(t,h,e,i,R,b){var T,S;t.save(),t.font="bold 16px Arial",t.textAlign="center",t.textBaseline="top";const l=SmUtils.isMetric(),n=l?`${i.toFixed(1)}m`:`${(i*3.28084).toFixed(1)}ft`,a=(((S=(T=this._sm)==null?void 0:T.carState)==null?void 0:S.vEgo)||0)+R,f=Math.max(0,l?a*3.6:a*2.237),v=`${Math.round(f)}`,M=120,g=22,P=6;t.fillStyle="rgba(0, 0, 0, 0.5)",t.beginPath(),t.roundRect(h-M/2,e-2,M,g,4),t.fill(),t.fillStyle=b,t.fillText(`${n} ${v}`,h,e),t.restore()}_drawTargetBrackets(t,h,e,i,R,b){const l=i*.35,n=i*.5;t.strokeStyle=R,t.lineWidth=3,t.lineCap="square",t.save(),t.translate(h,e),t.beginPath(),t.moveTo(-n,-n+l),t.lineTo(-n,-n),t.lineTo(-n+l,-n),t.stroke(),t.beginPath(),t.moveTo(n-l,-n),t.lineTo(n,-n),t.lineTo(n,-n+l),t.stroke(),t.beginPath(),t.moveTo(n,n-l),t.lineTo(n,n),t.lineTo(n-l,n),t.stroke(),t.beginPath(),t.moveTo(-n+l,n),t.lineTo(-n,n),t.lineTo(-n,n-l),t.stroke();const r=i*.12;t.lineWidth=2,t.globalAlpha=b,t.beginPath(),t.moveTo(-r,0),t.lineTo(r,0),t.stroke(),t.beginPath(),t.moveTo(0,-r),t.lineTo(0,r),t.stroke(),t.globalAlpha=1,t.restore()}}window.FlightModelRenderer=w;class p extends HudRenderer{render(t,h,e){return!t||t.width<=0||t.height<=0||(window.EdgeIndicators&&EdgeIndicators.draw(e,t,h),window.FlightHud&&FlightHud.draw(e,t,h)),!1}}window.FlightHudRenderer=p;class d extends BaseTheme{}_(d,"layout",Layouts.fullResponsive),_(d,"requiresVideo",!0),_(d,"modules",["FlightHud","NavMap","EdgeIndicators"]),_(d,"layers",[]),_(d,"minimapConfig",{useGrid:!1,options:{responsiveThird:!0,zoom:16,interactive:!0,scale:1.5}}),_(d,"modelRenderer","FlightModelRenderer"),_(d,"hudRenderer","FlightHudRenderer"),window.FlightPanel=d})();
|
||||
24
dragonpilot/dashy/web/dist/js/themes/nav_free.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/nav_free.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
var v=Object.defineProperty;var w=(t,i,e)=>i in t?v(t,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[i]=e;var l=(t,i,e)=>w(t,typeof i!="symbol"?i+"":i,e);var u=(t,i,e)=>new Promise((a,o)=>{var s=d=>{try{r(e.next(d))}catch(c){o(c)}},n=d=>{try{r(e.throw(d))}catch(c){o(c)}},r=d=>d.done?a(d.value):Promise.resolve(d.value).then(s,n);r((e=e.apply(t,i)).next())});(function(){"use strict";class t extends BaseTheme{constructor(){super(),this._mapReady=!1}init(e,a){return u(this,null,function*(){this._canvas=e,this._ctx=a,this._enabled=!0;const o=document.getElementById("videoPlayer");if(o&&(o.style.display="none"),window.NavMap&&NavMap.destroy(),window.NavigationFree&&NavigationFree.init(),window.NavMap){yield NavMap.init();const n=document.getElementById("hud-page-content");n&&(NavMap.show(n,{fullscreen:!0,scale:1.5,interactive:!0,enableRouting:!0,autoTileCache:!0,followResumeDelay:3e3}),this._mapReady=!0)}const s=document.getElementById("hud-page-content");return s&&window.NavSearch&&NavSearch.show(s),!0})}update(e){var s;const a=SmUtils.gps(e),o=SmUtils.speedKmh(e);if(this._mapReady&&window.NavMap&&a.lat!==0&&NavMap.setPosition(a.lat,a.lon,a.heading,o),window.NavSearch&&NavSearch.updatePosition(a.lat,a.lon),(s=window.NavigationFree)!=null&&s.isNavigating()){NavigationFree.updatePosition(a.lat,a.lon,a.heading);const n=NavigationFree.getRoute();n&&window.NavMap&&NavMap.setRoute(n)}}render(e,a,o){if(!this._enabled)return!1;e.clearRect(0,0,a,o);const s=window.sm||{};for(const n of this.constructor.layers){const r=window[n.module];if(r!=null&&r.draw){const d=this._layout.getRect(n.region||"full",a,o);r.draw(e,d,s)}}return!1}destroy(){this._enabled=!1,window.NavMap&&NavMap.destroy(),window.NavigationFree&&NavigationFree.clearRoute(),window.NavSearch&&NavSearch.hide();const e=document.getElementById("videoPlayer");e&&(e.style.display=""),this._mapReady=!1}}l(t,"layout",Layouts.full),l(t,"requiresVideo",!1),l(t,"modules",["NavSidebar","NavHud","NavMap","EdgeIndicators"]),l(t,"layers",[{module:"NavSidebar",region:"full"},{module:"NavHud",region:"full"},{module:"EdgeIndicators",region:"full"}]),window.NavFreeTheme=t})();
|
||||
57
dragonpilot/dashy/web/dist/js/themes/op_split.js
vendored
Normal file
57
dragonpilot/dashy/web/dist/js/themes/op_split.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
var c=Object.defineProperty;var _=(s,r,t)=>r in s?c(s,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[r]=t;var l=(s,r,t)=>_(s,typeof r!="symbol"?r+"":r,t);var p=(s,r,t)=>new Promise((e,n)=>{var d=a=>{try{i(t.next(a))}catch(h){n(h)}},o=a=>{try{i(t.throw(a))}catch(h){n(h)}},i=a=>a.done?e(a.value):Promise.resolve(a.value).then(d,o);i((t=t.apply(s,r)).next())});(function(){"use strict";class s extends BaseTheme{constructor(){super(),this._splitContainer=null,this._mapContainer=null,this._videoContainer=null,this._videoCanvas=null,this._videoCtx=null,this._opModel=null,this._minimapReady=!1,this._isPortrait=!1,this._resizeHandler=null}init(t,e){return p(this,null,function*(){return this._canvas=t,this._ctx=e,this._enabled=!0,window.Minimap&&Minimap.destroy(),window.NavMap&&NavMap.destroy(),this._createSplitLayout(),yield this._initMap(),this._resizeHandler=()=>this._handleResize(),window.addEventListener("resize",this._resizeHandler),!0})}_handleResize(){const t=window.innerWidth,e=window.innerHeight,n=this._isPortrait;this._isPortrait=this._layout.isPortrait(t,e),n!==this._isPortrait&&this._rebuildLayout()}_rebuildLayout(){const t=this._minimapReady;this._minimapReady&&window.NavMap&&NavMap.destroy(),this._minimapReady=!1;const e=document.getElementById("videoPlayer"),n=document.getElementById("hud-page-content");e&&this._videoContainer&&n&&(e.style.cssText="",n.insertBefore(e,this._videoContainer),this._videoContainer.remove()),this._splitContainer&&this._splitContainer.remove(),this._createSplitLayout(),t&&this._initMap()}_createSplitLayout(){const t=document.getElementById("hud-page-content");if(!t)return;const e=t.offsetWidth||window.innerWidth,n=t.offsetHeight||window.innerHeight;this._isPortrait=this._layout.isPortrait(e,n);const d=this._layout.getRegionRect("primary",e,n),o=this._layout.getRegionRect("secondary",e,n),i=60;this._splitContainer=document.createElement("div"),this._splitContainer.id="op-split-container",this._splitContainer.style.cssText=`
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
display: flex; pointer-events: none; z-index: 1;
|
||||
flex-direction: ${this._isPortrait?"column-reverse":"row-reverse"};
|
||||
`,this._mapContainer=document.createElement("div"),this._mapContainer.id="op-split-map",this._isPortrait?this._mapContainer.style.cssText=`
|
||||
width: ${d.width}px; height: ${d.height}px;
|
||||
position: relative; pointer-events: auto;
|
||||
-webkit-mask-image: linear-gradient(to top, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
mask-image: linear-gradient(to top, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
`:this._mapContainer.style.cssText=`
|
||||
width: ${d.width}px; height: ${d.height}px;
|
||||
position: relative; pointer-events: auto;
|
||||
-webkit-mask-image: linear-gradient(to left, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
mask-image: linear-gradient(to left, black 0%, black calc(100% - ${i}px), transparent 100%);
|
||||
`,this._splitContainer.appendChild(this._mapContainer),t.appendChild(this._splitContainer);const a=document.getElementById("videoPlayer");a&&(this._videoContainer=document.createElement("div"),this._videoContainer.id="op-split-video",this._isPortrait?this._videoContainer.style.cssText=`
|
||||
position: absolute; left: ${o.x}px; top: 0;
|
||||
width: ${o.width}px; height: ${o.height+i}px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(to top, transparent 0%, black ${i}px, black 100%);
|
||||
mask-image: linear-gradient(to top, transparent 0%, black ${i}px, black 100%);
|
||||
`:this._videoContainer.style.cssText=`
|
||||
position: absolute; left: 0; top: ${o.y}px;
|
||||
width: ${o.width+i}px; height: ${o.height}px;
|
||||
overflow: hidden;
|
||||
-webkit-mask-image: linear-gradient(to left, transparent 0%, black ${i}px, black 100%);
|
||||
mask-image: linear-gradient(to left, transparent 0%, black ${i}px, black 100%);
|
||||
`,a.parentNode.insertBefore(this._videoContainer,a),this._videoContainer.appendChild(a),a.style.cssText=`
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
`,this._videoCanvas=document.createElement("canvas"),this._videoCanvas.width=o.width,this._videoCanvas.height=o.height,this._videoCanvas.style.cssText=`
|
||||
position: absolute; top: 0; left: 0;
|
||||
width: 100%; height: 100%; pointer-events: none;
|
||||
`,this._videoContainer.appendChild(this._videoCanvas),this._videoCtx=this._videoCanvas.getContext("2d"),window.OpModel&&(this._opModel=OpModel.create()))}_initMap(){return p(this,null,function*(){!this._mapContainer||!window.NavMap||(NavMap.destroy(),yield NavMap.init(),NavMap.show(this._mapContainer,{fullscreen:!0,scale:1.5,zoom:16,interactive:!0}),this._minimapReady=!0)})}update(t){if(this._minimapReady&&window.NavMap&&NavMap.isVisible()){const e=SmUtils.gps(t);e.lat!==0&&NavMap.setPosition(e.lat,e.lon,e.heading,SmUtils.speedKmh(t))}}render(t,e,n){if(!this._enabled)return!1;const d=window.sm||{};this._opModel&&this._videoCtx&&(this._videoCtx.clearRect(0,0,this._videoCanvas.width,this._videoCanvas.height),this._opModel.draw(this._videoCtx,{x:0,y:0,width:this._videoCanvas.width,height:this._videoCanvas.height},d));for(const o of this.constructor.layers){const i=window[o.module];if(!(i!=null&&i.draw))continue;let a;o.region==="hud"?this._isPortrait?a={x:0,y:0,width:e,height:n*.5}:a={x:0,y:0,width:e*.5,height:n}:a={x:0,y:0,width:e,height:n},i.draw(t,a,d)}return!1}destroy(){this._enabled=!1,this._resizeHandler&&(window.removeEventListener("resize",this._resizeHandler),this._resizeHandler=null),this._minimapReady&&window.NavMap&&NavMap.destroy(),this._opModel&&this._opModel.destroy();const t=document.getElementById("videoPlayer"),e=document.getElementById("hud-page-content");t&&this._videoContainer&&e&&(t.style.cssText="",t.className="absolute inset-0 w-full h-full object-cover",e.insertBefore(t,this._videoContainer),this._videoContainer.remove()),this._splitContainer&&this._splitContainer.remove(),this._splitContainer=null,this._mapContainer=null,this._videoContainer=null,this._videoCanvas=null,this._videoCtx=null,this._opModel=null,this._minimapReady=!1,this._isPortrait=!1}}l(s,"layout",Layouts.splitResponsive),l(s,"requiresVideo",!0),l(s,"handlesOwnRendering",!0),l(s,"modules",["NavSidebar","NavMap","OpModel","OpBorder","OpAlerts","EdgeIndicators"]),l(s,"layers",[{module:"NavSidebar",region:"hud"},{module:"OpBorder",region:"full"},{module:"EdgeIndicators",region:"full"},{module:"OpAlerts",region:"full"}]),window.OpSplitTheme=s})();
|
||||
24
dragonpilot/dashy/web/dist/js/themes/openpilot.js
vendored
Normal file
24
dragonpilot/dashy/web/dist/js/themes/openpilot.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
var n=Object.defineProperty;var u=(r,e,d)=>e in r?n(r,e,{enumerable:!0,configurable:!0,writable:!0,value:d}):r[e]=d;var s=(r,e,d)=>u(r,typeof e!="symbol"?e+"":e,d);(function(){"use strict";class r extends HudRenderer{render(t,i,a){return super.render(t,i,a),window.EdgeIndicators&&EdgeIndicators.draw(a,t,i),!1}}window.OpenpilotHudRenderer=r;class e extends BaseTheme{}s(e,"layout",Layouts.fullResponsive),s(e,"requiresVideo",!0),s(e,"modules",["OpHud","OpBorder","OpAlerts","NavMap","EdgeIndicators"]),s(e,"layers",[]),s(e,"minimapConfig",{useGrid:!1,options:{responsiveThird:!0,zoom:16,interactive:!0,scale:1.5}}),s(e,"hudRenderer","OpenpilotHudRenderer"),window.OpenpilotTheme=e})();
|
||||
2
dragonpilot/dashy/web/dist/lib/hls.min.js
vendored
Normal file
2
dragonpilot/dashy/web/dist/lib/hls.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
25
dragonpilot/dashy/web/dist/pages/player.html
vendored
Normal file
25
dragonpilot/dashy/web/dist/pages/player.html
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!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>
|
||||
101
dragonpilot/dashy/web/dist/sw-tiles.js
vendored
Normal file
101
dragonpilot/dashy/web/dist/sw-tiles.js
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Tile Cache Service Worker
|
||||
* Caches map tiles from OpenFreeMap for offline use
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'dashy-map-tiles-v1';
|
||||
const TILE_HOSTS = ['tiles.openfreemap.org'];
|
||||
const MAX_CACHE_SIZE = 2000; // Max tiles to cache
|
||||
const TRIM_INTERVAL = 60000; // Only trim cache every 60 seconds
|
||||
|
||||
// Debug mode - can be set via message from main thread
|
||||
let _debug = false;
|
||||
let _lastTrimTime = 0;
|
||||
|
||||
function debugLog(...args) {
|
||||
if (_debug) console.log(...args);
|
||||
}
|
||||
|
||||
// Listen for debug toggle from main thread
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SET_DEBUG') {
|
||||
_debug = event.data.value;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName.startsWith('dashy-map-tiles-') && cacheName !== CACHE_NAME) {
|
||||
debugLog('[TileCache SW] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// Only cache tile requests from OpenFreeMap
|
||||
const isTileRequest = TILE_HOSTS.some(host => url.hostname.includes(host));
|
||||
if (!isTileRequest) return;
|
||||
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
// Return cached, but also update cache in background
|
||||
fetchAndCache(event.request, cache);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
return fetchAndCache(event.request, cache);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
async function fetchAndCache(request, cache) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
trimCache(cache);
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (e) {
|
||||
// Return cached version if offline
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function trimCache(cache) {
|
||||
// Only trim every TRIM_INTERVAL to avoid constant overhead
|
||||
const now = Date.now();
|
||||
if (now - _lastTrimTime < TRIM_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
_lastTrimTime = now;
|
||||
|
||||
const keys = await cache.keys();
|
||||
if (keys.length > MAX_CACHE_SIZE) {
|
||||
// Delete oldest entries
|
||||
const toDelete = keys.slice(0, keys.length - MAX_CACHE_SIZE);
|
||||
for (const key of toDelete) {
|
||||
await cache.delete(key);
|
||||
}
|
||||
debugLog('[TileCache SW] Trimmed', toDelete.length, 'old tiles');
|
||||
}
|
||||
}
|
||||
0
dragonpilot/selfdrive/assets/.gitignore
vendored
Normal file
0
dragonpilot/selfdrive/assets/.gitignore
vendored
Normal file
BIN
dragonpilot/selfdrive/assets/dragonpilot.png
Normal file
BIN
dragonpilot/selfdrive/assets/dragonpilot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
dragonpilot/selfdrive/assets/icons/chffr_wheel.png
Normal file
BIN
dragonpilot/selfdrive/assets/icons/chffr_wheel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
35
dragonpilot/selfdrive/assets/icons/icon_empty.svg
Normal file
35
dragonpilot/selfdrive/assets/icons/icon_empty.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="256"
|
||||
height="256"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="icon_empty.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="3.0820312"
|
||||
inkscape:cx="128"
|
||||
inkscape:cy="114.37262"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1021"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
dragonpilot/selfdrive/assets/images/spinner_comma.png
Normal file
BIN
dragonpilot/selfdrive/assets/images/spinner_comma.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
0
dragonpilot/selfdrive/controls/.gitignore
vendored
Normal file
0
dragonpilot/selfdrive/controls/.gitignore
vendored
Normal file
0
dragonpilot/selfdrive/controls/lib/.gitignore
vendored
Normal file
0
dragonpilot/selfdrive/controls/lib/.gitignore
vendored
Normal file
161
dragonpilot/selfdrive/controls/lib/acm.py
Normal file
161
dragonpilot/selfdrive/controls/lib/acm.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# Configuration parameters
|
||||
SPEED_RATIO = 0.98 # Must be within 2% over cruise speed
|
||||
TTC_THRESHOLD = 3.0 # seconds - disable ACM when lead is within this time
|
||||
|
||||
# Emergency thresholds - IMMEDIATELY disable ACM
|
||||
EMERGENCY_TTC = 2.0 # seconds - emergency situation
|
||||
EMERGENCY_RELATIVE_SPEED = 10.0 # m/s (~36 km/h closing speed - only for rapid closing)
|
||||
EMERGENCY_DECEL_THRESHOLD = -1.5 # m/s² - if MPC wants this much braking, emergency disable
|
||||
|
||||
# Safety cooldown after lead detection
|
||||
LEAD_COOLDOWN_TIME = 0.5 # seconds - brief cooldown to handle sensor glitches
|
||||
|
||||
# Speed-based distance scaling - more practical for real traffic
|
||||
SPEED_BP = [0., 10., 20., 30.] # m/s (0, 36, 72, 108 km/h)
|
||||
MIN_DIST_V = [15., 20., 25., 30.] # meters - closer to original 25m baseline
|
||||
|
||||
|
||||
class ACM:
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self._is_speed_over_cruise = False
|
||||
self._has_lead = False
|
||||
self._active_prev = False
|
||||
self._last_lead_time = 0.0 # Track when we last saw a lead
|
||||
|
||||
self.active = False
|
||||
self.just_disabled = False
|
||||
|
||||
def _check_emergency_conditions(self, lead, v_ego, current_time):
|
||||
"""Check for emergency conditions that require immediate ACM disable."""
|
||||
if not lead or not lead.status:
|
||||
return False
|
||||
|
||||
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
|
||||
relative_speed = v_ego - lead.vLead # Positive = closing
|
||||
|
||||
# Speed-adaptive minimum distance
|
||||
min_dist_for_speed = np.interp(v_ego, SPEED_BP, MIN_DIST_V)
|
||||
|
||||
# Emergency disable conditions - only for truly dangerous situations
|
||||
# Require BOTH close distance AND (fast closing OR very short TTC)
|
||||
if lead.dRel < min_dist_for_speed and (
|
||||
self.lead_ttc < EMERGENCY_TTC or
|
||||
relative_speed > EMERGENCY_RELATIVE_SPEED):
|
||||
|
||||
self._last_lead_time = current_time
|
||||
if self.active: # Only log if we're actually disabling
|
||||
cloudlog.warning(f"ACM emergency disable: dRel={lead.dRel:.1f}m, TTC={self.lead_ttc:.1f}s, relSpeed={relative_speed:.1f}m/s")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_lead_status(self, lead, v_ego, current_time):
|
||||
"""Update lead vehicle detection status."""
|
||||
if lead and lead.status:
|
||||
self.lead_ttc = lead.dRel / max(v_ego, 0.1)
|
||||
|
||||
if self.lead_ttc < TTC_THRESHOLD:
|
||||
self._has_lead = True
|
||||
self._last_lead_time = current_time
|
||||
else:
|
||||
self._has_lead = False
|
||||
else:
|
||||
self._has_lead = False
|
||||
self.lead_ttc = float('inf')
|
||||
|
||||
def _check_cooldown(self, current_time):
|
||||
"""Check if we're still in cooldown period after lead detection."""
|
||||
time_since_lead = current_time - self._last_lead_time
|
||||
return time_since_lead < LEAD_COOLDOWN_TIME
|
||||
|
||||
def _should_activate(self, user_ctrl_lon, v_ego, v_cruise, in_cooldown):
|
||||
"""Determine if ACM should be active based on all conditions."""
|
||||
self._is_speed_over_cruise = v_ego > (v_cruise * SPEED_RATIO)
|
||||
|
||||
return (not user_ctrl_lon and
|
||||
not self._has_lead and
|
||||
not in_cooldown and
|
||||
self._is_speed_over_cruise)
|
||||
|
||||
def update_states(self, cc, rs, user_ctrl_lon, v_ego, v_cruise):
|
||||
"""Update ACM state with multiple safety checks."""
|
||||
# Basic validation
|
||||
if not self.enabled or len(cc.orientationNED) != 3:
|
||||
self.active = False
|
||||
return
|
||||
|
||||
current_time = time.monotonic()
|
||||
lead = rs.leadOne
|
||||
|
||||
# Check emergency conditions first (highest priority)
|
||||
if self._check_emergency_conditions(lead, v_ego, current_time):
|
||||
self.active = False
|
||||
self._active_prev = self.active
|
||||
return
|
||||
|
||||
# Update normal lead status
|
||||
self._update_lead_status(lead, v_ego, current_time)
|
||||
|
||||
# Check cooldown period
|
||||
in_cooldown = self._check_cooldown(current_time)
|
||||
|
||||
# Determine if ACM should be active
|
||||
self.active = self._should_activate(user_ctrl_lon, v_ego, v_cruise, in_cooldown)
|
||||
|
||||
# Track state changes for logging
|
||||
self.just_disabled = self._active_prev and not self.active
|
||||
if self.active and not self._active_prev:
|
||||
cloudlog.info(f"ACM activated: v_ego={v_ego*3.6:.1f} km/h, v_cruise={v_cruise*3.6:.1f} km/h")
|
||||
elif self.just_disabled:
|
||||
cloudlog.info("ACM deactivated")
|
||||
|
||||
self._active_prev = self.active
|
||||
|
||||
def update_a_desired_trajectory(self, a_desired_trajectory):
|
||||
"""
|
||||
Modify acceleration trajectory to allow coasting.
|
||||
SAFETY: Check for any strong braking request and abort.
|
||||
"""
|
||||
if not self.active:
|
||||
return a_desired_trajectory
|
||||
|
||||
# SAFETY CHECK: If MPC wants significant braking, DON'T suppress it
|
||||
min_accel = np.min(a_desired_trajectory)
|
||||
if min_accel < EMERGENCY_DECEL_THRESHOLD:
|
||||
cloudlog.warning(f"ACM aborting: MPC requested {min_accel:.2f} m/s² braking")
|
||||
self.active = False # Immediately deactivate
|
||||
return a_desired_trajectory # Return unmodified trajectory
|
||||
|
||||
# Only suppress very mild braking (> -1.0 m/s²)
|
||||
# This allows coasting but preserves any meaningful braking
|
||||
modified_trajectory = np.copy(a_desired_trajectory)
|
||||
for i in range(len(modified_trajectory)):
|
||||
if -1.0 < modified_trajectory[i] < 0:
|
||||
# Only suppress very gentle braking for cruise control
|
||||
modified_trajectory[i] = 0.0
|
||||
# Any braking stronger than -1.0 m/s² is preserved!
|
||||
|
||||
return modified_trajectory
|
||||
74
dragonpilot/selfdrive/controls/lib/aem.py
Normal file
74
dragonpilot/selfdrive/controls/lib/aem.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
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_STOP = 0.5 # seconds - for stop sign/light detection
|
||||
AEM_COOLDOWN_TTC = 3.0 # seconds - for lead TTC events
|
||||
|
||||
# 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(AEM_COOLDOWN_STOP)
|
||||
|
||||
# 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)
|
||||
|
||||
145
dragonpilot/selfdrive/controls/lib/dtsc.py
Normal file
145
dragonpilot/selfdrive/controls/lib/dtsc.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
# Physics constants
|
||||
COMFORT_LAT_G = 0.2 # g units - universal human comfort threshold
|
||||
BASE_LAT_ACC = COMFORT_LAT_G * 9.81 # ~2.0 m/s^2
|
||||
SAFETY_FACTOR = 0.9 # 10% safety margin on calculated speeds
|
||||
MIN_CURVE_DISTANCE = 5.0 # meters - minimum distance to react to curves
|
||||
MAX_DECEL = -2.0 # m/s^2 - maximum comfortable deceleration
|
||||
|
||||
|
||||
class DTSC:
|
||||
"""
|
||||
Dynamic Turn Speed Controller - Predictive curve speed management via MPC constraints.
|
||||
|
||||
Core physics: v_max = sqrt(lateral_acceleration / curvature) * safety_factor
|
||||
|
||||
Operation:
|
||||
1. Scans predicted path for curvature (up to ~10 seconds ahead)
|
||||
2. Calculates safe speed for each point using physics + comfort limits
|
||||
3. Identifies critical points where current speed would exceed safe speed
|
||||
4. Calculates required deceleration to reach safe speed at critical point
|
||||
5. Provides deceleration as MPC constraint for smooth trajectory planning
|
||||
"""
|
||||
|
||||
def __init__(self, aggressiveness=1.0):
|
||||
"""
|
||||
Initialize DTSC with user-adjustable aggressiveness.
|
||||
|
||||
Args:
|
||||
aggressiveness: Factor to adjust lateral acceleration limit
|
||||
0.7 = 30% more conservative (slower in curves)
|
||||
1.0 = default balanced behavior
|
||||
1.3 = 30% more aggressive (faster in curves)
|
||||
"""
|
||||
self.aggressiveness = np.clip(aggressiveness, 0.5, 1.5)
|
||||
self.active = False
|
||||
self.debug_msg = ""
|
||||
cloudlog.info(f"DTSC: Initialized with aggressiveness {self.aggressiveness:.2f}")
|
||||
|
||||
def set_aggressiveness(self, value):
|
||||
"""Update aggressiveness factor (0.5 to 1.5)."""
|
||||
self.aggressiveness = np.clip(value, 0.5, 1.5)
|
||||
cloudlog.info(f"DTSC: Aggressiveness updated to {self.aggressiveness:.2f}")
|
||||
|
||||
def get_mpc_constraints(self, model_msg, v_ego, base_a_min, base_a_max):
|
||||
"""
|
||||
Calculate MPC acceleration constraints based on predicted path curvature.
|
||||
|
||||
Args:
|
||||
model_msg: ModelDataV2 containing predicted path
|
||||
v_ego: Current vehicle speed (m/s)
|
||||
base_a_min: Default minimum acceleration constraint
|
||||
base_a_max: Default maximum acceleration constraint
|
||||
|
||||
Returns:
|
||||
(a_min_array, a_max_array): Modified constraints for each MPC timestep
|
||||
"""
|
||||
|
||||
# Initialize with base constraints
|
||||
a_min = np.ones(len(T_IDXS_MPC)) * base_a_min
|
||||
a_max = np.ones(len(T_IDXS_MPC)) * base_a_max
|
||||
|
||||
# Validate model data
|
||||
if not self._is_model_data_valid(model_msg):
|
||||
self.active = False
|
||||
return a_min, a_max
|
||||
|
||||
# Extract predictions for MPC horizon
|
||||
v_pred = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x)
|
||||
turn_rates = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.orientationRate.z)
|
||||
positions = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x)
|
||||
|
||||
# Calculate curvature (turn_rate / velocity)
|
||||
curvatures = np.abs(turn_rates / np.clip(v_pred, 1.0, 100.0))
|
||||
|
||||
# Calculate safe speeds
|
||||
lat_acc_limit = BASE_LAT_ACC * self.aggressiveness
|
||||
safe_speeds = np.sqrt(lat_acc_limit / (curvatures + 1e-6)) * SAFETY_FACTOR
|
||||
|
||||
# Find speed violations
|
||||
speed_excess = v_pred - safe_speeds
|
||||
if np.all(speed_excess <= 0):
|
||||
self._deactivate()
|
||||
return a_min, a_max
|
||||
|
||||
# Find critical point (maximum speed excess)
|
||||
critical_idx = np.argmax(speed_excess)
|
||||
critical_distance = positions[critical_idx]
|
||||
critical_safe_speed = safe_speeds[critical_idx]
|
||||
|
||||
# Only act if we have sufficient distance
|
||||
if critical_distance <= MIN_CURVE_DISTANCE:
|
||||
self._deactivate()
|
||||
return a_min, a_max
|
||||
|
||||
# Calculate required deceleration: a = (v_f^2 - v_i^2) / (2*d)
|
||||
required_decel = (critical_safe_speed**2 - v_ego**2) / (2 * critical_distance)
|
||||
required_decel = max(required_decel, MAX_DECEL)
|
||||
|
||||
# Apply constraint progressively until critical point
|
||||
for i in range(len(T_IDXS_MPC)):
|
||||
t = T_IDXS_MPC[i]
|
||||
distance_at_t = v_ego * t + 0.5 * required_decel * t**2
|
||||
|
||||
if distance_at_t < critical_distance:
|
||||
a_max[i] = min(a_max[i], required_decel)
|
||||
|
||||
# Update status
|
||||
self.active = True
|
||||
self.debug_msg = f"Curve in {critical_distance:.0f}m → {critical_safe_speed*3.6:.0f} km/h"
|
||||
cloudlog.info(f"DTSC: {self.debug_msg} (aggr={self.aggressiveness:.1f})")
|
||||
|
||||
return a_min, a_max
|
||||
|
||||
def _is_model_data_valid(self, model_msg):
|
||||
"""Check if model message contains valid prediction data."""
|
||||
return (len(model_msg.position.x) == ModelConstants.IDX_N and
|
||||
len(model_msg.velocity.x) == ModelConstants.IDX_N and
|
||||
len(model_msg.orientationRate.z) == ModelConstants.IDX_N)
|
||||
|
||||
def _deactivate(self):
|
||||
"""Clear active state and debug message."""
|
||||
self.active = False
|
||||
self.debug_msg = ""
|
||||
57
dragonpilot/selfdrive/controls/lib/road_edge_detector.py
Normal file
57
dragonpilot/selfdrive/controls/lib/road_edge_detector.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/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
|
||||
463
dragonpilot/selfdrive/gpsd/gpsd.py
Executable file
463
dragonpilot/selfdrive/gpsd/gpsd.py
Executable file
@@ -0,0 +1,463 @@
|
||||
#!/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.
|
||||
|
||||
GPS Location Service - Fuses GPS with livePose for smooth position output.
|
||||
|
||||
States:
|
||||
INITIALIZING: Waiting for first GPS fix
|
||||
CALIBRATING: Collecting yaw offset samples (need to be moving > 5 m/s)
|
||||
RUNNING: Outputting calibrated dead-reckoned position
|
||||
RECALIBRATING: Drift detected, blending back to GPS
|
||||
"""
|
||||
import json
|
||||
import numpy as np
|
||||
from enum import Enum
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process
|
||||
from openpilot.common.transformations.coordinates import geodetic2ecef, ecef2geodetic, LocalCoord
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.gps import get_gps_location_service
|
||||
|
||||
|
||||
class State(Enum):
|
||||
INITIALIZING = 0
|
||||
CALIBRATING = 1
|
||||
RUNNING = 2
|
||||
RECALIBRATING = 3
|
||||
|
||||
|
||||
class LiveGPS:
|
||||
# Calibration
|
||||
CALIB_MIN_SPEED = 5.0 # m/s - need speed for reliable GPS bearing
|
||||
CALIB_MIN_SAMPLES = 5 # yaw samples needed
|
||||
CALIB_MAX_TIME = 30.0 # seconds before timeout
|
||||
|
||||
# Recalibration triggers
|
||||
RECALIB_POS_ERROR = 30.0 # meters - triggers gradual recalib
|
||||
RECALIB_POS_HARD = 500.0 # meters - triggers hard reset
|
||||
RECALIB_YAW_ERROR = 0.785 # 45 degrees in radians
|
||||
RECALIB_YAW_HARD = 1.571 # 90 degrees in radians
|
||||
RECALIB_GPS_LOST = 10.0 # seconds
|
||||
|
||||
# GPS quality
|
||||
GPS_MAX_ACCURACY = 30.0 # meters - reject worse
|
||||
GPS_MAX_JUMP = 50.0 # meters - reject jumps
|
||||
GPS_MAX_SPEED = 100.0 # m/s (~360 km/h)
|
||||
|
||||
# Smoothing
|
||||
MAX_POS_CORRECTION = 10.0 # m/s max correction rate
|
||||
MAX_YAW_CORRECTION = 0.524 # 30 deg/s in radians
|
||||
STATIONARY_SPEED = 0.5 # m/s
|
||||
|
||||
def __init__(self):
|
||||
self.state = State.INITIALIZING
|
||||
|
||||
# GPS raw data
|
||||
self.last_gps_pos = None # [lat, lon, alt]
|
||||
self.gps_speed = 0.0
|
||||
self.gps_bearing = 0.0
|
||||
self.gps_accuracy_h = 100.0
|
||||
self.gps_accuracy_v = 100.0
|
||||
self.gps_quality = 1.0 # 0-1 weight
|
||||
self.unix_timestamp_millis = 0
|
||||
|
||||
# Position tracking (NED frame)
|
||||
self.local_coord = None
|
||||
self.pos_ned = np.zeros(3)
|
||||
self.pos_error = np.zeros(3)
|
||||
self.target_pos = np.zeros(3)
|
||||
|
||||
# livePose data
|
||||
self.orientation_ned = np.zeros(3)
|
||||
self.vel_device = np.zeros(3)
|
||||
|
||||
# Yaw calibration
|
||||
self.yaw_offset = 0.0
|
||||
self.yaw_offset_valid = False
|
||||
self.yaw_samples = []
|
||||
self.target_yaw = 0.0
|
||||
|
||||
# Timing
|
||||
self.last_t = None
|
||||
self.last_gps_t = 0.0
|
||||
self.calib_start_t = 0.0
|
||||
|
||||
def get_yaw(self):
|
||||
"""Get calibrated absolute yaw."""
|
||||
if self.yaw_offset_valid:
|
||||
return (self.orientation_ned[2] + self.yaw_offset) % (2 * np.pi)
|
||||
return np.radians(self.gps_bearing)
|
||||
|
||||
def _check_gps_valid(self, gps):
|
||||
"""Check if GPS data is usable."""
|
||||
if abs(gps.latitude) < 0.1 or abs(gps.longitude) < 0.1:
|
||||
return False
|
||||
if abs(gps.latitude) > 90 or abs(gps.longitude) > 180:
|
||||
return False
|
||||
return gps.hasFix or gps.unixTimestampMillis > 0
|
||||
|
||||
def _check_gps_quality(self, t, gps):
|
||||
"""Check quality and detect jumps. Returns (accept, weight)."""
|
||||
# Unknown accuracy = assume decent
|
||||
accuracy = gps.horizontalAccuracy if gps.horizontalAccuracy > 0 else 8.0
|
||||
|
||||
# Reject known bad accuracy
|
||||
if gps.horizontalAccuracy > self.GPS_MAX_ACCURACY:
|
||||
return False, 0.0
|
||||
|
||||
# Jump detection
|
||||
if self.last_gps_pos is not None and self.last_gps_t > 0:
|
||||
dt = t - self.last_gps_t
|
||||
if dt > 0.01:
|
||||
last_ecef = geodetic2ecef(self.last_gps_pos)
|
||||
curr_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
distance = np.linalg.norm(np.array(curr_ecef) - np.array(last_ecef))
|
||||
if distance > max(self.GPS_MAX_JUMP, self.GPS_MAX_SPEED * dt):
|
||||
return False, 0.0
|
||||
|
||||
# Weight by accuracy (5m = 1.0, 30m = 0.17)
|
||||
weight = min(1.0, 5.0 / max(accuracy, 1.0))
|
||||
return True, max(0.1, weight)
|
||||
|
||||
def handle_gps(self, t, gps):
|
||||
"""Process GPS update."""
|
||||
if not self._check_gps_valid(gps):
|
||||
return
|
||||
|
||||
accept, weight = self._check_gps_quality(t, gps)
|
||||
|
||||
# Always store for display (even if rejected)
|
||||
self.last_gps_pos = [gps.latitude, gps.longitude, gps.altitude]
|
||||
self.gps_speed = gps.speed
|
||||
self.gps_bearing = gps.bearingDeg
|
||||
|
||||
if not accept:
|
||||
# Allow poor GPS for initialization only
|
||||
if self.state == State.INITIALIZING:
|
||||
weight = 0.1
|
||||
else:
|
||||
return
|
||||
|
||||
# Store quality data
|
||||
self.gps_accuracy_h = gps.horizontalAccuracy if gps.horizontalAccuracy > 0 else 10.0
|
||||
self.gps_accuracy_v = gps.verticalAccuracy if gps.verticalAccuracy > 0 else 20.0
|
||||
self.gps_quality = weight
|
||||
self.last_gps_t = t
|
||||
self.unix_timestamp_millis = gps.unixTimestampMillis
|
||||
|
||||
# State machine
|
||||
if self.state == State.INITIALIZING:
|
||||
self._init_position(gps)
|
||||
self.state = State.CALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
cloudlog.info("LiveGPS: GPS acquired, calibrating")
|
||||
|
||||
elif self.state == State.CALIBRATING:
|
||||
self._calibrate(t, gps)
|
||||
|
||||
elif self.state == State.RUNNING:
|
||||
self._update_running(t, gps)
|
||||
|
||||
elif self.state == State.RECALIBRATING:
|
||||
self._recalibrate(t, gps)
|
||||
|
||||
def _init_position(self, gps):
|
||||
"""Initialize local coordinate frame."""
|
||||
self.local_coord = LocalCoord.from_geodetic([gps.latitude, gps.longitude, gps.altitude])
|
||||
self.pos_ned = np.zeros(3)
|
||||
self.pos_error = np.zeros(3)
|
||||
|
||||
def _collect_yaw_sample(self, gps):
|
||||
"""Collect yaw calibration sample if conditions met."""
|
||||
if gps.speed > self.CALIB_MIN_SPEED and self.gps_quality > 0.3:
|
||||
gps_yaw = np.radians(gps.bearingDeg)
|
||||
pose_yaw = self.orientation_ned[2]
|
||||
offset = np.arctan2(np.sin(gps_yaw - pose_yaw), np.cos(gps_yaw - pose_yaw))
|
||||
self.yaw_samples.append(offset)
|
||||
|
||||
def _calibrate(self, t, gps):
|
||||
"""Calibration state: collect yaw samples."""
|
||||
self._collect_yaw_sample(gps)
|
||||
|
||||
if len(self.yaw_samples) >= self.CALIB_MIN_SAMPLES:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.yaw_offset_valid = True
|
||||
self._init_position(gps)
|
||||
self.state = State.RUNNING
|
||||
cloudlog.info(f"LiveGPS: calibrated, yaw_offset={np.degrees(self.yaw_offset):.1f}deg")
|
||||
|
||||
elif t - self.calib_start_t > self.CALIB_MAX_TIME:
|
||||
if self.yaw_samples:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.yaw_offset_valid = True
|
||||
self._init_position(gps)
|
||||
self.state = State.RUNNING
|
||||
cloudlog.warning("LiveGPS: calibration timeout")
|
||||
|
||||
def _update_running(self, t, gps):
|
||||
"""Running state: update position error and check for drift."""
|
||||
gps_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
gps_ned = self.local_coord.ecef2ned(gps_ecef)
|
||||
self.pos_error = gps_ned - self.pos_ned
|
||||
|
||||
pos_error_mag = np.linalg.norm(self.pos_error[:2])
|
||||
gps_age = t - self.last_gps_t
|
||||
|
||||
# Check for hard reset conditions
|
||||
if pos_error_mag > self.RECALIB_POS_HARD or gps_age > self.RECALIB_GPS_LOST * 3:
|
||||
cloudlog.warning(f"LiveGPS: hard reset, error={pos_error_mag:.1f}m")
|
||||
self._init_position(gps)
|
||||
self.yaw_offset_valid = False
|
||||
self.state = State.CALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
return
|
||||
|
||||
# Check yaw drift
|
||||
if gps.speed > self.CALIB_MIN_SPEED and self.gps_quality > 0.3:
|
||||
gps_yaw = np.radians(gps.bearingDeg)
|
||||
new_offset = np.arctan2(np.sin(gps_yaw - self.orientation_ned[2]),
|
||||
np.cos(gps_yaw - self.orientation_ned[2]))
|
||||
diff = abs(np.arctan2(np.sin(new_offset - self.yaw_offset),
|
||||
np.cos(new_offset - self.yaw_offset)))
|
||||
|
||||
if diff > self.RECALIB_YAW_HARD:
|
||||
cloudlog.warning(f"LiveGPS: yaw reset, diff={np.degrees(diff):.1f}deg")
|
||||
self.yaw_offset = new_offset
|
||||
self._init_position(gps)
|
||||
elif diff > self.RECALIB_YAW_ERROR:
|
||||
cloudlog.warning(f"LiveGPS: yaw drift, diff={np.degrees(diff):.1f}deg")
|
||||
self.state = State.RECALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
self.target_yaw = new_offset
|
||||
self.target_pos = gps_ned
|
||||
else:
|
||||
# Slow adaptation
|
||||
alpha = 0.1 * self.gps_quality
|
||||
self.yaw_offset += alpha * np.arctan2(np.sin(new_offset - self.yaw_offset),
|
||||
np.cos(new_offset - self.yaw_offset))
|
||||
|
||||
# Check position drift
|
||||
if pos_error_mag > self.RECALIB_POS_ERROR:
|
||||
cloudlog.warning(f"LiveGPS: pos drift, error={pos_error_mag:.1f}m")
|
||||
self.state = State.RECALIBRATING
|
||||
self.calib_start_t = t
|
||||
self.yaw_samples = []
|
||||
self.target_pos = gps_ned
|
||||
|
||||
# Reset anchor if drifted too far
|
||||
if np.linalg.norm(self.pos_ned[:2]) > 100:
|
||||
self._init_position(gps)
|
||||
|
||||
def _recalibrate(self, t, gps):
|
||||
"""Recalibrating state: blend back to GPS."""
|
||||
gps_ecef = geodetic2ecef([gps.latitude, gps.longitude, gps.altitude])
|
||||
self.target_pos = self.local_coord.ecef2ned(gps_ecef)
|
||||
|
||||
self._collect_yaw_sample(gps)
|
||||
if len(self.yaw_samples) >= 3:
|
||||
self.target_yaw = float(np.median(self.yaw_samples[-10:]))
|
||||
|
||||
# Check if done
|
||||
pos_error = np.linalg.norm(self.target_pos - self.pos_ned)
|
||||
if pos_error < 5.0 and len(self.yaw_samples) >= self.CALIB_MIN_SAMPLES:
|
||||
self.yaw_offset = self.target_yaw
|
||||
self.state = State.RUNNING
|
||||
cloudlog.info(f"LiveGPS: recalibrated, error={pos_error:.1f}m")
|
||||
elif t - self.calib_start_t > self.CALIB_MAX_TIME:
|
||||
if self.yaw_samples:
|
||||
self.yaw_offset = float(np.median(self.yaw_samples))
|
||||
self.state = State.RUNNING
|
||||
cloudlog.warning(f"LiveGPS: recalib timeout, error={pos_error:.1f}m")
|
||||
|
||||
def handle_pose(self, t, pose):
|
||||
"""Process livePose update - dead-reckon position."""
|
||||
if pose.orientationNED.valid:
|
||||
self.orientation_ned = np.array([pose.orientationNED.x, pose.orientationNED.y, pose.orientationNED.z])
|
||||
if pose.velocityDevice.valid:
|
||||
self.vel_device = np.array([pose.velocityDevice.x, pose.velocityDevice.y, pose.velocityDevice.z])
|
||||
|
||||
if self.state not in (State.RUNNING, State.RECALIBRATING) or self.local_coord is None:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
if self.last_t is None:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
dt = t - self.last_t
|
||||
if dt <= 0 or dt > 1.0:
|
||||
self.last_t = t
|
||||
return
|
||||
|
||||
# Stationary detection
|
||||
speed = np.linalg.norm(self.vel_device[:2])
|
||||
is_stationary = speed < self.STATIONARY_SPEED and self.gps_speed < self.STATIONARY_SPEED
|
||||
|
||||
# Yaw blending during recalibration
|
||||
if self.state == State.RECALIBRATING and self.yaw_samples:
|
||||
yaw_diff = np.arctan2(np.sin(self.target_yaw - self.yaw_offset),
|
||||
np.cos(self.target_yaw - self.yaw_offset))
|
||||
yaw_rate = 0.9 if abs(yaw_diff) > 0.5 else 0.5
|
||||
correction = np.clip(yaw_rate * dt * yaw_diff, -self.MAX_YAW_CORRECTION * dt, self.MAX_YAW_CORRECTION * dt)
|
||||
self.yaw_offset += correction
|
||||
|
||||
# Transform velocity to NED
|
||||
yaw = self.get_yaw()
|
||||
cos_yaw, sin_yaw = np.cos(yaw), np.sin(yaw)
|
||||
vel_ned = np.array([
|
||||
cos_yaw * self.vel_device[0] - sin_yaw * self.vel_device[1],
|
||||
sin_yaw * self.vel_device[0] + cos_yaw * self.vel_device[1],
|
||||
self.vel_device[2]
|
||||
])
|
||||
|
||||
# Integrate position (skip if stationary)
|
||||
if not is_stationary:
|
||||
self.pos_ned += vel_ned * dt
|
||||
|
||||
# Position correction
|
||||
if is_stationary:
|
||||
correction = self.pos_error * 0.05 * dt
|
||||
elif self.state == State.RECALIBRATING:
|
||||
error = self.target_pos - self.pos_ned
|
||||
rate = 0.95 if np.linalg.norm(error[:2]) > 50 else 0.4
|
||||
correction = error * rate * self.gps_quality * dt
|
||||
else:
|
||||
correction = self.pos_error * 0.8 * self.gps_quality * dt
|
||||
|
||||
# Cap correction
|
||||
mag = np.linalg.norm(correction[:2])
|
||||
max_corr = self.MAX_POS_CORRECTION * dt
|
||||
if mag > max_corr:
|
||||
correction *= max_corr / mag
|
||||
|
||||
self.pos_ned += correction
|
||||
if self.state == State.RUNNING:
|
||||
self.pos_error -= correction
|
||||
|
||||
self.last_t = t
|
||||
|
||||
def get_msg(self, log_mono_time):
|
||||
"""Build liveGPS message."""
|
||||
msg = messaging.new_message('liveGPS')
|
||||
msg.logMonoTime = log_mono_time
|
||||
gps = msg.liveGPS
|
||||
|
||||
t = log_mono_time * 1e-9
|
||||
gps_age = t - self.last_gps_t
|
||||
is_valid = self.state in (State.RUNNING, State.RECALIBRATING)
|
||||
gps_ok = is_valid and gps_age < 5.0
|
||||
|
||||
if is_valid and self.local_coord is not None:
|
||||
pos_ecef = self.local_coord.ned2ecef(self.pos_ned)
|
||||
geodetic = ecef2geodetic(pos_ecef)
|
||||
gps.latitude = float(geodetic[0])
|
||||
gps.longitude = float(geodetic[1])
|
||||
gps.altitude = float(geodetic[2])
|
||||
gps.bearingDeg = float(np.degrees(self.get_yaw()) % 360)
|
||||
gps.speed = float(np.linalg.norm(self.vel_device[:2]))
|
||||
gps.horizontalAccuracy = float(self.gps_accuracy_h + np.linalg.norm(self.pos_ned[:2]) * 0.1)
|
||||
gps.verticalAccuracy = float(self.gps_accuracy_v)
|
||||
gps.status = 'valid' if gps_ok else ('recalibrating' if self.state == State.RECALIBRATING else 'gpsStale')
|
||||
|
||||
elif self.last_gps_pos is not None:
|
||||
gps.latitude = float(self.last_gps_pos[0])
|
||||
gps.longitude = float(self.last_gps_pos[1])
|
||||
gps.altitude = float(self.last_gps_pos[2])
|
||||
gps.speed = float(self.gps_speed)
|
||||
gps.bearingDeg = float(self.gps_bearing)
|
||||
gps.horizontalAccuracy = float(self.gps_accuracy_h) if self.gps_accuracy_h > 0 else 50.0
|
||||
gps.verticalAccuracy = float(self.gps_accuracy_v) if self.gps_accuracy_v > 0 else 50.0
|
||||
gps.status = 'calibrating' if self.state == State.CALIBRATING else 'initializing'
|
||||
|
||||
else:
|
||||
gps.latitude = 0.0
|
||||
gps.longitude = 0.0
|
||||
gps.altitude = 0.0
|
||||
gps.speed = 0.0
|
||||
gps.bearingDeg = 0.0
|
||||
gps.horizontalAccuracy = 100.0
|
||||
gps.verticalAccuracy = 100.0
|
||||
gps.status = 'noGps'
|
||||
|
||||
gps.gpsOK = gps_ok
|
||||
gps.unixTimestampMillis = self.unix_timestamp_millis
|
||||
gps.lastGpsTimestamp = int(self.last_gps_t * 1e9) if self.last_gps_t > 0 else 0
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def main():
|
||||
config_realtime_process([0, 1, 2, 3], 5)
|
||||
|
||||
params = Params()
|
||||
gps_service = get_gps_location_service(params)
|
||||
cloudlog.info(f"LiveGPS: using {gps_service}")
|
||||
|
||||
pm = messaging.PubMaster(['liveGPS'])
|
||||
sm = messaging.SubMaster([gps_service, 'livePose'], poll='livePose', ignore_alive=[gps_service])
|
||||
|
||||
gps = LiveGPS()
|
||||
|
||||
# Load last GPS position or default to Taipei 101
|
||||
try:
|
||||
last_pos = params.get("LastGPSPosition")
|
||||
if last_pos:
|
||||
pos_data = json.loads(last_pos)
|
||||
gps.last_gps_pos = [pos_data['latitude'], pos_data['longitude'], pos_data['altitude']]
|
||||
cloudlog.info(f"LiveGPS: loaded last position: {gps.last_gps_pos}")
|
||||
else:
|
||||
raise ValueError("No saved position")
|
||||
except Exception:
|
||||
gps.last_gps_pos = [25.033976, 121.564472, 10.0] # Taipei 101
|
||||
cloudlog.info("LiveGPS: using default position (Taipei 101)")
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
|
||||
if sm.updated[gps_service] and sm.valid[gps_service]:
|
||||
gps.handle_gps(sm.logMonoTime[gps_service] * 1e-9, sm[gps_service])
|
||||
|
||||
if sm.updated['livePose']:
|
||||
if sm.valid['livePose']:
|
||||
gps.handle_pose(sm.logMonoTime['livePose'] * 1e-9, sm['livePose'])
|
||||
|
||||
msg = gps.get_msg(sm.logMonoTime['livePose'])
|
||||
pm.send('liveGPS', msg)
|
||||
|
||||
# Save position periodically
|
||||
if sm.frame % 1200 == 0 and gps.state == State.RUNNING and gps.last_gps_pos:
|
||||
if (sm.logMonoTime['livePose'] * 1e-9 - gps.last_gps_t) < 5.0:
|
||||
params.put("LastGPSPosition", json.dumps({
|
||||
'latitude': gps.last_gps_pos[0],
|
||||
'longitude': gps.last_gps_pos[1],
|
||||
'altitude': gps.last_gps_pos[2]
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
dragonpilot/selfdrive/ui/.gitignore
vendored
Normal file
0
dragonpilot/selfdrive/ui/.gitignore
vendored
Normal file
152
dragonpilot/selfdrive/ui/beepd.py
Normal file
152
dragonpilot/selfdrive/ui/beepd.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2025, Rick Lan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, and/or sublicense,
|
||||
for non-commercial purposes only, subject to the following conditions:
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
- Commercial use (e.g. use in a product, service, or activity intended to
|
||||
generate revenue) is prohibited without explicit written permission from
|
||||
the copyright holder.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
from cereal import car, messaging
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
import threading
|
||||
|
||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
||||
|
||||
class Beepd:
|
||||
def __init__(self, test=False):
|
||||
self.current_alert = AudibleAlert.none
|
||||
self._test = test
|
||||
self.enable_gpio()
|
||||
|
||||
def enable_gpio(self):
|
||||
try:
|
||||
if self._test:
|
||||
print("enabling GPIO")
|
||||
subprocess.run("echo 42 | sudo tee /sys/class/gpio/export",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
except Exception:
|
||||
if self._test:
|
||||
print("GPIO failed to enable")
|
||||
pass
|
||||
subprocess.run("echo \"out\" | sudo tee /sys/class/gpio/gpio42/direction",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def _beep(self, on):
|
||||
val = "1" if on else "0"
|
||||
subprocess.run(f"echo \"{val}\" | sudo tee /sys/class/gpio/gpio42/value",
|
||||
shell=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
encoding='utf8')
|
||||
|
||||
def engage(self):
|
||||
if self._test:
|
||||
print("beepd: engage")
|
||||
self._beep(True)
|
||||
time.sleep(0.05)
|
||||
self._beep(False)
|
||||
|
||||
def disengage(self):
|
||||
if self._test:
|
||||
print("beepd: disengage")
|
||||
for _ in range(2):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def prompt(self):
|
||||
if self._test:
|
||||
print("beepd: prompt")
|
||||
for _ in range(3):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def warning_immediate(self):
|
||||
if self._test:
|
||||
print("beepd: warning_immediate")
|
||||
for _ in range(5):
|
||||
self._beep(True)
|
||||
time.sleep(0.01)
|
||||
self._beep(False)
|
||||
time.sleep(0.01)
|
||||
|
||||
def dispatch_beep(self, func):
|
||||
threading.Thread(target=func, daemon=True).start()
|
||||
|
||||
def update_alert(self, new_alert):
|
||||
current_alert_played_once = self.current_alert == AudibleAlert.none
|
||||
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
|
||||
self.current_alert = new_alert
|
||||
if new_alert == AudibleAlert.engage:
|
||||
self.dispatch_beep(self.engage)
|
||||
if new_alert == AudibleAlert.disengage:
|
||||
self.dispatch_beep(self.disengage)
|
||||
if new_alert == AudibleAlert.prompt:
|
||||
self.dispatch_beep(self.prompt)
|
||||
if new_alert == AudibleAlert.warningImmediate:
|
||||
self.dispatch_beep(self.warning_immediate)
|
||||
|
||||
def get_audible_alert(self, sm):
|
||||
if sm.updated['selfdriveState']:
|
||||
new_alert = sm['selfdriveState'].alertSound.raw
|
||||
self.update_alert(new_alert)
|
||||
|
||||
def test_beepd_thread(self):
|
||||
frame = 0
|
||||
rk = Ratekeeper(20)
|
||||
pm = messaging.PubMaster(['selfdriveState'])
|
||||
while True:
|
||||
cs = messaging.new_message('selfdriveState')
|
||||
if frame == 20:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.engage
|
||||
if frame == 40:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.disengage
|
||||
if frame == 60:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.prompt
|
||||
if frame == 80:
|
||||
cs.selfdriveState.alertSound = AudibleAlert.warningImmediate
|
||||
|
||||
pm.send("selfdriveState", cs)
|
||||
frame += 1
|
||||
rk.keep_time()
|
||||
|
||||
def beepd_thread(self):
|
||||
if self._test:
|
||||
threading.Thread(target=self.test_beepd_thread).start()
|
||||
|
||||
sm = messaging.SubMaster(['selfdriveState'])
|
||||
rk = Ratekeeper(20)
|
||||
|
||||
while True:
|
||||
sm.update(0)
|
||||
self.get_audible_alert(sm)
|
||||
rk.keep_time()
|
||||
|
||||
def main():
|
||||
s = Beepd(test=False)
|
||||
s.beepd_thread()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
dragonpilot/selfdrive/ui/dashy_qr.py
Normal file
93
dragonpilot/selfdrive/ui/dashy_qr.py
Normal file
@@ -0,0 +1,93 @@
|
||||
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()
|
||||
241
dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py
Normal file
241
dragonpilot/selfdrive/ui/layouts/settings/dragonpilot.py
Normal file
@@ -0,0 +1,241 @@
|
||||
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
|
||||
|
||||
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._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
|
||||
|
||||
for i, section in enumerate(settings_data):
|
||||
if self._check_condition(section.get("condition")):
|
||||
formatted_title = f"### {section['title']} ###"
|
||||
self._toggles[f"title_{i}"] = simple_item(title=formatted_title)
|
||||
for setting in section.get("settings", []):
|
||||
if self._check_condition(setting.get("condition")) and self._check_brands(setting.get("brands")):
|
||||
self._create_item(setting)
|
||||
|
||||
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 _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)
|
||||
|
||||
# Handle initial enabled state
|
||||
if "initially_enabled_by" in setting:
|
||||
enabled_by = setting["initially_enabled_by"]
|
||||
source_param = enabled_by["param"]
|
||||
source_val_raw = ui_state.params.get(source_param)
|
||||
source_val = source_val_raw.decode() if isinstance(source_val_raw, bytes) else source_val_raw
|
||||
if source_val is None:
|
||||
source_val = enabled_by.get("default")
|
||||
|
||||
if source_val is not None:
|
||||
condition_str = enabled_by["condition"]
|
||||
try:
|
||||
is_enabled = eval(condition_str, {"value": int(source_val)})
|
||||
args["enabled"] = is_enabled
|
||||
except Exception:
|
||||
args["enabled"] = True
|
||||
else:
|
||||
args["enabled"] = True
|
||||
|
||||
# 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))
|
||||
|
||||
side_effects = []
|
||||
if "on_change" in setting:
|
||||
for effect in setting["on_change"]:
|
||||
target_key = effect.get("target")
|
||||
action = effect.get("action")
|
||||
condition_str = effect.get("condition")
|
||||
|
||||
if target_key and action == "set_enabled" and condition_str:
|
||||
def create_side_effect(tk=target_key, cs=condition_str):
|
||||
def side_effect_action(val):
|
||||
if tk in self._toggles:
|
||||
try:
|
||||
is_enabled = eval(cs, {"value": val})
|
||||
self._toggles[tk].action_item.set_enabled(is_enabled)
|
||||
except Exception:
|
||||
pass
|
||||
return side_effect_action
|
||||
side_effects.append(create_side_effect())
|
||||
|
||||
def combined_callback(val):
|
||||
if primary_action:
|
||||
primary_action(val)
|
||||
for effect in side_effects:
|
||||
effect(val)
|
||||
|
||||
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"))
|
||||
gui_app.set_modal_overlay(dialog, callback=reset_dp_conf)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
self._update_toggles()
|
||||
|
||||
def _update_toggles(self):
|
||||
ui_state.update_params()
|
||||
|
||||
# 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))
|
||||
else: # spin_button_item and text_spin_button_item
|
||||
pass
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
60
dragonpilot/selfdrive/ui/mici/layouts/dashy_qrcode.py
Normal file
60
dragonpilot/selfdrive/ui/mici/layouts/dashy_qrcode.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
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 = MiciLabel(tr("scan to access"), font_size=32, font_weight=FontWeight.BOLD,
|
||||
color=rl.WHITE, wrap_text=True)
|
||||
self._subtitle_label = MiciLabel("dashy", font_size=48, font_weight=FontWeight.DISPLAY,
|
||||
color=rl.WHITE)
|
||||
self._or_label = MiciLabel(tr("or open browser"), font_size=24, font_weight=FontWeight.NORMAL,
|
||||
color=rl.GRAY)
|
||||
self._url_label = MiciLabel("", font_size=24, font_weight=FontWeight.NORMAL,
|
||||
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_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_width(text_width)
|
||||
self._url_label.set_position(text_x, rect.y + rect.height - 24)
|
||||
self._url_label.render()
|
||||
46
dragonpilot/selfdrive/ui/update_translations.py
Executable file
46
dragonpilot/selfdrive/ui/update_translations.py
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/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()
|
||||
289
dragonpilot/settings.py
Normal file
289
dragonpilot/settings.py
Normal file
@@ -0,0 +1,289 @@
|
||||
try:
|
||||
from dragonpilot.system.ui.lib.multilang import tr
|
||||
except:
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
SETTINGS = [
|
||||
{
|
||||
"title": "Toyota / Lexus",
|
||||
"condition": "brand == 'toyota'",
|
||||
"settings": [
|
||||
{
|
||||
"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."),
|
||||
},
|
||||
{
|
||||
"key": "dp_toyota_tss1_sng",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable TSS1 SnG Mod"),
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"key": "dp_toyota_stock_lon",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Use Stock Longitudinal Control"),
|
||||
"description": ""
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "VAG",
|
||||
"condition": "brand == 'volkswagen'",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_vag_a0_sng",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("MQB A0 SnG Mod"),
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"key": "dp_vag_pq_steering_patch",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("PQ Steering Patch"),
|
||||
"description": "",
|
||||
},
|
||||
{
|
||||
"key": "dp_vag_avoid_eps_lockout",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Avoid EPS Lockout"),
|
||||
"description": ""
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Mazda",
|
||||
"condition": "brand == 'mazda'",
|
||||
"settings": [
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Lateral",
|
||||
"settings": [
|
||||
{
|
||||
"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"],
|
||||
},
|
||||
{
|
||||
"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"),
|
||||
"on_change": [{
|
||||
"target": "dp_lat_lca_auto_sec",
|
||||
"action": "set_enabled",
|
||||
"condition": "value > 0"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"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"),
|
||||
"initially_enabled_by": {
|
||||
"param": "dp_lat_lca_speed",
|
||||
"condition": "value > 0",
|
||||
"default": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"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."),
|
||||
},
|
||||
{
|
||||
"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"),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Longitudinal",
|
||||
"condition": "openpilotLongitudinalControl",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_lon_acm",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable Adaptive Coasting Mode (ACM)"),
|
||||
"description": lambda: tr("Adaptive Coasting Mode (ACM) reduces braking to allow smoother coasting when appropriate."),
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_aem",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Adaptive Experimental Mode (AEM)"),
|
||||
"description": lambda: tr("Adaptive mode switcher between ACC and Blended based on driving context."),
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_dtsc",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Dynamic Turn Speed Control (DTSC)"),
|
||||
"description": lambda: tr("DTSC automatically adjusts the vehicle's predicted speed based on upcoming road curvature and grip conditions.<br>Originally from the openpilot TACO branch."),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "UI",
|
||||
"settings": [
|
||||
{
|
||||
"key": "dp_ui_display_mode",
|
||||
"type": "text_spin_button_item",
|
||||
"title": lambda: tr("Display Mode"),
|
||||
"description": lambda: tr("Std.: Stock behavior.<br>MAIN+: ACC MAIN on = Display ON.<br>OP+: OP enabled = Display ON.<br>MAIN-: ACC MAIN on = Display OFF<br>OP-: OP enabled = Display OFF."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Std."),
|
||||
lambda: tr("MAIN+"),
|
||||
lambda: tr("OP+"),
|
||||
lambda: tr("MAIN-"),
|
||||
lambda: tr("OP-"),
|
||||
],
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_hide_hud_speed_kph",
|
||||
"type": "spin_button_item",
|
||||
"title": lambda: tr("Hide HUD When Moves above:"),
|
||||
"description": lambda: tr("To prevent screen burn-in, hide Speed, MAX Speed, and Steering/DM Icons when the car moves.<br>Off = Stock Behavior<br>1 km/h = 0.6 mph"),
|
||||
"default": 0,
|
||||
"min_val": 0,
|
||||
"max_val": 120,
|
||||
"step": 5,
|
||||
"suffix": lambda: tr("km/h"),
|
||||
"special_value_text": lambda: tr("Off"),
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_rainbow",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Rainbow Driving Path like Tesla"),
|
||||
"description": lambda: tr("Why not?"),
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_lead",
|
||||
"type": "text_spin_button_item",
|
||||
"title": lambda: tr("Display Lead Stats"),
|
||||
"description": lambda: tr("Display the statistics of lead car and/or radar tracking points.<br>Lead: Lead stats only<br>Radar: Radar tracking point stats only<br>All: Lead and Radar stats<br>NOTE: Radar option only works on certain vehicle models."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Off"),
|
||||
lambda: tr("Lead"),
|
||||
lambda: tr("Radar"),
|
||||
lambda: tr("All"),
|
||||
],
|
||||
"condition": "not MICI",
|
||||
},
|
||||
{
|
||||
"key": "dp_ui_mici",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Use MICI (comma four) UI"),
|
||||
"description": lambda: tr("Why not?"),
|
||||
"condition": "not MICI",
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": "Device",
|
||||
"settings": [
|
||||
{
|
||||
"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",
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_beep",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Enable Beep (Warning)"),
|
||||
"description": lambda: tr("Use Buzzer for audiable alerts."),
|
||||
"condition": "LITE",
|
||||
},
|
||||
{
|
||||
"key": "dp_lon_ext_radar",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("Use External Radar"),
|
||||
"description": lambda: tr("See https://github.com/eFiniLan/openpilot-ext-radar-addon for more information."),
|
||||
},
|
||||
{
|
||||
"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."),
|
||||
"default": 0,
|
||||
"options": [
|
||||
lambda: tr("Std."),
|
||||
lambda: tr("Warning"),
|
||||
lambda: tr("Off"),
|
||||
],
|
||||
"condition": "not LITE",
|
||||
},
|
||||
{
|
||||
"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"),
|
||||
},
|
||||
{
|
||||
"key": "dp_dev_dashy",
|
||||
"type": "toggle_item",
|
||||
"title": lambda: tr("dashy HUD"),
|
||||
"description": lambda: tr("dashy - dragonpilot's all-in-one system hub for you.<br><br>Visit http://<device_ip>:5088 to access.<br><br>Enable this to use HUD feature (live streaming)."),
|
||||
},
|
||||
{
|
||||
"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"),
|
||||
},
|
||||
{
|
||||
"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."),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
]
|
||||
52
dragonpilot/system/ui/lib/multilang.py
Normal file
52
dragonpilot/system/ui/lib/multilang.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import gettext
|
||||
from openpilot.system.ui.lib.multilang import (
|
||||
multilang as base_multilang,
|
||||
TRANSLATIONS_DIR,
|
||||
tr_noop,
|
||||
)
|
||||
|
||||
|
||||
class DpMultilang:
|
||||
"""Wrapper that syncs with base multilang and adds dragonpilot translations."""
|
||||
|
||||
def __init__(self):
|
||||
self._dragon_translation: gettext.NullTranslations | gettext.GNUTranslations = gettext.NullTranslations()
|
||||
self._loaded_language: str = ""
|
||||
|
||||
@property
|
||||
def languages(self):
|
||||
"""Delegate to base multilang."""
|
||||
return base_multilang.languages
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
"""Delegate to base multilang."""
|
||||
return base_multilang.language
|
||||
|
||||
def _ensure_loaded(self):
|
||||
"""Reload dragon translations if base language changed."""
|
||||
current_lang = base_multilang.language
|
||||
if current_lang != self._loaded_language:
|
||||
self._loaded_language = current_lang
|
||||
try:
|
||||
with TRANSLATIONS_DIR.joinpath(f'dragonpilot_{current_lang}.mo').open('rb') as fh:
|
||||
self._dragon_translation = gettext.GNUTranslations(fh)
|
||||
except FileNotFoundError:
|
||||
self._dragon_translation = gettext.NullTranslations()
|
||||
|
||||
def tr(self, text: str) -> str:
|
||||
self._ensure_loaded()
|
||||
result = self._dragon_translation.gettext(text)
|
||||
return result if result != text else base_multilang.tr(text)
|
||||
|
||||
def trn(self, singular: str, plural: str, n: int) -> str:
|
||||
self._ensure_loaded()
|
||||
result = self._dragon_translation.ngettext(singular, plural, n)
|
||||
return result if result not in (singular, plural) else base_multilang.trn(singular, plural, n)
|
||||
|
||||
|
||||
multilang = DpMultilang()
|
||||
|
||||
tr, trn = multilang.tr, multilang.trn
|
||||
|
||||
__all__ = ['multilang', 'tr', 'trn', 'tr_noop', 'TRANSLATIONS_DIR']
|
||||
@@ -27,6 +27,34 @@ function agnos_init {
|
||||
fi
|
||||
}
|
||||
|
||||
set_tici_hw() {
|
||||
if grep -q "tici" /sys/firmware/devicetree/base/model 2>/dev/null; then
|
||||
echo "Querying panda MCU type..."
|
||||
MCU_OUTPUT=$(python -c "from panda_tici import Panda; p = Panda(cli=False); print(p.get_mcu_type()); p.close()" 2>/dev/null)
|
||||
|
||||
if [[ "$MCU_OUTPUT" == *"McuType.F4"* ]]; then
|
||||
echo "TICI (DOS) detected"
|
||||
elif [[ "$MCU_OUTPUT" == *"McuType.H7"* ]]; then
|
||||
echo "TICI (TRES) detected"
|
||||
export TICI_TRES=1
|
||||
else
|
||||
echo "TICI (UNKNOWN) detected"
|
||||
fi
|
||||
export TICI_HW=1
|
||||
fi
|
||||
}
|
||||
|
||||
set_lite_hw() {
|
||||
if grep -q "tici" /sys/firmware/devicetree/base/model 2>/dev/null; then
|
||||
output=$(i2cget -y 0 0x10 0x00 2>/dev/null)
|
||||
|
||||
if [ -z "$output" ]; then
|
||||
echo "Lite HW"
|
||||
export LITE=1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function launch {
|
||||
# Remove orphaned git lock if it exists on boot
|
||||
[ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
|
||||
@@ -71,6 +99,8 @@ function launch {
|
||||
|
||||
# hardware specific init
|
||||
if [ -f /AGNOS ]; then
|
||||
set_tici_hw
|
||||
set_lite_hw
|
||||
agnos_init
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
SConscript(['opendbc/dbc/SConscript'])
|
||||
|
||||
# test files
|
||||
if GetOption('extras'):
|
||||
SConscript('opendbc/safety/tests/libsafety/SConscript')
|
||||
#if GetOption('extras'):
|
||||
SConscript('opendbc/safety/tests/libsafety/SConscript')
|
||||
|
||||
@@ -91,6 +91,9 @@ class Bus(StrEnum):
|
||||
party = auto()
|
||||
ap_party = auto()
|
||||
|
||||
sdsu = auto()
|
||||
|
||||
zss = auto()
|
||||
|
||||
def rate_limit(new_value, last_value, dw_step, up_step):
|
||||
return float(np.clip(new_value, last_value + dw_step, last_value + up_step))
|
||||
|
||||
@@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.notCar = True
|
||||
ret.brand = "body"
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.body)]
|
||||
|
||||
@@ -82,8 +82,8 @@ def can_fingerprint(can_recv: CanRecvCallable) -> tuple[str | None, dict[int, di
|
||||
|
||||
# **** for use live only ****
|
||||
def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, num_pandas: int,
|
||||
cached_params: CarParamsT | None) -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
|
||||
fixed_fingerprint = os.environ.get('FINGERPRINT', "")
|
||||
cached_params: CarParamsT | None, dp_fingerprint: str = "") -> tuple[str | None, dict, str, list[CarParams.CarFw], CarParams.FingerprintSource, bool]:
|
||||
fixed_fingerprint = os.environ.get('FINGERPRINT', dp_fingerprint)
|
||||
skip_fw_query = os.environ.get('SKIP_FW_QUERY', False)
|
||||
disable_fw_cache = os.environ.get('DISABLE_FW_CACHE', False)
|
||||
ecu_rx_addrs = set()
|
||||
@@ -149,15 +149,15 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
|
||||
|
||||
|
||||
def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multiplexing: ObdCallback, alpha_long_allowed: bool,
|
||||
is_release: bool, num_pandas: int = 1, cached_params: CarParamsT | None = None):
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params)
|
||||
is_release: bool, num_pandas: int = 1, dp_params: int = 0, cached_params: CarParamsT | None = None, dp_fingerprint: str = ""):
|
||||
candidate, fingerprints, vin, car_fw, source, exact_match = fingerprint(can_recv, can_send, set_obd_multiplexing, num_pandas, cached_params, dp_fingerprint=dp_fingerprint)
|
||||
|
||||
if candidate is None:
|
||||
carlog.error({"event": "car doesn't match any fingerprints", "fingerprints": repr(fingerprints)})
|
||||
candidate = "MOCK"
|
||||
|
||||
CarInterface = interfaces[candidate]
|
||||
CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, docs=False)
|
||||
CP: CarParams = CarInterface.get_params(candidate, fingerprints, car_fw, alpha_long_allowed, is_release, dp_params, docs=False)
|
||||
CP.carVin = vin
|
||||
CP.carFw = car_fw
|
||||
CP.fingerprintSource = source
|
||||
|
||||
@@ -13,7 +13,7 @@ class CarInterface(CarInterfaceBase):
|
||||
RadarInterface = RadarInterface
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "chrysler"
|
||||
ret.dashcamOnly = candidate in RAM_HD
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def get_params_for_docs(platform) -> CarParams:
|
||||
cp_platform = platform if platform in interfaces else MOCK.MOCK
|
||||
CP: CarParams = interfaces[cp_platform].get_params(cp_platform, fingerprint=gen_empty_fingerprint(),
|
||||
car_fw=[CarParams.CarFw(ecu=CarParams.Ecu.unknown)],
|
||||
alpha_long=True, is_release=False, docs=True)
|
||||
alpha_long=True, is_release=False, dp_params=0, docs=True)
|
||||
return CP
|
||||
|
||||
|
||||
|
||||
@@ -265,10 +265,10 @@ class CarDocs:
|
||||
# longitudinal column
|
||||
op_long = "Stock"
|
||||
if CP.alphaLongitudinalAvailable:
|
||||
op_long = "openpilot available"
|
||||
op_long = "dragonpilot available"
|
||||
self.footnotes.append(CommonFootnote.EXP_LONG_AVAIL)
|
||||
elif CP.openpilotLongitudinalControl:
|
||||
op_long = "openpilot"
|
||||
op_long = "dragonpilot"
|
||||
|
||||
# min steer & enable speed columns
|
||||
# TODO: set all the min steer speeds in carParams and remove this
|
||||
|
||||
@@ -113,6 +113,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.lc_button, prev_lc_button, {1: ButtonType.lkas}),
|
||||
]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -26,7 +26,7 @@ class CarInterface(CarInterfaceBase):
|
||||
return CarControllerParams.ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "ford"
|
||||
|
||||
ret.radarUnavailable = Bus.radar not in DBC[candidate]
|
||||
|
||||
@@ -82,7 +82,7 @@ class CarInterface(CarInterfaceBase):
|
||||
return self.lateral_accel_from_torque_linear
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "gm"
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.gm)]
|
||||
ret.autoResumeSng = False
|
||||
|
||||
@@ -48,6 +48,8 @@ class CarState(CarStateBase):
|
||||
# When available we use cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] to populate vEgoCluster
|
||||
# However, on cars without a digital speedometer this is not always present (HRV, FIT, CRV 2016, ILX and RDX)
|
||||
self.dash_speed_seen = False
|
||||
self.is_metric = False
|
||||
self.v_cruise_factor = 1.
|
||||
|
||||
def update(self, can_parsers) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
@@ -68,7 +70,7 @@ class CarState(CarStateBase):
|
||||
self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"]
|
||||
|
||||
# used for car hud message
|
||||
self.is_metric = not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"]
|
||||
self.is_metric = True if self.CP.carFingerprint in (CAR.HONDA_ODYSSEY_TWN) else not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"]
|
||||
self.v_cruise_factor = CV.MPH_TO_MS if self.dynamic_v_cruise_units and not self.is_metric else CV.KPH_TO_MS
|
||||
|
||||
# ******************* parse out can *******************
|
||||
@@ -127,10 +129,11 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.espDisabled = cp.vl["VSA_STATUS"]["ESP_DISABLED"] != 0
|
||||
|
||||
self.dash_speed_seen = self.dash_speed_seen or cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] > 1e-3
|
||||
if self.dash_speed_seen:
|
||||
conversion = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
|
||||
ret.vEgoCluster = cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] * conversion
|
||||
if self.CP.carFingerprint not in (CAR.HONDA_ODYSSEY_TWN):
|
||||
self.dash_speed_seen = self.dash_speed_seen or cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] > 1e-3
|
||||
if self.dash_speed_seen:
|
||||
conversion = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS
|
||||
ret.vEgoCluster = cp.vl["CAR_SPEED"]["ROUGH_CAR_SPEED_2"] * conversion
|
||||
|
||||
ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE"]
|
||||
ret.steeringRateDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE_RATE"]
|
||||
@@ -219,6 +222,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.cruise_setting, prev_cruise_setting, SETTINGS_BUTTONS_DICT),
|
||||
]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
def get_can_parsers(self, CP):
|
||||
|
||||
@@ -577,6 +577,17 @@ FW_VERSIONS = {
|
||||
b'54008-THR-A020\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.HONDA_ODYSSEY_TWN: {
|
||||
(Ecu.eps, 0x18da30f1, None): [
|
||||
b'39990-T6A-J210\x00\x00',
|
||||
],
|
||||
(Ecu.srs, 0x18da53f1, None): [
|
||||
b'77959-T6A-P110\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x18dab0f1, None): [
|
||||
b'36161-T6A-P040\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.HONDA_ODYSSEY_5G_MMR: {
|
||||
(Ecu.vsa, 0x18da28f1, None): [
|
||||
b'57114-THR-A240\x00\x00',
|
||||
|
||||
@@ -31,7 +31,7 @@ class CarInterface(CarInterfaceBase):
|
||||
return CarControllerParams.NIDEC_ACCEL_MIN, np.interp(current_speed, ACCEL_MAX_BP, ACCEL_MAX_VALS)
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "honda"
|
||||
|
||||
CAN = CanBus(ret, fingerprint)
|
||||
@@ -91,13 +91,25 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.longitudinalTuning.kiV = [1.2, 0.8, 0.5]
|
||||
|
||||
# Disable control if EPS mod detected
|
||||
eps_modified = False
|
||||
for fw in car_fw:
|
||||
if fw.ecu == "eps" and b"," in fw.fwVersion:
|
||||
ret.dashcamOnly = True
|
||||
# ret.dashcamOnly = True
|
||||
eps_modified = True
|
||||
|
||||
if candidate == CAR.HONDA_CIVIC:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]]
|
||||
if eps_modified:
|
||||
# stock request input values: 0x0000, 0x00DE, 0x014D, 0x01EF, 0x0290, 0x0377, 0x0454, 0x0610, 0x06EE
|
||||
# stock request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x1680, 0x1680
|
||||
# modified request output values: 0x0000, 0x0917, 0x0DC5, 0x1017, 0x119F, 0x140B, 0x1680, 0x2880, 0x3180
|
||||
# stock filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108
|
||||
# modified filter output values: 0x009F, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0108, 0x0400, 0x0480
|
||||
# note: max request allowed is 4096, but request is capped at 3840 in firmware, so modifications result in 2x max
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 8000], [0, 2560, 3840]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.1]]
|
||||
else:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]]
|
||||
|
||||
elif candidate in (CAR.HONDA_CIVIC_BOSCH, CAR.HONDA_CIVIC_BOSCH_DIESEL):
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
|
||||
@@ -112,7 +124,10 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
elif candidate == CAR.HONDA_ACCORD:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
|
||||
if eps_modified:
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.09]]
|
||||
else:
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]]
|
||||
|
||||
elif candidate == CAR.ACURA_ILX:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] # TODO: determine if there is a dead zone at the top end
|
||||
@@ -124,8 +139,15 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.wheelSpeedFactor = 1.025
|
||||
|
||||
elif candidate == CAR.HONDA_CRV_5G:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.64], [0.192]]
|
||||
if eps_modified:
|
||||
# stock request input values: 0x0000, 0x00DB, 0x01BB, 0x0296, 0x0377, 0x0454, 0x0532, 0x0610, 0x067F
|
||||
# stock request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x129A, 0x134D, 0x1400
|
||||
# modified request output values: 0x0000, 0x0500, 0x0A15, 0x0E6D, 0x1100, 0x1200, 0x1ACD, 0x239A, 0x2800
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 10000], [0, 2560, 3840]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.21], [0.07]]
|
||||
else:
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.64], [0.192]]
|
||||
ret.wheelSpeedFactor = 1.025
|
||||
|
||||
elif candidate == CAR.HONDA_CRV_HYBRID:
|
||||
@@ -141,6 +163,10 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]]
|
||||
|
||||
elif candidate == CAR.HONDA_ODYSSEY_TWN:
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]]
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 32767], [0, 32767]] # TODO: determine if there is a dead zone at the top end
|
||||
|
||||
elif candidate in (CAR.HONDA_HRV, CAR.HONDA_HRV_3G):
|
||||
ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]]
|
||||
if candidate == CAR.HONDA_HRV:
|
||||
@@ -213,7 +239,7 @@ class CarInterface(CarInterfaceBase):
|
||||
# to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not
|
||||
# conflict with PCM acc
|
||||
ret.autoResumeSng = candidate in (HONDA_BOSCH | {CAR.HONDA_CIVIC})
|
||||
ret.minEnableSpeed = -1. if ret.autoResumeSng else 25.51 * CV.MPH_TO_MS
|
||||
ret.minEnableSpeed = -1. #if ret.autoResumeSng else 25.51 * CV.MPH_TO_MS
|
||||
|
||||
ret.steerLimitTimer = 0.8
|
||||
ret.radarDelay = 0.1
|
||||
|
||||
@@ -324,6 +324,12 @@ class CAR(Platforms):
|
||||
radar_dbc_dict('honda_odyssey_exl_2018_generated'),
|
||||
flags=HondaFlags.NIDEC_ALT_PCM_ACCEL | HondaFlags.HAS_ALL_DOOR_STATES,
|
||||
)
|
||||
HONDA_ODYSSEY_TWN = HondaNidecPlatformConfig(
|
||||
[],
|
||||
CarSpecs(mass=1865, wheelbase=2.9, steerRatio=14.35, centerToFrontRatio=0.44, tireStiffnessFactor=0.82),
|
||||
radar_dbc_dict('honda_odyssey_twn_2018_generated'),
|
||||
flags=HondaFlags.NIDEC_ALT_SCM_MESSAGES,
|
||||
)
|
||||
ACURA_RDX = HondaNidecPlatformConfig(
|
||||
[HondaCarDocs("Acura RDX 2016-18", "AcuraWatch Plus or Advance Package", min_steer_speed=12. * CV.MPH_TO_MS)],
|
||||
CarSpecs(mass=3925 * CV.LB_TO_KG, wheelbase=2.68, steerRatio=15.0, centerToFrontRatio=0.38, tireStiffnessFactor=0.444), # as spec
|
||||
|
||||
@@ -118,11 +118,11 @@ class CarController(CarControllerBase):
|
||||
can_sends = []
|
||||
|
||||
# HUD messages
|
||||
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.enabled, self.car_fingerprint,
|
||||
sys_warning, sys_state, left_lane_warning, right_lane_warning = process_hud_alert(CC.latActive, self.car_fingerprint,
|
||||
hud_control)
|
||||
|
||||
can_sends.append(hyundaican.create_lkas11(self.packer, self.frame, self.CP, apply_torque, apply_steer_req,
|
||||
torque_fault, CS.lkas11, sys_warning, sys_state, CC.enabled,
|
||||
torque_fault, CS.lkas11, sys_warning, sys_state, CC.latActive,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
left_lane_warning, right_lane_warning))
|
||||
|
||||
@@ -148,7 +148,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# 20 Hz LFA MFA message
|
||||
if self.frame % 5 == 0 and self.CP.flags & HyundaiFlags.SEND_LFA.value:
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.enabled))
|
||||
can_sends.append(hyundaican.create_lfahda_mfc(self.packer, CC.latActive))
|
||||
|
||||
# 5 Hz ACC options
|
||||
if self.frame % 20 == 0 and self.CP.openpilotLongitudinalControl:
|
||||
@@ -167,7 +167,7 @@ class CarController(CarControllerBase):
|
||||
lka_steering_long = lka_steering and self.CP.openpilotLongitudinalControl
|
||||
|
||||
# steering control
|
||||
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.enabled, apply_steer_req, apply_torque))
|
||||
can_sends.extend(hyundaicanfd.create_steering_messages(self.packer, self.CP, self.CAN, CC.latActive, apply_steer_req, apply_torque))
|
||||
|
||||
# prevent LFA from activating on LKA steering cars by sending "no lane lines detected" to ADAS ECU
|
||||
if self.frame % 5 == 0 and lka_steering:
|
||||
@@ -176,7 +176,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
# LFA and HDA icons
|
||||
if self.frame % 5 == 0 and (not lka_steering or lka_steering_long):
|
||||
can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.enabled))
|
||||
can_sends.append(hyundaicanfd.create_lfahda_cluster(self.packer, self.CAN, CC.latActive))
|
||||
|
||||
# blinkers
|
||||
if lka_steering and self.CP.flags & HyundaiFlags.ENABLE_BLINKERS:
|
||||
|
||||
@@ -193,6 +193,9 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main (cruiseState.available)
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
ret.blockPcmEnable = not self.recent_button_interaction()
|
||||
|
||||
# low speed steer alert hysteresis logic (only for cars with steer cut off above 10 m/s)
|
||||
@@ -291,6 +294,13 @@ class CarState(CarStateBase):
|
||||
*create_button_events(self.main_buttons[-1], prev_main_buttons, {1: ButtonType.mainCruise}),
|
||||
*create_button_events(self.lda_button, prev_lda_button, {1: ButtonType.lkas})]
|
||||
|
||||
# dp - ALKA: direct tracking - lkas_on follows acc_main
|
||||
if not self.CP.openpilotLongitudinalControl:
|
||||
cp_cruise_info = cp_cam if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp
|
||||
self.lkas_on = cp_cruise_info.vl["SCC_CONTROL"]["MainMode_ACC"] == 1
|
||||
else:
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
ret.blockPcmEnable = not self.recent_button_interaction()
|
||||
|
||||
return ret
|
||||
|
||||
@@ -23,7 +23,7 @@ class CarInterface(CarInterfaceBase):
|
||||
RadarInterface = RadarInterface
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "hyundai"
|
||||
|
||||
# "LKA steering" if LKAS or LKAS_ALT messages are seen coming from the camera.
|
||||
@@ -152,6 +152,13 @@ class CarInterface(CarInterfaceBase):
|
||||
if candidate in (CAR.KIA_OPTIMA_H,):
|
||||
ret.dashcamOnly = True
|
||||
|
||||
# w/ SMDPS, allow steering to 0
|
||||
if 0x2AA in fingerprint[0]:
|
||||
ret.minSteerSpeed = 0.
|
||||
print("----------------------------------------------")
|
||||
print("dragonpilot: SMDPS detected!")
|
||||
print("----------------------------------------------")
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestHyundaiFingerprint:
|
||||
if lka_steering:
|
||||
cam_can = CanBus(None, fingerprint).CAM
|
||||
fingerprint[cam_can] = [0x50, 0x110] # LKA steering messages
|
||||
CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], False, False, False)
|
||||
CP = CarInterface.get_params(CAR.KIA_EV6, fingerprint, [], False, False, 0, False)
|
||||
assert bool(CP.flags & HyundaiFlags.CANFD_LKA_STEERING) == lka_steering
|
||||
|
||||
# radar available
|
||||
@@ -59,14 +59,14 @@ class TestHyundaiFingerprint:
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
if radar:
|
||||
fingerprint[1][RADAR_START_ADDR] = 8
|
||||
CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, False)
|
||||
CP = CarInterface.get_params(CAR.HYUNDAI_SONATA, fingerprint, [], False, False, 0, False)
|
||||
assert CP.radarUnavailable != radar
|
||||
|
||||
def test_alternate_limits(self):
|
||||
# Alternate lateral control limits, for high torque cars, verify Panda safety mode flag is set
|
||||
fingerprint = gen_empty_fingerprint()
|
||||
for car_model in CAR:
|
||||
CP = CarInterface.get_params(car_model, fingerprint, [], False, False, False)
|
||||
CP = CarInterface.get_params(car_model, fingerprint, [], False, False, 0, False)
|
||||
assert bool(CP.flags & HyundaiFlags.ALT_LIMITS) == bool(CP.safetyConfigs[-1].safetyParam & HyundaiSafetyFlags.ALT_LIMITS)
|
||||
|
||||
def test_can_features(self):
|
||||
|
||||
@@ -122,11 +122,11 @@ class CarInterfaceBase(ABC):
|
||||
"""
|
||||
Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints.
|
||||
"""
|
||||
return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False, False)
|
||||
return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False, 0, False)
|
||||
|
||||
@classmethod
|
||||
def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw],
|
||||
alpha_long: bool, is_release: bool, docs: bool) -> structs.CarParams:
|
||||
alpha_long: bool, is_release: bool, dp_params: int, docs: bool) -> structs.CarParams:
|
||||
ret = CarInterfaceBase.get_std_params(candidate)
|
||||
|
||||
platform = PLATFORMS[candidate]
|
||||
@@ -139,7 +139,7 @@ class CarInterfaceBase(ABC):
|
||||
ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor
|
||||
ret.flags |= int(platform.config.flags)
|
||||
|
||||
ret = cls._get_params(ret, candidate, fingerprint, car_fw, alpha_long, is_release, docs)
|
||||
ret = cls._get_params(ret, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs)
|
||||
|
||||
# Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
|
||||
if not ret.notCar:
|
||||
@@ -154,7 +154,7 @@ class CarInterfaceBase(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint: dict[int, dict[int, int]],
|
||||
car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release: bool, docs: bool) -> structs.CarParams:
|
||||
car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release: bool, dp_params: int, docs: bool) -> structs.CarParams:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@@ -279,6 +279,9 @@ class CarStateBase(ABC):
|
||||
self.cluster_min_speed = 0.0 # min speed before dropping to 0
|
||||
self.secoc_key: bytes = b"00" * 16
|
||||
|
||||
# dp - ALKA: lkas_on state (mirrors panda's lkas_on for Python-panda sync)
|
||||
self.lkas_on = False
|
||||
|
||||
Q = [[0.0, 0.0], [0.0, 100.0]]
|
||||
R = 0.3
|
||||
A = [[1.0, DT_CTRL], [0.0, 1.0]]
|
||||
|
||||
@@ -115,6 +115,9 @@ class CarState(CarStateBase):
|
||||
# TODO: add button types for inc and dec
|
||||
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "mazda"
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)]
|
||||
ret.radarUnavailable = True
|
||||
|
||||
@@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "mock"
|
||||
ret.mass = 1700.
|
||||
ret.wheelbase = 2.70
|
||||
|
||||
@@ -61,7 +61,8 @@ class CarController(CarControllerBase):
|
||||
# Below are the HUD messages. We copy the stock message and modify
|
||||
if self.CP.carFingerprint != CAR.NISSAN_ALTIMA:
|
||||
if self.frame % 2 == 0:
|
||||
can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
# dp - ALKA: use latActive to show HUD when ALKA is active
|
||||
can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.latActive, hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart))
|
||||
|
||||
if self.frame % 50 == 0:
|
||||
|
||||
@@ -128,6 +128,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.buttonEvents = create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "nissan"
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.nissan)]
|
||||
ret.autoResumeSng = False
|
||||
|
||||
@@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = 'psa'
|
||||
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.psa)]
|
||||
|
||||
162
opendbc_repo/opendbc/car/radar_interface.py
Normal file
162
opendbc_repo/opendbc/car/radar_interface.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
from opendbc.car.interfaces import RadarInterfaceBase
|
||||
from opendbc.can.parser import CANParser
|
||||
from opendbc.car.structs import RadarData
|
||||
from typing import List, Tuple
|
||||
|
||||
# car head to radar
|
||||
DREL_OFFSET = -1.52
|
||||
|
||||
|
||||
# typically max lane width is 3.7m
|
||||
LANE_WIDTH = 3.8
|
||||
LANE_WIDTH_HALF = LANE_WIDTH/2
|
||||
|
||||
LANE_CENTER_MIN_LAT = 0.
|
||||
LANE_CENTER_MAX_LAT = LANE_WIDTH_HALF
|
||||
LANE_CENTER_MIN_DIST = 5.
|
||||
|
||||
LANE_SIDE_MIN_LAT = LANE_WIDTH_HALF
|
||||
LANE_SIDE_MAX_LAT = LANE_WIDTH_HALF + LANE_WIDTH
|
||||
LANE_SIDE_MIN_DIST = 10.
|
||||
|
||||
|
||||
# lat distance, typically max lane width is 3.7m
|
||||
MAX_LAT_DIST = 6.
|
||||
|
||||
# objects to ignore thats really close to the vehicle (after DREL_OFFSET applied)
|
||||
MIN_DIST = 5.
|
||||
|
||||
# ignore oncoming objects
|
||||
IGNORE_OBJ_STATE = 2
|
||||
|
||||
# ignore objects that we haven't seen for 5 secs
|
||||
NOT_SEEN_INIT = 33
|
||||
|
||||
def _create_radar_parser():
|
||||
return CANParser('u_radar', [("Status", float('nan')), ("ObjectData", float('nan'))], 1)
|
||||
|
||||
class RadarInterface(RadarInterfaceBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
|
||||
self.updated_messages = set()
|
||||
|
||||
self.rcp = _create_radar_parser()
|
||||
|
||||
self._pts_cache = dict()
|
||||
self._pts_not_seen = {key: 0 for key in range(255)}
|
||||
self._should_clear_cache = False
|
||||
|
||||
# called by card.py, 100hz
|
||||
def update(self, can_strings):
|
||||
vls = self.rcp.update(can_strings)
|
||||
self.updated_messages.update(vls)
|
||||
|
||||
if 1546 in self.updated_messages:
|
||||
self._should_clear_cache = True
|
||||
|
||||
if 1547 in self.updated_messages:
|
||||
all_objects = zip(
|
||||
self.rcp.vl_all['ObjectData']['ID'],
|
||||
self.rcp.vl_all['ObjectData']['DistLong'],
|
||||
self.rcp.vl_all['ObjectData']['DistLat'],
|
||||
self.rcp.vl_all['ObjectData']['VRelLong'],
|
||||
self.rcp.vl_all['ObjectData']['VRelLat'],
|
||||
self.rcp.vl_all['ObjectData']['DynProp'],
|
||||
self.rcp.vl_all['ObjectData']['Class'],
|
||||
self.rcp.vl_all['ObjectData']['RCS'],
|
||||
)
|
||||
|
||||
# clean cache when we see a 0x60a then a 0x60b
|
||||
if self._should_clear_cache:
|
||||
self._pts_cache.clear()
|
||||
self._should_clear_cache = False
|
||||
|
||||
for track_id, dist_long, dist_lat, vrel_long, vrel_lat, dyn_prop, obj_class, rcs in all_objects:
|
||||
|
||||
d_rel = dist_long + DREL_OFFSET
|
||||
y_rel = -dist_lat
|
||||
|
||||
should_ignore = False
|
||||
|
||||
# ignore point (obj_class = 0)
|
||||
if not should_ignore and int(obj_class) == 0:
|
||||
should_ignore = True
|
||||
|
||||
# ignore oncoming objects
|
||||
# @todo remove this because it's always 0 ?
|
||||
if not should_ignore and int(dyn_prop) == IGNORE_OBJ_STATE:
|
||||
should_ignore = True
|
||||
|
||||
# far away lane object, ignore
|
||||
if not should_ignore and abs(y_rel) > LANE_SIDE_MAX_LAT:
|
||||
should_ignore = True
|
||||
|
||||
# close object, ignore, use vision
|
||||
if not should_ignore and LANE_CENTER_MIN_LAT > abs(y_rel) > LANE_CENTER_MAX_LAT and d_rel < LANE_CENTER_MIN_DIST:
|
||||
should_ignore = True
|
||||
|
||||
# close object, ignore, use vision
|
||||
if not should_ignore and LANE_SIDE_MIN_LAT > abs(y_rel) > LANE_SIDE_MAX_LAT and d_rel < LANE_SIDE_MIN_DIST:
|
||||
should_ignore = True
|
||||
|
||||
if not should_ignore and track_id not in self._pts_cache:
|
||||
self._pts_cache[track_id] = RadarData.RadarPoint()
|
||||
self._pts_cache[track_id].trackId = track_id
|
||||
|
||||
if should_ignore:
|
||||
self._pts_not_seen[track_id] = -1
|
||||
else:
|
||||
self._pts_not_seen[track_id] = NOT_SEEN_INIT
|
||||
|
||||
# init cache
|
||||
if track_id not in self._pts_cache:
|
||||
self._pts_cache[track_id] = RadarData.RadarPoint()
|
||||
self._pts_cache[track_id].trackId = track_id
|
||||
|
||||
# add/update to cache
|
||||
self._pts_cache[track_id].dRel = d_rel
|
||||
self._pts_cache[track_id].yRel = y_rel
|
||||
self._pts_cache[track_id].vRel = float(vrel_long)
|
||||
self._pts_cache[track_id].yvRel = float('nan')
|
||||
self._pts_cache[track_id].aRel = float('nan')
|
||||
self._pts_cache[track_id].measured = True
|
||||
|
||||
self.updated_messages.clear()
|
||||
|
||||
# publish to cereal
|
||||
if self.frame % 3 == 0:
|
||||
keys_to_remove = [key for key in self.pts if key not in self._pts_cache]
|
||||
for key in keys_to_remove:
|
||||
self._pts_not_seen[key] -= 1
|
||||
if self._pts_not_seen[key] <= 0:
|
||||
del self.pts[key]
|
||||
|
||||
self.pts.update(self._pts_cache)
|
||||
|
||||
ret = RadarData()
|
||||
if not self.rcp.can_valid:
|
||||
ret.errors.canError = True
|
||||
|
||||
ret.points = list(self.pts.values())
|
||||
return ret
|
||||
|
||||
return None
|
||||
@@ -12,7 +12,7 @@ class CarInterface(CarInterfaceBase):
|
||||
RadarInterface = RadarInterface
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "rivian"
|
||||
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.rivian)]
|
||||
|
||||
@@ -18,3 +18,14 @@ CarStateT = capnp.lib.capnp._StructModule
|
||||
RadarDataT = capnp.lib.capnp._StructModule
|
||||
CarControlT = capnp.lib.capnp._StructModule
|
||||
CarParamsT = capnp.lib.capnp._StructModule
|
||||
|
||||
class DPFlags:
|
||||
LatALKA = 1
|
||||
ExtRadar = 2
|
||||
ToyotaLockCtrl = 2 ** 2
|
||||
ToyotaTSS1SnG = 2 ** 3
|
||||
ToyotaStockLon = 2 ** 4
|
||||
VagA0SnG = 2 ** 5
|
||||
VAGPQSteeringPatch = 2 ** 6
|
||||
VagAvoidEPSLockout = 2 ** 7
|
||||
pass
|
||||
|
||||
@@ -98,7 +98,7 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(subarucan.create_es_dashstatus(self.packer, self.frame // 10, CS.es_dashstatus_msg, CC.enabled,
|
||||
self.CP.openpilotLongitudinalControl, CC.longActive, hud_control.leadVisible))
|
||||
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.enabled, hud_control.visualAlert,
|
||||
can_sends.append(subarucan.create_es_lkas_state(self.packer, self.frame // 10, CS.es_lkas_state_msg, CC.latActive, hud_control.visualAlert,
|
||||
hud_control.leftLaneVisible, hud_control.rightLaneVisible,
|
||||
hud_control.leftLaneDepart, hud_control.rightLaneDepart))
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ class CarState(CarStateBase):
|
||||
if self.CP.flags & SubaruFlags.SEND_INFOTAINMENT:
|
||||
self.es_infotainment_msg = copy.copy(cp_cam.vl["ES_Infotainment"])
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -11,7 +11,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "subaru"
|
||||
ret.radarUnavailable = True
|
||||
# for HYBRID CARS to be upstreamed, we need:
|
||||
@@ -33,6 +33,8 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.subaru)]
|
||||
if ret.flags & SubaruFlags.GLOBAL_GEN2:
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.GEN2.value
|
||||
elif ret.flags & (SubaruFlags.IMPREZA_2018 | SubaruFlags.HYBRID) :
|
||||
ret.safetyConfigs[0].safetyParam |= SubaruSafetyFlags.IMPREZA_2018.value
|
||||
|
||||
ret.steerLimitTimer = 0.4
|
||||
ret.steerActuatorDelay = 0.1
|
||||
@@ -50,11 +52,11 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]]
|
||||
|
||||
elif candidate == CAR.SUBARU_IMPREZA:
|
||||
ret.steerActuatorDelay = 0.4 # end-to-end angle controller
|
||||
ret.steerActuatorDelay = 0.1 # end-to-end angle controller
|
||||
ret.lateralTuning.init('pid')
|
||||
ret.lateralTuning.pid.kf = 0.00005
|
||||
ret.lateralTuning.pid.kf = 0.00003
|
||||
ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]]
|
||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.12, 0.18], [0.012, 0.018]]
|
||||
|
||||
elif candidate == CAR.SUBARU_IMPREZA_2020:
|
||||
ret.lateralTuning.init('pid')
|
||||
|
||||
@@ -26,6 +26,12 @@ class CarControllerParams:
|
||||
elif CP.carFingerprint == CAR.SUBARU_IMPREZA_2020:
|
||||
self.STEER_DELTA_UP = 35
|
||||
self.STEER_MAX = 1439
|
||||
self.STEER_DELTA_UP = 35
|
||||
self.STEER_DELTA_DOWN = 70
|
||||
elif CP.carFingerprint == CAR.SUBARU_IMPREZA:
|
||||
self.STEER_MAX = 3071
|
||||
self.STEER_DELTA_UP = 60
|
||||
self.STEER_DELTA_DOWN = 60
|
||||
else:
|
||||
self.STEER_MAX = 2047
|
||||
|
||||
@@ -57,7 +63,7 @@ class SubaruSafetyFlags(IntFlag):
|
||||
GEN2 = 1
|
||||
LONG = 2
|
||||
PREGLOBAL_REVERSED_DRIVER_TORQUE = 4
|
||||
|
||||
IMPREZA_2018 = 8
|
||||
|
||||
class SubaruFlags(IntFlag):
|
||||
# Detected flags
|
||||
@@ -74,6 +80,8 @@ class SubaruFlags(IntFlag):
|
||||
HYBRID = 32
|
||||
LKAS_ANGLE = 64
|
||||
|
||||
# rick
|
||||
IMPREZA_2018 = 2 ** 10
|
||||
|
||||
GLOBAL_ES_ADDR = 0x787
|
||||
GEN2_ES_BUTTONS_DID = b'\x11\x30'
|
||||
@@ -143,7 +151,8 @@ class CAR(Platforms):
|
||||
SubaruCarDocs("Subaru Crosstrek 2018-19", video="https://youtu.be/Agww7oE1k-s?t=26"),
|
||||
SubaruCarDocs("Subaru XV 2018-19", video="https://youtu.be/Agww7oE1k-s?t=26"),
|
||||
],
|
||||
CarSpecs(mass=1568, wheelbase=2.67, steerRatio=15),
|
||||
CarSpecs(mass=1568, wheelbase=2.67, steerRatio=13.5),
|
||||
flags=SubaruFlags.IMPREZA_2018,
|
||||
)
|
||||
SUBARU_IMPREZA_2020 = SubaruPlatformConfig(
|
||||
[
|
||||
|
||||
@@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "tesla"
|
||||
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.tesla)]
|
||||
|
||||
@@ -268,6 +268,7 @@ routes = [
|
||||
CarTestRoute("6719965b0e1d1737/2023-02-09--22-44-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid
|
||||
CarTestRoute("6719965b0e1d1737/2023-08-29--06-40-05", TOYOTA.TOYOTA_CHR_TSS2), # hybrid, openpilot longitudinal, radar disabled
|
||||
CarTestRoute("14623aae37e549f3/2021-10-24--01-20-49", TOYOTA.TOYOTA_PRIUS_V),
|
||||
CarTestRoute("ea8fbe72b96a185c|2023-02-22--09-20-34", TOYOTA.TOYOTA_CHR_TSS2), # openpilot longitudinal, with Radar Filter
|
||||
|
||||
CarTestRoute("202c40641158a6e5/2021-09-21--09-43-24", VOLKSWAGEN.VOLKSWAGEN_ARTEON_MK1),
|
||||
CarTestRoute("2c68dda277d887ac/2021-05-11--15-22-20", VOLKSWAGEN.VOLKSWAGEN_ATLAS_MK1),
|
||||
|
||||
@@ -52,7 +52,7 @@ def get_fuzzy_car_interface(car_name: str, draw: DrawType) -> CarInterfaceBase:
|
||||
# initialize car interface
|
||||
CarInterface = interfaces[car_name]
|
||||
car_params = CarInterface.get_params(car_name, params['fingerprints'], params['car_fw'],
|
||||
alpha_long=params['alpha_long'], is_release=False, docs=False)
|
||||
alpha_long=params['alpha_long'], is_release=False, dp_params=0, docs=False)
|
||||
return CarInterface(car_params)
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"HONDA_CRV_EU" = "HONDA_CRV"
|
||||
"HONDA_CIVIC_BOSCH_DIESEL" = "HONDA_CIVIC_BOSCH"
|
||||
"HONDA_E" = "HONDA_CIVIC_BOSCH"
|
||||
"HONDA_ODYSSEY_TWN" = "HONDA_ODYSSEY"
|
||||
|
||||
"BUICK_LACROSSE" = "CHEVROLET_VOLT"
|
||||
"BUICK_REGAL" = "CHEVROLET_VOLT"
|
||||
|
||||
@@ -2,6 +2,7 @@ import math
|
||||
import numpy as np
|
||||
from opendbc.car import Bus, make_tester_present_msg, rate_limit, structs, ACCELERATION_DUE_TO_GRAVITY, DT_CTRL
|
||||
from opendbc.car.lateral import apply_meas_steer_torque_limits, apply_std_steer_angle_limits, common_fault_avoidance
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.carlog import carlog
|
||||
from opendbc.car.common.filter_simple import FirstOrderFilter, HighPassFilter
|
||||
from opendbc.car.common.pid import PIDController
|
||||
@@ -34,6 +35,17 @@ MAX_STEER_RATE_FRAMES = 18 # tx control frames needed before torque can be cut
|
||||
# EPS allows user torque above threshold for 50 frames before permanently faulting
|
||||
MAX_USER_TORQUE = 500
|
||||
|
||||
# Lock / unlock door commands - Credit goes to AlexandreSato!
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
LOCK_SPEED = 20 * CV.KPH_TO_MS
|
||||
|
||||
LOCK_UNLOCK_CAN_ID = 0x750
|
||||
UNLOCK_CMD = b'\x40\x05\x30\x11\x00\x40\x00\x00'
|
||||
LOCK_CMD = b'\x40\x05\x30\x11\x00\x80\x00\x00'
|
||||
|
||||
from cereal import car
|
||||
PARK = car.CarState.GearShifter.park
|
||||
DRIVE = car.CarState.GearShifter.drive
|
||||
|
||||
def get_long_tune(CP, params):
|
||||
if CP.carFingerprint in TSS2_CAR:
|
||||
@@ -78,6 +90,8 @@ class CarController(CarControllerBase):
|
||||
self.secoc_acc_message_counter = 0
|
||||
self.secoc_prev_reset_counter = 0
|
||||
|
||||
self.doors_locked = False
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
actuators = CC.actuators
|
||||
stopping = actuators.longControlState == LongCtrlState.stopping
|
||||
@@ -190,6 +204,9 @@ class CarController(CarControllerBase):
|
||||
if not should_resume and CS.out.cruiseState.standstill:
|
||||
self.standstill_req = True
|
||||
|
||||
if (self.CP.flags & ToyotaFlags.TSS1_SNG.value) and CS.out.standstill and not self.last_standstill:
|
||||
self.standstill_req = False
|
||||
|
||||
self.last_standstill = CS.out.standstill
|
||||
|
||||
# handle UI messages
|
||||
@@ -261,6 +278,10 @@ class CarController(CarControllerBase):
|
||||
elif net_acceleration_request_min > 0.3:
|
||||
self.permit_braking = False
|
||||
|
||||
# rick - do not do delay compensation for non-TSS2 vehicles (e.g. car with sDSU?), assign the value back to actuators.accel
|
||||
# from Jason, see https://github.com/sunnypilot/opendbc/compare/dd2016f77f8467ca2f7934db1b8c6d73164b3df7...f90b75b1531d0ef949c1c7fb8c175059448a2a97#diff-dc03b1fc7156134429efc0cdced75bc227d0ceb8bbd0c55763022fb9db6794d9
|
||||
if not self.CP.carFingerprint in TSS2_CAR:
|
||||
pcm_accel_cmd = actuators.accel
|
||||
pcm_accel_cmd = float(np.clip(pcm_accel_cmd, self.params.ACCEL_MIN, self.params.ACCEL_MAX))
|
||||
|
||||
main_accel_cmd = 0. if self.CP.flags & ToyotaFlags.SECOC.value else pcm_accel_cmd
|
||||
@@ -301,15 +322,16 @@ class CarController(CarControllerBase):
|
||||
send_ui = True
|
||||
|
||||
if self.frame % 20 == 0 or send_ui:
|
||||
# dp - ALKA: use lat_active to show HUD when ALKA is active
|
||||
can_sends.append(toyotacan.create_ui_command(self.packer, steer_alert, pcm_cancel_cmd, hud_control.leftLaneVisible,
|
||||
hud_control.rightLaneVisible, hud_control.leftLaneDepart,
|
||||
hud_control.rightLaneDepart, CC.enabled, CS.lkas_hud))
|
||||
hud_control.rightLaneDepart, lat_active, CS.lkas_hud))
|
||||
|
||||
if (self.frame % 100 == 0 or send_ui) and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
if (self.frame % 100 == 0 or send_ui) and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value and not self.CP.flags & ToyotaFlags.RADAR_FILTER.value:
|
||||
can_sends.append(toyotacan.create_fcw_command(self.packer, fcw_alert))
|
||||
|
||||
# keep radar disabled
|
||||
if self.frame % 20 == 0 and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
if self.frame % 20 == 0 and self.CP.flags & ToyotaFlags.DISABLE_RADAR.value and not self.CP.flags & ToyotaFlags.RADAR_FILTER.value:
|
||||
can_sends.append(make_tester_present_msg(0x750, 0, 0xF))
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
@@ -318,5 +340,13 @@ class CarController(CarControllerBase):
|
||||
new_actuators.steeringAngleDeg = self.last_angle
|
||||
new_actuators.accel = self.accel
|
||||
|
||||
if self.CP.flags & ToyotaFlags.LOCK_CTRL.value:
|
||||
if not self.doors_locked and CS.out.gearShifter == DRIVE and CS.out.vEgo >= LOCK_SPEED:
|
||||
can_sends.append(CanData(LOCK_UNLOCK_CAN_ID, LOCK_CMD, 0))
|
||||
self.doors_locked = True
|
||||
elif self.doors_locked and CS.out.gearShifter == PARK:
|
||||
can_sends.append(CanData(LOCK_UNLOCK_CAN_ID, UNLOCK_CMD, 0))
|
||||
self.doors_locked = False
|
||||
|
||||
self.frame += 1
|
||||
return new_actuators, can_sends
|
||||
|
||||
@@ -53,12 +53,32 @@ class CarState(CarStateBase):
|
||||
self.gvc = 0.0
|
||||
self.secoc_synchronization = None
|
||||
|
||||
# radar filter (mainly for CHR/Camry)
|
||||
# the idea is to place a Panda in between Radar and camera/body (engine room) to block 0x343 (longitudinal)
|
||||
# depends on the firmware, we should be able to read most CAN directly from cp (not cp_cam, its empty)
|
||||
self.dp_radar_filter = bool(self.CP.flags & ToyotaFlags.RADAR_FILTER.value)
|
||||
|
||||
from opendbc.car.toyota.sdsu import SDSU
|
||||
self.sdsu = SDSU(CP.flags)
|
||||
|
||||
# rick - dsu_bypass from cydia2020: https://github.com/cydia2020/toyota-dsu-reroute-harness/
|
||||
# the idea is to "re-route" the DSU to Panda CAN2 (Which connects to ADAS Camera)
|
||||
# * when comma device is not available, the DSU message can still communicate with ADAS Camera, and over to car.
|
||||
# * when comma device is active, CAN message of DSU and ADAS camera will the be blocked by Panda, only forward some CAN messages over to car (from CAN0).
|
||||
self.dp_dsu_bypass = self.CP.flags & ToyotaFlags.DSU_BYPASS.value
|
||||
|
||||
from opendbc.car.toyota.zss import ZSS
|
||||
self.zss = ZSS(CP.flags)
|
||||
|
||||
def update(self, can_parsers) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
cp_cam = can_parsers[Bus.cam]
|
||||
|
||||
ret = structs.CarState()
|
||||
cp_acc = cp_cam if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) else cp
|
||||
if self.dp_dsu_bypass:
|
||||
cp_acc = cp_cam
|
||||
else:
|
||||
cp_acc = cp_cam if self.CP.carFingerprint in (TSS2_CAR - RADAR_ACC_CAR) else cp
|
||||
|
||||
if not self.CP.flags & ToyotaFlags.SECOC.value:
|
||||
self.gvc = cp.vl["VSC1S07"]["GVC"]
|
||||
@@ -78,7 +98,7 @@ class CarState(CarStateBase):
|
||||
else:
|
||||
ret.gasPressed = cp.vl["PCM_CRUISE"]["GAS_RELEASED"] == 0 # TODO: these also have GAS_PEDAL, come back and unify
|
||||
can_gear = int(cp.vl["GEAR_PACKET"]["GEAR"])
|
||||
if not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
if not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value or self.dp_radar_filter:
|
||||
ret.stockAeb = bool(cp_acc.vl["PRE_COLLISION"]["PRECOLLISION_ACTIVE"] and cp_acc.vl["PRE_COLLISION"]["FORCE"] < -1e-5)
|
||||
|
||||
self.parse_wheel_speeds(ret,
|
||||
@@ -117,6 +137,10 @@ class CarState(CarStateBase):
|
||||
# we could use the override bit from dbc, but it's triggered at too high torque values
|
||||
ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD
|
||||
|
||||
if self.zss.enabled:
|
||||
self.zss.set_values(can_parsers[Bus.zss])
|
||||
ret.steeringAngleDeg = self.zss.get_steering_angle_deg(cp.vl["PCM_CRUISE"]["CRUISE_ACTIVE"], ret.steeringAngleDeg)
|
||||
|
||||
# Check EPS LKA/LTA fault status
|
||||
ret.steerFaultTemporary = cp.vl["EPS_STATUS"]["LKA_STATE"] in TEMP_STEER_FAULTS
|
||||
ret.steerFaultPermanent = cp.vl["EPS_STATUS"]["LKA_STATE"] in PERM_STEER_FAULTS
|
||||
@@ -147,8 +171,10 @@ class CarState(CarStateBase):
|
||||
conversion_factor = CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS
|
||||
ret.cruiseState.speedCluster = cluster_set_speed * conversion_factor
|
||||
|
||||
if self.CP.carFingerprint in TSS2_CAR and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
|
||||
if (self.CP.carFingerprint in TSS2_CAR and not self.CP.flags & ToyotaFlags.DISABLE_RADAR.value) or \
|
||||
self.dp_dsu_bypass:
|
||||
if not (self.CP.flags & ToyotaFlags.SDSU.value):
|
||||
self.acc_type = cp_acc.vl["ACC_CONTROL"]["ACC_TYPE"]
|
||||
ret.stockFcw = bool(cp_acc.vl["PCS_HUD"]["FCW"])
|
||||
|
||||
# some TSS2 cars have low speed lockout permanently set, so ignore on those cars
|
||||
@@ -191,7 +217,15 @@ class CarState(CarStateBase):
|
||||
buttonEvents.extend(create_button_events(1, 0, {1: ButtonType.lkas}) +
|
||||
create_button_events(0, 1, {1: ButtonType.lkas}))
|
||||
|
||||
if self.CP.carFingerprint not in (RADAR_ACC_CAR | SECOC_CAR):
|
||||
if self.sdsu.enabled:
|
||||
# The follow distance button signal as forwarded by the sdsu
|
||||
self.sdsu.update_states(can_parsers[Bus.sdsu])
|
||||
prev_distance_button = self.distance_button
|
||||
self.distance_button = self.sdsu.dist_btn
|
||||
|
||||
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
elif self.CP.carFingerprint not in (RADAR_ACC_CAR | SECOC_CAR):
|
||||
# distance button is wired to the ACC module (camera or radar)
|
||||
prev_distance_button = self.distance_button
|
||||
self.distance_button = cp_acc.vl["ACC_CONTROL"]["DISTANCE"]
|
||||
@@ -199,6 +233,10 @@ class CarState(CarStateBase):
|
||||
buttonEvents += create_button_events(self.distance_button, prev_distance_button, {1: ButtonType.gapAdjustCruise})
|
||||
|
||||
ret.buttonEvents = buttonEvents
|
||||
|
||||
# dp - ALKA: Toyota requires main ON to use ACC/LKA, use main as switch
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
@@ -207,7 +245,15 @@ class CarState(CarStateBase):
|
||||
("BLINKERS_STATE", float('nan')),
|
||||
]
|
||||
|
||||
return {
|
||||
parsers = {
|
||||
Bus.pt: CANParser(DBC[CP.carFingerprint][Bus.pt], pt_messages, 0),
|
||||
Bus.cam: CANParser(DBC[CP.carFingerprint][Bus.pt], [], 2),
|
||||
}
|
||||
|
||||
if CP.flags & ToyotaFlags.SDSU:
|
||||
parsers[Bus.sdsu] = CANParser("toyota_sdsu", [("SDSU", 100)], 0)
|
||||
|
||||
if CP.flags & ToyotaFlags.ZSS:
|
||||
parsers[Bus.zss] = CANParser("toyota_zss", [("SECONDARY_STEER_ANGLE", float('nan'))], 0)
|
||||
|
||||
return parsers
|
||||
|
||||
@@ -4,7 +4,7 @@ from opendbc.car.toyota.carcontroller import CarController
|
||||
from opendbc.car.toyota.radar_interface import RadarInterface
|
||||
from opendbc.car.toyota.values import Ecu, CAR, DBC, ToyotaFlags, CarControllerParams, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, \
|
||||
MIN_ACC_SPEED, EPS_SCALE, NO_STOP_TIMER_CAR, ANGLE_CONTROL_CAR, \
|
||||
ToyotaSafetyFlags
|
||||
ToyotaSafetyFlags, UNSUPPORTED_DSU_CAR
|
||||
from opendbc.car.disable_ecu import disable_ecu
|
||||
from opendbc.car.interfaces import CarInterfaceBase
|
||||
|
||||
@@ -21,11 +21,14 @@ class CarInterface(CarInterfaceBase):
|
||||
return CarControllerParams(CP).ACCEL_MIN, CarControllerParams(CP).ACCEL_MAX
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "toyota"
|
||||
ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.toyota)]
|
||||
ret.safetyConfigs[0].safetyParam = EPS_SCALE[candidate]
|
||||
|
||||
if candidate in UNSUPPORTED_DSU_CAR:
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.UNSUPPORTED_DSU.value
|
||||
|
||||
# BRAKE_MODULE is on a different address for these cars
|
||||
if DBC[candidate][Bus.pt] == "toyota_new_mc_pt_generated":
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.ALT_BRAKE.value
|
||||
@@ -56,13 +59,32 @@ class CarInterface(CarInterfaceBase):
|
||||
if Ecu.hybrid in found_ecus:
|
||||
ret.flags |= ToyotaFlags.HYBRID.value
|
||||
|
||||
# 0x343 should not be present on bus 2 on cars other than TSS2_CAR unless we are re-routing DSU
|
||||
dsu_bypass = False
|
||||
if (0x343 in fingerprint[2] or 0x4CB in fingerprint[2]) and candidate not in TSS2_CAR:
|
||||
print("----------------------------------------------")
|
||||
print("dragonpilot: DSU_BYPASS detected!")
|
||||
print("----------------------------------------------")
|
||||
# rick - disable for now, breaks TOYOTA_AVALON_2019 model tests.
|
||||
# dsu_bypass = True
|
||||
# ret.flags |= ToyotaFlags.DSU_BYPASS.value
|
||||
|
||||
if 0x23 in fingerprint[0]:
|
||||
print("----------------------------------------------")
|
||||
print("dragonpilot: ZSS detected!")
|
||||
print("----------------------------------------------")
|
||||
ret.flags |= ToyotaFlags.ZSS.value
|
||||
|
||||
if candidate == CAR.TOYOTA_PRIUS:
|
||||
stop_and_go = True
|
||||
# Only give steer angle deadzone to for bad angle sensor prius
|
||||
for fw in car_fw:
|
||||
if fw.ecu == "eps" and not fw.fwVersion == b'8965B47060\x00\x00\x00\x00\x00\x00':
|
||||
ret.steerActuatorDelay = 0.25
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg=0.2)
|
||||
if ret.flags & ToyotaFlags.ZSS.value:
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
||||
else:
|
||||
ret.steerActuatorDelay = 0.25
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg=0.2)
|
||||
|
||||
elif candidate in (CAR.LEXUS_RX, CAR.LEXUS_RX_TSS2):
|
||||
stop_and_go = True
|
||||
@@ -112,6 +134,27 @@ class CarInterface(CarInterfaceBase):
|
||||
if alpha_long and candidate in RADAR_ACC_CAR:
|
||||
ret.flags |= ToyotaFlags.DISABLE_RADAR.value
|
||||
|
||||
# RADAR_ACC_CAR = CHR TSS2 / RAV4 TSS2
|
||||
# NO_DSU_CAR = CAMRY / CHR
|
||||
if 0x2FF in fingerprint[0] or 0x2AA in fingerprint[0]:
|
||||
print("----------------------------------------------")
|
||||
print("dragonpilot: RADAR_FILTER detected!")
|
||||
print("----------------------------------------------")
|
||||
ret.alphaLongitudinalAvailable = False
|
||||
ret.flags |= ToyotaFlags.RADAR_FILTER.value | ToyotaFlags.DISABLE_RADAR.value
|
||||
|
||||
sdsu_active = False
|
||||
if not (candidate in (RADAR_ACC_CAR | NO_DSU_CAR)) and 0x2FF in fingerprint[0]:
|
||||
print("----------------------------------------------")
|
||||
print("dragonpilot: SDSU detected!")
|
||||
print("----------------------------------------------")
|
||||
|
||||
sdsu_active = True
|
||||
stop_and_go = True
|
||||
|
||||
ret.flags |= ToyotaFlags.SDSU.value
|
||||
ret.alphaLongitudinalAvailable = False
|
||||
|
||||
# openpilot longitudinal enabled by default:
|
||||
# - cars w/ DSU disconnected
|
||||
# - TSS2 cars with camera sending ACC_CONTROL where we can block it
|
||||
@@ -119,7 +162,13 @@ class CarInterface(CarInterfaceBase):
|
||||
# - TSS2 radar ACC cars (disables radar)
|
||||
|
||||
ret.openpilotLongitudinalControl = (candidate in (TSS2_CAR - RADAR_ACC_CAR) or
|
||||
bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value))
|
||||
bool(ret.flags & ToyotaFlags.DISABLE_RADAR.value) or \
|
||||
dsu_bypass or \
|
||||
sdsu_active)
|
||||
|
||||
if dp_params & structs.DPFlags.ToyotaStockLon:
|
||||
ret.openpilotLongitudinalControl = False
|
||||
ret.alphaLongitudinalAvailable = False
|
||||
|
||||
ret.autoResumeSng = ret.openpilotLongitudinalControl and candidate in NO_STOP_TIMER_CAR
|
||||
|
||||
@@ -141,12 +190,19 @@ class CarInterface(CarInterfaceBase):
|
||||
if ret.flags & ToyotaFlags.HYBRID.value:
|
||||
ret.longitudinalActuatorDelay = 0.05
|
||||
|
||||
if dp_params & structs.DPFlags.ToyotaLockCtrl:
|
||||
ret.flags |= ToyotaFlags.LOCK_CTRL.value
|
||||
ret.safetyConfigs[0].safetyParam |= ToyotaSafetyFlags.LOCK_CTRL.value
|
||||
|
||||
if dp_params & structs.DPFlags.ToyotaTSS1SnG:
|
||||
ret.flags |= ToyotaFlags.TSS1_SNG.value
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def init(CP, can_recv, can_send, communication_control=None):
|
||||
# disable radar if alpha longitudinal toggled on radar-ACC car
|
||||
if CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
if not CP.flags & ToyotaFlags.RADAR_FILTER.value and CP.flags & ToyotaFlags.DISABLE_RADAR.value:
|
||||
if communication_control is None:
|
||||
communication_control = bytes([uds.SERVICE_TYPE.COMMUNICATION_CONTROL, uds.CONTROL_TYPE.ENABLE_RX_DISABLE_TX, uds.MESSAGE_TYPE.NORMAL])
|
||||
disable_ecu(can_recv, can_send, bus=0, addr=0x750, sub_addr=0xf, com_cont_req=communication_control)
|
||||
|
||||
18
opendbc_repo/opendbc/car/toyota/sdsu.py
Normal file
18
opendbc_repo/opendbc/car/toyota/sdsu.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
from opendbc.can.parser import CANParser
|
||||
|
||||
class SDSU:
|
||||
def __init__(self, flags: int):
|
||||
self.enabled = flags & ToyotaFlags.SDSU.value
|
||||
self.dist_btn = 0
|
||||
|
||||
def update_states(self, cp: CANParser):
|
||||
self.dist_btn = cp.vl["SDSU"]["FD_BUTTON"]
|
||||
@@ -56,11 +56,15 @@ class ToyotaSafetyFlags(IntFlag):
|
||||
STOCK_LONGITUDINAL = (2 << 8)
|
||||
LTA = (4 << 8)
|
||||
SECOC = (8 << 8)
|
||||
UNSUPPORTED_DSU = (16 << 8) # dp - use DSU_CRUISE (0x365) for ACC main instead of PCM_CRUISE_2 (0x1D3)
|
||||
LOCK_CTRL = (32 << 8)
|
||||
|
||||
|
||||
class ToyotaFlags(IntFlag):
|
||||
# Detected flags
|
||||
HYBRID = 1
|
||||
# use legacy id
|
||||
SDSU = 2
|
||||
DISABLE_RADAR = 4
|
||||
|
||||
# Static flags
|
||||
@@ -79,6 +83,11 @@ class ToyotaFlags(IntFlag):
|
||||
# these cars are speculated to allow stop and go when the DSU is unplugged
|
||||
SNG_WITHOUT_DSU_DEPRECATED = 512
|
||||
|
||||
LOCK_CTRL = 2 ** 13
|
||||
TSS1_SNG = 2 ** 14
|
||||
RADAR_FILTER = 2 ** 15
|
||||
DSU_BYPASS = 2 ** 16
|
||||
ZSS = 2 ** 17
|
||||
|
||||
def dbc_dict(pt, radar):
|
||||
return {Bus.pt: pt, Bus.radar: radar}
|
||||
|
||||
81
opendbc_repo/opendbc/car/toyota/zss.py
Normal file
81
opendbc_repo/opendbc/car/toyota/zss.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from opendbc.car.toyota.values import ToyotaFlags
|
||||
from opendbc.can.parser import CANParser
|
||||
|
||||
ANGLE_DIFF_THRESHOLD = 4.0
|
||||
THRESHOLD_COUNT = 10
|
||||
|
||||
class ZSS:
|
||||
|
||||
def __init__(self, flags: int):
|
||||
self.enabled = flags & ToyotaFlags.ZSS.value
|
||||
# self._alka_enabled = flags & ToyotaFlags.ALKA.value
|
||||
|
||||
self._offset_compute_once = True
|
||||
# self._alka_active_prev = False
|
||||
self._cruise_active_prev = False
|
||||
self._offset = 0.
|
||||
self._threshold_count = 0
|
||||
self._zss_value = 0.
|
||||
self._print_allow = True
|
||||
|
||||
def set_values(self, cp: CANParser):
|
||||
self._zss_value = cp.vl["SECONDARY_STEER_ANGLE"]["ZORRO_STEER"]
|
||||
|
||||
def get_enabled(self):
|
||||
return self.enabled
|
||||
|
||||
def get_steering_angle_deg(self, cruise_active, stock_steering_angle_deg):
|
||||
# off, fall back to stock
|
||||
if not self.enabled:
|
||||
return stock_steering_angle_deg
|
||||
|
||||
# when lka control is off, use stock
|
||||
# alka_active = self._is_alka_active(main_on)
|
||||
if not cruise_active: # and not alka_active:
|
||||
return stock_steering_angle_deg
|
||||
|
||||
# lka just activated
|
||||
# if not self._offset_compute_once and ((alka_active and not self._alka_active_prev) or (cruise_active and not self._cruise_active_prev)):
|
||||
if not self._offset_compute_once and (cruise_active and not self._cruise_active_prev):
|
||||
self._threshold_count = 0
|
||||
self._offset_compute_once = True
|
||||
self._print_allow = True
|
||||
|
||||
# self._alka_active_prev = alka_active
|
||||
self._cruise_active_prev = cruise_active
|
||||
|
||||
# compute offset when required
|
||||
if self._offset_compute_once:
|
||||
self._offset_compute_once = not self._compute_offset(stock_steering_angle_deg)
|
||||
|
||||
# error checking
|
||||
if self._threshold_count >= THRESHOLD_COUNT:
|
||||
if self._print_allow:
|
||||
print("ZSS: Too many large diff, fallback to stock.")
|
||||
self._print_allow = False
|
||||
return stock_steering_angle_deg
|
||||
|
||||
if self._offset_compute_once:
|
||||
print("ZSS: Compute offset required, fallback to stock.")
|
||||
return stock_steering_angle_deg
|
||||
|
||||
zss_steering_angle_deg = self._zss_value - self._offset
|
||||
angle_diff = abs(stock_steering_angle_deg - zss_steering_angle_deg)
|
||||
if angle_diff > ANGLE_DIFF_THRESHOLD:
|
||||
print(f"ZSS: Diff too large ({angle_diff}), fallback to stock. ")
|
||||
# if self._is_alka_active(main_on) or cruise_active:
|
||||
if cruise_active:
|
||||
self._threshold_count += 1
|
||||
return stock_steering_angle_deg
|
||||
|
||||
return zss_steering_angle_deg
|
||||
|
||||
# def _is_alka_active(self, main_on):
|
||||
# return self._alka_enabled and main_on != 0
|
||||
|
||||
def _compute_offset(self, steering_angle_deg):
|
||||
if abs(steering_angle_deg) > 1e-3 and abs(self._zss_value) > 1e-3:
|
||||
self._offset = self._zss_value - steering_angle_deg
|
||||
print(f"ZSS: offset computed: {self._offset}")
|
||||
return True
|
||||
return False
|
||||
@@ -31,6 +31,9 @@ class CarController(CarControllerBase):
|
||||
self.eps_timer_soft_disable_alert = False
|
||||
self.hca_frame_timer_running = 0
|
||||
self.hca_frame_same_torque = 0
|
||||
self._dp_vag_a0_sng = bool(self.CP.flags & VolkswagenFlags.A0SnG)
|
||||
self.dp_vag_pq_steering_patch = 7 if CP.flags & VolkswagenFlags.PQSteeringPatch else 5
|
||||
self.dp_avoid_eps_lockout = CP.flags & VolkswagenFlags.AVOID_EPS_LOCKOUT
|
||||
|
||||
def update(self, CC, CS, now_nanos):
|
||||
actuators = CC.actuators
|
||||
@@ -50,7 +53,13 @@ class CarController(CarControllerBase):
|
||||
# of HCA disabled; this is done whenever output happens to be zero.
|
||||
|
||||
if CC.latActive:
|
||||
new_torque = int(round(actuators.torque * self.CCP.STEER_MAX))
|
||||
if self.dp_avoid_eps_lockout:
|
||||
#根據速度縮放new_torque扭力上限
|
||||
torque_scale = np.interp(CS.out.vEgo, [0.4, 3.5, 4.0], [0.8, 0.95, 1.0])
|
||||
scaled_steer_max = self.CCP.STEER_MAX * torque_scale
|
||||
new_torque = int(round(actuators.torque * scaled_steer_max))
|
||||
else:
|
||||
new_torque = int(round(actuators.torque * self.CCP.STEER_MAX))
|
||||
apply_torque = apply_driver_steer_torque_limits(new_torque, self.apply_torque_last, CS.out.steeringTorque, self.CCP)
|
||||
self.hca_frame_timer_running += self.CCP.STEER_STEP
|
||||
if self.apply_torque_last == apply_torque:
|
||||
@@ -70,7 +79,7 @@ class CarController(CarControllerBase):
|
||||
|
||||
self.eps_timer_soft_disable_alert = self.hca_frame_timer_running > self.CCP.STEER_TIME_ALERT / DT_CTRL
|
||||
self.apply_torque_last = apply_torque
|
||||
can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled))
|
||||
can_sends.append(self.CCS.create_steering_control(self.packer_pt, self.CAN.pt, apply_torque, hca_enabled, self.dp_vag_pq_steering_patch))
|
||||
|
||||
if self.CP.flags & VolkswagenFlags.STOCK_HCA_PRESENT:
|
||||
# Pacify VW Emergency Assist driver inactivity detection by changing its view of driver steering input torque
|
||||
@@ -119,11 +128,22 @@ class CarController(CarControllerBase):
|
||||
lead_distance, hud_control.leadDistanceBars))
|
||||
|
||||
# **** Stock ACC Button Controls **************************************** #
|
||||
if self._dp_vag_a0_sng:
|
||||
if self.CP.pcmCruise and CS.gra_stock_values["COUNTER"] != self.gra_acc_counter_last:
|
||||
standing_resume_spam = CS.out.standstill
|
||||
spam_window = self.frame % 50 < 15
|
||||
|
||||
gra_send_ready = self.CP.pcmCruise and CS.gra_stock_values["COUNTER"] != self.gra_acc_counter_last
|
||||
if gra_send_ready and (CC.cruiseControl.cancel or CC.cruiseControl.resume):
|
||||
can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, self.CAN.ext, CS.gra_stock_values,
|
||||
cancel=CC.cruiseControl.cancel, resume=CC.cruiseControl.resume))
|
||||
send_cancel = CC.cruiseControl.cancel
|
||||
send_resume = CC.cruiseControl.resume or (standing_resume_spam and spam_window)
|
||||
|
||||
if send_cancel or send_resume:
|
||||
can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, self.CAN.ext, CS.gra_stock_values,
|
||||
cancel=send_cancel, resume=send_resume))
|
||||
else:
|
||||
gra_send_ready = self.CP.pcmCruise and CS.gra_stock_values["COUNTER"] != self.gra_acc_counter_last
|
||||
if gra_send_ready and (CC.cruiseControl.cancel or CC.cruiseControl.resume):
|
||||
can_sends.append(self.CCS.create_acc_buttons_control(self.packer_pt, self.CAN.ext, CS.gra_stock_values,
|
||||
cancel=CC.cruiseControl.cancel, resume=CC.cruiseControl.resume))
|
||||
|
||||
new_actuators = actuators.as_builder()
|
||||
new_actuators.torque = self.apply_torque_last / self.CCP.STEER_MAX
|
||||
|
||||
@@ -136,6 +136,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo)
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
self.frame += 1
|
||||
return ret
|
||||
|
||||
@@ -227,6 +230,9 @@ class CarState(CarStateBase):
|
||||
|
||||
ret.lowSpeedAlert = self.update_low_speed_alert(ret.vEgo)
|
||||
|
||||
# dp - ALKA: use ACC main state
|
||||
self.lkas_on = ret.cruiseState.available
|
||||
|
||||
self.frame += 1
|
||||
return ret
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class CarInterface(CarInterfaceBase):
|
||||
CarController = CarController
|
||||
|
||||
@staticmethod
|
||||
def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, docs) -> structs.CarParams:
|
||||
def _get_params(ret: structs.CarParams, candidate: CAR, fingerprint, car_fw, alpha_long, is_release, dp_params, docs) -> structs.CarParams:
|
||||
ret.brand = "volkswagen"
|
||||
ret.radarUnavailable = True
|
||||
|
||||
@@ -35,7 +35,7 @@ class CarInterface(CarInterfaceBase):
|
||||
# It is documented in a four-part blog series:
|
||||
# https://blog.willemmelching.nl/carhacking/2022/01/02/vw-part1/
|
||||
# Panda ALLOW_DEBUG firmware required.
|
||||
ret.dashcamOnly = True
|
||||
# ret.dashcamOnly = True
|
||||
|
||||
elif ret.flags & VolkswagenFlags.MLB:
|
||||
# Set global MLB parameters
|
||||
@@ -106,4 +106,13 @@ class CarInterface(CarInterfaceBase):
|
||||
safety_configs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
|
||||
ret.safetyConfigs = safety_configs
|
||||
|
||||
if dp_params & structs.DPFlags.VagA0SnG:
|
||||
ret.flags |= VolkswagenFlags.A0SnG.value
|
||||
|
||||
if ret.flags & VolkswagenFlags.PQ and dp_params & structs.DPFlags.VAGPQSteeringPatch:
|
||||
ret.flags |= VolkswagenFlags.PQSteeringPatch.value
|
||||
|
||||
if dp_params & structs.DPFlags.VagAvoidEPSLockout:
|
||||
ret.flags |= VolkswagenFlags.AVOID_EPS_LOCKOUT.value
|
||||
|
||||
return ret
|
||||
|
||||
@@ -2,7 +2,7 @@ from opendbc.car.volkswagen.mqbcan import (volkswagen_mqb_meb_checksum, xor_chec
|
||||
create_lka_hud_control as mqb_create_lka_hud_control)
|
||||
|
||||
# TODO: Parameterize the hca control type (5 vs 7) and consolidate with MQB (and PQ?)
|
||||
def create_steering_control(packer, bus, apply_steer, lkas_enabled):
|
||||
def create_steering_control(packer, bus, apply_steer, lkas_enabled, dp_vag_pq_steering_patch):
|
||||
values = {
|
||||
"HCA_01_Status_HCA": 7 if lkas_enabled else 3,
|
||||
"HCA_01_LM_Offset": abs(apply_steer),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user