Compare commits

...

51 Commits

Author SHA1 Message Date
Jason Wen e87dbbaba7 models: sanitize default model name for HF (#1984) 2026-09-02 14:53:31 -04:00
Jason Wen 15efdb392f Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1983)
* ui: remove raygui usage (#38708)

* ui: remove raygui usage

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.

* log chestnut supply fault (#38711)

* log chestnut INA supply fault

* ci

* bump raylib (#38712)

* cabana: replace custom non-view Qt signals w/ plain observer (#38713)

* cabana: move RoutesDialog out of streams/ (#38716)

* cabana: string helpers in utils return std::string (#38720)

* cabana: use std::string in RoutesDialog API results (#38717)

* cabana: move stream open widgets into streamselector (#38715)

* cabana: remove Qt from livestream (#38722)

* cabana: split SettingsDialog out of settings (#38719)

cabana: split SettingsDialog out of settings.{h,cc}

* cabana: split comma API route fetching out of RoutesDialog (#38721)

* cabana: de-QT streams (#38718)

* ui: fix install update button overflow (#38696)

* cabana: split utils/util into Qt-free util and qtutil (#38723)

* ui: guard branch switcher before internet connected (#38692)

* ui: check for update on target branch switch (#38693)

* ui: sync gpu loading to offroad (#38727)

ui: sync gpu loading state

* add chestnut offroad alerts (#38706)

* system: add chestnut offroad alerts

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

* system: detect missing chestnut power from INA voltage

* common: fix OpenpilotPrefix cleanup on macOS (#38728)

The destructor built its cleanup commands as "rm %s -rf", with the flags
after the operand. GNU rm permutes arguments so this works on device and
in CI, but BSD rm on macOS stops option parsing at the first operand and
treats "-rf" as a second filename:

  $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf
  rm: /tmp/rmtest: is a directory
  rm: -rf: No such file or directory
  exit=1

So nothing is removed, and each of the four calls prints two errors plus
"system command failed (256)" from check_system. Every run of a tool that
owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its
comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my
machine.

Pass the flags first.

* replay: capture downloader's stderr so download progress is reported again (#38734)

* bump panda (new health packet) (#38736)

pandad: support compact health packet

* BMRLNAP (#38681)

* ui: clarify branch switcher error message (#38732)

* ui(mici): name updater signal constants (#38731)

* mici: name updater signal constants

* drop SIGNAL_ prefix

* self contained

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>

* modem.py: accept hex chars in ICCID (#38735)

E.118 specifies decimal digits, but many real SIMs carry hex characters
in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs).
AT+QCCID returns them verbatim, and the strict isdigit() check blanked
the ICCID, leaving the modem daemon stuck in INITIALIZING forever and
cellular dead. ModemManager parses ICCID as hex for the same reason.

Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously
stuck retrying 'identity read incomplete', now dials and passes traffic.

* TGC (#38739)

* 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc

* here

* monitor chestnut USB in hardwared (#38741)

hardwared: monitor chestnut USB independently

* modeld: wait for stable chestnut (#38742)

modeld: wait for stable chestnut

* Revert "monitor chestnut USB in hardwared (#38741)" (#38744)

This reverts commit 7d5596d5c3.

* amd warp (#38684)

* modeld: fuse warp and policy TinyJit

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

* slower

* ui: show usb connection (#38745)

* ui: show USB status

* ui: resize USB icon

* ui: classify USB device once

* ui: debounce USB disconnect

* cereal: log big model in drivingModelData (#38747)

* ui: show one GPU status (#38748)

ui: show one GPU status icon

* AGNOS 19.7 (#38750)

---------

Co-authored-by: Trey Moen <50057480+greatgitsby@users.noreply.github.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Robbe Derks <robbe.derks@gmail.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Shane Smiskol <shane@smiskol.com>
Co-authored-by: XiaoXX <xiaoxx97@outlook.com>
Co-authored-by: YassineYousfi <yyousfi1@binghamton.edu>
2026-09-02 13:57:07 -04:00
Jason Wen f5bb855477 Merge commit '6249f4d5b0e63c05f08bce12ca3afebda9f764a3' into sync-20260902
# Conflicts:
#	openpilot/selfdrive/modeld/SConscript
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/pandad/pandad.cc
#	openpilot/selfdrive/selfdrived/alerts_offroad.json
#	openpilot/selfdrive/ui/layouts/onboarding.py
#	openpilot/selfdrive/ui/mici/layouts/home.py
#	openpilot/system/hardware/hardwared.py
#	panda
#	tinygrad_repo
2026-09-02 13:47:27 -04:00
Jason Wen 47db84ebfb models: add big model ONNX hash tracking (#1982) 2026-09-02 01:28:43 -04:00
Jason Wen 68be777395 bump tg 2026-09-01 22:14:43 -04:00
github-actions[bot] ab389498a8 [bot] Update Python packages (#1950)
* Update Python packages

* bump tg

* bump

* ci: route build_model runner by target_hardware instead of hardcoding chestnut

* hack, remove before merge

* Revert build-model runner hack and uv.lock update

* why were they hard coded

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-09-01 22:12:54 -04:00
Daniel Koepping 6249f4d5b0 AGNOS 19.7 (#38750) 2026-09-01 18:32:59 -07:00
Daniel Koepping 8b88f7dd6e ui: show one GPU status (#38748)
ui: show one GPU status icon
2026-09-01 18:32:39 -07:00
Harald Schäfer 79658800ce cereal: log big model in drivingModelData (#38747) 2026-09-01 17:15:13 -07:00
Daniel Koepping 36561258fa ui: show usb connection (#38745)
* ui: show USB status

* ui: resize USB icon

* ui: classify USB device once

* ui: debounce USB disconnect
2026-09-01 15:45:57 -07:00
YassineYousfi cb85ac1f0e amd warp (#38684)
* modeld: fuse warp and policy TinyJit

* bump tg

* fix?

* this simple trick...

* debug 1

* bump tg

* pack all

* wips

* fix

* BIG_INTO_SMALL remove

* slower
2026-09-01 13:59:55 -07:00
Daniel Koepping c9f1602040 Revert "monitor chestnut USB in hardwared (#38741)" (#38744)
This reverts commit 7d5596d5c3.
2026-09-01 11:13:30 -07:00
Daniel Koepping 06af2abe67 modeld: wait for stable chestnut (#38742)
modeld: wait for stable chestnut
2026-09-01 07:20:56 -07:00
Daniel Koepping 7d5596d5c3 monitor chestnut USB in hardwared (#38741)
hardwared: monitor chestnut USB independently
2026-09-01 05:59:02 -07:00
YassineYousfi a2e422eee0 TGC (#38739)
* 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc

* here
2026-08-31 22:30:28 -07:00
Jason Wen 51987a62d0 ci: route build_model runner by hardware type 2026-09-01 01:16:04 -04:00
XiaoXX e10c0fd960 modem.py: accept hex chars in ICCID (#38735)
E.118 specifies decimal digits, but many real SIMs carry hex characters
in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs).
AT+QCCID returns them verbatim, and the strict isdigit() check blanked
the ICCID, leaving the modem daemon stuck in INITIALIZING forever and
cellular dead. ModemManager parses ICCID as hex for the same reason.

Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously
stuck retrying 'identity read incomplete', now dials and passes traffic.
2026-08-31 21:35:56 -07:00
James Vecellio-Grant 98ed8111f6 modeld_v2: big to small model fallback (#1974) 2026-09-01 00:15:23 -04:00
Trey Moen da8ce858ec ui(mici): name updater signal constants (#38731)
* mici: name updater signal constants

* drop SIGNAL_ prefix

* self contained

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-08-31 15:55:16 -07:00
Trey Moen 9fa7ef3d17 ui: clarify branch switcher error message (#38732) 2026-08-31 15:46:52 -07:00
Harald Schäfer 4adbb85742 BMRLNAP (#38681) 2026-08-31 09:25:32 -07:00
Robbe Derks 70df7f227b bump panda (new health packet) (#38736)
pandad: support compact health packet
2026-08-31 14:01:20 +02:00
royjr de197ba6fa chestnut: alert when big model ready (#1947)
egpu: alert when big model ready

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-30 16:20:16 -04:00
Trey Moen 0e32059484 replay: capture downloader's stderr so download progress is reported again (#38734) 2026-08-30 09:32:36 -07:00
Trey Moen 7cf55c3b7a common: fix OpenpilotPrefix cleanup on macOS (#38728)
The destructor built its cleanup commands as "rm %s -rf", with the flags
after the operand. GNU rm permutes arguments so this works on device and
in CI, but BSD rm on macOS stops option parsing at the first operand and
treats "-rf" as a second filename:

  $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf
  rm: /tmp/rmtest: is a directory
  rm: -rf: No such file or directory
  exit=1

So nothing is removed, and each of the four calls prints two errors plus
"system command failed (256)" from check_system. Every run of a tool that
owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its
comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my
machine.

Pass the flags first.
2026-08-28 22:11:52 -07:00
Daniel Koepping 682b6a20df add chestnut offroad alerts (#38706)
* system: add chestnut offroad alerts

* system: refine chestnut offroad alerts

* system: refine chestnut power alerts

* system: confirm chestnut power recovery from PCIe

* system: detect missing chestnut power from INA voltage
2026-08-28 15:46:56 -07:00
Daniel Koepping a67cdf9a51 ui: sync gpu loading to offroad (#38727)
ui: sync gpu loading state
2026-08-28 15:08:18 -07:00
Trey Moen e571e21d14 ui: check for update on target branch switch (#38693) 2026-08-28 12:07:15 -07:00
Trey Moen 839d3f5004 ui: guard branch switcher before internet connected (#38692) 2026-08-28 12:06:33 -07:00
Trey Moen 5645370f84 cabana: split utils/util into Qt-free util and qtutil (#38723) 2026-08-28 11:37:09 -07:00
Trey Moen 633d17cd12 ui: fix install update button overflow (#38696) 2026-08-28 11:30:13 -07:00
Trey Moen 5419f57b3a cabana: de-QT streams (#38718) 2026-08-28 10:18:10 -07:00
Jason Wen 1dd5a7c91d Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1973) 2026-08-28 12:58:26 -04:00
Trey Moen 46f612224c cabana: split comma API route fetching out of RoutesDialog (#38721) 2026-08-28 09:57:47 -07:00
Trey Moen 6e0f4f4630 cabana: split SettingsDialog out of settings (#38719)
cabana: split SettingsDialog out of settings.{h,cc}
2026-08-28 09:55:28 -07:00
Trey Moen 0f9c753e6e cabana: remove Qt from livestream (#38722) 2026-08-28 09:46:45 -07:00
nayan acb784d207 Merge commit '4a13639cfd122ccb9113a4d6ce225dcbd8e61914' into sync-20260827
# Conflicts:
#	openpilot/selfdrive/modeld/SConscript
#	openpilot/selfdrive/modeld/modeld.py
#	openpilot/selfdrive/ui/mici/layouts/home.py
#	openpilot/selfdrive/ui/ui_state.py
#	tinygrad_repo
2026-08-28 12:39:53 -04:00
Trey Moen 131e473f37 cabana: move stream open widgets into streamselector (#38715) 2026-08-28 09:36:26 -07:00
Trey Moen 30f358eb59 cabana: use std::string in RoutesDialog API results (#38717) 2026-08-28 07:25:34 -07:00
Trey Moen 9b9e3ea604 cabana: string helpers in utils return std::string (#38720) 2026-08-28 07:25:11 -07:00
Trey Moen 7cc48b5bc9 cabana: move RoutesDialog out of streams/ (#38716) 2026-08-27 22:00:24 -07:00
Trey Moen cbf750de20 cabana: replace custom non-view Qt signals w/ plain observer (#38713) 2026-08-27 18:54:06 -07:00
Trey Moen 318257fa3b bump raylib (#38712) 2026-08-27 11:38:53 -07:00
Daniel Koepping 4cdc16031f log chestnut supply fault (#38711)
* log chestnut INA supply fault

* ci
2026-08-27 11:21:56 -07:00
Trey Moen 31ea1850f7 ui: remove raygui usage (#38708)
* ui: remove raygui usage

* match previous gui_text_box line spacing

* Revert "match previous gui_text_box line spacing"

This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66.

* Reapply "match previous gui_text_box line spacing"

This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16.
2026-08-27 10:54:52 -07:00
Daniel Koepping 4a13639cfd reduce chestnut states (#38705)
ui: unify chestnut status presentation
2026-08-26 19:12:49 -07:00
YassineYousfi fa75fdd852 chestnut stats: overlap with gpu work (#38704)
* chestnut stats: overlap with gpu work

* ci

---------

Co-authored-by: elkoled <elkoled@gmail.com>
2026-08-26 17:57:55 -07:00
Daniel Koepping 5cfdb2f4da rename usbgpu to chestnut (#38703)
chestnut: rename eGPU interfaces
2026-08-26 15:42:59 -07:00
Daniel Koepping 63548ce10d bump tinygrad (#38702) 2026-08-26 15:20:04 -07:00
Daniel Koepping 980fb79c1a update orange GPU icon (#38701)
mici: update failed eGPU icon
2026-08-26 12:21:14 -07:00
Harald Schäfer d40df6f829 modeld: fall back on invalid big model outputs (#38700) 2026-08-26 12:06:53 -07:00
169 changed files with 3168 additions and 2362 deletions
@@ -8,13 +8,13 @@ on:
required: true required: true
type: string type: string
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)' description: 'Hardware target to compile for (qcom or chestnut)'
required: true required: true
type: choice type: choice
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- usbgpu - chestnut
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
@@ -59,7 +59,7 @@ jobs:
id: get-json id: get-json
run: | run: |
cd docs/docs cd docs/docs
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v" PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v"
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1) latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
next=$((latest+1)) next=$((latest+1))
json_file="${PREFIX}${next}.json" json_file="${PREFIX}${next}.json"
+11 -8
View File
@@ -30,6 +30,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
outputs: outputs:
model_name: ${{ steps.resolve.outputs.model_name }} model_name: ${{ steps.resolve.outputs.model_name }}
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
onnx_path: ${{ steps.resolve.outputs.onnx_path }} onnx_path: ${{ steps.resolve.outputs.onnx_path }}
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
@@ -64,7 +65,9 @@ jobs:
exit 1 exit 1
fi fi
SAFE_NAME="${NAME// /-}"
echo "model_name=${NAME}" >> $GITHUB_OUTPUT echo "model_name=${NAME}" >> $GITHUB_OUTPUT
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
@@ -135,7 +138,7 @@ jobs:
- name: Prepare output - name: Prepare output
env: env:
MODEL_NAME: ${{ needs.resolve.outputs.model_name }} MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }} export PYTHONPATH=${{ github.workspace }}
@@ -158,13 +161,13 @@ jobs:
- name: Upload small model artifact - name: Upload small model artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/small_output/ path: ${{ github.workspace }}/small_output/
- name: Upload artifact name file - name: Upload artifact name file
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.model_name }} name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/small_output/artifact_name.txt path: ${{ github.workspace }}/small_output/artifact_name.txt
- name: Re-enable powersave - name: Re-enable powersave
@@ -176,7 +179,7 @@ jobs:
build_big_model: build_big_model:
needs: resolve needs: resolve
if: ${{ inputs.target == 'big' }} if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, usbgpu] runs-on: [self-hosted, chestnut]
env: env:
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
@@ -254,7 +257,7 @@ jobs:
- name: Prepare output - name: Prepare output
env: env:
MODEL_NAME: ${{ needs.resolve.outputs.model_name }} MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
run: | run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
export PYTHONPATH=${{ github.workspace }} export PYTHONPATH=${{ github.workspace }}
@@ -277,13 +280,13 @@ jobs:
- name: Upload big model artifact - name: Upload big model artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
path: ${{ github.workspace }}/big_output/ path: ${{ github.workspace }}/big_output/
- name: Upload artifact name file - name: Upload artifact name file
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.model_name }} name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: ${{ github.workspace }}/big_output/artifact_name.txt path: ${{ github.workspace }}/big_output/artifact_name.txt
- name: Re-enable powersave - name: Re-enable powersave
@@ -318,7 +321,7 @@ jobs:
if: ${{ inputs.target == 'small' || inputs.target == 'big' }} if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: artifact-name-${{ needs.resolve.outputs.model_name }} name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
path: artifact_name path: artifact_name
- name: Read artifact name - name: Read artifact name
@@ -30,7 +30,7 @@ on:
type: boolean type: boolean
default: true default: true
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)' description: 'Hardware target to compile for (qcom or chestnut)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -101,7 +101,7 @@ on:
default: 'qcom' default: 'qcom'
options: options:
- qcom - qcom
- usbgpu - chestnut
hf_repo: hf_repo:
description: 'Hugging Face dataset repository' description: 'Hugging Face dataset repository'
required: false required: false
@@ -109,7 +109,7 @@ on:
default: 'sunnypilot/sunnypilot_models_v1' default: 'sunnypilot/sunnypilot_models_v1'
env: env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
jobs: jobs:
build_model: build_model:
@@ -31,7 +31,7 @@ on:
type: string type: string
default: '' default: ''
target_hardware: target_hardware:
description: 'Hardware target to compile for (qcom or usbgpu)' description: 'Hardware target to compile for (qcom or chestnut)'
required: false required: false
type: string type: string
default: 'qcom' default: 'qcom'
@@ -57,7 +57,7 @@ on:
type: choice type: choice
options: options:
- qcom - qcom
- usbgpu - chestnut
default: 'qcom' default: 'qcom'
@@ -102,7 +102,7 @@ jobs:
cat $GITHUB_OUTPUT cat $GITHUB_OUTPUT
- run: | - run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx" git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
else else
@@ -121,7 +121,7 @@ jobs:
if-no-files-found: error if-no-files-found: error
build_model: build_model:
runs-on: [self-hosted, usbgpu] runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
needs: get_model needs: get_model
env: env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -185,9 +185,9 @@ jobs:
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
echo "USBGPU build" echo "CHESTNUT build"
export USBGPU=1 export CHESTNUT=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12 export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6" export AGNOS_VERSION="19.7"
fi fi
export STAGING_ROOT="/data/safe_staging" export STAGING_ROOT="/data/safe_staging"
+1
View File
@@ -353,6 +353,7 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitPending @22; speedLimitPending @22;
e2eChime @23; e2eChime @23;
laneChangeRoadEdge @24; laneChangeRoadEdge @24;
bigModelReady @25;
} }
} }
+2
View File
@@ -725,6 +725,7 @@ struct ChestnutState {
pcieLtssm @7 :UInt8; pcieLtssm @7 :UInt8;
supplyVoltage @8 :UInt16; # mV supplyVoltage @8 :UInt16; # mV
supplyCurrent @9 :Int16; # mA supplyCurrent @9 :Int16; # mA
supplyFault @10 :Bool;
} }
struct RadarState @0x9a185389d6fdd05f { struct RadarState @0x9a185389d6fdd05f {
@@ -1004,6 +1005,7 @@ struct DrivingModelData {
frameIdExtra @1 :UInt32; frameIdExtra @1 :UInt32;
frameDropPerc @6 :Float32; frameDropPerc @6 :Float32;
modelExecutionTime @7 :Float32; modelExecutionTime @7 :Float32;
big @8 :Bool;
action @2 :ModelDataV2.Action; action @2 :ModelDataV2.Action;
+11 -11
View File
@@ -56,29 +56,29 @@
}, },
{ {
"name": "boot", "name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", "hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", "hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
"size": 46897152, "size": 46897152,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
"has_ab": true, "has_ab": true,
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e" "ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
}, },
{ {
"name": "system", "name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b", "hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", "hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"size": 4718592000, "size": 4718592000,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468", "ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
"alt": { "alt": {
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", "hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img", "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
"size": 4718592000 "size": 4718592000
} }
} }
] ]
+2 -1
View File
@@ -5,6 +5,7 @@ import logging
import os import os
import select import select
import signal import signal
import string
import struct import struct
import subprocess import subprocess
import tempfile import tempfile
@@ -354,7 +355,7 @@ class Modem:
imei = "" imei = ""
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
if not iccid.isdigit(): if not all(c in string.hexdigits for c in iccid):
iccid = "" iccid = ""
imsi = first_line("AT+CIMI") imsi = first_line("AT+CIMI")
+7 -1
View File
@@ -4,11 +4,17 @@ from pathlib import Path
CHESTNUT_FW_VERSION = "ed4e39b7" CHESTNUT_FW_VERSION = "ed4e39b7"
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
USB_DEVICES_PATH = Path("/sys/bus/usb/devices") USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
PRIMARY_USB_CONTROLLER = "a600000.ssusb" PRIMARY_USB_CONTROLLER = "a600000.ssusb"
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
return (vendor_id, product_id) in ids
def get_usb_topology() -> set[str]: def get_usb_topology() -> set[str]:
try: try:
return set(os.listdir(USB_DEVICES_PATH)) return set(os.listdir(USB_DEVICES_PATH))
@@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
entry.linkErrorCount = device["linkErrorCount"] entry.linkErrorCount = device["linkErrorCount"]
entry.usb3Lane = device.get("usb3Lane", "unknown") entry.usb3Lane = device.get("usb3Lane", "unknown")
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS: if is_chestnut_usb_id(entry.vendorId, entry.productId):
chestnut_present = True chestnut_present = True
device_state.chestnutPresent = chestnut_present device_state.chestnutPresent = chestnut_present
+13 -5
View File
@@ -92,6 +92,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
@@ -130,8 +136,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UpdaterLastFetchTime", {PERSISTENT, TIME}},
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
{"Version", {PERSISTENT, STRING}}, {"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- // // --- sunnypilot params --- //
@@ -195,15 +202,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params // Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, {"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync?
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}}, {"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}},
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
{"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, {"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}}, {"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control // Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
+4 -4
View File
@@ -27,14 +27,14 @@ public:
auto param_path = Params().getParamPath(); auto param_path = Params().getParamPath();
if (util::file_exists(param_path)) { if (util::file_exists(param_path)) {
std::string real_path = util::readlink(param_path); std::string real_path = util::readlink(param_path);
util::check_system(util::string_format("rm %s -rf", real_path.c_str())); util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
unlink(param_path.c_str()); unlink(param_path.c_str());
} }
if (getenv("COMMA_CACHE") == nullptr) { if (getenv("COMMA_CACHE") == nullptr) {
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str())); util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
} }
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str())); util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str())); util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
unsetenv("OPENPILOT_PREFIX"); unsetenv("OPENPILOT_PREFIX");
} }
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
size 13079
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
size 2034
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
size 7279
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
size 10876
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
size 1544
+22 -27
View File
@@ -7,14 +7,9 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
Import('env', 'arch') Import('env', 'arch')
chunker_file = File("#openpilot/common/file_chunker.py") chunker_file = File("#openpilot/common/file_chunker.py")
lenv = env.Clone() lenv = env.Clone()
@@ -24,30 +19,32 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
def estimate_pickle_max_size(onnx_size): def estimate_pickle_max_size(onnx_size):
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty # QCOM programs for models with spatial recurrent features can approach 2x
# the ONNX size. Overestimating only adds an empty trailing chunk.
return 2.0 * onnx_size + 10 * 1024 * 1024
if arch == 'comma_arm64': if arch == 'comma_arm64':
from openpilot.common.hardware import HARDWARE
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
camera_configs = [(camera.width, camera.height)]
tg_backend = 'QCOM' tg_backend = 'QCOM'
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1' tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
else: else:
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
tg_backend = 'CPU' tg_backend = 'CPU'
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM' tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
tg_devices = { # which device to put jit inputs to at runtime tg_devices = { # which device to put jit inputs to at runtime
'openpilot.selfdrive.modeld.modeld': {
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
},
'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'openpilot.selfdrive.modeld.dmonitoringmodeld': {
'default': {'DEV': tg_backend} 'default': {'DEV': tg_backend}
}, },
} }
USBGPU = usbgpu_present() CHESTNUT = chestnut_present()
if USBGPU: if CHESTNUT:
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
usbgpu_lock = File("models/.usb_gpu.lock").abspath chestnut_lock = File("models/.chestnut.lock").abspath
def write_tg_devices(target, source, env): def write_tg_devices(target, source, env):
with open(str(target[0]), "w") as f: with open(str(target[0]), "w") as f:
@@ -74,12 +71,11 @@ model_w, model_h = MEDMODEL_INPUT_SIZE
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
if not os.getenv('SKIP_TINYGRAD_COMPILE'): if not os.getenv('SKIP_TINYGRAD_COMPILE'):
for usbgpu in [False, True] if USBGPU else [False]: for chestnut in [False, True] if CHESTNUT else [False]:
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath) driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS) camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
@@ -104,14 +100,14 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
chunk_file(pkl, chunks) chunk_file(pkl, chunks)
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks) chunk_file(pkl, chunks)
actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
node = lenv.Command( node = lenv.Command(
chunk_targets, chunk_targets,
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
actions, actions,
) )
if usbgpu: if chestnut:
lenv.SideEffect(usbgpu_lock, node) lenv.SideEffect(chestnut_lock, node)
# get model metadata # get model metadata
fn = File(f"models/dmonitoring_model").abspath fn = File(f"models/dmonitoring_model").abspath
@@ -121,7 +117,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
dm_w, dm_h = DM_INPUT_SIZE dm_w, dm_h = DM_INPUT_SIZE
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")] compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
for cam_w, cam_h in CAMERA_CONFIGS: for cam_w, cam_h in camera_configs:
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py ' cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} ' f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
@@ -143,5 +139,4 @@ def tg_compile(flags, model_name):
Action(do_chunk, " [CHUNK] $TARGET")], Action(do_chunk, " [CHUNK] $TARGET")],
) )
if not os.getenv('SKIP_TINYGRAD_COMPILE'): tg_compile(tg_flags, 'dmonitoring_model')
tg_compile(tg_flags, 'dmonitoring_model')
+86 -73
View File
@@ -37,17 +37,12 @@ from tinygrad.engine.jit import TinyJit
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
WARP_INPUTS = ['tfm', 'big_tfm'] MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
WARP_DEV = os.getenv('WARP_DEV')
def make_random_images(keys, shape, device=None): def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys} # Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
return stride * (y_height + uv_height)
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None): def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
@@ -99,7 +94,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
def frame_prepare_tinygrad(input_frame, M_inv): def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV) M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
# deinterleave NV12 UV plane (UVUV... -> separate U, V) # deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0): with Context(SPLIT_REDUCEOP=0):
@@ -118,49 +113,43 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
return frame_prepare_tinygrad return frame_prepare_tinygrad
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
img = vision_input_shapes['img'] # (1, 12, 128, 256)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
npy = {
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
input_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(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def get_policy_npy_shapes(input_shapes): def get_policy_npy_shapes(input_shapes):
dp = input_shapes['desire_pulse'] # (1, 25, 8) dp = input_shapes['desire_pulse'] # (1, 25, 8)
tc = input_shapes['traffic_convention'] # (1, 2) tc = input_shapes['traffic_convention'] # (1, 2)
at = input_shapes['action_t'] # (1, 2) at = input_shapes['action_t'] # (1, 2)
fb = input_shapes['features_buffer'] # (1, 24, 512) fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
feat_dim = math.prod(fb[2:])
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
return shapes, [math.prod(s) for s in shapes.values()] return shapes, [math.prod(s) for s in shapes.values()]
def make_input_queues(input_shapes, frame_skip, device): def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) img = input_shapes['img'] # (1, 12, 128, 256)
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature feat_dim = math.prod(fb[2:])
dp = input_shapes['desire_pulse'] # (1, 25, 8) dp = input_shapes['desire_pulse'] # (1, 25, 8)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
shapes, sizes = get_policy_npy_shapes(input_shapes) policy_shapes, _ = get_policy_npy_shapes(input_shapes)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
sizes = [math.prod(s) for s in shapes.values()]
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
frames = packed_input[packed_npy_size:]
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
# views into the packed inputs, to be refilled at runtime # views into the packed inputs, to be refilled at runtime
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
input_queues.update({ input_queues = {
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), '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(),
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), 'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
}) }
return input_queues, npy return input_queues, npy, frame_views
def shift_and_sample(buf, new_val, sample_fn): def shift_and_sample(buf, new_val, sample_fn):
@@ -176,13 +165,15 @@ def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0) return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_warp(nv12, model_w, model_h, frame_skip): def make_warp(nv12, model_w, model_h):
frame_prepare = make_frame_prepare(nv12, model_w, model_h) frame_prepare = make_frame_prepare(nv12, model_w, model_h)
def warp(tfm, big_tfm, frame, big_frame): def warp(tfm, big_tfm, frame, big_frame):
tfm = tfm.to(WARP_DEV) tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(WARP_DEV) big_tfm = big_tfm.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm) frame = frame.to(Device.DEFAULT)
big_frame = big_frame.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, frame, big_frame)
warped_frame = frame_prepare(frame, tfm).unsqueeze(0) warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
@@ -195,10 +186,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT) packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
warped = warped.to(Device.DEFAULT)
Tensor.realize(packed_npy_inputs, warped) Tensor.realize(packed_npy_inputs, warped)
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn) img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
@@ -211,33 +202,50 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
inputs = { inputs = {
'img': img, 'img': img,
'big_img': big_img, 'big_img': big_img,
'features_buffer': feat_buf, 'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
'desire_pulse': desire_buf, 'desire_pulse': desire_buf,
'traffic_convention': traffic_convention, 'traffic_convention': traffic_convention,
'action_t': action_t, 'action_t': action_t,
} }
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
out = next(iter(model_runner(inputs).values())).cast('float32') out = next(iter(model_runner(inputs).values())).cast('float32')
return out, return out,
return run_policy return run_policy
def compile_jit(jit, make_random_inputs, input_keys, make_queues): def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
SEED = 42 _, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
input_queues, npy = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
n_runs = 1 if testing else 3 packed_input = packed_npy_inputs.to(Device.DEFAULT)
Tensor.realize(packed_input)
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
big_frame = packed_input[packed_npy_size + frame_copy_size:]
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
return run_model
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
if benchmark_runs < 1:
raise ValueError("benchmark_runs must be at least 1")
SEED = 42
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
rng = np.random.default_rng(seed)
for i in range(n_runs): for i in range(n_runs):
for v in npy.values(): for v in npy.values():
v[:] = rng.standard_normal(v.shape).astype(v.dtype) v[:] = rng.standard_normal(v.shape).astype(v.dtype)
for v in frame_views.values():
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
Device.default.synchronize() Device.default.synchronize()
random_inputs = make_random_inputs()
st = time.perf_counter() st = time.perf_counter()
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs) outs = fn(**{k: input_queues[k] for k in input_keys})
mt = time.perf_counter() mt = time.perf_counter()
Device.default.synchronize() Device.default.synchronize()
et = time.perf_counter() et = time.perf_counter()
@@ -256,14 +264,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
return val, buffers return val, buffers
print('capture + replay') print('capture + replay')
test_val, test_buffers = random_inputs_run(jit, SEED) test_val, test_buffers = random_inputs_run(jit, SEED, 3)
print('pickle round trip') print(f'pickle round trip ({benchmark_runs} runs per seed)')
with tempfile.TemporaryFile(dir=".") as f: with tempfile.TemporaryFile(dir=".") as f:
dump_oob(jit, f) dump_oob(jit, f)
f.seek(0) f.seek(0)
jit = load_oob(f) loaded_jit = load_oob(f)
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True) random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False) random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
return jit return jit
@@ -292,27 +301,31 @@ if __name__ == "__main__":
p.add_argument('--onnx', required=True) p.add_argument('--onnx', required=True)
p.add_argument('--output', required=True) p.add_argument('--output', required=True)
p.add_argument('--frame-skip', type=int, required=True) p.add_argument('--frame-skip', type=int, required=True)
p.add_argument('--benchmark-runs', type=int, default=1,
help='timed loaded-JIT runs for each correctness seed')
args = p.parse_args() args = p.parse_args()
model_path = read_file_chunked_to_disk(args.onnx) model_path = read_file_chunked_to_disk(args.onnx)
model_w, model_h = args.model_size model_w, model_h = args.model_size
model_runner = OnnxRunner(model_path) model_runner = OnnxRunner(model_path)
out = {'metadata': make_metadata_dict(model_path)} out = {
'metadata': make_metadata_dict(model_path),
'input_devices': {'model': Device.DEFAULT},
'run_model': {},
}
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
make_policy_queues)
for cam_w, cam_h in args.camera_resolutions: for cam_w, cam_h in args.camera_resolutions:
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip) frame_copy_size=frame_copy_size)
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) warp = make_warp(nv12, model_w, model_h)
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
args.benchmark_runs)
with open(args.output, "wb") as f: with open(args.output, "wb") as f:
dump_oob(out, f) dump_oob(out, f)
@@ -29,7 +29,7 @@ class ModelState:
output: np.ndarray output: np.ndarray
def __init__(self, cam_w: int, cam_h: int): def __init__(self, cam_w: int, cam_h: int):
self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV'] self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV']
with open(METADATA_PATH, 'rb') as f: with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f) model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes'] self.input_shapes = model_metadata['input_shapes']
@@ -64,6 +64,7 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
driving_model_data.frameIdExtra = modelV2.frameIdExtra driving_model_data.frameIdExtra = modelV2.frameIdExtra
driving_model_data.frameDropPerc = modelV2.frameDropPerc driving_model_data.frameDropPerc = modelV2.frameDropPerc
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
driving_model_data.big = modelV2.big
driving_model_data.action = modelV2.action driving_model_data.action = modelV2.action
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
+15 -9
View File
@@ -7,18 +7,20 @@ import tempfile
from pathlib import Path from pathlib import Path
from openpilot.common.file_chunker import get_manifest_path from openpilot.common.file_chunker import get_manifest_path
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
MODELS_DIR = Path(__file__).resolve().parent / 'models' MODELS_DIR = Path(__file__).resolve().parent / 'models'
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
CHESTNUT_POWERED_VOLTAGE = 5000
CHESTNUT_PCIE_READY = 0x78
def get_tg_input_devices(process_name: str, usbgpu: bool): def get_tg_input_devices(process_name: str, chestnut: bool):
with open(TG_INPUT_DEVICES_PATH) as f: with open(TG_INPUT_DEVICES_PATH) as f:
return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu'] return json.load(f)[process_name]['default' if not chestnut else 'chestnut']
def modeld_pkl_path(usbgpu: bool): def modeld_pkl_path(chestnut: bool):
prefix = 'big_' if usbgpu else '' prefix = 'big_' if chestnut else ''
return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl'
def dump_oob(obj, f): def dump_oob(obj, f):
@@ -45,16 +47,20 @@ def load_oob(f):
yield pb yield pb
return pickle.load(io.BytesIO(opcodes), buffers=buffers()) return pickle.load(io.BytesIO(opcodes), buffers=buffers())
def usbgpu_present() -> bool: def chestnut_present() -> bool:
for d in USB_DEVICES_PATH.glob("*"): for d in USB_DEVICES_PATH.glob("*"):
try: try:
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
product = (d / "product").read_text().strip() product = (d / "product").read_text().strip()
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN": if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
return True return True
except Exception: except Exception:
pass pass
return False return False
def usbgpu_compiled() -> bool: def chestnut_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file() return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
def chestnut_ready(state) -> bool:
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
+105 -66
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from collections.abc import Callable
import ctypes
from functools import cached_property from functools import cached_property
import os import os
os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
from tinygrad.tensor import Tensor
from tinygrad.device import Device from tinygrad.device import Device
import usb1
import struct import struct
import threading import threading
import time import time
@@ -26,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
LAT_SMOOTH_SECONDS = 0.0 LAT_SMOOTH_SECONDS = 0.0
@@ -81,6 +83,37 @@ class ChestnutState:
self.valid = True self.valid = True
self.sends = 0 self.sends = 0
self.metrics = {} self.metrics = {}
self._asm_usb = None
def _close_asm_usb(self) -> None:
if self._asm_usb is not None:
self._asm_usb.close()
self._asm_usb = None
def _open_asm_usb(self):
context = usb1.USBContext()
for vendor_id, product_id in CHESTNUT_USB_IDS:
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
return handle
context.close()
def _read_ina(self) -> tuple[int, int, bool]:
if "AMD" in Device._opened_devices and self._asm_usb is None:
try:
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
return struct.unpack('<Hh?', bytes(raw))
except Exception:
pass
if self._asm_usb is None:
self._asm_usb = self._open_asm_usb()
if self._asm_usb is None:
raise usb1.USBErrorNoDevice
try:
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
except usb1.USBError:
self._close_asm_usb()
raise
return struct.unpack('<Hh?', bytes(raw))
@cached_property @cached_property
def power_limit(self) -> int: def power_limit(self) -> int:
@@ -94,8 +127,10 @@ class ChestnutState:
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
try: try:
smu = Device["AMD"].iface.dev_impl.smu smu = Device["AMD"].iface.dev_impl.smu
metrics_t = smu.smu_mod.SmuMetricsExternal_t
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
'powerDrawW': metrics.AverageSocketPower, 'powerDrawW': metrics.AverageSocketPower,
@@ -114,13 +149,15 @@ class ChestnutState:
setattr(state, k, v) setattr(state, k, v)
asm_valid = False asm_valid = False
try:
# ASM runs on USB-C power, these still read without a gpu
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
asm_valid = True
except Exception:
pass
if "AMD" in Device._opened_devices: if "AMD" in Device._opened_devices:
try: try:
# ASM runs on USB-C power, these still read without a gpu state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
asm = Device["AMD"].iface.pci_dev.usb
state.pcieLtssm = asm.read(0xB450, 1)[0]
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
asm_valid = True
except Exception: except Exception:
pass pass
@@ -141,42 +178,34 @@ class FrameMeta:
class ModelState(ModelStateBase): class ModelState(ModelStateBase):
prev_desire: np.ndarray # for tracking the rising edge of the pulse prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool): def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] input_devices = jits['input_devices']
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu))) self.model_device = input_devices['model']
metadata = jits['metadata'] metadata = jits['metadata']
self.input_shapes = metadata['input_shapes'] self.input_shapes = metadata['input_shapes']
self.vision_input_names = [k for k in self.input_shapes if 'img' in k] self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
self.output_slices = metadata['output_slices'] self.output_slices = metadata['output_slices']
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
self.usbgpu = usbgpu self.chestnut = chestnut
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
self.full_frames: dict[str, Tensor] = {} self.input_queues, self.npy, self.frame_views = make_input_queues(
self._blob_cache: dict[tuple[str, int], Tensor] = {} self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.parser = Parser() self.parser = Parser()
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')} self.run_model = jits['run_model'][(cam_w,cam_h)]
self.run_policy = jits['run_policy']
self.warp = jits[(cam_w,cam_h)]
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
return parsed_model_outputs return parsed_model_outputs
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
for key in bufs.keys(): for key, buf in bufs.items():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
yuv_size = self.frame_buf_params[key][3]
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
cache_key = (key, ptr)
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
self.full_frames[key] = self._blob_cache[cache_key]
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge # Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0 inputs['desire_pulse'][0] = 0
@@ -187,16 +216,12 @@ class ModelState(ModelStateBase):
self.npy['tfm'][:,:] = transforms['img'][:,:] self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:] self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']) outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
if after_enqueue is not None:
outs, = self.run_policy( after_enqueue()
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
)
model_output = outs.numpy()[0] model_output = outs.numpy()[0]
if self.usbgpu and not np.all(np.isfinite(model_output)): if self.chestnut and not np.all(np.isfinite(model_output)):
# TODO remove with prev_feat raise RuntimeError("model output not finite")
cloudlog.error("model output not finite, dropping frame")
return None
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
@@ -205,25 +230,37 @@ class ModelState(ModelStateBase):
return outputs_dict return outputs_dict
def warmup(self) -> None: def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names} dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
eye = np.eye(3, dtype=np.float32) eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2} dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()}) self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) self.input_queues, self.npy, self.frame_views = make_input_queues(
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
self.prev_desire[:] = 0 self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
def main(demo=False): def main(demo=False):
cloudlog.warning("modeld init") cloudlog.warning("modeld init")
USBGPU = usbgpu_present() and usbgpu_compiled() chestnut_available = chestnut_present() and chestnut_compiled()
if USBGPU: CHESTNUT = False
if chestnut_available:
poller = messaging.Poller()
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
if not poller.poll(round(remaining * 1000)):
break
msg = messaging.recv_one_or_none(sock)
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
if CHESTNUT:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params() params = Params()
params.put_bool("UsbGpuLoading", USBGPU) params.put_bool("ChestnutLoading", CHESTNUT)
params.remove("UsbGpuActive") if chestnut_available and not CHESTNUT:
params.put_bool("ChestnutActive", False)
else:
params.remove("ChestnutActive")
config_realtime_process(7, 54) config_realtime_process(7, 54)
@@ -253,7 +290,7 @@ def main(demo=False):
st = time.monotonic() st = time.monotonic()
cloudlog.warning("loading model") cloudlog.warning("loading model")
model = None model = None
if USBGPU: if CHESTNUT:
big_model = None big_model = None
def load_big(): def load_big():
nonlocal big_model nonlocal big_model
@@ -267,23 +304,27 @@ def main(demo=False):
loader.start() loader.start()
loader.join(BIG_MODEL_TIMEOUT) loader.join(BIG_MODEL_TIMEOUT)
model = big_model model = big_model
params.put_bool("UsbGpuActive", model is not None) if model is None:
params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", model is not None)
if model is not None:
params.remove("ChestnutModelError")
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
if model is None: if model is None:
model = small_model model = small_model
params.put_bool("UsbGpuLoading", False) params.put_bool("ChestnutLoading", False)
assert model is not None assert model is not None
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging # messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
pm = PubMaster(pub_socks) pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState() publish_state = PublishState()
params = Params() params = Params()
chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
# setup filter to track dropped frames # setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
@@ -393,13 +434,16 @@ def main(demo=False):
mt1 = time.perf_counter() mt1 = time.perf_counter()
try: try:
model_output = model.run(bufs, transforms, inputs) send_chestnut = (chestnut_state is not None and
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0)
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
except Exception: except Exception:
if not params.get_bool("UsbGpuActive"): if not params.get_bool("ChestnutActive"):
raise raise
# fallback to small model # fallback to small model
cloudlog.exception("big model failed, fall back to small") cloudlog.exception("big model failed, fall back to small")
params.put_bool("UsbGpuActive", False) params.put_bool("ChestnutModelError", True)
params.put_bool("ChestnutActive", False)
assert small_model is not None assert small_model is not None
model = small_model model = small_model
if chestnut_state is not None: if chestnut_state is not None:
@@ -419,18 +463,17 @@ def main(demo=False):
fill_model_msg(modelv2_send, model_output, action, fill_model_msg(modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen)
modelv2_send.modelV2.big = model.usbgpu modelv2_send.modelV2.big = model.chestnut
desire_state = modelv2_send.modelV2.meta.desireState desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
r_lane_change_prob = desire_state[log.Desire.laneChangeRight] r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
lane_change_prob = l_lane_change_prob + r_lane_change_prob lane_change_prob = l_lane_change_prob + r_lane_change_prob
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send = messaging.new_message('modelDataV2SP') mdv2sp_send = messaging.new_message('modelDataV2SP')
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego) left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
fill_driving_model_data(drivingdata_send, modelv2_send) fill_driving_model_data(drivingdata_send, modelv2_send)
@@ -441,10 +484,6 @@ def main(demo=False):
pm.send('modelDataV2SP', mdv2sp_send) pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id last_vipc_frame_id = meta_main.frame_id
if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0:
chestnut_state.send()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
import argparse import argparse
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
size 1757355221 size 765950064
+10 -10
View File
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
ps.setUptime(health.uptime_pkt); ps.setUptime(health.uptime_pkt);
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt); ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt); ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
ps.setIgnitionLine(health.ignition_line_pkt); ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
ps.setIgnitionCan(health.ignition_can_pkt); ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
ps.setControlsAllowed(health.controls_allowed_pkt); ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
ps.setPandaType(hw_type); ps.setPandaType(hw_type);
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt)); ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
ps.setSafetyParam(health.safety_param_pkt); ps.setSafetyParam(health.safety_param_pkt);
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt)); ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt)); ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt)); ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
ps.setAlternativeExperience(health.alternative_experience_pkt); ps.setAlternativeExperience(health.alternative_experience_pkt);
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
ps.setInterruptLoad(health.interrupt_load_pkt); ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
ps.setFanPower(health.fan_power); ps.setFanPower(health.fan_power);
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSpiErrorCount(health.spi_error_count_pkt);
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
} }
if (spoofing_started) { if (spoofing_started) {
health.ignition_line_pkt = 1; health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
} }
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad; bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node // Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) { if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
} }
bool power_save_desired = !ignition_local; bool power_save_desired = !ignition_local;
if (health.power_save_enabled_pkt != power_save_desired) { if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
panda->set_power_saving(power_save_desired); panda->set_power_saving(power_save_desired);
} }
@@ -19,6 +19,30 @@
}, },
"Offroad_ChestnutBranch": { "Offroad_ChestnutBranch": {
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.", "text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
"severity": -1
},
"Offroad_ChestnutNotDetected": {
"text": "Chestnut not detected. Check USB and 12V connections.",
"severity": 0
},
"Offroad_ChestnutOverheated": {
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
"severity": 0
},
"Offroad_ChestnutPcieUnavailable": {
"text": "%1",
"severity": 0
},
"Offroad_ChestnutUncompiled": {
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
"severity": 0
},
"Offroad_ChestnutUpdateFailed": {
"text": "Chestnut update failed. Check the USB cable.",
"severity": 0
},
"Offroad_ChestnutUsbSlow": {
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
"severity": 0 "severity": 0
}, },
"Offroad_UnregisteredHardware": { "Offroad_UnregisteredHardware": {
+5 -4
View File
@@ -195,17 +195,18 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.joystickDebug) self.events.add(EventName.joystickDebug)
self.startup_event = None self.startup_event = None
loading = self.params.get_bool("UsbGpuLoading") loading = self.params.get_bool("ChestnutLoading")
if self.big_model_loading and not loading: if self.big_model_loading and not loading:
self.big_model_ready_t = time.monotonic() self.big_model_ready_t = time.monotonic()
self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady)
self.big_model_loading = loading self.big_model_loading = loading
if self.big_model_loading: if self.big_model_loading:
self.events.add(EventName.bigModelLoading) self.events.add(EventName.bigModelLoading)
big_active = self.params.get("UsbGpuActive") big_active = self.params.get("ChestnutActive")
usbgpu_present = self.sm['deviceState'].chestnutPresent chestnut_present = self.sm['deviceState'].chestnutPresent
model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2']
big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present) big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present)
if big_failed and not self.big_model_failed: if big_failed and not self.big_model_failed:
self.events.add(EventName.bigModelFailed) self.events.add(EventName.bigModelFailed)
self.big_model_failed = big_failed self.big_model_failed = big_failed
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
add_ops = [] add_ops = []
for _, msg in msgs: for _, msg in msgs:
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime) dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]: for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field)) setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
for meta_field in ["laneChangeState", "laneChangeState"]: for meta_field in ["laneChangeState", "laneChangeState"]:
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field)) setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN) GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
EXEC_TIMINGS = [ EXEC_TIMINGS = [
# model, instant max, average max # model, instant max, average max, chestnut average max
("modelV2", 0.05, 0.028), ("modelV2", 0.05, 0.03, 0.05),
("driverStateV2", 0.05, 0.018), ("driverStateV2", 0.05, 0.018, 0.018),
] ]
def get_log_fn(test_route, ref="master"): def get_log_fn(test_route, ref="master"):
@@ -169,11 +169,13 @@ def model_replay(lr, frs):
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs) dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
msgs = modeld_msgs + dmonitoringmodeld_msgs msgs = modeld_msgs + dmonitoringmodeld_msgs
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result'] header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
rows = [] rows = []
timings_ok = True timings_ok = True
for (s, instant_max, avg_max) in EXEC_TIMINGS: for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
avg_max = chestnut_avg_max if chestnut else avg_max
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s] ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
# TODO some init can happen in first iteration # TODO some init can happen in first iteration
ts = ts[1:] ts = ts[1:]
@@ -1,7 +1,7 @@
import time import time
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
self._last_input_time = time.monotonic() self._last_input_time = time.monotonic()
self._was_active = False self._was_active = False
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY, self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color): def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS) spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
+2 -2
View File
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
from openpilot.system.ui.lib.multilang import tr, trn from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
version_text_width, self.header_rect.height) version_text_width, self.header_rect.height)
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
def _render_home_content(self): def _render_home_content(self):
self._render_left_column() self._render_left_column()
+4 -4
View File
@@ -5,7 +5,7 @@ from enum import IntEnum
import pyray as rl import pyray as rl
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -115,9 +115,9 @@ class TermsPage(Widget):
self._on_accept = on_accept self._on_accept = on_accept
self._on_decline = on_decline self._on_decline = on_decline
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."), self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline) self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept) self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
def __init__(self, back_callback=None): def __init__(self, back_callback=None):
super().__init__() super().__init__()
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."), self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback) self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER, self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked) click_callback=self._on_uninstall_clicked)
@@ -199,6 +199,9 @@ class SoftwareLayout(Widget):
selection = self._branch_dialog.selection selection = self._branch_dialog.selection
ui_state.params.put("UpdaterTargetBranch", selection, block=True) ui_state.params.put("UpdaterTargetBranch", selection, block=True)
self._branch_btn.action_item.set_value(selection) self._branch_btn.action_item.set_value(selection)
self._download_btn.action_item.set_enabled(False)
self._waiting_for_updater = True
self._waiting_start_ts = time.monotonic()
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
self._branch_dialog = None self._branch_dialog = None
+25 -12
View File
@@ -1,4 +1,5 @@
import datetime import datetime
import math
import time import time
from openpilot.cereal import log from openpilot.cereal import log
@@ -8,8 +9,8 @@ from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.common.version import RELEASE_BRANCHES from openpilot.common.version import RELEASE_BRANCHES
HEAD_BUTTON_FONT_SIZE = 40 HEAD_BUTTON_FONT_SIZE = 40
@@ -69,8 +70,8 @@ class AlertsPill(Widget):
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h) count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
gui_label(count_rect, str(alert_count), font_size=36, gui_label(count_rect, str(alert_count), font_size=36,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
class NetworkIcon(Widget): class NetworkIcon(Widget):
@@ -139,8 +140,10 @@ class MiciHomeLayout(Widget):
self._version_text = self._get_version_text() self._version_text = self._get_version_text()
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37)) self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37)) self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
@@ -150,13 +153,15 @@ class MiciHomeLayout(Widget):
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(), NetworkIcon(),
self._experimental_icon, self._experimental_icon,
self._egpu_icon, self._usb_icon,
self._egpu_icon_gray, self._chestnut_icon,
self._chestnut_loading_icon,
self._chestnut_failed_icon,
self._body_icon, self._body_icon,
self._mic_icon, self._mic_icon,
], spacing=18) ], spacing=18)
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False) self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
@@ -247,12 +252,20 @@ class MiciHomeLayout(Widget):
self._version_commit_label.render() self._version_commit_label.render()
# ***** Center-aligned bottom section icons ***** # ***** Center-aligned bottom section icons *****
usb_connected = ui_state.usb_connected
usb_unknown = ui_state.usb_unknown
chestnut_state = ui_state.chestnut_state
self._experimental_icon.set_visible(ui_state.experimental_mode) self._experimental_icon.set_visible(ui_state.experimental_mode)
if gui_app.sunnypilot_ui(): if gui_app.sunnypilot_ui():
self._set_egpu_visibility() self._set_chestnut_visibility()
else: else:
self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) self._usb_icon.set_visible(usb_connected and usb_unknown)
self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
self._mic_icon.set_visible(ui_state.recording_audio) self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(bool(ui_state.is_body)) self._body_icon.set_visible(bool(ui_state.is_body))
@@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
REFRESH_INTERVAL = 5.0 # seconds REFRESH_INTERVAL = 5.0 # seconds
@@ -62,12 +62,12 @@ class AlertItem(Widget):
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE) self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR, self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95) alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR, self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95) alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
self._title_text = "" self._title_text = ""
self._body_text = "" self._body_text = ""
@@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
# Create empty state label # Create empty state label
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
# Build initial alert list # Build initial alert list
self._build_alerts() self._build_alerts()
@@ -4,7 +4,7 @@ import pyray as rl
from collections.abc import Callable from collections.abc import Callable
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.qrcode import make_texture from openpilot.common.qrcode import make_texture
from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import SmallCircleIconButton from openpilot.system.ui.widgets.button import SmallCircleIconButton
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
if not self._camera_view.frame: if not self._camera_view.frame:
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) alignment=TextAlignment.CENTER)
rl.end_scissor_mode() rl.end_scissor_mode()
return return
@@ -74,6 +74,10 @@ class SoftwareInfoLayoutMici(Widget):
class CheckUpdateButton(BigButton): class CheckUpdateButton(BigButton):
UPDATER_PROC = "openpilot.system.updated.updated"
CHECK_FOR_UPDATE = "SIGUSR1"
DOWNLOAD_UPDATE = "SIGHUP"
def __init__(self): def __init__(self):
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
@@ -97,15 +101,20 @@ class CheckUpdateButton(BigButton):
gui_app.push_widget(dlg) gui_app.push_widget(dlg)
return return
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
def check_for_update(self):
self._signal_updater(self.CHECK_FOR_UPDATE)
def _signal_updater(self, sig: str):
self.set_enabled(False) self.set_enabled(False)
self._state = UpdaterState.WAITING_FOR_UPDATER self._state = UpdaterState.WAITING_FOR_UPDATER
self._hide_value_t = None
self.set_value("")
self.set_icon(self._txt_update_icon) self.set_icon(self._txt_update_icon)
def run(): def run():
if self.get_value() == "download update": subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
else:
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
threading.Thread(target=run, daemon=True).start() threading.Thread(target=run, daemon=True).start()
@@ -184,7 +193,7 @@ class CheckUpdateButton(BigButton):
class InstallUpdateButton(BigButton): class InstallUpdateButton(BigButton):
def __init__(self): def __init__(self):
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
def _update_state(self): def _update_state(self):
@@ -232,8 +241,9 @@ class BranchSelectPage(NavScroller):
class TargetBranchButton(BigButton): class TargetBranchButton(BigButton):
def __init__(self): def __init__(self, check_update_btn: CheckUpdateButton):
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
self._check_update_btn = check_update_btn
self.set_click_callback(self._on_click) self.set_click_callback(self._on_click)
self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
self.set_enabled(lambda: ui_state.is_offroad()) self.set_enabled(lambda: ui_state.is_offroad())
@@ -246,12 +256,15 @@ class TargetBranchButton(BigButton):
self.set_value(target) self.set_value(target)
def _on_click(self): def _on_click(self):
if not ui_state.params.get("UpdaterAvailableBranches"):
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
return
gui_app.push_widget(BranchSelectPage(self._on_select)) gui_app.push_widget(BranchSelectPage(self._on_select))
def _on_select(self, branch: str): def _on_select(self, branch: str):
ui_state.params.put("UpdaterTargetBranch", branch, block=True) ui_state.params.put("UpdaterTargetBranch", branch, block=True)
self.set_value(branch) self.set_value(branch)
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) self._check_update_btn.check_for_update()
class SoftwareLayoutMici(NavScroller): class SoftwareLayoutMici(NavScroller):
@@ -265,10 +278,11 @@ class SoftwareLayoutMici(NavScroller):
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
uninstall_openpilot_callback, exit_on_confirm=False) uninstall_openpilot_callback, exit_on_confirm=False)
check_update_btn = CheckUpdateButton()
self._scroller.add_widgets([ self._scroller.add_widgets([
SoftwareInfoLayoutMici(), SoftwareInfoLayoutMici(),
CheckUpdateButton(), check_update_btn,
InstallUpdateButton(), InstallUpdateButton(),
TargetBranchButton(), TargetBranchButton(check_update_btn),
uninstall_openpilot_btn, uninstall_openpilot_btn,
]) ])
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -333,7 +333,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alert_text1_label.set_text(alert_text1) self._alert_text1_label.set_text(alert_text1)
self._alert_text1_label.set_text_color(color) self._alert_text1_label.set_text_color(color)
self._alert_text1_label.set_font_size(font_size) self._alert_text1_label.set_font_size(font_size)
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
self._alert_text1_label.render(text_rect1) self._alert_text1_label.render(text_rect1)
alert_text2 = alert.text2.lower() alert_text2 = alert.text2.lower()
@@ -365,5 +365,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
self._alert_text2_label.set_text(alert_text2) self._alert_text2_label.set_text(alert_text2)
self._alert_text2_label.set_text_color(color) self._alert_text2_label.set_text_color(color)
self._alert_text2_label.set_font_size(small_font_size) self._alert_text2_label.set_font_size(small_font_size)
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
self._alert_text2_label.render(text_rect2) self._alert_text2_label.render(text_rect2)
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.common.filter_simple import BounceFilter from openpilot.common.filter_simple import BounceFilter
@@ -158,8 +158,8 @@ class AugmentedRoadView(CameraView):
self._confidence_ball = ConfidenceBall() self._confidence_ball = ConfidenceBall()
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
text_color=rl.Color(255, 255, 255, int(255 * 0.9)), text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
if not self._camera_view.frame: if not self._camera_view.frame:
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD, gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) alignment=TextAlignment.CENTER)
rl.end_scissor_mode() rl.end_scissor_mode()
self._publish_alert_sound(None) self._publish_alert_sound(None)
return return
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height), gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment=TextAlignment.RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
color=rl.Color(0, 0, 0, 180)) color=rl.Color(0, 0, 0, 180))
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment=TextAlignment.RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
color=rl.Color(255, 255, 255, int(255 * 0.9))) color=rl.Color(255, 255, 255, int(255 * 0.9)))
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none: if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
# Show alert level # Show alert level
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}" alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment, alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, alignment_vertical=TextAlignmentVertical.BOTTOM,
color=rl.Color(0, 0, 0, 180)) color=rl.Color(0, 0, 0, 180))
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
alignment=alignment, alignment=alignment,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, alignment_vertical=TextAlignmentVertical.BOTTOM,
color=rl.Color(255, 255, 255, int(255 * 0.9))) color=rl.Color(255, 255, 255, int(255 * 0.9)))
def _load_eye_textures(self): def _load_eye_textures(self):
@@ -3,7 +3,7 @@ import pyray as rl
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.common.constants import CV from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
@@ -107,8 +107,7 @@ class HudRenderer(Widget):
self.speed: float = 0.0 self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False self.v_ego_cluster_seen: bool = False
self._engaged: bool = False self._engaged: bool = False
self._small_model_engaged: bool = False self._chestnut_fade_time: float = 0
self._egpu_fade_time: float = 0
self._can_draw_top_icons = True self._can_draw_top_icons = True
self._show_wheel_critical = False self._show_wheel_critical = False
@@ -124,17 +123,15 @@ class HudRenderer(Widget):
self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50)
self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50)
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44)
self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44)
self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44)
self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44) self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44)
self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) self._chestnut_icon: rl.Texture | None = None
self._egpu_icon: rl.Texture | None = None
self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
def set_wheel_critical_icon(self, critical: bool): def set_wheel_critical_icon(self, critical: bool):
"""Set the wheel icon to critical or normal state.""" """Set the wheel icon to critical or normal state."""
@@ -165,13 +162,10 @@ class HudRenderer(Widget):
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
) )
engaged = sm['selfdriveState'].enabled engaged = sm['selfdriveState'].enabled
if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and
ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame):
self._small_model_engaged = True
if engaged != self._engaged:
self._egpu_fade_time = rl.get_time() if engaged else 0
if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged):
self._set_speed_changed_time = rl.get_time() self._set_speed_changed_time = rl.get_time()
if engaged != self._engaged:
self._chestnut_fade_time = rl.get_time() if engaged else 0
self._engaged = engaged self._engaged = engaged
self.set_speed = set_speed self.set_speed = set_speed
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
@@ -191,8 +185,7 @@ class HudRenderer(Widget):
if self.is_cruise_set: if self.is_cruise_set:
self._draw_set_speed(rect) self._draw_set_speed(rect)
if ui_state.usbgpu and ui_state.usbgpu_compiled: self._draw_model_source(rect)
self._draw_model_source(rect)
self._draw_steering_wheel(rect) self._draw_steering_wheel(rect)
@@ -200,30 +193,24 @@ class HudRenderer(Widget):
if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame:
return return
big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or loading = ui_state.chestnut_state == ChestnutState.LOADING
(ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and
not ui_state.sm.alive['modelV2']) or
(ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame))
self._small_model_engaged &= big_failed
loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed)
if loading: if loading:
pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) icon = self._txt_chestnut
icon = self._txt_egpu opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
opacity = 0.35 + 0.65 * pulse elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED):
elif self._small_model_engaged: icon = self._txt_chestnut_orange
icon = self._txt_egpu_crossed opacity = 1.0
opacity = 0.65 elif ui_state.chestnut_state == ChestnutState.ACTIVE:
elif big_failed: icon = self._txt_chestnut_green
icon = self._txt_egpu_orange
opacity = 1.0 opacity = 1.0
else: else:
icon = self._txt_egpu_green return
opacity = 1.0
if icon is not self._egpu_icon: if icon is not self._chestnut_icon:
self._egpu_fade_time = rl.get_time() self._chestnut_fade_time = rl.get_time()
self._egpu_icon = icon self._chestnut_icon = icon
alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE) visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE
alpha = self._chestnut_alpha_filter.update(visible)
if alpha < 1e-2: if alpha < 1e-2:
return return
@@ -6,7 +6,7 @@ from collections.abc import Callable
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.scroller import DO_ZOOM from openpilot.system.ui.widgets.scroller import DO_ZOOM
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
from openpilot.common.filter_simple import BounceFilter from openpilot.common.filter_simple import BounceFilter
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -125,10 +125,10 @@ class BigButton(Widget):
self._rotate_icon_t: float | None = None self._rotate_icon_t: float | None = None
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD, self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll, text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
line_height=0.9) line_height=0.9)
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN, self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
self._update_label_layout() self._update_label_layout()
self._load_images() self._load_images()
@@ -167,9 +167,9 @@ class BigButton(Widget):
def _update_label_layout(self): def _update_label_layout(self):
self._label.set_font_size(self._get_label_font_size()) self._label.set_font_size(self._get_label_font_size())
if self.value: if self.value:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
else: else:
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
def set_text(self, text: str): def set_text(self, text: str):
self.text = text self.text = text
@@ -356,8 +356,8 @@ class GreyBigButton(BigButton):
self._sub_label.set_font_size(36) self._sub_label.set_font_size(36)
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR) self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) TextAlignmentVertical.BOTTOM)
self._sub_label.set_line_height(0.95) self._sub_label.set_line_height(0.95)
@property @property
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from openpilot.cereal import messaging, log from openpilot.cereal import messaging, log
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.common.hardware import COMMA_HARDWARE from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
# font size is set dynamically # font size is set dynamically
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) text_alignment_vertical=TextAlignmentVertical.TOP)
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) text_alignment_vertical=TextAlignmentVertical.TOP)
def get_alert(self, sm: messaging.SubMaster) -> Alert | None: def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
"""Generate the current alert based on selfdrive state.""" """Generate the current alert based on selfdrive state."""
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
from openpilot.selfdrive.ui.ui_state import ui_state, device from openpilot.selfdrive.ui.ui_state import ui_state, device
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
tr("camera starting"), tr("camera starting"),
font_size=100, font_size=100,
font_weight=FontWeight.BOLD, font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
) )
return -1 return -1
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
""" """
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING 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.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.multilang import tr, trn from openpilot.system.ui.lib.multilang import tr, trn
from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets.label import gui_label
@@ -59,7 +59,7 @@ class HomeLayoutSP(HomeLayout):
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE) desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
desc_width = desc_size.x desc_width = desc_size.x
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height) 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) gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=TextAlignment.RIGHT)
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE) brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
spacing = BRAND_DESC_SPACING if description else 0 spacing = BRAND_DESC_SPACING if description else 0
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
""" """
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight, TextAlignment
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
self._done_callback = done_callback self._done_callback = done_callback
self._step = 0 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.AUDIOWIDE, text_alignment=TextAlignment.LEFT))
self._content = [ self._content = [
{ {
@@ -43,7 +43,7 @@ class SunnylinkConsentPage(Widget):
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))) self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))) self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))) self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)) self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT))
def _handle_choice(self, choice): def _handle_choice(self, choice):
if choice == "enable": if choice == "enable":
@@ -62,14 +62,14 @@ class ModelsLayout(Widget):
self.big_model_item = ListItemSP( self.big_model_item = ListItemSP(
title=tr("Big Model"), title=tr("Big Model"),
action_item=ScrollingButtonAction(tr("SELECT")), action_item=ScrollingButtonAction(tr("SELECT")),
callback=lambda: self._open_source_dialog("usbgpu") callback=lambda: self._open_source_dialog("chestnut")
) )
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status")) self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status"))
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "", self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0), lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 0), ui_state.params.put("ModelManager_LastSyncTime_Chestnut", 0),
gui_app.push_widget(alert_dialog(tr("Fetching Latest Models"))))) gui_app.push_widget(alert_dialog(tr("Fetching Latest Models")))))
self.clear_cache_item = ListItemSP( self.clear_cache_item = ListItemSP(
@@ -177,14 +177,14 @@ class ModelsLayout(Widget):
big_state = big_model_state() big_state = big_model_state()
carry_source, carry_internal, _ = carrying_model() carry_source, carry_internal, _ = carrying_model()
segments = [] segments = []
for source, label in (("qcom", tr("small")), ("usbgpu", tr("big"))): for source, label in (("qcom", tr("small")), ("chestnut", tr("big"))):
if segments: if segments:
segments.append(("|", rl.GRAY, None, None)) segments.append(("|", rl.GRAY, None, None))
bundle = get_selected_bundle(ui_state.params, source) bundle = get_selected_bundle(ui_state.params, source)
name = bundle.internalName if bundle else default_model_name(source) name = bundle.internalName if bundle else default_model_name(source)
color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY
name = "" + name name = "" + name
if source == "usbgpu": if source == "chestnut":
if big_state == 'failed': if big_state == 'failed':
color = rl.RED color = rl.RED
elif big_state == 'loading': elif big_state == 'loading':
@@ -208,10 +208,10 @@ class ModelsLayout(Widget):
"""The failover story for the Model Status row. One-way big -> small, and the """The failover story for the Model Status row. One-way big -> small, and the
fallback is runner-matched: a Default big can only fall back to the Default fallback is runner-matched: a Default big can only fall back to the Default
small (stock modeld), a custom big has no automatic fallback yet.""" small (stock modeld), a custom big has no automatic fallback yet."""
if not ui_state.usbgpu: if not ui_state.chestnut_present:
return "" return ""
big_bundle = get_selected_bundle(ui_state.params, "usbgpu") big_bundle = get_selected_bundle(ui_state.params, "chestnut")
big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu") big_name = big_bundle.internalName if big_bundle else default_model_name("chestnut")
big_is_default = big_bundle is None big_is_default = big_bundle is None
fallback_name = default_model_name("qcom") fallback_name = default_model_name("qcom")
state = big_model_state() state = big_model_state()
@@ -225,7 +225,7 @@ class ModelsLayout(Widget):
return tr("Getting the big model ready.") return tr("Getting the big model ready.")
if big_is_default: if big_is_default:
return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name) return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name)
return tr("{} will drive when the eGPU is ready.").format(big_name) return tr("{} will drive when the chestnut is ready.").format(big_name)
@staticmethod @staticmethod
def _download_row_state(progresses, name: str) -> dict: def _download_row_state(progresses, name: str) -> dict:
@@ -261,7 +261,7 @@ class ModelsLayout(Widget):
ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref) ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref)
def _resolve_selected_bundle(self, ref): def _resolve_selected_bundle(self, ref):
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")}
resolved = resolve_bundle_by_ref(ref, source_bundles) resolved = resolve_bundle_by_ref(ref, source_bundles)
return resolved[0] if resolved else None return resolved[0] if resolved else None
@@ -329,7 +329,7 @@ class ModelsLayout(Widget):
self._handle_bundle_download_progress() self._handle_bundle_download_progress()
carry_source, _, carry_display = carrying_model() carry_source, _, carry_display = carrying_model()
for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")): for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "chestnut")):
bundle = get_selected_bundle(ui_state.params, item_source) bundle = get_selected_bundle(ui_state.params, item_source)
name = bundle.displayName if bundle else default_model_name(item_source) name = bundle.displayName if bundle else default_model_name(item_source)
color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR
@@ -9,7 +9,7 @@ from openpilot.cereal import custom
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
@@ -32,8 +32,8 @@ class SunnylinkHeader(Widget):
font_size=90, font_size=90,
font_weight=FontWeight.AUDIOWIDE, font_weight=FontWeight.AUDIOWIDE,
text_color=rl.WHITE, text_color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
wrap_text=False, wrap_text=False,
elide=False elide=False
) )
@@ -43,8 +43,8 @@ class SunnylinkHeader(Widget):
font_size=40, font_size=40,
font_weight=FontWeight.NORMAL, font_weight=FontWeight.NORMAL,
text_color=rl.Color(0, 255, 0, 255), # Green text_color=rl.Color(0, 255, 0, 255), # Green
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
wrap_text=True, wrap_text=True,
elide=False elide=False
) )
@@ -55,8 +55,8 @@ class SunnylinkHeader(Widget):
font_size=35, font_size=35,
font_weight=FontWeight.NORMAL, font_weight=FontWeight.NORMAL,
text_color=rl.Color(255, 165, 0, 255), # Orange text_color=rl.Color(255, 165, 0, 255), # Orange
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
wrap_text=True, wrap_text=True,
elide=False elide=False
) )
@@ -109,8 +109,8 @@ class SunnylinkDescriptionItem(Widget):
font_size=40, font_size=40,
font_weight=FontWeight.NORMAL, font_weight=FontWeight.NORMAL,
text_color=rl.WHITE, text_color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical=TextAlignmentVertical.TOP,
wrap_text=True, wrap_text=True,
elide=False, elide=False,
) )
@@ -9,7 +9,7 @@ import math
import pyray as rl import pyray as rl
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr_noop from openpilot.system.ui.lib.multilang import tr_noop
@@ -21,8 +21,8 @@ METRIC_MARGIN = 30
METRIC_START_Y = 300 METRIC_START_Y = 300
HOME_BTN = rl.Rectangle(60, 860, 180, 180) HOME_BTN = rl.Rectangle(60, 860, 180, 180)
EGPU_ICON_WIDTH = 180 CHESTNUT_ICON_WIDTH = 180
EGPU_ICON_HEIGHT = 133 CHESTNUT_ICON_HEIGHT = 133
# Color scheme # Color scheme
@@ -59,10 +59,9 @@ class MetricData:
class SidebarSP: class SidebarSP:
def __init__(self): def __init__(self):
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING) self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
self._egpu_green_img = gui_app.texture("icons_mici/egpu_green.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) self._chestnut_green_img = gui_app.texture("icons_mici/chestnut_green.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
self._egpu_default_img = gui_app.texture("icons_mici/egpu.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) self._chestnut_default_img = gui_app.texture("icons_mici/chestnut.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
self._egpu_orange_img = gui_app.texture("icons_mici/egpu_orange.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT) self._chestnut_orange_img = gui_app.texture("icons_mici/chestnut_orange.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
self._egpu_gray_img = gui_app.texture("icons_mici/egpu_gray.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
def _update_sunnylink_status(self): def _update_sunnylink_status(self):
if not ui_state.params.get_bool("SunnylinkEnabled"): if not ui_state.params.get_bool("SunnylinkEnabled"):
@@ -90,22 +89,17 @@ class SidebarSP:
def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]: def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]:
default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y) default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y)
if not ui_state.sm["deviceState"].chestnutPresent: state = ui_state.chestnut_state
if state == ChestnutState.DISCONNECTED:
return default_img, default_pos, 1.0 return default_img, default_pos, 1.0
big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad if state == ChestnutState.LOADING:
big_model_failed = ui_state.started and ui_state.big_model_failed icon = self._chestnut_default_img
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
if loading:
icon = self._egpu_default_img
opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
elif big_model_selected and big_model_failed: elif state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED):
icon, opacity = self._egpu_orange_img, 1.0 icon, opacity = self._chestnut_orange_img, 1.0
elif big_model_selected:
icon, opacity = self._egpu_green_img, 1.0
else: else:
icon, opacity = self._egpu_gray_img, 1.0 icon, opacity = self._chestnut_green_img, 1.0
x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2 x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2
y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2 y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2
@@ -9,9 +9,8 @@ import math
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.widgets.icon_widget import IconWidget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -19,35 +18,16 @@ class MiciHomeLayoutSP(MiciHomeLayout):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
self._egpu_icon_default = IconWidget("icons_mici/egpu.png", (50, 37))
self._egpu_icon_default.set_visible(False)
self._egpu_icon_orange = IconWidget("icons_mici/egpu_orange.png", (50, 37))
self._egpu_icon_orange.set_visible(False)
gray_idx = self._status_bar_layout.widgets.index(self._egpu_icon_gray)
self._status_bar_layout.widgets.insert(gray_idx + 1, self._egpu_icon_default)
self._status_bar_layout.widgets.insert(gray_idx + 2, self._egpu_icon_orange)
def _set_egpu_visibility(self): def _set_chestnut_visibility(self):
chestnut = ui_state.sm["deviceState"].chestnutPresent usb_connected = ui_state.usb_connected
if not chestnut: usb_unknown = ui_state.usb_unknown
self._egpu_icon.set_visible(False) chestnut_state = ui_state.chestnut_state
self._egpu_icon_default.set_visible(False) loading = chestnut_state == ChestnutState.LOADING
self._egpu_icon_orange.set_visible(False)
self._egpu_icon_gray.set_visible(False)
return
big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad self._usb_icon.set_visible(usb_connected and usb_unknown)
big_model_failed = ui_state.started and ui_state.big_model_failed self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None) self._chestnut_loading_icon.set_visible(not usb_unknown and loading)
self._chestnut_icon.set_visible(not usb_unknown and not loading and
if loading: chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
self._egpu_icon_default._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
self._egpu_icon_default.set_visible(True)
self._egpu_icon.set_visible(False)
self._egpu_icon_orange.set_visible(False)
self._egpu_icon_gray.set_visible(False)
else:
self._egpu_icon_default.set_visible(False)
self._egpu_icon.set_visible(big_model_selected and not big_model_failed)
self._egpu_icon_orange.set_visible(big_model_selected and big_model_failed)
self._egpu_icon_gray.set_visible(not big_model_selected)
@@ -27,14 +27,14 @@ def _model_info() -> tuple[str, str, str]:
state = big_model_state() state = big_model_state()
_, _, carry_display = carrying_model() _, _, carry_display = carrying_model()
if carry_display is None: if carry_display is None:
big = get_selected_bundle(ui_state.params, "usbgpu") big = get_selected_bundle(ui_state.params, "chestnut")
carry_display = big.displayName if big else default_model_name("usbgpu") carry_display = big.displayName if big else default_model_name("chestnut")
active_text = (carry_display or active_name).lower() active_text = (carry_display or active_name).lower()
if state == 'failed': if state == 'failed':
return active_text, tr("big model"), tr("unavailable") return active_text, tr("big model"), tr("unavailable")
if state == 'loading': if state == 'loading':
return active_text, tr("big model"), tr("getting ready") return active_text, tr("big model"), tr("getting ready")
header = tr("small model") if source == "usbgpu" else tr("big model") header = tr("small model") if source == "chestnut" else tr("big model")
return active_text, header, other_name.lower() return active_text, header, other_name.lower()
@@ -113,7 +113,7 @@ class ModelsLayoutMici(NavScroller):
hardware_btns = [] hardware_btns = []
active = active_source() active = active_source()
for source, label in (("qcom", tr("small models")), ("usbgpu", tr("big models"))): for source, label in (("qcom", tr("small models")), ("chestnut", tr("big models"))):
bundle = get_selected_bundle(ui_state.params, source) bundle = get_selected_bundle(ui_state.params, source)
value = (bundle.internalName if bundle else default_model_name(source)).lower() value = (bundle.internalName if bundle else default_model_name(source)).lower()
if source == active: if source == active:
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
import pyray as rl import pyray as rl
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators
@@ -22,8 +21,6 @@ class HudRendererSP(HudRenderer):
def _render(self, rect: rl.Rectangle) -> None: def _render(self, rect: rl.Rectangle) -> None:
super()._render(rect) super()._render(rect)
if ui_state.usbgpu and not ui_state.usbgpu_compiled and ui_state.model_runner_tinygrad:
self._draw_model_source(rect)
self.blind_spot_indicators.render(rect) self.blind_spot_indicators.render(rect)
def _has_blind_spot_detected(self) -> bool: def _has_blind_spot_detected(self) -> bool:
+17 -20
View File
@@ -4,15 +4,15 @@ 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. 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. See the LICENSE.md file in the root directory for more details.
""" """
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
from openpilot.sunnypilot.models.fetcher import get_cached_bundles from openpilot.sunnypilot.models.fetcher import get_cached_bundles
from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref
from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL
def active_source() -> str: def active_source() -> str:
return get_active_source(usbgpu=ui_state.usbgpu, return get_active_source(chestnut=ui_state.chestnut_present,
usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_loading, chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading,
offroad=ui_state.is_offroad()) offroad=ui_state.is_offroad())
@@ -23,7 +23,7 @@ def bundles_for_source(source: str):
def default_model(source: str) -> str: def default_model(source: str) -> str:
return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL return DEFAULT_BIG_MODEL if source == 'chestnut' else DEFAULT_MODEL
def default_model_name(source: str) -> str: def default_model_name(source: str) -> str:
@@ -31,13 +31,10 @@ def default_model_name(source: str) -> str:
def big_model_state() -> str | None: def big_model_state() -> str | None:
"""'failed' | 'loading' | None, mirroring the sidebar's detection (#1969).""" """'failed' | 'loading' | None, from the same state the icons render."""
if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed: return {ChestnutState.UNCOMPILED: 'failed',
return 'failed' ChestnutState.FAILED: 'failed',
big_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad ChestnutState.LOADING: 'loading'}.get(ui_state.chestnut_state)
if ui_state.usbgpu_loading or (big_selected and ui_state.started and ui_state.usbgpu_active is None):
return 'loading'
return None
def carrying_model() -> tuple[str | None, str | None, str | None]: def carrying_model() -> tuple[str | None, str | None, str | None]:
@@ -45,14 +42,14 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
when a Default big cannot carry, stock modeld runs the Default small, never the when a Default big cannot carry, stock modeld runs the Default small, never the
small slot's pick; a custom big has no automatic fallback yet -> (None, None, None).""" small slot's pick; a custom big has no automatic fallback yet -> (None, None, None)."""
source = active_source() source = active_source()
if source == "usbgpu": if source == "chestnut":
bundle = get_selected_bundle(ui_state.params, "usbgpu") bundle = get_selected_bundle(ui_state.params, "chestnut")
if bundle: if bundle:
return "usbgpu", bundle.internalName, bundle.displayName return "chestnut", bundle.internalName, bundle.displayName
name = default_model_name("usbgpu") name = default_model_name("chestnut")
return "usbgpu", name, name return "chestnut", name, name
if ui_state.usbgpu: if ui_state.chestnut_present:
if get_selected_bundle(ui_state.params, "usbgpu") is None: if get_selected_bundle(ui_state.params, "chestnut") is None:
name = default_model_name("qcom") name = default_model_name("qcom")
return "qcom", name, name return "qcom", name, name
return None, None, None return None, None, None
@@ -66,7 +63,7 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
def queued_name(current_ref) -> str | None: def queued_name(current_ref) -> str | None:
ref = ui_state.params.get("ModelManager_DownloadRef") ref = ui_state.params.get("ModelManager_DownloadRef")
if ref and ref != current_ref: if ref and ref != current_ref:
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")} source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")}
if resolved := resolve_bundle_by_ref(ref, source_bundles): if resolved := resolve_bundle_by_ref(ref, source_bundles):
return resolved[0].internalName return resolved[0].internalName
return None return None
@@ -79,7 +76,7 @@ def model_info() -> tuple[str, str, str]:
manager republishes a tick after a chestnut change, so the stale bundle manager republishes a tick after a chestnut change, so the stale bundle
would flash the wrong model.""" would flash the wrong model."""
source = active_source() source = active_source()
other = "qcom" if source == "usbgpu" else "usbgpu" other = "qcom" if source == "chestnut" else "chestnut"
active_bundle = get_selected_bundle(ui_state.params, source) active_bundle = get_selected_bundle(ui_state.params, source)
other_bundle = get_selected_bundle(ui_state.params, other) other_bundle = get_selected_bundle(ui_state.params, other)
@@ -152,10 +152,13 @@ class UIStateSP:
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement") self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self._enforce_constraints() self._enforce_constraints()
source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active, source = get_active_source(chestnut=self.chestnut_present, chestnut_active=self.chestnut_active,
usbgpu_loading=self.usbgpu_loading, offroad=self.is_offroad()) chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source]) self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad" self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
# chestnut just the same, so ChestnutState has to see it as available too.
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
self.blindspot = self.params.get_bool("BlindSpot") self.blindspot = self.params.get_bool("BlindSpot")
self.chevron_metrics = self.params.get("ChevronInfo") self.chevron_metrics = self.params.get("ChevronInfo")
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True) self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
+65 -20
View File
@@ -12,7 +12,8 @@ from openpilot.common.swaglog import cloudlog
from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.common.hardware import HARDWARE, PC from openpilot.common.hardware import HARDWARE, PC
from openpilot.selfdrive.modeld.helpers import usbgpu_compiled from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
@@ -28,6 +29,15 @@ class UIStatus(Enum):
LONG_ONLY = "long_only" LONG_ONLY = "long_only"
class ChestnutState(Enum):
DISCONNECTED = "disconnected"
UNCOMPILED = "uncompiled"
READY = "ready"
LOADING = "loading"
ACTIVE = "active"
FAILED = "failed"
class UIState(UIStateSP): class UIState(UIStateSP):
_instance: 'UIState | None' = None _instance: 'UIState | None' = None
@@ -82,10 +92,15 @@ class UIState(UIStateSP):
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed")
self.usbgpu: bool = False self.chestnut_present: bool = False
self.usbgpu_compiled: bool = usbgpu_compiled() self.chestnut_compiled: bool = chestnut_compiled()
self.usbgpu_active: bool | None = self.params.get("UsbGpuActive") self.chestnut_active: bool | None = None
self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") self.chestnut_loading: bool = False
self.usb_connected: bool = False
self.usb_connected_ts: float | None = None
self.usb_disconnected_ts: float | None = None
self.usb_unknown: bool = False
self.chestnut_state = ChestnutState.DISCONNECTED
self.started: bool = False self.started: bool = False
self.ignition: bool = False self.ignition: bool = False
self.recording_audio: bool = False self.recording_audio: bool = False
@@ -112,15 +127,6 @@ class UIState(UIStateSP):
def add_on_body_changed_callbacks(self, callback: Callable[[], None]): def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
self._on_body_changed_callbacks.append(callback) self._on_body_changed_callbacks.append(callback)
@property
def big_model_failed(self) -> bool:
# Mirrors the onroad HUD's four-condition check so sidebar and home icons reflect the same failure states
return (self.usbgpu_active is False or
not self.sm['deviceState'].chestnutPresent or
(self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and
not self.sm.alive['modelV2']) or
(self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame))
@property @property
def engaged(self) -> bool: def engaged(self) -> bool:
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled) return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
@@ -140,6 +146,7 @@ class UIState(UIStateSP):
self.sm.update(0) self.sm.update(0)
self._update_state() self._update_state()
self._update_status() self._update_status()
self._update_chestnut_state()
device.update() device.update()
UIStateSP.update(self) UIStateSP.update(self)
@@ -203,12 +210,35 @@ class UIState(UIStateSP):
self.status = UIStatus.DISENGAGED self.status = UIStatus.DISENGAGED
self.started_frame = self.sm.frame self.started_frame = self.sm.frame
self.started_time = time.monotonic() self.started_time = time.monotonic()
self.chestnut_present = self.sm["deviceState"].chestnutPresent
for callback in self._offroad_transition_callbacks: for callback in self._offroad_transition_callbacks:
callback() callback()
self._started_prev = self.started self._started_prev = self.started
def _update_chestnut_state(self) -> None:
detected = self.sm["deviceState"].chestnutPresent
if not self.started:
self.chestnut_present = detected
self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else
ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED)
return
model_seen = self.sm.recv_frame["modelV2"] > self.started_frame
if not self.chestnut_present:
self.chestnut_state = ChestnutState.DISCONNECTED
elif not self.chestnut_compiled:
self.chestnut_state = ChestnutState.UNCOMPILED
elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)):
self.chestnut_state = ChestnutState.FAILED
elif self.chestnut_loading or not model_seen:
self.chestnut_state = ChestnutState.LOADING
elif self.chestnut_active is False:
self.chestnut_state = ChestnutState.FAILED
else:
self.chestnut_state = ChestnutState.ACTIVE
def update_params(self) -> None: def update_params(self) -> None:
# For slower operations # For slower operations
# Update longitudinal control state # Update longitudinal control state
@@ -225,12 +255,27 @@ class UIState(UIStateSP):
self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.always_on_dm = self.params.get_bool("AlwaysOnDM")
self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode = self.params.get_bool("ExperimentalMode")
self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed")
# keep usbgpu UI active until offroad transition when gpu disappears if not self.chestnut_compiled:
self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started) self.chestnut_compiled = chestnut_compiled()
if not self.usbgpu_compiled: self.chestnut_active = self.params.get("ChestnutActive")
self.usbgpu_compiled = usbgpu_compiled() self.chestnut_loading = self.params.get_bool("ChestnutLoading")
self.usbgpu_active = self.params.get("UsbGpuActive") now = time.monotonic()
self.usbgpu_loading = self.params.get_bool("UsbGpuLoading") if read_int(TYPEC_CC_ORIENTATION_PATH) != 0:
self.usb_disconnected_ts = None
if not self.usb_connected:
self.usb_connected = True
self.usb_connected_ts = now
self.usb_unknown = False
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
self.usb_connected_ts = None
elif self.usb_connected:
if self.usb_disconnected_ts is None:
self.usb_disconnected_ts = now
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
self.usb_connected = False
self.usb_connected_ts = None
self.usb_unknown = False
UIStateSP.update_params(self) UIStateSP.update_params(self)
@@ -298,7 +298,7 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
if __name__ == "__main__": if __name__ == "__main__":
if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'): if 'USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT'):
from openpilot.system.hardware.chestnut.flash import link_up from openpilot.system.hardware.chestnut.flash import link_up
for _ in range(10): for _ in range(10):
if link_up(): if link_up():
+56 -42
View File
@@ -8,22 +8,22 @@ See the LICENSE.md file in the root directory for more details.
import os import os
os.environ['GMMU'] = '0' os.environ['GMMU'] = '0'
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob
import time
import numpy as np import numpy as np
import threading
import time
from setproctitle import setproctitle
from tinygrad.tensor import Tensor
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
from openpilot.common.hardware import COMMA_HARDWARE
from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob
from openpilot.cereal import log from openpilot.cereal import log
from opendbc.car.structs import car from opendbc.car.structs import car
from openpilot.cereal.services import SERVICE_LIST from openpilot.cereal.services import SERVICE_LIST
from setproctitle import setproctitle
from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.messaging import PubMaster, SubMaster
from openpilot.cereal.visionipc import VisionStreamType from openpilot.cereal.visionipc import VisionStreamType
from msgq.visionipc import VisionIpcClient, VisionBuf from msgq.visionipc import VisionIpcClient, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params from opendbc.car.car_helpers import get_demo_car_params
from tinygrad.tensor import Tensor
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params from openpilot.common.params import Params
@@ -42,13 +42,13 @@ from openpilot.sunnypilot.modeld_v2.constants import Plan
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants 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.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.helpers import get_active_bundle
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad" PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad"
BIG_MODEL_TIMEOUT = 60
def _pkl_exists(path): def _pkl_exists(path):
@@ -68,6 +68,7 @@ def _find_driving_pkl(bundle):
pkl_path = os.path.join(model_root, pkl_name) pkl_path = os.path.join(model_root, pkl_name)
if _pkl_exists(pkl_path): if _pkl_exists(pkl_path):
return pkl_path return pkl_path
return None
class FrameMeta: class FrameMeta:
@@ -84,14 +85,14 @@ class ModelState(ModelStateBase):
inputs: dict[str, np.ndarray] inputs: dict[str, np.ndarray]
prev_desire: np.ndarray prev_desire: np.ndarray
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False): def __init__(self, cam_w: int, cam_h: int, chestnut: bool = False):
ModelStateBase.__init__(self) ModelStateBase.__init__(self)
env_pkl = os.environ.get('COMBINED_MODEL_PKL') env_pkl = os.environ.get('COMBINED_MODEL_PKL')
if env_pkl and os.path.exists(env_pkl): if env_pkl and os.path.exists(env_pkl):
model_bundle = None model_bundle = None
else: else:
model_bundle = get_active_bundle(usbgpu=usbgpu) model_bundle = get_active_bundle(chestnut=chestnut)
self.generation = model_bundle.generation if model_bundle is not None else None self.generation = model_bundle.generation if model_bundle is not None else None
overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {} overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {}
@@ -99,10 +100,10 @@ class ModelState(ModelStateBase):
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
self.MIN_LAT_CONTROL_SPEED = 0.3 self.MIN_LAT_CONTROL_SPEED = 0.3
self.PLANPLUS_CONTROL: float = 1.0 self.PLANPLUS_CONTROL: float = 1.0
self.usbgpu = usbgpu self.chestnut = chestnut
pkl_path = _find_driving_pkl(model_bundle) pkl_path = _find_driving_pkl(model_bundle)
assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" assert pkl_path is not None, f"No driving pkl found for {'chestnut' if chestnut else 'small model'} — all models must be compiled with compile_modeld.py"
self._init_combined(pkl_path, cam_w, cam_h, model_bundle) self._init_combined(pkl_path, cam_w, cam_h, model_bundle)
def _init_combined(self, pkl_path, cam_w, cam_h, bundle): def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
@@ -110,7 +111,7 @@ class ModelState(ModelStateBase):
jits = load_oob(open_file_chunked(pkl_path)) jits = load_oob(open_file_chunked(pkl_path))
self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU'
self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV self.DEV = 'AMD' if self.chestnut else self.WARP_DEV
self.QUEUE_DEV = self.DEV self.QUEUE_DEV = self.DEV
metadata = jits['metadata'] metadata = jits['metadata']
@@ -185,9 +186,6 @@ class ModelState(ModelStateBase):
else: else:
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor)
if self.usbgpu:
self.warmup()
def warmup(self) -> None: def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names} dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names}
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k} transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
@@ -287,9 +285,8 @@ class ModelState(ModelStateBase):
buf[0, :-1] = buf[0, 1:] buf[0, :-1] = buf[0, 1:]
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
cloudlog.error("model output not finite, dropping frame") raise RuntimeError("model output not finite")
return None
return outputs return outputs
@@ -327,13 +324,13 @@ def main(demo=False):
setproctitle(PROCESS_NAME) setproctitle(PROCESS_NAME)
config_realtime_process(7, 54) config_realtime_process(7, 54)
USBGPU = usbgpu_present() CHESTNUT = chestnut_present()
if USBGPU: if CHESTNUT:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params() params = Params()
params.put_bool("UsbGpuLoading", USBGPU) params.put_bool("ChestnutLoading", CHESTNUT)
params.remove("UsbGpuActive") params.remove("ChestnutActive")
# visionipc clients # visionipc clients
while True: while True:
@@ -362,31 +359,36 @@ def main(demo=False):
st = time.monotonic() st = time.monotonic()
model = None model = None
if USBGPU: if CHESTNUT:
import threading big_model = None
def load(): def load_big():
nonlocal model nonlocal big_model
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True) try:
t = threading.Thread(target=load, daemon=True) m = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True)
t.start() m.warmup()
t.join(60) big_model = m
if model is None: except Exception:
params.put_bool("UsbGpuActive", False) cloudlog.exception("chestnut load failed")
raise RuntimeError("eGPU model load failed or timed out (60s)") loader = threading.Thread(target=load_big, daemon=True)
params.put_bool("UsbGpuActive", True) loader.start()
else: loader.join(BIG_MODEL_TIMEOUT)
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False) model = big_model
params.put_bool("ChestnutActive", model is not None)
params.put_bool("UsbGpuLoading", False) small_model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) if model is None or CHESTNUT else None
if model is None:
model = small_model
params.put_bool("ChestnutLoading", False)
assert model is not None
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging # messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else []) pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
pm = PubMaster(pub_socks) pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState() publish_state = PublishState()
chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
# setup filter to track dropped frames # setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
@@ -509,7 +511,19 @@ def main(demo=False):
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32) inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
mt1 = time.perf_counter() mt1 = time.perf_counter()
model_output = model.run(bufs, transforms, inputs, prepare_only) try:
model_output = model.run(bufs, transforms, inputs, prepare_only)
except Exception:
if not params.get_bool("ChestnutActive"):
raise
cloudlog.exception("chestnut failed, falling back to small")
params.put_bool("ChestnutActive", False)
assert small_model is not None
model = small_model
if chestnut_state is not None:
chestnut_state.big = False
run_count = 0
model_output = None
mt2 = time.perf_counter() mt2 = time.perf_counter()
model_execution_time = mt2 - mt1 model_execution_time = mt2 - mt1
@@ -524,7 +538,7 @@ def main(demo=False):
fill_model_msg(drivingdata_send, modelv2_send, model_output, action, fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants)
modelv2_send.modelV2.big = model.usbgpu modelv2_send.modelV2.big = model.chestnut
desire_state = modelv2_send.modelV2.meta.desireState desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
@@ -190,8 +190,8 @@ def tmp_path():
def patch_modeld(monkeypatch): def patch_modeld(monkeypatch):
def _patch(bundle): def _patch(bundle):
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
return _patch return _patch
@@ -59,8 +59,8 @@ class TestFindDrivingPkl(OpenpilotTestCase):
class TestModelStateCombinedInit(OpenpilotTestCase): class TestModelStateCombinedInit(OpenpilotTestCase):
def test_asserts_when_no_pkl(self, monkeypatch): def test_asserts_when_no_pkl(self, monkeypatch):
bundle = DummyBundle(models=[], is_20hz=True) bundle = DummyBundle(models=[], is_20hz=True)
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle) monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
with self.assertRaisesRegex(AssertionError, "No driving pkl found"): with self.assertRaisesRegex(AssertionError, "No driving pkl found"):
ModelState(cam_w=CAM_W, cam_h=CAM_H) ModelState(cam_w=CAM_W, cam_h=CAM_H)
@@ -75,11 +75,11 @@ class TestStockEquivalence(OpenpilotTestCase):
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES) frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)} 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') stock_queues, stock_npy, _frame_views = make_input_queues(stock_shapes, frame_skip, device='NPY', frame_copy_size=49152)
assert set(state.input_queues.keys()) == set(stock_queues.keys()) # sunnypilot split pipeline has tfm/big_tfm as queues (stock has them in npy only)
assert set(stock_queues.keys()) <= set(state.input_queues.keys())
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys()) assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
def test_split_queue_keys_work_with_desire_key(self, model_state_factory): 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 from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
import os import os
import tempfile import tempfile
import unittest
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
@@ -248,23 +249,27 @@ class TestStockCompileModeldEquivalence(OpenpilotTestCase):
input_shapes = { input_shapes = {
'img': (1, 12, 128, 256), 'img': (1, 12, 128, 256),
'desire_pulse': (1, 25, 8), 'desire_pulse': (1, 25, 8),
'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512 'features_buffer': (1, 24, 32, 512),
'traffic_convention': (1, 2), 'traffic_convention': (1, 2),
'action_t': (1, 2), 'action_t': (1, 2),
} }
frame_skip = 4 frame_skip = 4
stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY') stock_queues, stock_npy, _frame_views = stock_make_input_queues(input_shapes, frame_skip, device='NPY', frame_copy_size=49152)
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY') sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
assert set(sunny_queues.keys()) == set(stock_queues.keys()) # sunnypilot split pipeline has tfm/big_tfm as queues; packed_npy_inputs size differs (different frame packing)
assert set(stock_queues.keys()) <= set(sunny_queues.keys())
for key in stock_queues: for key in stock_queues:
if key == 'packed_npy_inputs':
continue
assert sunny_queues[key].shape == stock_queues[key].shape, \ assert sunny_queues[key].shape == stock_queues[key].shape, \
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}" f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
assert set(sunny_npy.keys()) == set(stock_npy.keys()) assert set(stock_npy.keys()) <= set(sunny_npy.keys())
for key in stock_npy: for key in stock_npy:
assert sunny_npy[key].shape == stock_npy[key].shape, \ assert sunny_npy[key].shape == stock_npy[key].shape, \
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}" f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
@unittest.skip("upstream removed make_warp_input_queues — warp merged into run_model")
def test_make_warp_queues_stock_equivalence(self): def test_make_warp_queues_stock_equivalence(self):
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
@@ -0,0 +1,62 @@
"""
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 io
import requests
from openpilot.common.file_chunker import get_chunk_name
from openpilot.common.hardware import hw
from openpilot.common.test import OpenpilotTestCase
from openpilot.selfdrive.modeld.helpers import dump_oob
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
from openpilot.sunnypilot.modeld_v2.tests import helpers as tests_helpers
from openpilot.sunnypilot.modeld_v2.tests.helpers import DummyModel, DummyBundle, CAM_W, CAM_H
from openpilot.sunnypilot.models.fetcher import ModelParser, ModelFetcher
tmp_path = tests_helpers.tmp_path
class TestFallback(OpenpilotTestCase):
def test_find_dual_model_in_bundle(self, tmp_path, monkeypatch):
lebowski_file = 'driving_lebowski.pkl'
tsfdo_file = 'driving_tsfdo.pkl'
(tmp_path / lebowski_file).write_bytes(b'fkasdjfkljf')
(tmp_path / tsfdo_file).write_bytes(b'dskfajklsdjlsfka')
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
big_bundle = DummyBundle(models=[DummyModel('supercombo', lebowski_file)])
small_bundle = DummyBundle(models=[DummyModel('supercombo', tsfdo_file)])
big_pkl = modeld_module._find_driving_pkl(big_bundle)
small_pkl = modeld_module._find_driving_pkl(small_bundle)
assert big_pkl is not None and lebowski_file in big_pkl
assert small_pkl is not None and tsfdo_file in small_pkl
def test_download_models_and_init_modelstate_fallback(self, tmp_path, monkeypatch):
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
big_json = requests.get(ModelFetcher.MODEL_URL_CHESTNUT).json()
big_bundle = ModelParser.parse_models(big_json)[-1]
small_json = requests.get(ModelFetcher.MODEL_URL).json()
small_bundle = ModelParser.parse_models(small_json)[-1]
buf = io.BytesIO()
dump_oob(tests_helpers.make_pkl_data(tests_helpers.ARCHETYPES['supercombo_non20hz']), buf)
oob_bytes = buf.getvalue()
for bundle in (big_bundle, small_bundle):
artifact = bundle.models[0].artifact
for i in range(len(artifact.chunks)):
(tmp_path / get_chunk_name(artifact.fileName, i, len(artifact.chunks))).write_bytes(oob_bytes if i == 0 else b"")
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: small_bundle)
assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=False).chestnut is False
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: big_bundle)
try:
assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=True).chestnut is True
except Exception as e:
assert "AMD" in str(e) or "device" in str(e).lower()
+51 -5
View File
@@ -1,43 +1,89 @@
import argparse import argparse
import os import os
import hashlib import hashlib
import requests
import re
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
from openpilot.sunnypilot.models.fetcher import ModelFetcher
def get_default_model() -> str: def get_default_model() -> str:
show_big_model = (ui_state.usbgpu show_big_model = (ui_state.chestnut_present
and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad())) and (ui_state.chestnut_active or ui_state.chestnut_loading or ui_state.is_offroad()))
return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash")
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx") SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx")
def _read_model_name_fields():
with open(DEFAULT_MODEL_NAME_PATH) as f:
content = f.read()
fields = {}
for line in content.splitlines():
if "=" in line:
key, val = line.split("=", 1)
fields[key.strip()] = val.strip().strip('"')
return fields
def update_model_hash(): def update_model_hash():
fields = _read_model_name_fields()
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
with open(MODEL_HASH_PATH, "w") as f: with open(MODEL_HASH_PATH, "w") as f:
f.write(combined_hash) f.write(combined_hash)
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
import subprocess
rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
with open(BIG_MODEL_HASH_PATH, "w") as f:
f.write(big_combined_hash)
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
def get_ref_for_name(url: str, name: str) -> str:
response = requests.get(url, timeout=10)
if response.status_code == 200:
bundles = response.json()["bundles"]
matching = [b for b in bundles if re.search(name, f"{b['short_name']} {b['display_name']}", re.IGNORECASE)]
if matching:
return max(matching, key=lambda b: int(b["index"]))["ref"]
return ""
def update_default_model_names(default_model_name: str, default_big_model_name: str): def update_default_model_names(default_model_name: str, default_big_model_name: str):
print("[CHANGE DEFAULT MODEL NAMES]") print("[CHANGE DEFAULT MODEL NAMES]")
small_ref = get_ref_for_name(ModelFetcher.MODEL_URL, default_model_name)
big_ref = get_ref_for_name(ModelFetcher.MODEL_URL_CHESTNUT, default_big_model_name)
with open(DEFAULT_MODEL_NAME_PATH, "w") as f: with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
f.write(f'DEFAULT_MODEL = "{default_model_name}"\n') f.write(f'DEFAULT_MODEL = "{default_model_name}"\n')
f.write(f'DEFAULT_MODEL_REF = "{small_ref}"\n')
f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n') f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n')
f.write(f'DEFAULT_BIG_MODEL_REF = "{big_ref}"\n')
print(f'New default small model name: "{default_model_name}"') print(f'New default small model name: "{default_model_name}" (ref: {small_ref})')
print(f'New default big model name: "{default_big_model_name}"') print(f'New default big model name: "{default_big_model_name}" (ref: {big_ref})')
print("[DONE]") print("[DONE]")
+8 -8
View File
@@ -138,12 +138,12 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json"
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json" MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json"
MODEL_SOURCES = { MODEL_SOURCES = {
"qcom": (MODEL_URL, ""), "qcom": (MODEL_URL, ""),
"usbgpu": (MODEL_URL_USBGPU, "_USBGPU"), "chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"),
} }
def __init__(self, params: Params): def __init__(self, params: Params):
@@ -156,12 +156,12 @@ class ModelFetcher:
self._refetched: set[str] = set() self._refetched: set[str] = set()
self.params.put("ModelManager_ActiveJson", { self.params.put("ModelManager_ActiveJson", {
"qcom": self.MODEL_URL, "qcom": self.MODEL_URL,
"usbgpu": self.MODEL_URL_USBGPU, "chestnut": self.MODEL_URL_CHESTNUT,
}, block=True) }, block=True)
@staticmethod @staticmethod
def active_source(chestnut_present: bool) -> str: def active_source(chestnut_present: bool) -> str:
return "usbgpu" if chestnut_present else "qcom" return "chestnut" if chestnut_present else "qcom"
def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None: def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache. """Fetches fresh model data from remote and updates cache.
@@ -200,7 +200,7 @@ class ModelFetcher:
@staticmethod @staticmethod
def _cache_matches_source(source: str, cached_data: dict) -> bool: def _cache_matches_source(source: str, cached_data: dict) -> bool:
bundles = cached_data.get("bundles", []) bundles = cached_data.get("bundles", [])
if source == "usbgpu": if source == "chestnut":
return any(bundle.get("is_big") is True for bundle in bundles) return any(bundle.get("is_big") is True for bundle in bundles)
return not any(bundle.get("is_big") is True for bundle in bundles) return not any(bundle.get("is_big") is True for bundle in bundles)
@@ -261,10 +261,10 @@ def get_cached_bundles(params: Params, source: str) -> list[custom.ModelManagerS
if __name__ == "__main__": if __name__ == "__main__":
from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.selfdrive.modeld.helpers import chestnut_present
params = Params() params = Params()
model_fetcher = ModelFetcher(params) model_fetcher = ModelFetcher(params)
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present())) bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(chestnut_present()))
for bundle in bundles: for bundle in bundles:
for model in bundle.models: for model in bundle.models:
model_overrides = {override.key: override.value for override in bundle.overrides} model_overrides = {override.key: override.value for override in bundle.overrides}
+12 -12
View File
@@ -16,10 +16,10 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
from openpilot.common.hardware.hw import Paths from openpilot.common.hardware.hw import Paths
from openpilot.selfdrive.modeld.helpers import usbgpu_present from openpilot.selfdrive.modeld.helpers import chestnut_present
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 18 REQUIRED_JSON_VERSION = 19
CUSTOM_MODEL_PATH = Paths.model_root() CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
@@ -27,7 +27,7 @@ ModelManager = custom.ModelManagerSP
ACTIVE_BUNDLE_KEYS = { ACTIVE_BUNDLE_KEYS = {
"qcom": "ModelManager_ActiveBundle", "qcom": "ModelManager_ActiveBundle",
"usbgpu": "ModelManager_ActiveBundleUSBGPU", "chestnut": "ModelManager_ActiveBundleChestnut",
} }
_LAST_VALIDATED_RAW: dict[str, dict | None] = {} _LAST_VALIDATED_RAW: dict[str, dict | None] = {}
@@ -126,20 +126,20 @@ def get_selected_bundle(params: Params | None = None, source: str = "qcom") -> "
return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source])) return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source]))
def get_active_source(usbgpu: bool | None = None, usbgpu_active: bool | None = None, def get_active_source(chestnut: bool | None = None, chestnut_active: bool | None = None,
usbgpu_loading: bool | None = None, offroad: bool | None = None) -> str: chestnut_loading: bool | None = None, offroad: bool | None = None) -> str:
if usbgpu is None: if chestnut is None:
usbgpu = usbgpu_present() chestnut = chestnut_present()
state_valid = usbgpu_active is not None or usbgpu_loading is not None or offroad is not None state_valid = chestnut_active is not None or chestnut_loading is not None or offroad is not None
big_active = usbgpu and (not state_valid or usbgpu_active or usbgpu_loading or offroad) big_active = chestnut and (not state_valid or chestnut_active or chestnut_loading or offroad)
return "usbgpu" if big_active else "qcom" return "chestnut" if big_active else "qcom"
def get_active_bundle(params: Params | None = None, *, usbgpu: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None": def get_active_bundle(params: Params | None = None, *, chestnut: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
# no cross-slot fallback: an empty active slot means the hardware default, which # no cross-slot fallback: an empty active slot means the hardware default, which
# only stock modeld can run - modeld_v2 requires a real bundle # only stock modeld can run - modeld_v2 requires a real bundle
params = params or Params() params = params or Params()
return get_selected_bundle(params, get_active_source(usbgpu=usbgpu)) return get_selected_bundle(params, get_active_source(chestnut=chestnut))
def resolve_bundle_by_ref( def resolve_bundle_by_ref(
+9 -3
View File
@@ -40,7 +40,7 @@ class ModelManagerSP:
self.available_models: list[custom.ModelManagerSP.ModelBundle] = [] self.available_models: list[custom.ModelManagerSP.ModelBundle] = []
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {} self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
self._chunk_size = 128 * 1000 # 128 KB chunks self._chunk_size = 128 * 1000 # 128 KB chunks
self._download_start_times: dict[str, float] = {} # Track start time per model self._download_start_times: dict[str, float] = {} # Track start time per model
self._download_ref: bytes | str | None = None self._download_ref: bytes | str | None = None
@@ -280,7 +280,7 @@ class ModelManagerSP:
raise DownloadCancelled("Download cancelled") raise DownloadCancelled("Download cancelled")
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True) self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True)
self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
except Exception: except Exception:
if self.selected_bundle is not None: if self.selected_bundle is not None:
@@ -326,7 +326,13 @@ class ModelManagerSP:
self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES} self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES}
self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)] self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)]
validate_active_bundles(self.params, self.source_models) validate_active_bundles(self.params, self.source_models)
self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present) self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
if get_selected_bundle(self.params, "chestnut") is not None and get_selected_bundle(self.params, "qcom") is None:
if self.params.get("ModelManager_DownloadRef") is None:
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL_REF
if DEFAULT_MODEL_REF:
self.params.put("ModelManager_DownloadRef", DEFAULT_MODEL_REF)
self._process_download_requests() self._process_download_requests()
+3 -1
View File
@@ -1,2 +1,4 @@
DEFAULT_MODEL = "CD210" DEFAULT_MODEL = "CD210"
DEFAULT_BIG_MODEL = "Lebowski" DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
DEFAULT_BIG_MODEL = "BMRLNAP Model v4"
DEFAULT_BIG_MODEL_REF = "f877d7a0ccc3cce943c76e285214c020cd65c899"
@@ -0,0 +1 @@
2c814f08a2c51323b87839fbf8d2c2a9853a2b5536271b3d67f7b7a2de7f9374
+1 -1
View File
@@ -1 +1 @@
49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809 c5be11d2fb1115be953c541f30c50f7c71a00bc4a0e128e19aa11b60689317fc
@@ -5,19 +5,47 @@ 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. See the LICENSE.md file in the root directory for more details.
""" """
import os
import subprocess
from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \
BIG_SUPERCOMBO_ONNX_PATH, _read_model_name_fields
import hashlib import hashlib
from openpilot.common.test import OpenpilotTestCase from openpilot.common.test import OpenpilotTestCase
def _get_lfs_oid(path: str) -> str:
"""Extract the LFS OID (SHA256 of actual content) from git, works whether the file is smudged or not."""
pointer = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
for line in pointer.splitlines():
if line.startswith("oid sha256:"):
return line.split(":", 1)[1]
raise ValueError(f"No LFS OID found for {path}")
class TestDefaultModel(OpenpilotTestCase): class TestDefaultModel(OpenpilotTestCase):
def test_compare_onnx_hashes(self): def test_compare_onnx_hashes(self):
fields = _read_model_name_fields()
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
with open(MODEL_HASH_PATH) as f: with open(MODEL_HASH_PATH) as f:
current_hash = f.read().strip() current_hash = f.read().strip()
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash" assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
def test_compare_big_onnx_hashes(self):
if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
self.skipTest("big_driving_supercombo.onnx not present")
fields = _read_model_name_fields()
oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()))
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
with open(BIG_MODEL_HASH_PATH) as f:
current_hash = f.read().strip()
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
@@ -386,13 +386,13 @@ class TestManagerDownload(ManagerDownloadTestBase):
def body(): def body():
artifact = self.make_artifact(chunked=True) artifact = self.make_artifact(chunked=True)
self._bundle.ref = "test-ref" self._bundle.ref = "test-ref"
self._bundle.minimumSelectorVersion = 18 self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
params, store = self._make_params_with_store() params, store = self._make_params_with_store()
self.manager.params = params self.manager.params = params
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot" assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot"
assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu slot" assert "ModelManager_ActiveBundleChestnut" not in store, "qcom download must not touch the chestnut slot"
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref" assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref"
assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
@@ -401,18 +401,18 @@ class TestManagerDownload(ManagerDownloadTestBase):
assert missing == [], f"chunks missing from the cache: {missing}" assert missing == [], f"chunks missing from the cache: {missing}"
self.run_with_server(body) self.run_with_server(body)
def test_download_writes_usbgpu_slot(self): def test_download_writes_chestnut_slot(self):
"""A download resolved to the usbgpu source writes the usbgpu active bundle slot only.""" """A download resolved to the chestnut source writes the chestnut active bundle slot only."""
def body(): def body():
self.make_artifact(chunked=True) self.make_artifact(chunked=True)
self._bundle.ref = "big-ref" self._bundle.ref = "big-ref"
self._bundle.minimumSelectorVersion = 18 self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
params, store = self._make_params_with_store() params, store = self._make_params_with_store()
self.manager.params = params self.manager.params = params
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu")) asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
assert "ModelManager_ActiveBundleUSBGPU" in store, "usbgpu download must write the usbgpu slot" assert "ModelManager_ActiveBundleChestnut" in store, "chestnut download must write the chestnut slot"
assert "ModelManager_ActiveBundle" not in store, "usbgpu download must not touch the qcom slot" assert "ModelManager_ActiveBundle" not in store, "chestnut download must not touch the qcom slot"
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
self.run_with_server(body) self.run_with_server(body)
@@ -447,20 +447,20 @@ class TestResolveBundleByRef(OpenpilotTestCase):
def test_qcom_ref_resolves_to_qcom_slot(self): def test_qcom_ref_resolves_to_qcom_slot(self):
small = self._bundle("small") small = self._bundle("small")
assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom") assert resolve_bundle_by_ref("small", {"qcom": [small], "chestnut": []}) == (small, "qcom")
def test_usbgpu_ref_resolves_to_usbgpu_slot(self): def test_chestnut_ref_resolves_to_chestnut_slot(self):
big = self._bundle("big") big = self._bundle("big")
assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu") assert resolve_bundle_by_ref("big", {"qcom": [], "chestnut": [big]}) == (big, "chestnut")
def test_unknown_ref_returns_none(self): def test_unknown_ref_returns_none(self):
source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []} source_bundles = {"qcom": [self._bundle("small")], "chestnut": []}
assert resolve_bundle_by_ref("nope", source_bundles) is None assert resolve_bundle_by_ref("nope", source_bundles) is None
def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict: def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict:
"""Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects). """Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects).
Big (usbgpu) bundles carry `is_big: true` in the manifest JSON.""" Big (chestnut) bundles carry `is_big: true` in the manifest JSON."""
return { return {
"index": index, "index": index,
"short_name": short_name, "short_name": short_name,
@@ -469,7 +469,7 @@ def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = Fa
"environment": "release", "environment": "release",
"runner": "tinygrad", "runner": "tinygrad",
"is_big": is_big, "is_big": is_big,
"minimum_selector_version": "18", "minimum_selector_version": str(helpers.REQUIRED_JSON_VERSION),
"ref": ref, "ref": ref,
"models": [{ "models": [{
"type": "supercombo", "type": "supercombo",
@@ -489,15 +489,15 @@ class TestModelFetcherSources(OpenpilotTestCase):
"""Both manifests are always maintained: get_bundles_for_source exposes either """Both manifests are always maintained: get_bundles_for_source exposes either
source by name, and active_source picks which one matches the attached hardware.""" source by name, and active_source picks which one matches the attached hardware."""
def _make_params(self, qcom_manifest, usbgpu_manifest): def _make_params(self, qcom_manifest, chestnut_manifest):
params = mock.MagicMock() params = mock.MagicMock()
def get(key): def get(key):
if key == "ModelManager_ModelsCache": if key == "ModelManager_ModelsCache":
return qcom_manifest return qcom_manifest
if key == "ModelManager_ModelsCache_USBGPU": if key == "ModelManager_ModelsCache_Chestnut":
return usbgpu_manifest return chestnut_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
return fresh_sync_time() return fresh_sync_time()
return None return None
@@ -506,14 +506,14 @@ class TestModelFetcherSources(OpenpilotTestCase):
def test_active_source_follows_chestnut_presence(self): def test_active_source_follows_chestnut_presence(self):
assert ModelFetcher.active_source(False) == "qcom" assert ModelFetcher.active_source(False) == "qcom"
assert ModelFetcher.active_source(True) == "usbgpu" assert ModelFetcher.active_source(True) == "chestnut"
def test_get_bundles_for_source_returns_each_source(self): def test_get_bundles_for_source_returns_each_source(self):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) {"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"]
def test_get_bundles_for_source_unknown(self): def test_get_bundles_for_source_unknown(self):
assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == [] assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == []
@@ -522,16 +522,16 @@ class TestModelFetcherSources(OpenpilotTestCase):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]}) {"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
qcom_bundles = get_cached_bundles(params, "qcom") qcom_bundles = get_cached_bundles(params, "qcom")
usbgpu_bundles = get_cached_bundles(params, "usbgpu") chestnut_bundles = get_cached_bundles(params, "chestnut")
assert [b.ref for b in qcom_bundles] == ["aaa"] assert [b.ref for b in qcom_bundles] == ["aaa"]
assert [b.ref for b in usbgpu_bundles] == ["bbb"] assert [b.ref for b in chestnut_bundles] == ["bbb"]
assert qcom_bundles[0].displayName == "SMALL" assert qcom_bundles[0].displayName == "SMALL"
def test_get_cached_bundles_empty_when_missing(self): def test_get_cached_bundles_empty_when_missing(self):
params = mock.MagicMock() params = mock.MagicMock()
params.get.return_value = None params.get.return_value = None
assert get_cached_bundles(params, "qcom") == [] assert get_cached_bundles(params, "qcom") == []
assert get_cached_bundles(params, "usbgpu") == [] assert get_cached_bundles(params, "chestnut") == []
def test_get_cached_bundles_unknown_source(self): def test_get_cached_bundles_unknown_source(self):
assert get_cached_bundles(mock.MagicMock(), "bogus") == [] assert get_cached_bundles(mock.MagicMock(), "bogus") == []
@@ -543,27 +543,27 @@ class TestModelFetcherSources(OpenpilotTestCase):
assert active_json_calls, "expected ModelManager_ActiveJson to be written" assert active_json_calls, "expected ModelManager_ActiveJson to be written"
assert active_json_calls[-1].args[1] == { assert active_json_calls[-1].args[1] == {
"qcom": ModelFetcher.MODEL_URL, "qcom": ModelFetcher.MODEL_URL,
"usbgpu": ModelFetcher.MODEL_URL_USBGPU, "chestnut": ModelFetcher.MODEL_URL_CHESTNUT,
} }
class TestSourceCacheIntegrity(OpenpilotTestCase): class TestSourceCacheIntegrity(OpenpilotTestCase):
"""Each source's cached manifest must contain only that source's models; the """Each source's cached manifest must contain only that source's models; the
`is_big` flag in the JSON marks the big (usbgpu) models. A mismatched cache is `is_big` flag in the JSON marks the big (chestnut) models. A mismatched cache is
legacy data from before the per-source split (the active manifest was cached legacy data from before the per-source split (the active manifest was cached
under the unsuffixed key regardless of hardware) and is refetched. This under the unsuffixed key regardless of hardware) and is refetched. This
replaces the old one-time bundle migration.""" replaces the old one-time bundle migration."""
def _make_params(self, qcom_manifest, usbgpu_manifest): def _make_params(self, qcom_manifest, chestnut_manifest):
params = mock.MagicMock() params = mock.MagicMock()
def get(key): def get(key):
if key == "ModelManager_ModelsCache": if key == "ModelManager_ModelsCache":
return qcom_manifest return qcom_manifest
if key == "ModelManager_ModelsCache_USBGPU": if key == "ModelManager_ModelsCache_Chestnut":
return usbgpu_manifest return chestnut_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"): if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
return fresh_sync_time() return fresh_sync_time()
return None return None
@@ -584,13 +584,13 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
bundles = fetcher.get_bundles_for_source("qcom") bundles = fetcher.get_bundles_for_source("qcom")
assert [bundle.ref for bundle in bundles] == ["aaa"] assert [bundle.ref for bundle in bundles] == ["aaa"]
def test_usbgpu_cache_without_big_models_is_refetched(self): def test_chestnut_cache_without_big_models_is_refetched(self):
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]}, params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
{"bundles": [manifest_bundle("big2", "ccc")]}) {"bundles": [manifest_bundle("big2", "ccc")]})
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True)) fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True))
with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched): with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched):
bundles = fetcher.get_bundles_for_source("usbgpu") bundles = fetcher.get_bundles_for_source("chestnut")
assert [bundle.ref for bundle in bundles] == ["bbb"] assert [bundle.ref for bundle in bundles] == ["bbb"]
def test_matching_caches_are_used_without_fetch(self): def test_matching_caches_are_used_without_fetch(self):
@@ -599,7 +599,7 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
fetcher = ModelFetcher(params) fetcher = ModelFetcher(params)
with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")): with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")):
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"] assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"]
def test_stale_version_cache_is_refetched(self): def test_stale_version_cache_is_refetched(self):
"""A source-matching cache whose bundles are all filtered by the selector version """A source-matching cache whose bundles are all filtered by the selector version
@@ -655,33 +655,33 @@ class TestActiveBundleValidation(OpenpilotTestCase):
def _raw_bundle(ref: str, runner: int | None = None) -> dict: def _raw_bundle(ref: str, runner: int | None = None) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref bundle.ref = ref
bundle.minimumSelectorVersion = 18 bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
if runner is not None: if runner is not None:
bundle.runner = runner bundle.runner = runner
return bundle.to_dict() return bundle.to_dict()
def _params(self, qcom=None, usbgpu=None): def _params(self, qcom=None, chestnut=None):
params = mock.MagicMock() params = mock.MagicMock()
def get(key, *args, **kwargs): def get(key, *args, **kwargs):
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.get(key) return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleChestnut": chestnut}.get(key)
params.get.side_effect = get params.get.side_effect = get
return params return params
def test_empty_catalog_does_not_reset_slot(self): def test_empty_catalog_does_not_reset_slot(self):
params = self._params(qcom=self._raw_bundle("small")) params = self._params(qcom=self._raw_bundle("small"))
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
validate_active_bundles(params, {"qcom": [], "usbgpu": []}) validate_active_bundles(params, {"qcom": [], "chestnut": []})
params.remove.assert_not_called() params.remove.assert_not_called()
def test_reset_recomputes_runner_from_surviving_slot(self): def test_reset_recomputes_runner_from_surviving_slot(self):
tinygrad = int(custom.ModelManagerSP.Runner.tinygrad) tinygrad = int(custom.ModelManagerSP.Runner.tinygrad)
big_raw = self._raw_bundle("big", runner=tinygrad) big_raw = self._raw_bundle("big", runner=tinygrad)
params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw) params = self._params(qcom=self._raw_bundle("gone"), chestnut=big_raw)
catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))], catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))],
"usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]} "chestnut": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
validate_active_bundles(params, catalog) validate_active_bundles(params, catalog)
params.remove.assert_called_once_with("ModelManager_ActiveBundle") params.remove.assert_called_once_with("ModelManager_ActiveBundle")
runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"] runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"]
@@ -689,7 +689,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
class TestActiveBundleSelection(OpenpilotTestCase): class TestActiveBundleSelection(OpenpilotTestCase):
"""The effective active bundle is the active source's slot: usbgpu when a GPU is """The effective active bundle is the active source's slot: chestnut when a GPU is
present, qcom otherwise. An empty active slot means the hardware default (stock present, qcom otherwise. An empty active slot means the hardware default (stock
runner), never the other slot's pick - modeld_v2 requires a real bundle.""" runner), never the other slot's pick - modeld_v2 requires a real bundle."""
@@ -697,40 +697,40 @@ class TestActiveBundleSelection(OpenpilotTestCase):
def _raw_bundle(ref: str) -> dict: def _raw_bundle(ref: str) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref bundle.ref = ref
bundle.minimumSelectorVersion = 18 bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
return bundle.to_dict() return bundle.to_dict()
def _params(self, qcom=None, usbgpu=None): def _params(self, qcom=None, chestnut=None):
params = mock.MagicMock() params = mock.MagicMock()
def get(key, *args, **kwargs): def get(key, *args, **kwargs):
if key == "ModelManager_ActiveBundle": if key == "ModelManager_ActiveBundle":
return qcom return qcom
if key == "ModelManager_ActiveBundleUSBGPU": if key == "ModelManager_ActiveBundleChestnut":
return usbgpu return chestnut
return None return None
params.get.side_effect = get params.get.side_effect = get
return params return params
def test_selected_bundle_is_per_slot(self): def test_selected_bundle_is_per_slot(self):
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
assert get_selected_bundle(params, "qcom").ref == "small" assert get_selected_bundle(params, "qcom").ref == "small"
assert get_selected_bundle(params, "usbgpu").ref == "big" assert get_selected_bundle(params, "chestnut").ref == "big"
def test_no_gpu_uses_qcom_slot(self): def test_no_gpu_uses_qcom_slot(self):
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
assert get_active_bundle(params).ref == "small" assert get_active_bundle(params).ref == "small"
def test_gpu_uses_usbgpu_slot(self): def test_gpu_uses_chestnut_slot(self):
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big")) params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
assert get_active_bundle(params).ref == "big" assert get_active_bundle(params).ref == "big"
def test_gpu_without_big_selection_is_hardware_default(self): def test_gpu_without_big_selection_is_hardware_default(self):
params = self._params(qcom=self._raw_bundle("small"), usbgpu=None) params = self._params(qcom=self._raw_bundle("small"), chestnut=None)
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
assert get_active_bundle(params) is None assert get_active_bundle(params) is None
@@ -744,40 +744,40 @@ class TestEffectiveSource(OpenpilotTestCase):
def _raw_bundle(ref: str) -> dict: def _raw_bundle(ref: str) -> dict:
bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle = custom.ModelManagerSP.ModelBundle.new_message()
bundle.ref = ref bundle.ref = ref
bundle.minimumSelectorVersion = 18 bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
return bundle.to_dict() return bundle.to_dict()
def test_runtime_no_gpu(self): def test_runtime_no_gpu(self):
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
assert get_active_source() == "qcom" assert get_active_source() == "qcom"
def test_runtime_gpu_present(self): def test_runtime_gpu_present(self):
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
assert get_active_source() == "usbgpu" assert get_active_source() == "chestnut"
def test_display_offroad_gpu_present_shows_big(self): def test_display_offroad_gpu_present_shows_big(self):
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu" assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=True) == "chestnut"
def test_display_onroad_gpu_loading_shows_big(self): def test_display_onroad_gpu_loading_shows_big(self):
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu" assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=True, offroad=False) == "chestnut"
def test_display_onroad_gpu_active_shows_big(self): def test_display_onroad_gpu_active_shows_big(self):
assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu" assert get_active_source(chestnut=True, chestnut_active=True, chestnut_loading=False, offroad=False) == "chestnut"
def test_display_onroad_gpu_idle_shows_small(self): def test_display_onroad_gpu_idle_shows_small(self):
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom" assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=False) == "qcom"
def test_display_active_none_is_idle(self): def test_display_active_none_is_idle(self):
assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_loading=False, offroad=False) == "qcom" assert get_active_source(chestnut=True, chestnut_active=None, chestnut_loading=False, offroad=False) == "qcom"
def test_active_bundle_follows_source(self): def test_active_bundle_follows_source(self):
params = mock.MagicMock() params = mock.MagicMock()
params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"), params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"),
"ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key) "ModelManager_ActiveBundleChestnut": self._raw_bundle("big")}.get(key)
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False): with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
assert get_active_bundle(params).ref == "small" assert get_active_bundle(params).ref == "small"
assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False, assert get_selected_bundle(params, get_active_source(chestnut=True, chestnut_active=False,
usbgpu_loading=False, offroad=True)).ref == "big" chestnut_loading=False, offroad=True)).ref == "big"
@unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network') @unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network')
@@ -252,4 +252,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
AlertStatus.userPrompt, AlertSize.small, AlertStatus.userPrompt, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1), Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
}, },
EventNameSP.bigModelReady: {
ET.PERMANENT: Alert(
"Big Model Ready",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 2.),
},
} }
@@ -183,7 +183,7 @@ def getParamsMetadata() -> str:
schema["capability_labels"] = CAPABILITY_LABELS schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL schema["default_model"] = DEFAULT_MODEL
schema["default_big_model"] = DEFAULT_BIG_MODEL schema["default_big_model"] = DEFAULT_BIG_MODEL
schema["usbgpu_active"] = params.get_bool("UsbGpuActive") schema["chestnut_active"] = params.get_bool("ChestnutActive")
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception: except Exception:
+1 -1
View File
@@ -65,7 +65,7 @@ def sp_stats(end_event):
'MadsSteeringMode', 'MadsSteeringMode',
'MadsUnifiedEngagementMode', 'MadsUnifiedEngagementMode',
'ModelManager_ActiveBundle', 'ModelManager_ActiveBundle',
'ModelManager_ActiveBundleUSBGPU', 'ModelManager_ActiveBundleChestnut',
'ModelManager_Favs', 'ModelManager_Favs',
'EnableSunnylinkUploader', 'EnableSunnylinkUploader',
'SunnylinkEnabled', 'SunnylinkEnabled',
@@ -89,12 +89,16 @@ def _migrate_model_bundle_slots(_params):
# ActiveBundle. Seed both slots; validation drops whichever does not match # ActiveBundle. Seed both slots; validation drops whichever does not match
# its own manifest. # its own manifest.
try: try:
if _params.get("ModelManager_ActiveBundleUSBGPU") is not None: if _params.get("ModelManager_ActiveBundleChestnut") is not None:
return
if (chestnut_bundle := _params.get("ModelManager_ActiveBundleUSBGPU")) is not None:
_params.put("ModelManager_ActiveBundleChestnut", chestnut_bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundleUSBGPU")
return return
if (bundle := _params.get("ModelManager_ActiveBundle")) is None: if (bundle := _params.get("ModelManager_ActiveBundle")) is None:
return return
_params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True) _params.put("ModelManager_ActiveBundleChestnut", bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle") cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundle")
except Exception as e: except Exception as e:
cloudlog.exception(f"Error migrating model bundle slots: {e}") cloudlog.exception(f"Error migrating model bundle slots: {e}")
@@ -136,5 +140,5 @@ def run_migration(_params):
# seed TeslaMadsScreenButton for existing Tesla installs # seed TeslaMadsScreenButton for existing Tesla installs
_migrate_tesla_mads_screen_button(_params) _migrate_tesla_mads_screen_button(_params)
# seed the usbgpu model slot from the pre-split single slot # seed the chestnut model slot from the pre-split single slot
_migrate_model_bundle_slots(_params) _migrate_model_bundle_slots(_params)
@@ -15,22 +15,22 @@ class TestModelBundleSlotMigration(OpenpilotTestCase):
The migration seeds both slots; per-source validation later drops whichever does not The migration seeds both slots; per-source validation later drops whichever does not
match its own manifest.""" match its own manifest."""
def test_seeds_usbgpu_slot_from_active_bundle(self): def test_seeds_chestnut_slot_from_active_bundle(self):
params = Params() params = Params()
bundle = {"ref": "big", "minimumSelectorVersion": 18} bundle = {"ref": "big", "minimumSelectorVersion": 18}
params.put("ModelManager_ActiveBundle", bundle, block=True) params.put("ModelManager_ActiveBundle", bundle, block=True)
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle assert params.get("ModelManager_ActiveBundleChestnut") == bundle
assert params.get("ModelManager_ActiveBundle") == bundle assert params.get("ModelManager_ActiveBundle") == bundle
def test_noop_when_usbgpu_slot_already_set(self): def test_noop_when_chestnut_slot_already_set(self):
params = Params() params = Params()
params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True) params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True)
params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True) params.put("ModelManager_ActiveBundleChestnut", {"ref": "big"}, block=True)
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"} assert params.get("ModelManager_ActiveBundleChestnut") == {"ref": "big"}
def test_noop_when_no_selection(self): def test_noop_when_no_selection(self):
params = Params() params = Params()
_migrate_model_bundle_slots(params) _migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleUSBGPU") is None assert params.get("ModelManager_ActiveBundleChestnut") is None
@@ -0,0 +1,101 @@
import time
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id
from openpilot.common.version import get_build_metadata, CHESTNUT_BRANCHES
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging")
CHESTNUT_POWERED_VOLTAGE = 5000
GPU_TEMP_LIMIT = 100.
MEMORY_TEMP_LIMIT = 95.
TEMP_HYSTERESIS = 5.
class ChestnutStatus:
def __init__(self):
self.started = time.monotonic()
self.offroad = True
self.pcie_failed = False
self.power_seen = False
self.power_unavailable = False
self.power_lost = False
self.power_restored = False
self.link_failures = 0
self.model_loading_seen = False
self.model_attempted = False
self.overheated = False
self.usb_seen = False
self.usb_failed = False
def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool,
model_loading: bool, model_active: bool | None, state, set_alert) -> None:
detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)]
devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])]
firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT
if self.offroad and not offroad:
self.pcie_failed = False
self.power_seen = False
self.power_unavailable = False
self.power_lost = False
self.power_restored = False
self.link_failures = 0
self.model_loading_seen = False
self.model_attempted = False
self.usb_seen = firmware_ok
self.usb_failed = False
self.model_loading_seen |= model_loading
self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None
if not offroad and self.usb_seen and not firmware_ok:
self.usb_failed = True
if not offroad and state is not None:
powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
power_lost = state.supplyFault or not powered
if self.model_attempted and power_lost and not self.power_lost:
self.power_unavailable = not self.power_seen
self.power_seen |= powered
if not offroad and self.model_attempted and state is not None:
self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0
self.pcie_failed |= self.link_failures >= 2 or power_lost
self.power_lost |= power_lost
if self.pcie_failed and self.power_lost and state is not None:
self.power_restored |= not state.supplyFault and state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
if self.usb_failed:
self.pcie_failed = False
self.power_seen = False
self.power_unavailable = False
self.power_lost = False
self.power_restored = False
if state is not None:
gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit
release = branch in CHESTNUT_RELEASE_BRANCHES
missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1)
slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
current_channel = get_build_metadata().channel
chestnut_target = CHESTNUT_BRANCHES.get(current_channel)
chestnut_needs_switch = len(devices) == 1 and not big_model_available and chestnut_target is not None
set_alert("Offroad_ChestnutBranch", chestnut_needs_switch, chestnut_target if chestnut_needs_switch else None)
set_alert("Offroad_ChestnutNotDetected", missing)
set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None)
set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None)
if self.power_lost:
pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else
"Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else
"Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.")
else:
pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated."
set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert)
set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled())
set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed)
self.offroad = offroad
+20 -15
View File
@@ -16,19 +16,19 @@ from openpilot.common.utils import strip_deprecated_keys
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.realtime import DT_HW from openpilot.common.realtime import DT_HW
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state from openpilot.common.git import get_short_branch
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state
from openpilot.common.linux import LinuxSystemStats from openpilot.common.linux import LinuxSystemStats
from openpilot.system.loggerd.config import get_available_percent from openpilot.system.loggerd.config import get_available_percent
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.system.statsd import statlog from openpilot.sunnypilot.system.statsd import statlog
from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.power_monitoring import PowerMonitoring
from openpilot.system.hardware.fan_controller import FanController from openpilot.system.hardware.fan_controller import FanController
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp, CHESTNUT_BRANCHES from openpilot.system.hardware.chestnut.status import ChestnutStatus
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp
ThermalStatus = log.DeviceState.ThermalStatus ThermalStatus = log.DeviceState.ThermalStatus
NetworkType = log.DeviceState.NetworkType NetworkType = log.DeviceState.NetworkType
@@ -49,6 +49,11 @@ class Chestnut:
self.attempts = 0 self.attempts = 0
self.last_attempt = 0. self.last_attempt = 0.
self.flashed = False self.flashed = False
self.mismatch = False
@property
def failed(self) -> bool:
return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed
def flash(self) -> None: def flash(self) -> None:
ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION], ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION],
@@ -57,9 +62,9 @@ class Chestnut:
self.flashed = ret.returncode == 0 self.flashed = ret.returncode == 0
def update(self, offroad: bool, usb_state: list[dict]) -> None: def update(self, offroad: bool, usb_state: list[dict]) -> None:
mismatch = any((d["vendorId"], d["productId"]) in CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS and self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and
d["product"] != f"custom {CHESTNUT_FW_VERSION}-CLEAN" for d in usb_state) d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state)
if not mismatch: if not self.mismatch:
self.flashed = False self.flashed = False
return return
@@ -191,7 +196,7 @@ def hw_state_thread(end_event, hw_queue):
def hardware_thread(end_event, hw_queue) -> None: def hardware_thread(end_event, hw_queue) -> None:
system_stats = LinuxSystemStats() system_stats = LinuxSystemStats()
pm = messaging.PubMaster(['deviceState']) pm = messaging.PubMaster(['deviceState'])
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates") sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates")
count = 0 count = 0
@@ -239,7 +244,8 @@ def hardware_thread(end_event, hw_queue) -> None:
fan_controller = FanController(int(1./DT_HW)) fan_controller = FanController(int(1./DT_HW))
chestnut = Chestnut() chestnut = Chestnut()
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled() chestnut_status = ChestnutStatus()
branch = get_short_branch()
while not end_event.is_set(): while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT) sm.update(PANDA_STATES_TIMEOUT)
@@ -301,12 +307,11 @@ def hardware_thread(end_event, hw_queue) -> None:
set_usb_state(msg.deviceState, last_hw_state.usb_state) set_usb_state(msg.deviceState, last_hw_state.usb_state)
chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state)
current_channel = get_build_metadata().channel chestnut_state = sm["chestnutState"]
chestnut_target = CHESTNUT_BRANCHES.get(current_channel) chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"]
chestnut_needs_switch = msg.deviceState.chestnutPresent and not big_model_available and chestnut_target is not None chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed,
set_offroad_alert_if_changed("Offroad_ChestnutBranch", chestnut_needs_switch, params.get_bool("ChestnutLoading"), params.get("ChestnutActive"),
extra_text=chestnut_target if chestnut_needs_switch else None) chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed)
# this subset is only used for offroad # this subset is only used for offroad
temp_sources = [ temp_sources = [
msg.deviceState.memoryTempC, msg.deviceState.memoryTempC,
+13 -10
View File
@@ -13,7 +13,7 @@ import subprocess
from contextlib import contextmanager from contextlib import contextmanager
from collections.abc import Callable from collections.abc import Callable
from collections import deque from collections import deque
from enum import StrEnum from enum import IntEnum, StrEnum
from pathlib import Path from pathlib import Path
from typing import NamedTuple from typing import NamedTuple
from importlib.resources import as_file, files from importlib.resources import as_file, files
@@ -118,6 +118,18 @@ class FontWeight(StrEnum):
DISPLAY = "Inter-Bold.ttf" DISPLAY = "Inter-Bold.ttf"
class TextAlignment(IntEnum):
LEFT = 0
CENTER = 1
RIGHT = 2
class TextAlignmentVertical(IntEnum):
TOP = 0
MIDDLE = 1
BOTTOM = 2
def font_fallback(font: rl.Font) -> rl.Font: def font_fallback(font: rl.Font) -> rl.Font:
"""Use a Noto fallback for languages not covered by Inter.""" """Use a Noto fallback for languages not covered by Inter."""
if multilang.requires_font_fallback(): if multilang.requires_font_fallback():
@@ -335,7 +347,6 @@ class GuiApplication(GuiApplicationExt):
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps) rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
self._target_fps = fps self._target_fps = fps
self._set_styles()
self._load_fonts() self._load_fonts()
self._patch_text_functions() self._patch_text_functions()
self._patch_scissor_mode() self._patch_scissor_mode()
@@ -739,14 +750,6 @@ class GuiApplication(GuiApplicationExt):
self._fonts[font_weight_file] = font self._fonts[font_weight_file] = font
if multilang.requires_font_fallback(): if multilang.requires_font_fallback():
self.fallback_font() self.fallback_font()
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
def _set_styles(self):
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE)
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK))
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
def _patch_text_functions(self): def _patch_text_functions(self):
# Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt
-19
View File
@@ -1,19 +0,0 @@
import pyray as rl
from collections.abc import Sequence
class GuiStyleContext:
def __init__(self, styles: Sequence[tuple[int, int, int]]):
"""styles is a list of tuples (control, prop, new_value)"""
self.styles = styles
self.prev_styles: list[tuple[int, int, int]] = []
def __enter__(self):
for control, prop, new_value in self.styles:
prev_value = rl.gui_get_style(control, prop)
self.prev_styles.append((control, prop, prev_value))
rl.gui_set_style(control, prop, new_value)
def __exit__(self, exc_type, exc_value, traceback):
for control, prop, prev_value in self.prev_styles:
rl.gui_set_style(control, prop, prev_value)
+6 -6
View File
@@ -18,7 +18,7 @@ from openpilot.common.realtime import config_realtime_process, set_core_affinity
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.time_helpers import system_time_valid from openpilot.common.time_helpers import system_time_valid
from openpilot.common.utils import run_cmd from openpilot.common.utils import run_cmd
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
@@ -105,8 +105,8 @@ class StartPage(Widget):
super().__init__() super().__init__()
self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False) self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False)
self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False) self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False)
@@ -197,7 +197,7 @@ class DownloadingPage(NavWidget):
self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
font_weight=FontWeight.DISPLAY) font_weight=FontWeight.DISPLAY)
self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) font_weight=FontWeight.ROMAN, alignment_vertical=TextAlignmentVertical.BOTTOM)
self._progress = 0 self._progress = 0
def _back_enabled(self) -> bool: def _back_enabled(self) -> bool:
@@ -261,8 +261,8 @@ class BigPillButton(BigButton):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._label.set_font_size(48) self._label.set_font_size(48)
self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) self._label.set_alignment(TextAlignment.CENTER)
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) self._label.set_alignment_vertical(TextAlignmentVertical.MIDDLE)
def _load_images(self): def _load_images(self):
if self._green: if self._green:
+2 -2
View File
@@ -7,7 +7,7 @@ import pyray as rl
from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.realtime import config_realtime_process, set_core_affinity
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
@@ -30,7 +30,7 @@ class ProgressPage(NavWidget):
font_weight=FontWeight.DISPLAY, line_height=0.8) font_weight=FontWeight.DISPLAY, line_height=0.8)
self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
font_weight=FontWeight.ROMAN, font_weight=FontWeight.ROMAN,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) alignment_vertical=TextAlignmentVertical.BOTTOM)
def _back_enabled(self) -> bool: def _back_enabled(self) -> bool:
return False return False
+2 -2
View File
@@ -8,7 +8,7 @@ from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
from openpilot.system.ui.sunnypilot.lib.styles import style from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel
@@ -40,7 +40,7 @@ class ScrollingButtonAction(ButtonActionSP):
super().__init__(text=text, width=width, enabled=enabled) super().__init__(text=text, width=width, enabled=enabled)
self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL, self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
text_color=self._value_color, scroll=True, text_color=self._value_color, scroll=True,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR): def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
if self.value != _resolve_value(value, ""): if self.value != _resolve_value(value, ""):
@@ -9,6 +9,8 @@ import math
import numpy as np import numpy as np
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import TextAlignment, TextAlignmentVertical
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
@@ -61,11 +63,11 @@ class DownloadStatusAction(ItemAction):
self._sweep = 0.0 self._sweep = 0.0
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR, self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR, self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment=TextAlignment.RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None): def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None):
self.segments = segments self.segments = segments
@@ -8,7 +8,7 @@ from collections.abc import Callable, Sequence
import pyray as rl import pyray as rl
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -90,8 +90,8 @@ class ButtonActionSP(ButtonAction):
if value_text: if value_text:
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height) value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color, gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color,
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
pressed = self._pressed pressed = self._pressed
self._pressed = False self._pressed = False
@@ -319,7 +319,7 @@ class ListItemSP(ListItem):
) )
if value_rect.width > 0: if value_rect.width > 0:
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL, gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment=TextAlignment.RIGHT, alignment_vertical=TextAlignmentVertical.MIDDLE)
# Render toggle and handle callback # Render toggle and handle callback
if self.action_item.render(left_rect) and self.action_item.enabled: if self.action_item.render(left_rect) and self.action_item.enabled:
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
import pyray as rl import pyray as rl
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.system.ui.lib.application import TextAlignment
from openpilot.system.ui.lib.application import FontWeight from openpilot.system.ui.lib.application import FontWeight
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets import DialogResult
@@ -35,7 +36,7 @@ class TreeFolder:
class TreeItemWidget(Button): class TreeItemWidget(Button):
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False): def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False):
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=TextAlignment.LEFT,
text_padding=20 + indent_level * 30, elide_right=True) text_padding=20 + indent_level * 30, elide_right=True)
self.text = text self.text = text
self.ref = ref self.ref = ref
+3 -2
View File
@@ -11,7 +11,7 @@ from openpilot.common.hardware import PC
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_label, gui_text_box from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
USERDATA = "/dev/disk/by-partlabel/userdata" USERDATA = "/dev/disk/by-partlabel/userdata"
TIMEOUT = 3*60 TIMEOUT = 3*60
@@ -38,6 +38,7 @@ class Reset(Widget):
self._cancel_button = Button("Cancel", gui_app.request_close) self._cancel_button = Button("Cancel", gui_app.request_close)
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
self._reboot_button = Button("Reboot", self._reboot) self._reboot_button = Button("Reboot", self._reboot)
self._body_label = UnifiedLabel(self._get_body_text, 90, line_height=1 / 0.9)
@staticmethod @staticmethod
def _reboot() -> None: def _reboot() -> None:
@@ -75,7 +76,7 @@ class Reset(Widget):
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE) text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE)
gui_text_box(text_rect, self._get_body_text(), 90) self._body_label.render(text_rect)
button_height = 160 button_height = 160
button_spacing = 50 button_spacing = 50
+13 -13
View File
@@ -13,7 +13,7 @@ import pyray as rl
from openpilot.cereal import log from openpilot.cereal import log
from openpilot.common.hardware import HARDWARE from openpilot.common.hardware import HARDWARE
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.keyboard import Keyboard
@@ -67,17 +67,17 @@ class Setup(Widget):
self.warning = gui_app.texture("icons/warning.png", 150, 150) self.warning = gui_app.texture("icons/warning.png", 150, 150)
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100)
self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT,
text_color=rl.Color(255, 89, 79, 255), text_padding=20) text_color=rl.Color(255, 89, 79, 255), text_padding=20)
self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE,
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) text_alignment=TextAlignment.LEFT, text_padding=20)
self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback)
self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown)
self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0)
self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.",
BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
@@ -85,32 +85,32 @@ class Setup(Widget):
button_style=ButtonStyle.PRIMARY) button_style=ButtonStyle.PRIMARY)
self._software_selection_continue_button.set_enabled(False) self._software_selection_continue_button.set_enabled(False)
self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback)
self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT,
text_padding=20) text_padding=20)
self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot)
self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY)
self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback)
self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback,
button_style=ButtonStyle.PRIMARY) button_style=ButtonStyle.PRIMARY)
self._network_setup_continue_button.set_enabled(False) self._network_setup_continue_button.set_enabled(False)
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback, self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback,
button_style=ButtonStyle.PRIMARY) button_style=ButtonStyle.PRIMARY)
self._custom_software_warning_continue_button.set_enabled(False) self._custom_software_warning_continue_button.set_enabled(False)
self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback)
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, TextAlignment.LEFT,
text_color=rl.Color(255, 89, 79, 255), text_color=rl.Color(255, 89, 79, 255),
text_padding=60) text_padding=60)
self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE)) self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE))
self._custom_software_warning_body_labels = [ self._custom_software_warning_body_labels = [
Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, Label(text, 68, text_alignment=TextAlignment.LEFT,
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, text_alignment_vertical=TextAlignmentVertical.TOP,
text_padding=60, icon=self._yellow_warning_icon if has_icon else None) text_padding=60, icon=self._yellow_warning_icon if has_icon else None)
for text, has_icon in [ for text, has_icon in [
("Use caution when installing third-party software.", False), ("Use caution when installing third-party software.", False),
+4 -5
View File
@@ -10,7 +10,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import gui_text_box, gui_label from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.widgets.network import WifiManagerUI from openpilot.system.ui.widgets.network import WifiManagerUI
# Constants # Constants
@@ -50,6 +50,8 @@ class Updater(Widget):
self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY) self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY)
self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT)) self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT))
self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot()) self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot())
self._desc_label = UnifiedLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
"The download size is approximately 1GB.", BODY_FONT_SIZE, line_height=1 / 0.9)
def set_current_screen(self, screen: Screen): def set_current_screen(self, screen: Screen):
self.current_screen = screen self.current_screen = screen
@@ -99,11 +101,8 @@ class Updater(Widget):
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
# Description # Description
desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
"The download size is approximately 1GB.")
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4) desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4)
gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) self._desc_label.render(desc_rect)
# Buttons at the bottom # Buttons at the bottom
button_y = rect.height - MARGIN - BUTTON_HEIGHT button_y = rect.height - MARGIN - BUTTON_HEIGHT
+3 -3
View File
@@ -3,7 +3,7 @@ from enum import IntEnum
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import Label from openpilot.system.ui.widgets.label import Label
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
@@ -86,7 +86,7 @@ class Button(Widget):
font_weight: FontWeight = FontWeight.MEDIUM, font_weight: FontWeight = FontWeight.MEDIUM,
button_style: ButtonStyle = ButtonStyle.NORMAL, button_style: ButtonStyle = ButtonStyle.NORMAL,
border_radius: int = 10, border_radius: int = 10,
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, text_alignment: int = TextAlignment.CENTER,
text_padding: int = 20, text_padding: int = 20,
icon=None, icon=None,
elide_right: bool = False, elide_right: bool = False,
@@ -139,7 +139,7 @@ class ButtonRadio(Button):
icon, icon,
click_callback: Callable[[], None] | None = None, click_callback: Callable[[], None] | None = None,
font_size: int = DEFAULT_BUTTON_FONT_SIZE, font_size: int = DEFAULT_BUTTON_FONT_SIZE,
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_alignment: int = TextAlignment.LEFT,
border_radius: int = 10, border_radius: int = 10,
text_padding: int = 20, text_padding: int = 20,
): ):
@@ -14,3 +14,6 @@ class IconWidget(Widget):
def _render(self, _) -> None: def _render(self, _) -> None:
color = rl.Color(255, 255, 255, int(self._opacity * 255)) color = rl.Color(255, 255, 255, int(self._opacity * 255))
rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color) rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color)
def set_opacity(self, opacity: float) -> None:
self._opacity = opacity
+3 -3
View File
@@ -5,7 +5,7 @@ from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.button import ButtonStyle, Button
@@ -65,8 +65,8 @@ class Keyboard(Widget):
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
self._caps_lock = False self._caps_lock = False
self._last_shift_press_time = 0 self._last_shift_press_time = 0
self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
self._max_text_size = max_text_size self._max_text_size = max_text_size
self._min_text_size = min_text_size self._min_text_size = min_text_size
+28 -57
View File
@@ -4,10 +4,9 @@ from collections.abc import Callable
from typing import Union from typing import Union
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.lib.utils import GuiStyleContext
from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.wrap_text import wrap_text
ICON_PADDING = 15 ICON_PADDING = 15
@@ -32,8 +31,8 @@ def gui_label(
font_size: int = DEFAULT_TEXT_SIZE, font_size: int = DEFAULT_TEXT_SIZE,
color: rl.Color = DEFAULT_TEXT_COLOR, color: rl.Color = DEFAULT_TEXT_COLOR,
font_weight: FontWeight = FontWeight.NORMAL, font_weight: FontWeight = FontWeight.NORMAL,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment: int = TextAlignment.LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, alignment_vertical: int = TextAlignmentVertical.MIDDLE,
elide_right: bool = True elide_right: bool = True
): ):
font = gui_app.font(font_weight) font = gui_app.font(font_weight)
@@ -57,16 +56,16 @@ def gui_label(
# Calculate horizontal position based on alignment # Calculate horizontal position based on alignment
text_x = rect.x + { text_x = rect.x + {
rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, TextAlignment.LEFT: 0,
rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, TextAlignment.CENTER: (rect.width - text_size.x) / 2,
rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, TextAlignment.RIGHT: rect.width - text_size.x,
}.get(alignment, 0) }.get(alignment, 0)
# Calculate vertical position based on alignment # Calculate vertical position based on alignment
text_y = rect.y + { text_y = rect.y + {
rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, TextAlignmentVertical.TOP: 0,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, TextAlignmentVertical.MIDDLE: (rect.height - text_size.y) / 2,
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, TextAlignmentVertical.BOTTOM: rect.height - text_size.y,
}.get(alignment_vertical, 0) }.get(alignment_vertical, 0)
# Draw the text in the specified rectangle # Draw the text in the specified rectangle
@@ -74,42 +73,14 @@ def gui_label(
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
def gui_text_box(
rect: rl.Rectangle,
text: str,
font_size: int = DEFAULT_TEXT_SIZE,
color: rl.Color = DEFAULT_TEXT_COLOR,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
font_weight: FontWeight = FontWeight.NORMAL,
line_scale: float = 1.0,
):
styles = [
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)),
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)),
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE * line_scale)),
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment),
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical),
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD)
]
if font_weight != FontWeight.NORMAL:
rl.gui_set_font(gui_app.font(font_weight))
with GuiStyleContext(styles):
rl.gui_label(rect, text)
if font_weight != FontWeight.NORMAL:
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
# Non-interactive text area. Can render an optional specified icon. # Non-interactive text area. Can render an optional specified icon.
class Label(Widget): class Label(Widget):
def __init__(self, def __init__(self,
text: str | Callable[[], str], text: str | Callable[[], str],
font_size: int = DEFAULT_TEXT_SIZE, font_size: int = DEFAULT_TEXT_SIZE,
font_weight: FontWeight = FontWeight.NORMAL, font_weight: FontWeight = FontWeight.NORMAL,
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, text_alignment: int = TextAlignment.CENTER,
text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, text_alignment_vertical: int = TextAlignmentVertical.MIDDLE,
text_padding: int = 0, text_padding: int = 0,
text_color: rl.Color = DEFAULT_TEXT_COLOR, text_color: rl.Color = DEFAULT_TEXT_COLOR,
icon: Union[rl.Texture, None] = None, icon: Union[rl.Texture, None] = None,
@@ -181,10 +152,10 @@ class Label(Widget):
self._update_text(self._text) self._update_text(self._text)
text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: if self._text_alignment_vertical == TextAlignmentVertical.MIDDLE:
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2))
elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: elif self._text_alignment_vertical == TextAlignmentVertical.BOTTOM:
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height) text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height)
else: else:
@@ -193,10 +164,10 @@ class Label(Widget):
if self._icon: if self._icon:
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
if len(self._text_wrapped) > 0: if len(self._text_wrapped) > 0:
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: if self._text_alignment == TextAlignment.LEFT:
icon_x = self._rect.x + self._text_padding icon_x = self._rect.x + self._text_padding
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: elif self._text_alignment == TextAlignment.CENTER:
total_width = self._icon.width + ICON_PADDING + text_size.x total_width = self._icon.width + ICON_PADDING + text_size.x
icon_x = self._rect.x + (self._rect.width - total_width) / 2 icon_x = self._rect.x + (self._rect.width - total_width) / 2
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
@@ -208,11 +179,11 @@ class Label(Widget):
for text, text_size in zip(self._text_wrapped, self._text_size, strict=True): for text, text_size in zip(self._text_wrapped, self._text_size, strict=True):
line_pos = rl.Vector2(text_pos.x, text_pos.y) line_pos = rl.Vector2(text_pos.x, text_pos.y)
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: if self._text_alignment == TextAlignment.LEFT:
line_pos.x += self._text_padding line_pos.x += self._text_padding
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: elif self._text_alignment == TextAlignment.CENTER:
line_pos.x += (self._rect.width - text_size.x) // 2 line_pos.x += (self._rect.width - text_size.x) // 2
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: elif self._text_alignment == TextAlignment.RIGHT:
line_pos.x += self._rect.width - text_size.x - self._text_padding line_pos.x += self._rect.width - text_size.x - self._text_padding
rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color) rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color)
@@ -221,7 +192,7 @@ class Label(Widget):
class UnifiedLabel(Widget): class UnifiedLabel(Widget):
""" """
Unified label widget that combines functionality from gui_label, gui_text_box, and Label. Unified label widget that combines functionality from gui_label and Label.
Supports: Supports:
- Text wrapping - Text wrapping
@@ -241,8 +212,8 @@ class UnifiedLabel(Widget):
font_size: int = DEFAULT_TEXT_SIZE, font_size: int = DEFAULT_TEXT_SIZE,
font_weight: FontWeight = FontWeight.NORMAL, font_weight: FontWeight = FontWeight.NORMAL,
text_color: rl.Color = DEFAULT_TEXT_COLOR, text_color: rl.Color = DEFAULT_TEXT_COLOR,
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, alignment: int = TextAlignment.LEFT,
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, alignment_vertical: int = TextAlignmentVertical.TOP,
text_padding: int = 0, text_padding: int = 0,
max_width: int | None = None, max_width: int | None = None,
elide: bool = True, elide: bool = True,
@@ -561,9 +532,9 @@ class UnifiedLabel(Widget):
total_visible_height += size.y * self._line_height total_visible_height += size.y * self._line_height
# Calculate vertical alignment offset # Calculate vertical alignment offset
if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: if self._alignment_vertical == TextAlignmentVertical.TOP:
start_y = self._rect.y start_y = self._rect.y
elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: elif self._alignment_vertical == TextAlignmentVertical.BOTTOM:
start_y = self._rect.y + self._rect.height - total_visible_height start_y = self._rect.y + self._rect.height - total_visible_height
else: # TEXT_ALIGN_MIDDLE else: # TEXT_ALIGN_MIDDLE
start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 start_y = self._rect.y + (self._rect.height - total_visible_height) / 2
@@ -640,11 +611,11 @@ class UnifiedLabel(Widget):
def _render_line(self, line, size, current_y, x_offset=0.0): def _render_line(self, line, size, current_y, x_offset=0.0):
# Calculate horizontal position # Calculate horizontal position
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: if self._alignment == TextAlignment.LEFT:
line_x = self._rect.x + self._text_padding line_x = self._rect.x + self._text_padding
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: elif self._alignment == TextAlignment.CENTER:
line_x = self._rect.x + (self._rect.width - size.x) / 2 line_x = self._rect.x + (self._rect.width - size.x) / 2
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: elif self._alignment == TextAlignment.RIGHT:
line_x = self._rect.x + self._rect.width - size.x - self._text_padding line_x = self._rect.x + self._rect.width - size.x - self._text_padding
else: else:
line_x = self._rect.x + self._text_padding line_x = self._rect.x + self._text_padding
@@ -662,9 +633,9 @@ class UnifiedLabel(Widget):
def _render_line_shimmer(self, line, line_x, current_y): def _render_line_shimmer(self, line, line_x, current_y):
# Shimmer range based on widest line so sweep is even across all lines # Shimmer range based on widest line so sweep is even across all lines
max_width = self.text_width max_width = self.text_width
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: if self._alignment == TextAlignment.RIGHT:
shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: elif self._alignment == TextAlignment.CENTER:
shimmer_left = self._rect.x + (self._rect.width - max_width) / 2 shimmer_left = self._rect.x + (self._rect.width - max_width) / 2
else: else:
shimmer_left = self._rect.x + self._text_padding shimmer_left = self._rect.x + self._text_padding
+5 -5
View File
@@ -3,7 +3,7 @@ import os
import pyray as rl import pyray as rl
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from abc import ABC from abc import ABC
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -139,8 +139,8 @@ class ButtonAction(ItemAction):
if value_text: if value_text:
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height) value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR, gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
# TODO: just use the generic Widget click callbacks everywhere, no returning from render # TODO: just use the generic Widget click callbacks everywhere, no returning from render
pressed = self._pressed pressed = self._pressed
@@ -168,8 +168,8 @@ class TextAction(ItemAction):
def _render(self, rect: rl.Rectangle) -> bool: def _render(self, rect: rl.Rectangle) -> bool:
gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color,
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, font_weight=FontWeight.NORMAL, alignment=TextAlignment.RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) alignment_vertical=TextAlignmentVertical.MIDDLE)
return False return False
def set_text(self, text: str | Callable[[], str]): def set_text(self, text: str | Callable[[], str]):
+5 -5
View File
@@ -3,7 +3,7 @@ from functools import partial
from typing import Any, cast from typing import Any, cast
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app, TextAlignment
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid
@@ -67,7 +67,7 @@ class NavButton(Widget):
def _render(self, _): def _render(self, _):
color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255) color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255)
rl.draw_rectangle_rounded(self._rect, 0.6, 10, color) rl.draw_rectangle_rounded(self._rect, 0.6, 10, color)
gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(self.rect, self.text, font_size=60, alignment=TextAlignment.CENTER)
class NetworkUI(Widget): class NetworkUI(Widget):
@@ -315,7 +315,7 @@ class WifiManagerUI(Widget):
def _render(self, rect: rl.Rectangle): def _render(self, rect: rl.Rectangle):
if not self._networks: if not self._networks:
gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=TextAlignment.CENTER)
return return
if self.state == UIState.NEEDS_AUTH and self._state_network: if self.state == UIState.NEEDS_AUTH and self._state_network:
@@ -389,7 +389,7 @@ class WifiManagerUI(Widget):
if status_text: if status_text:
status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT)
gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) gui_label(status_text_rect, status_text, font_size=48, alignment=TextAlignment.CENTER)
else: else:
# If the network is saved, show the "Forget" button # If the network is saved, show the "Forget" button
if self._wifi_manager.is_connection_saved(network.ssid): if self._wifi_manager.is_connection_saved(network.ssid):
@@ -455,7 +455,7 @@ class WifiManagerUI(Widget):
self._networks = networks self._networks = networks
for n in self._networks: for n in self._networks:
self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55, self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55,
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
font_size=45) font_size=45)
+2 -2
View File
@@ -1,6 +1,6 @@
import pyray as rl import pyray as rl
from collections.abc import Callable from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.button import Button, ButtonStyle
@@ -29,7 +29,7 @@ class MultiOptionDialog(Widget):
# Create scroller with option buttons # Create scroller with option buttons
self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt), self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt),
font_weight=option_font_weight, font_weight=option_font_weight,
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL, text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.NORMAL,
text_padding=50, elide_right=True) for option in options] text_padding=50, elide_right=True) for option in options]
self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING) self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING)
+3 -3
View File
@@ -3,7 +3,7 @@ from collections.abc import Callable
import pyray as rl import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter
@@ -41,8 +41,8 @@ class SliderBase(Widget, abc.ABC):
self._is_dragging_circle = False self._is_dragging_circle = False
self._label = self._child(UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE, self._label = self._child(UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment=TextAlignment.RIGHT,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True)) alignment_vertical=TextAlignmentVertical.MIDDLE, line_height=0.9, shimmer=True))
@abc.abstractmethod @abc.abstractmethod
def _load_assets(self): def _load_assets(self):
+8 -4
View File
@@ -99,10 +99,10 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET")
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"]))
cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc',
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'utils/export.cc', 'utils/util.cc', 'utils/qtutil.cc', 'utils/strings.cc', 'utils/elidedlabel.cc',
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc',
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc']
if arch != "Darwin": if arch != "Darwin":
cabana_srcs += ['streams/socketcanstream.cc'] cabana_srcs += ['streams/socketcanstream.cc']
@@ -119,8 +119,12 @@ if GetOption('extras'):
dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'),
dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'),
dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'),
dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'),
dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'),
dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src),
dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'),
] ]
dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common])
output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json'
generate_dbc = cabana_env.Command('#' + output_json_file, generate_dbc = cabana_env.Command('#' + output_json_file,

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