mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-15 20:33:41 +08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a8a6607de | |||
| 6d47f03160 | |||
| 273eb9f997 | |||
| e143888eb1 | |||
| 8092f13438 | |||
| a87788b65c | |||
| 4cbe97fd20 | |||
| 98254867a9 | |||
| bee1cdd45d | |||
| 91e40b80d8 | |||
| 110568a9d1 | |||
| bb1b9a27d8 | |||
| 76b21c72cf | |||
| 70424bd661 | |||
| d215eab1d4 | |||
| 91316c8cb5 | |||
| fa284be7e6 | |||
| 6e7d9e5e52 | |||
| a4a7c2335d | |||
| ae573c7c3f | |||
| 7d7b6ee306 | |||
| 6a4c59c3e0 | |||
| fb5cb7a1cc | |||
| 049dfd2eaa | |||
| be20848487 | |||
| cdd232b606 | |||
| b21c70b1ba | |||
| 67e5bd3c1e | |||
| 74692d0b5f | |||
| dd35c27981 | |||
| 159140e64e | |||
| f1ab6c8dfb | |||
| e1fe30fd3e | |||
| fba521dcff | |||
| a8ef55bfaa | |||
| a232f54e2d | |||
| 2697008aa7 | |||
| ad5abd242a | |||
| 6c1e0f370b | |||
| 1083f5bf21 | |||
| dc5116c718 | |||
| 8611e08dc6 | |||
| dc0f73c63b |
@@ -0,0 +1,39 @@
|
||||
name: prebuilt
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD: release/ci/docker_build_sp.sh
|
||||
|
||||
jobs:
|
||||
build_prebuilt:
|
||||
name: build prebuilt
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'sunnypilot/sunnypilot'
|
||||
env:
|
||||
PUSH_IMAGE: true
|
||||
permissions:
|
||||
checks: read
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Wait for green check mark
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
|
||||
with:
|
||||
ref: master
|
||||
wait-interval: 30
|
||||
running-workflow-name: 'build prebuilt'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
check-regexp: ^((?!.*(build master-ci|create badges).*).)*$
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- run: git lfs pull
|
||||
- name: Build and Push docker image
|
||||
run: |
|
||||
$DOCKER_LOGIN
|
||||
eval "$BUILD"
|
||||
@@ -132,6 +132,7 @@ jobs:
|
||||
|
||||
process_replay:
|
||||
name: process replay
|
||||
if: false # disable process_replay for forks
|
||||
runs-on: ${{
|
||||
(github.repository == 'commaai/openpilot') &&
|
||||
((github.event_name != 'pull_request') ||
|
||||
@@ -168,14 +169,14 @@ jobs:
|
||||
name: diff_report_${{ github.event.number }}
|
||||
path: openpilot/selfdrive/test/process_replay/diff_report.txt
|
||||
- name: Checkout ci-artifacts
|
||||
if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
|
||||
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: sunnypilot/ci-artifacts
|
||||
repository: commaai/ci-artifacts
|
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||
path: ${{ github.workspace }}/ci-artifacts
|
||||
- name: Prepare refs
|
||||
if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
|
||||
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
|
||||
working-directory: ${{ github.workspace }}/ci-artifacts
|
||||
run: |
|
||||
git config user.name "GitHub Actions Bot"
|
||||
@@ -187,7 +188,7 @@ jobs:
|
||||
git add .
|
||||
git commit -m "process-replay refs for ${{ github.repository }}@${{ github.sha }}" || echo "No changes to commit"
|
||||
- name: Push refs
|
||||
if: github.repository == 'sunnypilot/sunnypilot' && github.ref == 'refs/heads/master'
|
||||
if: github.repository == 'commaai/openpilot' && github.ref == 'refs/heads/master'
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# AI policy
|
||||
|
||||
## Why this exists
|
||||
|
||||
We use AI tools ourselves, so this isn't an anti-AI stance. The problem is people submitting code, issues, or comments they don't actually understand. AI makes that very easy to do, and it creates real work for reviewers who have to figure out what you meant when you can't explain it yourself.
|
||||
|
||||
If you're not going to put effort into understanding and verifying your submission, we're not going to put effort into reviewing it.
|
||||
|
||||
## The rule
|
||||
|
||||
You are responsible for everything you submit: code, PR descriptions, issues, bug reports, comments.
|
||||
|
||||
1. Understand what you submit. If a reviewer asks why you did something, you answer from your own understanding, not by re-prompting. If you can't do that, don't submit it.
|
||||
|
||||
2. Test your change. AI gets things wrong all the time. Run it, break it, confirm it actually works.
|
||||
|
||||
3. Driving fixes need real evidence. Attach a dongle ID, upload logs, and include segments that show the fix working. A route hash by itself proves nothing.
|
||||
|
||||
4. No AI-generated media (images, diagrams, videos) in issues or PRs.
|
||||
|
||||
## Disclosure
|
||||
|
||||
If AI tools helped you write something, say so. Add an `Assisted-by:` line in your commit message:
|
||||
|
||||
```
|
||||
Assisted-by: GitHub Copilot
|
||||
Assisted-by: Claude
|
||||
```
|
||||
|
||||
Disclosing won't count against your PR. It helps reviewers know where to look. Hiding it and getting caught will.
|
||||
|
||||
## How we review
|
||||
|
||||
Reviewers are looking at whether you understand your own change. Can you explain it? Can you respond to feedback without re-prompting? Does your PR description say why you made the change, not just list what changed?
|
||||
|
||||
Good code from someone who used AI and understands what they wrote is fine. How you got there doesn't matter as long as you can stand behind it.
|
||||
|
||||
## What happens
|
||||
|
||||
Submissions that don't meet this bar get closed. If it keeps happening, you get blocked.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Maintainers use AI at their discretion. They've earned that through sustained contribution and they know the codebase.
|
||||
@@ -1,5 +1,3 @@
|
||||
> sunnypilot follows [commaai/openpilot](https://github.com/commaai/openpilot)'s contributing guidelines. The following applies to all contributions here.
|
||||
|
||||
# How to contribute
|
||||
|
||||
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/).
|
||||
@@ -37,7 +35,6 @@ All of these are examples of good PRs:
|
||||
* **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.
|
||||
* **AI-generated contributions**: see our [AI policy](AI_POLICY.md)
|
||||
|
||||
### First contribution
|
||||
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: c89db21621...d6b9c1adaa
@@ -69,8 +69,6 @@ struct LeadData {
|
||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||
mads @0 :ModularAssistiveDrivingSystem;
|
||||
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
|
||||
buttonsPressed @2 :UInt16;
|
||||
buttonsReleaseToggle @3 :UInt16;
|
||||
|
||||
enum AudibleAlert {
|
||||
none @0;
|
||||
|
||||
@@ -222,7 +222,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
|
||||
@@ -56,9 +56,6 @@ class CarSpecificEvents:
|
||||
if self.CP.minEnableSpeed > 0 and CS.vEgo < 0.001:
|
||||
events.add(EventName.manualRestart)
|
||||
|
||||
if CS.brakeHoldActive and CS.blockPcmEnable: # set by Nidec Hybrid which cannot resume from brakehold
|
||||
events.add(EventName.belowEngageSpeed)
|
||||
|
||||
elif self.CP.brand == 'toyota':
|
||||
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
|
||||
@@ -25,8 +25,6 @@ from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotc
|
||||
from openpilot.tools.lib.file_sources import Source
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
|
||||
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
SteerControlType = structs.CarParams.SteerControlType
|
||||
|
||||
@@ -134,7 +132,7 @@ class TestCarModelBase(unittest.TestCase):
|
||||
segment_range = f"{cls.test_route.route}/{seg}"
|
||||
|
||||
try:
|
||||
sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source, sunnypilot_car_segments_source]
|
||||
sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
|
||||
lr = LogReader(segment_range, sources=sources, sort_by_time=True)
|
||||
return cls.get_testing_data_from_logreader(lr)
|
||||
except (LogsUnavailable, AssertionError):
|
||||
|
||||
@@ -158,7 +158,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
def publish(self, sm, pm):
|
||||
plan_send = messaging.new_message('longitudinalPlan')
|
||||
|
||||
plan_send.valid = sm.all_checks()
|
||||
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState'])
|
||||
|
||||
longitudinalPlan = plan_send.longitudinalPlan
|
||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||
|
||||
@@ -29,19 +29,19 @@ def main():
|
||||
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
||||
'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service],
|
||||
poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
||||
'liveMapDataSP', 'carStateSP', gps_location_service],
|
||||
poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
||||
|
||||
while True:
|
||||
sm.update()
|
||||
longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle)
|
||||
longitudinal_planner.sla.update_car_state(sm['carState'])
|
||||
if sm.updated['modelV2']:
|
||||
longitudinal_planner.update(sm)
|
||||
longitudinal_planner.publish(sm, pm)
|
||||
|
||||
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
|
||||
msg = messaging.new_message('driverAssistance')
|
||||
msg.valid = sm.all_checks()
|
||||
msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters'])
|
||||
msg.driverAssistance.leftLaneDeparture = ldw.left
|
||||
msg.driverAssistance.rightLaneDeparture = ldw.right
|
||||
pm.send('driverAssistance', msg)
|
||||
|
||||
@@ -30,7 +30,6 @@ from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
REPLAY = "REPLAY" in os.environ
|
||||
@@ -178,7 +177,6 @@ class SelfdriveD(CruiseHelper):
|
||||
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
|
||||
|
||||
CruiseHelper.__init__(self, self.CP)
|
||||
self.button_state_tracker = ButtonStateTracker()
|
||||
|
||||
def update_events(self, CS):
|
||||
"""Compute onroadEvents from carState"""
|
||||
@@ -599,8 +597,6 @@ class SelfdriveD(CruiseHelper):
|
||||
icbm.sendButton = self.icbm.cruise_button
|
||||
icbm.vTarget = self.icbm.v_target
|
||||
|
||||
self.button_state_tracker.publish(ss_sp)
|
||||
|
||||
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
||||
|
||||
# onroadEventsSP - logged every second or on change
|
||||
@@ -620,7 +616,6 @@ class SelfdriveD(CruiseHelper):
|
||||
self.mads.update(CS)
|
||||
self.update_alerts(CS)
|
||||
|
||||
self.button_state_tracker.update(CS)
|
||||
self.publish_selfdriveState(CS)
|
||||
|
||||
self.CS_prev = CS
|
||||
|
||||
@@ -66,7 +66,7 @@ segments = [
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "body", "psa"]
|
||||
|
||||
BASE_URL = "https://raw.githubusercontent.com/sunnypilot/ci-artifacts/refs/heads/process-replay/"
|
||||
BASE_URL = "https://raw.githubusercontent.com/commaai/ci-artifacts/refs/heads/process-replay/"
|
||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||
EXCLUDED_PROCS = {"modeld", "dmonitoringmodeld"}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.home import HomeLayoutSP as HomeLayout
|
||||
|
||||
|
||||
class MainState(IntEnum):
|
||||
|
||||
@@ -13,7 +13,6 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
BRAND_FONT_SIZE = 48
|
||||
BRAND_DESC_SPACING = 12
|
||||
|
||||
|
||||
class HomeLayoutSP(HomeLayout):
|
||||
def _render_header(self):
|
||||
font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
version_text_width = self.header_rect.width
|
||||
|
||||
if self.update_available:
|
||||
version_text_width -= self.update_notif_rect.width
|
||||
|
||||
highlight_color = rl.Color(75, 95, 255, 255) if self.current_state == HomeLayoutState.UPDATE else rl.Color(54, 77, 239, 255)
|
||||
rl.draw_rectangle_rounded(self.update_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
text = tr("UPDATE")
|
||||
text_size = measure_text_cached(font, text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.update_notif_rect.x + (self.update_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.update_notif_rect.y + (self.update_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
if self.alert_count > 0:
|
||||
version_text_width -= self.alert_notif_rect.width
|
||||
|
||||
highlight_color = rl.Color(255, 70, 70, 255) if self.current_state == HomeLayoutState.ALERTS else rl.Color(226, 44, 44, 255)
|
||||
rl.draw_rectangle_rounded(self.alert_notif_rect, 0.3, 10, highlight_color)
|
||||
|
||||
alert_text = trn("{} ALERT", "{} ALERTS", self.alert_count).format(self.alert_count)
|
||||
text_size = measure_text_cached(font, alert_text, HEAD_BUTTON_FONT_SIZE)
|
||||
text_x = self.alert_notif_rect.x + (self.alert_notif_rect.width - text_size.x) // 2
|
||||
text_y = self.alert_notif_rect.y + (self.alert_notif_rect.height - text_size.y) // 2
|
||||
rl.draw_text_ex(font, alert_text, rl.Vector2(int(text_x), int(text_y)), HEAD_BUTTON_FONT_SIZE, 0, rl.WHITE)
|
||||
|
||||
if self.update_available or self.alert_count > 0:
|
||||
version_text_width -= SPACING * 1.5
|
||||
|
||||
version_right = self.header_rect.x + self.header_rect.width
|
||||
version_left = version_right - version_text_width
|
||||
|
||||
brand = "sunnypilot"
|
||||
description = self.params.get("UpdaterCurrentDescription") or ""
|
||||
|
||||
desc_width = 0
|
||||
if description:
|
||||
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
||||
desc_width = desc_size.x
|
||||
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
||||
spacing = BRAND_DESC_SPACING if description else 0
|
||||
brand_x = version_right - desc_width - spacing - brand_size.x
|
||||
brand_rect = rl.Rectangle(max(version_left, brand_x), self.header_rect.y, brand_size.x, self.header_rect.height)
|
||||
gui_label(brand_rect, brand, BRAND_FONT_SIZE, rl.WHITE, font_weight=FontWeight.AUDIOWIDE)
|
||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||
|
||||
self._content = [
|
||||
{
|
||||
|
||||
+2
-5
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
@@ -96,10 +96,7 @@ class MadsSettingsLayout(Widget):
|
||||
if brand == "rivian":
|
||||
return True
|
||||
elif brand == "tesla":
|
||||
if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
|
||||
return True
|
||||
screen_button = int(ui_state.params.get("TeslaMadsScreenButton", return_default=True))
|
||||
return screen_button == MadsScreenButtonType.OFF
|
||||
return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||
return False
|
||||
|
||||
def _update_steering_mode_description(self, button_index: int):
|
||||
|
||||
@@ -4,11 +4,10 @@ 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.
|
||||
"""
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
COOP_STEERING_MIN_KMH = 23
|
||||
OEM_STEERING_MIN_KMH = 48
|
||||
@@ -19,14 +18,7 @@ class TeslaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
||||
self.mads_screen_button = multiple_button_item_sp(
|
||||
title=lambda: tr("MADS Screen Activation"),
|
||||
description="",
|
||||
buttons=[lambda: tr("Off"), lambda: tr("3-Finger"), lambda: tr("4-Finger"), lambda: tr("5-Finger")],
|
||||
param="TeslaMadsScreenButton",
|
||||
inline=False,
|
||||
)
|
||||
self.items = [self.coop_steering_toggle, self.mads_screen_button]
|
||||
self.items = [self.coop_steering_toggle]
|
||||
|
||||
def update_settings(self):
|
||||
is_metric = ui_state.is_metric
|
||||
@@ -49,18 +41,3 @@ class TeslaSettings(BrandSettings):
|
||||
|
||||
self.coop_steering_toggle.set_description(coop_steering_desc)
|
||||
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
|
||||
has_vehicle_bus = ui_state.CP_SP is not None and bool(ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||
self.mads_screen_button.set_visible(has_vehicle_bus)
|
||||
|
||||
mads_screen_button_desc = (
|
||||
f"{tr('Use a multi-finger press on the infotainment screen to toggle MADS.')} " +
|
||||
f"{tr('This allows the use of full MADS functionality when enabled.')}<br><br>" +
|
||||
f"{tr('Selecting a higher finger count may reduce accidental activations.')}<br><br>" +
|
||||
f"<b>{tr('Note: Setting this to Off will reset your MADS settings to default.')}</b>"
|
||||
)
|
||||
if not ui_state.is_offroad():
|
||||
mads_screen_button_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to change.")
|
||||
mads_screen_button_desc = f"<b>{mads_screen_button_disabled_msg}</b><br><br>{mads_screen_button_desc}"
|
||||
self.mads_screen_button.set_description(mads_screen_button_desc)
|
||||
self.mads_screen_button.action_item.set_enabled(ui_state.is_offroad())
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
|
||||
|
||||
class MiciHomeLayoutSP(MiciHomeLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||
@@ -4,6 +4,7 @@ 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.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.cereal import custom
|
||||
@@ -47,8 +48,10 @@ class CurrentModelInfo(Widget):
|
||||
self.info_text.render()
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_info = CurrentModelInfo()
|
||||
@@ -82,10 +85,12 @@ class ModelsLayoutMici(NavScroller):
|
||||
|
||||
return folders
|
||||
|
||||
def _push_selection_view(self, items):
|
||||
scroller = NavScroller()
|
||||
scroller._scroller.add_widgets(items)
|
||||
gui_app.push_widget(scroller)
|
||||
def _show_selection_view(self, items, back_callback: Callable):
|
||||
self._scroller._items = items
|
||||
for item in items:
|
||||
item.set_touch_valid_callback(lambda: self._scroller.scroll_panel.is_touch_valid() and self._scroller.enabled)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
@@ -107,18 +112,15 @@ class ModelsLayoutMici(NavScroller):
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
self._push_selection_view(folder_buttons)
|
||||
|
||||
def _pop_to_main(self):
|
||||
gui_app.pop_widgets_to(self)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
|
||||
def _select_model(self, bundle):
|
||||
ui_state.params.put("ModelManager_DownloadIndex", bundle.index)
|
||||
self._pop_to_main()
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_default(self):
|
||||
ui_state.params.remove("ModelManager_ActiveBundle")
|
||||
self._pop_to_main()
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
@@ -133,7 +135,13 @@ class ModelsLayoutMici(NavScroller):
|
||||
btn = BigButton(txt)
|
||||
btn.set_click_callback(lambda b=bundle: self._select_model(b))
|
||||
btns.append(btn)
|
||||
self._push_selection_view(btns)
|
||||
self._show_selection_view(btns, self._show_folders)
|
||||
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment]
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
|
||||
@@ -32,11 +32,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici()
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
|
||||
models_panel = ModelsLayoutMici()
|
||||
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
||||
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
@@ -53,8 +54,9 @@ class SunnylinkInfo(Widget):
|
||||
self.sponsor_text.render()
|
||||
|
||||
class SunnylinkLayoutMici(NavScroller):
|
||||
def __init__(self):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self._restore_in_progress = False
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
@@ -338,11 +338,8 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
|
||||
|
||||
settings_cases: Cases = [
|
||||
lambda: scroll_through_cases(toggle_cases),
|
||||
None, # sunnylink (just open and close)
|
||||
None, # models (just open and close)
|
||||
lambda: scroll_through_cases(network_cases),
|
||||
lambda: scroll_through_cases(device_cases),
|
||||
lambda: script.wait(WAIT_SHORT), # software
|
||||
lambda: script.wait(WAIT_SHORT), # pairing
|
||||
lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up)
|
||||
lambda: scroll_through_cases(developer_cases),
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
|
||||
from opendbc.car import structs
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
|
||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
|
||||
|
||||
MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla")
|
||||
@@ -21,20 +21,17 @@ class MadsSteeringModeOnBrake:
|
||||
DISENGAGE = 2
|
||||
|
||||
|
||||
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params) -> bool:
|
||||
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> bool:
|
||||
if CP.brand == 'rivian':
|
||||
return True
|
||||
if CP.brand == 'tesla':
|
||||
if not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
|
||||
return True
|
||||
screen_button = int(params.get("TeslaMadsScreenButton", return_default=True))
|
||||
return screen_button == MadsScreenButtonType.OFF
|
||||
return not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
|
||||
if get_mads_limited_brands(CP, CP_SP, params):
|
||||
if get_mads_limited_brands(CP, CP_SP):
|
||||
return MadsSteeringModeOnBrake.DISENGAGE
|
||||
|
||||
return params.get("MadsSteeringMode", return_default=True)
|
||||
@@ -66,7 +63,7 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p
|
||||
# MADS is currently partially supported for these platforms due to lack of consistent states to engage controls
|
||||
# Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms
|
||||
# TODO-SP: To enable MADS full support for Rivian and most Tesla, identify consistent signals for MADS toggling
|
||||
mads_partial_support = get_mads_limited_brands(CP, CP_SP, params)
|
||||
mads_partial_support = get_mads_limited_brands(CP, CP_SP)
|
||||
if mads_partial_support:
|
||||
params.put("MadsSteeringMode", 2, block=True)
|
||||
params.put_bool("MadsUnifiedEngagementMode", True, block=True)
|
||||
|
||||
@@ -13,7 +13,7 @@ from openpilot.selfdrive.selfdrived.events import Events
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
|
||||
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
|
||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
|
||||
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
EventName = log.OnroadEvent.EventName
|
||||
@@ -38,12 +38,6 @@ def make_panda_state(mocker, controls_allowed_lateral=True):
|
||||
return ps
|
||||
|
||||
|
||||
def make_params_mock(mocker, values):
|
||||
params = mocker.MagicMock()
|
||||
params.get = mocker.MagicMock(side_effect=lambda k, **kwargs: values[k])
|
||||
return params
|
||||
|
||||
|
||||
def make_mads(mocker, steering_mode):
|
||||
sd = mocker.MagicMock()
|
||||
sd.CP = structs.CarParams()
|
||||
@@ -229,27 +223,15 @@ class TestBrandSteeringModeRestrictions:
|
||||
params = mocker.MagicMock()
|
||||
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
|
||||
|
||||
@pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER,
|
||||
MadsScreenButtonType.FOUR_FINGER,
|
||||
MadsScreenButtonType.FIVE_FINGER])
|
||||
def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button):
|
||||
def test_tesla_with_vehicle_bus_uses_param(self, mocker):
|
||||
CP = structs.CarParams()
|
||||
CP.brand = "tesla"
|
||||
CP_SP = structs.CarParamsSP()
|
||||
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
|
||||
params = make_params_mock(mocker, {"TeslaMadsScreenButton": screen_button,
|
||||
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
|
||||
params = mocker.MagicMock()
|
||||
params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE)
|
||||
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE
|
||||
|
||||
def test_tesla_with_vehicle_bus_screen_button_off_forced_to_disengage(self, mocker):
|
||||
CP = structs.CarParams()
|
||||
CP.brand = "tesla"
|
||||
CP_SP = structs.CarParamsSP()
|
||||
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
|
||||
params = make_params_mock(mocker, {"TeslaMadsScreenButton": MadsScreenButtonType.OFF,
|
||||
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
|
||||
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
|
||||
|
||||
@pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"])
|
||||
def test_other_brands_use_param(self, mocker, brand):
|
||||
CP = structs.CarParams()
|
||||
|
||||
@@ -82,3 +82,18 @@ if os.path.isfile(supercombo_onnx):
|
||||
compile_combined('supercombo',
|
||||
f'--supercombo-onnx {supercombo_onnx}',
|
||||
'driving_combined_supercombo_tinygrad.pkl')
|
||||
|
||||
if PC:
|
||||
inputs = tinygrad_files + [File(Dir("#openpilot/sunnypilot/modeld_v2").File("install_models_pc.py").abspath)]
|
||||
outputs = []
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#openpilot/sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
outputs.append(File(f"models/{model_name}_metadata.pkl"))
|
||||
if outputs:
|
||||
lenv.Command(outputs, inputs, cmd)
|
||||
|
||||
|
||||
@@ -57,26 +57,7 @@ def derive_frame_skip(vision_input_shapes: dict, policy_input_shapes: dict) -> i
|
||||
return 1 if not features_buffer or features_buffer[1] >= 99 else 4
|
||||
|
||||
|
||||
def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tuple[dict, list[int]]:
|
||||
desire_key = _detect_desire_key(input_shapes)
|
||||
shapes = {}
|
||||
if desire_key:
|
||||
shapes['desire'] = (input_shapes[desire_key][2],)
|
||||
|
||||
if is_supercombo and 'features_buffer' in input_shapes:
|
||||
fb = input_shapes['features_buffer']
|
||||
shapes['prev_feat'] = (fb[0], fb[2])
|
||||
|
||||
for key, shape in input_shapes.items():
|
||||
if key not in (desire_key, 'features_buffer') and 'img' not in key:
|
||||
shapes[key] = tuple(shape)
|
||||
|
||||
sizes = [int(np.prod(size)) for size in shapes.values()]
|
||||
return shapes, sizes
|
||||
|
||||
|
||||
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT,
|
||||
is_supercombo: bool = False, use_packed: bool = True) -> tuple[dict, dict]:
|
||||
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
||||
road_key, _ = _detect_vision_keys(input_shapes)
|
||||
if not road_key:
|
||||
raise ValueError("Vision road key missing from input shapes.")
|
||||
@@ -92,69 +73,37 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D
|
||||
desire_shape = input_shapes[desire_key]
|
||||
features_buffer = input_shapes.get('features_buffer')
|
||||
|
||||
if use_packed: # remove packed detection block after all models are recompiled
|
||||
npy_arrays = {
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32)
|
||||
}
|
||||
npy_arrays = {
|
||||
'desire': np.zeros(desire_shape[2], dtype=np.float32),
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32)
|
||||
}
|
||||
|
||||
shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
|
||||
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||
for key, shape in input_shapes.items():
|
||||
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
|
||||
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else []
|
||||
split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else []
|
||||
for (k, s), v in zip(shapes.items(), split_views, strict=True):
|
||||
npy_arrays[k] = v.reshape(s)
|
||||
queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
}
|
||||
|
||||
queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize(),
|
||||
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
|
||||
}
|
||||
|
||||
if features_buffer:
|
||||
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
|
||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
|
||||
else:
|
||||
# TODO-SP: Remove legacy queuing fallback else block after all models are recompiled
|
||||
npy_arrays = {
|
||||
'desire': np.zeros(desire_shape[2], dtype=np.float32),
|
||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||
'big_tfm': np.zeros((3, 3), dtype=np.float32)
|
||||
}
|
||||
|
||||
for key, shape in input_shapes.items():
|
||||
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
|
||||
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
}
|
||||
|
||||
if features_buffer:
|
||||
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
|
||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
|
||||
if features_buffer:
|
||||
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
|
||||
dtype=np.float32), device=device).contiguous().realize()
|
||||
|
||||
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
|
||||
return queues, npy_arrays
|
||||
|
||||
|
||||
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict,
|
||||
frame_skip: int, device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
|
||||
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False, use_packed=use_packed)
|
||||
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
||||
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device)
|
||||
|
||||
|
||||
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int,
|
||||
device: str = Device.DEFAULT, use_packed: bool = True) -> tuple[dict, dict]:
|
||||
return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True, use_packed=use_packed)
|
||||
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
|
||||
return generate_queues_and_npy(input_shapes, frame_skip, device)
|
||||
|
||||
|
||||
def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int],
|
||||
@@ -169,17 +118,22 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode
|
||||
if not desire_key or not road_key or not wide_key:
|
||||
raise ValueError("Missing required vision or desire keys in input shapes.")
|
||||
|
||||
is_supercombo = vision_runner is None
|
||||
npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
|
||||
extra_keys = [key for key in input_shapes if key not in (desire_key, 'features_buffer', 'traffic_convention') and 'img' not in key]
|
||||
|
||||
def runner(img_q, big_img_q, feat_q, packed_npy_inputs, frame, big_frame, tfm, big_tfm, **kwargs):
|
||||
def runner(img_q, big_img_q, feat_q, frame, big_frame, tfm, big_tfm, **kwargs):
|
||||
desire_q = kwargs['desire_q']
|
||||
desire = kwargs['desire']
|
||||
traffic_convention = kwargs.get('traffic_convention')
|
||||
|
||||
packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT)
|
||||
tfm_dev = tfm.to(Device.DEFAULT)
|
||||
big_tfm_dev = big_tfm.to(Device.DEFAULT)
|
||||
npys = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT), desire.to(Device.DEFAULT)]
|
||||
if traffic_convention is not None:
|
||||
npys.append(traffic_convention.to(Device.DEFAULT))
|
||||
|
||||
Tensor.realize(packed_npy_inputs_dev, tfm_dev, big_tfm_dev)
|
||||
extra_tensors = {key: kwargs[key].to(Device.DEFAULT) for key in extra_keys if key in kwargs}
|
||||
Tensor.realize(*npys, *extra_tensors.values())
|
||||
|
||||
tfm_dev, big_tfm_dev, desire_dev = npys[:3]
|
||||
traffic_conv_dev = npys[3] if traffic_convention is not None else None
|
||||
|
||||
img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn).realize()
|
||||
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn).realize()
|
||||
@@ -187,37 +141,22 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode
|
||||
if prepare_only:
|
||||
return img, big_img
|
||||
|
||||
unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
|
||||
unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
|
||||
|
||||
desire_dev = unpacked_dict['desire']
|
||||
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize()
|
||||
inputs = {desire_key: desire_buf, **extra_tensors}
|
||||
|
||||
inputs = {desire_key: desire_buf}
|
||||
for key, tensor_val in unpacked_dict.items():
|
||||
if key not in ('desire', 'prev_feat'):
|
||||
inputs[key] = tensor_val
|
||||
|
||||
if 'prev_feat' in unpacked_dict:
|
||||
prev_feat_dev = unpacked_dict['prev_feat']
|
||||
inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).realize()
|
||||
if traffic_conv_dev is not None:
|
||||
inputs['traffic_convention'] = traffic_conv_dev
|
||||
|
||||
if vision_runner:
|
||||
vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize()
|
||||
if 'features_buffer' not in inputs:
|
||||
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
||||
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
||||
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners]
|
||||
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
|
||||
|
||||
inputs.update({road_key: img, wide_key: big_img})
|
||||
if 'features_buffer' not in inputs:
|
||||
inputs['features_buffer'] = sample_skip_fn(feat_q)
|
||||
|
||||
inputs.update({road_key: img, wide_key: big_img, 'features_buffer': sample_skip_fn(feat_q)})
|
||||
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize()
|
||||
if 'features_buffer' not in inputs and features_slice is not None:
|
||||
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
||||
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
|
||||
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
|
||||
return policy_out
|
||||
|
||||
return runner
|
||||
@@ -235,17 +174,16 @@ def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_onl
|
||||
features_slice = feat_meta['output_slices']['hidden_state']
|
||||
WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT
|
||||
|
||||
is_supercombo = vision_runner is None
|
||||
run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only)
|
||||
run_jit = TinyJit(run_func, prune=True)
|
||||
queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT, is_supercombo=is_supercombo)
|
||||
queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT)
|
||||
|
||||
for i in range(3):
|
||||
rng = np.random.default_rng(42 + i)
|
||||
np.random.seed(42 + i)
|
||||
frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
|
||||
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
|
||||
for arr in npy_arrays.values():
|
||||
arr[:] = rng.standard_normal(arr.shape).astype(arr.dtype)
|
||||
arr[:] = np.random.randn(*arr.shape).astype(arr.dtype)
|
||||
|
||||
Device.default.synchronize()
|
||||
start_time = time.perf_counter()
|
||||
@@ -254,7 +192,6 @@ def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_onl
|
||||
Device.default.synchronize()
|
||||
print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms")
|
||||
|
||||
# TODO-SP: switch to dump_oob/load_oob on next full recompile of all models
|
||||
return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit
|
||||
|
||||
|
||||
@@ -352,7 +289,6 @@ if __name__ == "__main__":
|
||||
vision_runner, policy_runners, output_data['metadata']))
|
||||
|
||||
with open(args.output, "wb") as file:
|
||||
# TODO-SP: switch to dump_oob from openpilot/selfdrive/helpers on next full recompile of all models
|
||||
pickle.dump(output_data, file)
|
||||
|
||||
pkl_size = os.path.getsize(args.output)
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import shutil
|
||||
import pickle
|
||||
import codecs
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name
|
||||
|
||||
|
||||
def generate_metadata_pkl(model_path, output_path):
|
||||
try:
|
||||
model = MetadataOnnxPBParser(model_path).parse()
|
||||
output_slices = get_metadata_value_by_name(model, 'output_slices')
|
||||
if not output_slices:
|
||||
return False
|
||||
metadata = {
|
||||
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
|
||||
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
|
||||
'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]),
|
||||
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
|
||||
}
|
||||
with open(output_path, 'wb') as f:
|
||||
pickle.dump(metadata, f)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def install_models(model_dir):
|
||||
model_dir = Path(model_dir)
|
||||
models = ["driving_off_policy", "driving_on_policy", "driving_vision"]
|
||||
found_models = []
|
||||
|
||||
for model in models:
|
||||
if (model_dir / f"{model}.onnx").exists():
|
||||
found_models.append(model)
|
||||
|
||||
if not found_models:
|
||||
return
|
||||
|
||||
try:
|
||||
custom_name = input(f"Found models ({', '.join(found_models)}). Enter model short name (e.g. wmiv4): ").strip()
|
||||
except EOFError:
|
||||
return
|
||||
|
||||
if not custom_name:
|
||||
print("No name provided, skipping installation.")
|
||||
return
|
||||
|
||||
dest_dir = Path(Paths.model_root())
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for model in found_models:
|
||||
onnx_path = model_dir / f"{model}.onnx"
|
||||
tinygrad_pkl = model_dir / f"{model}_tinygrad.pkl"
|
||||
metadata_pkl = model_dir / f"{model}_metadata.pkl"
|
||||
|
||||
if not metadata_pkl.exists():
|
||||
generate_metadata_pkl(onnx_path, metadata_pkl)
|
||||
|
||||
dest_tinygrad = dest_dir / f"{model}_{custom_name}_tinygrad.pkl"
|
||||
dest_metadata = dest_dir / f"{model}_{custom_name}_metadata.pkl"
|
||||
|
||||
if tinygrad_pkl.exists():
|
||||
shutil.move(str(tinygrad_pkl), str(dest_tinygrad))
|
||||
if metadata_pkl.exists():
|
||||
shutil.move(str(metadata_pkl), str(dest_metadata))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: install_models_pc.py <model_dir>")
|
||||
sys.exit(1)
|
||||
install_models(sys.argv[1])
|
||||
@@ -24,11 +24,6 @@ from setproctitle import setproctitle
|
||||
from openpilot.cereal.messaging import PubMaster, SubMaster
|
||||
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
from tinygrad.device import Device
|
||||
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
@@ -36,7 +31,6 @@ from openpilot.common.realtime import config_realtime_process, DT_MDL
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.system import sentry
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
|
||||
|
||||
@@ -44,7 +38,6 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p
|
||||
from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
@@ -107,8 +100,14 @@ class ModelState(ModelStateBase):
|
||||
self._init_combined(pkl_path, cam_w, cam_h, model_bundle)
|
||||
|
||||
def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
|
||||
from tinygrad.tensor import Tensor
|
||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||
from tinygrad.device import Device
|
||||
|
||||
from openpilot.common.file_chunker import open_file_chunked
|
||||
|
||||
cloudlog.warning(f"loading combined pkl: {pkl_path}")
|
||||
# TODO-SP: switch to load_oob from openpilot/selfdrive/helpers on next full recompile of all models
|
||||
jits = pickle.load(open_file_chunked(pkl_path))
|
||||
|
||||
self.DEV = Device.DEFAULT
|
||||
@@ -116,28 +115,16 @@ class ModelState(ModelStateBase):
|
||||
self.QUEUE_DEV = self.DEV
|
||||
|
||||
metadata = jits['metadata']
|
||||
|
||||
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
|
||||
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
|
||||
|
||||
# TODO-SP: Remove legacy use_packed detection block after all models are recompiled
|
||||
captured = getattr(self._run_policy, 'captured', None)
|
||||
if captured is not None:
|
||||
use_packed = 'packed_npy_inputs' in getattr(captured, 'expected_names', [])
|
||||
else:
|
||||
use_packed = True
|
||||
|
||||
if 'model' in metadata:
|
||||
model_metadata = metadata['model']
|
||||
self.vision_output_slices = model_metadata['output_slices']
|
||||
self.policy_output_slices = {}
|
||||
self._policy_slices_list = []
|
||||
self._combined_model_type = 'supercombo'
|
||||
self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key]
|
||||
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
|
||||
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
|
||||
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'],
|
||||
frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
|
||||
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.QUEUE_DEV)
|
||||
else:
|
||||
vision_metadata = metadata['vision']
|
||||
policy_keys = [k for k in metadata if k != 'vision']
|
||||
@@ -155,13 +142,16 @@ class ModelState(ModelStateBase):
|
||||
policy_input_shapes = first_policy_metadata['input_shapes']
|
||||
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
|
||||
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
|
||||
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes,
|
||||
frame_skip, device=self.QUEUE_DEV, use_packed=use_packed)
|
||||
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.QUEUE_DEV)
|
||||
|
||||
self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire'))
|
||||
self._road_key = next(key for key in self._vision_input_names if 'big' not in key)
|
||||
self._wide_key = next(key for key in self._vision_input_names if 'big' in key)
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||
self.parser = SplitParser() if self._combined_model_type != 'supercombo' else CombinedParser()
|
||||
|
||||
is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy')
|
||||
if is_20hz:
|
||||
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
|
||||
@@ -170,19 +160,14 @@ class ModelState(ModelStateBase):
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||
self.constants = ModelConstants()
|
||||
|
||||
if self._combined_model_type != 'supercombo':
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||
self.parser = SplitParser()
|
||||
else:
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||
self.parser = CombinedParser()
|
||||
|
||||
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
|
||||
self.full_frames: dict = {}
|
||||
self._blob_cache: dict = {}
|
||||
nv12_info = get_nv12_info(cam_w, cam_h)
|
||||
self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info)
|
||||
|
||||
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
|
||||
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
|
||||
yuv_size = self.frame_buf_params[self._road_key][3]
|
||||
self._warp_enqueue(
|
||||
**self.input_queues,
|
||||
@@ -204,6 +189,8 @@ class ModelState(ModelStateBase):
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
from tinygrad.tensor import Tensor
|
||||
|
||||
for key in bufs.keys():
|
||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||
yuv_size = self.frame_buf_params[key][3]
|
||||
@@ -235,16 +222,11 @@ class ModelState(ModelStateBase):
|
||||
model_output = raw_outputs.numpy().flatten()
|
||||
sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
|
||||
outputs = self.parser.parse_outputs(sliced)
|
||||
if 'prev_feat' in self.numpy_inputs:
|
||||
self.numpy_inputs['prev_feat'][:] = model_output[self.vision_output_slices['hidden_state']]
|
||||
else:
|
||||
vision_output = raw_outputs[0].numpy().flatten()
|
||||
vision_sliced = {k: vision_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
|
||||
outputs = self.parser.parse_vision_outputs(vision_sliced)
|
||||
|
||||
if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices:
|
||||
self.numpy_inputs['prev_feat'][:] = vision_output[self.vision_output_slices['hidden_state']]
|
||||
|
||||
for i, policy_slices in enumerate(self._policy_slices_list):
|
||||
policy_output = raw_outputs[i + 1].numpy().flatten()
|
||||
policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()}
|
||||
@@ -265,11 +247,6 @@ class ModelState(ModelStateBase):
|
||||
buf[0, :-1] = buf[0, 1:]
|
||||
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
|
||||
|
||||
# TODO-SP: This is a hack to prevent GPU corruption by calculating in CPU space, it can be removed on next recompile
|
||||
if 'prev_feat' not in self.numpy_inputs and 'feat_q' in self.input_queues:
|
||||
feat_val = self.input_queues['feat_q'].numpy()
|
||||
self.input_queues['feat_q'].assign(feat_val).realize()
|
||||
|
||||
return outputs
|
||||
|
||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
@@ -278,6 +255,7 @@ class ModelState(ModelStateBase):
|
||||
plan = model_output['plan'][0]
|
||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
action_t=long_action_t)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
|
||||
curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0]
|
||||
if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan)
|
||||
@@ -287,8 +265,6 @@ class ModelState(ModelStateBase):
|
||||
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
||||
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
|
||||
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
|
||||
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
|
||||
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import numpy as np
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||
|
||||
|
||||
def safe_exp(x, out=None):
|
||||
# -11 is around 10**14, more causes float16 overflow
|
||||
return np.exp(np.clip(x, -np.inf, 11), out=out)
|
||||
|
||||
|
||||
def sigmoid(x):
|
||||
return 1. / (1. + safe_exp(-x))
|
||||
|
||||
|
||||
def softmax(x, axis=-1):
|
||||
x -= np.max(x, axis=axis, keepdims=True)
|
||||
if x.dtype == np.float32 or x.dtype == np.float64:
|
||||
@@ -20,19 +17,6 @@ def softmax(x, axis=-1):
|
||||
x /= np.sum(x, axis=axis, keepdims=True)
|
||||
return x
|
||||
|
||||
|
||||
def _infer_mhp(slice_size: int, prod_out_shape: int, max_in_n: int = 16, max_out_n: int = 6) -> tuple[int, int]:
|
||||
for out_n in range(max_out_n + 1):
|
||||
per = 2 * prod_out_shape + out_n
|
||||
if per <= 0:
|
||||
continue
|
||||
if slice_size % per == 0:
|
||||
in_n = slice_size // per
|
||||
if 1 <= in_n <= max_in_n:
|
||||
return in_n, out_n
|
||||
return 1, 0 # single hypothesis, no weights — matches a non-MDN output
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, ignore_missing=False):
|
||||
self.ignore_missing = ignore_missing
|
||||
@@ -56,22 +40,17 @@ class Parser:
|
||||
raw = outs[name]
|
||||
outs[name] = sigmoid(raw)
|
||||
|
||||
def parse_mdn(self, name, outs, out_shape, in_N=0, out_N=0):
|
||||
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None):
|
||||
if self.check_missing(outs, name):
|
||||
return
|
||||
raw = outs[name]
|
||||
|
||||
if in_N == 0 and out_N == 0:
|
||||
prod = int(np.prod(out_shape))
|
||||
in_N, out_N = _infer_mhp(raw.shape[1], prod)
|
||||
|
||||
raw = raw.reshape((raw.shape[0], in_N, -1))
|
||||
raw = raw.reshape((raw.shape[0], max(in_N, 1), -1))
|
||||
|
||||
n_values = (raw.shape[2] - out_N)//2
|
||||
pred_mu = raw[:,:,:n_values]
|
||||
pred_std = safe_exp(raw[:,:,n_values: 2*n_values])
|
||||
|
||||
if in_N > 1 and out_N > 0:
|
||||
if in_N > 1:
|
||||
weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype)
|
||||
for i in range(out_N):
|
||||
weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1)
|
||||
@@ -82,6 +61,7 @@ class Parser:
|
||||
weights[fidx] = weights[fidx][idxs]
|
||||
pred_mu[fidx] = pred_mu[fidx][idxs]
|
||||
pred_std[fidx] = pred_std[fidx][idxs]
|
||||
assert out_shape is not None
|
||||
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
|
||||
outs[name + '_weights'] = weights
|
||||
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
|
||||
@@ -94,43 +74,37 @@ class Parser:
|
||||
idxs = np.argsort(weights[fidx,:,hidx])[::-1]
|
||||
pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]]
|
||||
pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]]
|
||||
elif in_N > 1 and out_N == 0:
|
||||
# MHP without weights: keep every hypothesis intact, surface them as
|
||||
# ``*_hypotheses`` and propagate the full multi-hypothesis tensor.
|
||||
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
|
||||
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
|
||||
outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape)
|
||||
pred_mu_final = pred_mu
|
||||
pred_std_final = pred_std
|
||||
else:
|
||||
pred_mu_final = pred_mu
|
||||
pred_std_final = pred_std
|
||||
|
||||
if out_N > 1 or (in_N > 1 and out_N == 0):
|
||||
n_selections = out_N if out_N > 1 else in_N
|
||||
final_shape = tuple([raw.shape[0], n_selections] + list(out_shape))
|
||||
if out_N > 1:
|
||||
assert out_shape is not None
|
||||
final_shape = tuple([raw.shape[0], out_N] + list(out_shape))
|
||||
else:
|
||||
assert out_shape is not None
|
||||
final_shape = tuple([raw.shape[0],] + list(out_shape))
|
||||
outs[name] = pred_mu_final.reshape(final_shape)
|
||||
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
|
||||
|
||||
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
# supercombo (4955 / 102) and newer variants (e.g. 990 / 144).
|
||||
self.parse_mdn('plan', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, out_shape=(ModelConstants.NUM_ROAD_EDGES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
if 'sim_pose' in outs:
|
||||
self.parse_mdn('sim_pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('lead', outs, out_shape=(ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH))
|
||||
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
|
||||
if 'lat_planner_solution' in outs:
|
||||
self.parse_mdn('lat_planner_solution', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
|
||||
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
|
||||
if 'desired_curvature' in outs:
|
||||
self.parse_mdn('desired_curvature', outs, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
|
||||
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
|
||||
for k in ['lead_prob', 'lane_lines_prob', 'meta']:
|
||||
self.parse_binary_crossentropy(k, outs)
|
||||
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN, ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
return outs
|
||||
|
||||
@@ -123,7 +123,7 @@ class Parser:
|
||||
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,))
|
||||
if 'lane_lines' in outs:
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0,
|
||||
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
||||
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
||||
if 'lane_lines_prob' in outs:
|
||||
self.parse_binary_crossentropy('lane_lines_prob', outs)
|
||||
if 'lead_prob' in outs:
|
||||
@@ -134,7 +134,7 @@ class Parser:
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
if 'road_edges' in outs:
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0,
|
||||
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
||||
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
|
||||
if 'sim_pose' in outs:
|
||||
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
|
||||
if 'action' in outs:
|
||||
|
||||
@@ -67,12 +67,16 @@ class TestStockEquivalence:
|
||||
state = model_state_factory(ARCHETYPES['vision_policy_split'])
|
||||
|
||||
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
|
||||
# action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below
|
||||
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
|
||||
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
|
||||
|
||||
assert set(state.input_queues.keys()) == set(stock_queues.keys())
|
||||
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
|
||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
|
||||
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue)
|
||||
skip_keys = {'action_t'}
|
||||
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
|
||||
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
|
||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
|
||||
|
||||
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||
|
||||
@@ -64,6 +64,8 @@ class ModelParser:
|
||||
|
||||
model.type = model_data.get("type")
|
||||
model.artifact = ModelParser._parse_artifact(model_data.get("artifact", {}))
|
||||
if metadata := model_data.get("metadata"):
|
||||
model.metadata = ModelParser._parse_artifact(metadata)
|
||||
return model
|
||||
|
||||
@staticmethod
|
||||
@@ -209,3 +211,5 @@ if __name__ == "__main__":
|
||||
# Print metadata details
|
||||
if model.artifact.chunks:
|
||||
print(f"Contains {len(model.artifact.chunks)} chunks.")
|
||||
if hasattr(model, 'metadata') and model.metadata and model.metadata.fileName:
|
||||
print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}")
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
|
||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||
REQUIRED_JSON_VERSION = 16
|
||||
REQUIRED_JSON_VERSION = 15
|
||||
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
||||
@@ -56,20 +56,12 @@ def is_bundle_version_compatible(bundle: dict) -> bool:
|
||||
|
||||
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
|
||||
artifacts = []
|
||||
from openpilot.common.file_chunker import get_chunk_name
|
||||
for model in getattr(bundle, 'models', []) or []:
|
||||
for artifact in (getattr(model, 'artifact', None),):
|
||||
if artifact and getattr(artifact, 'fileName', None):
|
||||
if len(artifact.chunks) > 0:
|
||||
for i, chunk in enumerate(artifact.chunks):
|
||||
chunk_name = get_chunk_name(artifact.fileName, i, len(artifact.chunks))
|
||||
if getattr(chunk, 'sha256', None):
|
||||
artifacts.append((chunk_name, chunk.sha256))
|
||||
else:
|
||||
if getattr(artifact, 'downloadUri', None):
|
||||
sha256 = getattr(artifact.downloadUri, 'sha256', None)
|
||||
if sha256:
|
||||
artifacts.append((artifact.fileName, sha256))
|
||||
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)):
|
||||
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None):
|
||||
sha256 = getattr(artifact.downloadUri, 'sha256', None)
|
||||
if sha256:
|
||||
artifacts.append((artifact.fileName, sha256))
|
||||
return artifacts
|
||||
|
||||
|
||||
@@ -164,7 +156,8 @@ def _get_model():
|
||||
|
||||
|
||||
def load_metadata():
|
||||
metadata_path = METADATA_PATH
|
||||
model = _get_model()
|
||||
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
|
||||
|
||||
with open(metadata_path, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
|
||||
@@ -38,11 +38,11 @@ class ModelManagerSP:
|
||||
if not self.selected_bundle:
|
||||
return
|
||||
for model in self.selected_bundle.models:
|
||||
artifact = model.artifact
|
||||
if artifact is not source_artifact and artifact.fileName == source_artifact.fileName:
|
||||
artifact.downloadProgress.status = source_artifact.downloadProgress.status
|
||||
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
|
||||
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
|
||||
for artifact in (model.artifact, model.metadata):
|
||||
if artifact is not source_artifact and artifact.fileName == source_artifact.fileName:
|
||||
artifact.downloadProgress.status = source_artifact.downloadProgress.status
|
||||
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
|
||||
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
|
||||
|
||||
def _calculate_eta(self, filename: str, progress: float) -> int:
|
||||
"""Calculate ETA based on elapsed time and current progress"""
|
||||
@@ -136,22 +136,7 @@ class ModelManagerSP:
|
||||
full_path = os.path.join(destination_path, filename)
|
||||
|
||||
try:
|
||||
is_cached = False
|
||||
if len(artifact.chunks) > 0:
|
||||
from openpilot.common.file_chunker import get_chunk_name
|
||||
chunks_valid = True
|
||||
for i, chunk in enumerate(artifact.chunks):
|
||||
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
|
||||
if not await verify_file(chunk_path, chunk.sha256):
|
||||
chunks_valid = False
|
||||
break
|
||||
if chunks_valid and len(artifact.chunks) > 0:
|
||||
is_cached = True
|
||||
else:
|
||||
if await verify_file(full_path, expected_hash):
|
||||
is_cached = True
|
||||
|
||||
if is_cached:
|
||||
if await verify_file(full_path, expected_hash):
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
@@ -161,15 +146,11 @@ class ModelManagerSP:
|
||||
|
||||
if len(artifact.chunks) > 0:
|
||||
await self._download_chunked(url, full_path, artifact)
|
||||
from openpilot.common.file_chunker import get_chunk_name
|
||||
for i, chunk in enumerate(artifact.chunks):
|
||||
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
|
||||
if not await verify_file(chunk_path, chunk.sha256):
|
||||
raise ValueError(f"Hash validation failed for chunk {i+1} of {filename}")
|
||||
else:
|
||||
await self._download_file(url, full_path, artifact)
|
||||
if not await verify_file(full_path, expected_hash):
|
||||
raise ValueError(f"Hash validation failed for {filename}")
|
||||
|
||||
if not await verify_file(full_path, expected_hash):
|
||||
raise ValueError(f"Hash validation failed for {filename}")
|
||||
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
artifact.downloadProgress.progress = 100
|
||||
@@ -217,16 +198,16 @@ class ModelManagerSP:
|
||||
try:
|
||||
seen_artifacts: set[str] = set()
|
||||
for model in self.selected_bundle.models:
|
||||
artifact = model.artifact
|
||||
if not artifact.fileName:
|
||||
continue
|
||||
if artifact.fileName in seen_artifacts:
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
else:
|
||||
seen_artifacts.add(artifact.fileName)
|
||||
await self._process_artifact(artifact, destination_path)
|
||||
for artifact in (model.metadata, model.artifact):
|
||||
if not artifact.fileName:
|
||||
continue
|
||||
if artifact.fileName in seen_artifacts:
|
||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||
artifact.downloadProgress.progress = 100
|
||||
artifact.downloadProgress.eta = 0
|
||||
else:
|
||||
seen_artifacts.add(artifact.fileName)
|
||||
await self._process_artifact(artifact, destination_path)
|
||||
|
||||
self.active_bundle = self.selected_bundle
|
||||
self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
@@ -287,6 +268,8 @@ class ModelManagerSP:
|
||||
for model in self.active_bundle.models:
|
||||
if hasattr(model, 'artifact') and model.artifact.fileName:
|
||||
active_files.append(model.artifact.fileName)
|
||||
if hasattr(model, 'metadata') and model.metadata.fileName:
|
||||
active_files.append(model.metadata.fileName)
|
||||
|
||||
# Remove all files except active ones (including their chunk files)
|
||||
model_dir = Paths.model_root()
|
||||
|
||||
@@ -123,7 +123,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
# tesla
|
||||
keys.extend([
|
||||
"TeslaCoopSteering",
|
||||
"TeslaMadsScreenButton",
|
||||
])
|
||||
|
||||
# toyota
|
||||
|
||||
@@ -82,7 +82,7 @@ class LatControlTorque(LatControl):
|
||||
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
||||
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
||||
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / max(lat_delay, self.dt)
|
||||
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
|
||||
|
||||
measurement = measured_curvature * CS.vEgo ** 2
|
||||
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
|
||||
|
||||
@@ -36,13 +36,12 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
super().__init__(lac_torque, CP, CP_SP, CI)
|
||||
self.params = Params()
|
||||
self.enabled = self.params.get_bool("NeuralNetworkLateralControl")
|
||||
model_path = CP_SP.neuralNetworkLateralControl.model.path
|
||||
self.has_nn_model = model_path not in (MOCK_MODEL_PATH, '')
|
||||
self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH
|
||||
|
||||
# NN model takes current v_ego, lateral_accel, lat accel/jerk error, roll, and past/future/planned data
|
||||
# of lat accel and roll
|
||||
# Past value is computed using previous desired lat accel and observed roll
|
||||
self.model = NNTorqueModel(model_path) if self.has_nn_model else None
|
||||
self.model = NNTorqueModel(CP_SP.neuralNetworkLateralControl.model.path)
|
||||
|
||||
self.pitch = FirstOrderFilter(0.0, 0.5, 0.01)
|
||||
self.pitch_last = 0.0
|
||||
|
||||
@@ -91,7 +91,7 @@ class SpeedLimitAssist:
|
||||
|
||||
self._plus_hold = 0.
|
||||
self._minus_hold = 0.
|
||||
self._release_toggle_prev = 0
|
||||
self._last_carstate_ts = 0.
|
||||
|
||||
# TODO-SP: SLA's own output_a_target for planner
|
||||
# Solution functions mapped to respective states
|
||||
@@ -146,16 +146,16 @@ class SpeedLimitAssist:
|
||||
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||
|
||||
def update_buttons(self, release_toggle: int) -> None:
|
||||
released = self._release_toggle_prev ^ release_toggle
|
||||
self._release_toggle_prev = release_toggle
|
||||
if not released:
|
||||
return
|
||||
def update_car_state(self, CS: car.CarState) -> None:
|
||||
now = time.monotonic()
|
||||
if any((released >> b) & 1 for b in CRUISE_BUTTONS_PLUS):
|
||||
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
||||
if any((released >> b) & 1 for b in CRUISE_BUTTONS_MINUS):
|
||||
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
||||
self._last_carstate_ts = now
|
||||
|
||||
for b in CS.buttonEvents:
|
||||
if not b.pressed:
|
||||
if b.type in CRUISE_BUTTONS_PLUS:
|
||||
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
||||
elif b.type in CRUISE_BUTTONS_MINUS:
|
||||
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
||||
|
||||
def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool:
|
||||
now = time.monotonic()
|
||||
|
||||
+1
-91
@@ -5,14 +5,11 @@ 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.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.cereal import custom
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.rivian.values import CAR as RIVIAN
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.tesla.values import CAR as TESLA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from openpilot.common.constants import CV
|
||||
@@ -24,13 +21,9 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
|
||||
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
|
||||
@@ -283,86 +276,3 @@ class TestSpeedLimitAssist:
|
||||
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
|
||||
elif initial_state in ACTIVE_STATES:
|
||||
assert self.sla.state in ACTIVE_STATES
|
||||
|
||||
|
||||
class TestButtonStateTrackerSLAIntegration:
|
||||
|
||||
def setup_method(self, method):
|
||||
self.tracker = ButtonStateTracker()
|
||||
self.params = Params()
|
||||
self.params.put("IsReleaseSpBranch", True, block=True)
|
||||
self.params.put("SpeedLimitMode", int(Mode.assist), block=True)
|
||||
self.params.put_bool("IsMetric", False, block=True)
|
||||
self.params.put("SpeedLimitOffsetType", 0, block=True)
|
||||
self.params.put("SpeedLimitValueOffset", 0, block=True)
|
||||
|
||||
CarInterface = interfaces[DEFAULT_CAR]
|
||||
CP = CarInterface.get_non_essential_params(DEFAULT_CAR)
|
||||
CP.openpilotLongitudinalControl = True
|
||||
CP_SP = CarInterface.get_non_essential_params_sp(CP, DEFAULT_CAR)
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
|
||||
def _make_cs(self, events=None) -> car.CarState:
|
||||
CS = car.CarState()
|
||||
CS.buttonEvents = events or []
|
||||
return CS
|
||||
|
||||
def _run_ctrl_frames(self, frames: list[car.CarState]) -> None:
|
||||
for cs in frames:
|
||||
self.tracker.update(cs)
|
||||
|
||||
def test_button_confirm_via_tracker(self) -> None:
|
||||
self._run_ctrl_frames([
|
||||
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
|
||||
self._make_cs(),
|
||||
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
|
||||
self._make_cs(),
|
||||
self._make_cs(),
|
||||
])
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
assert self.sla._get_button_release(req_plus=True, req_minus=False)
|
||||
|
||||
def test_rapid_press_release_between_polls(self) -> None:
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
|
||||
self._run_ctrl_frames([
|
||||
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]),
|
||||
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=False)]),
|
||||
self._make_cs(),
|
||||
self._make_cs(),
|
||||
self._make_cs(),
|
||||
])
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
assert self.sla._get_button_release(req_plus=False, req_minus=True)
|
||||
|
||||
def test_multiple_releases_between_polls(self) -> None:
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
|
||||
self._run_ctrl_frames([
|
||||
self._make_cs([
|
||||
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
|
||||
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
|
||||
]),
|
||||
self._make_cs([
|
||||
ButtonEvent(type=ButtonType.accelCruise, pressed=False),
|
||||
ButtonEvent(type=ButtonType.decelCruise, pressed=False),
|
||||
]),
|
||||
])
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
assert self.sla._get_button_release(req_plus=True, req_minus=False)
|
||||
assert self.sla._get_button_release(req_plus=False, req_minus=True)
|
||||
|
||||
def test_no_false_positive_same_toggle(self) -> None:
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
|
||||
assert not self.sla._get_button_release(req_plus=False, req_minus=True)
|
||||
|
||||
def test_button_confirm_expires(self) -> None:
|
||||
self._run_ctrl_frames([
|
||||
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
|
||||
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
|
||||
])
|
||||
self.sla.update_buttons(self.tracker.release_toggle)
|
||||
time.sleep(CRUISE_BUTTON_CONFIRM_HOLD + 0.1)
|
||||
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from opendbc.car import structs
|
||||
|
||||
|
||||
class ButtonStateTracker:
|
||||
def __init__(self) -> None:
|
||||
self.pressed: int = 0
|
||||
self.release_toggle: int = 0
|
||||
|
||||
def update(self, CS: structs.CarState) -> None:
|
||||
for b in CS.buttonEvents:
|
||||
bit = 1 << b.type.raw
|
||||
if b.pressed:
|
||||
self.pressed |= bit
|
||||
else:
|
||||
self.pressed &= ~bit
|
||||
self.release_toggle ^= bit
|
||||
|
||||
def publish(self, ss_sp) -> None:
|
||||
ss_sp.buttonsPressed = self.pressed
|
||||
ss_sp.buttonsReleaseToggle = self.release_toggle
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from opendbc.car.structs import car
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||
|
||||
ButtonEvent = car.CarState.ButtonEvent
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
|
||||
|
||||
class TestButtonStateTracker:
|
||||
def setup_method(self) -> None:
|
||||
self.tracker = ButtonStateTracker()
|
||||
|
||||
def make_cs(self, events: list) -> car.CarState:
|
||||
CS = car.CarState()
|
||||
CS.buttonEvents = events
|
||||
return CS
|
||||
|
||||
def test_initial_state(self) -> None:
|
||||
assert self.tracker.pressed == 0
|
||||
assert self.tracker.release_toggle == 0
|
||||
|
||||
def test_press_sets_bit(self) -> None:
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
|
||||
assert self.tracker.pressed == (1 << ButtonType.accelCruise)
|
||||
assert self.tracker.release_toggle == 0
|
||||
|
||||
def test_release_clears_and_toggles(self) -> None:
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
|
||||
assert self.tracker.pressed == 0
|
||||
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
|
||||
|
||||
def test_multiple_buttons(self) -> None:
|
||||
self.tracker.update(self.make_cs([
|
||||
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
|
||||
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
|
||||
]))
|
||||
assert self.tracker.pressed == (1 << ButtonType.accelCruise) | (1 << ButtonType.decelCruise)
|
||||
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
|
||||
assert self.tracker.pressed == (1 << ButtonType.decelCruise)
|
||||
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
|
||||
|
||||
def test_release_toggle_flips(self) -> None:
|
||||
for _ in range(2):
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=True)]))
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=False)]))
|
||||
assert self.tracker.release_toggle == 0
|
||||
|
||||
def test_publish(self) -> None:
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]))
|
||||
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
|
||||
|
||||
class MockSP:
|
||||
buttonsPressed = 0
|
||||
buttonsReleaseToggle = 0
|
||||
|
||||
sp = MockSP()
|
||||
self.tracker.publish(sp)
|
||||
assert sp.buttonsPressed == self.tracker.pressed
|
||||
assert sp.buttonsReleaseToggle == self.tracker.release_toggle
|
||||
@@ -2161,42 +2161,6 @@
|
||||
"type": "offroad_only"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "TeslaMadsScreenButton",
|
||||
"widget": "multiple_button",
|
||||
"title": "MADS Screen Activation",
|
||||
"description": "Use a multi-finger press on the infotainment screen to toggle MADS. This allows the use of full MADS functionality when enabled. Selecting a higher finger count may reduce accidental activations. Note: Setting this to Off will reset your MADS settings to default.",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Off"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "3-Finger"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "4-Finger"
|
||||
},
|
||||
{
|
||||
"value": 3,
|
||||
"label": "5-Finger"
|
||||
}
|
||||
],
|
||||
"visibility": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "tesla_has_vehicle_bus",
|
||||
"equals": true
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "offroad_only"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -56,28 +56,6 @@ sections:
|
||||
title: Cooperative Steering (Beta)
|
||||
enablement:
|
||||
- $ref: '#/macros/offroad'
|
||||
- key: TeslaMadsScreenButton
|
||||
widget: multiple_button
|
||||
title: MADS Screen Activation
|
||||
description: 'Use a multi-finger press on the infotainment screen to toggle MADS.
|
||||
This allows the use of full MADS functionality when enabled. Selecting a higher
|
||||
finger count may reduce accidental activations. Note: Setting this to Off will
|
||||
reset your MADS settings to default.'
|
||||
options:
|
||||
- value: 0
|
||||
label: 'Off'
|
||||
- value: 1
|
||||
label: 3-Finger
|
||||
- value: 2
|
||||
label: 4-Finger
|
||||
- value: 3
|
||||
label: 5-Finger
|
||||
visibility:
|
||||
- type: capability
|
||||
field: tesla_has_vehicle_bus
|
||||
equals: true
|
||||
enablement:
|
||||
- $ref: '#/macros/offroad'
|
||||
- id: toyota
|
||||
title: Toyota / Lexus Settings
|
||||
description: ''
|
||||
|
||||
@@ -17,26 +17,6 @@ ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 3, 1: 5, 2: 7, 3: 10, 4: 15, 5: 30, **{i: (
|
||||
VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values())
|
||||
|
||||
|
||||
def _resolve_brand(_params) -> str:
|
||||
bundle = _params.get("CarPlatformBundle")
|
||||
if isinstance(bundle, dict) and bundle.get("brand"):
|
||||
return str(bundle["brand"])
|
||||
|
||||
# Auto-fingerprinted cars have no bundle, fall back to the last known CarParams.
|
||||
CP_bytes = _params.get("CarParamsPersistent")
|
||||
if CP_bytes is None:
|
||||
return ""
|
||||
|
||||
# Never raises: callers rely on "" to mean "brand unknown, skip the migration".
|
||||
try:
|
||||
from openpilot.cereal import messaging # lazy: avoids heavy import at module level
|
||||
from opendbc.car.structs import car
|
||||
return str(messaging.log_from_bytes(CP_bytes, car.CarParams).brand)
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"params_migration: failed to resolve brand from CarParamsPersistent: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def _migrate_car_platform_bundle(_params):
|
||||
bundle = _params.get("CarPlatformBundle")
|
||||
if bundle is None:
|
||||
@@ -67,23 +47,6 @@ def _migrate_car_platform_bundle(_params):
|
||||
cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}")
|
||||
|
||||
|
||||
def _migrate_tesla_mads_screen_button(_params):
|
||||
# TeslaMadsScreenButton defaults to Off for fresh installs, but the screen button was previously always
|
||||
# active on Teslas with a vehicle bus. Seed existing Tesla installs with 3-finger to preserve that.
|
||||
try:
|
||||
if _params.get("TeslaMadsScreenButton") is not None:
|
||||
return
|
||||
|
||||
if _resolve_brand(_params) != "tesla":
|
||||
return
|
||||
|
||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType # lazy: avoids heavy import at module level
|
||||
_params.put("TeslaMadsScreenButton", MadsScreenButtonType.THREE_FINGER, block=True)
|
||||
cloudlog.info("params_migration: seeded TeslaMadsScreenButton with 3-finger to preserve existing behavior")
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error migrating TeslaMadsScreenButton: {e}")
|
||||
|
||||
|
||||
def run_migration(_params):
|
||||
# migrate OnroadScreenOffBrightness
|
||||
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
|
||||
@@ -117,6 +80,3 @@ def run_migration(_params):
|
||||
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")
|
||||
|
||||
_migrate_car_platform_bundle(_params)
|
||||
|
||||
# seed TeslaMadsScreenButton for existing Tesla installs
|
||||
_migrate_tesla_mads_screen_button(_params)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
SUNNYPILOT_CAR_SEGMENTS_REPO = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO",
|
||||
"https://huggingface.co/datasets/sunnypilot/sunnypilotCarSegments")
|
||||
SUNNYPILOT_CAR_SEGMENTS_BRANCH = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_BRANCH", "main")
|
||||
|
||||
|
||||
def get_url(route, segment, file="rlog.zst"):
|
||||
return f"{SUNNYPILOT_CAR_SEGMENTS_REPO}/resolve/{SUNNYPILOT_CAR_SEGMENTS_BRANCH}/segments/{route.replace('|', '/')}/{segment}/{file}"
|
||||
|
||||
|
||||
def sunnypilot_car_segments_source(sr, seg_idxs, fns, /):
|
||||
from openpilot.tools.lib.file_sources import eval_source
|
||||
return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs})
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import requests
|
||||
from huggingface_hub import HfApi
|
||||
from tqdm import tqdm
|
||||
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
REPO_ID = os.environ.get("SUNNYPILOT_CAR_SEGMENTS_REPO_ID", "sunnypilot/sunnypilotCarSegments")
|
||||
|
||||
|
||||
def upload_route(route_name: str, dry_run: bool = False) -> None:
|
||||
route = Route(route_name)
|
||||
log_paths = route.log_paths()
|
||||
valid_segments = [(i, url) for i, url in enumerate(log_paths) if url is not None]
|
||||
|
||||
print(f"Route: {route_name}")
|
||||
print(f"Segments: {len(valid_segments)}/{len(log_paths)}")
|
||||
|
||||
if not valid_segments:
|
||||
print("No segments found.")
|
||||
return
|
||||
|
||||
api = HfApi()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for seg_idx, url in tqdm(valid_segments, desc="Uploading"):
|
||||
filename = url.split("?")[0].rsplit("/", 1)[-1]
|
||||
local_path = os.path.join(tmpdir, f"{seg_idx}_{filename}")
|
||||
resp = requests.get(url, stream=True)
|
||||
resp.raise_for_status()
|
||||
with open(local_path, "wb") as f:
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
repo_path = f"segments/{route_name.replace('|', '/')}/{seg_idx}/{filename}"
|
||||
|
||||
if dry_run:
|
||||
size_mb = os.path.getsize(local_path) / 1024 / 1024
|
||||
print(f" [{seg_idx}] {size_mb:.1f} MB -> {repo_path}")
|
||||
else:
|
||||
api.upload_file(
|
||||
path_or_fileobj=local_path,
|
||||
path_in_repo=repo_path,
|
||||
repo_id=REPO_ID,
|
||||
repo_type="dataset",
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Upload route rlogs to sunnypilot HuggingFace dataset")
|
||||
parser.add_argument("route", help="Route ID (e.g. 5beb9b58bd12b691/0000010a--a51155e496)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Download and show sizes without uploading")
|
||||
args = parser.parse_args()
|
||||
|
||||
upload_route(args.route, dry_run=args.dry_run)
|
||||
@@ -22,8 +22,6 @@ from openpilot.tools.lib.file_sources import comma_api_source, internal_source,
|
||||
from openpilot.tools.lib.route import SegmentRange, FileName
|
||||
from openpilot.tools.lib.log_time_series import msgs_to_time_series
|
||||
|
||||
from openpilot.sunnypilot.tools.lib.sunnypilot_car_segments import sunnypilot_car_segments_source
|
||||
|
||||
LogMessage = type[capnp._DynamicStructReader]
|
||||
LogIterable = Iterable[LogMessage]
|
||||
RawLogIterable = Iterable[bytes]
|
||||
@@ -248,7 +246,7 @@ class LogReader:
|
||||
def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG,
|
||||
sources: list[Source] | None = None, sort_by_time=False, only_union_types=False):
|
||||
if sources is None:
|
||||
sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source, sunnypilot_car_segments_source]
|
||||
sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source]
|
||||
|
||||
self.default_mode = default_mode
|
||||
self.sources = sources
|
||||
|
||||
@@ -75,7 +75,6 @@ testing = [
|
||||
]
|
||||
|
||||
dev = [
|
||||
"huggingface_hub",
|
||||
"matplotlib",
|
||||
]
|
||||
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
OPENPILOT_DIR=$SCRIPT_DIR/../../
|
||||
|
||||
DOCKER_IMAGE=sunnypilot
|
||||
DOCKER_FILE=Dockerfile.openpilot
|
||||
DOCKER_REGISTRY=ghcr.io/sunnypilot
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
|
||||
if [ -n "$TARGET_ARCHITECTURE" ]; then
|
||||
PLATFORM="linux/$TARGET_ARCHITECTURE"
|
||||
TAG_SUFFIX="-$TARGET_ARCHITECTURE"
|
||||
else
|
||||
PLATFORM="linux/$(uname -m)"
|
||||
TAG_SUFFIX=""
|
||||
fi
|
||||
|
||||
LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX
|
||||
REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG
|
||||
REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA
|
||||
|
||||
DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR
|
||||
|
||||
if [ -n "$PUSH_IMAGE" ]; then
|
||||
docker push $REMOTE_TAG
|
||||
docker tag $REMOTE_TAG $REMOTE_SHA_TAG
|
||||
docker push $REMOTE_SHA_TAG
|
||||
fi
|
||||
@@ -5,19 +5,6 @@ requires-python = ">=3.12.3, <3.13"
|
||||
[manifest]
|
||||
overrides = [{ name = "opendbc", editable = "opendbc_repo" }]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
@@ -408,15 +395,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.32.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/57/3ba6e6cb097f85b855b00163d169f35365f44277df044dcf96d55b8f62a3/filelock-3.32.2.tar.gz", hash = "sha256:c33351e1f49cae33414acbc6d56784e6ecee82514ec90795da1161fc4836b5b8", size = 217172, upload-time = "2026-07-29T22:46:04.895Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/e8/72f8cef9fdfeffe06213fe8508039396ee48daa0e3259457ed766173bfd6/filelock-3.32.2-py3-none-any.whl", hash = "sha256:87dd94cf281e586d135fa51132b8e3d9a598b316e90377a288663c9321036c82", size = 98830, upload-time = "2026-07-29T22:46:03.52Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.63.0"
|
||||
@@ -434,88 +412,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsspec"
|
||||
version = "2026.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hf-xet"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
|
||||
{ name = "httpx" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hypothesis"
|
||||
version = "6.47.5"
|
||||
@@ -834,7 +730,6 @@ dependencies = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "matplotlib" },
|
||||
]
|
||||
docs = [
|
||||
@@ -893,7 +788,6 @@ requires-dist = [
|
||||
{ name = "comma-deps-zstd" },
|
||||
{ name = "coverage", marker = "extra == 'testing'" },
|
||||
{ name = "cython" },
|
||||
{ name = "huggingface-hub", marker = "extra == 'dev'" },
|
||||
{ name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" },
|
||||
{ name = "inputs" },
|
||||
{ name = "jeepney" },
|
||||
@@ -1408,7 +1302,7 @@ provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "tinygrad"
|
||||
version = "0.13.0"
|
||||
version = "0.12.0"
|
||||
source = { editable = "tinygrad_repo" }
|
||||
|
||||
[package.metadata]
|
||||
@@ -1519,15 +1413,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
|
||||
Reference in New Issue
Block a user