Compare commits

..

35 Commits

Author SHA1 Message Date
Jason Wen
6fc9247eaa test 2025-12-03 03:10:28 -05:00
Jason Wen
b9c54e07fb Revert "ci: disable macos builds" (#1529)
Revert "ci: disable macos builds (#1514)"

This reverts commit 42b2e1534b.
2025-12-03 01:18:00 -05:00
James Vecellio-Grant
c560ac43aa ui: NetworkUISP (#1535)
* ui: network panel

* refactor adding scan button without overriding upstream layout

* trigger immediate network refresh instead of waiting

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-03 01:08:38 -05:00
David
a83c64ffbd ui: fix sidebar scroll in UI screenshots (#1519)
* fix: delay between scroll clicks; add larger delay; fix cruise button

* scroll more for cruise

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-03 00:56:19 -05:00
Nayan
f08dfa1c14 ui: sunnylink client-side implementation (#1488)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* sp raylib preview

* fix callback

* fix ui preview

* sunnylink state

* introducing ui_state_sp for py

* poll from ui_state_sp

* cloudlog & ruff

* param to control stock vs sp ui

* better

* better padding

* this

* listitem -> listitemsp

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* fetch only when connected to network

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-03 00:20:40 -05:00
James Vecellio-Grant
3c5841ff02 ui: vehicle brand settings (#1509)
* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* sunnypilot: remove Qt

* cabana: revert to stock Qt

* commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9

* commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e

* commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e

* sum more

* bump opendbc

* not yet

* should've been symlink'ed

* raylib says wut

* quiet mode back

* more fixes

* no more

* too extra red diff on the side

* need to bring this back

* too extra

* let's update docs here

* Revert "let's update docs here"

This reverts commit 51fe03cd51.

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* multi-button

* Lint

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* dialog txt

* compare vs what used to be done before InputDialog

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* support for next line multi-button

* uhh

* disabled colors

* listitem -> listitemsp

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* lint

* no fancy toggles :(

* match them

* mici scroller - no touchy

* no

* more

* size adjustments

* fix scroller. yay

* ui vehicle panel: platform selector

* some brands

* platform selector

* bruh

* ui_state_sp

* o

* is_offroad() and format

* huh

* use in toggles panel

* ugh. no

* better & animated

* lint

* cleanup

* lint. LINT

* slight

* rebase

* no more

* rebae

* vic

* v

* # Conflicts:
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/helpers/fuzzy_search.py

* loathing loathing, unadulterated loathing, i loathe it all

* more changes!

* Update styles.py

* set visibility

* Update hyundai.py

* add padding

* use symlink on sp side

* use make from json and show all actual makes

* all done!

* Revert "all done!"

This reverts commit 595c45f057.

* reimpl onroad/offroad confirmation

* use global offroad directly

* ui: highlight on pressed, and less indent

* inherit MultiOptionDialog main buttons

* align top level folders to the edge properly

* lint

* lint

* handle folder presses too

* ui: overridable title and subtitle for `TreeOptionDialog`

* override TreeOptionDialog title and subtitle

* lint

* more

* ui: magnifying glass icon and new search bar style in `TreeDialog`

* cleanup

* ui: recreate search dialog with the latest query

* make model year but display as platform

* move into settings directory

* move into dir

* sync

* equality

* use singleton directly

* also use singleton directly

* inherit from base class

* include all brands

* added refresh

* always assume it's subaru

* slight

* split get brand

* hyundai changes

* tesla changes

* do not allow while offroad

* fix

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: nayan <nayan8teen@gmail.com>
2025-12-02 14:44:50 -05:00
Jason Wen
138d637bbd ui: couple Platform Selector refresh in Vehicle panel state updates (#1545) 2025-12-02 13:34:35 -05:00
Jason Wen
eda189c564 ui: refine height calculations and action placement in ListViewSP (#1543)
* ui: refine height calculations and action placement in `ListViewSP`

* relative
2025-12-02 02:29:29 -05:00
James Vecellio-Grant
04504d47f3 ui: Platform Selector (#1508)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* dialog txt

* compare vs what used to be done before InputDialog

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* lint

* no fancy toggles :(

* match them

* mici scroller - no touchy

* no

* more

* size adjustments

* fix scroller. yay

* ui vehicle panel: platform selector

* platform selector

* bruh

* ui_state_sp

* huh

* rebase

* rebae

* vic

* # Conflicts:
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/helpers/fuzzy_search.py

* loathing loathing, unadulterated loathing, i loathe it all

* more changes!

* Update styles.py

* add padding

* use symlink on sp side

* use make from json and show all actual makes

* all done!

* Revert "all done!"

This reverts commit 595c45f057.

* reimpl onroad/offroad confirmation

* use global offroad directly

* ui: highlight on pressed, and less indent

* inherit MultiOptionDialog main buttons

* align top level folders to the edge properly

* lint

* lint

* handle folder presses too

* ui: overridable title and subtitle for `TreeOptionDialog`

* override TreeOptionDialog title and subtitle

* lint

* more

* ui: magnifying glass icon and new search bar style in `TreeDialog`

* cleanup

* ui: recreate search dialog with the latest query

* make model year but display as platform

* move into settings directory

* move into dir

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-02 00:02:33 -05:00
Jason Wen
7ba9876fa4 ui: recreate search dialog with the latest query (#1542) 2025-12-01 23:44:01 -05:00
Jason Wen
f312c011e8 ui: magnifying glass icon and new search bar style in TreeDialog (#1541)
* ui: magnifying glass icon and new search bar style in `TreeDialog`

* cleanup
2025-12-01 23:33:26 -05:00
Jason Wen
9ee965d2e0 ui: overridable title and subtitle for search query in TreeOptionDialog (#1538)
* ui: overridable title and subtitle for `TreeOptionDialog`

* lint
2025-12-01 21:35:28 -05:00
James Vecellio-Grant
859745ea86 ui: tree dialog improvements (#1537)
* ui: highlight on pressed, and less indent

* inherit MultiOptionDialog main buttons

* align top level folders to the edge properly

* lint

* handle folder presses too

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-12-01 17:31:37 -05:00
James Vecellio-Grant
79fa8803b6 ui: add padding above tree dialog buttons (#1533)
add padding
2025-12-01 09:22:49 -05:00
James Vecellio-Grant
c68c914444 ui: tree dialog widget (#1493)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* sp raylib preview

* fix callback

* fix ui preview

* dialog txt

* compare vs what used to be done before InputDialog

* merge origin raylib toggles

* tree dialog

* less trees for the planet

* the heck

* save the trees we got icons

* Update process.py

* Remove 'sunnypilot_ui' 

Removed 'sunnypilot_ui' parameter from params_keys.h

* Update raylib_screenshots.py

Removed the parameter setting for 'sunnypilot_ui' in the test.

* ui: fuzzy search helper

* better tree. fully dynamic and stuff

* rm

* more indent

* only show if fav_param is used in the call

* conditional for mypy

* sunny's new x,y makes this even easier!

* loathing loathing, unadulterated loathing, i loathe it all

* more changes!

* more changes!

* Update BUTTON_DISABLED_BG_COLOR to a lighter shade

* Update tree_dialog.py

* final

---------

Co-authored-by: nayan <nayan8teen@gmail.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-30 01:21:15 -05:00
Nayan
0f7498e214 ui: UIStateSP (#1489)
* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* add cp_sp to ui_state_sp

* fix ui crash

* update params

* more

* slightly more

* add directly to the list

* nah

* move around

* rename

* call before param time tracker is updated

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-30 00:54:03 -05:00
James Vecellio-Grant
ebca4fc901 ui: fuzzy search helper (#1507)
* ui: fuzzy search helper

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-29 04:19:48 -05:00
Jason Wen
023b842e3c controlsd_ext: use time.monotonic to check params intervals (#1481)
* controlsd_ext: use time.monotonic to check params intervals

* test

* Revert "test"

This reverts commit 151ac3bc68.
2025-11-29 03:58:58 -05:00
DevTekVE
a4454721ea sunnylink: dynamic param metadata (#1522)
* feat(params): add support for parameter metadata retrieval

- Introduced `getKeyMetadata` method for accessing metadata associated with params.
- Enhanced `getParamsAllKeysV1` to include metadata parsing and optional dynamic enum generation.
- Extended unit tests to verify metadata parsing, enum mapping, and edge cases.

* Revert "feat(params): add support for parameter metadata retrieval"

This reverts commit 865b695ff9.

* update: integrate params metadata management and unit tests

- Added `update_params_metadata.py` to manage and update parameters metadata.
- Enhanced `getParamsAllKeysV1` to include metadata for params.
- Created comprehensive tests (`test_params_metadata.py`, `test_params_sync.py`) to validate metadata integrity and params consistency.

* update: improve params metadata readability and enhance enums

- Renamed params titles for clarity and consistency.
- Added enum options and mappings to selected params for better usability.

* update: enhance params metadata with improved enum structures

- Replaced plain enum lists with detailed objects (`value`, `label`) for clarity.
- Standardized parameter options for consistency across metadata.

* update: add validation constraints to params metadata

- Introduced `min`, `max`, and `step` attributes for improved parameter range validation.
- Enhances user input handling and ensures consistency in metadata.

* lint

* more lint stuff and permissions

* does this suffice?

* more lint

* update: refine params type hinting and remove unused shebang

- Adjusted type annotation in `params_dict` for better compatibility.
- Removed unnecessary shebang from `test_params_metadata.py`.

* update: expand test coverage for params metadata validation

- Added detailed test cases to ensure metadata consistency (`options`, `constraints`, `titles`).
- Validates API response alignment with `params_metadata.json`.

* the finals

* names

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-29 03:52:01 -05:00
James Vecellio-Grant
3e29a0ccfe ui: ProgressBarAction (#1492)
* raylib: progress bar

* freaking test dir

* easier to see

* smoother updating

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-29 03:30:18 -05:00
James Vecellio-Grant
bee820f8ed ui: InputDialogSP (#1484)
* dialog txt

* compare vs what used to be done before InputDialog

* rm

* final

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-29 03:23:03 -05:00
Nayan
99ed90d459 ui: Option Control (#1479)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* Option Control

* Need this

* better padding

* this

* listitem -> listitemsp

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* simplify

* I. SAID. SIMPLIFY.

* AAARGGGGGG.....

* option control value fix

* forgot about the setter

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* ugh. no

* new style. old style.

* lint

* rename

* old but gold <3

---------

Co-authored-by: discountchubbs <alexgrant990@gmail.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
2025-11-29 03:10:59 -05:00
Jason Wen
aa5a7ecb31 ci: no parallelism for LiveLocationKalman compile (#1531)
* ci: no parallelism for locationd compile

* just LLK

* bump to 2
2025-11-29 02:24:12 -05:00
Jason Wen
9595a6f246 ui Qt: remove leftover files (#1530) 2025-11-28 20:46:09 -05:00
DevTekVE
1d6d0fb85c ci: prevent build OOM by throttling locationd concurrency (#1527)
* ci: add conditional swap creation to prebuilt workflow

- Dynamically creates an 8GB swap file on systems with less than 8GB RAM.
- Ensures builds complete reliably on low-memory environments.
- Introduced cleanup step to remove swap after workflow execution.

* Nice save

* clean

* reduce swap size in prebuilt workflow
- Adjusted swap file size from 8GB to 4GB to optimize resource usage.

* remove swap creation from prebuilt workflow
- Simplified workflow by removing dynamic swap file creation and cleanup.
- Adjusted resource management to rely on existing system resources.

* update sunnypilot build workflow to use explicit script calls

- Replaced `op` commands with explicit `/data/openpilot/tools/op.sh` script calls for better reliability and clarity.

* update sunnypilot build workflow to use systemd for process management

- Replaced `/data/openpilot/tools/op.sh` script calls with `systemctl` commands for improved compatibility and reliability.
- Ensures consistent resource management during start and stop operations.

* Splitting a bit the build then?

* split build steps in sunnypilot prebuilt workflow

- Added separate build steps for `modeld`, `modeld_v2`, and `locationd` with descriptive messages.
- Improves logging and clarity during the build process.

* limit locationd parallel processes aiming to help with resource consumption

* typo

* adding op's location d AND bumping to 4 cores for them

* Apply suggestion from @sunnyhaibin

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-28 20:19:30 -05:00
Nayan
4914445415 ui: sunnypilot MultiButtonControl (#1478)
* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* sunnypilot: remove Qt

* cabana: revert to stock Qt

* commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9

* commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e

* commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e

* sum more

* bump opendbc

* not yet

* should've been symlink'ed

* raylib says wut

* quiet mode back

* more fixes

* no more

* too extra red diff on the side

* need to bring this back

* too extra

* let's update docs here

* Revert "let's update docs here"

This reverts commit 51fe03cd51.

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* multi-button

* Lint

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* better padding

* this

* support for next line multi-button

* uhh

* disabled colors

* listitem -> listitemsp

* listitem -> listitemsp

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use in toggles panel

* ugh. no

* better & animated

* lint

* cleanup

* lint. LINT

* slight

* default no inline ty <3

* style change

* move stuff around

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-28 01:02:31 -05:00
Jason Wen
c229eb4f38 ui: reimplement sunnypilot branding with Raylib (#1517)
* ui: sunnypilot branding

* fr??
2025-11-25 18:37:09 -05:00
Jason Wen
da35e60101 ui: display mouse coordinates with debug mode (#1512)
* ui: add debug to display mouse coordinates

* cleanup

* use mouse x and y directly

* even less

* try this out

* ui: increase `scroll` delay in UI Preview

* sleep for clicks too

* don't lol

* try 1 secs

* try this out

* wait a sec before doing a screenshot

* nah
2025-11-25 18:10:40 -05:00
Jason Wen
1e95e1cc01 Revert "ui: reduce sidebar scroll in UI Preview" (#1516)
Revert "ui: reduce sidebar `scroll` in UI Preview (#1513)"

This reverts commit ad88c306e9.
2025-11-25 18:04:21 -05:00
Jason Wen
ad88c306e9 ui: reduce sidebar scroll in UI Preview (#1513)
* ui: increase `scroll` delay in UI Preview

* sleep for clicks too

* don't lol

* try 1 secs

* try this out

* wait a sec before doing a screenshot

* nah
2025-11-25 17:51:03 -05:00
Jason Wen
42b2e1534b ci: disable macos builds (#1514)
* ci: fix macos builds

* Revert "ci: fix macos builds"

This reverts commit 433ca0d7f6.

* disable
2025-11-25 16:56:53 -05:00
DevTekVE
c53e2134e2 sunnylink: centralize key pair handling in sunnylink registration (#1510)
* refactor(api): centralize key pair handling in SunnyLink registration

- Replaced manual key reading with `BaseApi.get_key_pair` for consistency.
- Simplifies code and improves maintainability of key management.

* cleanup
2025-11-24 23:33:40 +01:00
Nayan
844f4cbc74 ui: sunnypilot panels (#1477)
* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* better padding

* this

* listitem -> listitemsp

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* lint

* no fancy toggles :(

* match them

* mici scroller - no touchy

* no

* more

* size adjustments

* fix scroller. yay

* more

* loopy loop

* update brand name

* ui preview fixes

* Revert "update brand name"

This reverts commit ad44c32fc7.

* fix sidebar scroller reset

* fix developer click

* more

* more

* try this out

* one more time

* bruh

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-24 14:43:33 -05:00
Nayan
9edc36ca66 ui: fix scroll panel mouse wheel behavior (#1506)
fix scroller. yay

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-24 11:18:12 -05:00
James Vecellio-Grant
3cd55260d9 modeld: remove SNPE accelerator (#1476)
* commaai/openpilot:d05cb31e2e916fba41ba8167030945f427fd811b

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* bump opendbc

* sunnypilot: remove Qt

* cabana: revert to stock Qt

* commaai/openpilot:5198b1b079c37742c1050f02ce0aa6dd42b038b9

* commaai/openpilot:954b567b9ba0f3d1ae57d6aa7797fa86dd92ec6e

* commaai/openpilot:7534b2a160faa683412c04c1254440e338931c5e

* sum more

* bump opendbc

* not yet

* should've been symlink'ed

* raylib says wut

* quiet mode back

* more fixes

* no more

* too extra red diff on the side

* need to bring this back

* too extra

* let's update docs here

* Revert "let's update docs here"

This reverts commit 51fe03cd51.

* del snpe

* reset min for safety catch as we killed snpe

* uhhh

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2025-11-23 15:22:23 -05:00
138 changed files with 3225 additions and 7518 deletions

View File

@@ -6,10 +6,10 @@ env:
CI_DIR: ${{ github.workspace }}/release/ci
SCONS_CACHE_DIR: ${{ github.workspace }}/release/ci/scons_cache
PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot"
# Branch configurations
STAGING_SOURCE_BRANCH: 'master'
# Runtime configuration
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
@@ -75,7 +75,7 @@ jobs:
cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')";
echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT
echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
@@ -85,7 +85,7 @@ jobs:
fi
echo "build=$BUILD" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
validate_tests:
runs-on: ubuntu-24.04
needs: [ prepare_strategy ]
@@ -119,7 +119,7 @@ jobs:
needs.prepare_strategy.result == 'success' &&
(needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') &&
(!contains(github.event_name, 'pull_request') ||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
}}
steps:
- uses: actions/checkout@v4
@@ -134,7 +134,7 @@ jobs:
with:
path: ${{env.SCONS_CACHE_DIR}}
key: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}-${{ github.sha }}
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
# Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.)
# for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden.
restore-keys: |
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }}
@@ -148,7 +148,7 @@ jobs:
echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
# Set up common environment
source /etc/profile;
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
@@ -180,6 +180,15 @@ jobs:
./release/release_files.py | sort | uniq | rsync -rRl${RUNNER_DEBUG:+v} --files-from=- . $BUILD_DIR/
cd $BUILD_DIR
sed -i '/from .board.jungle import PandaJungle, PandaJungleDFU/s/^/#/' panda/__init__.py
echo "Building sunnypilot's modeld..."
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld
echo "Building sunnypilot's modeld_v2..."
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/modeld_v2
echo "Building sunnypilot's locationd..."
scons -j2 cache_dir=${{env.SCONS_CACHE_DIR}} --minimal sunnypilot/selfdrive/locationd
echo "Building openpilot's locationd..."
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal selfdrive/locationd
echo "Building rest of sunnypilot"
scons -j$(nproc) cache_dir=${{env.SCONS_CACHE_DIR}} --minimal
touch ${BUILD_DIR}/prebuilt
if [[ "${{ runner.debug }}" == "1" ]]; then
@@ -241,8 +250,8 @@ jobs:
if: always()
run: |
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
publish:
concurrency:
# We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
@@ -293,7 +302,7 @@ jobs:
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
echo "3. Update as needed (JSON array with no spaces)"
- name: Tag ${{ needs.prepare_strategy.outputs.environment }}
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
run: |
@@ -302,7 +311,7 @@ jobs:
git push -f origin ${TAG}
notify:
needs:
needs:
- prepare_strategy
- build
- publish
@@ -331,7 +340,7 @@ jobs:
${{ vars.DISCOURSE_GENERAL_UPDATE_NOTICE }}
EOF
)
{
echo 'content<<EOFMARKER'
echo "$MESSAGE"

View File

@@ -75,7 +75,6 @@ env = Environment(
"#third_party/acados/include/hpipm/include",
"#third_party/catch2/include",
"#third_party/libyuv/include",
"#third_party/snpe/include",
],
LIBPATH=[
"#common",
@@ -101,7 +100,6 @@ if arch == "larch64":
"/usr/local/lib",
"/system/vendor/lib64",
"/usr/lib/aarch64-linux-gnu",
"#third_party/snpe/larch64",
])
arch_flags = ["-D__TICI__", "-mcpu=cortex-a57", "-DQCOM2"]
env.Append(CCFLAGS=arch_flags)
@@ -125,14 +123,6 @@ else:
"/usr/local/lib",
])
if arch == "x86_64":
env.Append(LIBPATH=[
f"#third_party/snpe/{arch}"
])
env.Append(RPATH=[
Dir(f"#third_party/snpe/{arch}").abspath,
])
# Sanitizers and extra CCFLAGS from CLI
if GetOption('asan'):
env.Append(CCFLAGS=["-fsanitize=address", "-fno-omit-frame-pointer"])

Binary file not shown.

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
import math
import threading
import time
from numbers import Number
from cereal import car, log
@@ -22,8 +20,6 @@ from openpilot.selfdrive.controls.lib.longcontrol import LongControl
from openpilot.selfdrive.modeld.modeld import LAT_SMOOTH_SECONDS
from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt
State = log.SelfdriveState.OpenpilotState
@@ -33,7 +29,7 @@ LaneChangeDirection = log.LaneChangeDirection
ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys())
class Controls(ControlsExt, ModelStateBase):
class Controls(ControlsExt):
def __init__(self) -> None:
self.params = Params()
cloudlog.info("controlsd is waiting for CarParams")
@@ -42,7 +38,6 @@ class Controls(ControlsExt, ModelStateBase):
# Initialize sunnypilot controlsd extension and base model state
ControlsExt.__init__(self, self.CP, self.params)
ModelStateBase.__init__(self)
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
@@ -231,30 +226,15 @@ class Controls(ControlsExt, ModelStateBase):
cc_send.carControl = CC
self.pm.send('carControl', cc_send)
def params_thread(self, evt):
while not evt.is_set():
self.get_params_sp()
if self.CP.lateralTuning.which() == 'torque':
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
time.sleep(0.1)
def run(self):
rk = Ratekeeper(100, print_delay_threshold=None)
e = threading.Event()
t = threading.Thread(target=self.params_thread, args=(e,))
try:
t.start()
while True:
self.update()
CC, lac_log = self.state_control()
self.publish(CC, lac_log)
self.run_ext(self.sm, self.pm)
rk.monitor_time()
finally:
e.set()
t.join()
while True:
self.update()
CC, lac_log = self.state_control()
self.publish(CC, lac_log)
self.get_params_sp(self.sm)
self.run_ext(self.sm, self.pm)
rk.monitor_time()
def main():

View File

@@ -177,7 +177,7 @@ class HomeLayout(Widget):
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)
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=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, font_weight=FontWeight.AUDIOWIDE)
def _render_home_content(self):
self._render_left_column()
@@ -227,6 +227,6 @@ class HomeLayout(Widget):
self._prev_alerts_present = alerts_present
def _get_version_text(self) -> str:
brand = "openpilot"
brand = "sunnypilot"
description = self.params.get("UpdaterCurrentDescription")
return f"{brand} {description}" if description else brand

View File

@@ -109,8 +109,8 @@ class TermsPage(Widget):
self._on_accept = on_accept
self._on_decline = on_decline
self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."),
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._desc = Label(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
@@ -143,10 +143,10 @@ class TermsPage(Widget):
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."),
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
self._back_btn = Button(tr("Back"), click_callback=back_callback)
self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER,
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
click_callback=self._on_uninstall_clicked)
def _on_uninstall_clicked(self):

View File

@@ -23,10 +23,11 @@ DESCRIPTIONS = {
"other than your own. A comma employee will NEVER ask you to add their GitHub username."
),
'alpha_longitudinal': tr_noop(
"<b>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
"On this car, openpilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. " +
"Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. " +
"Changing this setting will restart openpilot if the car is powered on."
"<b>WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</b><br><br>" +
"On this car, sunnypilot defaults to the car's built-in ACC instead of sunnypilot's longitudinal control. " +
"Enable this to switch to sunnypilot longitudinal control. " +
"Enabling Experimental mode is recommended when enabling sunnypilot longitudinal control alpha. " +
"Changing this setting will restart sunnypilot if the car is powered on."
),
}
@@ -71,7 +72,7 @@ class DeveloperLayout(Widget):
)
self._alpha_long_toggle = toggle_item(
lambda: tr("openpilot Longitudinal Control (Alpha)"),
lambda: tr("sunnypilot Longitudinal Control (Alpha)"),
description=lambda: tr(DESCRIPTIONS["alpha_longitudinal"]),
initial_state=self._params.get_bool("AlphaLongitudinalEnabled"),
callback=self._on_alpha_long_enabled,
@@ -148,6 +149,7 @@ class DeveloperLayout(Widget):
self._params.put_bool("ShowDebugInfo", state)
gui_app.set_show_touches(state)
gui_app.set_show_fps(state)
gui_app.set_show_mouse_coords(state)
def _on_enable_adb(self, state: bool):
self._params.put_bool("AdbEnabled", state)

View File

@@ -23,8 +23,8 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
DESCRIPTIONS = {
'pair_device': tr_noop("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."),
'driver_camera': tr_noop("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)"),
'reset_calibration': tr_noop("openpilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': tr_noop("Review the rules, features, and limitations of openpilot"),
'reset_calibration': tr_noop("sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down."),
'review_guide': tr_noop("Review the rules, features, and limitations of sunnypilot"),
}
@@ -162,8 +162,8 @@ class DeviceLayout(Widget):
cloudlog.exception("invalid LiveTorqueParameters")
desc += "<br><br>"
desc += tr("openpilot is continuously calibrating, resetting is rarely required. " +
"Resetting calibration will restart openpilot if the car is powered on.")
desc += tr("sunnypilot is continuously calibrating, resetting is rarely required. " +
"Resetting calibration will restart sunnypilot if the car is powered on.")
self._reset_calib_btn.set_description(desc)

View File

@@ -17,7 +17,7 @@ from openpilot.selfdrive.ui.lib.api_helpers import get_token
TITLE = tr_noop("Firehose Mode")
DESCRIPTION = tr_noop(
"openpilot learns to drive by watching humans, like you, drive.\n\n"
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
+ "Firehose Mode allows you to maximize your training data uploads to improve "
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
)

View File

@@ -11,26 +11,27 @@ from openpilot.selfdrive.ui.ui_state import ui_state
if gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp as multiple_button_item
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
# Description constants
DESCRIPTIONS = {
"OpenpilotEnabledToggle": tr_noop(
"Use the openpilot system for adaptive cruise control and lane keep driver assistance. " +
"Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. " +
"Your attention is required at all times to use this feature."
),
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage openpilot."),
"DisengageOnAccelerator": tr_noop("When enabled, pressing the accelerator pedal will disengage sunnypilot."),
"LongitudinalPersonality": tr_noop(
"Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " +
"In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. " +
"In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " +
"your steering wheel distance button."
),
"IsLdwEnabled": tr_noop(
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
"without a turn signal activated while driving over 31 mph (50 km/h)."
),
"AlwaysOnDM": tr_noop("Enable driver monitoring even when openpilot is not engaged."),
"AlwaysOnDM": tr_noop("Enable driver monitoring even when sunnypilot is not engaged."),
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."),
@@ -46,7 +47,7 @@ class TogglesLayout(Widget):
# param, title, desc, icon, needs_restart
self._toggle_defs = {
"OpenpilotEnabledToggle": (
lambda: tr("Enable openpilot"),
lambda: tr("Enable sunnypilot"),
DESCRIPTIONS["OpenpilotEnabledToggle"],
"chffr_wheel.png",
True,
@@ -99,7 +100,7 @@ class TogglesLayout(Widget):
lambda: tr("Driving Personality"),
lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]),
buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")],
button_width=255,
button_width=300,
callback=self._set_longitudinal_personality,
selected_index=self._params.get("LongitudinalPersonality", return_default=True),
icon="speed_limit.png"
@@ -125,7 +126,7 @@ class TogglesLayout(Widget):
# Make description callable for live translation
additional_desc = ""
if needs_restart and not locked:
additional_desc = tr("Changing this setting will restart openpilot if the car is powered on.")
additional_desc = tr("Changing this setting will restart sunnypilot if the car is powered on.")
toggle.set_description(lambda og_desc=toggle.description, add_desc=additional_desc: tr(og_desc) + (" " + tr(add_desc) if add_desc else ""))
# track for engaged state updates
@@ -158,10 +159,10 @@ class TogglesLayout(Widget):
ui_state.update_params()
e2e_description = tr(
"openpilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
"sunnypilot defaults to driving in chill mode. Experimental mode enables alpha-level features that aren't ready for chill mode. " +
"Experimental features are listed below:<br>" +
"<h4>End-to-End Longitudinal Control</h4><br>" +
"Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
"Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. " +
"Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; " +
"mistakes should be expected.<br>" +
"<h4>New Driving Visualization</h4><br>" +
@@ -183,13 +184,13 @@ class TogglesLayout(Widget):
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
long_desc = unavailable + " " + tr("sunnypilot longitudinal control may come in a future update.")
if ui_state.CP.alphaLongitudinalAvailable:
if self._is_release:
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
long_desc = unavailable + " " + tr("An alpha version of sunnypilot longitudinal control can be tested, along with " +
"Experimental mode, on non-release branches.")
else:
long_desc = tr("Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.")
long_desc = tr("Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode.")
self._toggles["ExperimentalMode"].set_description("<b>" + long_desc + "</b><br><br>" + e2e_description)
else:

View File

@@ -45,7 +45,7 @@ class DeviceStatus(Widget):
self._version_text = self._get_version_text()
def _get_version_text(self) -> str:
brand = "openpilot"
brand = "sunnypilot"
description = ui_state.params.get("UpdaterCurrentDescription")
return f"{brand} {description}" if description else brand
@@ -111,7 +111,7 @@ class MiciHomeLayout(Widget):
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
self._openpilot_label = MiciLabel("openpilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)

View File

@@ -254,7 +254,7 @@ class MiciOffroadAlerts(Widget):
parts = new_desc.split(" / ")
if len(parts) > 3:
version, date = parts[0], parts[3]
update_alert_data.text = f"update available\n openpilot {version}, {date}. go to comma.ai/blog to read the release notes."
update_alert_data.text = f"update available\n sunnypilot {version}, {date}. go to comma.ai/blog to read the release notes."
update_alert_data.visible = True
active_count += 1

View File

@@ -182,7 +182,7 @@ class TrainingGuideAttentionNotice(SetupTermsPage):
def __init__(self, continue_callback):
super().__init__(continue_callback, continue_text="continue")
self._title_header = TermsHeader("driver assistance", gui_app.texture("icons_mici/setup/warning.png", 60, 60))
self._warning_label = UnifiedLabel("1. openpilot is a driver assistance system.\n\n" +
self._warning_label = UnifiedLabel("1. sunnypilot is a driver assistance system.\n\n" +
"2. You must pay attention at all times.\n\n" +
"3. You must be ready to take over at any time.\n\n" +
"4. You are fully responsible for driving the car.", 42,
@@ -239,12 +239,12 @@ class TrainingGuide(Widget):
class DeclinePage(Widget):
def __init__(self, back_callback=None):
super().__init__()
self._uninstall_slider = SmallSlider("uninstall openpilot", self._on_uninstall)
self._uninstall_slider = SmallSlider("uninstall sunnypilot", self._on_uninstall)
self._back_button = SmallButton("back")
self._back_button.set_click_callback(back_callback)
self._warning_header = TermsHeader("you must accept the\nterms to use openpilot",
self._warning_header = TermsHeader("you must accept the\nterms to use sunnypilot",
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
def _on_uninstall(self):
@@ -282,7 +282,7 @@ class TermsPage(SetupTermsPage):
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
self._title_header = TermsHeader("terms & conditions", info_txt)
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use openpilot. " +
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
FontWeight.ROMAN)

View File

@@ -156,7 +156,7 @@ class UpdateOpenpilotBigButton(BigButton):
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64)
self._txt_reboot_icon = gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64)
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
super().__init__("update openpilot", "", self._txt_update_icon)
super().__init__("update sunnypilot", "", self._txt_update_icon)
self._waiting_for_updater_t: float | None = None
self._hide_value_t: float | None = None
@@ -193,7 +193,7 @@ class UpdateOpenpilotBigButton(BigButton):
if value:
self.set_text("")
else:
self.set_text("update openpilot")
self.set_text("update sunnypilot")
def _update_state(self):
if ui_state.started:
@@ -294,7 +294,7 @@ class DeviceLayoutMici(NavWidget):
reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png")
reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset"))
uninstall_openpilot_btn = BigButton("uninstall openpilot", "", "icons_mici/settings/device/uninstall.png")
uninstall_openpilot_btn = BigButton("uninstall sunnypilot", "", "icons_mici/settings/device/uninstall.png")
uninstall_openpilot_btn.set_click_callback(lambda: _engaged_confirmation_callback(uninstall_openpilot_callback, "uninstall"))
reboot_btn = BigCircleButton("icons_mici/settings/device/reboot.png", red=False)

View File

@@ -17,7 +17,7 @@ from openpilot.system.ui.widgets import NavWidget
TITLE = tr_noop("Firehose Mode")
DESCRIPTION = tr_noop(
"openpilot learns to drive by watching humans, like you, drive.\n\n"
"sunnypilot learns to drive by watching humans, like you, drive.\n\n"
+ "Firehose Mode allows you to maximize your training data uploads to improve "
+ "openpilot's driving models. More data means bigger models, which means better Experimental Mode."
)

View File

@@ -24,7 +24,7 @@ class TogglesLayoutMici(NavWidget):
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback)
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
self._scroller = Scroller([
self._personality_toggle,

View File

@@ -65,7 +65,7 @@ class Alert:
# Pre-defined alert instances
ALERT_STARTUP_PENDING = Alert(
text1="openpilot Unavailable",
text1="sunnypilot Unavailable",
text2="Waiting to start",
size=AlertSize.mid,
status=AlertStatus.normal,

View File

@@ -153,7 +153,7 @@ class AugmentedRoadView(CameraView):
self._alert_renderer = AlertRenderer()
self._driver_state_renderer = DriverStateRenderer()
self._confidence_ball = ConfidenceBall()
self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 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)),
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
@@ -171,7 +171,7 @@ class AugmentedRoadView(CameraView):
if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._offroad_label.set_text("system booting")
else:
self._offroad_label.set_text("start the car to\nuse openpilot")
self._offroad_label.set_text("start the car to\nuse sunnypilot")
def _handle_mouse_release(self, mouse_pos: MousePos):
# Don't trigger click callback if bookmark was triggered

View File

@@ -48,7 +48,7 @@ class Alert:
# Pre-defined alert instances
ALERT_STARTUP_PENDING = Alert(
text1=tr("openpilot Unavailable"),
text1=tr("sunnypilot Unavailable"),
text2=tr("Waiting to start"),
size=AlertSize.mid,
status=AlertStatus.normal,

View File

@@ -1,158 +0,0 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#include "selfdrive/ui/qt/offroad/offroad_home.h"
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/prime.h"
// OffroadHome: the offroad home page
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(40, 40, 40, 40);
// top header
header_layout = new QHBoxLayout();
header_layout->setContentsMargins(0, 0, 0, 0);
header_layout->setSpacing(16);
update_notif = new QPushButton(tr("UPDATE"));
update_notif->setVisible(false);
update_notif->setStyleSheet("background-color: #364DEF;");
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); });
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
alert_notif = new QPushButton();
alert_notif->setVisible(false);
alert_notif->setStyleSheet("background-color: #E22C2C;");
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); });
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
version = new ElidedLabel();
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
main_layout->addLayout(header_layout);
// main content
main_layout->addSpacing(25);
center_layout = new QStackedLayout();
QWidget *home_widget = new QWidget(this);
{
home_layout = new QHBoxLayout(home_widget);
home_layout->setContentsMargins(0, 0, 0, 0);
home_layout->setSpacing(30);
#ifndef SUNNYPILOT
// left: PrimeAdWidget
QStackedWidget *left_widget = new QStackedWidget(this);
QVBoxLayout *left_prime_layout = new QVBoxLayout();
left_prime_layout->setContentsMargins(0, 0, 0, 0);
QWidget *prime_user = new PrimeUserWidget();
prime_user->setStyleSheet(R"(
border-radius: 10px;
background-color: #333333;
)");
left_prime_layout->addWidget(prime_user);
left_prime_layout->addStretch();
left_widget->addWidget(new LayoutWidget(left_prime_layout));
left_widget->addWidget(new PrimeAdWidget);
left_widget->setStyleSheet("border-radius: 10px;");
connect(uiState()->prime_state, &PrimeState::changed, [left_widget]() {
left_widget->setCurrentIndex(uiState()->prime_state->isSubscribed() ? 0 : 1);
});
home_layout->addWidget(left_widget, 1);
#endif
// right: ExperimentalModeButton, SetupWidget
QWidget* right_widget = new QWidget(this);
QVBoxLayout* right_column = new QVBoxLayout(right_widget);
right_column->setContentsMargins(0, 0, 0, 0);
right_widget->setFixedWidth(750);
right_column->setSpacing(30);
ExperimentalModeButton *experimental_mode = new ExperimentalModeButton(this);
QObject::connect(experimental_mode, &ExperimentalModeButton::openSettings, this, &OffroadHome::openSettings);
right_column->addWidget(experimental_mode, 1);
SetupWidget *setup_widget = new SetupWidget;
QObject::connect(setup_widget, &SetupWidget::openSettings, this, &OffroadHome::openSettings);
right_column->addWidget(setup_widget, 1);
home_layout->addWidget(right_widget, 1);
}
center_layout->addWidget(home_widget);
// add update & alerts widgets
update_widget = new UpdateAlert();
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(update_widget);
alerts_widget = new OffroadAlert();
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(alerts_widget);
main_layout->addLayout(center_layout, 1);
// set up refresh timer
timer = new QTimer(this);
timer->callOnTimeout(this, &OffroadHome::refresh);
setStyleSheet(R"(
* {
color: white;
}
OffroadHome {
background-color: black;
}
OffroadHome > QPushButton {
padding: 15px 30px;
border-radius: 5px;
font-size: 40px;
font-weight: 500;
}
OffroadHome > QLabel {
font-size: 55px;
}
)");
}
void OffroadHome::showEvent(QShowEvent *event) {
refresh();
timer->start(10 * 1000);
}
void OffroadHome::hideEvent(QHideEvent *event) {
timer->stop();
}
void OffroadHome::refresh() {
version->setText(getBrand() + " " + QString::fromStdString(params.get("UpdaterCurrentDescription")));
bool updateAvailable = update_widget->refresh();
int alerts = alerts_widget->refresh();
// pop-up new notification
int idx = center_layout->currentIndex();
if (!updateAvailable && !alerts) {
idx = 0;
} else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
idx = 1;
} else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
idx = 2;
}
center_layout->setCurrentIndex(idx);
update_notif->setVisible(updateAvailable);
alert_notif->setVisible(alerts);
if (alerts) {
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
}
}

View File

@@ -1,59 +0,0 @@
/**
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
*
* This file is part of sunnypilot and is licensed under the MIT License.
* See the LICENSE.md file in the root directory for more details.
*/
#pragma once
#include "common/params.h"
#include "selfdrive/ui/qt/body.h"
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
#ifdef SUNNYPILOT
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
#include "selfdrive/ui/sunnypilot/qt/onroad/onroad_home.h"
#include "selfdrive/ui/sunnypilot/qt/sidebar.h"
#include "selfdrive/ui/sunnypilot/qt/widgets/prime.h"
#define OnroadWindow OnroadWindowSP
#define LayoutWidget LayoutWidgetSP
#define Sidebar SidebarSP
#define ElidedLabel ElidedLabelSP
#define SetupWidget SetupWidgetSP
#else
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/onroad/onroad_home.h"
#include "selfdrive/ui/qt/sidebar.h"
#include "selfdrive/ui/qt/widgets/prime.h"
#endif
class OffroadHome : public QFrame {
Q_OBJECT
public:
explicit OffroadHome(QWidget* parent = 0);
signals:
void openSettings(int index = 0, const QString &param = "");
protected:
QHBoxLayout *home_layout;
QHBoxLayout *header_layout;
void showEvent(QShowEvent *event) override;
void refresh();
private:
void hideEvent(QHideEvent *event) override;
Params params;
QTimer* timer;
ElidedLabel* version;
QStackedLayout* center_layout;
UpdateAlert *update_widget;
OffroadAlert* alerts_widget;
QPushButton* alert_notif;
QPushButton* update_notif;
};

View File

@@ -0,0 +1,46 @@
"""
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 threading
import time
import pyray as rl
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.network import NetworkUI, PanelType
class NetworkUISP(NetworkUI):
def __init__(self, wifi_manager):
super().__init__(wifi_manager)
self.scan_button = Button(tr("Scan"), self._scan_clicked, button_style=ButtonStyle.NORMAL, font_size=60, border_radius=30)
self.scan_button.set_rect(rl.Rectangle(0, 0, 400, 100))
self._scanning = False
self._wifi_manager.add_callbacks(networks_updated=self._on_networks_updated)
def _scan_clicked(self):
self._scanning = True
self.scan_button.set_text(tr("Scanning..."))
self.scan_button.set_enabled(False)
threading.Thread(target=self._wifi_manager._update_networks, daemon=True).start()
self._wifi_manager._request_scan()
self._wifi_manager._last_network_update = time.monotonic()
def _on_networks_updated(self, networks):
if self._scanning:
self._scanning = False
self.scan_button.set_text(tr("Scan"))
self.scan_button.set_enabled(True)
def _render(self, rect: rl.Rectangle):
super()._render(rect)
if self._current_panel == PanelType.WIFI:
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
self.scan_button.render()

View File

@@ -19,10 +19,10 @@ from openpilot.system.ui.lib.multilang import tr_noop
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets.network import NetworkUI
from openpilot.system.ui.lib.wifi_manager import WifiManager
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
@@ -60,6 +60,43 @@ class PanelInfo(OP.PanelInfo):
icon: str = ""
class NavButton(Widget):
def __init__(self, parent, p_type, p_info):
super().__init__()
self.parent = parent
self.panel_type = p_type
self.panel_info = p_info
def _render(self, rect):
is_selected = self.panel_type == self.parent._current_panel
text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL
content_x = rect.x + 90
text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65)
# Draw background if selected
if is_selected:
self.container_rect = rl.Rectangle(
content_x - 50, rect.y, OP.SIDEBAR_WIDTH - 50, OP.NAV_BTN_HEIGHT
)
rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR)
if self.panel_info.icon:
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2),
rl.WHITE)
content_x += ICON_SIZE + 20
# Draw button text (right-aligned)
text_pos = rl.Vector2(
content_x,
rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2
)
rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color)
# Store button rect for click detection
self.panel_info.button_rect = rect
class SettingsLayoutSP(OP.SettingsLayout):
def __init__(self):
OP.SettingsLayout.__init__(self)
@@ -74,7 +111,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
self._panels = {
OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"),
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager), icon="icons/network.png"),
OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUISP(wifi_manager), icon="icons/network.png"),
OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/shell.png"),
OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"),
OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"),
@@ -91,44 +128,6 @@ class SettingsLayoutSP(OP.SettingsLayout):
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"),
}
def _create_nav_button(self, panel_type: OP.PanelType, panel_info: PanelInfo) -> Widget:
class NavButton(Widget):
def __init__(self, parent, p_type, p_info):
super().__init__()
self.parent = parent
self.panel_type = p_type
self.panel_info = p_info
def _render(self, rect):
is_selected = self.panel_type == self.parent._current_panel
text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL
content_x = rect.x + 90
text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65)
# Draw background if selected
if is_selected:
self.container_rect = rl.Rectangle(
content_x - 50, rect.y, OP.SIDEBAR_WIDTH - 50, OP.NAV_BTN_HEIGHT
)
rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR)
if self.panel_info.icon:
icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True)
rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2), rl.WHITE)
content_x += ICON_SIZE + 20
# Draw button text (right-aligned)
text_pos = rl.Vector2(
content_x,
rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2
)
rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color)
# Store button rect for click detection
self.panel_info.button_rect = rect
return NavButton(self, panel_type, panel_info)
def _draw_sidebar(self, rect: rl.Rectangle):
rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR)
@@ -164,7 +163,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
# Navigation buttons with scroller
if not self._nav_items:
for panel_type, panel_info in self._panels.items():
nav_button = self._create_nav_button(panel_type, panel_info)
nav_button = NavButton(self, panel_type, panel_info)
nav_button.rect.width = rect.width - 100 # Full width minus padding
nav_button.rect.height = OP.NAV_BTN_HEIGHT
self._nav_items.append(nav_button)
@@ -196,3 +195,8 @@ class SettingsLayoutSP(OP.SettingsLayout):
return True
return False
def show_event(self):
super().show_event()
self._panels[self._current_panel].instance.show_event()
self._sidebar_scroller.show_event()

View File

@@ -1,30 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
class VehicleLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
items = self._initialize_items()
self._scroller = Scroller(items, line_separator=True, spacing=0)
def _initialize_items(self):
items = [
]
return items
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,67 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.list_view import ButtonAction
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.factory import BrandSettingsFactory
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.platform_selector import PlatformSelector, LegendWidget
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
class VehicleLayout(Widget):
def __init__(self):
super().__init__()
self._brand_settings = None
self._brand_items = []
self._current_brand = None
self._platform_selector = PlatformSelector(self._update_brand_settings)
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
callback=self._platform_selector._on_clicked)
self._vehicle_item.title_color = self._platform_selector.color
self._legend_widget = LegendWidget(self._platform_selector)
self.items = [self._vehicle_item, self._legend_widget]
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
@staticmethod
def get_brand():
if bundle := ui_state.params.get("CarPlatformBundle"):
return bundle.get("brand", "")
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
return ui_state.CP.brand
return ""
def _update_brand_settings(self):
self._vehicle_item._title = self._platform_selector.text
self._vehicle_item.title_color = self._platform_selector.color
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
self._vehicle_item.action_item.set_text(vehicle_text)
brand = self.get_brand()
if brand != self._current_brand:
self._current_brand = brand
self._brand_settings = BrandSettingsFactory.create_brand_settings(brand)
self._brand_items = self._brand_settings.items if self._brand_settings else []
self.items = [self._vehicle_item, self._legend_widget] + self._brand_items
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
def _update_state(self):
self._update_brand_settings()
if self._brand_settings:
self._brand_settings.update_settings()
self._platform_selector.refresh()
def _render(self, rect):
self._scroller.render(rect)
def show_event(self):
self._scroller.show_event()

View File

@@ -0,0 +1,16 @@
"""
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 abc
class BrandSettings(abc.ABC):
def __init__(self):
self.items = []
@abc.abstractmethod
def update_settings(self) -> None:
"""Update the settings based on the current vehicle brand."""

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class BodySettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class ChryslerSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,45 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.body import BodySettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.chrysler import ChryslerSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.ford import FordSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.gm import GMSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.honda import HondaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.hyundai import HyundaiSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.mazda import MazdaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.nissan import NissanSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.psa import PSASettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.rivian import RivianSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.subaru import SubaruSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.tesla import TeslaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.toyota import ToyotaSettings
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.volkswagen import VolkswagenSettings
class BrandSettingsFactory:
_BRAND_MAP: dict[str, type[BrandSettings]] = {
"body": BodySettings,
"chrysler": ChryslerSettings,
"ford": FordSettings,
"gm": GMSettings,
"honda": HondaSettings,
"hyundai": HyundaiSettings,
"mazda": MazdaSettings,
"nissan": NissanSettings,
"psa": PSASettings,
"rivian": RivianSettings,
"subaru": SubaruSettings,
"tesla": TeslaSettings,
"toyota": ToyotaSettings,
"volkswagen": VolkswagenSettings,
}
@staticmethod
def create_brand_settings(brand: str) -> BrandSettings | None:
cls = BrandSettingsFactory._BRAND_MAP.get(brand)
return cls() if cls is not None else None

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class FordSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class GMSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class HondaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,59 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp
from opendbc.car.hyundai.values import CAR, CANFD_UNSUPPORTED_LONGITUDINAL_CAR, UNSUPPORTED_LONGITUDINAL_CAR
class HyundaiSettings(BrandSettings):
def __init__(self):
super().__init__()
self.alpha_long_available = False
tuning_texts = [tr("Off"), tr("Dynamic"), tr("Predictive")]
self.longitudinal_tuning_item = multiple_button_item_sp(tr("Custom Longitudinal Tuning"), "", tuning_texts,
button_width=300, callback=self._on_tuning_selected,
param="HyundaiLongitudinalTuning", inline=False)
self.items = [self.longitudinal_tuning_item]
@staticmethod
def _on_tuning_selected(index):
ui_state.params.put("HyundaiLongitudinalTuning", index)
def update_settings(self):
self.alpha_long_available = False
bundle = ui_state.params.get("CarPlatformBundle")
if bundle:
platform = bundle.get("platform")
self.alpha_long_available = CAR[platform] not in (UNSUPPORTED_LONGITUDINAL_CAR | CANFD_UNSUPPORTED_LONGITUDINAL_CAR)
elif ui_state.CP:
self.alpha_long_available = ui_state.CP.alphaLongitudinalAvailable
tuning_param = int(ui_state.params.get("HyundaiLongitudinalTuning") or "0")
long_enabled = ui_state.has_longitudinal_control
long_tuning_descs = [
tr("Your vehicle will use the Default longitudinal tuning."),
tr("Your vehicle will use the Dynamic longitudinal tuning."),
tr("Your vehicle will use the Predictive longitudinal tuning."),
]
long_tuning_desc = long_tuning_descs[tuning_param] if tuning_param < len(long_tuning_descs) else long_tuning_descs[0]
longitudinal_tuning_disabled = not ui_state.is_offroad() or not long_enabled
if longitudinal_tuning_disabled:
if not ui_state.is_offroad():
long_tuning_desc = tr("This feature is unavailable while the car is onroad.")
elif not long_enabled:
long_tuning_desc = tr("This feature is unavailable because sunnypilot Longitudinal Control (Alpha) is not enabled.")
self.longitudinal_tuning_item.action_item.set_enabled(not longitudinal_tuning_disabled)
self.longitudinal_tuning_item.set_description(long_tuning_desc)
self.longitudinal_tuning_item.show_description(True)
self.longitudinal_tuning_item.action_item.set_selected_button(tuning_param)
self.longitudinal_tuning_item.set_visible(self.alpha_long_available)

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class MazdaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class NissanSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class PSASettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class RivianSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,54 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from opendbc.car.subaru.values import CAR, SubaruFlags
class SubaruSettings(BrandSettings):
def __init__(self):
super().__init__()
self.has_stop_and_go = False
self.stop_and_go_toggle = toggle_item_sp(tr("Stop and Go (Beta)"), "", param="SubaruStopAndGo", callback=self._on_toggle_changed)
self.stop_and_go_manual_parking_brake_toggle = toggle_item_sp(tr("Stop and Go for Manual Parking Brake (Beta)"), "",
param="SubaruStopAndGoManualParkingBrake", callback=self._on_toggle_changed)
self.items = [self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle]
def _on_toggle_changed(self, _):
self.update_settings()
def stop_and_go_disabled_msg(self):
if not self.has_stop_and_go:
return tr("This feature is currently not available on this platform.")
elif not ui_state.is_offroad():
return tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
return ""
def update_settings(self):
bundle = ui_state.params.get("CarPlatformBundle")
if bundle:
platform = bundle.get("platform")
config = CAR[platform].config
self.has_stop_and_go = not (config.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
elif ui_state.CP:
self.has_stop_and_go = not (ui_state.CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
disabled_msg = self.stop_and_go_disabled_msg()
descriptions = [
tr("Experimental feature to enable auto-resume during stop-and-go for certain supported Subaru platforms."),
tr("Experimental feature to enable stop and go for Subaru Global models with manual handbrake. " +
"Models with electric parking brake should keep this disabled. Thanks to martinl for this implementation!")
]
for toggle, desc in zip([self.stop_and_go_toggle, self.stop_and_go_manual_parking_brake_toggle], descriptions, strict=True):
toggle.action_item.set_enabled(self.has_stop_and_go and ui_state.is_offroad())
toggle.set_description(f"<b>{disabled_msg}</b><br><br>{desc}" if disabled_msg else desc)

View File

@@ -0,0 +1,43 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
COOP_STEERING_MIN_KMH = 23
OEM_STEERING_MIN_KMH = 48
KM_TO_MILE = 0.621371
class TeslaSettings(BrandSettings):
def __init__(self):
super().__init__()
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
self.items = [self.coop_steering_toggle]
def update_settings(self):
is_metric = ui_state.is_metric
unit = "km/h" if is_metric else "mph"
display_value_coop = COOP_STEERING_MIN_KMH if is_metric else round(COOP_STEERING_MIN_KMH * KM_TO_MILE)
display_value_oem = OEM_STEERING_MIN_KMH if is_metric else round(OEM_STEERING_MIN_KMH * KM_TO_MILE)
coop_steering_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to toggle.")
coop_steering_warning = tr(f"Warning: May experience steering oscillations below {display_value_oem} {unit} during turns, " +
"recommend disabling this feature if you experience these.")
coop_steering_desc = (
f"<b>{coop_steering_warning}</b><br><br>" +
f"{tr('Allows the driver to provide limited steering input while openpilot is engaged.')}<br>" +
f"{tr(f'Only works above {display_value_coop} {unit}.')}"
)
if not ui_state.is_offroad():
coop_steering_desc = f"<b>{coop_steering_disabled_msg}</b><br><br>{coop_steering_desc}"
self.coop_steering_toggle.set_description(coop_steering_desc)
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class ToyotaSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,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.
See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
class VolkswagenSettings(BrandSettings):
def __init__(self):
super().__init__()
def update_settings(self):
pass

View File

@@ -0,0 +1,138 @@
"""
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 json
import os
import pyray as rl
from collections.abc import Callable
from functools import partial
from openpilot.common.basedir import BASEDIR
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
from openpilot.selfdrive.ui.ui_state import ui_state
CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json")
class LegendWidget(Widget):
def __init__(self, platform_selector):
super().__init__()
self.set_rect(rl.Rectangle(0, 0, 0, 350))
self._platform_selector = platform_selector
self._font = gui_app.font(FontWeight.NORMAL)
self._bold_font = gui_app.font(FontWeight.BOLD)
def _render(self, rect):
x = rect.x + 20
y = rect.y + 20
rl.draw_text_ex(self._font, tr("Select vehicle to force fingerprint manually."), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
y += 80
rl.draw_text_ex(self._font, tr("Colors represent vehicle fingerprint status:"), rl.Vector2(x, y), 40, 0, style.ITEM_DESC_TEXT_COLOR)
y += 80
items = [
(style.GREEN, tr("Fingerprinted automatically")),
(style.BLUE, tr("Manually selected fingerprint")),
(style.YELLOW, tr("Not fingerprinted or manually selected")),
]
for color, text in items:
p_color = self._platform_selector.color
is_active = p_color.r == color.r and p_color.g == color.g and p_color.b == color.b and p_color.a == color.a
rl.draw_rectangle(int(x), int(y + 5), 30, 30, color)
font = self._bold_font if is_active else self._font
text_color = rl.WHITE if is_active else style.ITEM_DESC_TEXT_COLOR
rl.draw_text_ex(font, f"- {text}", rl.Vector2(x + 50, y - 7), 40, 0, text_color)
y += 50
class PlatformSelector(Button):
def __init__(self, on_platform_change: Callable[[], None] | None = None):
super().__init__(tr("Vehicle"), self._on_clicked, button_style=ButtonStyle.NORMAL)
self.set_rect(rl.Rectangle(0, 0, 0, 120))
with open(CAR_LIST_JSON_OUT) as car_list_json:
self._platforms = json.load(car_list_json)
self._on_platform_change = on_platform_change
self.refresh()
@property
def text(self):
return self._label._text
def set_parent_rect(self, parent_rect):
super().set_parent_rect(parent_rect)
self._rect.width = parent_rect.width
def _on_clicked(self):
if ui_state.params.get("CarPlatformBundle"):
ui_state.params.remove("CarPlatformBundle")
self.refresh()
if self._on_platform_change:
self._on_platform_change()
else:
self._show_platform_dialog()
def _set_platform(self, platform_name):
if data := self._platforms.get(platform_name):
ui_state.params.put("CarPlatformBundle", {**data, "name": platform_name})
self.refresh()
if self._on_platform_change:
self._on_platform_change()
def _on_platform_selected(self, dialog, res):
if res == DialogResult.CONFIRM and dialog.selection_ref:
offroad_msg = tr("This setting will take effect immediately.") if ui_state.is_offroad else \
tr("This setting will take effect once the device enters offroad state.")
confirm_dialog = ConfirmDialog(offroad_msg, tr("Confirm"))
callback = partial(self._confirm_platform, dialog.selection_ref)
gui_app.set_modal_overlay(confirm_dialog, callback=callback)
def _confirm_platform(self, platform_name, res):
if res == DialogResult.CONFIRM:
self._set_platform(platform_name)
def _show_platform_dialog(self):
platforms = sorted(self._platforms.keys())
makes = sorted({self._platforms[p].get('make') for p in platforms})
folders = [TreeFolder(make, [TreeNode(p, {
'display_name': p,
'search_tags': f"{p} {self._platforms[p].get('make')} {' '.join(map(str, self._platforms[p].get('year', [])))} {self._platforms[p].get('model', p)}"
}) for p in platforms if self._platforms[p].get('make') == make]) for make in makes]
dialog = TreeOptionDialog(
tr("Select a vehicle"),
folders,
search_title=tr("Search your vehicle"),
search_subtitle=tr("Enter model year (e.g., 2021) and model (Toyota Corolla):"),
search_funcs=[lambda node: node.data.get('display_name', ''), lambda node: node.data.get('search_tags', '')]
)
callback = partial(self._on_platform_selected, dialog)
dialog.on_exit = callback
gui_app.set_modal_overlay(dialog, callback=callback)
def refresh(self):
self.color = style.YELLOW
self._platform = tr("Unrecognized Vehicle")
self.set_text(tr("No vehicle selected"))
if bundle := ui_state.params.get("CarPlatformBundle"):
self._platform = bundle.get("name", "")
self.set_text(self._platform)
self.color = style.BLUE
elif ui_state.CP and ui_state.CP.carFingerprint != "MOCK":
self._platform = ui_state.CP.carFingerprint
self.set_text(self._platform)
self.color = style.GREEN
self.set_enabled(True)

View File

@@ -0,0 +1,29 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from cereal import messaging, custom
from openpilot.common.params import Params
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
class UIStateSP:
def __init__(self):
self.params = Params()
self.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP"
]
self.sunnylink_state = SunnylinkState()
def update(self) -> None:
self.sunnylink_state.start()
def update_params(self) -> None:
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
if CP_SP_bytes is not None:
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")

View File

@@ -57,7 +57,7 @@ def setup_settings(click, pm: PubMaster, scroll=None):
def close_settings(click, pm: PubMaster, scroll=None):
click(240, 216)
click(140, 120)
def setup_settings_network(click, pm: PubMaster, scroll=None):
@@ -72,13 +72,13 @@ def setup_settings_network_advanced(click, pm: PubMaster, scroll=None):
def setup_settings_toggles(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 720)
click(278, 620)
def setup_settings_software(click, pm: PubMaster, scroll=None):
put_update_params(Params())
setup_settings(click, pm)
click(278, 845)
click(278, 730)
def setup_settings_software_download(click, pm: PubMaster, scroll=None):
@@ -105,8 +105,8 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None):
def setup_settings_firehose(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 950)
scroll(-20, 278, 950)
click(278, 850)
def setup_settings_developer(click, pm: PubMaster, scroll=None):
@@ -115,8 +115,8 @@ def setup_settings_developer(click, pm: PubMaster, scroll=None):
Params().put("CarParamsPersistent", CP.to_bytes())
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 1040)
scroll(-20, 278, 950)
click(278, 950)
def setup_keyboard(click, pm: PubMaster, scroll=None):
@@ -151,49 +151,59 @@ def setup_experimental_mode_description(click, pm: PubMaster, scroll=None):
def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster, scroll=None):
setup_settings_developer(click, pm, scroll=scroll)
click(650, 960) # toggle openpilot longitudinal control
click(650, 960) # toggle sunnypilot longitudinal control
def setup_settings_sunnylink(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 600)
click(278, 510)
def setup_settings_models(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 950)
click(278, 840)
def setup_settings_steering(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
click(278, 1040)
click(278, 950)
def setup_settings_cruise(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-40, 278, 950)
click(278, 1040)
scroll(-4, 278, 950)
click(278, 860)
def setup_settings_visuals(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
scroll(-20, 278, 950)
click(278, 330)
def setup_settings_display(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 450)
scroll(-20, 278, 950)
click(278, 420)
def setup_settings_osm(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 600)
scroll(-20, 278, 950)
click(278, 520)
def setup_settings_trips(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 720)
scroll(-20, 278, 950)
click(278, 630)
def setup_settings_vehicle(click, pm: PubMaster, scroll=None):
setup_settings(click, pm)
scroll(-1000, 278, 950)
click(278, 845)
scroll(-20, 278, 950)
click(278, 750)
def setup_onroad(click, pm: PubMaster, scroll=None):
ds = messaging.new_message('deviceState')
@@ -332,9 +342,14 @@ class TestUI:
time.sleep(0.01)
pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs)
def scroll(self, clicks, x, y, *args, **kwargs):
pyautogui.scroll(clicks, self.ui.left + x, self.ui.top + y, *args, **kwargs)
time.sleep(UI_DELAY)
def scroll(self, clicks: int, x, y, *args, **kwargs):
if clicks == 0:
return
click = -1 if clicks < 0 else 1 # -1 = down, 1 = up
for _ in range(abs(clicks)):
pyautogui.scroll(click, self.ui.left + x, self.ui.top + y, *args, **kwargs) # scroll for individual clicks since we need to delay between clicks
time.sleep(0.01) # small delay between scroll clicks to work properly
time.sleep(2) # wait for scroll to fully settle
@with_processes(["ui"])
def test_ui(self, name, setup_case):

View File

@@ -12,6 +12,8 @@ from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.hardware import HARDWARE, PC
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
@@ -21,7 +23,7 @@ class UIStatus(Enum):
OVERRIDE = "override"
class UIState:
class UIState(UIStateSP):
_instance: 'UIState | None' = None
def __new__(cls):
@@ -31,6 +33,7 @@ class UIState:
return cls._instance
def _initialize(self):
UIStateSP.__init__(self)
self.params = Params()
self.sm = messaging.SubMaster(
[
@@ -55,7 +58,7 @@ class UIState:
"carControl",
"liveParameters",
"rawAudioData",
]
] + self.sm_services_ext
)
self.prime_state = PrimeState()
@@ -111,6 +114,7 @@ class UIState:
if time.monotonic() - self._param_update_time > 5.0:
self.update_params()
device.update()
UIStateSP.update(self)
def _update_state(self) -> None:
# Handle panda states updates
@@ -180,6 +184,7 @@ class UIState:
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
else:
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
UIStateSP.update_params(self)
self._param_update_time = time.monotonic()

View File

@@ -1,5 +1,3 @@
import glob
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
lenv = env.Clone()
lenvCython = envCython.Clone()
@@ -23,12 +21,6 @@ thneed_src_qcom = thneed_src_common + ["thneed/thneed_qcom2.cc"]
thneed_src_pc = thneed_src_common + ["thneed/thneed_pc.cc"]
thneed_src = thneed_src_qcom if arch == "larch64" else thneed_src_pc
# SNPE except on Mac and ARM Linux
snpe_lib = []
if arch != "Darwin" and arch != "aarch64":
common_src += ['runners/snpemodel.cc']
snpe_lib += ['SNPE']
# OpenCL is a framework on Mac
if arch == "Darwin":
frameworks += ['OpenCL']
@@ -40,20 +32,13 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
for xenv in (lenv, lenvCython):
xenv['CXXFLAGS'].append(f'-D{pathdef}_PATH=\\"{File(fn).abspath}\\"')
# Compile cython
snpe_rpath_qcom = "/data/pythonpath/third_party/snpe/larch64"
snpe_rpath_pc = f"{Dir('#').abspath}/third_party/snpe/x86_64-linux-clang"
snpe_rpath = lenvCython['RPATH'] + [snpe_rpath_qcom if arch == "larch64" else snpe_rpath_pc]
cython_libs = envCython["LIBS"] + libs
snpemodel_lib = lenv.Library('snpemodel', ['runners/snpemodel.cc'])
commonmodel_lib = lenv.Library('commonmodel', common_src)
lenvCython.Program('runners/runmodel_pyx.so', 'runners/runmodel_pyx.pyx', LIBS=cython_libs, FRAMEWORKS=frameworks)
lenvCython.Program('runners/snpemodel_pyx.so', 'runners/snpemodel_pyx.pyx', LIBS=[snpemodel_lib, snpe_lib, *cython_libs], FRAMEWORKS=frameworks, RPATH=snpe_rpath)
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
if arch == "larch64":
thneed_lib = env.SharedLibrary('thneed', thneed_src, LIBS=[common, 'OpenCL', 'dl'])
thneedmodel_lib = env.Library('thneedmodel', ['runners/thneedmodel.cc'])
lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL'])
lenvCython.Program('runners/thneedmodel_pyx.so', 'runners/thneedmodel_pyx.pyx', LIBS=envCython["LIBS"]+[thneedmodel_lib, thneed_lib, common, 'dl', 'OpenCL'])

View File

@@ -4,20 +4,15 @@ from openpilot.sunnypilot.modeld.runners.runmodel_pyx import RunModel, Runtime
assert Runtime
USE_THNEED = int(os.getenv('USE_THNEED', str(int(TICI))))
USE_SNPE = int(os.getenv('USE_SNPE', str(int(TICI))))
class ModelRunner(RunModel):
THNEED = 'THNEED'
SNPE = 'SNPE'
ONNX = 'ONNX'
def __new__(cls, paths, *args, **kwargs):
if ModelRunner.THNEED in paths and USE_THNEED:
from openpilot.sunnypilot.modeld.runners.thneedmodel_pyx import ThneedModel as Runner
runner_type = ModelRunner.THNEED
elif ModelRunner.SNPE in paths and USE_SNPE:
from openpilot.sunnypilot.modeld.runners.snpemodel_pyx import SNPEModel as Runner
runner_type = ModelRunner.SNPE
elif ModelRunner.ONNX in paths:
from openpilot.sunnypilot.modeld.runners.onnxmodel import ONNXModel as Runner
runner_type = ModelRunner.ONNX

View File

@@ -1,4 +1,3 @@
#pragma once
#include "sunnypilot/modeld/runners/runmodel.h"
#include "sunnypilot/modeld/runners/snpemodel.h"

View File

@@ -1,116 +0,0 @@
#pragma clang diagnostic ignored "-Wexceptions"
#include "sunnypilot/modeld/runners/snpemodel.h"
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "common/util.h"
#include "common/timing.h"
void PrintErrorStringAndExit() {
std::cerr << zdl::DlSystem::getLastErrorString() << std::endl;
std::exit(EXIT_FAILURE);
}
SNPEModel::SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool _use_tf8, cl_context context) {
output = _output;
output_size = _output_size;
use_tf8 = _use_tf8;
#ifdef QCOM2
if (runtime == USE_GPU_RUNTIME) {
snpe_runtime = zdl::DlSystem::Runtime_t::GPU;
} else if (runtime == USE_DSP_RUNTIME) {
snpe_runtime = zdl::DlSystem::Runtime_t::DSP;
} else {
snpe_runtime = zdl::DlSystem::Runtime_t::CPU;
}
assert(zdl::SNPE::SNPEFactory::isRuntimeAvailable(snpe_runtime));
#endif
model_data = util::read_file(path);
assert(model_data.size() > 0);
// load model
std::unique_ptr<zdl::DlContainer::IDlContainer> container = zdl::DlContainer::IDlContainer::open((uint8_t*)model_data.data(), model_data.size());
if (!container) { PrintErrorStringAndExit(); }
LOGW("loaded model with size: %lu", model_data.size());
// create model runner
zdl::SNPE::SNPEBuilder snpe_builder(container.get());
while (!snpe) {
#ifdef QCOM2
snpe = snpe_builder.setOutputLayers({})
.setRuntimeProcessor(snpe_runtime)
.setUseUserSuppliedBuffers(true)
.setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE)
.build();
#else
snpe = snpe_builder.setOutputLayers({})
.setUseUserSuppliedBuffers(true)
.setPerformanceProfile(zdl::DlSystem::PerformanceProfile_t::HIGH_PERFORMANCE)
.build();
#endif
if (!snpe) std::cerr << zdl::DlSystem::getLastErrorString() << std::endl;
}
// create output buffer
zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float;
zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory();
const auto &output_tensor_names_opt = snpe->getOutputTensorNames();
if (!output_tensor_names_opt) throw std::runtime_error("Error obtaining output tensor names");
const auto &output_tensor_names = *output_tensor_names_opt;
assert(output_tensor_names.size() == 1);
const char *output_tensor_name = output_tensor_names.at(0);
const zdl::DlSystem::TensorShape &buffer_shape = snpe->getInputOutputBufferAttributes(output_tensor_name)->getDims();
if (output_size != 0) {
assert(output_size == buffer_shape[1]);
} else {
output_size = buffer_shape[1];
}
std::vector<size_t> output_strides = {output_size * sizeof(float), sizeof(float)};
output_buffer = ub_factory.createUserBuffer(output, output_size * sizeof(float), output_strides, &ub_encoding_float);
output_map.add(output_tensor_name, output_buffer.get());
}
void SNPEModel::addInput(const std::string name, float *buffer, int size) {
const int idx = inputs.size();
const auto &input_tensor_names_opt = snpe->getInputTensorNames();
if (!input_tensor_names_opt) throw std::runtime_error("Error obtaining input tensor names");
const auto &input_tensor_names = *input_tensor_names_opt;
const char *input_tensor_name = input_tensor_names.at(idx);
const bool input_tf8 = use_tf8 && strcmp(input_tensor_name, "input_img") == 0; // TODO: This is a terrible hack, get rid of this name check both here and in onnx_runner.py
LOGW("adding index %d: %s", idx, input_tensor_name);
zdl::DlSystem::UserBufferEncodingFloat ub_encoding_float;
zdl::DlSystem::UserBufferEncodingTf8 ub_encoding_tf8(0, 1./255); // network takes 0-1
zdl::DlSystem::IUserBufferFactory &ub_factory = zdl::SNPE::SNPEFactory::getUserBufferFactory();
zdl::DlSystem::UserBufferEncoding *input_encoding = input_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_tf8 : (zdl::DlSystem::UserBufferEncoding*)&ub_encoding_float;
const auto &buffer_shape_opt = snpe->getInputDimensions(input_tensor_name);
const zdl::DlSystem::TensorShape &buffer_shape = *buffer_shape_opt;
size_t size_of_input = input_tf8 ? sizeof(uint8_t) : sizeof(float);
std::vector<size_t> strides(buffer_shape.rank());
strides[strides.size() - 1] = size_of_input;
size_t product = 1;
for (size_t i = 0; i < buffer_shape.rank(); i++) product *= buffer_shape[i];
size_t stride = strides[strides.size() - 1];
for (size_t i = buffer_shape.rank() - 1; i > 0; i--) {
stride *= buffer_shape[i];
strides[i-1] = stride;
}
auto input_buffer = ub_factory.createUserBuffer(buffer, product*size_of_input, strides, input_encoding);
input_map.add(input_tensor_name, input_buffer.get());
inputs.push_back(std::unique_ptr<SNPEModelInput>(new SNPEModelInput(name, buffer, size, std::move(input_buffer))));
}
void SNPEModel::execute() {
if (!snpe->execute(input_map, output_map)) {
PrintErrorStringAndExit();
}
}

View File

@@ -1,52 +0,0 @@
#pragma once
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#include <memory>
#include <string>
#include <utility>
#include <DlContainer/IDlContainer.hpp>
#include <DlSystem/DlError.hpp>
#include <DlSystem/ITensor.hpp>
#include <DlSystem/ITensorFactory.hpp>
#include <DlSystem/IUserBuffer.hpp>
#include <DlSystem/IUserBufferFactory.hpp>
#include <SNPE/SNPE.hpp>
#include <SNPE/SNPEBuilder.hpp>
#include <SNPE/SNPEFactory.hpp>
#include "sunnypilot/modeld/runners/runmodel.h"
struct SNPEModelInput : public ModelInput {
std::unique_ptr<zdl::DlSystem::IUserBuffer> snpe_buffer;
SNPEModelInput(const std::string _name, float *_buffer, int _size, std::unique_ptr<zdl::DlSystem::IUserBuffer> _snpe_buffer) : ModelInput(_name, _buffer, _size), snpe_buffer(std::move(_snpe_buffer)) {}
void setBuffer(float *_buffer, int _size) {
ModelInput::setBuffer(_buffer, _size);
assert(snpe_buffer->setBufferAddress(_buffer) == true);
}
};
class SNPEModel : public RunModel {
public:
SNPEModel(const std::string path, float *_output, size_t _output_size, int runtime, bool use_tf8 = false, cl_context context = NULL);
void addInput(const std::string name, float *buffer, int size);
void execute();
private:
std::string model_data;
#ifdef QCOM2
zdl::DlSystem::Runtime_t snpe_runtime;
#endif
// snpe model stuff
std::unique_ptr<zdl::SNPE::SNPE> snpe;
zdl::DlSystem::UserBufferMap input_map;
zdl::DlSystem::UserBufferMap output_map;
std::unique_ptr<zdl::DlSystem::IUserBuffer> output_buffer;
bool use_tf8;
float *output;
size_t output_size;
};

View File

@@ -1,9 +0,0 @@
# distutils: language = c++
from libcpp.string cimport string
from msgq.visionipc.visionipc cimport cl_context
cdef extern from "sunnypilot/modeld/runners/snpemodel.h":
cdef cppclass SNPEModel:
SNPEModel(string, float*, size_t, int, bool, cl_context)

View File

@@ -1,17 +0,0 @@
# distutils: language = c++
# cython: c_string_encoding=ascii, language_level=3
import os
from libcpp cimport bool
from libcpp.string cimport string
from .snpemodel cimport SNPEModel as cppSNPEModel
from openpilot.sunnypilot.modeld.models.commonmodel_pyx cimport CLContext
from openpilot.sunnypilot.modeld.runners.runmodel_pyx cimport RunModel
from openpilot.sunnypilot.modeld.runners.runmodel cimport RunModel as cppRunModel
os.environ['ADSP_LIBRARY_PATH'] = "/data/pythonpath/third_party/snpe/dsp/"
cdef class SNPEModel(RunModel):
def __cinit__(self, string path, float[:] output, int runtime, bool use_tf8, CLContext context):
self.model = <cppRunModel *> new cppSNPEModel(path, &output[0], len(output), runtime, use_tf8, context.context)

View File

@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v9.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-docs/refs/heads/gh-pages/docs/driving_models_v10.json"
def __init__(self, params: Params):
self.params = params

View File

@@ -19,8 +19,8 @@ from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 11
REQUIRED_MIN_SELECTOR_VERSION = 11
CURRENT_SELECTOR_VERSION = 12
REQUIRED_MIN_SELECTOR_VERSION = 12
USE_ONNX = os.getenv('USE_ONNX', PC)

View File

@@ -4,21 +4,27 @@ 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 time
import cereal.messaging as messaging
from cereal import log, custom
from opendbc.car import structs
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral
class ControlsExt:
class ControlsExt(ModelStateBase):
def __init__(self, CP: structs.CarParams, params: Params):
ModelStateBase.__init__(self)
self.CP = CP
self.params = params
self._param_update_time: float = 0.0
self.blinker_pause_lateral = BlinkerPauseLateral()
self.get_params_sp()
cloudlog.info("controlsd_ext is waiting for CarParamsSP")
self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP)
@@ -27,8 +33,14 @@ class ControlsExt:
self.sm_services_ext = ['radarState', 'selfdriveStateSP']
self.pm_services_ext = ['carControlSP']
def get_params_sp(self) -> None:
self.blinker_pause_lateral.get_params()
def get_params_sp(self, sm: messaging.SubMaster) -> None:
if time.monotonic() - self._param_update_time > PARAMS_UPDATE_PERIOD:
self.blinker_pause_lateral.get_params()
if self.CP.lateralTuning.which() == 'torque':
self.lat_delay = get_lat_delay(self.params, sm["liveDelay"].lateralDelay)
self._param_update_time = time.monotonic()
def get_lat_active(self, sm: messaging.SubMaster) -> bool:
if self.blinker_pause_lateral.update(sm['carState']):

View File

@@ -3,7 +3,6 @@ import os
import random
import time
from datetime import datetime, timedelta
from pathlib import Path
import jwt
from openpilot.common.api.base import BaseApi
@@ -81,23 +80,19 @@ class SunnylinkApi(BaseApi):
if sunnylink_dongle_id not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID):
return sunnylink_dongle_id
privkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa")
pubkey_path = Path(f"{Paths.persist_root()}/comma/id_rsa.pub")
jwt_algo, private_key, public_key = BaseApi.get_key_pair()
start_time = time.monotonic()
successful_registration = False
if not pubkey_path.is_file():
if not public_key:
sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID
self._status_update("Public key not found, setting dongle ID to unregistered.")
else:
Params().put("LastSunnylinkPingTime", 0) # Reset the last ping time to 0 if we are trying to register
with pubkey_path.open() as f1, privkey_path.open() as f2:
public_key = f1.read()
private_key = f2.read()
backoff = 1
while True:
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm='RS256')
register_token = jwt.encode({'register': True, 'exp': datetime.utcnow() + timedelta(hours=1)}, private_key, algorithm=jwt_algo)
try:
if verbose or time.monotonic() - start_time < timeout / 2:
self._status_update("Registering device to sunnylink...")

View File

@@ -1,5 +1,10 @@
#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from __future__ import annotations
import base64
@@ -32,9 +37,11 @@ LOCAL_PORT_WHITELIST = {8022}
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
DISALLOW_LOG_UPLOAD = threading.Event()
METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json")
params = Params()
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
cloudlog.info("sunnylinkd.handle_long_poll started")
sm = messaging.SubMaster(['deviceState'])
@@ -180,16 +187,30 @@ def getParamsAllKeys() -> list[str]:
@dispatcher.add_method
def getParamsAllKeysV1() -> dict[str, str]:
try:
with open(METADATA_PATH) as f:
metadata = json.load(f)
except Exception:
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception")
metadata = {}
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
params_dict: dict[str, list[dict[str, str | bool | int | None]]] = {"params": []}
params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []}
for key in available_keys:
value = get_param_as_byte(key, get_default=True)
params_dict["params"].append({
param_entry = {
"key": key,
"type": int(params.get_type(key).value),
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
})
}
if key in metadata:
meta_copy = metadata[key].copy()
param_entry["_extra"] = meta_copy
params_dict["params"].append(param_entry)
return {"keys": json.dumps(params_dict.get("params", []))}
@@ -238,10 +259,7 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local
cloudlog.debug("athena.startLocalProxy.starting")
ws = create_connection(
remote_ws_uri,
header={"Authorization": f"Bearer {sunnylink_api.get_token()}"},
enable_multithread=True,
sslopt={"cert_reqs": ssl.CERT_NONE}
remote_ws_uri, header={"Authorization": f"Bearer {sunnylink_api.get_token()}"}, enable_multithread=True, sslopt={"cert_reqs": ssl.CERT_NONE}
)
return start_local_proxy_shim(global_end_event, local_port, ws)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,205 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from enum import IntEnum
import threading
import time
import json
from cereal import messaging
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID, SunnylinkApi
class RoleType(IntEnum):
READONLY = 0
SPONSOR = 1
ADMIN = 2
class SponsorTier(IntEnum):
FREE = 0
NOVICE = 1
SUPPORTER = 2
CONTRIBUTOR = 3
BENEFACTOR = 4
GUARDIAN = 5
class User:
device_id: str
user_id: str
created_at: int
updated_at: int
token_hash: str
def __init__(self, json_data):
self.device_id = json_data.get("device_id")
self.user_id = json_data.get("user_id")
self.created_at = json_data.get("created_at")
self.updated_at = json_data.get("updated_at")
self.token_hash = json_data.get("token_hash")
class Role:
role_type: str
role_tier: str
def __init__(self, json_data):
self.role_type = json_data.get("role_type")
self.role_tier = json_data.get("role_tier")
def _parse_roles(roles: str) -> list[Role]:
lst_roles = []
try:
roles_list = json.loads(roles)
for r in roles_list:
try:
role = Role(r)
lst_roles.append(role)
except Exception as e:
cloudlog.exception(f"Failed to parse role {r}: {e}")
return lst_roles
except Exception as e:
cloudlog.exception(f"Error parsing roles: {e}")
return []
def _parse_users(users: str) -> list[User]:
lst_users = []
try:
users_list = json.loads(users)
for u in users_list:
try:
user = User(u)
lst_users.append(user)
except Exception as e:
cloudlog.exception(f"Failed to parse user {u}: {e}")
return lst_users
except Exception as e:
cloudlog.exception(f"Error parsing users: {e}")
return []
class SunnylinkState:
FETCH_INTERVAL = 5.0 # seconds between API calls
API_TIMEOUT = 10.0 # seconds for API requests
SLEEP_INTERVAL = 0.5 # seconds to sleep between checks in the worker thread
NOT_PAIRED_USERNAMES = ["unregisteredsponsor", "temporarysponsor"]
def __init__(self):
self._params = Params()
self._lock = threading.Lock()
self._running = False
self._thread = None
self._sm = messaging.SubMaster(['deviceState'])
self._roles: list[Role] = []
self._users: list[User] = []
self.sponsor_tier: SponsorTier = SponsorTier.FREE
self.sunnylink_dongle_id = self._params.get("SunnylinkDongleId")
self._api = SunnylinkApi(self.sunnylink_dongle_id)
self._load_initial_state()
def _load_initial_state(self) -> None:
roles_cache = self._params.get("SunnylinkCache_Roles")
users_cache = self._params.get("SunnylinkCache_Users")
if roles_cache is not None:
self._roles = _parse_roles(roles_cache)
self.sponsor_tier = self._get_highest_tier()
if users_cache is not None:
self._users = _parse_users(users_cache)
def _get_highest_tier(self) -> SponsorTier:
role_tier = SponsorTier.FREE
for role in self._roles:
try:
if RoleType[role.role_type.upper()] == RoleType.SPONSOR:
role_tier = max(role_tier, SponsorTier[role.role_tier.upper()])
except Exception as e:
cloudlog.exception(f"Error parsing role {role}: {e} for dongle id {self.sunnylink_dongle_id}")
return role_tier
def _fetch_roles(self) -> None:
if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
return
try:
token = self._api.get_token()
response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/roles", method='GET', access_token=token)
if response.status_code == 200:
self._roles = _parse_roles(response.text)
self._params.put("SunnylinkCache_Roles", response.text)
sponsor_tier = self._get_highest_tier()
with self._lock:
if sponsor_tier != self.sponsor_tier:
self.sponsor_tier = sponsor_tier
cloudlog.info(f"Sunnylink sponsor tier updated to {sponsor_tier.name}")
except Exception as e:
cloudlog.exception(f"Failed to fetch sunnylink roles: {e} for dongle id {self.sunnylink_dongle_id}")
def _fetch_users(self) -> None:
if not self.sunnylink_dongle_id or self.sunnylink_dongle_id == UNREGISTERED_SUNNYLINK_DONGLE_ID:
return
try:
token = self._api.get_token()
response = self._api.api_get(f"device/{self.sunnylink_dongle_id}/users", method='GET', access_token=token)
if response.status_code == 200:
users = response.text
self._params.put("SunnylinkCache_Users", users)
with self._lock:
_parse_users(users)
except Exception as e:
cloudlog.exception(f"Failed to fetch sunnylink users: {e} for dongle id {self.sunnylink_dongle_id}")
def _worker_thread(self) -> None:
while self._running:
if self.is_connected():
self._fetch_roles()
self._fetch_users()
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
if not self._running:
break
time.sleep(self.SLEEP_INTERVAL)
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._running = True
self._thread = threading.Thread(target=self._worker_thread, daemon=True)
self._thread.start()
def stop(self) -> None:
self._running = False
if self._thread and self._thread.is_alive():
self._thread.join(timeout=1.0)
def get_sponsor_tier(self) -> SponsorTier:
with self._lock:
return self.sponsor_tier
def is_sponsor(self) -> bool:
with self._lock:
is_sponsor = any(role.role_type.upper() == RoleType.SPONSOR.name and role.role_tier.upper() != SponsorTier.FREE.name
for role in self._roles)
return is_sponsor
def is_paired(self) -> bool:
with self._lock:
is_paired = any(user.user_id not in self.NOT_PAIRED_USERNAMES for user in self._users)
return is_paired
def is_connected(self) -> bool:
network_type = self._sm["deviceState"].networkType
return bool(network_type != 0)
def __del__(self):
self.stop()

View File

@@ -0,0 +1,86 @@
"""
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 json
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH
def test_get_params_all_keys_v1():
"""
Test the getParamsAllKeysV1 API endpoint.
Why:
This endpoint is used by the UI (and potentially external tools) to fetch the list of
available parameters along with their metadata (titles, descriptions, options, constraints).
We need to ensure it returns the correct structure and that the metadata from
params_metadata.json is correctly merged into the response.
Expected:
- The response should contain a "keys" field which is a JSON string of a list of parameters.
- Each parameter object should have "key", "type", "default_value", and optionally "_extra".
- The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching
the source of truth (params_metadata.json).
"""
response = getParamsAllKeysV1()
assert "keys" in response
keys_json = response["keys"]
params_list = json.loads(keys_json)
assert isinstance(params_list, list)
assert len(params_list) > 0
# Check structure of first item
first_param = params_list[0]
assert "key" in first_param
assert "type" in first_param
assert "default_value" in first_param
if "_extra" in first_param:
assert isinstance(first_param["_extra"], dict)
assert "default" not in first_param["_extra"]
assert "type" not in first_param["_extra"]
# Load the source of truth
with open(METADATA_PATH) as f:
metadata = json.load(f)
# Verify that the API response matches the metadata file for a few sample keys
# This ensures the plumbing is working without being brittle to content changes
# 1. Check a key that should have metadata
keys_with_metadata = [k for k in params_list if k["key"] in metadata]
assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys"
for param in keys_with_metadata[:5]: # Check first 5 matches
key = param["key"]
expected_meta = metadata[key]
assert "_extra" in param, f"Parameter {key} should have _extra field"
actual_meta = param["_extra"]
# Verify all fields in JSON are present in the API response
for meta_key, meta_val in expected_meta.items():
assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}"
assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}"
# 2. Check that we are correctly serving options if they exist
params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})]
if params_with_options:
param = params_with_options[0]
key = param["key"]
assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list"
assert param["_extra"]["options"] == metadata[key]["options"]
# 3. Check that we are correctly serving numeric constraints if they exist
params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})]
if params_with_constraints:
param = params_with_constraints[0]
key = param["key"]
assert param["_extra"]["min"] == metadata[key]["min"]
assert param["_extra"]["max"] == metadata[key]["max"]
assert param["_extra"]["step"] == metadata[key]["step"]

View File

@@ -0,0 +1,202 @@
"""
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 json
import os
import pytest
from openpilot.common.params import Params
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH
def test_metadata_json_exists():
"""
Test that the params_metadata.json file exists at the expected path.
Why:
The metadata file is the source of truth for parameter descriptions, options, and constraints.
If it's missing, the UI will not be able to display rich information for parameters.
Expected:
The file should exist at sunnypilot/sunnylink/params_metadata.json.
"""
assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}"
def test_metadata_json_valid():
"""
Test that the params_metadata.json file contains valid JSON.
Why:
Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or
resulting in missing metadata.
Expected:
The file content should be parseable as a JSON object (dictionary).
"""
with open(METADATA_PATH) as f:
try:
data = json.load(f)
except json.JSONDecodeError:
pytest.fail("Metadata file is not valid JSON")
assert isinstance(data, dict), "Metadata root must be a dictionary"
def test_all_params_have_metadata():
"""
Test that every parameter in the codebase has a corresponding entry in params_metadata.json.
Why:
We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase
should also be documented in the metadata file.
Expected:
There should be no parameters in Params() that are missing from the metadata file.
If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'.
"""
params = Params()
all_keys = [k.decode('utf-8') for k in params.all_keys()]
with open(METADATA_PATH) as f:
metadata = json.load(f)
missing_keys = [key for key in all_keys if key not in metadata]
if missing_keys:
pytest.fail(
f"The following parameters are missing from metadata: {missing_keys}. "
+ "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update."
)
def test_metadata_keys_exist_in_params():
"""
Test that all keys in params_metadata.json actually exist in the codebase.
Why:
We want to avoid stale metadata for parameters that have been removed or renamed.
This keeps the metadata file clean and relevant.
Expected:
There should be no keys in the metadata file that are not present in Params().
This prints a warning rather than failing, as it's less critical than missing metadata.
"""
params = Params()
all_keys = {k.decode('utf-8') for k in params.all_keys()}
with open(METADATA_PATH) as f:
metadata = json.load(f)
extra_keys = [key for key in metadata.keys() if key not in all_keys]
if extra_keys:
print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}")
def test_no_default_titles():
"""
Test that no parameter has a title that is identical to its key.
Why:
The default behavior of the update script is to set the title equal to the key.
We want to force developers to provide human-readable, descriptive titles for all parameters.
Expected:
No parameter metadata should have 'title' == 'key'.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key]
if default_title_keys:
pytest.fail(
f"The following parameters have default titles (title == key): {default_title_keys}. "
+ "Please update 'params_metadata.json' with descriptive titles."
)
def test_options_structure():
"""
Test that the 'options' field in metadata follows the correct structure.
Why:
The UI expects 'options' to be a list of objects with 'value' and 'label' keys.
Incorrect structure will break the UI rendering for dropdowns/toggles.
Expected:
If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
for key, meta in metadata.items():
if "options" in meta:
options = meta["options"]
assert isinstance(options, list), f"Options for {key} must be a list"
for option in options:
assert isinstance(option, dict), f"Option in {key} must be a dictionary"
assert "value" in option, f"Option in {key} must have a 'value' key"
assert "label" in option, f"Option in {key} must have a 'label' key"
def test_numeric_constraints():
"""
Test that numeric parameters have valid 'min', 'max', and 'step' constraints.
Why:
The UI uses these constraints to validate user input and render sliders/steppers.
Missing or invalid constraints can lead to UI bugs or invalid parameter values.
Expected:
If any of min/max/step is present, ALL of them must be present.
They must be numbers (int/float), and min must be less than max.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
for key, meta in metadata.items():
if "min" in meta or "max" in meta or "step" in meta:
assert "min" in meta, f"Numeric param {key} must have 'min'"
assert "max" in meta, f"Numeric param {key} must have 'max'"
assert "step" in meta, f"Numeric param {key} must have 'step'"
assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number"
assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number"
assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number"
assert meta["min"] < meta["max"], f"Min must be less than max for {key}"
def test_known_params_metadata():
"""
Test specific known parameters to ensure they have the expected rich metadata.
Why:
This acts as a spot check to ensure that our rich metadata population logic is working correctly
and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved.
Expected:
'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed).
'CustomAccLongPressIncrement' should have min=1, max=10, step=1.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
# Check an enum-like param
lp = metadata.get("LongitudinalPersonality")
assert lp is not None
assert "options" in lp
assert len(lp["options"]) == 3
assert lp["options"][0]["label"] == "Aggressive"
assert lp["options"][0]["value"] == 0
# Check a numeric param
acc_long = metadata.get("CustomAccLongPressIncrement")
assert acc_long is not None
assert acc_long["min"] == 1
assert acc_long["max"] == 10
assert acc_long["step"] == 1

View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import os
from openpilot.common.params import Params
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
def main():
params = Params()
all_keys = params.all_keys()
if os.path.exists(METADATA_PATH):
with open(METADATA_PATH) as f:
try:
data = json.load(f)
except json.JSONDecodeError:
data = {}
else:
data = {}
# Add new keys
for key in all_keys:
key_str = key.decode("utf-8")
if key_str not in data:
print(f"Adding new key: {key_str}")
data[key_str] = {
"title": key_str,
"description": "",
}
# Remove deleted keys
# keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys]
# for k in keys_to_remove:
# print(f"Removing deleted key: {k}")
# del data[k]
# Sort keys
sorted_data = dict(sorted(data.items()))
with open(METADATA_PATH, "w") as f:
json.dump(sorted_data, f, indent=2)
f.write("\n")
print(f"Updated {METADATA_PATH}")
if __name__ == "__main__":
main()

View File

@@ -94,6 +94,7 @@ class FontWeight(StrEnum):
BOLD = "Inter-Bold.fnt"
SEMI_BOLD = "Inter-SemiBold.fnt"
UNIFONT = "unifont.fnt"
AUDIOWIDE = "Audiowide-Regular.ttf"
# Small UI fonts
DISPLAY_REGULAR = "Inter-Regular.fnt"
@@ -227,6 +228,8 @@ class GuiApplication(GuiApplicationExt):
self._render_profiler = None
self._render_profile_start_time = None
GuiApplicationExt.__init__(self)
@property
def frame(self):
return self._frame
@@ -467,6 +470,9 @@ class GuiApplication(GuiApplicationExt):
if self._show_touches:
self._draw_touch_points()
if self._show_mouse_coords:
self._draw_mouse_coordinates(gui_app.font(FontWeight.SEMI_BOLD))
if self._grid_size > 0:
self._draw_grid()

View File

@@ -383,7 +383,7 @@ class WifiManager:
'connection': {
'type': ('s', '802-11-wireless'),
'uuid': ('s', str(uuid.uuid4())),
'id': ('s', f'openpilot connection {ssid}'),
'id': ('s', f'sunnypilot connection {ssid}'),
'autoconnect-retries': ('i', 0),
},
'802-11-wireless': {

View File

@@ -701,7 +701,7 @@ class Setup(Widget):
except urllib.error.HTTPError as e:
if e.code == 409:
error_msg = "Incompatible openpilot version"
error_msg = "Incompatible sunnypilot version"
self.download_failed(self.download_url, error_msg)
except Exception:
error_msg = "Invalid URL"

View File

@@ -6,10 +6,35 @@ See the LICENSE.md file in the root directory for more details.
"""
import os
import pyray as rl
SHOW_MOUSE_COORDS = os.getenv("SHOW_MOUSE_COORDS") == "1"
SUNNYPILOT_UI = os.getenv("SUNNYPILOT_UI", "1") == "1"
class GuiApplicationExt:
def __init__(self):
self._show_mouse_coords = SHOW_MOUSE_COORDS
@staticmethod
def sunnypilot_ui() -> bool:
return SUNNYPILOT_UI
def _draw_mouse_coordinates(self, font):
coords_text = f"X:{int(rl.get_mouse_x())}, Y:{int(rl.get_mouse_y())}"
green_color = rl.Color(0, 159, 47, 255) # Match the green color of FPS counter
# Calculate text width to position it at the right edge; estimate width based on text length
# Each character is approximately 10-12 pixels wide at font size 20
estimated_text_width = len(coords_text) * 11
# Position text at the top right corner, 10px from the top
screen_width = self._scaled_width if self._scale != 1.0 else self._width
text_pos = rl.Vector2(screen_width - estimated_text_width - 10, 6)
# Draw the text
rl.draw_text_ex(font, coords_text, text_pos, 20, 0, green_color)
def set_show_mouse_coords(self, show: bool):
self._show_mouse_coords = show

View File

@@ -24,6 +24,10 @@ class Base:
TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75)
TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20
# Button Control
BUTTON_WIDTH = 300
BUTTON_HEIGHT = 120
@dataclass
class DefaultStyleSP(Base):
@@ -47,5 +51,29 @@ class DefaultStyleSP(Base):
TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR
TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey
# Multi Button Control
MBC_TRANSPARENT = rl.Color(255, 255, 255, 0)
MBC_BG_CHECKED_ENABLED = rl.Color(0x69, 0x68, 0x68, 0xFF)
MBC_DISABLED = rl.Color(0xFF, 0xFF, 0xFF, 0x33)
# Option Control
OPTION_CONTROL_CONTAINER_BG = OFF_BG_COLOR
OPTION_CONTROL_BTN_ENABLED = rl.Color(88, 88, 88, 255)
OPTION_CONTROL_BTN_PRESSED = rl.Color(0x69, 0x68, 0x68, 0xFF)
OPTION_CONTROL_BTN_DISABLED = DISABLED_OFF_BG_COLOR
OPTION_CONTROL_TEXT_ENABLED = rl.WHITE
OPTION_CONTROL_TEXT_PRESSED = rl.WHITE
OPTION_CONTROL_TEXT_DISABLED = ITEM_DISABLED_TEXT_COLOR
# Tree Button Colors
BUTTON_PRIMARY_COLOR = rl.Color(70, 91, 234, 255) # Royal Blue
BUTTON_NEUTRAL_GRAY = rl.Color(51, 51, 51, 255)
BUTTON_DISABLED_BG_COLOR = rl.Color(30, 30, 30, 255) # Very Dark Grey
# Vehicle Description Colors
GREEN = rl.Color(0, 241, 0, 255)
BLUE = rl.Color(0, 134, 233, 255)
YELLOW = rl.Color(255, 213, 0, 255)
style = DefaultStyleSP

View File

@@ -1,3 +1,9 @@
"""
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 re
import unicodedata

View File

@@ -0,0 +1,26 @@
"""
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 math
import pyray as rl
def draw_star(center_x, center_y, radius, is_filled, color):
center = rl.Vector2(center_x, center_y)
points = []
for i in range(10):
angle = -(i * 36 + 18) * math.pi / 180
r = radius if i % 2 == 0 else radius / 2
x = center_x + r * math.cos(angle)
y = center_y + r * math.sin(angle)
points.append(rl.Vector2(x, y))
for i in range(10):
if is_filled:
rl.draw_triangle(center, points[i], points[(i + 1) % 10], color)
rl.draw_line_ex(points[i], points[(i + 1) % 10], 2, color)

View File

@@ -1,8 +1,13 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.keyboard import Keyboard
@@ -21,14 +26,16 @@ class InputDialogSP:
def show(self):
self.keyboard.reset(min_text_size=self.keyboard._min_text_size)
self.keyboard.set_title(tr(self.title), *(tr(self.sub_title),) if self.sub_title else ())
if self.sub_title:
self.keyboard.set_title(self.title, self.sub_title)
else:
self.keyboard.set_title(self.title)
self.keyboard.set_text(self.current_text)
def internal_callback(result: DialogResult):
text = self.keyboard.text if result == DialogResult.CONFIRM else ""
if result == DialogResult.CONFIRM:
if self.param:
self._params.put(self.param, text)
if result == DialogResult.CONFIRM and self.param:
self._params.put(self.param, text)
if self.callback:
self.callback(result, text)

View File

@@ -7,10 +7,13 @@ See the LICENSE.md file in the root directory for more details.
from collections.abc import Callable
import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction
from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction, MultipleButtonAction, _resolve_value
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP, LABEL_WIDTH
class ToggleActionSP(ToggleAction):
@@ -20,11 +23,80 @@ class ToggleActionSP(ToggleAction):
self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param)
class MultipleButtonActionSP(MultipleButtonAction):
def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None,
param: str | None = None):
MultipleButtonAction.__init__(self, buttons, button_width, selected_index, callback)
self.param_key = param
self.params = Params()
if self.param_key:
self.selected_button = int(self.params.get(self.param_key, return_default=True))
self._anim_x: float | None = None
def _render(self, rect: rl.Rectangle):
button_y = rect.y + (rect.height - style.BUTTON_HEIGHT) / 2
total_width = len(self.buttons) * self.button_width
track_rect = rl.Rectangle(rect.x, button_y, total_width, style.BUTTON_HEIGHT)
bg_color = style.MBC_TRANSPARENT
text_color = style.ITEM_TEXT_COLOR if self.enabled else style.MBC_DISABLED
highlight_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
# background
rl.draw_rectangle_rounded(track_rect, 0.2, 20, bg_color)
# border
border_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED
rl.draw_rectangle_rounded_lines_ex(track_rect, 0.2, 20, 2, border_color)
# highlight with animation
target_x = rect.x + self.selected_button * self.button_width
if not self._anim_x:
self._anim_x = target_x
self._anim_x += (target_x - self._anim_x) * 0.2
highlight_rect = rl.Rectangle(self._anim_x, button_y, self.button_width, style.BUTTON_HEIGHT)
rl.draw_rectangle_rounded(highlight_rect, 0.2, 20, highlight_color)
# text
for i, _text in enumerate(self.buttons):
button_x = rect.x + i * self.button_width
text = _resolve_value(_text, "")
text_size = measure_text_cached(self._font, text, 40)
text_x = button_x + (self.button_width - text_size.x) / 2
text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color)
def _handle_mouse_release(self, mouse_pos: MousePos):
MultipleButtonAction._handle_mouse_release(self, mouse_pos)
if self.param_key:
self.params.put(self.param_key, self.selected_button)
class ListItemSP(ListItem):
def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
description_visible: bool = False, callback: Callable | None = None,
action_item: ItemAction | None = None):
action_item: ItemAction | None = None, inline: bool = True, title_color: rl.Color = style.ITEM_TEXT_COLOR):
ListItem.__init__(self, title, icon, description, description_visible, callback, action_item)
self.title_color = title_color
self.inline = inline
if not self.inline:
self._rect.height += style.ITEM_BASE_HEIGHT/1.75
def get_item_height(self, font: rl.Font, max_width: int) -> float:
height = super().get_item_height(font, max_width)
if self.description_visible:
height += style.ITEM_PADDING * 1.5
if not self.inline:
height += style.ITEM_BASE_HEIGHT / 1.75
return height
def show_description(self, show: bool):
self._set_description_visible(show)
@@ -33,10 +105,19 @@ class ListItemSP(ListItem):
if not self.action_item:
return rl.Rectangle(0, 0, 0, 0)
if not self.inline:
has_description = bool(self.description) and self.description_visible
if has_description:
action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3
else:
action_y = item_rect.y + item_rect.height - style.BUTTON_HEIGHT - style.ITEM_PADDING * 1.5
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT)
right_width = self.action_item.rect.width
if right_width == 0: # Full width action (like DualButtonAction)
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y,
item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
if right_width == 0:
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y, item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
action_width = self.action_item.rect.width
if isinstance(self.action_item, ToggleAction):
@@ -47,6 +128,13 @@ class ListItemSP(ListItem):
return rl.Rectangle(action_x, action_y, action_width, style.ITEM_BASE_HEIGHT)
def _render(self, _):
if not self.is_visible:
return
# Don't draw items that are not in parent's viewport
if (self._rect.y + self.rect.height) <= self._parent_rect.y or self._rect.y >= (self._parent_rect.y + self._parent_rect.height):
return
content_x = self._rect.x + style.ITEM_PADDING
text_x = content_x
left_action_item = isinstance(self.action_item, ToggleAction)
@@ -62,9 +150,9 @@ class ListItemSP(ListItem):
# Draw title
if self.title:
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
# Render toggle and handle callback
if self.action_item.render(left_rect) and self.action_item.enabled:
@@ -74,14 +162,13 @@ class ListItemSP(ListItem):
else:
if self.title:
# Draw main text
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, self.title_color)
# Draw right item if present
if self.action_item:
right_rect = self.get_right_item_rect(self._rect)
right_rect.y = self._rect.y
if self.action_item.render(right_rect) and self.action_item.enabled:
# Right item was clicked/activated
if self.callback:
@@ -91,12 +178,12 @@ class ListItemSP(ListItem):
if self.description_visible:
content_width = int(self._rect.width - style.ITEM_PADDING * 2)
description_height = self._html_renderer.get_total_height(content_width)
description_rect = rl.Rectangle(
self._rect.x + style.ITEM_PADDING,
self._rect.y + style.ITEM_DESC_V_OFFSET,
content_width,
description_height
)
desc_y = self._rect.y + style.ITEM_DESC_V_OFFSET
if not self.inline and self.action_item:
desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 0.5
description_rect = rl.Rectangle(self._rect.x + style.ITEM_PADDING, desc_y, content_width, description_height)
self._html_renderer.render(description_rect)
@@ -104,3 +191,23 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItemSP:
action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param)
return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback)
def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]],
selected_index: int = 0, button_width: int = style.BUTTON_WIDTH, callback: Callable = None,
icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP:
action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param)
return ListItemSP(title=title, description=description, icon=icon, action_item=action, inline=inline)
def option_item_sp(title: str | Callable[[], str], param: str,
min_value: int, max_value: int, description: str | Callable[[], str] | None = None,
value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None,
enabled: bool | Callable[[], bool] = True,
icon: str = "", label_width: int = LABEL_WIDTH, value_map: dict[int, int] | None = None,
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP:
action = OptionControlSP(
param, min_value, max_value, value_change_step,
enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback
)
return ListItemSP(title=title, description=description, action_item=action, icon=icon)

View File

@@ -0,0 +1,165 @@
import pyray as rl
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.widgets.list_view import ItemAction
# Dimensions and styling constants
BUTTON_WIDTH = 150
BUTTON_HEIGHT = 150
LABEL_WIDTH = 350
BUTTON_SPACING = 25
VALUE_FONT_SIZE = 50
BUTTON_FONT_SIZE = 60
CONTAINER_PADDING = 20
class OptionControlSP(ItemAction):
def __init__(self, param: str, min_value: int, max_value: int,
value_change_step: int = 1, enabled: bool | Callable[[], bool] = True,
on_value_changed: Callable[[int], None] | None = None,
value_map: dict[int, int] | None = None,
label_width: int = LABEL_WIDTH,
use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None):
super().__init__(enabled=enabled)
self.params = Params()
self.param_key = param
self.min_value = min_value
self.max_value = max_value
self.value_change_step = value_change_step
self._minus_enabled = enabled
self._plus_enabled = enabled
self.on_value_changed = on_value_changed
self.value_map = value_map
self.label_width = label_width
self.use_float_scaling = use_float_scaling
self.current_value = min_value
self.label_callback = label_callback
if self.value_map:
for key in self.value_map:
if self.value_map[key] == self.params.get(self.param_key, return_default=True):
self.current_value = int(key)
break
else:
self.current_value = int(self.params.get(self.param_key, return_default=True))
# Initialize font and button styles
self._font = gui_app.font(FontWeight.MEDIUM)
# Layout rectangles for components
self.minus_btn_rect = rl.Rectangle(0, 0, 0, 0)
self.plus_btn_rect = rl.Rectangle(0, 0, 0, 0)
def get_value(self) -> int:
"""Get the current value of the control"""
return self.current_value
def set_value(self, value: int):
"""Set the control to a specific value"""
if self.min_value <= value <= self.max_value:
self.current_value = value
if self.value_map:
self.params.put(self.param_key, self.value_map[value])
else:
if self.use_float_scaling:
self.params.put(self.param_key, value / 100.0)
else:
self.params.put(self.param_key, value)
if self.on_value_changed:
self.on_value_changed(value)
def get_displayed_value(self) -> str:
"""Get the displayed value, handling value mapping if present"""
value = self.current_value
if callable(self.label_callback):
if self.value_map:
return self.label_callback(self.value_map[value])
else:
return self.label_callback(value)
if self.value_map:
# Use the value map to get the display string
if value in self.value_map:
return str(self.value_map[value]) # Return the display string
# If using float scaling, format as float
if self.use_float_scaling:
return f"{value / 100.0:.2f}"
return str(value)
def _render(self, rect: rl.Rectangle):
if self._rect.width == 0 or self._rect.height == 0 or not self.is_visible:
return
control_width = (BUTTON_WIDTH * 2) + self.label_width + (BUTTON_SPACING * 2)
total_width = control_width + (CONTAINER_PADDING * 2)
self._rect.width = total_width
start_x = self._rect.x + self._rect.width - control_width - (CONTAINER_PADDING * 2)
component_y = rect.y + (rect.height - BUTTON_HEIGHT) / 2
self.container_rect = rl.Rectangle(start_x, component_y, total_width, BUTTON_HEIGHT)
# background
rl.draw_rectangle_rounded(self.container_rect, 0.2, 20, style.OPTION_CONTROL_CONTAINER_BG)
# minus button
self.minus_btn_rect = rl.Rectangle(self.container_rect.x, component_y, BUTTON_WIDTH + CONTAINER_PADDING,
BUTTON_HEIGHT)
# label
label_x = self.container_rect.x + CONTAINER_PADDING + BUTTON_WIDTH + BUTTON_SPACING
self.label_rect = rl.Rectangle(label_x, component_y, self.label_width, BUTTON_HEIGHT)
# plus button
plus_x = label_x + self.label_width + BUTTON_SPACING
self.plus_btn_rect = rl.Rectangle(plus_x, component_y, BUTTON_WIDTH + CONTAINER_PADDING, BUTTON_HEIGHT)
self._minus_enabled = self.enabled and self.current_value > self.min_value
self._plus_enabled = self.enabled and self.current_value < self.max_value
self._render_button(self.minus_btn_rect, "-", self._minus_enabled)
self._render_value_label()
self._render_button(self.plus_btn_rect, "+", self._plus_enabled)
def _render_button(self, rect: rl.Rectangle, text: str, enabled: bool):
mouse_pos = rl.get_mouse_position()
is_pressed = (rl.check_collision_point_rec(mouse_pos, rect) and
self._touch_valid() and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT))
text_color = style.ITEM_TEXT_COLOR if enabled else style.ITEM_DISABLED_TEXT_COLOR
# highlight
if enabled and is_pressed:
rl.draw_rectangle_rounded(rect, 0.2, 20, style.OPTION_CONTROL_BTN_PRESSED)
# button text
text_size = measure_text_cached(self._font, text, BUTTON_FONT_SIZE)
text_x = rect.x + (rect.width - text_size.x) / 2
text_y = rect.y + (rect.height - text_size.y) / 2
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), BUTTON_FONT_SIZE, 0, text_color)
def _render_value_label(self):
"""Render the current value label"""
text = self.get_displayed_value()
text_color = style.ITEM_TEXT_COLOR if self.enabled else style.ITEM_DISABLED_TEXT_COLOR
text_size = measure_text_cached(self._font, text, VALUE_FONT_SIZE)
text_x = self.label_rect.x + (self.label_rect.width - text_size.x) / 2
text_y = self.label_rect.y + (self.label_rect.height - text_size.y) / 2
rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), VALUE_FONT_SIZE, 0, text_color)
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect):
self.current_value -= self.value_change_step
self.current_value = max(self.min_value, self.current_value)
elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect):
self.current_value += self.value_change_step
self.current_value = min(self.max_value, self.current_value)
self.set_value(self.current_value)

View File

@@ -0,0 +1,57 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets.list_view import ListItem, ItemAction
class ProgressBarAction(ItemAction):
def __init__(self, width=600):
super().__init__(width=width)
self.progress = 0.0
self.text = ""
self.show_progress = False
self.text_color = rl.GRAY
self._font = gui_app.font(FontWeight.NORMAL)
def update(self, progress, text, show_progress=False, text_color=rl.GRAY):
self.progress = progress
self.text = text
self.show_progress = show_progress
self.text_color = text_color
def _render(self, rect: rl.Rectangle):
font_size = 40
text_size = measure_text_cached(self._font, self.text, font_size)
padding = 30
bar_width = text_size.x + 2 * padding
text_x = (bar_width - text_size.x) / 2
if self.show_progress and len(parts := self.text.split(' - ', 1)) == 2:
prefix = parts[0]
max_prefix_w = measure_text_cached(self._font, "100%", font_size).x
current_prefix_w = measure_text_cached(self._font, prefix, font_size).x
bar_width = (text_size.x - current_prefix_w + max_prefix_w) + 2 * padding
text_x = padding + (max_prefix_w - current_prefix_w)
bar_height = 60
bar_rect = rl.Rectangle(rect.x + rect.width - bar_width, rect.y + (rect.height - bar_height) / 2, bar_width, bar_height)
if self.show_progress:
inner_rect = rl.Rectangle(bar_rect.x + 4, bar_rect.y + 4, bar_rect.width - 8, bar_rect.height - 8)
if inner_rect.width > 0:
fill_width = max(0, min(inner_rect.width, inner_rect.width * (self.progress / 100.0)))
rl.draw_rectangle_rounded(rl.Rectangle(inner_rect.x, inner_rect.y, fill_width, inner_rect.height), 0.2, 10, rl.Color(30, 121, 232, 255))
rl.draw_text_ex(self._font, self.text, rl.Vector2(bar_rect.x + text_x, bar_rect.y + (bar_height - text_size.y) / 2), font_size, 0, self.text_color)
def progress_item(title):
action = ProgressBarAction()
return ListItem(title=title, action_item=action)

View File

@@ -1,58 +0,0 @@
import os
import pytest
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.sunnypilot.widgets.input_dialog import InputDialogSP
os.environ['SDL_VIDEODRIVER'] = 'dummy'
if not os.environ.get('CI'):
pytest.skip("Test in CI environment, or comment out this flag to test locally", allow_module_level=True)
class TestInputDialog:
def setup_method(self):
self.params = Params()
def test_input_dialog_int(self):
gui_app.init_window("test window")
dialog = InputDialogSP("title", current_text="current_text", param="MapTargetVelocities")
dialog.show()
dialog.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
assert self.params.get("MapTargetVelocities") == "current_text"
def test_before_input_dialog(self):
gui_app.init_window("test window")
current_apn = "gsmapn"
# This tests the pre InputDialogSP, where keyboard setup had to be done for every single dialog box you want to use.
self.keyboard = Keyboard()
self.keyboard.reset(min_text_size=0)
self.keyboard.set_title(("Enter APN"), ("networking"))
self.keyboard.set_text(current_apn)
def pre_input_dialog_callback(result):
if result == 1:
apn = self.keyboard.text.strip()
self.params.put("GsmApn", apn)
gui_app.set_modal_overlay(self.keyboard, pre_input_dialog_callback)
self.keyboard.set_text("new_apn")
self.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
pre_input_dialog_result = self.params.get("GsmApn")
assert pre_input_dialog_result == "new_apn"
dialog = InputDialogSP(title="Enter APN", sub_title="networking", current_text=current_apn, param="GsmApn")
dialog.show()
dialog.keyboard.set_text("new_apn")
dialog.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
input_dialog_result = self.params.get("GsmApn")
assert input_dialog_result == "new_apn"
assert pre_input_dialog_result == input_dialog_result

View File

@@ -0,0 +1,240 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from dataclasses import dataclass, field
import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import FontWeight, gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle, BUTTON_PRESSED_BACKGROUND_COLORS
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.sunnypilot.lib.styles import style
from openpilot.system.ui.sunnypilot.widgets.helpers.fuzzy_search import search_from_list
from openpilot.system.ui.sunnypilot.widgets.helpers.star_icon import draw_star
from openpilot.system.ui.sunnypilot.widgets.input_dialog import InputDialogSP
@dataclass
class TreeNode:
ref: str
data: dict = field(default_factory=dict)
@dataclass
class TreeFolder:
folder: str
nodes: list
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):
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
text_padding=20 + indent_level * 30, elide_right=True)
self.text = text
self.ref = ref
self.is_folder = is_folder
self.indent_level = indent_level
self.is_favorite = is_favorite
self.selected = False
self._favorite_callback = favorite_callback
self.text_padding = 20 + indent_level * 30
self.border_radius = 10
self.is_expanded = is_expanded
def _render(self, rect):
indent = 60 * self.indent_level
self._rect = rl.Rectangle(rect.x + indent, rect.y, rect.width - indent, rect.height)
if self.is_pressed:
color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style]
elif self.selected and self.ref != "search_bar":
color = style.BUTTON_PRIMARY_COLOR
else:
color = style.BUTTON_DISABLED_BG_COLOR
roundness = self.border_radius / (min(self._rect.width, self._rect.height) / 2)
rl.draw_rectangle_rounded(self._rect, roundness, 10, color)
text_offset = self.text_padding + 20 - 15 if self.is_expanded and not self.is_folder and self.indent_level > 0 else self.text_padding + 20
text_rect = rl.Rectangle(self._rect.x + text_offset, self._rect.y, self._rect.width - self.text_padding - 20 - 90, self._rect.height)
self._label.render(text_rect)
if not self.is_folder and self._favorite_callback:
draw_star(self._rect.x + self._rect.width - 90, self._rect.y + self._rect.height / 2, 40, self.is_favorite,
style.ON_BG_COLOR if self.is_favorite else rl.GRAY)
def _handle_mouse_release(self, mouse_pos):
star_rect = rl.Rectangle(self._rect.x + self._rect.width - 90 - 40, self._rect.y + self._rect.height / 2 - 40, 80, 80)
if not self.is_folder and self._favorite_callback and rl.check_collision_point_rec(mouse_pos, star_rect):
self._favorite_callback()
return True
return super()._handle_mouse_release(mouse_pos)
class TreeOptionDialog(MultiOptionDialog):
def __init__(self, title, folders, current_ref="", fav_param="", option_font_weight=FontWeight.MEDIUM, search_prompt=None,
get_folders_fn=None, on_exit=None, display_func=None, search_funcs=None, search_title=None, search_subtitle=None):
super().__init__(title, [], "", option_font_weight)
self.folders = folders
self.selection_ref = current_ref
self.fav_param = fav_param
self.expanded = set()
self.params = Params()
val = self.params.get(fav_param) if fav_param else None
self.favorites = set(val.split(';')) if val else set()
self.query = ""
self.search_prompt = search_prompt or tr("Search")
self.get_folders_fn = get_folders_fn
self.on_exit = on_exit
self.display_func = display_func or (lambda node: node.data.get('display_name', node.ref))
self.search_funcs = search_funcs or [lambda node: node.data.get('display_name', ''), lambda node: node.data.get('short_name', '')]
self._search_rect = None
self._search_width = 0.475
# Default title & overridable subtitle for InputDialogSP
self.search_title = search_title or tr("Enter search query")
self.search_subtitle = search_subtitle
self.search_dialog = None
self._build_visible_items()
def _on_search_confirm(self, result, text):
if result == DialogResult.CONFIRM:
self.query = text
self._build_visible_items()
gui_app.set_modal_overlay(self, callback=self.on_exit)
def _on_search_clicked(self):
self.search_dialog = InputDialogSP(
self.search_title,
self.search_subtitle,
current_text=self.query,
callback=self._on_search_confirm,
)
self.search_dialog.show()
def _toggle_folder(self, folder):
if folder.folder:
if folder.folder in self.expanded:
self.expanded.remove(folder.folder)
else:
self.expanded.add(folder.folder)
if folder == self.folders[-1] and folder.folder in self.expanded:
self.scroller.scroll_panel.set_offset(self.scroller.scroll_panel.offset - 200)
self._build_visible_items(reset_scroll=False)
def _select_node(self, node):
self.selection = self.display_func(node)
self.selection_ref = node.ref
def _toggle_favorite(self, node):
self.favorites.remove(node.ref) if node.ref in self.favorites else self.favorites.add(node.ref)
if self.fav_param:
self.params.put(self.fav_param, ';'.join(self.favorites))
if self.get_folders_fn:
self.folders = self.get_folders_fn(self.favorites)
self._build_visible_items(reset_scroll=False)
def _build_visible_items(self, reset_scroll=True):
self.visible_items = []
for folder in self.folders:
nodes = [node for node in folder.nodes if not self.query or search_from_list(self.query, [search_func(node) for search_func in self.search_funcs])]
if not nodes and self.query:
continue
expanded = folder.folder in self.expanded or not folder.folder or bool(self.query)
if folder.folder:
self.visible_items.append(TreeItemWidget(f"{'-' if expanded else '+'} {folder.folder}", "", True, 0,
lambda folder_ref=folder: self._toggle_folder(folder_ref)))
if expanded:
for node in nodes:
favorite_cb = (lambda node_ref=node: self._toggle_favorite(node_ref)) if self.fav_param and node.ref != "Default" else None
self.visible_items.append(TreeItemWidget(self.display_func(node), node.ref, False, 1 if folder.folder else 0,
lambda node_ref=node: self._select_node(node_ref),
favorite_cb, node.ref in self.favorites, is_expanded=expanded))
self.option_buttons = self.visible_items
self.options = [item.text for item in self.visible_items]
self.scroller._items = self.visible_items
if reset_scroll:
self.scroller.scroll_panel.set_offset(0)
def _render(self, rect):
dialog_content_rect = rl.Rectangle(rect.x + 50, rect.y + 50, rect.width - 100, rect.height - 100)
rl.draw_rectangle_rounded(dialog_content_rect, 0.02, 20, rl.BLACK)
# Title on the left
title_rect = rl.Rectangle(dialog_content_rect.x + 50, dialog_content_rect.y + 50, dialog_content_rect.width * 0.5, 70)
gui_label(title_rect, self.title, 70, font_weight=FontWeight.BOLD)
# Search bar on the top right
search_width = dialog_content_rect.width * self._search_width
search_height = 110
search_x = dialog_content_rect.x + dialog_content_rect.width - 50 - search_width
search_y = dialog_content_rect.y + 40 # align roughly with title
self._search_rect = rl.Rectangle(search_x, search_y, search_width, search_height)
# Draw search field
inset = 4
roundness = 0.3
input_rect = rl.Rectangle(self._search_rect.x + inset, self._search_rect.y + inset,
self._search_rect.width - inset * 2, self._search_rect.height - inset * 2)
# Transparent fill + border
rl.draw_rectangle_rounded(input_rect, roundness, 10, rl.Color(0, 0, 0, 0))
rl.draw_rectangle_rounded_lines_ex(input_rect, roundness, 10, 3, rl.Color(150, 150, 150, 200))
# Magnifying glass icon
icon_color = rl.Color(180, 180, 180, 240)
cx = input_rect.x + 60
cy = input_rect.y + input_rect.height / 2 - 5
radius = min(input_rect.height * 0.28, 26)
circle_thickness = 4
for i in range(circle_thickness):
rl.draw_circle_lines(int(cx), int(cy), radius - i, icon_color)
handle_thickness = 5
inner_x = cx + radius * 0.65
inner_y = cy + radius * 0.65
outer_x = cx + radius * 1.45
outer_y = cy + radius * 1.45
rl.draw_line_ex(rl.Vector2(inner_x, inner_y), rl.Vector2(outer_x, outer_y), handle_thickness, icon_color)
# User text (query), placed after the icon if present
if self.query:
text_start_x = outer_x + 45
text_rect = rl.Rectangle(text_start_x, input_rect.y, input_rect.x + input_rect.width - text_start_x - 10, input_rect.height)
gui_label(text_rect, self.query, 70, font_weight=FontWeight.MEDIUM)
options_top = self._search_rect.y + self._search_rect.height + 40
options_area_rect = rl.Rectangle(dialog_content_rect.x + 50, options_top, dialog_content_rect.width - 100,
dialog_content_rect.height - (options_top - dialog_content_rect.y) - 210)
for index, option_text in enumerate(self.options):
self.option_buttons[index].selected = (option_text == self.selection)
self.option_buttons[index].set_button_style(ButtonStyle.PRIMARY if option_text == self.selection else ButtonStyle.NORMAL)
self.option_buttons[index].set_rect(rl.Rectangle(0, 0, options_area_rect.width, 135))
self.scroller.render(options_area_rect)
button_width = (dialog_content_rect.width - 150) / 2
button_y_position = dialog_content_rect.y + dialog_content_rect.height - 160
cancel_rect = rl.Rectangle(dialog_content_rect.x + 50, button_y_position, button_width, 160)
self.cancel_button.render(cancel_rect)
select_rect = rl.Rectangle(dialog_content_rect.x + 100 + button_width, button_y_position, button_width, 160)
self.select_button.set_enabled(self.selection != self.current)
self.select_button.render(select_rect)
return self._result
def _handle_mouse_release(self, mouse_pos):
if self._search_rect and rl.check_collision_point_rec(mouse_pos, self._search_rect):
self._on_search_clicked()
return True
return super()._handle_mouse_release(mouse_pos)

View File

@@ -16,7 +16,9 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item
if gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP as ListItem
from openpilot.system.ui.sunnypilot.widgets.list_view import ToggleActionSP as ToggleAction
from openpilot.system.ui.sunnypilot.widgets.list_view import MultipleButtonActionSP as MultipleButtonAction
# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI
try:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,84 +0,0 @@
//=============================================================================
//
// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//=============================================================================
#ifndef __IDIAGLOG_HPP_
#define __IDIAGLOG_HPP_
#include <string>
#include "DiagLog/Options.hpp"
#include "DlSystem/String.hpp"
#include "DlSystem/ZdlExportDefine.hpp"
namespace zdl
{
namespace DiagLog
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/// @brief .
///
/// Interface for controlling logging for zdl components.
class ZDL_EXPORT IDiagLog
{
public:
/// @brief .
///
/// Sets the options after initialization occurs.
///
/// @param[in] loggingOptions The options to set up diagnostic logging.
///
/// @return False if the options could not be set. Ensure logging is not started.
virtual bool setOptions(const Options& loggingOptions) = 0;
/// @brief .
///
/// Gets the curent options for the diag logger.
///
/// @return Diag log options object.
virtual Options getOptions() = 0;
/// @brief .
///
/// Allows for setting the log mask once diag logging has started
///
/// @return True if the level was set successfully, false if a failure occurred.
virtual bool setDiagLogMask(const std::string& mask) = 0;
/// @brief .
///
/// Allows for setting the log mask once diag logging has started
///
/// @return True if the level was set successfully, false if a failure occurred.
virtual bool setDiagLogMask(const zdl::DlSystem::String& mask) = 0;
/// @brief .
///
/// Enables logging for zdl components.
///
/// Logging should be started prior to the instantiation of zdl components
/// to ensure all events are captured.
///
/// @return False if diagnostic logging could not be started.
virtual bool start(void) = 0;
/// @brief Disables logging for zdl components.
virtual bool stop(void) = 0;
virtual ~IDiagLog() {};
};
} // DiagLog namespace
} // zdl namespace
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
#endif

View File

@@ -1,79 +0,0 @@
//=============================================================================
//
// Copyright (c) 2015, 2020 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//=============================================================================
#ifndef __DIAGLOG_OPTIONS_HPP_
#define __DIAGLOG_OPTIONS_HPP_
#include <string>
#include "DlSystem/ZdlExportDefine.hpp"
namespace zdl
{
namespace DiagLog
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/// @brief .
///
/// Options for setting up diagnostic logging for zdl components.
class ZDL_EXPORT Options
{
public:
Options() :
DiagLogMask(""),
LogFileDirectory("diaglogs"),
LogFileName("DiagLog"),
LogFileRotateCount(20),
LogFileReplace(true)
{
// Solves the empty string problem with multiple std libs
DiagLogMask.reserve(1);
}
/// @brief .
///
/// Enables diag logging only on the specified area mask (DNN_RUNTIME=ON | OFF)
std::string DiagLogMask;
/// @brief .
///
/// The path to the directory where log files will be written.
/// The path may be relative or absolute. Relative paths are interpreted
/// from the current working directory.
/// Default value is "diaglogs"
std::string LogFileDirectory;
/// @brief .
///
//// The name used for log files. If this value is empty then BaseName will be
/// used as the default file name.
/// Default value is "DiagLog"
std::string LogFileName;
/// @brief .
///
/// The maximum number of log files to create. If set to 0 no log rotation
/// will be used and the log file name specified will be used each time, overwriting
/// any existing log file that may exist.
/// Default value is 20
uint32_t LogFileRotateCount;
/// @brief
///
/// If the log file already exists, control whether it will be replaced
/// (existing contents truncated), or appended.
/// Default value is true
bool LogFileReplace;
};
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
} // DiagLog namespace
} // zdl namespace
#endif

View File

@@ -1,191 +0,0 @@
//=============================================================================
//
// Copyright (c) 2015-2020 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//=============================================================================
#ifndef ZEROTH_IDNC_CONTAINER_HPP
#define ZEROTH_IDNC_CONTAINER_HPP
#include <memory>
#include <stdint.h>
#include <string>
#include <vector>
#include <set>
#include "DlSystem/ZdlExportDefine.hpp"
#include "DlSystem/String.hpp"
namespace zdl {
namespace DlContainer {
/** @addtogroup c_plus_plus_apis C++
@{ */
class IDlContainer;
class dlc_error;
/**
* The structure of a record in a DL container.
*/
struct ZDL_EXPORT DlcRecord
{
/// Name of the record.
std::string name;
/// Byte blob holding the data for the record.
std::vector<uint8_t> data;
DlcRecord();
DlcRecord( DlcRecord&& other )
: name(std::move(other.name))
, data(std::move(other.data))
{}
DlcRecord(const std::string& new_name)
: name(new_name)
, data()
{
if(name.empty())
{
name.reserve(1);
}
}
DlcRecord(const DlcRecord&) = delete;
};
// The maximum length of any record name.
extern const uint32_t RECORD_NAME_MAX_SIZE;
// The maximum size of the record payload (bytes).
extern const uint32_t RECORD_DATA_MAX_SIZE;
// The maximum number of records in an archive at one time.
extern const uint32_t ARCHIVE_MAX_RECORDS;
/**
* Represents a container for a neural network model which can
* be used to load the model into the SNPE runtime.
*/
class ZDL_EXPORT IDlContainer
{
public:
/**
* Initializes a container from a container archive file.
*
* @param[in] filename Container archive file path.
*
* @return A pointer to the initialized container
*/
static std::unique_ptr<IDlContainer>
open(const std::string &filename) noexcept;
/**
* Initializes a container from a container archive file.
*
* @param[in] filename Container archive file path.
*
* @return A pointer to the initialized container
*/
static std::unique_ptr<IDlContainer>
open(const zdl::DlSystem::String &filename) noexcept;
/**
* Initializes a container from a byte buffer.
*
* @param[in] buffer Byte buffer holding the contents of an archive
* file.
*
* @return A pointer to the initialized container
*/
static std::unique_ptr<IDlContainer>
open(const std::vector<uint8_t> &buffer) noexcept;
/**
* Initializes a container from a byte buffer.
*
* @param[in] buffer Byte buffer holding the contents of an archive
* file.
*
* @param[in] size Size of the byte buffer.
*
* @return A pointer to the initialized container
*/
static std::unique_ptr<IDlContainer>
open(const uint8_t* buffer, const size_t size) noexcept;
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
/**
* Get the record catalog for a container.
*
* @param[out] catalog Buffer that will hold the record names on
* return.
*/
virtual void getCatalog(std::set<std::string> &catalog) const = 0;
/**
* Get the record catalog for a container.
*
* @param[out] catalog Buffer that will hold the record names on
* return.
*/
virtual void getCatalog(std::set<zdl::DlSystem::String> &catalog) const = 0;
/**
* Get a record from a container by name.
*
* @param[in] name Name of the record to fetch.
* @param[out] record The passed in record will be populated with the
* record data on return. Note that the caller
* will own the data in the record and is
* responsible for freeing it if needed.
*/
virtual void getRecord(const std::string &name, DlcRecord &record) const = 0;
/**
* Get a record from a container by name.
*
* @param[in] name Name of the record to fetch.
* @param[out] record The passed in record will be populated with the
* record data on return. Note that the caller
* will own the data in the record and is
* responsible for freeing it if needed.
*/
virtual void getRecord(const zdl::DlSystem::String &name, DlcRecord &record) const = 0;
/**
* Save the container to an archive on disk. This function will save the
* container if the filename is different from the file that it was opened
* from, or if at least one record was modified since the container was
* opened.
*
* It will truncate any existing file at the target path.
*
* @param filename Container archive file path.
*
* @return indication of success/failure
*/
virtual bool save(const std::string &filename) = 0;
/**
* Save the container to an archive on disk. This function will save the
* container if the filename is different from the file that it was opened
* from, or if at least one record was modified since the container was
* opened.
*
* It will truncate any existing file at the target path.
*
* @param filename Container archive file path.
*
* @return indication of success/failure
*/
virtual bool save (const zdl::DlSystem::String &filename) = 0;
virtual ~IDlContainer() {}
};
} // ns DlContainer
} // ns zdl
#endif

View File

@@ -1,234 +0,0 @@
//==============================================================================
//
// Copyright (c) 2014-2021 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//==============================================================================
#ifndef _DL_ENUMS_HPP_
#define _DL_ENUMS_HPP_
#include "DlSystem/ZdlExportDefine.hpp"
namespace zdl {
namespace DlSystem
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* Enumeration of supported target runtimes.
*/
enum class Runtime_t
{
/// Run the processing on Snapdragon CPU.
/// Data: float 32bit
/// Math: float 32bit
CPU_FLOAT32 = 0,
/// Run the processing on the Adreno GPU.
/// Data: float 16bit
/// Math: float 32bit
GPU_FLOAT32_16_HYBRID = 1,
/// Run the processing on the Hexagon DSP.
/// Data: 8bit fixed point Tensorflow style format
/// Math: 8bit fixed point Tensorflow style format
DSP_FIXED8_TF = 2,
/// Run the processing on the Adreno GPU.
/// Data: float 16bit
/// Math: float 16bit
GPU_FLOAT16 = 3,
/// Run the processing on Snapdragon AIX+HVX.
/// Data: 8bit fixed point Tensorflow style format
/// Math: 8bit fixed point Tensorflow style format
AIP_FIXED8_TF = 5,
AIP_FIXED_TF = AIP_FIXED8_TF,
/// Default legacy enum to retain backward compatibility.
/// CPU = CPU_FLOAT32
CPU = CPU_FLOAT32,
/// Default legacy enum to retain backward compatibility.
/// GPU = GPU_FLOAT32_16_HYBRID
GPU = GPU_FLOAT32_16_HYBRID,
/// Default legacy enum to retain backward compatibility.
/// DSP = DSP_FIXED8_TF
DSP = DSP_FIXED8_TF,
/// Special value indicating the property is unset.
UNSET = -1
};
/**
* Enumeration of runtime available check options.
*/
enum class RuntimeCheckOption_t
{
/// Perform standard runtime available check
DEFAULT = 0,
/// Perform standard runtime available check
NORMAL_CHECK = 0,
/// Perform basic runtime available check, may be runtime specific
BASIC_CHECK = 1,
/// Perform unsignedPD runtime available check
UNSIGNEDPD_CHECK = 2,
};
/**
* Enumeration of various performance profiles that can be requested.
*/
enum class PerformanceProfile_t
{
/// Run in a standard mode.
/// This mode will be deprecated in the future and replaced with BALANCED.
DEFAULT = 0,
/// Run in a balanced mode.
BALANCED = 0,
/// Run in high performance mode
HIGH_PERFORMANCE = 1,
/// Run in a power sensitive mode, at the expense of performance.
POWER_SAVER = 2,
/// Use system settings. SNPE makes no calls to any performance related APIs.
SYSTEM_SETTINGS = 3,
/// Run in sustained high performance mode
SUSTAINED_HIGH_PERFORMANCE = 4,
/// Run in burst mode
BURST = 5,
/// Run in lower clock than POWER_SAVER, at the expense of performance.
LOW_POWER_SAVER = 6,
/// Run in higher clock and provides better performance than POWER_SAVER.
HIGH_POWER_SAVER = 7,
/// Run in lower balanced mode
LOW_BALANCED = 8,
};
/**
* Enumeration of various profilngLevels that can be requested.
*/
enum class ProfilingLevel_t
{
/// No profiling.
/// Collects no runtime stats in the DiagLog
OFF = 0,
/// Basic profiling
/// Collects some runtime stats in the DiagLog
BASIC = 1,
/// Detailed profiling
/// Collects more runtime stats in the DiagLog, including per-layer statistics
/// Performance may be impacted
DETAILED = 2,
/// Moderate profiling
/// Collects more runtime stats in the DiagLog, no per-layer statistics
MODERATE = 3
};
/**
* Enumeration of various execution priority hints.
*/
enum class ExecutionPriorityHint_t
{
/// Normal priority
NORMAL = 0,
/// Higher than normal priority
HIGH = 1,
/// Lower priority
LOW = 2
};
/** @} */ /* end_addtogroup c_plus_plus_apis C++*/
/**
* Enumeration that lists the supported image encoding formats.
*/
enum class ImageEncoding_t
{
/// For unknown image type. Also used as a default value for ImageEncoding_t.
UNKNOWN = 0,
/// The RGB format consists of 3 bytes per pixel: one byte for
/// Red, one for Green, and one for Blue. The byte ordering is
/// endian independent and is always in RGB byte order.
RGB = 1,
/// The ARGB32 format consists of 4 bytes per pixel: one byte for
/// Red, one for Green, one for Blue, and one for the alpha channel.
/// The alpha channel is ignored. The byte ordering depends on the
/// underlying CPU. For little endian CPUs, the byte order is BGRA.
/// For big endian CPUs, the byte order is ARGB.
ARGB32 = 2,
/// The RGBA format consists of 4 bytes per pixel: one byte for
/// Red, one for Green, one for Blue, and one for the alpha channel.
/// The alpha channel is ignored. The byte ordering is endian independent
/// and is always in RGBA byte order.
RGBA = 3,
/// The GRAYSCALE format is for 8-bit grayscale.
GRAYSCALE = 4,
/// NV21 is the Android version of YUV. The Chrominance is down
/// sampled and has a subsampling ratio of 4:2:0. Note that this
/// image format has 3 channels, but the U and V channels
/// are subsampled. For every four Y pixels there is one U and one V pixel. @newpage
NV21 = 5,
/// The BGR format consists of 3 bytes per pixel: one byte for
/// Red, one for Green and one for Blue. The byte ordering is
/// endian independent and is always BGR byte order.
BGR = 6
};
/**
* Enumeration that lists the supported LogLevels that can be set by users.
*/
enum class LogLevel_t
{
/// Enumeration variable to be used by user to set logging level to FATAL.
LOG_FATAL = 0,
/// Enumeration variable to be used by user to set logging level to ERROR.
LOG_ERROR = 1,
/// Enumeration variable to be used by user to set logging level to WARN.
LOG_WARN = 2,
/// Enumeration variable to be used by user to set logging level to INFO.
LOG_INFO = 3,
/// Enumeration variable to be used by user to set logging level to VERBOSE.
LOG_VERBOSE = 4
};
typedef enum : int
{
UNSPECIFIED = 0,
FLOATING_POINT_32 = 1,
FLOATING_POINT_16 = 2,
FIXED_POINT_8 = 3,
FIXED_POINT_16 = 4
} IOBufferDataType_t;
}} // namespaces end
#endif

View File

@@ -1,259 +0,0 @@
//==============================================================================
//
// Copyright (c) 2016-2021 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//==============================================================================
#ifndef _DL_ERROR_HPP_
#define _DL_ERROR_HPP_
#include <stdint.h>
#include <limits> // numeric_limits
#include "DlSystem/ZdlExportDefine.hpp"
namespace zdl {
namespace DlSystem {
// clang and arm gcc different in how ZDL_EXPORT is used with enum class
#if !defined (__clang__)
enum class ErrorCode : uint32_t ZDL_EXPORT {
#else
enum class ZDL_EXPORT ErrorCode : uint32_t {
#endif // ARM64V8A
NONE = 0,
// System config errors
SNPE_CONFIG_MISSING_PARAM = 100,
SNPE_CONFIG_INVALID_PARAM = 101,
SNPE_CONFIG_MISSING_FILE = 102,
SNPE_CONFIG_NNCONFIG_NOT_SET = 103,
SNPE_CONFIG_NNCONFIG_INVALID = 104,
SNPE_CONFIG_WRONG_INPUT_NAME = 105,
SNPE_CONFIG_INCORRECT_INPUT_DIMENSIONS = 106,
SNPE_CONFIG_DIMENSIONS_MODIFICATION_NOT_SUPPORTED = 107,
SNPE_CONFIG_BOTH_OUTPUT_LAYER_TENSOR_NAMES_SET = 108,
SNPE_CONFIG_NNCONFIG_ONLY_TENSOR_SUPPORTED = 120,
SNPE_CONFIG_NNCONFIG_ONLY_USER_BUFFER_SUPPORTED = 121,
// DlSystem errors
SNPE_DLSYSTEM_MISSING_BUFFER = 200,
SNPE_DLSYSTEM_TENSOR_CAST_FAILED = 201,
SNPE_DLSYSTEM_FIXED_POINT_PARAM_INVALID = 202,
SNPE_DLSYSTEM_SIZE_MISMATCH = 203,
SNPE_DLSYSTEM_NAME_NOT_FOUND = 204,
SNPE_DLSYSTEM_VALUE_MISMATCH = 205,
SNPE_DLSYSTEM_INSERT_FAILED = 206,
SNPE_DLSYSTEM_TENSOR_FILE_READ_FAILED = 207,
SNPE_DLSYSTEM_DIAGLOG_FAILURE = 208,
SNPE_DLSYSTEM_LAYER_NOT_SET = 209,
SNPE_DLSYSTEM_WRONG_NUMBER_INPUT_BUFFERS = 210,
SNPE_DLSYSTEM_RUNTIME_TENSOR_SHAPE_MISMATCH = 211,
SNPE_DLSYSTEM_TENSOR_MISSING = 212,
SNPE_DLSYSTEM_TENSOR_ITERATION_UNSUPPORTED = 213,
SNPE_DLSYSTEM_BUFFER_MANAGER_MISSING = 214,
SNPE_DLSYSTEM_RUNTIME_BUFFER_SOURCE_UNSUPPORTED = 215,
SNPE_DLSYSTEM_BUFFER_CAST_FAILED = 216,
SNPE_DLSYSTEM_WRONG_TRANSITION_TYPE = 217,
SNPE_DLSYSTEM_LAYER_ALREADY_REGISTERED = 218,
SNPE_DLSYSTEM_TENSOR_DIM_INVALID = 219,
SNPE_DLSYSTEM_BUFFERENCODING_UNKNOWN = 240,
SNPE_DLSYSTEM_BUFFER_INVALID_PARAM = 241,
// DlContainer errors
SNPE_DLCONTAINER_MODEL_PARSING_FAILED = 300,
SNPE_DLCONTAINER_UNKNOWN_LAYER_CODE = 301,
SNPE_DLCONTAINER_MISSING_LAYER_PARAM = 302,
SNPE_DLCONTAINER_LAYER_PARAM_NOT_SUPPORTED = 303,
SNPE_DLCONTAINER_LAYER_PARAM_INVALID = 304,
SNPE_DLCONTAINER_TENSOR_DATA_MISSING = 305,
SNPE_DLCONTAINER_MODEL_LOAD_FAILED = 306,
SNPE_DLCONTAINER_MISSING_RECORDS = 307,
SNPE_DLCONTAINER_INVALID_RECORD = 308,
SNPE_DLCONTAINER_WRITE_FAILURE = 309,
SNPE_DLCONTAINER_READ_FAILURE = 310,
SNPE_DLCONTAINER_BAD_CONTAINER = 311,
SNPE_DLCONTAINER_BAD_DNN_FORMAT_VERSION = 312,
SNPE_DLCONTAINER_UNKNOWN_AXIS_ANNOTATION = 313,
SNPE_DLCONTAINER_UNKNOWN_SHUFFLE_TYPE = 314,
SNPE_DLCONTAINER_TEMP_FILE_FAILURE = 315,
// Network errors
SNPE_NETWORK_EMPTY_NETWORK = 400,
SNPE_NETWORK_CREATION_FAILED = 401,
SNPE_NETWORK_PARTITION_FAILED = 402,
SNPE_NETWORK_NO_OUTPUT_DEFINED = 403,
SNPE_NETWORK_MISMATCH_BETWEEN_NAMES_AND_DIMS = 404,
SNPE_NETWORK_MISSING_INPUT_NAMES = 405,
SNPE_NETWORK_MISSING_OUTPUT_NAMES = 406,
SNPE_NETWORK_EXECUTION_FAILED = 407,
// Host runtime errors
SNPE_HOST_RUNTIME_TARGET_UNAVAILABLE = 500,
// CPU runtime errors
SNPE_CPU_LAYER_NOT_SUPPORTED = 600,
SNPE_CPU_LAYER_PARAM_NOT_SUPPORTED = 601,
SNPE_CPU_LAYER_PARAM_INVALID = 602,
SNPE_CPU_LAYER_PARAM_COMBINATION_INVALID = 603,
SNPE_CPU_BUFFER_NOT_FOUND = 604,
SNPE_CPU_NETWORK_NOT_SUPPORTED = 605,
SNPE_CPU_UDO_OPERATION_FAILED = 606,
// CPU fixed-point runtime errors
SNPE_CPU_FXP_LAYER_NOT_SUPPORTED = 700,
SNPE_CPU_FXP_LAYER_PARAM_NOT_SUPPORTED = 701,
SNPE_CPU_FXP_LAYER_PARAM_INVALID = 702,
// GPU runtime errors
SNPE_GPU_LAYER_NOT_SUPPORTED = 800,
SNPE_GPU_LAYER_PARAM_NOT_SUPPORTED = 801,
SNPE_GPU_LAYER_PARAM_INVALID = 802,
SNPE_GPU_LAYER_PARAM_COMBINATION_INVALID = 803,
SNPE_GPU_KERNEL_COMPILATION_FAILED = 804,
SNPE_GPU_CONTEXT_NOT_SET = 805,
SNPE_GPU_KERNEL_NOT_SET = 806,
SNPE_GPU_KERNEL_PARAM_INVALID = 807,
SNPE_GPU_OPENCL_CHECK_FAILED = 808,
SNPE_GPU_OPENCL_FUNCTION_ERROR = 809,
SNPE_GPU_BUFFER_NOT_FOUND = 810,
SNPE_GPU_TENSOR_DIM_INVALID = 811,
SNPE_GPU_MEMORY_FLAGS_INVALID = 812,
SNPE_GPU_UNEXPECTED_NUMBER_OF_IO = 813,
SNPE_GPU_LAYER_PROXY_ERROR = 814,
SNPE_GPU_BUFFER_IN_USE = 815,
SNPE_GPU_BUFFER_MODIFICATION_ERROR = 816,
SNPE_GPU_DATA_ARRANGEMENT_INVALID = 817,
SNPE_GPU_UDO_OPERATION_FAILED = 818,
// DSP runtime errors
SNPE_DSP_LAYER_NOT_SUPPORTED = 900,
SNPE_DSP_LAYER_PARAM_NOT_SUPPORTED = 901,
SNPE_DSP_LAYER_PARAM_INVALID = 902,
SNPE_DSP_LAYER_PARAM_COMBINATION_INVALID = 903,
SNPE_DSP_STUB_NOT_PRESENT = 904,
SNPE_DSP_LAYER_NAME_TRUNCATED = 905,
SNPE_DSP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 906,
SNPE_DSP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 907,
SNPE_DSP_RUNTIME_COMMUNICATION_ERROR = 908,
SNPE_DSP_RUNTIME_INVALID_PARAM_ERROR = 909,
SNPE_DSP_RUNTIME_SYSTEM_ERROR = 910,
SNPE_DSP_RUNTIME_CRASHED_ERROR = 911,
SNPE_DSP_BUFFER_SIZE_ERROR = 912,
SNPE_DSP_UDO_EXECUTE_ERROR = 913,
SNPE_DSP_UDO_LIB_NOT_REGISTERED_ERROR = 914,
SNPE_DSP_UDO_INVALID_QUANTIZATION_TYPE_ERROR = 915,
SNPE_DSP_RUNTIME_INVALID_RPC_DRIVER = 916,
SNPE_DSP_RUNTIME_RPC_PERMISSION_ERROR = 917,
SNPE_DSP_RUNTIME_DSP_FILE_OPEN_ERROR = 918,
// Model validataion errors
SNPE_MODEL_VALIDATION_LAYER_NOT_SUPPORTED = 1000,
SNPE_MODEL_VALIDATION_LAYER_PARAM_NOT_SUPPORTED = 1001,
SNPE_MODEL_VALIDATION_LAYER_PARAM_INVALID = 1002,
SNPE_MODEL_VALIDATION_LAYER_PARAM_MISSING = 1003,
SNPE_MODEL_VALIDATION_LAYER_PARAM_COMBINATION_INVALID = 1004,
SNPE_MODEL_VALIDATION_LAYER_ORDERING_INVALID = 1005,
SNPE_MODEL_VALIDATION_INVALID_CONSTRAINT = 1006,
SNPE_MODEL_VALIDATION_MISSING_BUFFER = 1007,
SNPE_MODEL_VALIDATION_BUFFER_REUSE_NOT_SUPPORTED = 1008,
SNPE_MODEL_VALIDATION_LAYER_COULD_NOT_BE_ASSIGNED = 1009,
SNPE_MODEL_VALIDATION_UDO_LAYER_FAILED = 1010,
// UDL errors
SNPE_UDL_LAYER_EMPTY_UDL_NETWORK = 1100,
SNPE_UDL_LAYER_PARAM_INVALID = 1101,
SNPE_UDL_LAYER_INSTANCE_MISSING = 1102,
SNPE_UDL_LAYER_SETUP_FAILED = 1103,
SNPE_UDL_EXECUTE_FAILED = 1104,
SNPE_UDL_BUNDLE_INVALID = 1105,
SNPE_UDO_REGISTRATION_FAILED = 1106,
SNPE_UDO_GET_PACKAGE_FAILED = 1107,
SNPE_UDO_GET_IMPLEMENTATION_FAILED = 1108,
// Dependent library errors
SNPE_STD_LIBRARY_ERROR = 1200,
// Unknown exception (catch (...)), Has no component attached to this
SNPE_UNKNOWN_EXCEPTION = 1210,
// Storage Errors
SNPE_STORAGE_INVALID_KERNEL_REPO = 1300,
// AIP runtime errors
SNPE_AIP_LAYER_NOT_SUPPORTED = 1400,
SNPE_AIP_LAYER_PARAM_NOT_SUPPORTED = 1401,
SNPE_AIP_LAYER_PARAM_INVALID = 1402,
SNPE_AIP_LAYER_PARAM_COMBINATION_INVALID = 1403,
SNPE_AIP_STUB_NOT_PRESENT = 1404,
SNPE_AIP_LAYER_NAME_TRUNCATED = 1405,
SNPE_AIP_LAYER_INPUT_BUFFER_NAME_TRUNCATED = 1406,
SNPE_AIP_LAYER_OUTPUT_BUFFER_NAME_TRUNCATED = 1407,
SNPE_AIP_RUNTIME_COMMUNICATION_ERROR = 1408,
SNPE_AIP_RUNTIME_INVALID_PARAM_ERROR = 1409,
SNPE_AIP_RUNTIME_SYSTEM_ERROR = 1410,
SNPE_AIP_RUNTIME_TENSOR_MISSING = 1411,
SNPE_AIP_RUNTIME_TENSOR_SHAPE_MISMATCH = 1412,
SNPE_AIP_RUNTIME_BAD_AIX_RECORD = 1413,
// DlCaching errors
SNPE_DLCACHING_INVALID_METADATA = 1500,
SNPE_DLCACHING_INVALID_INITBLOB = 1501,
// Infrastructure Errors
SNPE_INFRA_CLUSTERMGR_INSTANCE_INVALID = 1600,
SNPE_INFRA_CLUSTERMGR_EXECUTE_SYNC_FAILED = 1601,
// Memory Errors
SNPE_MEMORY_CORRUPTION_ERROR = 1700
};
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* Returns the error code of the last error encountered.
*
* @return The error code.
*
* @note The returned error code is significant only when the return
* value of the call indicated an error.
*/
ZDL_EXPORT ErrorCode getLastErrorCode();
/**
* Returns the error string of the last error encountered.
*
* @return The error string.
*
* @note The returned error string is significant only when the return
* value of the call indicated an error.
*/
ZDL_EXPORT const char* getLastErrorString();
/**
* Returns the info string of the last error encountered.
*/
ZDL_EXPORT const char* getLastInfoString();
/**
* Returns the uint32_t representation of the error code enum.
*
* @param[in] code The error code to be converted.
*
* @return uint32_t representation of the error code.
*/
ZDL_EXPORT uint32_t enumToUInt32(zdl::DlSystem::ErrorCode code);
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
} // DlSystem
} // zdl
#endif // _DL_ERROR_HPP_

View File

@@ -1,225 +0,0 @@
//==============================================================================
//
// Copyright (c) 2016, 2020 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//==============================================================================
#ifndef _DL_SYSTEM_OPTIONAL_HPP_
#define _DL_SYSTEM_OPTIONAL_HPP_
#include <cstdio>
#include <utility>
#include <stdexcept>
#include "DlSystem/ZdlExportDefine.hpp"
namespace zdl {
namespace DlSystem {
template <typename T>
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* @brief .
*
* Class to manage a value that may or may not exist. The boolean value
* of the Optional class is true if the object contains a value and false
* if it does not contain a value.
*
* The class must be evaluated and confirmed as true (containing a value)
* before being dereferenced.
*/
class ZDL_EXPORT Optional final {
public:
enum class LIFECYCLE {
NONE = 0,
REFERENCE_OWNED = 1,
POINTER_OWNED = 2,
POINTER_NOT_OWNED = 3
};
struct ReferenceCount {
size_t count = 0;
void increment() { count++; }
size_t decrement() {
if (count > 0) {
count--;
}
return count;
}
};
using U = typename std::remove_pointer<T>::type;
/**
* The default constructor is set to not have any value, and is
* therefore evaluated as false.
*/
// Do not explicit it so we can return {}
Optional() {
m_Type = LIFECYCLE::NONE;
}
/**
* Construct an Optional class using an object.
* @param[in] Reference to an object v
* @param[out] Optional instance of object v
*/
template <typename Q = T>
Optional (const T& v, typename std::enable_if<!std::is_pointer<Q>::value>::type* = 0)
: m_Type(LIFECYCLE::REFERENCE_OWNED) {
try {
m_StoragePtr = new T(v);
} catch (...) {
m_StoragePtr = nullptr;
m_Type = LIFECYCLE::NONE;
}
}
template <typename Q = T>
Optional(U* v, LIFECYCLE type, typename std::enable_if<std::is_pointer<Q>::value>::type* = 0)
: m_Type(type) {
switch (m_Type) {
case LIFECYCLE::POINTER_OWNED:
m_StoragePtr = v;
m_Count = new ReferenceCount();
m_Count->increment();
break;
case LIFECYCLE::POINTER_NOT_OWNED:
m_StoragePtr = v;
break;
case LIFECYCLE::REFERENCE_OWNED:
throw std::bad_exception();
case LIFECYCLE::NONE:
break;
}
}
Optional(const Optional &other) : m_Type(other.m_Type), m_Count(other.m_Count) {
if (isReference()) {
m_StoragePtr = new U(*other.m_StoragePtr);
} else if (isPointer()) {
m_StoragePtr = other.m_StoragePtr;
if (isOwned()) {
m_Count->increment();
}
}
}
Optional& operator=(const Optional& other) noexcept {
Optional tmp(other);
swap(std::move(tmp));
return *this;
}
Optional(Optional&& other) noexcept {
swap(std::move(other));
}
Optional& operator=(Optional&& other) noexcept {
swap(std::move(other));
return *this;
}
~Optional() {
if (isOwned()) {
if (isReference() || (isPointer() && m_Count->decrement() == 0)) {
delete m_StoragePtr;
delete m_Count;
}
}
}
/**
* Boolean value of Optional class is only true when there exists a value.
*/
operator bool() const noexcept { return isValid(); }
bool operator!() const noexcept { return !isValid(); }
/**
* Get reference of Optional object
* @warning User must validate Optional has value before.
*/
const T& operator*() { return this->GetReference(); }
/**
* Get reference of Optional object
* @warning User must validate Optional has value before.
*/
const T& operator*() const { return this->GetReference(); }
operator T&() { return this->GetReference(); }
T operator->() {
T self = this->GetReference();
return self;
}
private:
void swap(Optional&& other) {
m_Type = other.m_Type;
m_StoragePtr = other.m_StoragePtr;
m_Count = other.m_Count;
other.m_Type = LIFECYCLE::NONE;
other.m_StoragePtr = nullptr;
other.m_Count = nullptr;
}
template <typename Q = T>
typename std::enable_if<std::is_same<U, Q>::value, const Q&>::type GetReference() const noexcept {
if (!isReference()) std::terminate();
return *static_cast<const Q*>(m_StoragePtr);
}
template <typename Q = T>
typename std::enable_if<std::is_same<U*, Q>::value, const Q&>::type GetReference() const noexcept {
if (!isPointer()) std::terminate();
return static_cast<const Q&>(m_StoragePtr);
}
template <typename Q = T>
typename std::enable_if<std::is_same<U, Q>::value, Q&>::type GetReference() noexcept {
if (!isReference()) std::terminate();
return *m_StoragePtr;
}
template <typename Q = T>
typename std::enable_if<std::is_same<U*, Q>::value, Q&>::type GetReference() noexcept {
if (!isPointer()) std::terminate();
return m_StoragePtr;
}
bool isPointer() const {
return m_Type == LIFECYCLE::POINTER_OWNED || m_Type == LIFECYCLE::POINTER_NOT_OWNED;
}
bool isOwned() const {
return m_Type == LIFECYCLE::REFERENCE_OWNED || m_Type == LIFECYCLE::POINTER_OWNED;
}
bool isReference() const {
return m_Type == LIFECYCLE::REFERENCE_OWNED;
}
bool isValid() const {
return m_Type != LIFECYCLE::NONE;
}
U* m_StoragePtr = nullptr;
LIFECYCLE m_Type;
ReferenceCount *m_Count = nullptr;
};
} // ns DlSystem
} // ns zdl
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
#endif // _DL_SYSTEM_OPTIONAL_HPP_

View File

@@ -1,78 +0,0 @@
//==============================================================================
//
// Copyright (c) 2014-2015 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//==============================================================================
#ifndef _DL_VERSION_HPP_
#define _DL_VERSION_HPP_
#include "ZdlExportDefine.hpp"
#include <stdint.h>
#include <string>
#include "DlSystem/String.hpp"
namespace zdl {
namespace DlSystem
{
class Version_t;
}}
namespace zdl { namespace DlSystem
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* A class that contains the different portions of a version number.
*/
class ZDL_EXPORT Version_t
{
public:
/// Holds the major version number. Changes in this value indicate
/// major changes that break backward compatibility.
int32_t Major;
/// Holds the minor version number. Changes in this value indicate
/// minor changes made to library that are backwards compatible
/// (such as additions to the interface).
int32_t Minor;
/// Holds the teeny version number. Changes in this value indicate
/// changes such as bug fixes and patches made to the library that
/// do not affect the interface.
int32_t Teeny;
/// This string holds information about the build version.
///
std::string Build;
static zdl::DlSystem::Version_t fromString(const std::string &stringValue);
static zdl::DlSystem::Version_t fromString(const zdl::DlSystem::String &stringValue);
/**
* @brief Returns a string in the form Major.Minor.Teeny.Build
*
* @return A formatted string holding the version information.
*/
const std::string toString() const;
/**
* @brief Returns a string in the form Major.Minor.Teeny.Build
*
* @return A formatted string holding the version information.
*/
const zdl::DlSystem::String asString() const;
};
}}
/** @} */ /* end_addtogroup c_plus_plus_apis */
#endif

View File

@@ -1,86 +0,0 @@
//==============================================================================
//
// Copyright (c) 2017-2019 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//==============================================================================
#ifndef _IBUFFER_ATTRIBUTES_HPP
#define _IBUFFER_ATTRIBUTES_HPP
#include "IUserBuffer.hpp"
#include "TensorShape.hpp"
#include "ZdlExportDefine.hpp"
namespace zdl {
namespace DlSystem {
class UserBufferEncoding;
}
}
namespace zdl {
namespace DlSystem {
/**
* @brief IBufferAttributes returns a buffer's dimension and alignment
* requirements, along with info on its encoding type
*/
class ZDL_EXPORT IBufferAttributes {
public:
/**
* @brief Gets the buffer's element size, in bytes
*
* This can be used to compute the memory size required
* to back this buffer.
*
* @return Element size, in bytes
*/
virtual size_t getElementSize() const noexcept = 0;
/**
* @brief Gets the element's encoding type
*
* @return encoding type
*/
virtual zdl::DlSystem::UserBufferEncoding::ElementType_t getEncodingType() const noexcept = 0;
/**
* @brief Gets the number of elements in each dimension
*
* @return Dimension size, in terms of number of elements
*/
virtual const TensorShape getDims() const noexcept = 0;
/**
* @brief Gets the alignment requirement of each dimension
*
* Alignment per each dimension is expressed as an multiple, for
* example, if one particular dimension can accept multiples of 8,
* the alignment will be 8.
*
* @return Alignment in each dimension, in terms of multiple of
* number of elements
*/
virtual const TensorShape getAlignments() const noexcept = 0;
/**
* @brief Gets the buffer encoding returned from the network responsible
* for generating this buffer. Depending on the encoding type, this will
* be an instance of an encoding type specific derived class.
*
* @return Derived user buffer encoding object.
*/
virtual zdl::DlSystem::UserBufferEncoding* getEncoding() const noexcept = 0;
virtual ~IBufferAttributes() {}
};
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
}
}
#endif

View File

@@ -1,127 +0,0 @@
//=============================================================================
//
// Copyright (c) 2021-2022 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//=============================================================================
#ifndef DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP
#define DL_SYSTEM_IOBUFFER_DATATYPE_MAP_HPP
#include <cstddef>
#include <memory>
#include "DlSystem/DlEnums.hpp"
namespace DlSystem
{
// Forward declaration of IOBufferDataTypeMapImpl implementation.
class IOBufferDataTypeMapImpl;
}
namespace zdl
{
namespace DlSystem
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* @brief .
*
* The IoBufferDataTypeMap class definition
*/
class ZDL_EXPORT IOBufferDataTypeMap final
{
public:
/**
* @brief .
*
* Creates a new Buffer Data type map
*
*/
IOBufferDataTypeMap();
/**
* @brief Adds a name and the corresponding buffer data type
* to the map
*
* @param[name] name The name of the buffer
* @param[bufferDataType] buffer Data Type of the buffer
*
* @note If a buffer with the same name already exists, no new
* buffer is added.
*/
void add(const char* name, zdl::DlSystem::IOBufferDataType_t bufferDataType);
/**
* @brief Removes a buffer name from the map
*
* @param[name] name The name of the buffer
*
*/
void remove(const char* name);
/**
* @brief Returns the type of the named buffer
*
* @param[name] name The name of the buffer
*
* @return The type of the buffer, or UNSPECIFIED if the buffer does not exist
*
*/
zdl::DlSystem::IOBufferDataType_t getBufferDataType(const char* name);
/**
* @brief Returns the type of the first buffer
*
* @return The type of the first buffer, or UNSPECIFIED if the map is empty.
*
*/
zdl::DlSystem::IOBufferDataType_t getBufferDataType();
/**
* @brief Returns the size of the buffer type map.
*
* @return The size of the map
*
*/
size_t size();
/**
* @brief Checks the existence of the named buffer in the map
*
* @return True if the named buffer exists, false otherwise.
*
*/
bool find(const char* name);
/**
* @brief Resets the map
*
*/
void clear();
/**
* @brief Checks whether the map is empty
*
* @return True if the map is empty, false otherwise.
*
*/
bool empty();
/**
* @brief Destroys the map
*
*/
~IOBufferDataTypeMap();
private:
std::shared_ptr<::DlSystem::IOBufferDataTypeMapImpl> m_IOBufferDataTypeMapImpl;
};
}
}
#endif

View File

@@ -1,146 +0,0 @@
//=============================================================================
//
// Copyright (c) 2015-2020 Qualcomm Technologies, Inc.
// All Rights Reserved.
// Confidential and Proprietary - Qualcomm Technologies, Inc.
//
//=============================================================================
#ifndef _ITENSOR_HPP_
#define _ITENSOR_HPP_
#include "ITensorItr.hpp"
#include "ITensorItrImpl.hpp"
#include "TensorShape.hpp"
#include "ZdlExportDefine.hpp"
#include <memory>
#include <ostream>
#include <cmath>
namespace zdl {
namespace DlSystem
{
class ITensor;
}}
namespace zdl { namespace DlSystem
{
/** @addtogroup c_plus_plus_apis C++
@{ */
/**
* Represents a tensor which holds n-dimensional data. It is important to
* understand how the tensor data is represented in memory
* relative to the tensor dimensions. Tensors store data in
* memory in row-major order (i.e. the last tensor dimension is
* the fastest varying one). For example, if you have a two
* dimensional tensor with 3 rows and 2 columns (i.e. the tensor
* dimensions are 3,2 as returned in tensor dimension vectors)
* with the following data in terms rows and columns:
*
* | 1 2 | <br/>
* | 3 4 | <br/>
* | 5 6 | <br/>
*
* This data would be stored in memory as 1,2,3,4,5,6.
*/
class ZDL_EXPORT ITensor
{
public:
typedef zdl::DlSystem::ITensorItr<false> iterator;
typedef zdl::DlSystem::ITensorItr<true> const_iterator;
virtual ~ITensor() {}
/**
* Returns a tensor iterator pointing to the beginning
* of the data in the tensor.
*
* @return A tensor iterator that points to the first data
* element in the tensor.
*/
virtual iterator begin() = 0;
/**
* Returns the const version of a tensor iterator
* pointing to the beginning of the data in the tensor.
*
* @return A tensor const iterator that points to the first data
* element in the tensor.
*/
virtual const_iterator cbegin() const = 0;
/**
* Returns a tensor iterator pointing to the end of the
* data in the tensor. This tensor should not be
* dereferenced.
*
* @return A tensor iterator that points to the end of the data
* (one past the last element) in the tensor.
*/
virtual iterator end() = 0;
/**
* Returns the const version of a tensor iterator
* pointing to the end of the data in the tensor. This
* tensor should not be dereferenced.
*
* @return A tensor const iterator that points to the end of the
* data (one past the last element) in the tensor.
*/
virtual const_iterator cend() const = 0;
/**
* @brief Gets the shape of this tensor.
*
* The last element of the vector represents the fastest varying
* dimension and the zeroth element represents the slowest
* varying dimension, etc.
*
* @return A shape class holding the tensor dimensions.
*/
virtual TensorShape getShape() const = 0;
/**
* Returns the element size of the data in the tensor
* (discounting strides). This is how big a buffer would
* need to be to hold the tensor data contiguously in
* memory.
*
* @return The size of the tensor (in elements).
*/
virtual size_t getSize() const = 0;
/**
* @brief Serializes the tensor to an output stream.
*
* @param[in] output The output stream to which to write the tensor
*
* @throw std::runtime_error If the stream is ever in a bad
* state before the tensor is fully serialized.
*/
virtual void serialize(std::ostream &output) const = 0;
friend iterator;
friend const_iterator;
virtual bool isQuantized() {return false;}
virtual float GetDelta() {return NAN;};
virtual float GetOffset() {return NAN;};
protected:
/**
* Returns the tensor iterator implementation.
*
* @return A pointer to the tensor iterator implementation.
*/
virtual std::unique_ptr<::DlSystem::ITensorItrImpl> getItrImpl() const = 0;
};
}}
/** @} */ /* end_addtogroup c_plus_plus_apis C++ */
#endif

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