Project identity and attribution

Document StarPilot setup and third-party attribution. Keep cloud authentication scoped to the selected provider.

Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
This commit is contained in:
firestar5683
2026-09-26 16:21:14 -05:00
parent 4fe9166f0f
commit 1f9345818f
18 changed files with 808 additions and 192 deletions
+27
View File
@@ -0,0 +1,27 @@
# Source provenance
## Jetlink external inference
The comma-side Jetlink client and USB transport in `jetlink_repo` are by Zeph Leggett and the [Jetlink contributors](https://github.com/zoompilot/jetlink). They are vendored from revision `c17cd5c52be0ee83b9a6265b7da69d24ad6c5d02` under the MIT license, retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE). StarPilot owns the adapter to its model runner, settings and process lifecycle. Companion applications and inference servers remain distributed by Jetlink.
## MRR35 Hyundai angle and radar
The four MRR35 angle platforms (2026 Genesis GV70, second-generation electrified GV70, 2025 Genesis GV80, and Hyundai Ioniq 9) adapt Hyundai CAN FD angle-control design and the MRR35 radar DBC generator from the locally frozen StarPilot source snapshot `678af783`. That snapshot traces its Hyundai extension work to sunnypilot/opendbc `hkg-angle-steering-2025` at `cc4b08625a98e94b318cab15e45e05dad58042bd`; its historical import revision is not established. The relevant upstream contributors include Haibin (Jason) Wen, Shane Smiskol, and DevTekVE. Their authorship remains attributable to the upstream history; they do not maintain or endorse this port.
The copied MRR35 generator source and DBC definition are at `opendbc_repo/opendbc/dbc/generator/hyundai/hyundai_mrr35_radar.py` and `.dbc`. The host angle adapter and native safety profile are adaptations, not copies of a generic DBC or of unrelated vehicle support. Applicable upstream notices are preserved in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## Ford support adapted from BluePilot
The Ford lateral strategies, extended CAN constructors, camera-radar filtering and native checks are adapted from StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. Their lineage includes BluePilot's `bp-7.0` Ford work, principally by Alan Polk, with contributions from John Christman, Jacob Neulight, tonesto7, Haibin Wen and other sunnypilot and BluePilot contributors. The reference snapshot is [`e1d051d7ba270261b4455068bd68f1a58db15a4a`](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a); the exact checkout used for the earlier StarPilot import was not recorded.
Relevant lineage includes the controller and integration work in [`db2bdff05`](https://github.com/BluePilotDev/bluepilot/commit/db2bdff05df103d71df62f45c2a3cb5211aba6e6) and [`d0aac605f`](https://github.com/BluePilotDev/bluepilot/commit/d0aac605f99d37e9da205e419f7989c1e9eaa386), extended CAN and safety work in [`8f8d6d15f`](https://github.com/BluePilotDev/bluepilot/commit/8f8d6d15f0a590f42b78de964ffb0d0af7f5d63d), and manual-turn detection in [`97867c1eb`](https://github.com/BluePilotDev/bluepilot/commit/97867c1eb57b7472f6fc3de62f0fef576e5a5497).
The vehicle strategy and CAN/native owners preserve that attribution while adapting to the current controller interfaces and shared safety limits. Upstream contributors do not maintain or endorse this adaptation. Applicable notices are retained in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## Optional Ford angle controller
The vendored `opendbc_repo/opendbc/bluepilot_lateral` angle core, codecs and Ford-only safety hooks are adapted directly from [BluePilot bp-7.0 e1d051d7ba270261b4455068bd68f1a58db15a4a](https://github.com/BluePilotDev/bluepilot/commit/e1d051d7ba270261b4455068bd68f1a58db15a4a), principally by Alan Polk, with John Christman, Jacob Neulight, Nathan Ingraham, Praeuner and other BluePilot, sunnypilot and comma contributors. Exact source paths and intentionally stricter safety checks are recorded in that folder. Both upstream `LICENSE` and `LICENSE.md` notices are retained. Its layout follows Alan Polk's September 2026 library proposal; this is a StarPilot adapter, not a published BluePilot library release.
## RAV4 Continental ARS510 receiver
The optional ACC-target and fused-track receivers and DBC preserve firestar5683's Dom integration from `b220a37f4bd119e402225a7aa16a7cd84878d0af` (Sea Turtles, 2026-10-13) and its ARS510 source attribution to ds-sebastian/ars510-radar `fb5b29c2b5999f34afeb6f7ac494a73949d66c67`, copyright 2026 ARS510 radar contributors. The complete MIT permission and warranty notice is retained in THIRD_PARTY_NOTICES.md. Startup/settings ownership is adapted to the current StarPilot architecture; no original firmware binaries or safety permissions are copied.
+28 -105
View File
@@ -1,120 +1,43 @@
<div align="center" style="text-align: center;">
# StarPilot
<h1>openpilot</h1>
StarPilot is an independent fork of [openpilot](https://github.com/commaai/openpilot), with custom driving controls, vehicle integrations, raylib interfaces for comma 3 and comma four, and the Galaxy companion interface.
<p>
<b>openpilot is an operating system for robotics.</b>
<br>
Currently, it upgrades the driver assistance system in 300+ supported cars.
</p>
**Domathon is the development branch for StarPilot 7.0.** It rebuilds StarPilot on a current openpilot foundation. Feature migration and vehicle qualification are still in progress; this branch is not a completed replacement for the existing Dom release.
<h3>
<a href="https://docs.comma.ai">Docs</a>
<span> · </span>
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
<span> · </span>
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
<span> · </span>
<a href="https://discord.comma.ai">Community</a>
<span> · </span>
<a href="https://comma.ai/shop">Try it on a comma four</a>
</h3>
## Explore the project
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
- [Developer commands](tools/STARPILOT_DEVELOPMENT.md): host tools, replay, and device builds.
- [Driving interfaces](openpilot/starpilot/ui/README.md) and [Galaxy](openpilot/starpilot/galaxy/README.md): native and companion controls.
- [Driving models](openpilot/starpilot/models/README.md): model management, artifact compatibility, and Model Laboratory status.
- [Builds and developer tools](docs/how-to/laptop-device-build.md): local development and device builds.
- [Dependency maintenance](tools/vendor/README.md): tracked source folders and upstream sync records.
[![openpilot tests](https://github.com/commaai/openpilot/actions/workflows/tests.yaml/badge.svg)](https://github.com/commaai/openpilot/actions/workflows/tests.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![X Follow](https://img.shields.io/twitter/follow/comma_ai)](https://x.com/comma_ai)
[![Discord](https://img.shields.io/discord/469524606043160576)](https://discord.comma.ai)
## Development
</div>
Dependencies are included as ordinary source folders. No recursive submodule checkout is needed. [upstream-sync.json](upstream-sync.json) records the openpilot baseline and the source revisions used for vendored dependencies.
<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>
The familiar development entry points are retained:
| Command | Purpose |
| --- | --- |
| `./build` | Build the device software using the matching AGNOS environment |
| `./build --panda` | Build the supported Panda firmware targets |
| `./c3` | Open the comma 3 interface on a development host |
| `./c4` | Open the comma four interface on a development host |
| `./onroad` | Open the onroad development view |
| `./dev` | Use the isolated host development environment |
| `./test` | Run local pre-push regression checks without changing device artifacts |
Using openpilot in a car
------
See the [build guide](docs/how-to/laptop-device-build.md) for setup, prerequisites, and target-specific behavior. The [UI guide](openpilot/starpilot/ui/README.md) describes desktop previews and their limits.
To use openpilot in a car, you need four things:
1. **Supported Device:** a comma four, available at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
2. **Software:** The setup procedure for the comma four allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
3. **Supported Car:** Ensure that you have one of [the 300+ supported cars](docs/CARS.md).
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma four to your car.
## Testing and vehicle support
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.
StarPilot retains upstream vehicle and Panda safety interfaces and adds regression coverage for its custom behavior. The [safety workflow](.github/workflows/safety.yaml) and feature tests cover distinct parts of the system. A passing interface test, model comparison, or bench run does not establish driving support for every configuration.
Support depends on the vehicle configuration and enabled feature. An existing vehicle identifier or settings page does not by itself establish driving support. Model Laboratory currently exposes management controls, but paired driving inference remains unavailable pending integration and Chestnut validation.
### Branches
## Upstream and attribution
Running `master` and other branches directly is supported, but it's recommended to run one of the following prebuilt branches:
The current openpilot baseline is [`78dccf0b482ea1298fd1f6fa59b0554187b02ce6`](https://github.com/commaai/openpilot/commit/78dccf0b482ea1298fd1f6fa59b0554187b02ce6). Its history remains intact, followed by the StarPilot foundation and feature commits. Upstream development continues independently; the baseline changes only after an update has been integrated and checked.
| 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. |
For [chestnut](https://comma.ai/shop/chestnut), use the following installer URLs:
| branch | URL | description |
|------------------------------|------------------------------------------------------------|-------------------------------------------------------------------------------------|
| `release-chestnut` | installer.comma.ai/commaai/release-chestnut | This is openpilot's release branch. |
| `release-chestnut-staging` | installer.comma.ai/commaai/release-chestnut-staging | This is the staging branch for releases. Use it to get new releases slightly early. |
| `nightly-chestnut` | installer.comma.ai/commaai/nightly-chestnut | This is the bleeding edge development branch. Do not expect this to be stable. |
| `nightly-chestnut-dev` | installer.comma.ai/commaai/nightly-chestnut-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](openpilot/tools/)
* Code documentation lives at https://docs.comma.ai
* Information about running openpilot lives on the [community wiki](https://github.com/commaai/openpilot/wiki)
Want to get paid to work on openpilot? [comma is hiring](https://comma.ai/jobs#open-positions) and offers lots of [bounties](https://comma.ai/bounties) for external contributors.
Safety and Testing
----
* openpilot observes [ISO26262](https://en.wikipedia.org/wiki/ISO_26262) guidelines, see [SAFETY.md](docs/SAFETY.md) for more details.
* openpilot has software-in-the-loop [tests](.github/workflows/tests.yaml) that run on every commit.
* The code enforcing the safety model lives in panda and is written in C, see [code rigor](https://github.com/commaai/panda#code-rigor) for more details.
* panda has software-in-the-loop [safety tests](https://github.com/commaai/panda/tree/master/tests/safety).
* Internally, we have a hardware-in-the-loop Jenkins test suite that builds and unit tests the various processes.
* panda has additional hardware-in-the-loop [tests](https://github.com/commaai/panda/blob/master/Jenkinsfile).
* We run the latest openpilot in a testing closet containing 10 comma devices continuously replaying routes.
<details>
<summary>MIT Licensed</summary>
openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified.
Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user.
**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT.
YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS.
NO WARRANTY EXPRESSED OR IMPLIED.**
</details>
<details>
<summary>User Data and comma Account</summary>
By default, openpilot uploads driving data to our servers. You can also access your data through [comma connect](https://connect.comma.ai/). We use your data to train better models and improve openpilot for everyone.
openpilot is open source software, and users can disable data collection if they wish.
openpilot logs the road-facing cameras, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs.
The driver-facing camera and microphone are only logged if you explicitly opt-in in settings.
By using openpilot, you agree to [our Privacy Policy](https://comma.ai/privacy). You understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data.
</details>
See [LICENSE](LICENSE), [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and [CREDITS.md](CREDITS.md) for licensing and source attribution. Component licenses remain with their source folders. StarPilot is maintained independently of comma.
+11 -1
View File
@@ -1,3 +1,13 @@
# StarPilot 7.0 — Domathon development
The openpilot rebase is in progress. Domathon is not yet a completed StarPilot release, and feature or vehicle presence alone does not establish full parity with Dom.
See the [project overview](README.md) for the current development entry points and the [history guide](docs/DEVELOPMENT_HISTORY.md) for the upstream boundary and feature organization. Release-specific support and validation will be recorded when a release is prepared.
# Upstream openpilot release history
The entries below are retained upstream release notes. They describe openpilot releases, not completed StarPilot 7.0 milestones.
Version 0.11.2 (2026-08-12)
=======================
* New driving model
@@ -779,7 +789,7 @@ Version 0.5.10 (2019-03-19)
* New Driver Monitoring Model
* Support QR codes for login using comma connect
* Refactor comma pedal FW and use CRC-8 checksum algorithm for safety. Reflashing pedal is required.
Please see `#hw-pedal` on [discord](discord.comma.ai) for assistance updating comma pedal.
Please see `#hw-pedal` on [discord](https://discord.comma.ai) for assistance updating comma pedal.
* Additional speed limit rules for Germany thanks to arne182
* Allow negative speed limit offsets
+114
View File
@@ -0,0 +1,114 @@
# Third-party notices
## Jetlink
Jetlink's comma client is copyright (c) 2026 Zeph Leggett and distributed under the MIT License. Its complete license and permission notice are retained in [jetlink_repo/LICENSE](jetlink_repo/LICENSE); source and revision are recorded in [CREDITS.md](CREDITS.md#jetlink-external-inference) and `upstream-sync.json`.
## MRR35 Hyundai angle and radar
The MRR35 radar generator and related Hyundai angle integration derive from the frozen StarPilot snapshot `678af783`, with relevant sunnypilot/opendbc angle-branch ancestry at `cc4b08625a98e94b318cab15e45e05dad58042bd`. Both upstream `LICENSE` and `LICENSE.md` were published for that source. Their scope is not unambiguous, so both applicable notices and the extension-file attribution are retained here without choosing between them. See [CREDITS.md](CREDITS.md) for the narrow source map.
### Upstream `opendbc` `LICENSE` notice (MIT)
Copyright (c) 2020, 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.
### Upstream extension file notice
> Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
>
> This file is part of sunnypilot and is licensed under the MIT License.
> See the LICENSE.md file in the root directory for more details.
### Upstream `LICENSE.md` notice (published as “Custom MIT License”)
The following notice is reproduced verbatim from the reference branch:
> # Custom MIT License
>
> Copyright (c) 2024, Haibin Wen, SUNNYPILOT LLC
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to view and modify the Software, subject to the following conditions:
>
> 1. **Permission Required**: Permission Required for Commercial, For-Profit, or Closed Source Use: Use of the Software, in whole or in part, for any commercial purposes, for-profit projects, or in closed source projects requires explicit written permission from the original author(s).
>
> 2. **Redistribution**: Any redistribution of the Software, modified or unmodified, must retain this license notice and the following acknowledgment:
> "This software is licensed under a custom license requiring permission for use."
>
> 3. **Visibility**: Any project that uses the Software must visibly mention the following acknowledgment:
> "This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use."
>
> 4. **No Warranty**: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
>
> Contact sunnypilot Support <support@sunnypilot.ai> for permission requests.
>
> ---
>
> Haibin Wen, SUNNYPILOT LLC
Required upstream acknowledgments:
> This software is licensed under a custom license requiring permission for use.
>
> This project uses software from Haibin Wen and SUNNYPILOT LLC and is licensed under a custom license requiring permission for use.
For commercial, for-profit, or closed-source use, consult the upstream notice and obtain any
permission it requires. The upstream repository's simultaneous publication of two differently
scoped notices should be clarified with the relevant copyright holders before relying on one to
the exclusion of the other.
## BluePilot Ford support
The Ford lateral strategies, CAN constructors, camera-radar filtering and native checks include work adapted from BluePilot's `bp-7.0` source, through StarPilot `2efe6017bfe14cee4807833e05f9a7f6ba255cdb`. See [CREDITS.md](CREDITS.md#ford-support-adapted-from-bluepilot) for contributors and source revisions. The upstream extension-file and `LICENSE.md` notices reproduced above also apply to the Ford extension sources; they are retained without resolving the difference between their published license descriptions.
## Optional BluePilot angle library adapter
The angle-only adapter in `opendbc_repo/opendbc/bluepilot_lateral` is sourced directly from BluePilot bp-7.0 commit e1d051d7ba270261b4455068bd68f1a58db15a4a. The complete upstream MIT notice is retained in that folder's `LICENSE`, and the simultaneously published Custom MIT notice is retained verbatim in `LICENSE.md`. The applicable SUNNYPILOT acknowledgments reproduced above are also retained in its `CREDITS.md`. This preserves the published terms without resolving their scope or representing the adapted extension as unambiguously plain MIT.
The optional, default-disabled angle lane-centering trim additionally derives from
BluePilot bp-dev e22afa6be9b881fa784c92ebb316db47728a3d81. Its exact source and
contributors are recorded in the library CREDITS.md; the same root/opendbc MIT
and Custom MIT notices coexist unchanged at that revision. This adds attribution,
not a license-scope decision or permission grant.
## Continental ARS510 radar
The optional RAV4 radar decoders and `toyota_tss2_ars510.dbc` are adapted from
<https://github.com/ds-sebastian/ars510-radar> at
`fb5b29c2b5999f34afeb6f7ac494a73949d66c67`.
MIT License
Copyright (c) 2026 ARS510 radar 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, 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.
+19 -59
View File
@@ -1,73 +1,33 @@
# How to contribute
# Contributing to StarPilot
Our software is open source so you can solve your own problems without needing help from others. And if you solve a problem and are so kind, you can upstream it for the rest of the world to use. Check out our [post about externalization](https://blog.comma.ai/a-2020-theme-externalization/).
Start with the [project overview](../README.md), [build guide](how-to/laptop-device-build.md), and [developer commands](../tools/STARPILOT_DEVELOPMENT.md).
Development is coordinated through [Discord](https://discord.comma.ai) and GitHub.
## Scope and behavior
### Getting Started
Keep each change focused on one feature or defect. Describe the behavior before and after the change, the affected vehicle configurations or interfaces, and how it was verified. Preserve intentional StarPilot behavior and familiar controls unless the change explicitly proposes a different experience.
* Set up your [development environment](/tools/)
* Join our [Discord](https://discord.comma.ai)
* Docs are at https://docs.comma.ai and https://blog.comma.ai
Prefer small integration points around upstream interfaces. Keep feature settings, state, and side effects with their owning feature. Shared code should express a real common contract, with regression coverage for the features that use it. Comments should explain an invariant, protocol requirement, or non-obvious decision.
## What contributions are we looking for?
## Interfaces and dependencies
**openpilot's priorities are [safety](SAFETY.md), stability, quality, and features, in that order.**
openpilot is part of comma's mission to *solve self-driving cars while delivering shippable intermediaries*, and all development is towards that goal.
Follow cereal's [custom-fork guidance](../openpilot/cereal/README.md#custom-forks). Preserve upstream message definitions and field meanings; put fork-specific messaging in the custom schema. Vehicle support outside an upstream platform's contract needs a distinct platform identity and corresponding interface and safety evidence.
### What gets merged?
Dependencies are tracked source folders. Use the [vendoring workflow](../tools/vendor/README.md), retain provenance and licenses, and review local changes when updating an upstream revision.
The probability of a pull request being merged is a function of its value to the project and the effort it will take us to get it merged.
If a PR offers *some* value but will take lots of time to get merged, it will be closed.
Simple, well-tested bug fixes are the easiest to merge, and new features are the hardest to get merged.
## Verification
All of these are examples of good PRs:
* typo fix: https://github.com/commaai/openpilot/pull/30678
* removing unused code: https://github.com/commaai/openpilot/pull/30573
* simple car model port: https://github.com/commaai/openpilot/pull/30245
* car brand port: https://github.com/commaai/openpilot/pull/23331
Run checks appropriate to the change and describe the result in the pull request:
### What doesn't get merged?
- For a bug fix, demonstrate the failure and the corrected behavior.
- For driving behavior, compare against the intended source behavior or recorded evidence and cover engagement, override, reset, and relevant limits.
- For vehicle or safety changes, identify the exact configuration and run the affected interface and native safety checks.
- For UI changes, inspect the affected interface at its actual size and preserve the intended interactions.
- For performance changes, provide a reproducible comparison and distinguish a component benchmark from whole-system performance.
* **style changes**: code is art, and it's up to the author to make it beautiful
* **500+ line PRs**: clean it up, break it up into smaller PRs, or both
* **PRs without a clear goal**: every PR must have a singular and clear goal
* **UI design**: we do not have a good review process for this yet
* **New features**: We believe openpilot is mostly feature-complete, and the rest is a matter of refinement and fixing bugs. As a result of this, most feature PRs will be immediately closed, however the beauty of open source is that forks can and do offer features that upstream openpilot doesn't.
* **Negative expected value**: This is a class of PRs that makes an improvement, but the risk or validation costs more than the improvement. The risk can be mitigated by first getting a failing test merged.
Keep automated tests, bench observations, and driving results separate. State what remains unverified rather than broadening a narrow result into a fleet-wide claim.
### First contribution
## Commit history
[Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty.
There are a lot of bounties that don't require a comma four or a car.
Use concise feature or behavior names. Keep related changes together, including their tests and documentation. Preserve ordinary follow-up commits so contributors can follow changes.
## Pull Requests
Pull requests should be against the master branch.
A good pull request has all of the following:
* a clearly stated purpose
* every line changed directly contributes to the stated purpose
* verification, i.e. how did you test your PR?
* justification
* if you've optimized something, post benchmarks to prove it's better
* if you've improved your car's tuning, post before and after plots
* passes the CI tests
## Contributing without Code
* Report bugs in GitHub issues.
* Report driving issues in the `#driving-feedback` Discord channel.
* Consider opting into cabin camera uploads to improve the driver monitoring model.
* Connect your device to Wi-Fi regularly, so that we can pull data for training better driving models.
* Run the `nightly` branch and report issues. This branch is like `master` but it's built just like a release.
* Annotate images in the [comma10k dataset](https://github.com/commaai/comma10k).
## Contributing Training Data
### A guide for forks
In order for your fork's data to be eligible for the training set:
* **Your cereal messaging structs must be [compatible](../openpilot/cereal#custom-forks)**
* **The definitions of all the stock messaging structs must not change**: Do not change how any of the fields are set, including everything from `selfdriveState.enabled` to `carState.steeringAngleDeg`. Instead, create your own structs and set them however you'd like.
* **Do not include cars that are not supported in upstream platforms**: Instead, create new opendbc platforms for cars that you'd like to support outside of upstream, even if it's just a trim-level difference.
The original [openpilot contribution guide](https://github.com/commaai/openpilot/blob/521db4c825d37eb5f29acf955daa88da003e4433/docs/CONTRIBUTING.md) documents upstream's process. Changes intended for comma's openpilot should follow that project's contribution requirements.
+44 -2
View File
@@ -1,6 +1,6 @@
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->
# Support Information for 406 Known Cars
# Support Information for 448 Known Cars
|Make|Model|Package|Support Level|
|---|---|---|:---:|
@@ -34,12 +34,50 @@
|Audi|Q5 2017-24|All|[Not compatible](#flexray)|
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|[Upstream](#upstream)|
|Buick|Baby Enclave (SDGM Harness) 2020-23|Driver Assist Package|[Community](#community)|
|Buick|LaCrosse (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|Buick|LaCrosse (OBD-II Harness) 2017-19|Driver Confidence Package 2|[Community](#community)|
|Buick|LaCrosse US (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|Buick|Regal Essence (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|BYD|Atto 3 2022-25|All|[Dashcam mode](#dashcam)|
|Cadillac|ATS Premium Performance (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|CT6 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|Escalade (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|Escalade (OBD-II Harness) 2017|Driver Assist Package|[Community](#community)|
|Cadillac|Escalade ESV (OBD-II Harness) 2016|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Cadillac|Escalade ESV (OBD-II Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Cadillac|Escalade ESV Platinum (ASCM Harness) 2019|Adaptive Cruise Control (ACC)|[Community](#community)|
|Cadillac|XT4 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|XT4 (SDGM Harness) 2023|Driver Assist Package|[Community](#community)|
|Cadillac|XT5 (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Cadillac|XT5 (SDGM Harness) 2022|Driver Assist Package|[Community](#community)|
|Cadillac|XT6 (SDGM Harness) 2020|Driver Assist Package|[Community](#community)|
|Chevrolet|Blazer (SDGM Harness) 2019-25|Driver Assist Package|[Community](#community)|
|Chevrolet|Bolt (Camera Harness) 2022-23|ACC; recognized camera layout required|[Community](#community)|
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|[Upstream](#upstream)|
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|[Upstream](#upstream)|
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Equinox (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu (ASCM Harness) 2017-19|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Malibu (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu (SDGM Harness) 2019|SDGM Harness|[Community](#community)|
|Chevrolet|Malibu Hybrid (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Malibu Premier (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Silverado 1500 2020-21|Safety Package II|[Upstream](#upstream)|
|Chevrolet|Silverado (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Suburban (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|Chevrolet|Suburban (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Suburban (OBD-II Harness)|Configuration-specific install; observed CAN sources required|[Community](#community)|
|Chevrolet|Suburban Premier (ASCM Harness) 2016-20|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chevrolet|Trailblazer (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Chevrolet|Traverse (SDGM Harness) 2022-23|RS, Premier, or High Country Trim|[Community](#community)|
|Chevrolet|Trax (Camera Harness)|Manual camera configuration; recognized CAN layout required|[Community](#community)|
|Chevrolet|Volt (ASCM Harness) 2017-18|Adaptive Cruise Control (ACC); custom ASCM harness install|[Community](#community)|
|Chevrolet|Volt (Camera Harness)|Manual configuration; recognized camera layout required|[Community](#community)|
|Chevrolet|Volt (CC)|Manual development configuration; installation must be verified|[Community](#community)|
|Chevrolet|Volt (OBD-II Harness) 2017-18|Adaptive Cruise Control (ACC)|[Community](#community)|
|Chevrolet|Volt (SDGM Harness) 2019|Adaptive Cruise Control (ACC) & LKAS|[Community](#community)|
|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|[Upstream](#upstream)|
|Chrysler|Pacifica 2021-23|All|[Upstream](#upstream)|
@@ -88,8 +126,12 @@
|Genesis|GV70 Electrified (Australia Only) 2022|All|[Upstream](#upstream)|
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|[Upstream](#upstream)|
|Genesis|GV80 2023|All|[Upstream](#upstream)|
|GMC|Acadia (ASCM Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|GMC|Acadia (OBD-II Harness) 2018|Adaptive Cruise Control (ACC)|[Community](#community)|
|GMC|Sierra 1500 2020-21|Driver Alert Package II|[Upstream](#upstream)|
|GMC|Yukon 2019-20|Adaptive Cruise Control (ACC) & LKAS|[Dashcam mode](#dashcam)|
|GMC|Yukon (CC)|Cruise control; configuration-specific installation|[Community](#community)|
|Holden|Astra (OBD-II Harness) 2017|Adaptive Cruise Control (ACC)|[Community](#community)|
|Honda|Accord 2016-17|Honda Sensing|[Community](#community)|
|Honda|Accord 2018-22|All|[Upstream](#upstream)|
|Honda|Accord 2023-25|All|[Upstream](#upstream)|
@@ -473,4 +515,4 @@ Toyota, and the GM Global B platform.
All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a
CAN bus isn't the only way that the computers in your car can communicate. Most, if not all, vehicles from the following
manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars
may one day be supported, but we have no immediate plans to support FlexRay.
may one day be supported, but we have no immediate plans to support FlexRay.
+17 -1
View File
@@ -42,6 +42,7 @@ class SignalType:
RIVIAN_CHECKSUM = 15
FORD_CHECKSUM = 16
MG_CHECKSUM = 17
RAY_PEDAL_CHECKSUM = 18
@dataclass
@@ -216,8 +217,23 @@ class ChecksumState:
checksum_fields: dict[int, tuple[str, ...]] | None = None
def ray_pedal_setup_signal(sig, dbc_name, line_num):
if sig.name == "CHECKSUM_PEDAL":
if sig.size != 8:
raise RuntimeError("Ray pedal checksum must be 8 bits")
sig.type = SignalType.RAY_PEDAL_CHECKSUM
from opendbc.car.hyundai.ray_pedal import pedal_checksum
sig.calc_checksum = pedal_checksum
elif sig.name == "COUNTER_PEDAL":
if sig.size != 4:
raise RuntimeError("Ray pedal counter must be 4 bits")
sig.type = SignalType.COUNTER
def get_checksum_state(dbc_name: str) -> ChecksumState | None:
if dbc_name.startswith(("honda_", "acura_")):
if dbc_name == "hyundai_kia_ray_pedal":
return ChecksumState(SignalType.RAY_PEDAL_CHECKSUM, None, ray_pedal_setup_signal)
elif dbc_name.startswith(("honda_", "acura_")):
return ChecksumState(SignalType.HONDA_CHECKSUM, honda_checksum)
elif dbc_name.startswith(("toyota_", "lexus_")):
return ChecksumState(SignalType.TOYOTA_CHECKSUM, toyota_checksum)
@@ -0,0 +1 @@
"""Shared three-slot Quick Select configuration and owner-routed actions."""
+67
View File
@@ -0,0 +1,67 @@
"""Finite legacy-key mapping to current authoritative settings owners."""
import hashlib
from openpilot.starpilot.favorites.state import FavoriteAction
from openpilot.starpilot.ui.feature_settings_state import row_change
BOOKMARK = "__starpilot_controller_action__:bookmark"
INCREASE_SPEED = "__starpilot_controller_action__:increase_speed"
DECREASE_SPEED = "__starpilot_controller_action__:decrease_speed"
FORCE_COAST = "__starpilot_controller_action__:force_coast"
PULSE_GLIDE = "__starpilot_controller_action__:pulse_and_glide"
DISENGAGE = "__starpilot_controller_action__:disengage_openpilot"
SET_SPEED = "__starpilot_controller_action__:set_speed"
SELFIE = "__starpilot_controller_action__:selfie"
EXPERIMENTAL = "ExperimentalMode"
SCREEN_OFF = "__starpilot_controller_action__:toggle_screen_off"
TRAFFIC = "__starpilot_controller_action__:traffic_mode"
SWITCHBACK = "__starpilot_controller_action__:switchback_mode"
CYCLE_PERSONALITY = "__starpilot_controller_action__:cycle_driving_personality"
# Saved preference shortcuts retain the current owner's authority and dependencies.
FEATURE_KEYS = {
"slc": ("SpeedLimitController", "ShowSpeedLimits", "SLCConfirmation", "SLCConfirmationHigher", "SLCConfirmationLower", "SLCFallback"),
"lane": ("LaneCentering", "LaneCenteringPauseOnSignal"),
"curve": ("CurveSpeedController", "CurveSpeedControllerNoLead", "ShowCSCStatus"),
"aol": ("AlwaysOnLateral", "NostalgiaMode"),
"profiles": ("CustomPersonalities",),
}
# Slots saved with a retired visibility key stay assigned and read as unavailable.
APPEARANCE_KEYS = ("DriverCamera", "StockConfidenceBallWidget", "HideLeadMarker", "LeadInfo", "SignalMetrics", "BlindSpotMetrics",
"CameraView")
def _row_action(row, apply, section):
request = row_change(row)
allowed = (row.available and not row.repair_value and row.value in row.choices and
2 <= len(row.choices) <= 5 and request is not None and not request.confirmation)
token = hashlib.sha256(repr(row).encode()).hexdigest()
return FavoriteAction(row.key, row.label, "toggle" if row.choices == ("Off", "On") else "enum", row.value,
allowed, "" if allowed else row.reason or "Available while parked with the required feature enabled",
token, (lambda: apply(request)) if allowed else None, section)
def mapped_actions(feature_snapshot, feature_apply, appearance_snapshot, appearance_apply):
actions = {}
for page, keys in FEATURE_KEYS.items():
state = feature_snapshot(page)
for row in state.rows:
if row.key in keys:
actions[row.key] = _row_action(row, feature_apply, state.title)
appearance = appearance_snapshot()
for row in appearance.rows:
if row.key in APPEARANCE_KEYS:
actions[row.key] = _row_action(row, appearance_apply, "Visual")
actions[BOOKMARK] = FavoriteAction(BOOKMARK, "Bookmark", reason="Use this control on the device")
actions[EXPERIMENTAL] = FavoriteAction(EXPERIMENTAL, "Experimental Mode", kind="toggle", state_label="On device",
reason="Use this control on the device")
actions[CYCLE_PERSONALITY] = FavoriteAction(CYCLE_PERSONALITY, "Cycle Driving Personality", kind="enum", state_label="On device",
reason="Use this control on the device")
for key, label in ((INCREASE_SPEED, "Increase Set Speed"), (DECREASE_SPEED, "Decrease Set Speed"),
(SET_SPEED, "Set Speed To"), (FORCE_COAST, "Force Coasting"),
(PULSE_GLIDE, "Pulse and Glide"), (DISENGAGE, "Disengage Openpilot"),
(TRAFFIC, "Traffic Mode"), (SWITCHBACK, "Switchback Mode"), (SCREEN_OFF, "Toggle Screen Off"), (SELFIE, "Selfie")):
actions[key] = FavoriteAction(key, label, kind="toggle" if key in (TRAFFIC, SWITCHBACK, SCREEN_OFF) else "action",
reason="Use the qualified control on the device", section="Driving")
return actions
+174
View File
@@ -0,0 +1,174 @@
import hashlib
import json
import math
from collections.abc import Callable, Mapping
from openpilot.starpilot.favorites.actions import SET_SPEED
from openpilot.starpilot.favorites.state import FavoriteAction, FavoriteRequest, FavoriteResult, FavoriteSlot, FavoriteSnapshot
from openpilot.starpilot.saved_document import commit_exact
from openpilot.starpilot.saved_source import read_saved
FAVORITE_SLOTS_PARAM = "StarPilotFavoriteSlots"
FAVORITE_SLOT_COUNT = 3
MAX_BYTES = 8192
class FavoritesChanged(Exception):
pass
def default_slots():
return [{"enabled": False, "show_onroad": False, "key": None, "label": ""} for _ in range(FAVORITE_SLOT_COUNT)]
def revision(raw):
return hashlib.sha256(b"\x00" if raw is None else b"\x01" + raw).hexdigest()
def _unique(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("Duplicate favorite field")
result[key] = value
return result
def validate_slots(value, *, legacy=False):
if legacy and isinstance(value, dict):
value = value.get("slots")
if type(value) is not list or (len(value) > FAVORITE_SLOT_COUNT if legacy else len(value) != FAVORITE_SLOT_COUNT):
raise ValueError("Quick Select require three slots")
slots = default_slots()
for index, slot in enumerate(value):
if type(slot) is not dict or not set(slot) <= {"enabled", "show_onroad", "key", "label", "value"}:
raise ValueError("Invalid favorite slot")
if not legacy and not {"enabled", "show_onroad", "key", "label"} <= set(slot):
raise ValueError("Incomplete favorite slot")
key, label = slot.get("key"), slot.get("label", "")
enabled, shown = slot.get("enabled", False), slot.get("show_onroad", False)
if (key is not None and (type(key) is not str or not key or len(key) > 128) or
type(label) is not str or len(label) > 32 or type(enabled) is not bool or type(shown) is not bool):
raise ValueError("Invalid favorite values")
saved = {"enabled": enabled, "show_onroad": shown, "key": key, "label": label if key else ""}
if "value" in slot:
speed_value = slot.get("value")
if key != SET_SPEED or speed_value is not None and (
not isinstance(speed_value, int | float) or isinstance(speed_value, bool) or not math.isfinite(speed_value) or speed_value <= 0
):
raise ValueError("Invalid favorite action value")
saved["value"] = speed_value
slots[index] = saved
return slots
def read_slots(params):
raw, readable = read_saved(params, FAVORITE_SLOTS_PARAM, MAX_BYTES)
if not readable:
return default_slots(), raw, False, False
if raw is None:
return default_slots(), raw, True, True
try:
return validate_slots(json.loads(raw, object_pairs_hook=_unique), legacy=True), raw, True, True
except (ValueError, TypeError, UnicodeError, RecursionError):
return default_slots(), raw, True, False
class FavoritesOwner:
def __init__(self, params, actions: Callable[[], Mapping[str, FavoriteAction]], configurable: Callable[[], bool]):
self.params, self.actions, self.configurable = params, actions, configurable
def snapshot(self):
saved, raw, readable, valid = read_slots(self.params)
options = self.actions()
token = revision(raw)
slots = []
for index, slot in enumerate(saved):
action = options.get(slot["key"])
assigned = slot["enabled"] and slot["key"] is not None
available = bool(readable and valid and assigned and action and action.available and (action.invoke is not None or action.invoke_value is not None))
label = slot["label"] or (action.label if action else slot["key"]) or ""
reason = ("Saved Quick Select are unavailable" if not readable or not valid else
"Choose a control" if not slot["key"] else "Quick Select is disabled" if not slot["enabled"] else
"This saved control is not available in this build" if action is None else action.reason)
request = FavoriteRequest(index, slot["key"], token, action.token) if assigned and action else None
slots.append(FavoriteSlot(index, slot["key"], label, slot["enabled"], slot["show_onroad"],
action.kind if action else "action", action.state_label if action else "Unavailable" if slot["key"] else "Not assigned",
available, reason, request, slot.get("value")))
return FavoriteSnapshot(tuple(slots), tuple(sorted(options.values(), key=lambda item: (item.label.casefold(), item.key))),
token, readable and self.configurable(), valid)
def save(self, slots, expected_revision, *, authorized=lambda: True):
if type(expected_revision) is not str:
raise ValueError("Invalid Quick Select revision")
normalized = validate_slots(slots)
existing, raw, readable, _valid = read_slots(self.params)
if not readable:
raise OSError("Saved Quick Select are unavailable")
if revision(raw) != expected_revision:
raise FavoritesChanged("Quick Select changed. Reload before saving.")
options = self.actions()
for index, slot in enumerate(normalized):
if slot["key"] is not None and slot["key"] not in options:
previous = existing[index]
if slot["key"] != previous["key"] or slot.get("value") != previous.get("value"):
raise ValueError("Unsupported favorite control")
encoded = json.dumps(normalized, separators=(",", ":"), allow_nan=False).encode()
if len(encoded) > MAX_BYTES:
raise ValueError("Quick Select are too large")
result = commit_exact(self.params, key=FAVORITE_SLOTS_PARAM, max_bytes=MAX_BYTES, raw=encoded, expected=raw,
authorized=lambda: self.configurable() and authorized(), temp_prefix=".favorites-")
if not result.committed or not result.verified:
raise FavoritesChanged("Quick Select could not be saved. Reload before trying again.")
return self.snapshot()
def configure_index(self, index, key, expected_revision, *, enabled=True, show_onroad=True, label=None):
if type(index) is not int or not 0 <= index < FAVORITE_SLOT_COUNT:
return False
saved, _raw, readable, _valid = read_slots(self.params)
if not readable:
return False
options = self.actions()
if key is not None and key not in options:
return False
action = options.get(key)
saved[index] = {"enabled": enabled if key else False, "show_onroad": show_onroad if key else False,
"key": key, "label": (label if label is not None else action.label if action else "")[:32]}
try:
self.save(saved, expected_revision)
return True
except (FavoritesChanged, OSError, ValueError):
return False
def clear_index(self, index, expected_revision):
return self.configure_index(index, None, expected_revision)
def invoke(self, request):
if not isinstance(request, FavoriteRequest) or type(request.index) is not int or not 0 <= request.index < FAVORITE_SLOT_COUNT:
return FavoriteResult(False, "Quick Select is unavailable")
slots, raw, readable, valid = read_slots(self.params)
slot = slots[request.index]
if not readable or not valid or revision(raw) != request.revision or slot["key"] != request.key or not slot["enabled"]:
return FavoriteResult(False, "Quick Select changed. Try again.")
action = self.actions().get(request.key)
if action is None:
return FavoriteResult(False, "This saved control is not available in this build")
if not action.available or action.invoke is None and action.invoke_value is None:
return FavoriteResult(False, action.reason or "Quick Select is unavailable")
if action.token != request.action_token:
return FavoriteResult(False, "Control state changed. Try again.")
current, fresh = read_saved(self.params, FAVORITE_SLOTS_PARAM, MAX_BYTES)
if not fresh or current != raw:
return FavoriteResult(False, "Quick Select changed. Try again.")
try:
if action.invoke_value is not None:
applied = action.invoke_value(slot.get("value") if slot.get("value") is not None else 30)
else:
assert action.invoke is not None
applied = action.invoke()
if not applied:
return FavoriteResult(False, "Control is unavailable or changed. Try again.")
updated = self.actions().get(request.key)
return FavoriteResult(True, action.label, updated.state_label if updated else "Done")
except (OSError, RuntimeError, ValueError, TypeError):
return FavoriteResult(False, "Control could not be changed")
+55
View File
@@ -0,0 +1,55 @@
from collections.abc import Callable
from dataclasses import dataclass, field
@dataclass(frozen=True)
class FavoriteAction:
key: str
label: str
kind: str = "action"
state_label: str = "Press"
available: bool = False
reason: str = "Unavailable in this context"
token: str = ""
invoke: Callable[[], bool] | None = field(default=None, compare=False, repr=False)
section: str = "Actions"
invoke_value: Callable[[float], bool] | None = field(default=None, compare=False, repr=False)
@dataclass(frozen=True)
class FavoriteRequest:
index: int
key: str
revision: str
action_token: str
@dataclass(frozen=True)
class FavoriteSlot:
index: int
key: str | None = None
label: str = ""
enabled: bool = False
show_onroad: bool = False
kind: str = "action"
state_label: str = "Not assigned"
available: bool = False
reason: str = "Choose a control"
request: FavoriteRequest | None = None
value: float | None = None
@dataclass(frozen=True)
class FavoriteSnapshot:
slots: tuple[FavoriteSlot, ...]
options: tuple[FavoriteAction, ...]
revision: str
configurable: bool
valid: bool
@dataclass(frozen=True)
class FavoriteResult:
success: bool
message: str
state_label: str = ""
@@ -0,0 +1,144 @@
"""Explicit offline normalization of caches from this exact compiled schema."""
from datetime import UTC, datetime
import hashlib
import json
from pathlib import Path
import struct
import capnp
from openpilot.starpilot import schema_cache as cache
from openpilot.starpilot.state_migration import (
_atomic_write, _outside_params, _params_lock, _read_value, _save_snapshot, canonical_json, load_snapshot,
)
EVENT_KEYS = tuple(sorted(key for key, contract in cache.CONTRACTS.items() if contract.service is not None))
class NormalizationError(RuntimeError):
"""No incompatible cache is eligible; an I/O failure may leave a reported partial batch."""
class _EnvelopeSink:
def put(self, key, value, *, block):
self.raw = value
def _header(raw):
start = len(cache.MAGIC) + 4
size = struct.unpack_from('>I', raw, len(cache.MAGIC))[0]
return json.loads(raw[start:start + size])
def _values(value):
if isinstance(value, float):
return struct.pack('>d', value) # Preserve signed zero and nonfinite values during comparison.
if isinstance(value, dict):
return {key: _values(item) for key, item in value.items()}
if isinstance(value, list):
return [_values(item) for item in value]
return value
def _prepare(key, raw):
inspected = cache.inspect_cache(key, raw)
if inspected.status != 'valid' or inspected.payload is None:
raise NormalizationError(f'{key}: {inspected.status}: {inspected.reason}')
contract = cache.CONTRACTS[key]
if contract.service is None:
raise NormalizationError(f'{key}: only Event caches can be normalized')
try:
with contract.root.from_bytes(inspected.payload, traversal_limit_in_words=cache.MAX_PAYLOAD_BYTES // 8) as message:
# Event.to_dict suppresses union-member decode errors; decode the selected
# service explicitly before constructing a fresh typed message.
service_values = getattr(message, contract.service).to_dict()
decoded = message.to_dict()
if contract.service not in decoded or _values(decoded[contract.service]) != _values(service_values):
raise NormalizationError(f'{key}: selected service did not decode completely')
original_values = _values(decoded)
if _header(raw)['version'] == 2:
return raw
sink = _EnvelopeSink()
cache.put_cache(sink, key, contract.root.new_message(**decoded), block=True)
converted = cache.inspect_cache(key, sink.raw)
if converted.status != 'valid' or converted.payload is None or _header(sink.raw)['version'] != 2:
raise NormalizationError(f'{key}: current writer did not produce a valid v2 envelope')
with contract.root.from_bytes(converted.payload, traversal_limit_in_words=cache.MAX_PAYLOAD_BYTES // 8) as message:
if _values(message.to_dict()) != original_values:
raise NormalizationError(f'{key}: typed values changed during normalization')
except (ValueError, TypeError, capnp.KjException) as error:
raise NormalizationError(f'{key}: typed cache validation failed: {type(error).__name__}') from error
return sink.raw
def _read(path):
try:
return _read_value(path)
except FileNotFoundError:
return None
def _identity(raw):
return None if raw is None else {'sha256': hashlib.sha256(raw).hexdigest(), 'size': len(raw),
'version': _header(raw)['version'], 'schema_sha256': _header(raw)['schema_sha256']}
def normalize_current_event_caches(params, backup_dir, *, producers_stopped: bool = False):
"""Validate every present Event cache before backing up and writing any cache.
Run with cache producers stopped, before changing compiled schemas. This cannot
convert an incompatible v1 cache. It never changes CarParams or read-side APIs.
The Params lock covers comparisons and atomic replacements; each prepared
envelope comes from put_cache with a fully decoded current typed message.
"""
if producers_stopped is not True:
raise NormalizationError('Stop cache producers before explicitly normalizing caches')
namespace = Path(params.get_param_path(EVENT_KEYS[0])).parent.absolute()
storage = Path(backup_dir).absolute()
_outside_params(namespace, storage)
with _params_lock(namespace):
actual = namespace.resolve(strict=True)
originals = {key: _read(actual / key) for key in EVENT_KEYS}
prepared = {key: _prepare(key, raw) for key, raw in originals.items() if raw is not None}
changed = [key for key, raw in prepared.items() if raw != originals[key]]
report = {
'format': 'starpilot-current-event-cache-normalization', 'version': 1,
'recorded_utc': datetime.now(UTC).isoformat(), 'namespace': str(namespace),
'codec': {'path': cache.__file__, 'sha256': hashlib.sha256(Path(cache.__file__).read_bytes()).hexdigest()},
'normalizer': {'path': __file__, 'sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()},
'status': 'unchanged', 'backup': None, 'attempted': [], 'written': [],
'caches': {key: {'before': _identity(raw), 'after': _identity(prepared.get(key)),
'action': 'normalize' if key in changed else 'missing' if raw is None else 'unchanged'}
for key, raw in originals.items()},
}
if not changed:
return report
backup = _save_snapshot({key: raw for key, raw in originals.items() if raw is not None}, storage)
if load_snapshot(backup) != {key: raw for key, raw in originals.items() if raw is not None}:
raise NormalizationError('Backup readback differs from original caches')
report.update(status='prepared', backup=str(backup))
receipt = backup / 'normalization.json'
_atomic_write(receipt, canonical_json(report))
try:
if namespace.resolve(strict=True) != actual or any(_read(actual / key) != raw for key, raw in originals.items()):
raise NormalizationError('Cache source changed after validation; no caches were written')
for key in changed:
if namespace.resolve(strict=True) != actual or _read(actual / key) != originals[key]:
raise NormalizationError(f'{key}: source changed before replacement')
report['attempted'].append(key)
_atomic_write(receipt, canonical_json(report))
_atomic_write(actual / key, prepared[key])
report['written'].append(key)
if _read(actual / key) != prepared[key] or cache.inspect_cache(key, prepared[key]).status != 'valid':
raise NormalizationError(f'{key}: exact cache readback failed')
if any(_read(actual / key) != prepared.get(key) for key in EVENT_KEYS):
raise NormalizationError('Final cache readback differs from the prepared batch')
report['status'] = 'normalized'
_atomic_write(receipt, canonical_json(report))
except Exception as error:
report.update(status='failed', error=f'{type(error).__name__}: {error}')
_atomic_write(receipt, canonical_json(report))
raise NormalizationError(f'Normalization stopped; originals and partial-write receipt: {backup}') from error
return report
+17 -2
View File
@@ -1,12 +1,19 @@
import os
import requests
from requests.adapters import HTTPAdapter, Retry
from openpilot.starpilot.connect.provider import active_provider, PROVIDERS
API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com')
# TODO: this should be merged into common.api
class CommaApi:
def __init__(self, token=None):
def __init__(self, token=None, *, provider_name=None):
selected = active_provider()
if selected.name not in PROVIDERS or provider_name not in (None, selected.name):
raise APIError('Cloud provider is unavailable or changed')
self.provider_name = selected.name
self.api_host = API_HOST if selected.name == 'comma' else selected.api
self.session = requests.Session()
self.session.headers['User-agent'] = 'OpenpilotTools'
if token:
@@ -16,7 +23,15 @@ class CommaApi:
self.session.mount('https://', HTTPAdapter(max_retries=retries))
def request(self, method, endpoint, **kwargs):
with self.session.request(method, API_HOST + '/' + endpoint, **kwargs) as resp:
if active_provider().name != self.provider_name:
raise APIError('Cloud provider changed; sign in again')
with self.session.request(method, self.api_host.rstrip('/') + '/' + endpoint.lstrip('/'), **kwargs) as resp:
if resp.status_code in (401, 403):
raise UnauthorizedError('Unauthorized. Authenticate with openpilot/tools/lib/auth.py')
if resp.status_code >= 400:
error = APIError(f'Cloud API returned HTTP {resp.status_code}')
error.status_code = resp.status_code
raise error
resp_json = resp.json()
if isinstance(resp_json, dict) and resp_json.get('error'):
if resp.status_code in [401, 403]:
+20 -5
View File
@@ -33,6 +33,7 @@ from typing import Any
from urllib.parse import parse_qs, urlencode, urlsplit
from openpilot.tools.lib.api import CommaApi, UnauthorizedError
from openpilot.starpilot.connect.provider import active_provider
from openpilot.tools.lib.auth_config import set_token, get_token
class ClientRedirectServer(ThreadingHTTPServer):
@@ -72,6 +73,8 @@ class ClientRedirectHandler(BaseHTTPRequestHandler):
def auth_redirect_link(method, port):
if active_provider().name != 'comma':
raise ValueError('Browser OAuth client IDs belong to comma only')
provider_id = {
'google': 'g',
'apple': 'a',
@@ -112,6 +115,12 @@ def auth_redirect_link(method, port):
def login(method, timeout=180):
"""Sign in through a browser and save the token, returning a success/error status."""
selected = active_provider()
if selected.name == 'konik':
return {'error': 'Sign in at https://stable.konik.ai, open https://api.konik.ai/v2/user/token while signed in, ' +
'then use auth.py jwt with your Konik token. comma OAuth credentials are not used.'}
if selected.name != 'comma':
return {'error': 'Cloud provider is unavailable. Repair its configuration before signing in.'}
try:
with ClientRedirectServer(('localhost', 0), ClientRedirectHandler) as server:
url = auth_redirect_link(method, server.server_port)
@@ -129,12 +138,12 @@ def login(method, timeout=180):
provider = {'google': 'g', 'apple': 'a', 'github': 'h'}[method]
if len(params['code']) != 1 or not params['code'][0].strip() or params.get('provider') != [provider]:
return {"error": "Invalid sign-in response. Please try again."}
response = CommaApi().post('v2/auth/', data={'code': params['code'], 'provider': params['provider']}, timeout=30)
response = CommaApi(provider_name=selected.name).post('v2/auth/', data={'code': params['code'], 'provider': params['provider']}, timeout=30)
token = response.get('access_token')
if not isinstance(token, str) or not token.strip():
return {"error": "Sign-in did not return an access token. Please try again."}
CommaApi(token).get('v1/me', timeout=30)
set_token(token)
CommaApi(token, provider_name=selected.name).get('v1/me', timeout=30)
set_token(token, selected.name)
return {"success": True}
if browser.poll() not in (None, 0):
return {"error": "Could not open your browser. Check your default browser and try again."}
@@ -144,7 +153,7 @@ def login(method, timeout=180):
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Login to your comma account')
parser = argparse.ArgumentParser(description='Login to the active cloud provider')
parser.add_argument('method', default='google', const='google', nargs='?', choices=['google', 'apple', 'github', 'jwt'])
parser.add_argument('jwt', nargs='?')
parser.add_argument('--json', action='store_true', help='Return browser sign-in status as JSON')
@@ -161,7 +170,13 @@ if __name__ == '__main__':
print("method JWT selected, but no JWT was provided")
exit(1)
set_token(args.jwt)
selected = active_provider()
try:
CommaApi(args.jwt, provider_name=selected.name).get('v1/me', timeout=30)
set_token(args.jwt, selected.name)
except Exception:
print('The token was not accepted by the active provider; saved credentials were unchanged.', file=sys.stderr)
sys.exit(1)
else:
result = login(args.method)
if "error" in result:
+40 -8
View File
@@ -1,23 +1,55 @@
"""Tools credentials are isolated by the active cloud provider."""
import json
import os
from openpilot.common.hardware.hw import Paths
from openpilot.common.utils import atomic_write
from openpilot.starpilot.connect.provider import active_provider, PROVIDERS
class MissingAuthConfigError(Exception):
pass
def get_token():
def _provider(provider_name=None):
name = active_provider().name
if name not in PROVIDERS or provider_name not in (None, name):
raise MissingAuthConfigError('Cloud provider is unavailable or changed')
return name
def auth_path(provider_name=None):
name = _provider(provider_name)
return os.path.join(Paths.config_root(), 'auth.json' if name == 'comma' else 'auth-konik.json')
def get_token(provider_name=None):
try:
with open(os.path.join(Paths.config_root(), 'auth.json')) as f:
auth = json.load(f)
return auth['access_token']
except Exception:
name = _provider(provider_name)
with open(auth_path(name)) as stream:
auth = json.load(stream)
# Historical untagged credentials belong only to comma.
if not isinstance(auth, dict) or auth.get('provider', 'comma') != name:
return None
token = auth.get('access_token')
return token if isinstance(token, str) and token.strip() else None
except (OSError, ValueError, TypeError, MissingAuthConfigError):
return None
def set_token(token):
def set_token(token, provider_name=None):
name = _provider(provider_name)
if not isinstance(token, str) or not token.strip():
raise ValueError('A nonempty provider token is required')
os.makedirs(Paths.config_root(), exist_ok=True)
with atomic_write(os.path.join(Paths.config_root(), 'auth.json'), overwrite=True) as f:
json.dump({'access_token': token}, f)
with atomic_write(auth_path(name), overwrite=True) as stream:
if _provider(name) != name:
raise MissingAuthConfigError('Cloud provider changed')
json.dump({'provider': name, 'access_token': token}, stream)
def clear_token(provider_name=None):
try:
os.unlink(auth_path(provider_name))
except FileNotFoundError:
pass
+20 -5
View File
@@ -68,14 +68,22 @@ def make_decompressor(compression):
raise ValueError(f"Unsupported compression type: {compression}")
def decompress_file(source, destination, compression=None):
def decompress_file(source, destination, compression=None, max_bytes=0):
with open(source, 'rb') as src, open(destination, 'wb') as dst:
header = src.read(4)
compression = compression or compression_type(header)
decompressor = make_decompressor(compression)
dst.write(decompressor.decompress(header))
while data := src.read(1024 * 1024):
dst.write(decompressor.decompress(data))
written = 0
def write(data):
nonlocal written
written += len(data)
if max_bytes and written > max_bytes:
raise ValueError("Decompressed log exceeds replay memory limit")
dst.write(data)
write(decompressor.decompress(header))
# Bound a single decoder allocation as well as the completed output.
while data := src.read(256 if max_bytes else 1024 * 1024):
write(decompressor.decompress(data))
if not decompressor.eof:
raise EOFError(f"Compressed {compression} file ended before the end-of-stream marker")
@@ -291,7 +299,7 @@ def cmd_decompress(args):
output_fd, output_path = tempfile.mkstemp(dir=Paths.download_cache_root())
os.close(output_fd)
try:
decompress_file(args.path, output_path)
decompress_file(args.path, output_path, max_bytes=getattr(args, 'max_bytes', 0))
except Exception as e:
try:
os.unlink(output_path)
@@ -322,6 +330,12 @@ def cmd_device_routes(args):
def main():
parent = os.environ.get('SP_REPLAY_HELPER_PARENT')
if parent is not None:
import ctypes
import signal
if sys.platform != 'linux' or ctypes.CDLL(None).prctl(1, signal.SIGKILL, 0, 0, 0) != 0 or os.getppid() != int(parent):
raise RuntimeError('Replay source owner exited')
parser = argparse.ArgumentParser(description="File downloader CLI for openpilot tools")
subparsers = parser.add_subparsers(dest="command", required=True)
@@ -343,6 +357,7 @@ def main():
p_dc = subparsers.add_parser("decompress")
p_dc.add_argument("path")
p_dc.add_argument("--max-bytes", type=int, default=0)
p_dc.set_defaults(func=cmd_decompress)
p_dev = subparsers.add_parser("devices")
+8 -2
View File
@@ -65,8 +65,14 @@ def probe_routes(routes: dict, athena_proxy: list[str]) -> tuple[str | None, dic
except queue.Empty:
break
status[name] = (ms, err)
if err is None and (name == "local" or status.get("local", (None, "x"))[1] is not None or "local" not in routes):
choice = name
if status.get("local", (None, "x"))[1] is None:
choice = "local"
elif status.get("athena", (None, "x"))[1] is None and ("local" not in routes or "local" in status):
choice = "athena"
# A successful proxy remains usable if the local probe never completes.
if choice is None and status.get("athena", (None, "x"))[1] is None:
choice = "athena"
for p in procs:
p.kill()
+2 -2
View File
@@ -33,5 +33,5 @@ replacements explicitly, and update the main manifest revision only after
integration. Never accept a gitlink in place of a source folder. Updater and CI
checks reject that layout.
This branch is a migration foundation. Vehicle, settings, control-mode, UI,
firmware and device qualification must be completed before deployment.
Dependency provenance checks do not replace runtime, firmware, or vehicle
qualification for an update.