mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-09 01:25:11 +08:00
Compare commits
55 Commits
accel-pers
...
tools
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3375277b3c | ||
|
|
6e06bcb14f | ||
|
|
bc07cecfa0 | ||
|
|
de30e9a3cf | ||
|
|
c599542dfa | ||
|
|
b737989e64 | ||
|
|
5fbc358fd5 | ||
|
|
ce30d815f7 | ||
|
|
fdde1aa6a1 | ||
|
|
961b2a2d30 | ||
|
|
f3d39d481a | ||
|
|
6e037d80ff | ||
|
|
907bc5cf06 | ||
|
|
b3ff268f89 | ||
|
|
42e08515e6 | ||
|
|
d0ec46dc5d | ||
|
|
48a8802298 | ||
|
|
79971b9eb2 | ||
|
|
7ba21f9f1b | ||
|
|
6b4118ab27 | ||
|
|
0844424ad1 | ||
|
|
5901c9b41f | ||
|
|
d52ce19c15 | ||
|
|
05cc9a14e2 | ||
|
|
18f8956e0e | ||
|
|
0aa6f22c26 | ||
|
|
c90f262ce7 | ||
|
|
e8ee5a23f0 | ||
|
|
4a189f828a | ||
|
|
072e18faef | ||
|
|
3b1fddfde9 | ||
|
|
bddec6971e | ||
|
|
34e02b6ae5 | ||
|
|
c98cc5d40a | ||
|
|
4a0d8063e5 | ||
|
|
e2e52bcccb | ||
|
|
ccf86b7b72 | ||
|
|
483894cfc8 | ||
|
|
a678554122 | ||
|
|
bfd3eab260 | ||
|
|
f5aedbce6e | ||
|
|
4f860dd397 | ||
|
|
f308d9ab17 | ||
|
|
9226222ad4 | ||
|
|
3e317a8b4d | ||
|
|
0eae4e0b3b | ||
|
|
37ffa5ed21 | ||
|
|
05e3eaf2fc | ||
|
|
c8fc344d68 | ||
|
|
264948e5ff | ||
|
|
9d87beac8e | ||
|
|
2e0bc80f94 | ||
|
|
4b8781886a | ||
|
|
97edff5e5c | ||
|
|
a81570a6c2 |
2
.github/workflows/ci_weekly_report.yaml
vendored
2
.github/workflows/ci_weekly_report.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
report:
|
||||
needs: [ci_matrix_run]
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && github.repository == 'commaai/openpilot'
|
||||
if: always()
|
||||
steps:
|
||||
- name: Get job results
|
||||
uses: actions/github-script@v7
|
||||
|
||||
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
151
.github/workflows/mici_raylib_ui_preview.yaml
vendored
@@ -1,151 +0,0 @@
|
||||
name: "mici raylib ui preview"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [assigned, opened, synchronize, reopened, edited]
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- 'selfdrive/assets/**'
|
||||
- 'selfdrive/ui/**'
|
||||
- 'system/ui/**'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
UI_JOB_NAME: "Create mici raylib UI Report"
|
||||
REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }}
|
||||
BRANCH_NAME: "openpilot/pr-${{ github.event.number }}-mici-raylib-ui"
|
||||
MASTER_BRANCH_NAME: "openpilot_master_ui_mici_raylib"
|
||||
# All report files are pushed here
|
||||
REPORT_FILES_BRANCH_NAME: "mici-raylib-ui-reports"
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
if: github.repository == 'commaai/openpilot'
|
||||
name: preview
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Waiting for ui generation to end
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ env.SHA }}
|
||||
check-name: ${{ env.UI_JOB_NAME }}
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed-conclusions: success
|
||||
wait-interval: 20
|
||||
|
||||
- name: Getting workflow run ID
|
||||
id: get_run_id
|
||||
run: |
|
||||
echo "run_id=$(curl https://api.github.com/repos/${{ github.repository }}/commits/${{ env.SHA }}/check-runs | jq -r '.check_runs[] | select(.name == "${{ env.UI_JOB_NAME }}") | .html_url | capture("(?<number>[0-9]+)") | .number')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Getting proposed ui # filename: pr_ui/mici_ui_replay.mp4
|
||||
id: download-artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run_id: ${{ steps.get_run_id.outputs.run_id }}
|
||||
search_artifacts: true
|
||||
name: mici-raylib-report-1-${{ env.REPORT_NAME }}
|
||||
path: ${{ github.workspace }}/pr_ui
|
||||
|
||||
- name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: commaai/ci-artifacts
|
||||
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
|
||||
path: ${{ github.workspace }}/master_ui_raylib
|
||||
ref: ${{ env.MASTER_BRANCH_NAME }}
|
||||
|
||||
- name: Saving new master ui
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||
run: |
|
||||
git checkout --orphan=new_master_ui_mici_raylib
|
||||
git rm -rf *
|
||||
git branch -D ${{ env.MASTER_BRANCH_NAME }}
|
||||
git branch -m ${{ env.MASTER_BRANCH_NAME }}
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
mv ${{ github.workspace }}/pr_ui/* .
|
||||
git add .
|
||||
git commit -m "mici raylib video for commit ${{ env.SHA }}"
|
||||
git push origin ${{ env.MASTER_BRANCH_NAME }} --force
|
||||
|
||||
- name: Setup FFmpeg
|
||||
uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae
|
||||
|
||||
- name: Finding diff
|
||||
if: github.event_name == 'pull_request_target'
|
||||
id: find_diff
|
||||
run: |
|
||||
# Find the video file from PR
|
||||
pr_video="${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4"
|
||||
mv "${{ github.workspace }}/pr_ui/mici_ui_replay.mp4" "$pr_video"
|
||||
|
||||
master_video="${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4"
|
||||
mv "${{ github.workspace }}/master_ui_raylib/mici_ui_replay.mp4" "$master_video"
|
||||
|
||||
# Run report
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}"
|
||||
diff_exit_code=0
|
||||
python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$?
|
||||
|
||||
# Copy diff report files
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/
|
||||
|
||||
REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html"
|
||||
if [ $diff_exit_code -eq 0 ]; then
|
||||
DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)"
|
||||
else
|
||||
DIFF="❌ <strong>Videos differ!</strong> [View Diff Report]($REPORT_URL)"
|
||||
fi
|
||||
echo "DIFF=$DIFF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Saving proposed ui
|
||||
if: github.event_name == 'pull_request_target'
|
||||
working-directory: ${{ github.workspace }}/master_ui_raylib
|
||||
run: |
|
||||
# Overwrite PR branch w/ proposed ui, and master ui at this point in time for future reference
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
git checkout --orphan=${{ env.BRANCH_NAME }}
|
||||
git rm -rf *
|
||||
mv ${{ github.workspace }}/pr_ui/* .
|
||||
git add .
|
||||
git commit -m "mici raylib video for PR #${{ github.event.number }}"
|
||||
git push origin ${{ env.BRANCH_NAME }} --force
|
||||
|
||||
# Append diff report to report files branch
|
||||
git fetch origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
git checkout ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html diff_pr_${{ github.event.number }}.html
|
||||
git add diff_pr_${{ github.event.number }}.html
|
||||
git commit -m "mici raylib ui diff report for PR #${{ github.event.number }}" || echo "No changes to commit"
|
||||
git push origin ${{ env.REPORT_FILES_BRANCH_NAME }}
|
||||
|
||||
- name: Comment Video on PR
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: thollander/actions-comment-pull-request@v2
|
||||
with:
|
||||
message: |
|
||||
<!-- _(run_id_video_mici_raylib **${{ github.run_id }}**)_ -->
|
||||
## mici raylib UI Preview
|
||||
${{ steps.find_diff.outputs.DIFF }}
|
||||
comment_tag: run_id_video_mici_raylib
|
||||
pr_number: ${{ github.event.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
21
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
21
.github/workflows/sunnypilot-build-prebuilt.yaml
vendored
@@ -200,28 +200,37 @@ jobs:
|
||||
sudo rm -rf ${OUTPUT_DIR}
|
||||
mkdir -p ${OUTPUT_DIR}
|
||||
rsync -am${RUNNER_DEBUG:+v} \
|
||||
--include='**/panda/board/' \
|
||||
--include='**/panda/board/obj' \
|
||||
--include='**/panda/board/obj/panda.bin.signed' \
|
||||
--include='**/panda/board/obj/panda_h7.bin.signed' \
|
||||
--include='**/panda/board/obj/bootstub.panda.bin' \
|
||||
--include='**/panda/board/obj/bootstub.panda_h7.bin' \
|
||||
--exclude='.sconsign.dblite' \
|
||||
--exclude='*.a' \
|
||||
--exclude='*.o' \
|
||||
--exclude='*.os' \
|
||||
--exclude='*.pyc' \
|
||||
--exclude='moc_*' \
|
||||
--exclude='__pycache__' \
|
||||
--exclude='*.cc' \
|
||||
--exclude='Jenkinsfile' \
|
||||
--exclude='supercombo.onnx' \
|
||||
--exclude='**/panda/board/*' \
|
||||
--exclude='**/panda/board/obj/**' \
|
||||
--exclude='**/panda/certs/' \
|
||||
--exclude='**/panda/crypto/' \
|
||||
--exclude='**/release/' \
|
||||
--exclude='**/.github/' \
|
||||
--exclude='**/selfdrive/ui/replay/' \
|
||||
--exclude='**/__pycache__/' \
|
||||
--exclude='**/selfdrive/ui/*.h' \
|
||||
--exclude='**/selfdrive/ui/**/*.h' \
|
||||
--exclude='**/selfdrive/ui/qt/offroad/sunnypilot/' \
|
||||
--exclude='${{env.SCONS_CACHE_DIR}}' \
|
||||
--exclude='**/.git/' \
|
||||
--exclude='**/SConstruct' \
|
||||
--exclude='**/SConscript' \
|
||||
--exclude='**/.venv/' \
|
||||
--exclude='selfdrive/modeld/models/driving_vision.onnx' \
|
||||
--exclude='selfdrive/modeld/models/driving_policy.onnx' \
|
||||
--exclude='sunnypilot/modeld*/models/supercombo.onnx' \
|
||||
--exclude='third_party/*x86*' \
|
||||
--exclude='third_party/*Darwin*' \
|
||||
--delete-excluded \
|
||||
--chown=comma:comma \
|
||||
${BUILD_DIR}/ ${OUTPUT_DIR}/
|
||||
|
||||
26
.github/workflows/tests.yaml
vendored
26
.github/workflows/tests.yaml
vendored
@@ -297,29 +297,3 @@ jobs:
|
||||
with:
|
||||
name: raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/test_ui/raylib_report/screenshots
|
||||
|
||||
create_mici_raylib_ui_report:
|
||||
name: Create mici raylib UI Report
|
||||
runs-on: ${{
|
||||
(github.repository == 'commaai/openpilot') &&
|
||||
((github.event_name != 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))
|
||||
&& fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]')
|
||||
|| fromJSON('["ubuntu-24.04"]') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot
|
||||
run: ${{ env.RUN }} "scons -j$(nproc)"
|
||||
- name: Create mici raylib UI Report
|
||||
run: >
|
||||
${{ env.RUN }} "PYTHONWARNINGS=ignore &&
|
||||
source selfdrive/test/setup_xvfb.sh &&
|
||||
WINDOWED=1 python3 selfdrive/ui/tests/diff/replay.py"
|
||||
- name: Upload Raylib UI Report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mici-raylib-report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }}
|
||||
path: selfdrive/ui/tests/diff/report
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
Version 0.10.3 (2025-12-10)
|
||||
========================
|
||||
|
||||
Version 0.10.2 (2025-11-19)
|
||||
========================
|
||||
* comma four support
|
||||
|
||||
@@ -192,7 +192,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
accelPersonality @8 :AccelerationPersonality;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
@@ -204,11 +203,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
blended @1;
|
||||
}
|
||||
}
|
||||
enum AccelerationPersonality {
|
||||
sport @0;
|
||||
normal @1;
|
||||
eco @2;
|
||||
}
|
||||
|
||||
struct SmartCruiseControl {
|
||||
vision @0 :Vision;
|
||||
map @1 :Map;
|
||||
|
||||
@@ -22,5 +22,5 @@ def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
|
||||
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
|
||||
|
||||
|
||||
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
|
||||
def get_key_pair():
|
||||
return CommaConnectApi(None).get_key_pair()
|
||||
|
||||
@@ -6,9 +6,9 @@ from datetime import datetime, timedelta, UTC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.version import get_version
|
||||
|
||||
# name: jwt signature algorithm
|
||||
KEYS = {"id_rsa": "RS256",
|
||||
"id_ecdsa": "ES256"}
|
||||
# name : jwt signature algorithm
|
||||
KEYS = {"id_rsa" : "RS256",
|
||||
"id_ecdsa" : "ES256"}
|
||||
|
||||
|
||||
class BaseApi:
|
||||
@@ -62,7 +62,7 @@ class BaseApi:
|
||||
return requests.request(method, f"{self.api_host}/{endpoint}", timeout=timeout, headers=headers, json=json, params=params)
|
||||
|
||||
@staticmethod
|
||||
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
|
||||
def get_key_pair():
|
||||
for key in KEYS:
|
||||
if os.path.isfile(Paths.persist_root() + f'/comma/{key}') and os.path.isfile(Paths.persist_root() + f'/comma/{key}.pub'):
|
||||
with open(Paths.persist_root() + f'/comma/{key}') as private, open(Paths.persist_root() + f'/comma/{key}.pub') as public:
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Dark Souls 2 (Default)"
|
||||
#define DEFAULT_MODEL "The Cool People (Default)"
|
||||
|
||||
@@ -71,7 +71,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LastGPSPosition", {PERSISTENT, STRING}},
|
||||
{"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
|
||||
{"LastAgnosPowerMonitorShutdown", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
|
||||
@@ -133,8 +132,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from openpilot.common.utils import atomic_write
|
||||
from openpilot.common.utils import atomic_write_in_dir
|
||||
|
||||
|
||||
class TestFileHelpers:
|
||||
@@ -15,5 +15,5 @@ class TestFileHelpers:
|
||||
assert f.read() == "test"
|
||||
os.remove(path)
|
||||
|
||||
def test_atomic_write(self):
|
||||
self.run_atomic_write_func(atomic_write)
|
||||
def test_atomic_write_in_dir(self):
|
||||
self.run_atomic_write_func(atomic_write_in_dir)
|
||||
|
||||
@@ -32,8 +32,8 @@ class CallbackReader:
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def atomic_write(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
|
||||
overwrite: bool = False):
|
||||
def atomic_write_in_dir(path: str, mode: str = 'w', buffering: int = -1, encoding: str | None = None, newline: str | None = None,
|
||||
overwrite: bool = False):
|
||||
"""Write to a file atomically using a temporary file in the same directory as the destination file."""
|
||||
dir_name = os.path.dirname(path)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define COMMA_VERSION "0.10.3"
|
||||
#define COMMA_VERSION "0.10.2"
|
||||
|
||||
@@ -20,7 +20,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2018-21|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2018-21">Buy Here</a></sub></details>|||
|
||||
@@ -384,7 +384,7 @@ If your car has the following packages or features, then it's a good candidate f
|
||||
|
||||
| Make | Required Package/Features |
|
||||
| ---- | ------------------------- |
|
||||
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
|
||||
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
|
||||
| Ford | Any car with Lane Centering will likely work. |
|
||||
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
|
||||
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |
|
||||
|
||||
@@ -31,7 +31,7 @@ We'll run the `replay` tool with the demo route to get data streaming for testin
|
||||
tools/replay/replay --demo
|
||||
|
||||
# in terminal 2
|
||||
./selfdrive/ui/ui.py
|
||||
selfdrive/ui/ui
|
||||
```
|
||||
|
||||
The openpilot UI should launch and show a replay of the demo route.
|
||||
@@ -43,36 +43,39 @@ If you have your own comma device, you can replace `--demo` with one of your own
|
||||
|
||||
Now let’s update the speed display color in the UI.
|
||||
|
||||
Search for the function responsible for rendering the current speed:
|
||||
Search for the function responsible for rendering UI text:
|
||||
```bash
|
||||
git grep "_draw_current_speed" selfdrive/ui/onroad/hud_renderer.py
|
||||
git grep "drawText" selfdrive/ui/qt/onroad/hud.cc
|
||||
```
|
||||
|
||||
You'll find the relevant code inside `selfdrive/ui/onroad/hud_renderer.py`, in this function:
|
||||
You’ll find the relevant code inside `selfdrive/ui/qt/onroad/hud.cc`, in this function:
|
||||
|
||||
```python
|
||||
def _draw_current_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the current vehicle speed and unit."""
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white) # <- this sets the speed text color
|
||||
```cpp
|
||||
void HudRenderer::drawText(QPainter &p, int x, int y, const QString &text, int alpha) {
|
||||
QRect real_rect = p.fontMetrics().boundingRect(text);
|
||||
real_rect.moveCenter({x, y - real_rect.height() / 2});
|
||||
|
||||
p.setPen(QColor(0xff, 0xff, 0xff, alpha)); // <- this sets the speed text color
|
||||
p.drawText(real_rect.x(), real_rect.bottom(), text);
|
||||
}
|
||||
```
|
||||
|
||||
Change `COLORS.white` to make it **blue** instead of white. A nice soft blue is `#8080FF`, which you can change inline:
|
||||
Change the `QColor(...)` line to make it **blue** instead of white. A nice soft blue is `#8080FF`, which translates to:
|
||||
|
||||
```diff
|
||||
- rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
+ rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, rl.Color(0x80, 0x80, 0xFF, 255))
|
||||
- p.setPen(QColor(0xff, 0xff, 0xff, alpha));
|
||||
+ p.setPen(QColor(0x80, 0x80, 0xFF, alpha));
|
||||
```
|
||||
|
||||
This change will tint all speed-related UI text to blue with the same transparency (`alpha`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Re-run the UI
|
||||
## 4. Rebuild the UI
|
||||
|
||||
After making changes, re-run the UI to see your new UI:
|
||||
After making changes, rebuild Openpilot so your new UI is compiled:
|
||||
```bash
|
||||
./selfdrive/ui/ui.py
|
||||
scons -j$(nproc) && selfdrive/ui/ui
|
||||
```
|
||||

|
||||
|
||||
|
||||
Submodule opendbc_repo updated: f746884333...61bf5a90c5
2
panda
2
panda
Submodule panda updated: 5f3c09c910...dee9061b2a
@@ -85,7 +85,6 @@ docs = [
|
||||
]
|
||||
|
||||
testing = [
|
||||
"coverage",
|
||||
"hypothesis ==6.47.*",
|
||||
"mypy",
|
||||
"pytest",
|
||||
@@ -116,7 +115,7 @@ dev = [
|
||||
"pyautogui",
|
||||
"pygame",
|
||||
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
||||
"pytools>=2025.1.6; platform_machine != 'aarch64'",
|
||||
"pytools < 2024.1.11; platform_machine != 'aarch64'", # pyopencl use a broken version
|
||||
"pywinctl",
|
||||
"pyprof2calltree",
|
||||
"tabulate",
|
||||
@@ -126,7 +125,7 @@ dev = [
|
||||
|
||||
tools = [
|
||||
"metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')",
|
||||
"dearpygui>=2.1.0; (sys_platform != 'linux' or platform_machine != 'aarch64')", # not vended for linux aarch64
|
||||
"dearpygui>=2.1.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -227,7 +226,7 @@ lint.select = [
|
||||
"TRY203", "TRY400", "TRY401", # try/excepts
|
||||
"RUF008", "RUF100",
|
||||
"TID251",
|
||||
"PLE", "PLR1704",
|
||||
"PLR1704",
|
||||
]
|
||||
lint.ignore = [
|
||||
"E741",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
cd $DIR/../../tinygrad_repo
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
|
||||
#export DEBUG=2
|
||||
export PYTHONPATH=.
|
||||
export AM_RESET=1
|
||||
export AMD=1
|
||||
export AMD_IFACE=USB
|
||||
export AMD_LLVM=1
|
||||
|
||||
python3 -m unittest -q --buffer test.test_tiny.TestTiny.test_plus \
|
||||
> /tmp/test_tiny.log 2>&1 || (cat /tmp/test_tiny.log; exit 1)
|
||||
printf "${GREEN}Booted in ${SECONDS}s${NC}\n"
|
||||
printf "${GREEN}=============${NC}\n"
|
||||
|
||||
printf "\n\n"
|
||||
printf "${GREEN}Transfer speeds:${NC}\n"
|
||||
printf "${GREEN}================${NC}\n"
|
||||
python3 test/external/external_test_usb_asm24.py TestDevCopySpeeds
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
import numpy as np
|
||||
from scipy.io import wavfile
|
||||
|
||||
|
||||
sr = 48000
|
||||
max_int16 = 2**15 - 1
|
||||
|
||||
def harmonic_beep(freq, duration_seconds):
|
||||
n_total = int(sr * duration_seconds)
|
||||
|
||||
signal = np.sin(2 * np.pi * freq * np.arange(n_total) / sr)
|
||||
x = np.arange(n_total)
|
||||
exp_scale = np.exp(-x/5.5e3)
|
||||
return max_int16 * signal * exp_scale
|
||||
|
||||
engage_beep = harmonic_beep(1661.219, 0.5)
|
||||
wavfile.write("engage.wav", sr, engage_beep.astype(np.int16))
|
||||
disengage_beep = harmonic_beep(1318.51, 0.5)
|
||||
wavfile.write("disengage.wav", sr, disengage_beep.astype(np.int16))
|
||||
@@ -42,7 +42,7 @@ If your car has the following packages or features, then it's a good candidate f
|
||||
|
||||
| Make | Required Package/Features |
|
||||
| ---- | ------------------------- |
|
||||
| Acura | Any car with AcuraWatch will work. AcuraWatch comes standard on many newer models. |
|
||||
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
|
||||
| Ford | Any car with Lane Centering will likely work. |
|
||||
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
|
||||
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |
|
||||
|
||||
@@ -26,18 +26,6 @@ class MockCarState:
|
||||
return CS, CS_SP
|
||||
|
||||
|
||||
BRAND_EXTRA_GEARS = {
|
||||
'ford': [GearShifter.low, GearShifter.manumatic],
|
||||
'nissan': [GearShifter.brake],
|
||||
'chrysler': [GearShifter.low],
|
||||
'honda': [GearShifter.sport],
|
||||
'toyota': [GearShifter.sport],
|
||||
'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic],
|
||||
'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
|
||||
'hyundai': [GearShifter.sport, GearShifter.manumatic]
|
||||
}
|
||||
|
||||
|
||||
class CarSpecificEvents:
|
||||
def __init__(self, CP: structs.CarParams):
|
||||
self.CP = CP
|
||||
@@ -48,13 +36,17 @@ class CarSpecificEvents:
|
||||
self.silent_steer_warning = True
|
||||
|
||||
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
|
||||
extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None)
|
||||
|
||||
if self.CP.brand in ('body', 'mock'):
|
||||
events = Events()
|
||||
|
||||
elif self.CP.brand == 'ford':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low, GearShifter.manumatic])
|
||||
|
||||
elif self.CP.brand == 'nissan':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.brake])
|
||||
|
||||
elif self.CP.brand == 'chrysler':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.low])
|
||||
|
||||
# Low speed steer alert hysteresis logic
|
||||
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
|
||||
@@ -65,7 +57,7 @@ class CarSpecificEvents:
|
||||
events.add(EventName.belowSteerSpeed)
|
||||
|
||||
elif self.CP.brand == 'honda':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False)
|
||||
|
||||
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
|
||||
events.add(EventName.belowEngageSpeed)
|
||||
@@ -87,7 +79,7 @@ class CarSpecificEvents:
|
||||
|
||||
elif self.CP.brand == 'toyota':
|
||||
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport])
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
if CS.cruiseState.standstill and not CS.brakePressed:
|
||||
@@ -102,7 +94,9 @@ class CarSpecificEvents:
|
||||
events.add(EventName.manualRestart)
|
||||
|
||||
elif self.CP.brand == 'gm':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport, GearShifter.low,
|
||||
GearShifter.eco, GearShifter.manumatic],
|
||||
pcm_enable=self.CP.pcmCruise)
|
||||
|
||||
# Enabling at a standstill with brake is allowed
|
||||
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
|
||||
@@ -113,7 +107,8 @@ class CarSpecificEvents:
|
||||
events.add(EventName.resumeRequired)
|
||||
|
||||
elif self.CP.brand == 'volkswagen':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
|
||||
pcm_enable=self.CP.pcmCruise)
|
||||
|
||||
if self.CP.openpilotLongitudinalControl:
|
||||
if CS.vEgo < self.CP.minEnableSpeed + 0.5:
|
||||
@@ -126,14 +121,15 @@ class CarSpecificEvents:
|
||||
# events.add(EventName.steerTimeLimit)
|
||||
|
||||
elif self.CP.brand == 'hyundai':
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=(GearShifter.sport, GearShifter.manumatic),
|
||||
pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
|
||||
|
||||
else:
|
||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
||||
events = self.create_common_events(CS, CS_prev)
|
||||
|
||||
return events
|
||||
|
||||
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True,
|
||||
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears=None, pcm_enable=True,
|
||||
allow_button_cancel=True):
|
||||
events = Events()
|
||||
|
||||
|
||||
@@ -22,17 +22,15 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La
|
||||
# Additionally, there is friction in the steering wheel that needs
|
||||
# to be overcome to move it at all, this is compensated for too.
|
||||
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
KD = 0.0
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
LP_FILTER_CUTOFF_HZ = 1.2
|
||||
JERK_LOOKAHEAD_SECONDS = 0.19
|
||||
JERK_GAIN = 0.3
|
||||
LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0
|
||||
VERSION = 1
|
||||
VERSION = 0
|
||||
|
||||
class LatControlTorque(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI, dt):
|
||||
@@ -40,13 +38,13 @@ class LatControlTorque(LatControl):
|
||||
self.torque_params = CP.lateralTuning.torque.as_builder()
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.lateral_accel_from_torque = CI.lateral_accel_from_torque()
|
||||
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt)
|
||||
self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt)
|
||||
self.update_limits()
|
||||
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
|
||||
self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt)
|
||||
self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len)
|
||||
self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt)
|
||||
self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
|
||||
self.previous_measurement = 0.0
|
||||
self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt)
|
||||
|
||||
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
|
||||
|
||||
@@ -78,15 +76,17 @@ class LatControlTorque(LatControl):
|
||||
|
||||
delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len))
|
||||
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
|
||||
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))
|
||||
raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt)
|
||||
desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk)
|
||||
# TODO factor out lateral jerk from error to later replace it with delay independent alternative
|
||||
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
||||
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
||||
setpoint = expected_lateral_accel
|
||||
desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay
|
||||
|
||||
measurement = measured_curvature * CS.vEgo ** 2
|
||||
measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt)
|
||||
self.previous_measurement = measurement
|
||||
|
||||
setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel
|
||||
error = setpoint - measurement
|
||||
|
||||
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
||||
@@ -94,10 +94,15 @@ class LatControlTorque(LatControl):
|
||||
ff = gravity_adjusted_future_lateral_accel
|
||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||
ff -= self.torque_params.latAccelOffset
|
||||
ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
# TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it
|
||||
ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
|
||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator)
|
||||
output_lataccel = self.pid.update(pid_log.error,
|
||||
-measurement_rate,
|
||||
feedforward=ff,
|
||||
speed=CS.vEgo,
|
||||
freeze_integrator=freeze_integrator)
|
||||
output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params)
|
||||
|
||||
# Lateral acceleration torque controller extension updates
|
||||
|
||||
@@ -10,8 +10,6 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.modeld.constants import index_function
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
|
||||
if __name__ == '__main__': # generating code
|
||||
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
|
||||
else:
|
||||
@@ -230,7 +228,6 @@ class LongitudinalMpc:
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
|
||||
def reset(self):
|
||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
@@ -335,13 +332,6 @@ class LongitudinalMpc:
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
|
||||
if self.accel_controller.is_enabled():
|
||||
min_accel = self.accel_controller.get_min_accel(v_ego)
|
||||
else:
|
||||
min_accel = CRUISE_MIN_ACCEL
|
||||
a_cruise_min = min_accel
|
||||
|
||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||
|
||||
@@ -360,7 +350,7 @@ class LongitudinalMpc:
|
||||
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
||||
|
||||
@@ -124,11 +124,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
if self.accel_controller.is_enabled():
|
||||
max_accel = self.accel_controller.get_max_accel(v_ego)
|
||||
accel_clip = [ACCEL_MIN, max_accel]
|
||||
else:
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
else:
|
||||
|
||||
@@ -172,7 +172,7 @@ class PoseCalibrator:
|
||||
ned_from_calib_euler = self._ned_from_calib(pose.orientation)
|
||||
angular_velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
|
||||
acceleration_calib = self._transform_calib_from_device(pose.acceleration)
|
||||
velocity_calib = self._transform_calib_from_device(pose.velocity)
|
||||
velocity_calib = self._transform_calib_from_device(pose.angular_velocity)
|
||||
|
||||
return Pose(ned_from_calib_euler, velocity_calib, acceleration_calib, angular_velocity_calib)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
0
selfdrive/modeld/tests/__init__.py
Normal file
0
selfdrive/modeld/tests/__init__.py
Normal file
102
selfdrive/modeld/tests/test_modeld.py
Normal file
102
selfdrive/modeld/tests/test_modeld.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
|
||||
|
||||
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
|
||||
IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
|
||||
class TestModeld:
|
||||
|
||||
def setup_method(self):
|
||||
self.vipc_server = VisionIpcServer("camerad")
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.start_listener()
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry'])
|
||||
self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration'])
|
||||
|
||||
managed_processes['modeld'].start()
|
||||
self.pm.wait_for_readers_to_update("roadCameraState", 10)
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['modeld'].stop()
|
||||
del self.vipc_server
|
||||
|
||||
def _send_frames(self, frame_id, cams=None):
|
||||
if cams is None:
|
||||
cams = ('roadCameraState', 'wideRoadCameraState')
|
||||
|
||||
cs = None
|
||||
for cam in cams:
|
||||
msg = messaging.new_message(cam)
|
||||
cs = getattr(msg, cam)
|
||||
cs.frameId = frame_id
|
||||
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
|
||||
cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9))
|
||||
cam_meta = meta_from_camera_state(cam)
|
||||
|
||||
self.pm.send(msg.which(), msg)
|
||||
self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId,
|
||||
cs.timestampSof, cs.timestampEof)
|
||||
return cs
|
||||
|
||||
def _wait(self):
|
||||
self.sm.update(5000)
|
||||
if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId:
|
||||
self.sm.update(1000)
|
||||
|
||||
def test_modeld(self):
|
||||
for n in range(1, 500):
|
||||
cs = self._send_frames(n)
|
||||
self._wait()
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
assert mdl.frameId == n
|
||||
assert mdl.frameIdExtra == n
|
||||
assert mdl.timestampEof == cs.timestampEof
|
||||
assert mdl.frameAge == 0
|
||||
assert mdl.frameDropPerc == 0
|
||||
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert odo.frameId == n
|
||||
assert odo.timestampEof == cs.timestampEof
|
||||
|
||||
def test_dropped_frames(self):
|
||||
"""
|
||||
modeld should only run on consecutive road frames
|
||||
"""
|
||||
frame_id = -1
|
||||
road_frames = list()
|
||||
for n in range(1, 50):
|
||||
if (random.random() < 0.1) and n > 3:
|
||||
cams = random.choice([(), ('wideRoadCameraState', )])
|
||||
self._send_frames(n, cams)
|
||||
else:
|
||||
self._send_frames(n)
|
||||
road_frames.append(n)
|
||||
self._wait()
|
||||
|
||||
if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1:
|
||||
frame_id = road_frames[-1]
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert mdl.frameId == frame_id
|
||||
assert mdl.frameIdExtra == frame_id
|
||||
assert odo.frameId == frame_id
|
||||
if n != frame_id:
|
||||
assert not self.sm.updated['modelV2']
|
||||
assert not self.sm.updated['cameraOdometry']
|
||||
@@ -40,8 +40,8 @@ def dmonitoringd_thread():
|
||||
|
||||
# save rhd virtual toggle every 5 mins
|
||||
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
|
||||
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
||||
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
||||
DM.wheelpos_learner.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
||||
DM.wheel_on_right == (DM.wheelpos_learner.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
||||
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
|
||||
|
||||
def main():
|
||||
|
||||
@@ -40,9 +40,6 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._PHONE_THRESH2 = 15.0
|
||||
self._PHONE_MAX_OFFSET = 0.06
|
||||
self._PHONE_MIN_OFFSET = 0.025
|
||||
self._PHONE_DATA_AVG = 0.05
|
||||
self._PHONE_DATA_VAR = 3*0.005
|
||||
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||
@@ -50,11 +47,9 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._POSE_YAW_THRESHOLD = 0.4020
|
||||
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
|
||||
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
|
||||
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_THRESHOLD = 0.449
|
||||
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
|
||||
self._PITCH_NATURAL_VAR = 3*0.01
|
||||
self._YAW_NATURAL_VAR = 3*0.05
|
||||
self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned
|
||||
self._PITCH_MAX_OFFSET = 0.124
|
||||
self._PITCH_MIN_OFFSET = -0.0881
|
||||
self._YAW_MAX_OFFSET = 0.289
|
||||
@@ -75,9 +70,6 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._WHEELPOS_CALIB_MIN_SPEED = 11
|
||||
self._WHEELPOS_THRESHOLD = 0.5
|
||||
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
|
||||
self._WHEELPOS_DATA_AVG = 0.03
|
||||
self._WHEELPOS_DATA_VAR = 3*5.5e-5
|
||||
self._WHEELPOS_MAX_COUNT = -1
|
||||
|
||||
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
|
||||
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
|
||||
@@ -86,33 +78,30 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
|
||||
|
||||
class DistractedType:
|
||||
|
||||
NOT_DISTRACTED = 0
|
||||
DISTRACTED_POSE = 1 << 0
|
||||
DISTRACTED_BLINK = 1 << 1
|
||||
DISTRACTED_PHONE = 1 << 2
|
||||
|
||||
class DriverPose:
|
||||
def __init__(self, settings):
|
||||
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
|
||||
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
|
||||
def __init__(self, max_trackable):
|
||||
self.yaw = 0.
|
||||
self.pitch = 0.
|
||||
self.roll = 0.
|
||||
self.yaw_std = 0.
|
||||
self.pitch_std = 0.
|
||||
self.roll_std = 0.
|
||||
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.calibrated = False
|
||||
self.low_std = True
|
||||
self.cfactor_pitch = 1.
|
||||
self.cfactor_yaw = 1.
|
||||
|
||||
class DriverProb:
|
||||
def __init__(self, raw_priors, max_trackable):
|
||||
class DriverPhone:
|
||||
def __init__(self, max_trackable):
|
||||
self.prob = 0.
|
||||
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||
self.prob_offseter = RunningStatFilter(max_trackable=max_trackable)
|
||||
self.prob_calibrated = False
|
||||
|
||||
class DriverBlink:
|
||||
@@ -151,11 +140,9 @@ class DriverMonitoring:
|
||||
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
|
||||
|
||||
# init driver status
|
||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
|
||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
|
||||
self.pose = DriverPose(settings=self.settings)
|
||||
self.wheelpos_learner = RunningStatFilter()
|
||||
self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.phone = DriverPhone(self.settings._POSE_OFFSET_MAX_COUNT)
|
||||
self.blink = DriverBlink()
|
||||
|
||||
self.always_on = always_on
|
||||
@@ -247,11 +234,8 @@ class DriverMonitoring:
|
||||
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
|
||||
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
|
||||
yaw_error = abs(yaw_error)
|
||||
|
||||
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
|
||||
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
|
||||
|
||||
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||
if pitch_error > (self.settings._POSE_PITCH_THRESHOLD*self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD) or \
|
||||
yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw:
|
||||
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||
|
||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||
@@ -272,12 +256,9 @@ class DriverMonitoring:
|
||||
# calibrates only when there's movement and either face detected
|
||||
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
|
||||
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
|
||||
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
|
||||
|
||||
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
|
||||
|
||||
if self.wheelpos.prob_calibrated or demo_mode:
|
||||
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
||||
self.wheelpos_learner.push_and_update(rhd_pred)
|
||||
if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT or demo_mode:
|
||||
self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
||||
else:
|
||||
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
|
||||
# make sure no switching when engaged
|
||||
|
||||
@@ -1 +1 @@
|
||||
e0ad86508edb61b3eaa1b84662c515d2c3368295
|
||||
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
|
||||
@@ -206,9 +206,8 @@ class TestOnroad:
|
||||
result += "-------------- UI Draw Timing ------------------\n"
|
||||
result += "------------------------------------------------\n"
|
||||
|
||||
# other processes preempt ui while starting up
|
||||
offset = int(20 * LOG_OFFSET)
|
||||
ts = self.ts['uiDebug']['drawTimeMillis'][offset:]
|
||||
# skip first few frames -- connecting to vipc
|
||||
ts = self.ts['uiDebug']['drawTimeMillis'][15:]
|
||||
result += f"min {min(ts):.2f}ms\n"
|
||||
result += f"max {max(ts):.2f}ms\n"
|
||||
result += f"std {np.std(ts):.2f}ms\n"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -35,7 +34,7 @@ DESCRIPTIONS = {
|
||||
class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
|
||||
@@ -3,7 +3,6 @@ import math
|
||||
|
||||
from cereal import messaging, log
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -35,7 +34,7 @@ class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
import threading
|
||||
|
||||
from openpilot.common.api import api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
@@ -24,17 +32,50 @@ INSTRUCTIONS = tr_noop(
|
||||
)
|
||||
|
||||
|
||||
class FirehoseLayout(FirehoseLayoutBase):
|
||||
class FirehoseLayout(Widget):
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
GRAY = rl.Color(68, 68, 68, 255)
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._scroll_panel = GuiScrollPanel()
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self._content_height = 0
|
||||
|
||||
self.running = True
|
||||
self.update_thread = threading.Thread(target=self._update_loop, daemon=True)
|
||||
self.update_thread.start()
|
||||
self.last_update_time = 0
|
||||
|
||||
def show_event(self):
|
||||
self.scroll_panel.set_offset(0)
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self.params.get(self.PARAM_KEY)
|
||||
if not stats:
|
||||
return 0
|
||||
try:
|
||||
return int(stats.get("firehose", 0))
|
||||
except Exception:
|
||||
cloudlog.exception(f"Failed to decode firehose stats: {stats}")
|
||||
return 0
|
||||
|
||||
def __del__(self):
|
||||
self.running = False
|
||||
if self.update_thread and self.update_thread.is_alive():
|
||||
self.update_thread.join(timeout=1.0)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Calculate content dimensions
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._content_height)
|
||||
|
||||
# Handle scrolling and render with clipping
|
||||
scroll_offset = self._scroll_panel.update(rect, content_rect)
|
||||
scroll_offset = self.scroll_panel.update(rect, content_rect)
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
self._content_height = self._render_content(rect, scroll_offset)
|
||||
rl.end_scissor_mode()
|
||||
@@ -66,9 +107,9 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
y += 20 + 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
|
||||
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||
y += 20 + 20
|
||||
|
||||
@@ -80,7 +121,7 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
y = self._draw_wrapped_text(x, y, w, tr(INSTRUCTIONS), gui_app.font(FontWeight.NORMAL), 40, self.LIGHT_GRAY)
|
||||
|
||||
# bottom margin + remove effect of scroll offset
|
||||
return int(round(y - self._scroll_panel.offset + 40))
|
||||
return int(round(y - self.scroll_panel.offset + 40))
|
||||
|
||||
def _draw_wrapped_text(self, x, y, width, text, font, font_size, color):
|
||||
wrapped = wrap_text(font, text, font_size, width)
|
||||
@@ -88,3 +129,32 @@ class FirehoseLayout(FirehoseLayoutBase):
|
||||
rl.draw_text_ex(font, line, rl.Vector2(x, y), font_size, 0, color)
|
||||
y += font_size * FONT_SCALE
|
||||
return round(y)
|
||||
|
||||
def _get_status(self) -> tuple[str, rl.Color]:
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
network_metered = ui_state.sm["deviceState"].networkMetered
|
||||
|
||||
if not network_metered and network_type != 0: # Not metered and connected
|
||||
return tr("ACTIVE"), self.GREEN
|
||||
else:
|
||||
return tr("INACTIVE: connect to an unmetered network"), self.RED
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self.params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = get_token(dongle_id)
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, data)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self.running:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
@@ -17,13 +17,6 @@ if gui_app.sunnypilot_ui():
|
||||
# TODO: remove this. updater fails to respond on startup if time is not correct
|
||||
UPDATED_TIMEOUT = 10 # seconds to wait for updated to respond
|
||||
|
||||
# Mapping updater internal states to translated display strings
|
||||
STATE_TO_DISPLAY_TEXT = {
|
||||
"checking...": tr("checking..."),
|
||||
"downloading...": tr("downloading..."),
|
||||
"finalizing update...": tr("finalizing update..."),
|
||||
}
|
||||
|
||||
|
||||
def time_ago(date: datetime.datetime | None) -> str:
|
||||
if not date:
|
||||
@@ -110,9 +103,7 @@ class SoftwareLayout(Widget):
|
||||
# Updater responded
|
||||
self._waiting_for_updater = False
|
||||
self._download_btn.action_item.set_enabled(False)
|
||||
# Use the mapping, with a fallback to the original state string
|
||||
display_text = STATE_TO_DISPLAY_TEXT.get(updater_state, updater_state)
|
||||
self._download_btn.action_item.set_value(display_text)
|
||||
self._download_btn.action_item.set_value(updater_state)
|
||||
else:
|
||||
if failed_count > 0:
|
||||
self._download_btn.action_item.set_value(tr("failed to check for update"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.common.params import UnknownKeyName
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
@@ -41,7 +41,7 @@ DESCRIPTIONS = {
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.params
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
@@ -198,11 +198,6 @@ class TogglesLayout(Widget):
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
|
||||
# TODO: make a param control list item so we don't need to manage internal state as much here
|
||||
# refresh toggles from params to mirror external changes
|
||||
for param in self._toggle_defs:
|
||||
self._toggles[param].action_item.set_state(self._params.get_bool(param))
|
||||
|
||||
# these toggles need restart, block while engaged
|
||||
for toggle_def in self._toggle_defs:
|
||||
if self._toggle_defs[toggle_def][3] and toggle_def not in self._locked_toggles:
|
||||
|
||||
@@ -67,10 +67,8 @@ class PrimeState:
|
||||
cloudlog.info(f"Prime type updated to {prime_type}")
|
||||
|
||||
def _worker_thread(self) -> None:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
while self._running:
|
||||
if not ui_state.started and device._awake:
|
||||
self._fetch_prime_status()
|
||||
self._fetch_prime_status()
|
||||
|
||||
for _ in range(int(self.FETCH_INTERVAL / self.SLEEP_INTERVAL)):
|
||||
if not self._running:
|
||||
|
||||
@@ -3,16 +3,18 @@ import time
|
||||
from cereal import log
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets.label import gui_label, MiciLabel, UnifiedLabel
|
||||
from openpilot.system.ui.widgets.label import gui_label, MiciLabel
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.text import wrap_text
|
||||
from openpilot.system.version import training_version, RELEASE_BRANCHES
|
||||
from openpilot.system.version import training_version
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
|
||||
RELEASE_BRANCH = "release3"
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
NETWORK_TYPES = {
|
||||
@@ -113,7 +115,7 @@ class MiciHomeLayout(Widget):
|
||||
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)
|
||||
self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
self._branch_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN, elide_right=False, scroll=True)
|
||||
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
@@ -185,22 +187,27 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
if self._version_text is not None:
|
||||
# release branch
|
||||
release_branch = self._version_text[1] in RELEASE_BRANCHES
|
||||
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
|
||||
self._version_label.set_text(self._version_text[0])
|
||||
self._version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._version_label.render()
|
||||
if self._version_text[0] == RELEASE_BRANCH:
|
||||
version_pos = rl.Vector2(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16)
|
||||
self._large_version_label.set_text(self._version_text[0])
|
||||
self._large_version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._large_version_label.render()
|
||||
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
else:
|
||||
version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44)
|
||||
self._version_label.set_text(self._version_text[0])
|
||||
self._version_label.set_position(version_pos.x, version_pos.y)
|
||||
self._version_label.render()
|
||||
|
||||
self._branch_label.set_max_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1]))
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
self._branch_label.render()
|
||||
self._date_label.set_text(" " + self._version_text[3])
|
||||
self._date_label.set_position(version_pos.x + self._version_label.rect.width + 10, version_pos.y)
|
||||
self._date_label.render()
|
||||
|
||||
self._branch_label.set_width(gui_app.width - self._version_label.rect.width - self._date_label.rect.width - 32)
|
||||
self._branch_label.set_text(" " + self._version_text[1])
|
||||
self._branch_label.set_position(version_pos.x + self._version_label.rect.width + self._date_label.rect.width + 20, version_pos.y)
|
||||
self._branch_label.render()
|
||||
|
||||
if not release_branch:
|
||||
# 2nd line
|
||||
self._version_commit_label.set_text(self._version_text[2])
|
||||
self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7)
|
||||
|
||||
@@ -5,7 +5,6 @@ from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
@@ -221,7 +220,6 @@ class MiciOffroadAlerts(Widget):
|
||||
update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1)
|
||||
self.sorted_alerts.append(update_alert_data)
|
||||
update_alert_item = AlertItem(update_alert_data)
|
||||
update_alert_item.set_click_callback(lambda: HARDWARE.reboot())
|
||||
self.alert_items.append(update_alert_item)
|
||||
self._scroller.add_widget(update_alert_item)
|
||||
|
||||
@@ -246,18 +244,18 @@ class MiciOffroadAlerts(Widget):
|
||||
|
||||
if update_alert_data:
|
||||
if update_available:
|
||||
version_string = ""
|
||||
# Default text
|
||||
update_alert_data.text = "update available. go to comma.ai/blog to read the release notes."
|
||||
|
||||
# Get new version description and parse version and date
|
||||
new_desc = self.params.get("UpdaterNewDescription") or ""
|
||||
if new_desc:
|
||||
# format: "version / branch / commit / date"
|
||||
# Parse description (format: "version / branch / commit / date")
|
||||
parts = new_desc.split(" / ")
|
||||
if len(parts) > 3:
|
||||
version, date = parts[0], parts[3]
|
||||
version_string = f"\nsunnypilot {version}, {date}\n"
|
||||
update_alert_data.text = f"update available\n sunnypilot {version}, {date}. go to comma.ai/blog to read the release notes."
|
||||
|
||||
update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai."
|
||||
update_alert_data.visible = True
|
||||
active_count += 1
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
import weakref
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -93,10 +92,11 @@ class TrainingGuideDMTutorial(Widget):
|
||||
super().__init__()
|
||||
self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._original_continue_callback = continue_callback
|
||||
|
||||
# Wrap the continue callback to restore settings
|
||||
def wrapped_continue_callback():
|
||||
device.set_offroad_brightness(None)
|
||||
device.reset_interactive_timeout()
|
||||
self._restore_settings()
|
||||
continue_callback()
|
||||
|
||||
self._dialog = DriverCameraSetupDialog(wrapped_continue_callback)
|
||||
@@ -114,6 +114,10 @@ class TrainingGuideDMTutorial(Widget):
|
||||
device.set_offroad_brightness(100)
|
||||
device.reset_interactive_timeout(300) # 5 minutes
|
||||
|
||||
def _restore_settings(self):
|
||||
device.set_offroad_brightness(None)
|
||||
device.reset_interactive_timeout()
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
if device.awake:
|
||||
@@ -146,7 +150,7 @@ class TrainingGuideRecordFront(SetupTermsPage):
|
||||
super().__init__(on_continue, back_callback=on_back, back_text="no", continue_text="yes")
|
||||
self._title_header = TermsHeader("improve driver monitoring", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
||||
|
||||
self._dm_label = UnifiedLabel("Do you want to upload driver camera data?", 42,
|
||||
self._dm_label = UnifiedLabel("Do you want to upload driver camera data to improve driver monitoring?", 42,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
def show_event(self):
|
||||
@@ -210,17 +214,11 @@ class TrainingGuide(Widget):
|
||||
self._completed_callback = completed_callback
|
||||
self._step = 0
|
||||
|
||||
self_ref = weakref.ref(self)
|
||||
|
||||
def on_continue():
|
||||
if obj := self_ref():
|
||||
obj._advance_step()
|
||||
|
||||
self._steps = [
|
||||
TrainingGuideAttentionNotice(continue_callback=on_continue),
|
||||
TrainingGuidePreDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideDMTutorial(continue_callback=on_continue),
|
||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||
TrainingGuideAttentionNotice(continue_callback=self._advance_step),
|
||||
TrainingGuidePreDMTutorial(continue_callback=self._advance_step),
|
||||
TrainingGuideDMTutorial(continue_callback=self._advance_step),
|
||||
TrainingGuideRecordFront(continue_callback=self._advance_step),
|
||||
]
|
||||
|
||||
def _advance_step(self):
|
||||
|
||||
@@ -39,7 +39,7 @@ class MiciFccModal(NavWidget):
|
||||
content_height += self._fcc_logo.height + 20
|
||||
|
||||
scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height))
|
||||
scroll_offset = self._scroll_panel.update(rect, scroll_content_rect.height)
|
||||
|
||||
fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset)
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ from openpilot.common.api import api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.api_helpers import get_token
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
|
||||
|
||||
TITLE = tr_noop("Firehose Mode")
|
||||
DESCRIPTION = tr_noop(
|
||||
@@ -33,7 +34,9 @@ FAQ_ITEMS = [
|
||||
]
|
||||
|
||||
|
||||
class FirehoseLayoutBase(Widget):
|
||||
class FirehoseLayoutMici(NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
PARAM_KEY = "ApiCache_FirehoseStats"
|
||||
GREEN = rl.Color(46, 204, 113, 255)
|
||||
RED = rl.Color(231, 76, 60, 255)
|
||||
@@ -41,10 +44,12 @@ class FirehoseLayoutBase(Widget):
|
||||
LIGHT_GRAY = rl.Color(228, 228, 228, 255)
|
||||
UPDATE_INTERVAL = 30 # seconds
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, back_callback):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._segment_count = self._get_segment_count()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
self.params = Params()
|
||||
self.segment_count = self._get_segment_count()
|
||||
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._content_height = 0
|
||||
@@ -66,7 +71,7 @@ class FirehoseLayoutBase(Widget):
|
||||
self._scroll_panel.set_offset(0)
|
||||
|
||||
def _get_segment_count(self) -> int:
|
||||
stats = self._params.get(self.PARAM_KEY)
|
||||
stats = self.params.get(self.PARAM_KEY)
|
||||
if not stats:
|
||||
return 0
|
||||
try:
|
||||
@@ -78,7 +83,7 @@ class FirehoseLayoutBase(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# compute total content height for scrolling
|
||||
content_height = self._measure_content_height(rect)
|
||||
scroll_offset = round(self._scroll_panel.update(rect, content_height))
|
||||
scroll_offset = self._scroll_panel.update(rect, content_height)
|
||||
|
||||
# start drawing with offset
|
||||
x = int(rect.x + 40)
|
||||
@@ -106,9 +111,9 @@ class FirehoseLayoutBase(Widget):
|
||||
y += 20
|
||||
|
||||
# Contribution count (if available)
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
|
||||
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 42, rl.WHITE)
|
||||
y += 20
|
||||
|
||||
@@ -160,9 +165,9 @@ class FirehoseLayoutBase(Widget):
|
||||
y += int(len(status_lines) * 48 * FONT_SCALE) + 20
|
||||
|
||||
# Contribution count
|
||||
if self._segment_count > 0:
|
||||
if self.segment_count > 0:
|
||||
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
|
||||
"{} segments of your driving is in the training dataset so far.", self.segment_count).format(self.segment_count)
|
||||
contrib_lines = wrap_text(gui_app.font(FontWeight.BOLD), contrib_text, 42, w)
|
||||
y += int(len(contrib_lines) * 42 * FONT_SCALE) + 20
|
||||
|
||||
@@ -199,28 +204,20 @@ class FirehoseLayoutBase(Widget):
|
||||
|
||||
def _fetch_firehose_stats(self):
|
||||
try:
|
||||
dongle_id = self._params.get("DongleId")
|
||||
dongle_id = self.params.get("DongleId")
|
||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||
return
|
||||
identity_token = get_token(dongle_id)
|
||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
self._segment_count = data.get("firehose", 0)
|
||||
self._params.put(self.PARAM_KEY, data)
|
||||
self.segment_count = data.get("firehose", 0)
|
||||
self.params.put(self.PARAM_KEY, data)
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch firehose stats: {e}")
|
||||
|
||||
def _update_loop(self):
|
||||
while self._running:
|
||||
if not ui_state.started and device._awake:
|
||||
if not ui_state.started:
|
||||
self._fetch_firehose_stats()
|
||||
time.sleep(self.UPDATE_INTERVAL)
|
||||
|
||||
|
||||
class FirehoseLayout(FirehoseLayoutBase, NavWidget):
|
||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||
|
||||
def __init__(self, back_callback):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import math
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigMultiOptionDialog, BigInputDialog, BigDialogOptionButton, BigConfirmationDialogV2
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, MeteredType
|
||||
|
||||
|
||||
def normalize_ssid(ssid: str) -> str:
|
||||
return ssid.replace("’", "'") # for iPhone hotspots
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
|
||||
|
||||
class LoadingAnimation(Widget):
|
||||
def _render(self, _):
|
||||
cx = int(self._rect.x + 70)
|
||||
@@ -83,13 +91,11 @@ class WifiIcon(Widget):
|
||||
|
||||
class WifiItem(BigDialogOptionButton):
|
||||
LEFT_MARGIN = 20
|
||||
HEIGHT = 54
|
||||
SELECTED_HEIGHT = 74
|
||||
|
||||
def __init__(self, network: Network):
|
||||
super().__init__(network.ssid)
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, self.HEIGHT))
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, 64))
|
||||
|
||||
self._selected_txt = gui_app.texture("icons_mici/settings/network/new/wifi_selected.png", 48, 96)
|
||||
|
||||
@@ -97,10 +103,6 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._wifi_icon = WifiIcon()
|
||||
self._wifi_icon.set_current_network(network)
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
super().set_selected(selected)
|
||||
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
|
||||
|
||||
def set_current_network(self, network: Network):
|
||||
self._network = network
|
||||
self._wifi_icon.set_current_network(network)
|
||||
@@ -115,7 +117,7 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._wifi_icon.render(rl.Rectangle(
|
||||
self._rect.x + self.LEFT_MARGIN,
|
||||
self._rect.y,
|
||||
self.SELECTED_HEIGHT,
|
||||
self._rect.height,
|
||||
self._rect.height
|
||||
))
|
||||
|
||||
@@ -124,7 +126,7 @@ class WifiItem(BigDialogOptionButton):
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY)
|
||||
else:
|
||||
self._label.set_font_size(54)
|
||||
self._label.set_font_size(70)
|
||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
||||
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||
|
||||
@@ -213,7 +215,7 @@ class NetworkInfoPage(NavWidget):
|
||||
self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None)
|
||||
|
||||
self._title = UnifiedLabel("", 64, FontWeight.DISPLAY, rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True)
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
@@ -223,10 +225,6 @@ class NetworkInfoPage(NavWidget):
|
||||
self._network: Network | None = None
|
||||
self._connecting: Callable[[], str | None] | None = None
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._title.reset_scroll()
|
||||
|
||||
def update_networks(self, networks: dict[str, Network]):
|
||||
# update current network from latest scan results
|
||||
for ssid, network in networks.items():
|
||||
@@ -394,7 +392,7 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
self._network_info_page.set_current_network(_network)
|
||||
self._should_open_network_info_page = True
|
||||
|
||||
network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net))
|
||||
network_button.set_click_callback(lambda _net=network,_button=network_button: _button._selected and show_network_info_page(_net))
|
||||
|
||||
self.add_button(network_button)
|
||||
|
||||
@@ -445,3 +443,116 @@ class WifiUIMici(BigMultiOptionDialog):
|
||||
|
||||
if not self._networks:
|
||||
self._loading_animation.render(self._rect)
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
_tethering_icon = "icons_mici/settings/network/tethering.png"
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._wifi_ui.show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_ui.hide_event()
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._current_panel == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
@@ -1,182 +0,0 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType
|
||||
|
||||
|
||||
class NetworkPanelType(IntEnum):
|
||||
NONE = 0
|
||||
WIFI = 1
|
||||
|
||||
|
||||
class NetworkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
|
||||
self._current_panel = NetworkPanelType.WIFI
|
||||
self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE)
|
||||
|
||||
self._wifi_manager = WifiManager()
|
||||
self._wifi_manager.set_active(False)
|
||||
self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE))
|
||||
|
||||
self._wifi_manager.add_callbacks(
|
||||
networks_updated=self._on_network_updated,
|
||||
)
|
||||
|
||||
_tethering_icon = "icons_mici/settings/network/tethering.png"
|
||||
|
||||
# ******** Tethering ********
|
||||
def tethering_toggle_callback(checked: bool):
|
||||
self._tethering_toggle_btn.set_enabled(False)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
self._wifi_manager.set_tethering_active(checked)
|
||||
|
||||
self._tethering_toggle_btn = BigToggle("enable tethering", "", toggle_callback=tethering_toggle_callback)
|
||||
|
||||
def tethering_password_callback(password: str):
|
||||
if password:
|
||||
self._wifi_manager.set_tethering_password(password)
|
||||
|
||||
def tethering_password_clicked():
|
||||
tethering_password = self._wifi_manager.tethering_password
|
||||
dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8,
|
||||
confirm_callback=tethering_password_callback)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
txt_tethering = gui_app.texture(_tethering_icon, 64, 53)
|
||||
self._tethering_password_btn = BigButton("tethering password", "", txt_tethering)
|
||||
self._tethering_password_btn.set_click_callback(tethering_password_clicked)
|
||||
|
||||
# ******** IP Address ********
|
||||
self._ip_address_btn = BigButton("IP Address", "Not connected")
|
||||
|
||||
# ******** Network Metered ********
|
||||
def network_metered_callback(value: str):
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
metered = {
|
||||
'default': MeteredType.UNKNOWN,
|
||||
'metered': MeteredType.YES,
|
||||
'unmetered': MeteredType.NO
|
||||
}.get(value, MeteredType.UNKNOWN)
|
||||
self._wifi_manager.set_current_network_metered(metered)
|
||||
|
||||
# TODO: signal for current network metered type when changing networks, this is wrong until you press it once
|
||||
# TODO: disable when not connected
|
||||
self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback)
|
||||
self._network_metered_btn.set_enabled(False)
|
||||
|
||||
wifi_button = BigButton("wi-fi")
|
||||
wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI))
|
||||
|
||||
# ******** Advanced settings ********
|
||||
# ******** Roaming toggle ********
|
||||
self._roaming_btn = BigParamControl("enable roaming", "GsmRoaming", toggle_callback=self._toggle_roaming)
|
||||
|
||||
# ******** APN settings ********
|
||||
self._apn_btn = BigButton("apn settings", "edit")
|
||||
self._apn_btn.set_click_callback(self._edit_apn)
|
||||
|
||||
# ******** Cellular metered toggle ********
|
||||
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
|
||||
|
||||
# Main scroller ----------------------------------
|
||||
self._scroller = Scroller([
|
||||
wifi_button,
|
||||
self._network_metered_btn,
|
||||
self._tethering_toggle_btn,
|
||||
self._tethering_password_btn,
|
||||
# /* Advanced settings
|
||||
self._roaming_btn,
|
||||
self._apn_btn,
|
||||
self._cellular_metered_btn,
|
||||
# */
|
||||
self._ip_address_btn,
|
||||
], snap_items=False)
|
||||
|
||||
# Set initial config
|
||||
roaming_enabled = ui_state.params.get_bool("GsmRoaming")
|
||||
metered = ui_state.params.get_bool("GsmMetered")
|
||||
self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered)
|
||||
|
||||
# Set up back navigation
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# If not using prime SIM, show GSM settings and enable IPv4 forwarding
|
||||
show_cell_settings = ui_state.prime_state.get_type() in (PrimeType.NONE, PrimeType.LITE)
|
||||
self._wifi_manager.set_ipv4_forward(show_cell_settings)
|
||||
self._roaming_btn.set_visible(show_cell_settings)
|
||||
self._apn_btn.set_visible(show_cell_settings)
|
||||
self._cellular_metered_btn.set_visible(show_cell_settings)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._current_panel = NetworkPanelType.NONE
|
||||
self._wifi_ui.show_event()
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
self._wifi_ui.hide_event()
|
||||
|
||||
def _toggle_roaming(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
def _edit_apn(self):
|
||||
def update_apn(apn: str):
|
||||
apn = apn.strip()
|
||||
if apn == "":
|
||||
ui_state.params.remove("GsmApn")
|
||||
else:
|
||||
ui_state.params.put("GsmApn", apn)
|
||||
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), apn, ui_state.params.get_bool("GsmMetered"))
|
||||
|
||||
current_apn = ui_state.params.get("GsmApn") or ""
|
||||
dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _toggle_cellular_metered(self, checked: bool):
|
||||
self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked)
|
||||
|
||||
def _on_network_updated(self, networks: list[Network]):
|
||||
# Update tethering state
|
||||
tethering_active = self._wifi_manager.is_tethering_active()
|
||||
# TODO: use real signals (like activated/settings changed, etc.) to speed up re-enabling buttons
|
||||
self._tethering_toggle_btn.set_enabled(True)
|
||||
self._network_metered_btn.set_enabled(lambda: not tethering_active and bool(self._wifi_manager.ipv4_address))
|
||||
self._tethering_toggle_btn.set_checked(tethering_active)
|
||||
|
||||
# Update IP address
|
||||
self._ip_address_btn.set_value(self._wifi_manager.ipv4_address or "Not connected")
|
||||
|
||||
# Update network metered
|
||||
self._network_metered_btn.set_value(
|
||||
{
|
||||
MeteredType.UNKNOWN: 'default',
|
||||
MeteredType.YES: 'metered',
|
||||
MeteredType.NO: 'unmetered'
|
||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||
|
||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||
self._current_panel = panel_type
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._wifi_manager.process_callbacks()
|
||||
|
||||
if self._current_panel == NetworkPanelType.WIFI:
|
||||
self._wifi_ui.render(rect)
|
||||
else:
|
||||
self._scroller.render(rect)
|
||||
@@ -10,7 +10,7 @@ from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMi
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici, PairBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutMici
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget, NavWidget
|
||||
|
||||
@@ -67,7 +67,7 @@ class SettingsLayout(NavWidget):
|
||||
PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))),
|
||||
PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
}
|
||||
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
@@ -89,6 +89,10 @@ ALERT_CRITICAL_REBOOT = Alert(
|
||||
class AlertRenderer(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.font_regular: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
self.font_roman: rl.Font = gui_app.font(FontWeight.ROMAN)
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self.font_display: rl.Font = gui_app.font(FontWeight.DISPLAY)
|
||||
|
||||
self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86,
|
||||
letter_spacing=-0.02)
|
||||
@@ -200,11 +204,11 @@ class AlertRenderer(Widget):
|
||||
text_x = self._rect.x + ALERT_MARGIN
|
||||
text_width = self._rect.width - ALERT_MARGIN
|
||||
if icon_side == 'left':
|
||||
text_x = self._rect.x + self._txt_turn_signal_right.width
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
|
||||
text_x = self._rect.x + self._txt_turn_signal_right.width + 20 * 2
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
|
||||
elif icon_side == 'right':
|
||||
text_x = self._rect.x + ALERT_MARGIN
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width
|
||||
text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - 20 * 2
|
||||
|
||||
text_rect = rl.Rectangle(
|
||||
text_x,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from cereal import messaging, car, log
|
||||
from cereal import car, log
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||
@@ -161,9 +160,6 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
|
||||
# debug
|
||||
self._pm = messaging.PubMaster(['uiDebug'])
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
"""Check if currently swiping left (for scroller to disable)."""
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
@@ -183,7 +179,6 @@ class AugmentedRoadView(CameraView):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _render(self, _):
|
||||
start_draw = time.monotonic()
|
||||
self._switch_stream_if_needed(ui_state.sm)
|
||||
|
||||
# Update calibration before rendering
|
||||
@@ -249,11 +244,6 @@ class AugmentedRoadView(CameraView):
|
||||
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
|
||||
self._offroad_label.render(self._content_rect)
|
||||
|
||||
# publish uiDebug
|
||||
msg = messaging.new_message('uiDebug')
|
||||
msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000
|
||||
self._pm.send('uiDebug', msg)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
|
||||
@@ -107,6 +107,7 @@ else:
|
||||
class CameraView(Widget):
|
||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||
super().__init__()
|
||||
# TODO: implement a receiver and connect thread
|
||||
self._name = name
|
||||
# Primary stream
|
||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||
@@ -196,10 +197,7 @@ class CameraView(Widget):
|
||||
# Clean up shader
|
||||
if self.shader and self.shader.id:
|
||||
rl.unload_shader(self.shader)
|
||||
self.shader.id = 0
|
||||
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
self.client = None
|
||||
|
||||
def __del__(self):
|
||||
@@ -236,9 +234,6 @@ class CameraView(Widget):
|
||||
if buffer:
|
||||
self._texture_needs_update = True
|
||||
self.frame = buffer
|
||||
elif not self.client.is_connected():
|
||||
# ensure we clear the displayed frame when the connection is lost
|
||||
self.frame = None
|
||||
|
||||
if not self.frame:
|
||||
self._draw_placeholder(rect)
|
||||
|
||||
@@ -15,27 +15,20 @@ EventName = log.OnroadEvent.EventName
|
||||
EVENT_TO_INT = EventName.schema.enumerants
|
||||
|
||||
|
||||
class DriverCameraView(CameraView):
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle):
|
||||
base = super()._calc_frame_matrix(rect)
|
||||
driver_view_ratio = 1.5
|
||||
base[0, 0] *= driver_view_ratio
|
||||
base[1, 1] *= driver_view_ratio
|
||||
return base
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget):
|
||||
def __init__(self, no_escape=False):
|
||||
super().__init__()
|
||||
self._camera_view = DriverCameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self._camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||
self._original_calc_frame_matrix = self._camera_view._calc_frame_matrix
|
||||
self._camera_view._calc_frame_matrix = self._calc_driver_frame_matrix
|
||||
self.driver_state_renderer = DriverStateRenderer(lines=True)
|
||||
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200))
|
||||
self.driver_state_renderer.load_icons()
|
||||
self._pm: messaging.PubMaster | None = None
|
||||
self._pm = messaging.PubMaster(['selfdriveState'])
|
||||
if not no_escape:
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
self.set_back_callback(lambda: gui_app.set_modal_overlay(None))
|
||||
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
|
||||
self.set_back_callback(self._dismiss)
|
||||
self.set_back_enabled(not no_escape)
|
||||
|
||||
# Load eye icons
|
||||
@@ -47,24 +40,26 @@ class DriverCameraDialog(NavWidget):
|
||||
|
||||
self._load_eye_textures()
|
||||
|
||||
def stop_dmonitoringmodeld(self):
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
self._publish_alert_sound(None)
|
||||
device.reset_interactive_timeout(300)
|
||||
ui_state.params.remove("DriverTooDistracted")
|
||||
self._pm = messaging.PubMaster(['selfdriveState'])
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
device.reset_interactive_timeout()
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
ui_state.params.remove("DriverTooDistracted")
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
def _dismiss(self):
|
||||
self.stop_dmonitoringmodeld()
|
||||
|
||||
def close(self):
|
||||
if self._camera_view:
|
||||
@@ -108,9 +103,6 @@ class DriverCameraDialog(NavWidget):
|
||||
|
||||
def _publish_alert_sound(self, dm_state):
|
||||
"""Publish selfdriveState with only alertSound field set"""
|
||||
if self._pm is None:
|
||||
return
|
||||
|
||||
msg = messaging.new_message('selfdriveState')
|
||||
if dm_state is not None and len(dm_state.events):
|
||||
event_name = EVENT_TO_INT[dm_state.events[0].name]
|
||||
@@ -229,6 +221,13 @@ class DriverCameraDialog(NavWidget):
|
||||
glasses_prob = driver_data.sunglassesProb
|
||||
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
||||
|
||||
def _calc_driver_frame_matrix(self, rect: rl.Rectangle):
|
||||
base = self._original_calc_frame_matrix(rect)
|
||||
driver_view_ratio = 1.5
|
||||
base[0, 0] *= driver_view_ratio
|
||||
base[1, 1] *= driver_view_ratio
|
||||
return base
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("Driver Camera View (mici)")
|
||||
|
||||
@@ -3,7 +3,6 @@ from collections.abc import Callable
|
||||
import numpy as np
|
||||
import math
|
||||
from cereal import log
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -218,10 +217,7 @@ class DriverStateRenderer(Widget):
|
||||
rotation = math.degrees(math.atan2(pitch, yaw))
|
||||
angle_diff = rotation - self._rotation_filter.x
|
||||
angle_diff = ((angle_diff + 180) % 360) - 180
|
||||
if PC and self._confirm_mode:
|
||||
self._rotation_filter.x += 2
|
||||
else:
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
|
||||
if not self.should_draw:
|
||||
self._fade_filter.update(0.0)
|
||||
|
||||
@@ -30,8 +30,20 @@ class FontSizes:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
|
||||
white: rl.Color = rl.WHITE
|
||||
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
|
||||
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
|
||||
engaged: rl.Color = rl.Color(128, 216, 166, 255)
|
||||
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
|
||||
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
|
||||
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
|
||||
grey: rl.Color = rl.Color(166, 166, 166, 255)
|
||||
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
|
||||
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
|
||||
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
|
||||
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
|
||||
header_gradient_end: rl.Color = rl.BLANK
|
||||
|
||||
|
||||
FONT_SIZES = FontSizes()
|
||||
@@ -224,18 +236,16 @@ class HudRenderer(Widget):
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
|
||||
self._can_draw_top_icons and self._engaged)
|
||||
if alpha < 1e-2:
|
||||
return
|
||||
|
||||
x = rect.x
|
||||
y = rect.y
|
||||
|
||||
alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and
|
||||
self._can_draw_top_icons and self._engaged)
|
||||
|
||||
# draw drop shadow
|
||||
circle_radius = 162 // 2
|
||||
rl.draw_circle_gradient(int(x + circle_radius), int(y + circle_radius), circle_radius,
|
||||
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK)
|
||||
rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.Color(0, 0, 0, 0))
|
||||
|
||||
set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
||||
max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
||||
@@ -269,9 +279,9 @@ class HudRenderer(Widget):
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
@@ -130,9 +130,6 @@ def arc_bar_pts(cx: float, cy: float,
|
||||
|
||||
pts = np.vstack((outer, cap_end, inner, cap_start, outer[:1])).astype(np.float32)
|
||||
|
||||
# Rotate to start from middle of cap for proper triangulation
|
||||
pts = np.roll(pts, cap_segs, axis=0)
|
||||
|
||||
if DEBUG:
|
||||
n = len(pts)
|
||||
idx = int(time.monotonic() * 12) % max(1, n) # speed: 12 pts/sec
|
||||
|
||||
@@ -282,8 +282,7 @@ class BigDialogOptionButton(Widget):
|
||||
self._selected = False
|
||||
|
||||
self._label = UnifiedLabel(option, font_size=70, text_color=rl.Color(255, 255, 255, int(255 * 0.58)),
|
||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
scroll=True)
|
||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
self._selected = selected
|
||||
|
||||
@@ -68,6 +68,7 @@ else:
|
||||
class CameraView(Widget):
|
||||
def __init__(self, name: str, stream_type: VisionStreamType):
|
||||
super().__init__()
|
||||
# TODO: implement a receiver and connect thread
|
||||
self._name = name
|
||||
# Primary stream
|
||||
self.client = VisionIpcClient(name, stream_type, conflate=True)
|
||||
@@ -336,12 +337,12 @@ class CameraView(Widget):
|
||||
self._initialize_textures()
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
self._clear_textures()
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
|
||||
|
||||
def _clear_textures(self):
|
||||
if self.texture_y and self.texture_y.id:
|
||||
|
||||
@@ -35,20 +35,20 @@ class FontSizes:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
DISENGAGED = rl.Color(145, 155, 149, 255)
|
||||
OVERRIDE = rl.Color(145, 155, 149, 255) # Added
|
||||
ENGAGED = rl.Color(128, 216, 166, 255)
|
||||
DISENGAGED_BG = rl.Color(0, 0, 0, 153)
|
||||
OVERRIDE_BG = rl.Color(145, 155, 149, 204)
|
||||
ENGAGED_BG = rl.Color(128, 216, 166, 204)
|
||||
GREY = rl.Color(166, 166, 166, 255)
|
||||
DARK_GREY = rl.Color(114, 114, 114, 255)
|
||||
BLACK_TRANSLUCENT = rl.Color(0, 0, 0, 166)
|
||||
WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200)
|
||||
BORDER_TRANSLUCENT = rl.Color(255, 255, 255, 75)
|
||||
HEADER_GRADIENT_START = rl.Color(0, 0, 0, 114)
|
||||
HEADER_GRADIENT_END = rl.BLANK
|
||||
white: rl.Color = rl.WHITE
|
||||
disengaged: rl.Color = rl.Color(145, 155, 149, 255)
|
||||
override: rl.Color = rl.Color(145, 155, 149, 255) # Added
|
||||
engaged: rl.Color = rl.Color(128, 216, 166, 255)
|
||||
disengaged_bg: rl.Color = rl.Color(0, 0, 0, 153)
|
||||
override_bg: rl.Color = rl.Color(145, 155, 149, 204)
|
||||
engaged_bg: rl.Color = rl.Color(128, 216, 166, 204)
|
||||
grey: rl.Color = rl.Color(166, 166, 166, 255)
|
||||
dark_grey: rl.Color = rl.Color(114, 114, 114, 255)
|
||||
black_translucent: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
white_translucent: rl.Color = rl.Color(255, 255, 255, 200)
|
||||
border_translucent: rl.Color = rl.Color(255, 255, 255, 75)
|
||||
header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114)
|
||||
header_gradient_end: rl.Color = rl.BLANK
|
||||
|
||||
|
||||
UI_CONFIG = UIConfig()
|
||||
@@ -108,8 +108,8 @@ class HudRenderer(Widget):
|
||||
int(rect.y),
|
||||
int(rect.width),
|
||||
UI_CONFIG.header_height,
|
||||
COLORS.HEADER_GRADIENT_START,
|
||||
COLORS.HEADER_GRADIENT_END,
|
||||
COLORS.header_gradient_start,
|
||||
COLORS.header_gradient_end,
|
||||
)
|
||||
|
||||
if self.is_cruise_available:
|
||||
@@ -131,19 +131,19 @@ class HudRenderer(Widget):
|
||||
y = rect.y + 45
|
||||
|
||||
set_speed_rect = rl.Rectangle(x, y, set_speed_width, UI_CONFIG.set_speed_height)
|
||||
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.BLACK_TRANSLUCENT)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.BORDER_TRANSLUCENT)
|
||||
rl.draw_rectangle_rounded(set_speed_rect, 0.35, 10, COLORS.black_translucent)
|
||||
rl.draw_rectangle_rounded_lines_ex(set_speed_rect, 0.35, 10, 6, COLORS.border_translucent)
|
||||
|
||||
max_color = COLORS.GREY
|
||||
set_speed_color = COLORS.DARK_GREY
|
||||
max_color = COLORS.grey
|
||||
set_speed_color = COLORS.dark_grey
|
||||
if self.is_cruise_set:
|
||||
set_speed_color = COLORS.WHITE
|
||||
set_speed_color = COLORS.white
|
||||
if ui_state.status == UIStatus.ENGAGED:
|
||||
max_color = COLORS.ENGAGED
|
||||
max_color = COLORS.engaged
|
||||
elif ui_state.status == UIStatus.DISENGAGED:
|
||||
max_color = COLORS.DISENGAGED
|
||||
max_color = COLORS.disengaged
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
max_color = COLORS.OVERRIDE
|
||||
max_color = COLORS.override
|
||||
|
||||
max_text = tr("MAX")
|
||||
max_text_width = measure_text_cached(self._font_semi_bold, max_text, FONT_SIZES.max_speed).x
|
||||
@@ -172,9 +172,9 @@ class HudRenderer(Widget):
|
||||
speed_text = str(round(self.speed))
|
||||
speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed)
|
||||
speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.white)
|
||||
|
||||
unit_text = tr("km/h") if ui_state.is_metric else tr("mph")
|
||||
unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit)
|
||||
unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT)
|
||||
rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.white_translucent)
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
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.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import button_item
|
||||
|
||||
from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt"
|
||||
|
||||
|
||||
class DeveloperLayoutSP(DeveloperLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.error_log_path = os.path.join(Paths.crash_log_root(), "error.log")
|
||||
self._is_release_branch: bool = self._is_release or ui_state.params.get_bool("IsReleaseSpBranch")
|
||||
self._is_development_branch: bool = ui_state.params.get_bool("IsTestedBranch") or ui_state.params.get_bool("IsDevelopmentBranch")
|
||||
self._initialize_items()
|
||||
|
||||
for item in self.items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
def _initialize_items(self):
|
||||
self.show_advanced_controls = toggle_item_sp(tr("Show Advanced Controls"),
|
||||
tr("Toggle visibility of advanced sunnypilot controls.<br>This only changes the visibility of the toggles; " +
|
||||
"it does not change the actual enabled/disabled state."), param="ShowAdvancedControls")
|
||||
|
||||
self.enable_github_runner_toggle = toggle_item_sp(tr("GitHub Runner Service"), tr("Enables or disables the GitHub runner service."),
|
||||
param="EnableGithubRunner")
|
||||
|
||||
self.enable_copyparty_toggle = toggle_item_sp(tr("copyparty Service"),
|
||||
tr("copyparty is a very capable file server, you can use it to download your routes, view your logs " +
|
||||
"and even make some edits on some files from your browser. " +
|
||||
"Requires you to connect to your comma locally via its IP address."), param="EnableCopyparty")
|
||||
|
||||
self.prebuilt_toggle = toggle_item_sp(tr("Quickboot Mode"), "", param="QuickBootToggle", callback=self._on_prebuilt_toggled)
|
||||
|
||||
self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked)
|
||||
|
||||
self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle, self.error_log_btn,]
|
||||
|
||||
@staticmethod
|
||||
def _on_prebuilt_toggled(state):
|
||||
if state:
|
||||
Path(PREBUILT_PATH).touch(exist_ok=True)
|
||||
else:
|
||||
os.remove(PREBUILT_PATH)
|
||||
ui_state.params.put_bool("QuickBootToggle", state)
|
||||
|
||||
def _on_delete_confirm(self, result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
if os.path.exists(self.error_log_path):
|
||||
os.remove(self.error_log_path)
|
||||
|
||||
def _on_error_log_closed(self, result, log_exists):
|
||||
if result == DialogResult.CONFIRM and log_exists:
|
||||
dialog2 = ConfirmDialog(tr("Would you like to delete this log?"), tr("Yes"), tr("No"), rich=False)
|
||||
gui_app.set_modal_overlay(dialog2, callback=self._on_delete_confirm)
|
||||
|
||||
def _on_error_log_clicked(self):
|
||||
text = ""
|
||||
if os.path.exists(self.error_log_path):
|
||||
text = f"<b>{datetime.datetime.fromtimestamp(os.path.getmtime(self.error_log_path)).strftime('%d-%b-%Y %H:%M:%S').upper()}</b><br><br>"
|
||||
try:
|
||||
with open(self.error_log_path) as file:
|
||||
text += file.read()
|
||||
except Exception:
|
||||
pass
|
||||
dialog = HtmlModalSP(text=text, callback=lambda result: self._on_error_log_closed(result, os.path.exists(self.error_log_path)))
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
|
||||
def _update_state(self):
|
||||
disable_updates = ui_state.params.get_bool("DisableUpdates")
|
||||
show_advanced = ui_state.params.get_bool("ShowAdvancedControls")
|
||||
|
||||
if (prebuilt_file := os.path.exists(PREBUILT_PATH)) != ui_state.params.get_bool("QuickBootToggle"):
|
||||
ui_state.params.put_bool("QuickBootToggle", prebuilt_file)
|
||||
self.prebuilt_toggle.action_item.set_state(prebuilt_file)
|
||||
|
||||
self.prebuilt_toggle.set_visible(show_advanced and not (self._is_release_branch or self._is_development_branch))
|
||||
self.prebuilt_toggle.action_item.set_enabled(disable_updates)
|
||||
|
||||
if disable_updates:
|
||||
self.prebuilt_toggle.set_description(tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it " +
|
||||
"removes the prebuilt file so compilation of locally edited cpp files can be made."))
|
||||
else:
|
||||
self.prebuilt_toggle.set_description(tr("Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first."))
|
||||
|
||||
self.enable_copyparty_toggle.set_visible(show_advanced)
|
||||
self.enable_github_runner_toggle.set_visible(show_advanced and not self._is_release_branch)
|
||||
self.error_log_btn.set_visible(not self._is_release_branch)
|
||||
@@ -21,8 +21,7 @@ from openpilot.system.ui.widgets.toggle import ON_COLOR
|
||||
|
||||
from openpilot.sunnypilot.models.runners.constants import CUSTOM_MODEL_PATH
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP, toggle_item_sp, option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP, ListItemSP, toggle_item_sp, option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeOptionDialog, TreeNode, TreeFolder
|
||||
|
||||
@@ -30,6 +29,11 @@ if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
|
||||
|
||||
class ModelAction(ButtonActionSP):
|
||||
def get_width_hint(self):
|
||||
return super().get_width_hint() + 1
|
||||
|
||||
|
||||
class ModelsLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -51,7 +55,7 @@ class ModelsLayout(Widget):
|
||||
self.current_model_item = ListItemSP(
|
||||
title=tr("Current Model"),
|
||||
description="",
|
||||
action_item=NoElideButtonAction(tr("SELECT")),
|
||||
action_item=ModelAction(tr("SELECT")),
|
||||
callback=self._handle_current_model_clicked
|
||||
)
|
||||
|
||||
@@ -66,7 +70,7 @@ class ModelsLayout(Widget):
|
||||
self.clear_cache_item = ListItemSP(
|
||||
title=tr("Clear Model Cache"),
|
||||
description="",
|
||||
action_item=NoElideButtonAction(tr("CLEAR")),
|
||||
action_item=ModelAction(tr("CLEAR")),
|
||||
callback=self._clear_cache
|
||||
)
|
||||
|
||||
@@ -226,9 +230,7 @@ class ModelsLayout(Widget):
|
||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||
|
||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||
self.lagd_toggle.action_item.set_state(live_delay)
|
||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||
if self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||
|
||||
@@ -4,229 +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 datetime
|
||||
import os
|
||||
import platform
|
||||
import requests
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||
from openpilot.selfdrive.ui.layouts.settings.software import time_ago
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import text_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.utils import NoElideButtonAction
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP
|
||||
from openpilot.system.ui.sunnypilot.widgets.tree_dialog import TreeFolder, TreeNode, TreeOptionDialog
|
||||
from openpilot.system.ui.sunnypilot.widgets.progress_bar import progress_item
|
||||
|
||||
MAP_PATH = Path(Paths.mapd_root()) / "offline"
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
|
||||
class OSMLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._current_percent = 0
|
||||
self._last_map_size_update = 0
|
||||
self._mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else ui_state.params
|
||||
self._initialize_items()
|
||||
self._update_map_size()
|
||||
self._progress.set_visible(False)
|
||||
self._state_btn.set_visible(False)
|
||||
self._mapd_version.action_item.set_text(ui_state.params.get("MapdVersion") or "Loading...")
|
||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
def _initialize_items(self):
|
||||
self._mapd_version = text_item(tr("Mapd Version"), lambda: ui_state.params.get("MapdVersion") or "Loading...")
|
||||
self._delete_maps_btn = ListItemSP(tr("Downloaded Maps"), action_item=NoElideButtonAction(tr("DELETE"), enabled=True), callback=self._delete_maps)
|
||||
self._progress = progress_item(tr("Downloading Map"))
|
||||
self._update_btn = ListItemSP(tr("Database Update"), action_item=NoElideButtonAction(tr("CHECK"), enabled=True), callback=self._update_db)
|
||||
self._country_btn = ListItemSP(tr("Country"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("Country"))
|
||||
self._state_btn = ListItemSP(tr("State"), action_item=NoElideButtonAction(tr("SELECT"), enabled=True), callback=lambda: self._select_region("State"))
|
||||
items = [
|
||||
|
||||
self.items = [self._mapd_version, self._delete_maps_btn, self._progress, self._update_btn, self._country_btn, self._state_btn]
|
||||
|
||||
def _show_confirm(self, msg, confirm_text, func):
|
||||
gui_app.set_modal_overlay(ConfirmDialog(msg, confirm_text), lambda res: func() if res == DialogResult.CONFIRM else None)
|
||||
|
||||
def calculate_size(self):
|
||||
total_size = 0
|
||||
directories_to_scan = [MAP_PATH] if MAP_PATH.exists() else []
|
||||
while directories_to_scan:
|
||||
try:
|
||||
for entry in os.scandir(directories_to_scan.pop()):
|
||||
if entry.is_file():
|
||||
total_size += entry.stat().st_size
|
||||
elif entry.is_dir():
|
||||
directories_to_scan.append(entry.path)
|
||||
except OSError:
|
||||
pass
|
||||
self._delete_maps_btn.action_item.set_value(f"{total_size / 1024 ** 2:.2f} MB" if total_size < 1024 ** 3 else f"{total_size / 1024 ** 3:.2f} GB")
|
||||
|
||||
def _update_map_size(self):
|
||||
threading.Thread(target=self.calculate_size, daemon=True).start()
|
||||
|
||||
def _do_delete_maps(self):
|
||||
if MAP_PATH.exists():
|
||||
shutil.rmtree(MAP_PATH)
|
||||
|
||||
for param in ("OsmDownloadedDate", "OsmLocal", "OsmLocationName", "OsmLocationTitle", "OsmStateName", "OsmStateTitle"):
|
||||
ui_state.params.remove(param)
|
||||
|
||||
self._delete_maps_btn.action_item.set_enabled(True)
|
||||
self._delete_maps_btn.action_item.set_text(tr("DELETE"))
|
||||
self._update_map_size()
|
||||
|
||||
def _on_confirm_delete_maps(self):
|
||||
self._delete_maps_btn.action_item.set_enabled(False)
|
||||
self._delete_maps_btn.action_item.set_text("DELETING...")
|
||||
threading.Thread(target=self._do_delete_maps).start()
|
||||
|
||||
def _delete_maps(self):
|
||||
self._show_confirm(tr("This will delete ALL downloaded maps\n\nAre you sure you want to delete all maps?"),
|
||||
tr("Yes, delete all maps"), self._on_confirm_delete_maps)
|
||||
|
||||
def _update_db(self):
|
||||
self._show_confirm(tr("This will start the download process and it might take a while to complete."), tr("Start Download"),
|
||||
lambda: ui_state.params.put_bool("OsmDbUpdatesCheck", True))
|
||||
|
||||
def _select_region(self, region_type):
|
||||
is_country = region_type == "Country"
|
||||
btn = self._country_btn if is_country else self._state_btn
|
||||
btn.action_item.set_enabled(False)
|
||||
btn.action_item.set_text(tr("FETCHING..."))
|
||||
threading.Thread(target=self._do_select_region, args=(region_type, btn)).start()
|
||||
|
||||
def _handle_region_selection(self, region_type, locations, key, res, ref):
|
||||
if res != DialogResult.CONFIRM or not ref:
|
||||
if region_type == "State" and res == DialogResult.CANCEL:
|
||||
if ui_state.params.get("OsmLocationName") == "US" and not ui_state.params.get("OsmStateName"):
|
||||
ui_state.params.remove("OsmLocationName")
|
||||
ui_state.params.remove("OsmLocationTitle")
|
||||
ui_state.params.remove("OsmLocal")
|
||||
self._update_labels()
|
||||
return
|
||||
|
||||
if region_type == "Country":
|
||||
ui_state.params.put_bool("OsmLocal", True)
|
||||
ui_state.params.remove("OsmStateName")
|
||||
ui_state.params.remove("OsmStateTitle")
|
||||
|
||||
ui_state.params.put(f"{key}Name", ref)
|
||||
name = next((n.data['display_name'] for n in locations if n.ref == ref), ref)
|
||||
ui_state.params.put(f"{key}Title", name)
|
||||
|
||||
if ref == "US" and region_type == "Country":
|
||||
self._select_region("State")
|
||||
else:
|
||||
self._update_db()
|
||||
|
||||
def _do_select_region(self, region_type, btn):
|
||||
base_url = "https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/"
|
||||
url = base_url + ("nation_bounding_boxes.json" if region_type == "Country" else "us_states_bounding_boxes.json")
|
||||
try:
|
||||
data = requests.get(url, timeout=10).json()
|
||||
locations = sorted([TreeNode(ref=k, data={'display_name': v['full_name']}) for k, v in data.items()], key=lambda n: n.data['display_name'])
|
||||
except Exception:
|
||||
locations = []
|
||||
|
||||
if region_type == "State":
|
||||
locations.insert(0, TreeNode(ref="All", data={'display_name': tr("All states (~6.0 GB)")}))
|
||||
|
||||
btn.action_item.set_enabled(True)
|
||||
btn.action_item.set_text(tr("SELECT"))
|
||||
|
||||
key = "OsmLocation" if region_type == "Country" else "OsmState"
|
||||
current = ui_state.params.get(f"{key}Name") or ""
|
||||
|
||||
dialog = TreeOptionDialog(tr(f"Select {region_type}"), [TreeFolder(folder="", nodes=locations)], current_ref=current, search_prompt="Perform a search")
|
||||
dialog.on_exit = lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref)
|
||||
gui_app.set_modal_overlay(dialog, callback=lambda res: self._handle_region_selection(region_type, locations, key, res, dialog.selection_ref))
|
||||
|
||||
def _update_labels(self):
|
||||
downloading = bool(self._mem_params.get("OSMDownloadLocations"))
|
||||
self._country_btn.set_enabled(not downloading)
|
||||
self._state_btn.set_enabled(not downloading)
|
||||
self._state_btn.set_visible(ui_state.params.get("OsmLocationName") == "US")
|
||||
self._update_btn.set_visible(bool(ui_state.params.get("OsmLocationName")))
|
||||
|
||||
self._country_btn.action_item.set_value(ui_state.params.get("OsmLocationTitle") or "")
|
||||
self._state_btn.action_item.set_value(ui_state.params.get("OsmStateTitle") or "")
|
||||
|
||||
pending = ui_state.params.get_bool("OsmDbUpdatesCheck")
|
||||
if downloading or pending:
|
||||
if downloading:
|
||||
device.reset_interactive_timeout()
|
||||
self._update_map_size()
|
||||
self._progress.set_visible(True)
|
||||
progress = ui_state.params.get("OSMDownloadProgress")
|
||||
total = progress.get('total_files', 0) if progress else 0
|
||||
done = progress.get('downloaded_files', 0) if progress else 0
|
||||
failed = total > 0 and not downloading and done < total
|
||||
|
||||
if total > 0:
|
||||
progress_perc = max(0.0, min(100.0, (done / total) * 100.0))
|
||||
else:
|
||||
progress_perc = 0.0
|
||||
|
||||
if failed:
|
||||
text = "0% - Downloading Maps"
|
||||
btn_text = tr("Error: Invalid download. Retry.")
|
||||
self._current_percent = 0.0
|
||||
elif total > 0 and downloading:
|
||||
self._current_percent = progress_perc
|
||||
perc_int = int(progress_perc)
|
||||
text = f"{perc_int}% - Downloading Maps"
|
||||
btn_text = f"{done}/{total} ({perc_int}%)"
|
||||
else:
|
||||
self._current_percent = 0.0
|
||||
text = "0% - Downloading Maps"
|
||||
btn_text = tr("Downloading Maps...")
|
||||
|
||||
self._progress.action_item.update(self._current_percent, text, show_progress=total > 0 and downloading and not failed)
|
||||
self._update_btn.action_item.set_enabled(not downloading) # TODO-SP: introduce CANCEL database download with mapd
|
||||
self._update_btn.action_item.set_value(btn_text)
|
||||
self._country_btn.action_item.set_enabled(not downloading)
|
||||
self._state_btn.action_item.set_enabled(not downloading)
|
||||
self._delete_maps_btn.action_item.set_enabled(not downloading)
|
||||
else:
|
||||
self._progress.set_visible(False)
|
||||
self._update_btn.action_item.set_enabled(True)
|
||||
self._country_btn.action_item.set_enabled(True)
|
||||
self._state_btn.action_item.set_enabled(True)
|
||||
self._delete_maps_btn.action_item.set_enabled(True)
|
||||
|
||||
ts = ui_state.params.get("OsmDownloadedDate")
|
||||
dt: datetime.datetime | None = None
|
||||
|
||||
if ts:
|
||||
try:
|
||||
ts_f = float(ts)
|
||||
if ts_f > 0:
|
||||
dt = datetime.datetime.fromtimestamp(ts_f, tz=datetime.UTC)
|
||||
except (ValueError, TypeError):
|
||||
dt = None
|
||||
|
||||
formatted = time_ago(dt)
|
||||
self._update_btn.action_item.set_value(tr("Last checked {}").format(formatted))
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
def _update_state(self):
|
||||
now = monotonic()
|
||||
if now - self._last_map_size_update >= 1.0:
|
||||
self._last_map_size_update = now
|
||||
self._update_labels()
|
||||
]
|
||||
return items
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
|
||||
@@ -9,6 +9,7 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
@@ -30,7 +31,7 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import Steering
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
|
||||
|
||||
@@ -125,7 +126,7 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"),
|
||||
OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"),
|
||||
OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"),
|
||||
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayoutSP(), icon="icons/shell.png"),
|
||||
OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"),
|
||||
}
|
||||
|
||||
def _draw_sidebar(self, rect: rl.Rectangle):
|
||||
@@ -196,6 +197,10 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
|
||||
return False
|
||||
|
||||
def set_current_panel(self, panel_type: OP.PanelType):
|
||||
super().set_current_panel(panel_type)
|
||||
ui_state.set_active_layout(self._panels[self._current_panel].instance)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._panels[self._current_panel].instance.show_event()
|
||||
|
||||
@@ -320,7 +320,6 @@ class SunnylinkLayout(Widget):
|
||||
self._sunnylink_enabled = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
self._sunnylink_toggle.set_right_value(tr("Dongle ID") + ": " + self._get_sunnylink_dongle_id())
|
||||
self._sunnylink_toggle.action_item.set_enabled(not ui_state.is_onroad())
|
||||
self._sunnylink_toggle.action_item.set_state(self._sunnylink_enabled)
|
||||
self._sunnylink_uploader_toggle.action_item.set_enabled(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
|
||||
@@ -55,5 +55,4 @@ class HyundaiSettings(BrandSettings):
|
||||
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)
|
||||
|
||||
41
selfdrive/ui/sunnypilot/ui_helpers.py
Normal file
41
selfdrive/ui/sunnypilot/ui_helpers.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def update_item_from_param(item, key, params):
|
||||
if not (action := getattr(item, 'action_item', None)):
|
||||
return
|
||||
|
||||
if hasattr(action, 'set_state'):
|
||||
action.set_state(params.get_bool(key))
|
||||
elif hasattr(action, 'set_value'):
|
||||
action.set_value(params.get(key, return_default=True))
|
||||
else:
|
||||
try:
|
||||
val = int(params.get(key, return_default=True))
|
||||
if hasattr(action, 'selected_button'):
|
||||
action.selected_button = val
|
||||
if hasattr(action, 'current_value'):
|
||||
action.current_value = val
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
def sync_layout_params(layout, param_name, params):
|
||||
targets = []
|
||||
if toggles := getattr(layout, '_toggles', None):
|
||||
targets.extend([(item, k) for k, item in toggles.items()])
|
||||
|
||||
items = getattr(layout, 'items', []) or getattr(getattr(layout, '_scroller', None), '_items', [])
|
||||
for item in items:
|
||||
action = getattr(item, 'action_item', None)
|
||||
if key := getattr(action, 'param_key', None) or getattr(getattr(action, 'toggle', None), 'param_key', None):
|
||||
targets.append((item, key))
|
||||
|
||||
for item, key in targets:
|
||||
if param_name is None or key == param_name:
|
||||
update_item_from_param(item, key, params)
|
||||
@@ -5,23 +5,49 @@ 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.common.swaglog import cloudlog
|
||||
|
||||
from openpilot.sunnypilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_helpers import sync_layout_params
|
||||
|
||||
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.params.add_watcher(self.on_param_change)
|
||||
self.params.start()
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
self.active_layout = None
|
||||
self.changed_params = set()
|
||||
|
||||
def set_active_layout(self, layout):
|
||||
self.active_layout = layout
|
||||
if layout:
|
||||
sync_layout_params(layout, None, self.params)
|
||||
|
||||
def on_param_change(self, param_name):
|
||||
self.changed_params.add(param_name)
|
||||
|
||||
def update(self) -> None:
|
||||
self.sunnylink_state.start()
|
||||
|
||||
if not self.params.is_watching():
|
||||
cloudlog.warning("ParamWatcher thread died, restarting...")
|
||||
self.params.start()
|
||||
|
||||
if self.changed_params:
|
||||
while self.changed_params:
|
||||
self.changed_params.pop()
|
||||
|
||||
if self.active_layout:
|
||||
sync_layout_params(self.active_layout, None, self.params)
|
||||
|
||||
def update_params(self) -> None:
|
||||
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
||||
if CP_SP_bytes is not None:
|
||||
|
||||
5
selfdrive/ui/tests/.gitignore
vendored
5
selfdrive/ui/tests/.gitignore
vendored
@@ -2,8 +2,3 @@ test
|
||||
test_translations
|
||||
test_ui/report_1
|
||||
test_ui/raylib_report
|
||||
|
||||
diff/*.mp4
|
||||
diff/*.html
|
||||
diff/.coverage
|
||||
diff/htmlcov/
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import base64
|
||||
import webbrowser
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
DIFF_OUT_DIR = Path(BASEDIR) / "selfdrive" / "ui" / "tests" / "diff" / "report"
|
||||
|
||||
|
||||
def extract_frames(video_path, output_dir):
|
||||
output_pattern = str(output_dir / "frame_%04d.png")
|
||||
cmd = ['ffmpeg', '-i', video_path, '-vsync', '0', output_pattern, '-y']
|
||||
subprocess.run(cmd, capture_output=True, check=True)
|
||||
frames = sorted(output_dir.glob("frame_*.png"))
|
||||
return frames
|
||||
|
||||
|
||||
def compare_frames(frame1_path, frame2_path):
|
||||
result = subprocess.run(['cmp', '-s', frame1_path, frame2_path])
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def frame_to_data_url(frame_path):
|
||||
with open(frame_path, 'rb') as f:
|
||||
data = f.read()
|
||||
return f"data:image/png;base64,{base64.b64encode(data).decode()}"
|
||||
|
||||
|
||||
def create_diff_video(video1, video2, output_path):
|
||||
"""Create a diff video using ffmpeg blend filter with difference mode."""
|
||||
print("Creating diff video...")
|
||||
cmd = ['ffmpeg', '-i', video1, '-i', video2, '-filter_complex', '[0:v]blend=all_mode=difference', '-vsync', '0', '-y', output_path]
|
||||
subprocess.run(cmd, capture_output=True, check=True)
|
||||
|
||||
|
||||
def find_differences(video1, video2):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir = Path(tmpdir)
|
||||
|
||||
print(f"Extracting frames from {video1}...")
|
||||
frames1_dir = tmpdir / "frames1"
|
||||
frames1_dir.mkdir()
|
||||
frames1 = extract_frames(video1, frames1_dir)
|
||||
|
||||
print(f"Extracting frames from {video2}...")
|
||||
frames2_dir = tmpdir / "frames2"
|
||||
frames2_dir.mkdir()
|
||||
frames2 = extract_frames(video2, frames2_dir)
|
||||
|
||||
if len(frames1) != len(frames2):
|
||||
print(f"WARNING: Frame count mismatch: {len(frames1)} vs {len(frames2)}")
|
||||
min_frames = min(len(frames1), len(frames2))
|
||||
frames1 = frames1[:min_frames]
|
||||
frames2 = frames2[:min_frames]
|
||||
|
||||
print(f"Comparing {len(frames1)} frames...")
|
||||
different_frames = []
|
||||
frame_data = []
|
||||
|
||||
for i, (f1, f2) in enumerate(zip(frames1, frames2, strict=False)):
|
||||
is_different = not compare_frames(f1, f2)
|
||||
if is_different:
|
||||
different_frames.append(i)
|
||||
|
||||
if i < 10 or i >= len(frames1) - 10 or is_different:
|
||||
frame_data.append({'index': i, 'different': is_different, 'frame1_url': frame_to_data_url(f1), 'frame2_url': frame_to_data_url(f2)})
|
||||
|
||||
return different_frames, frame_data, len(frames1)
|
||||
|
||||
|
||||
def generate_html_report(video1, video2, basedir, different_frames, frame_data, total_frames):
|
||||
chunks = []
|
||||
if different_frames:
|
||||
current_chunk = [different_frames[0]]
|
||||
for i in range(1, len(different_frames)):
|
||||
if different_frames[i] == different_frames[i - 1] + 1:
|
||||
current_chunk.append(different_frames[i])
|
||||
else:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = [different_frames[i]]
|
||||
chunks.append(current_chunk)
|
||||
|
||||
result_text = (
|
||||
f"✅ Videos are identical! ({total_frames} frames)"
|
||||
if len(different_frames) == 0
|
||||
else f"❌ Found {len(different_frames)} different frames out of {total_frames} total ({(len(different_frames) / total_frames * 100):.1f}%)"
|
||||
)
|
||||
|
||||
html = f"""<h2>UI Diff</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<td width='33%'>
|
||||
<p><strong>Video 1</strong></p>
|
||||
<video id='video1' width='100%' autoplay muted loop onplay='syncVideos()'>
|
||||
<source src='{os.path.join(basedir, os.path.basename(video1))}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
<td width='33%'>
|
||||
<p><strong>Video 2</strong></p>
|
||||
<video id='video2' width='100%' autoplay muted loop onplay='syncVideos()'>
|
||||
<source src='{os.path.join(basedir, os.path.basename(video2))}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
<td width='33%'>
|
||||
<p><strong>Pixel Diff</strong></p>
|
||||
<video id='diffVideo' width='100%' autoplay muted loop>
|
||||
<source src='{os.path.join(basedir, 'diff.mp4')}' type='video/mp4'>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<script>
|
||||
function syncVideos() {{
|
||||
const video1 = document.getElementById('video1');
|
||||
const video2 = document.getElementById('video2');
|
||||
const diffVideo = document.getElementById('diffVideo');
|
||||
video1.currentTime = video2.currentTime = diffVideo.currentTime;
|
||||
}}
|
||||
video1.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(video1.currentTime - video2.currentTime) > 0.1) {{
|
||||
video2.currentTime = video1.currentTime;
|
||||
}}
|
||||
if (Math.abs(video1.currentTime - diffVideo.currentTime) > 0.1) {{
|
||||
diffVideo.currentTime = video1.currentTime;
|
||||
}}
|
||||
}});
|
||||
video2.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(video2.currentTime - video1.currentTime) > 0.1) {{
|
||||
video1.currentTime = video2.currentTime;
|
||||
}}
|
||||
if (Math.abs(video2.currentTime - diffVideo.currentTime) > 0.1) {{
|
||||
diffVideo.currentTime = video2.currentTime;
|
||||
}}
|
||||
}});
|
||||
diffVideo.addEventListener('timeupdate', () => {{
|
||||
if (Math.abs(diffVideo.currentTime - video1.currentTime) > 0.1) {{
|
||||
video1.currentTime = diffVideo.currentTime;
|
||||
video2.currentTime = diffVideo.currentTime;
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
<hr>
|
||||
<p><strong>Results:</strong> {result_text}</p>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Compare two videos and generate HTML diff report')
|
||||
parser.add_argument('video1', help='First video file')
|
||||
parser.add_argument('video2', help='Second video file')
|
||||
parser.add_argument('output', nargs='?', default='diff.html', help='Output HTML file (default: diff.html)')
|
||||
parser.add_argument("--basedir", type=str, help="Base directory for output", default="")
|
||||
parser.add_argument('--no-open', action='store_true', help='Do not open HTML report in browser')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
|
||||
|
||||
print("=" * 60)
|
||||
print("VIDEO DIFF - HTML REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Video 1: {args.video1}")
|
||||
print(f"Video 2: {args.video2}")
|
||||
print(f"Output: {args.output}")
|
||||
print()
|
||||
|
||||
# Create diff video
|
||||
diff_video_path = os.path.join(os.path.dirname(args.output), DIFF_OUT_DIR / "diff.mp4")
|
||||
create_diff_video(args.video1, args.video2, diff_video_path)
|
||||
|
||||
different_frames, frame_data, total_frames = find_differences(args.video1, args.video2)
|
||||
|
||||
if different_frames is None:
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
print("Generating HTML report...")
|
||||
html = generate_html_report(args.video1, args.video2, args.basedir, different_frames, frame_data, total_frames)
|
||||
|
||||
with open(DIFF_OUT_DIR / args.output, 'w') as f:
|
||||
f.write(html)
|
||||
|
||||
# Open in browser by default
|
||||
if not args.no_open:
|
||||
print(f"Opening {args.output} in browser...")
|
||||
webbrowser.open(f'file://{os.path.abspath(DIFF_OUT_DIR / args.output)}')
|
||||
|
||||
return 0 if len(different_frames) == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import coverage
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR
|
||||
|
||||
os.environ["RECORD"] = "1"
|
||||
if "RECORD_OUTPUT" not in os.environ:
|
||||
os.environ["RECORD_OUTPUT"] = "mici_ui_replay.mp4"
|
||||
|
||||
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
|
||||
FPS = 60
|
||||
HEADLESS = os.getenv("WINDOWED", "0") == "1"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyEvent:
|
||||
click: bool = False
|
||||
# TODO: add some kind of intensity
|
||||
swipe_left: bool = False
|
||||
swipe_right: bool = False
|
||||
swipe_down: bool = False
|
||||
|
||||
|
||||
SCRIPT = [
|
||||
(0, DummyEvent()),
|
||||
(FPS * 1, DummyEvent(click=True)),
|
||||
(FPS * 2, DummyEvent(click=True)),
|
||||
(FPS * 3, DummyEvent()),
|
||||
]
|
||||
|
||||
|
||||
def setup_state():
|
||||
params = Params()
|
||||
params.put("HasAcceptedTerms", terms_version)
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("DongleId", "test123456789")
|
||||
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
|
||||
return None
|
||||
|
||||
|
||||
def inject_click(coords):
|
||||
events = []
|
||||
x, y = coords[0]
|
||||
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=True, left_released=False, left_down=False, t=time.monotonic()))
|
||||
for x, y in coords[1:]:
|
||||
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=False, left_down=True, t=time.monotonic()))
|
||||
x, y = coords[-1]
|
||||
events.append(MouseEvent(pos=MousePos(x, y), slot=0, left_pressed=False, left_released=True, left_down=False, t=time.monotonic()))
|
||||
|
||||
with gui_app._mouse._lock:
|
||||
gui_app._mouse._events.extend(events)
|
||||
|
||||
|
||||
def handle_event(event: DummyEvent):
|
||||
if event.click:
|
||||
inject_click([(gui_app.width // 2, gui_app.height // 2)])
|
||||
if event.swipe_left:
|
||||
inject_click([(gui_app.width * 3 // 4, gui_app.height // 2),
|
||||
(gui_app.width // 4, gui_app.height // 2),
|
||||
(0, gui_app.height // 2)])
|
||||
if event.swipe_right:
|
||||
inject_click([(gui_app.width // 4, gui_app.height // 2),
|
||||
(gui_app.width * 3 // 4, gui_app.height // 2),
|
||||
(gui_app.width, gui_app.height // 2)])
|
||||
if event.swipe_down:
|
||||
inject_click([(gui_app.width // 2, gui_app.height // 4),
|
||||
(gui_app.width // 2, gui_app.height * 3 // 4),
|
||||
(gui_app.width // 2, gui_app.height)])
|
||||
|
||||
|
||||
def run_replay():
|
||||
setup_state()
|
||||
os.makedirs(DIFF_OUT_DIR, exist_ok=True)
|
||||
|
||||
if not HEADLESS:
|
||||
rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN)
|
||||
gui_app.init_window("ui diff test", fps=FPS)
|
||||
main_layout = MiciMainLayout()
|
||||
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
|
||||
frame = 0
|
||||
script_index = 0
|
||||
|
||||
for should_render in gui_app.render():
|
||||
while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame:
|
||||
_, event = SCRIPT[script_index]
|
||||
handle_event(event)
|
||||
script_index += 1
|
||||
|
||||
ui_state.update()
|
||||
|
||||
if should_render:
|
||||
main_layout.render()
|
||||
|
||||
frame += 1
|
||||
|
||||
if script_index >= len(SCRIPT):
|
||||
break
|
||||
|
||||
gui_app.close()
|
||||
|
||||
print(f"Total frames: {frame}")
|
||||
print(f"Video saved to: {os.environ['RECORD_OUTPUT']}")
|
||||
|
||||
|
||||
def main():
|
||||
cov = coverage.coverage(source=['openpilot.selfdrive.ui.mici'])
|
||||
with cov.collect():
|
||||
run_replay()
|
||||
cov.stop()
|
||||
cov.save()
|
||||
cov.report()
|
||||
cov.html_report(directory=os.path.join(DIFF_OUT_DIR, 'htmlcov'))
|
||||
print("HTML report: htmlcov/index.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -88,9 +88,9 @@ if __name__ == "__main__":
|
||||
print("Running...")
|
||||
patch_submaster(message_chunks)
|
||||
|
||||
W, H = 2048, 1216
|
||||
W, H = 1928, 1208
|
||||
vipc = VisionIpcServer("camerad")
|
||||
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H)
|
||||
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, 1928, 1208)
|
||||
vipc.start_listener()
|
||||
yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2
|
||||
yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()
|
||||
|
||||
@@ -68,10 +68,10 @@ msgid ""
|
||||
"control alpha. Changing this setting will restart openpilot if the car is "
|
||||
"powered on."
|
||||
msgstr ""
|
||||
"<b>경고: 이 차량에서 openpilot의 롱컨 제어는 알파 버전이며 자동 긴급 제동"
|
||||
"(AEB)을 비활성화합니다.</b><br><br>이 차량에서는 openpilot 롱컨 제어 대신 "
|
||||
"차량 내장 ACC가 기본으로 사용됩니다. openpilot 롱컨 제어로 전환하려면 이 설"
|
||||
"정을 켜세요. 롱컨 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전"
|
||||
"<b>경고: 이 차량에서 openpilot의 종방향 제어는 알파 버전이며 자동 긴급 제동"
|
||||
"(AEB)을 비활성화합니다.</b><br><br>이 차량에서는 openpilot 종방향 제어 대신 "
|
||||
"차량 내장 ACC가 기본으로 사용됩니다. openpilot 종방향 제어로 전환하려면 이 설"
|
||||
"정을 켜세요. 종방향 제어 알파를 켤 때는 실험 모드 사용을 권장합니다. 차량 전"
|
||||
"원이 켜져 있는 경우 이 설정을 변경하면 openpilot이 재시작됩니다."
|
||||
|
||||
#: selfdrive/ui/layouts/settings/device.py:148
|
||||
@@ -130,7 +130,7 @@ msgstr "동의"
|
||||
#: selfdrive/ui/layouts/settings/toggles.py:70
|
||||
#, python-format
|
||||
msgid "Always-On Driver Monitoring"
|
||||
msgstr "운전자 모니터링 항상 켜짐"
|
||||
msgstr "항상 켜짐 운전자 모니터링"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/toggles.py:186
|
||||
#, python-format
|
||||
@@ -138,7 +138,7 @@ msgid ""
|
||||
"An alpha version of openpilot longitudinal control can be tested, along with "
|
||||
"Experimental mode, on non-release branches."
|
||||
msgstr ""
|
||||
"openpilot 롱컨 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트"
|
||||
"openpilot 종방향 제어 알파 버전은 실험 모드와 함께 비릴리스 브랜치에서 테스트"
|
||||
"할 수 있습니다."
|
||||
|
||||
#: selfdrive/ui/layouts/settings/device.py:187
|
||||
@@ -192,7 +192,7 @@ msgstr "확인"
|
||||
#: selfdrive/ui/widgets/exp_mode_button.py:50
|
||||
#, python-format
|
||||
msgid "CHILL MODE ON"
|
||||
msgstr "안정적 모드 켜짐"
|
||||
msgstr "칠 모드 켜짐"
|
||||
|
||||
#: system/ui/widgets/network.py:155 selfdrive/ui/layouts/sidebar.py:73
|
||||
#: selfdrive/ui/layouts/sidebar.py:134 selfdrive/ui/layouts/sidebar.py:136
|
||||
@@ -283,7 +283,7 @@ msgstr "해제 후 재시작"
|
||||
#: selfdrive/ui/layouts/settings/device.py:103
|
||||
#, python-format
|
||||
msgid "Disengage to Reset Calibration"
|
||||
msgstr "해제 후 캘리브레이션 재설정"
|
||||
msgstr "해제 후 보정 재설정"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/toggles.py:32
|
||||
msgid "Display speed in km/h instead of mph."
|
||||
@@ -372,7 +372,7 @@ msgstr "openpilot 사용"
|
||||
msgid ""
|
||||
"Enable the openpilot longitudinal control (alpha) toggle to allow "
|
||||
"Experimental mode."
|
||||
msgstr "실험 모드를 사용하려면 openpilot 롱컨 제어(알파) 토글을 켜세요."
|
||||
msgstr "실험 모드를 사용하려면 openpilot 종방향 제어(알파) 토글을 켜세요."
|
||||
|
||||
#: system/ui/widgets/network.py:204
|
||||
#, python-format
|
||||
@@ -415,7 +415,7 @@ msgid ""
|
||||
"Experimental mode is currently unavailable on this car since the car's stock "
|
||||
"ACC is used for longitudinal control."
|
||||
msgstr ""
|
||||
"이 차량은 롱컨 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습"
|
||||
"이 차량은 종방향 제어에 순정 ACC를 사용하므로 현재 실험 모드를 사용할 수 없습"
|
||||
"니다."
|
||||
|
||||
#: system/ui/widgets/network.py:373
|
||||
@@ -430,11 +430,11 @@ msgstr "설정 완료"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/settings.py:66
|
||||
msgid "Firehose"
|
||||
msgstr "파이어호스"
|
||||
msgstr "Firehose"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/firehose.py:18
|
||||
msgid "Firehose Mode"
|
||||
msgstr "파이어호스 모드"
|
||||
msgstr "Firehose 모드"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/firehose.py:25
|
||||
msgid ""
|
||||
@@ -462,7 +462,7 @@ msgstr ""
|
||||
"최대의 효과를 위해 주 1회는 장치를 실내로 가져와 품질 좋은 USB‑C 어댑터와 "
|
||||
"Wi‑Fi에 연결하세요.\n"
|
||||
"\n"
|
||||
"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 파이어호스 모드가 동작합니"
|
||||
"핫스팟이나 무제한 SIM에 연결되어 있다면 주행 중에도 Firehose 모드가 동작합니"
|
||||
"다.\n"
|
||||
"\n"
|
||||
"\n"
|
||||
@@ -470,7 +470,7 @@ msgstr ""
|
||||
"\n"
|
||||
"어떻게, 어디서 운전하는지가 중요한가요? 아니요. 평소처럼 운전하세요.\n"
|
||||
"\n"
|
||||
"파이어호스 모드에서 모든 구간을 가져가지나요? 아니요. 일부 구간만 선택"
|
||||
"Firehose 모드에서 모든 세그먼트가 가져가지나요? 아니요. 일부 세그먼트만 선택"
|
||||
"적으로 가져갑니다.\n"
|
||||
"\n"
|
||||
"좋은 USB‑C 어댑터는 무엇인가요? 빠른 휴대폰 또는 노트북 충전기면 충분합니"
|
||||
@@ -544,7 +544,7 @@ msgstr "LTE"
|
||||
#: selfdrive/ui/layouts/settings/developer.py:64
|
||||
#, python-format
|
||||
msgid "Longitudinal Maneuver Mode"
|
||||
msgstr "롱컨 기동 모드"
|
||||
msgstr "종방향 매뉴버 모드"
|
||||
|
||||
#: selfdrive/ui/onroad/hud_renderer.py:148
|
||||
#, python-format
|
||||
@@ -623,7 +623,7 @@ msgstr "미리보기"
|
||||
#: selfdrive/ui/widgets/prime.py:44
|
||||
#, python-format
|
||||
msgid "PRIME FEATURES:"
|
||||
msgstr "프라임 기능:"
|
||||
msgstr "prime 기능:"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/device.py:48
|
||||
#, python-format
|
||||
@@ -646,7 +646,7 @@ msgid ""
|
||||
"Pair your device with comma connect (connect.comma.ai) and claim your comma "
|
||||
"prime offer."
|
||||
msgstr ""
|
||||
"장치를 comma connect(connect.comma.ai)와 페어링하고 comma 프라임 혜택을 받으세"
|
||||
"장치를 comma connect(connect.comma.ai)와 페어링하고 comma prime 혜택을 받으세"
|
||||
"요."
|
||||
|
||||
#: selfdrive/ui/widgets/setup.py:91
|
||||
@@ -748,7 +748,7 @@ msgstr "규제 정보"
|
||||
#: selfdrive/ui/layouts/settings/toggles.py:98
|
||||
#, python-format
|
||||
msgid "Relaxed"
|
||||
msgstr "편안한"
|
||||
msgstr "편안함"
|
||||
|
||||
#: selfdrive/ui/widgets/prime.py:47
|
||||
#, python-format
|
||||
@@ -773,7 +773,7 @@ msgstr "재설정"
|
||||
#: selfdrive/ui/layouts/settings/device.py:51
|
||||
#, python-format
|
||||
msgid "Reset Calibration"
|
||||
msgstr "캘리브레이션 재설정"
|
||||
msgstr "보정 재설정"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/device.py:65
|
||||
#, python-format
|
||||
@@ -841,7 +841,7 @@ msgid ""
|
||||
"cycle through these personalities with your steering wheel distance button."
|
||||
msgstr ""
|
||||
"표준을 권장합니다. 공격적 모드에서는 앞차를 더 가깝게 따라가고 가감속이 더 적"
|
||||
"극적입니다. 편안한 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어"
|
||||
"극적입니다. 편안함 모드에서는 앞차와 거리를 더 둡니다. 지원 차량에서는 스티어"
|
||||
"링의 차간 버튼으로 이 성향들을 전환할 수 있습니다."
|
||||
|
||||
#: selfdrive/ui/onroad/alert_renderer.py:59
|
||||
@@ -892,7 +892,7 @@ msgstr "제거"
|
||||
|
||||
#: selfdrive/ui/layouts/sidebar.py:117
|
||||
msgid "Unknown"
|
||||
msgstr "알수없음"
|
||||
msgstr "알 수 없음"
|
||||
|
||||
#: selfdrive/ui/layouts/settings/software.py:48
|
||||
#, python-format
|
||||
@@ -994,7 +994,7 @@ msgstr "카메라 시작 중"
|
||||
#: selfdrive/ui/widgets/prime.py:63
|
||||
#, python-format
|
||||
msgid "comma prime"
|
||||
msgstr "comma 프라임"
|
||||
msgstr "comma prime"
|
||||
|
||||
#: system/ui/widgets/network.py:142
|
||||
#, python-format
|
||||
@@ -1054,7 +1054,7 @@ msgstr "지금"
|
||||
#: selfdrive/ui/layouts/settings/developer.py:71
|
||||
#, python-format
|
||||
msgid "openpilot Longitudinal Control (Alpha)"
|
||||
msgstr "openpilot 롱컨 제어(알파)"
|
||||
msgstr "openpilot 종방향 제어(알파)"
|
||||
|
||||
#: selfdrive/ui/onroad/alert_renderer.py:51
|
||||
#, python-format
|
||||
@@ -1076,9 +1076,9 @@ msgid ""
|
||||
"some turns. The Experimental mode logo will also be shown in the top right "
|
||||
"corner."
|
||||
msgstr ""
|
||||
"openpilot은 기본적으로 안정적 모드로 주행합니다. 실험 모드를 사용하면 안정적 모드에 "
|
||||
"openpilot은 기본적으로 칠 모드로 주행합니다. 실험 모드를 사용하면 칠 모드에 "
|
||||
"아직 준비되지 않은 알파 수준의 기능이 활성화됩니다. 실험 기능은 아래와 같습니"
|
||||
"다:<br><h4>엔드투엔드 롱컨 제어</h4><br>주행 모델이 가속과 제동을 제어합니"
|
||||
"다:<br><h4>엔드투엔드 종방향 제어</h4><br>주행 모델이 가속과 제동을 제어합니"
|
||||
"다. openpilot은 빨간 신호 및 정지 표지에서의 정지를 포함해 사람이 운전한다고 "
|
||||
"판단하는 방식으로 주행합니다. 주행 속도는 모델이 결정하므로 설정 속도는 상한"
|
||||
"으로만 동작합니다. 알파 품질 기능이므로 오작동이 발생할 수 있습니다.<br><h4>"
|
||||
@@ -1111,7 +1111,7 @@ msgstr ""
|
||||
#: selfdrive/ui/layouts/settings/toggles.py:183
|
||||
#, python-format
|
||||
msgid "openpilot longitudinal control may come in a future update."
|
||||
msgstr "openpilot 롱컨 제어는 향후 업데이트에서 제공될 수 있습니다."
|
||||
msgstr "openpilot 종방향 제어는 향후 업데이트에서 제공될 수 있습니다."
|
||||
|
||||
#: selfdrive/ui/layouts/settings/device.py:26
|
||||
msgid ""
|
||||
@@ -1177,7 +1177,7 @@ msgstr[0] "{}분 전"
|
||||
#, python-format
|
||||
msgid "{} segment of your driving is in the training dataset so far."
|
||||
msgid_plural "{} segments of your driving is in the training dataset so far."
|
||||
msgstr[0] "현재까지 귀하의 주행 {}구간이 학습 데이터셋에 포함되었습니다."
|
||||
msgstr[0] "현재까지 귀하의 주행 {}세그먼트가 학습 데이터셋에 포함되었습니다."
|
||||
|
||||
#: selfdrive/ui/widgets/prime.py:62
|
||||
#, python-format
|
||||
@@ -1187,4 +1187,4 @@ msgstr "✓ 구독됨"
|
||||
#: selfdrive/ui/widgets/setup.py:22
|
||||
#, python-format
|
||||
msgid "🔥 Firehose Mode 🔥"
|
||||
msgstr "🔥 파이어호스 모드 🔥"
|
||||
msgstr "🔥 Firehose 모드 🔥"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@
|
||||
"Português": "pt-BR",
|
||||
"Español": "es",
|
||||
"Türkçe": "tr",
|
||||
"Українська": "uk",
|
||||
"العربية": "ar",
|
||||
"ไทย": "th",
|
||||
"中文(繁體)": "zh-CHT",
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from cereal import messaging, car, log
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -34,7 +33,6 @@ class UIState(UIStateSP):
|
||||
|
||||
def _initialize(self):
|
||||
UIStateSP.__init__(self)
|
||||
self.params = Params()
|
||||
self.sm = messaging.SubMaster(
|
||||
[
|
||||
"modelV2",
|
||||
|
||||
133
sunnypilot/common/README.md
Normal file
133
sunnypilot/common/README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Comparative Analysis of Parameter Access Methods: `Params::get` vs. `ParamWatcher`
|
||||
|
||||
## Inefficiencies in Standard Parameter Access
|
||||
The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This approach results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop.
|
||||
|
||||
### System Overhead Analysis
|
||||
* **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025).
|
||||
* **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025 -a; Linux man-pages, 2025 -b).
|
||||
* **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025).
|
||||
* **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.).
|
||||
|
||||
## The ParamWatcher Optimization
|
||||
The `ParamWatcher` implementation utilizes OS-level file system events, such as `inotify` on Linux or `FSEvents` on macOS, to maintain a Random Access Memory (RAM) cache. This architecture eliminates the need for continuous polling.
|
||||
|
||||
### Performance Comparison
|
||||
| Feature | Standard `Params::get` | Optimized `ParamWatcher` |
|
||||
| :--- | :--- | :--- |
|
||||
| **Workflow** | `open` → `malloc` → `read` → `close` | `dict.get()` (RAM lookup) |
|
||||
| **Complexity** | **O(N * F)** (Linear to toggles & FPS) | **O(1)** (Constant time) |
|
||||
| **Disk I/O** | ~1,000 reads/sec (50 toggles @ 20FPS) | **0 reads/sec** (Steady state) |
|
||||
| **Memory** | New string object per call (High GC pressure) | Returns reference (Zero GC pressure) |
|
||||
|
||||
## Architectural Mismatch of Standard Modules
|
||||
Standard C++ modules like `std::ifstream` are optimized for **throughput**—reading large files sequentially—rather than **latency** required for polling small files frequently.
|
||||
|
||||
* **The I/O Trap**: Even when a file resides in the OS page cache (RAM), invoking `open()` and `read()` forces a CPU mode switch (User → Kernel → User). Executing this sequence 1,000 times per second consumes CPU cycles merely to verify state constancy.
|
||||
* **The Memory Trap**: The `std::string` class allocates memory on the heap. Repeated allocation creates short-lived objects, which in C++ fragments memory. In Python (which wraps this), it triggers the Garbage Collector, pausing the UI.
|
||||
* **The Query Mismatch**: `Params::get` queries the current value every frame, whereas `ParamWatcher` waits for a notification of change, serving cached values in the interim.
|
||||
|
||||
## Implementation Analysis
|
||||
The `ParamWatcher` class provides a cross-platform solution for monitoring file system changes, specifically targeting the parameter files used in Openpilot. The implementation leverages the `ctypes` library to interface directly with operating system kernels, bypassing higher-level abstractions for maximum performance.
|
||||
|
||||
### Linux Implementation (`_run_linux`)
|
||||
The Linux implementation interacts directly with the kernel's `inotify` subsystem (Linux man-pages, 2025 -c).
|
||||
|
||||
* **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls.
|
||||
* **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor.
|
||||
* **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes.
|
||||
* **Event Loop**:
|
||||
* **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting.
|
||||
* **Reading**: When events occur, `os.read(fd, 1024)` retrieves the raw binary event data.
|
||||
* **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures.
|
||||
* **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access.
|
||||
|
||||
### macOS Implementation (`_run_darwin`)
|
||||
The macOS implementation uses the `FSEvents` API from the `CoreServices` framework (Apple Inc., n.d.-a), which is more efficient than `kqueue` for directory monitoring.
|
||||
|
||||
* **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`.
|
||||
* **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory.
|
||||
* **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available.
|
||||
* **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c).
|
||||
* **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b).
|
||||
* **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary.
|
||||
* **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter.
|
||||
|
||||
### Python ctypes Integration
|
||||
The use of `ctypes` (Python Software Foundation, 2025) is a strategic choice. It allows the Python interpreter to load shared libraries (`libc.so.6` on Linux, `CoreServices` on macOS) and call C functions directly. This approach avoids the overhead of spawning subprocesses or compiling external C extensions, keeping the codebase pure Python while achieving C-level system integration.
|
||||
|
||||
### Memory Impact Analysis
|
||||
With 232 defined parameters in `param_keys.h`, the maximum static RAM footprint of `ParamWatcher` is estimated to be **less than 250 KB**. Even if every single parameter were cached simultaneously, this static usage is negligible. Importantly, this stable footprint is likely more probable to maintain no trend of memory increase, whenc compared to the standard `Params::get` approach, which generates **megabytes** of short-lived "garbage" allocations per second, forcing the Python Garbage Collector to pause execution repeatedly.
|
||||
|
||||
## Architectural Integration: The Process-Local Singleton Pattern
|
||||
To ensure resource efficiency within openpilot's multi-process architecture (e.g., `ui`, `controlsd`, `modeld`), `ParamWatcher` implements the Singleton design pattern (Gamma et al., 1994) using the Python `__new__` allocator.
|
||||
|
||||
### Process Isolation and Concurrency
|
||||
In the context of Python's memory model, a Singleton ensures a single instance exists *per process*. This behavior aligns with openpilot's multiprocess design:
|
||||
|
||||
* **Intra-Process Efficiency**: Within a single heavy process like `ui`, multiple sub-components (e.g., `UIState`, `SunnylinkState`) import and use `Params`. The Singleton pattern ensures they share a single `inotify` thread and a unified RAM cache. This prevents the proliferation of redundant watcher threads, which would otherwise compete for the Global Interpreter Lock (GIL).
|
||||
* **Inter-Process Safety**: Distinct processes (e.g., `modeld` vs. `ui`) maintain completely isolated `ParamWatcher` instances. This isolation eliminates the need for complex Inter-Process Communication (IPC) locking mechanisms for the cache, as each process synchronizes its independent state via the OS file system events.
|
||||
|
||||
### Empirical Verification
|
||||
Runtime analysis demonstrates that multiple instantiation attempts result in a shared object reference, minimizing memory footprint.
|
||||
|
||||
* **Test Case**: Instantiating `ParamWatcher` in `UIStateSP` and subsequently in a standalone script within the same process.
|
||||
* **Result**: Both instances report the exact same memory address (`4814358960`) and share the same background thread ID (`6114635776`).
|
||||
* **Impact**: The system incurs the overhead of the watcher thread (measured at < 0.1% CPU idle usage) only once per active process, regardless of import frequency. The average CPU usage across one minute was 0.002%.
|
||||
|
||||
## Limitations and Trade-offs
|
||||
While `ParamWatcher` offers superior performance for UI rendering, it presents specific trade-offs:
|
||||
|
||||
* **Static RAM Usage**: `ParamWatcher` maintains a persistent dictionary cache of all accessed parameters (~50KB), whereas `Params::get` uses zero static RAM but incurs high dynamic memory access.
|
||||
* **Event Latency**: In high-load scenarios, `inotify` events may experience slight delays or coalescing compared to direct reads. However, for user interface applications, this latency (<10ms) is imperceptible.
|
||||
* **Complexity**: The solution (the process singleton approach) requires managing a background thread and OS-specific event loops, increasing code complexity compared to the synchronous `Params::get` function.
|
||||
|
||||
## Alternative Architecture Considered: ZeroMQ Service (ZMQ)
|
||||
During the development of `ParamWatcher`, a Client-Server architecture using ZMQ was evaluated. In this architecture, a single background service process would monitor file system events and publish changes over a ZMQ PUB socket to multiple client processes (SUB).
|
||||
|
||||
### Trade-off Analysis
|
||||
| Metric | In-Process (Current) | ZMQ Service (Rejected) |
|
||||
| :--- | :--- |:------------------------------------------------------|
|
||||
| **Memory Usage** | Low (1 thread/process) | High (1 full Python process + ZMQ buffers per client) |
|
||||
| **CPU Usage** | Low (Direct callback) | High (Serialization + TCP Stack + Deserialization) |
|
||||
| **Latency** | Instant (<0.1ms) | Variable (TCP Loopback overhead) |
|
||||
| **Scalability** | Limited by OS file handles | Limited by TCP ports/buffers |
|
||||
| **Robustness** | Process-isolated failure | Single point of failure (Service crash affects all) |
|
||||
|
||||
### Decision Rationale
|
||||
While the ZMQ approach offers better isolation and reduces the total number of OS file watchers (1 vs N), the overhead of inter-process communication (IPC) proved excessive for this use case.
|
||||
* **Efficiency**: Even with 50+ processes, the memory footprint of 50 simple threads is significantly lower than the overhead of a dedicated Python service process plus the ZMQ context in every client.
|
||||
* **Complexity**: The ZMQ architecture introduced synchronization challenges (e.g., service startup race conditions, "Address already in use" errors) that outweighed its benefits.
|
||||
* **Performance**: The latency of serializing messages and passing them through the TCP stack is orders of magnitude higher than a direct function call within the same process memory space.
|
||||
|
||||
## Conclusion
|
||||
Replacing polling mechanisms with event-driven caching shifts the computational load from kernel space (syscalls) to user space (RAM). This transition eliminates I/O overhead and UI stutters caused by garbage collection, resulting in a more responsive user experience. The In-Process Singleton approach was selected as the optimal balance between performance, complexity, and resource efficiency.
|
||||
|
||||
## References
|
||||
Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events
|
||||
|
||||
Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop
|
||||
|
||||
Apple Inc. (n.d.-c). *FSEventStreamEventFlags*. Retrieved from https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags
|
||||
|
||||
Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/
|
||||
|
||||
cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream
|
||||
|
||||
cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string
|
||||
|
||||
Linux man-pages. (2025 -a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html
|
||||
|
||||
Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html
|
||||
|
||||
Linux man-pages. (2025 -c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html
|
||||
|
||||
Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h
|
||||
|
||||
Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html
|
||||
|
||||
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
|
||||
|
||||
sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2
|
||||
|
||||
sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2
|
||||
0
sunnypilot/common/__init__.py
Normal file
0
sunnypilot/common/__init__.py
Normal file
193
sunnypilot/common/param_watcher.py
Normal file
193
sunnypilot/common/param_watcher.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import os
|
||||
import platform
|
||||
import struct
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import traceback
|
||||
from ctypes import c_void_p, c_size_t, POINTER, c_uint32, c_uint64
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
IN_MODIFY = 0x00000002
|
||||
IN_CREATE = 0x00000100
|
||||
IN_DELETE = 0x00000200
|
||||
IN_MOVED_TO = 0x00000080
|
||||
IN_CLOSE_WRITE = 0x00000008
|
||||
|
||||
|
||||
class ParamWatcher(Params):
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
super().__init__()
|
||||
cloudlog.warning("ParamWatcher initialized")
|
||||
self._cache = {}
|
||||
self._last_trigger = {}
|
||||
self._version = {}
|
||||
self._lock = threading.Lock()
|
||||
self._callbacks = []
|
||||
self.last_accessed_param = None
|
||||
self._initialized = True
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if getattr(self, '_thread', None) and self._thread.is_alive():
|
||||
return
|
||||
self._thread = threading.Thread(target=self._run_watcher, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_watching(self):
|
||||
return getattr(self, '_thread', None) and self._thread.is_alive()
|
||||
|
||||
def add_watcher(self, callback):
|
||||
if callback not in self._callbacks:
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def _trigger_callbacks(self, path):
|
||||
with self._lock:
|
||||
if (now := time.monotonic()) - self._last_trigger.get(path, 0) < 0.1:
|
||||
return
|
||||
self._last_trigger[path] = now
|
||||
self._version[path] = self._version.get(path, 0) + 1
|
||||
self._cache.pop(path, None)
|
||||
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(path)
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher callback failed")
|
||||
|
||||
def _get_cached(self, key, getter, sig):
|
||||
k = str(key)
|
||||
with self._lock:
|
||||
bucket = self._cache.get(k)
|
||||
if bucket and sig in bucket:
|
||||
if bucket[sig][0] == self._version.get(k, 0):
|
||||
return bucket[sig][1]
|
||||
|
||||
start_ver = self._version.get(k, 0)
|
||||
val = getter()
|
||||
with self._lock:
|
||||
if self._version.get(k, 0) != start_ver:
|
||||
val = getter()
|
||||
self._cache.setdefault(k, {})[sig] = (self._version.get(k, 0), val)
|
||||
return val
|
||||
|
||||
def get(self, key, block=False, return_default=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get(key, block, return_default)
|
||||
fetcher = super().get
|
||||
return self._get_cached(key, lambda: fetcher(key, block, return_default), (block, return_default))
|
||||
|
||||
def get_bool(self, key, block=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get_bool(key, block)
|
||||
fetcher = super().get_bool
|
||||
return self._get_cached(key, lambda: fetcher(key, block), ("bool", block))
|
||||
|
||||
def _run_watcher(self):
|
||||
system = platform.system()
|
||||
while True:
|
||||
try:
|
||||
if system == "Linux":
|
||||
self._run_linux()
|
||||
elif system == "Darwin":
|
||||
self._run_darwin()
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher crashed")
|
||||
time.sleep(2)
|
||||
|
||||
def _run_linux(self):
|
||||
path = Paths.params_root()
|
||||
libc = ctypes.CDLL('libc.so.6')
|
||||
fd = libc.inotify_init()
|
||||
libc.inotify_add_watch(fd, path.encode(), IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE)
|
||||
|
||||
try:
|
||||
poll = select.epoll()
|
||||
poll.register(fd, select.EPOLLIN)
|
||||
while True:
|
||||
for fileno, _ in poll.poll():
|
||||
if fileno == fd:
|
||||
buffer = os.read(fd, 2048)
|
||||
i = 0
|
||||
while i + 16 <= len(buffer):
|
||||
_, mask, _, name_len = struct.unpack_from("iIII", buffer, i)
|
||||
if mask & (IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE):
|
||||
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode()
|
||||
if not name.startswith("."):
|
||||
self._trigger_callbacks(name)
|
||||
i += 16 + name_len
|
||||
finally:
|
||||
if 'poll' in locals():
|
||||
poll.unregister(fd)
|
||||
poll.close()
|
||||
os.close(fd)
|
||||
|
||||
def _run_darwin(self):
|
||||
CS = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreServices"))
|
||||
CF = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
|
||||
|
||||
kCFAllocatorDefault = c_void_p(0)
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
kFSEventStreamCreateFlagFileEvents = 0x00000010
|
||||
kFSEventStreamEventFlagItemCreated = 0x00000100
|
||||
kFSEventStreamEventFlagItemRemoved = 0x00000200
|
||||
kFSEventStreamEventFlagItemRenamed = 0x00000800
|
||||
kFSEventStreamEventFlagItemModified = 0x00001000
|
||||
|
||||
CF.CFStringCreateWithCString.restype = c_void_p
|
||||
CF.CFStringCreateWithCString.argtypes = [c_void_p, ctypes.c_char_p, c_uint32]
|
||||
CF.CFArrayCreate.restype = c_void_p
|
||||
CF.CFArrayCreate.argtypes = [c_void_p, POINTER(c_void_p), c_size_t, c_void_p]
|
||||
CS.FSEventStreamCreate.restype = c_void_p
|
||||
CS.FSEventStreamCreate.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, ctypes.c_double, c_uint32]
|
||||
CS.FSEventStreamScheduleWithRunLoop.argtypes = [c_void_p, c_void_p, c_void_p]
|
||||
CS.FSEventStreamStart.argtypes = [c_void_p]
|
||||
CF.CFRunLoopGetCurrent.restype = c_void_p
|
||||
|
||||
def _cb(stream, ctx, num, paths, flags, ids):
|
||||
try:
|
||||
paths_arr = ctypes.cast(paths, POINTER(c_void_p))
|
||||
flags_arr = ctypes.cast(flags, POINTER(c_uint32))
|
||||
for i in range(num):
|
||||
path = ctypes.cast(paths_arr[i], ctypes.c_char_p).value
|
||||
if path and (flags_arr[i] & (kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved |
|
||||
kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)):
|
||||
self._trigger_callbacks(os.path.basename(path.decode('utf-8').rstrip('/')))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
self._darwin_cb = ctypes.CFUNCTYPE(None, c_void_p, c_void_p, c_size_t, c_void_p, POINTER(c_uint32), POINTER(c_uint64))(_cb)
|
||||
|
||||
path_str = Paths.params_root().encode('utf-8')
|
||||
cf_path = CF.CFStringCreateWithCString(kCFAllocatorDefault, path_str, kCFStringEncodingUTF8)
|
||||
cf_paths = CF.CFArrayCreate(kCFAllocatorDefault, (c_void_p * 1)(cf_path), 1, None)
|
||||
stream = CS.FSEventStreamCreate(kCFAllocatorDefault, self._darwin_cb, None, cf_paths, -1, 0.05, kFSEventStreamCreateFlagFileEvents)
|
||||
|
||||
run_loop = CF.CFRunLoopGetCurrent()
|
||||
kDefaultMode = CF.CFStringCreateWithCString(kCFAllocatorDefault, b"kCFRunLoopDefaultMode", kCFStringEncodingUTF8)
|
||||
CS.FSEventStreamScheduleWithRunLoop(stream, run_loop, kDefaultMode)
|
||||
CS.FSEventStreamStart(stream)
|
||||
CF.CFRunLoopRun()
|
||||
4
sunnypilot/common/params.py
Normal file
4
sunnypilot/common/params.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from openpilot.common.params_pyx import ParamKeyFlag, ParamKeyType, UnknownKeyName
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher as Params
|
||||
|
||||
__all__ = ["Params", "ParamKeyFlag", "ParamKeyType", "UnknownKeyName"]
|
||||
0
sunnypilot/common/tests/__init__.py
Normal file
0
sunnypilot/common/tests/__init__.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import time
|
||||
import pytest
|
||||
import threading
|
||||
import tracemalloc
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.params_pyx import UnknownKeyName
|
||||
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher
|
||||
|
||||
|
||||
class TestParamWatcher:
|
||||
BYTES_KEYS = ["LocationFilterInitialState", "UpdaterCurrentReleaseNotes", "UpdaterNewReleaseNotes"]
|
||||
BOOL_KEYS = [
|
||||
"IsMetric", "AdbEnabled", "AlwaysOnDM", "ExperimentalMode",
|
||||
"ExperimentalModeConfirmed", "DisengageOnAccelerator",
|
||||
"OpenpilotEnabledToggle", "RecordAudio", "RecordFront"
|
||||
]
|
||||
_key_counter = 0
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
self.key_index = TestParamWatcher._key_counter
|
||||
TestParamWatcher._key_counter += 1
|
||||
self.bytes_key = self.BYTES_KEYS[self.key_index % len(self.BYTES_KEYS)]
|
||||
self.bool_key = self.BOOL_KEYS[self.key_index % len(self.BOOL_KEYS)]
|
||||
|
||||
@pytest.fixture
|
||||
def param_watcher(self):
|
||||
ParamWatcher._instance = None
|
||||
param_watch = ParamWatcher()
|
||||
param_watch.start()
|
||||
assert param_watch.is_watching(), "ParamWatcher thread died"
|
||||
return param_watch
|
||||
|
||||
def teardown_method(self):
|
||||
for key in (self.bytes_key, self.bool_key):
|
||||
try:
|
||||
self.params.remove(key)
|
||||
except UnknownKeyName:
|
||||
pass
|
||||
|
||||
def test_watcher_detects_change(self, param_watcher):
|
||||
val = b"123"
|
||||
self.params.put(self.bytes_key, val)
|
||||
assert param_watcher.get(self.bytes_key) == val
|
||||
|
||||
def test_watcher_get_bool(self, param_watcher):
|
||||
self.params.put_bool(self.bool_key, True)
|
||||
assert param_watcher.get_bool(self.bool_key) is True # First read should populate internal cache
|
||||
|
||||
def test_performance_comparison(self, param_watcher):
|
||||
plain_params = self.params
|
||||
|
||||
for key in self.BYTES_KEYS:
|
||||
plain_params.put(key, b"x" * 10000)
|
||||
param_watcher.get(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
plain_params.put_bool(key, True)
|
||||
param_watcher.get_bool(key)
|
||||
|
||||
def bench(get_bytes, get_bool):
|
||||
tracemalloc.start()
|
||||
start_time = time.process_time()
|
||||
for _ in range(1000):
|
||||
for key in self.BYTES_KEYS:
|
||||
get_bytes(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
get_bool(key)
|
||||
duration = time.process_time() - start_time
|
||||
_, memory = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
return duration, memory
|
||||
|
||||
plain_cpu, plain_memory = bench(plain_params.get, plain_params.get_bool)
|
||||
watcher_cpu, watcher_memory = bench(param_watcher.get, param_watcher.get_bool)
|
||||
|
||||
# ParamWatcher *should* be significantly faster and use less memory than Params()
|
||||
assert watcher_cpu < plain_cpu * 0.6, f"PW CPU ({watcher_cpu:.4f}s) should be < 60% of Param call ({plain_cpu:.4f}s)"
|
||||
assert watcher_memory < plain_memory * 0.5, f"PW Memory ({watcher_memory}B) should be < 50% of Param call ({plain_memory}B)"
|
||||
|
||||
def test_cache_invalidation_simulation(self, param_watcher):
|
||||
self.params.put(self.bytes_key, b"old")
|
||||
assert param_watcher.get(self.bytes_key) == b"old"
|
||||
time.sleep(0.2)
|
||||
|
||||
event = threading.Event()
|
||||
param_watcher.add_watcher(lambda key: event.set())
|
||||
param_watcher._trigger_callbacks(self.bytes_key)
|
||||
assert event.wait(timeout=2), "Callback not triggered"
|
||||
|
||||
self.params.put(self.bytes_key, b"new")
|
||||
assert param_watcher.get(self.bytes_key) == b"new"
|
||||
@@ -64,7 +64,12 @@ def request_refresh_osm_location_data(nations: list[str], states: list[str] = No
|
||||
"states": states or []
|
||||
}
|
||||
|
||||
print(f"Downloading maps for {json.dumps(osm_download_locations)}")
|
||||
osm_download_locations_dump = json.dumps({
|
||||
"nations": nations,
|
||||
"states": states or []
|
||||
})
|
||||
|
||||
print(f"Downloading maps for {osm_download_locations_dump}")
|
||||
mem_params.put("OSMDownloadLocations", osm_download_locations)
|
||||
|
||||
|
||||
@@ -98,6 +103,8 @@ def filter_nations_and_states(nations: list[str], states: list[str] = None) -> t
|
||||
|
||||
|
||||
def update_osm_db() -> None:
|
||||
# last_downloaded_date = params.get("OsmDownloadedDate", return_default=True)
|
||||
# if params.get_bool("OsmDbUpdatesCheck") or time.monotonic() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60
|
||||
if params.get_bool("OsmDbUpdatesCheck"):
|
||||
cleanup_old_osm_data(get_files_for_cleanup())
|
||||
country = params.get("OsmLocationName", return_default=True)
|
||||
|
||||
@@ -11,12 +11,13 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
ConfidenceClass = log.ModelDataV2.ConfidenceClass
|
||||
|
||||
|
||||
def get_curvature_from_output(output, plan, vego, lat_action_t, mlsim):
|
||||
def get_curvature_from_output(output, vego, lat_action_t, mlsim):
|
||||
if not mlsim:
|
||||
if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly
|
||||
return float(desired_curv[0, 0])
|
||||
|
||||
return float(get_curvature_from_plan(plan[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan[:, Plan.ORIENTATION_RATE][:, 2],
|
||||
plan_output = output['plan'][0]
|
||||
return float(get_curvature_from_plan(plan_output[:, Plan.T_FROM_CURRENT_EULER][:, 2], plan_output[:, Plan.ORIENTATION_RATE][:, 2],
|
||||
ModelConstants.T_IDXS, vego, lat_action_t))
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||
RECOVERY_POWER = 1.0 # The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -157,13 +156,11 @@ class ModelState(ModelStateBase):
|
||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
||||
plan = model_output['plan'][0]
|
||||
if 'planplus' in model_output:
|
||||
plan = plan + RECOVERY_POWER*model_output['planplus'][0]
|
||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
action_t=long_action_t)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
|
||||
desired_curvature = get_curvature_from_output(model_output, plan, v_ego, lat_action_t, self.mlsim)
|
||||
desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim)
|
||||
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
|
||||
if v_ego > self.MIN_LAT_CONTROL_SPEED:
|
||||
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
|
||||
|
||||
@@ -108,8 +108,6 @@ class Parser:
|
||||
plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N,
|
||||
out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
|
||||
if 'planplus' in outs:
|
||||
self.parse_mdn('planplus', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
|
||||
|
||||
def split_outputs(self, outs: dict[str, np.ndarray]) -> None:
|
||||
if 'desired_curvature' in outs:
|
||||
|
||||
@@ -19,7 +19,7 @@ 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 = 13
|
||||
CURRENT_SELECTOR_VERSION = 12
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 12
|
||||
|
||||
USE_ONNX = os.getenv('USE_ONNX', PC)
|
||||
|
||||
@@ -1 +1 @@
|
||||
6168bc755ea17aececa535e8b94e6c798e5e855bfc47be19220d5cbc08483332
|
||||
030a2a502e95e51290bb1d76795013b72b25521a572c3942a232b9395e544250
|
||||
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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 custom
|
||||
import numpy as np
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
|
||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
|
||||
# Acceleration Profiles
|
||||
MAX_ACCEL_PROFILES = {
|
||||
AccelPersonality.eco: [2.0, 1.99, 1.88, 1.10, 0.500, 0.292, 0.15, 0.10],
|
||||
AccelPersonality.normal: [1.0, 2.00, 1.94, 1.22, 0.635, 0.33, 0.22, 0.16],
|
||||
AccelPersonality.sport: [.5, 2.00, 2.00, 1.85, 0.800, 0.54, 0.32, 0.22],
|
||||
}
|
||||
MAX_ACCEL_BREAKPOINTS = [0., 4., 6., 9., 16., 25., 30., 55.]
|
||||
# Braking Profiles
|
||||
MIN_ACCEL_PROFILES = {
|
||||
AccelPersonality.eco: [-0.14, -0.0006, -0.010, -0.30, -1.20],
|
||||
AccelPersonality.normal: [-0.1, -0.0007, -0.012, -0.35, -1.20],
|
||||
AccelPersonality.sport: [-0.6, -0.0008, -0.014, -0.40, -1.20],
|
||||
}
|
||||
MIN_ACCEL_BREAKPOINTS = [0., 3., 11., 14., 50.]
|
||||
|
||||
|
||||
class AccelPersonalityController:
|
||||
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.frame = 0
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.param_keys = {
|
||||
'personality': 'AccelPersonality',
|
||||
'enabled': 'AccelPersonalityEnabled'
|
||||
}
|
||||
self._load_personality_from_params()
|
||||
|
||||
def _load_personality_from_params(self):
|
||||
try:
|
||||
saved = self.params.get(self.param_keys['personality'])
|
||||
if saved is not None:
|
||||
personality_value = int(saved)
|
||||
if personality_value in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
|
||||
self.accel_personality = personality_value
|
||||
else:
|
||||
cloudlog.warning(f"Invalid personality value {personality_value}, using normal")
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
except (ValueError, TypeError) as e:
|
||||
cloudlog.warning(f"Failed to load personality from params: {e}")
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
|
||||
def _update_from_params(self):
|
||||
if self.frame % int(1. / DT_MDL) != 0:
|
||||
return
|
||||
self._load_personality_from_params()
|
||||
|
||||
def get_accel_personality(self) -> int:
|
||||
self._update_from_params()
|
||||
return int(self.accel_personality)
|
||||
|
||||
def set_accel_personality(self, personality: int):
|
||||
if personality not in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
|
||||
cloudlog.error(f"Invalid personality {personality}, ignoring")
|
||||
return
|
||||
|
||||
self.accel_personality = personality
|
||||
self.params.put(self.param_keys['personality'], str(personality))
|
||||
cloudlog.info(f"Accel personality set to {personality}")
|
||||
|
||||
def cycle_accel_personality(self) -> int:
|
||||
personalities = [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]
|
||||
current_idx = personalities.index(self.accel_personality)
|
||||
next_personality = personalities[(current_idx + 1) % len(personalities)]
|
||||
self.set_accel_personality(next_personality)
|
||||
return int(next_personality)
|
||||
|
||||
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
|
||||
max_a = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[self.accel_personality])
|
||||
min_a = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[self.accel_personality])
|
||||
return float(min_a), float(max_a)
|
||||
|
||||
def get_min_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[0]
|
||||
|
||||
def get_max_accel(self, v_ego: float) -> float:
|
||||
return self.get_accel_limits(v_ego)[1]
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
return bool(self.params.get_bool(self.param_keys['enabled']))
|
||||
|
||||
def set_enabled(self, enabled: bool):
|
||||
self.params.put_bool(self.param_keys['enabled'], enabled)
|
||||
cloudlog.info(f"Accel personality controller {'enabled' if enabled else 'disabled'}")
|
||||
|
||||
def toggle_enabled(self) -> bool:
|
||||
current = self.is_enabled()
|
||||
self.set_enabled(not current)
|
||||
return not current
|
||||
|
||||
def reset(self):
|
||||
self.accel_personality = AccelPersonality.normal
|
||||
self.frame = 0
|
||||
|
||||
def update(self):
|
||||
self.frame += 1
|
||||
self._update_from_params()
|
||||
@@ -1,286 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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 pytest
|
||||
import numpy as np
|
||||
from cereal import custom
|
||||
from sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import (
|
||||
AccelPersonalityController,
|
||||
MAX_ACCEL_PROFILES,
|
||||
MIN_ACCEL_PROFILES,
|
||||
MAX_ACCEL_BREAKPOINTS,
|
||||
MIN_ACCEL_BREAKPOINTS,
|
||||
)
|
||||
|
||||
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
|
||||
|
||||
class TestAccelPersonalityController:
|
||||
|
||||
@pytest.fixture
|
||||
def mock_params(self, mocker):
|
||||
params = mocker.Mock()
|
||||
params.get = mocker.Mock(return_value=None)
|
||||
params.get_bool = mocker.Mock(return_value=False)
|
||||
params.put = mocker.Mock()
|
||||
params.put_bool = mocker.Mock()
|
||||
return params
|
||||
|
||||
@pytest.fixture
|
||||
def controller(self, mock_params, mocker):
|
||||
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
|
||||
ctrl = AccelPersonalityController()
|
||||
ctrl.params = mock_params
|
||||
return ctrl
|
||||
|
||||
def test_initialization_defaults(self, controller):
|
||||
assert controller.frame == 0
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
assert controller.param_keys == {
|
||||
'personality': 'AccelPersonality',
|
||||
'enabled': 'AccelPersonalityEnabled'
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("personality,expected", [
|
||||
(AccelPersonality.eco, AccelPersonality.eco),
|
||||
(AccelPersonality.normal, AccelPersonality.normal),
|
||||
(AccelPersonality.sport, AccelPersonality.sport),
|
||||
])
|
||||
def test_load_personality_valid(self, mocker, personality, expected):
|
||||
mock_params = mocker.Mock()
|
||||
mock_params.get = mocker.Mock(return_value=str(personality).encode())
|
||||
mock_params.get_bool = mocker.Mock(return_value=False)
|
||||
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
|
||||
controller = AccelPersonalityController()
|
||||
assert controller.accel_personality == expected
|
||||
|
||||
def test_load_personality_invalid_value(self, mocker):
|
||||
mock_params = mocker.Mock()
|
||||
mock_params.get = mocker.Mock(return_value=b'999')
|
||||
mock_params.get_bool = mocker.Mock(return_value=False)
|
||||
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
|
||||
controller = AccelPersonalityController()
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
|
||||
def test_load_personality_parse_error(self, mocker):
|
||||
mock_params = mocker.Mock()
|
||||
mock_params.get = mocker.Mock(return_value=b'invalid_data')
|
||||
mock_params.get_bool = mocker.Mock(return_value=False)
|
||||
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
|
||||
controller = AccelPersonalityController()
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
|
||||
def test_load_personality_none(self, mocker):
|
||||
mock_params = mocker.Mock()
|
||||
mock_params.get = mocker.Mock(return_value=None)
|
||||
mock_params.get_bool = mocker.Mock(return_value=False)
|
||||
mocker.patch('sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller.Params', return_value=mock_params)
|
||||
controller = AccelPersonalityController()
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
|
||||
def test_get_accel_personality(self, controller):
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
result = controller.get_accel_personality()
|
||||
assert result == AccelPersonality.sport
|
||||
assert isinstance(result, int)
|
||||
|
||||
@pytest.mark.parametrize("personality", [
|
||||
AccelPersonality.eco,
|
||||
AccelPersonality.normal,
|
||||
AccelPersonality.sport,
|
||||
])
|
||||
def test_set_accel_personality_valid(self, controller, mock_params, personality):
|
||||
controller.set_accel_personality(personality)
|
||||
assert controller.accel_personality == personality
|
||||
mock_params.put.assert_called_once_with('AccelPersonality', str(personality))
|
||||
|
||||
def test_set_accel_personality_invalid(self, controller, mock_params):
|
||||
original = controller.accel_personality
|
||||
controller.set_accel_personality(999)
|
||||
assert controller.accel_personality == original
|
||||
mock_params.put.assert_not_called()
|
||||
|
||||
def test_cycle_accel_personality_full_cycle(self, controller):
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
|
||||
result = controller.cycle_accel_personality()
|
||||
assert result == AccelPersonality.normal
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
|
||||
result = controller.cycle_accel_personality()
|
||||
assert result == AccelPersonality.sport
|
||||
assert controller.accel_personality == AccelPersonality.sport
|
||||
|
||||
result = controller.cycle_accel_personality()
|
||||
assert result == AccelPersonality.eco
|
||||
assert controller.accel_personality == AccelPersonality.eco
|
||||
|
||||
def test_cycle_accel_personality_return_type(self, controller):
|
||||
result = controller.cycle_accel_personality()
|
||||
assert isinstance(result, int)
|
||||
|
||||
@pytest.mark.parametrize("personality", [
|
||||
AccelPersonality.eco,
|
||||
AccelPersonality.normal,
|
||||
AccelPersonality.sport,
|
||||
])
|
||||
def test_get_accel_limits_at_zero_speed(self, controller, personality):
|
||||
controller.accel_personality = personality
|
||||
min_a, max_a = controller.get_accel_limits(0.0)
|
||||
|
||||
expected_max = MAX_ACCEL_PROFILES[personality][0]
|
||||
expected_min = MIN_ACCEL_PROFILES[personality][0]
|
||||
|
||||
assert abs(max_a - expected_max) < 1e-6
|
||||
assert abs(min_a - expected_min) < 1e-6
|
||||
|
||||
@pytest.mark.parametrize("v_ego,personality", [
|
||||
(0.0, AccelPersonality.eco),
|
||||
(10.0, AccelPersonality.normal),
|
||||
(25.0, AccelPersonality.sport),
|
||||
(50.0, AccelPersonality.eco),
|
||||
(4.0, AccelPersonality.normal),
|
||||
(30.0, AccelPersonality.sport),
|
||||
])
|
||||
def test_get_accel_limits_interpolation(self, controller, v_ego, personality):
|
||||
controller.accel_personality = personality
|
||||
min_a, max_a = controller.get_accel_limits(v_ego)
|
||||
|
||||
expected_max = np.interp(v_ego, MAX_ACCEL_BREAKPOINTS, MAX_ACCEL_PROFILES[personality])
|
||||
expected_min = np.interp(v_ego, MIN_ACCEL_BREAKPOINTS, MIN_ACCEL_PROFILES[personality])
|
||||
|
||||
assert abs(max_a - expected_max) < 1e-6
|
||||
assert abs(min_a - expected_min) < 1e-6
|
||||
|
||||
def test_get_accel_limits_return_types(self, controller):
|
||||
min_a, max_a = controller.get_accel_limits(10.0)
|
||||
assert isinstance(min_a, float)
|
||||
assert isinstance(max_a, float)
|
||||
|
||||
def test_get_min_accel(self, controller):
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
v_ego = 15.0
|
||||
min_a = controller.get_min_accel(v_ego)
|
||||
expected = controller.get_accel_limits(v_ego)[0]
|
||||
assert min_a == expected
|
||||
assert isinstance(min_a, float)
|
||||
|
||||
def test_get_max_accel(self, controller):
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
v_ego = 20.0
|
||||
max_a = controller.get_max_accel(v_ego)
|
||||
expected = controller.get_accel_limits(v_ego)[1]
|
||||
assert max_a == expected
|
||||
assert isinstance(max_a, float)
|
||||
|
||||
def test_is_enabled_true(self, controller, mock_params):
|
||||
mock_params.get_bool.return_value = True
|
||||
assert controller.is_enabled() is True
|
||||
|
||||
def test_is_enabled_false(self, controller, mock_params):
|
||||
mock_params.get_bool.return_value = False
|
||||
assert controller.is_enabled() is False
|
||||
|
||||
def test_is_enabled_calls_params(self, controller, mock_params):
|
||||
controller.is_enabled()
|
||||
mock_params.get_bool.assert_called_once_with('AccelPersonalityEnabled')
|
||||
|
||||
@pytest.mark.parametrize("enabled", [True, False])
|
||||
def test_set_enabled(self, controller, mock_params, enabled):
|
||||
controller.set_enabled(enabled)
|
||||
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', enabled)
|
||||
|
||||
def test_toggle_enabled_from_false(self, controller, mock_params):
|
||||
mock_params.get_bool.return_value = False
|
||||
result = controller.toggle_enabled()
|
||||
assert result is True
|
||||
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', True)
|
||||
|
||||
def test_toggle_enabled_from_true(self, controller, mock_params):
|
||||
mock_params.get_bool.return_value = True
|
||||
result = controller.toggle_enabled()
|
||||
assert result is False
|
||||
mock_params.put_bool.assert_called_once_with('AccelPersonalityEnabled', False)
|
||||
|
||||
def test_reset(self, controller):
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
controller.frame = 100
|
||||
controller.reset()
|
||||
assert controller.accel_personality == AccelPersonality.normal
|
||||
assert controller.frame == 0
|
||||
|
||||
def test_update_increments_frame(self, controller):
|
||||
initial_frame = controller.frame
|
||||
controller.update()
|
||||
assert controller.frame == initial_frame + 1
|
||||
|
||||
def test_update_multiple_calls(self, controller):
|
||||
for i in range(1, 11):
|
||||
controller.update()
|
||||
assert controller.frame == i
|
||||
|
||||
@pytest.mark.parametrize("v_ego", [0, 5, 10, 15, 20, 25, 30, 40, 50, 55])
|
||||
@pytest.mark.parametrize("personality", [
|
||||
AccelPersonality.eco,
|
||||
AccelPersonality.normal,
|
||||
AccelPersonality.sport,
|
||||
])
|
||||
def test_accel_limits_physical_constraints(self, controller, v_ego, personality):
|
||||
controller.accel_personality = personality
|
||||
min_a, max_a = controller.get_accel_limits(v_ego)
|
||||
assert min_a < 0
|
||||
assert max_a > 0
|
||||
assert min_a < max_a
|
||||
|
||||
def test_max_accel_decreases_with_speed(self, controller):
|
||||
test_speeds = [0, 10, 20, 30, 40, 50]
|
||||
for personality in [AccelPersonality.eco, AccelPersonality.normal, AccelPersonality.sport]:
|
||||
controller.accel_personality = personality
|
||||
max_accels = [controller.get_max_accel(v) for v in test_speeds]
|
||||
assert max_accels[-1] < max_accels[0]
|
||||
|
||||
@pytest.mark.parametrize("v_ego", [5, 10, 15, 20, 25])
|
||||
def test_acceleration_personality_ordering(self, controller, v_ego):
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
_, eco_max = controller.get_accel_limits(v_ego)
|
||||
|
||||
controller.accel_personality = AccelPersonality.normal
|
||||
_, normal_max = controller.get_accel_limits(v_ego)
|
||||
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
_, sport_max = controller.get_accel_limits(v_ego)
|
||||
|
||||
assert sport_max >= normal_max
|
||||
assert normal_max >= eco_max
|
||||
|
||||
@pytest.mark.parametrize("v_ego", [5, 10, 15, 20])
|
||||
def test_braking_personality_ordering(self, controller, v_ego):
|
||||
controller.accel_personality = AccelPersonality.eco
|
||||
eco_min, _ = controller.get_accel_limits(v_ego)
|
||||
|
||||
controller.accel_personality = AccelPersonality.sport
|
||||
sport_min, _ = controller.get_accel_limits(v_ego)
|
||||
|
||||
assert sport_min <= eco_min
|
||||
|
||||
def test_accel_limits_at_max_speed(self, controller):
|
||||
max_speed = MAX_ACCEL_BREAKPOINTS[-1]
|
||||
min_a, max_a = controller.get_accel_limits(max_speed)
|
||||
assert isinstance(min_a, float)
|
||||
assert isinstance(max_a, float)
|
||||
|
||||
def test_accel_limits_beyond_max_speed(self, controller):
|
||||
beyond_max = MAX_ACCEL_BREAKPOINTS[-1] + 10
|
||||
min_a, max_a = controller.get_accel_limits(beyond_max)
|
||||
assert isinstance(min_a, float)
|
||||
assert isinstance(max_a, float)
|
||||
|
||||
def test_accel_limits_very_low_speed(self, controller):
|
||||
min_a, max_a = controller.get_accel_limits(0.5)
|
||||
assert isinstance(min_a, float)
|
||||
assert isinstance(max_a, float)
|
||||
assert min_a >= -2.0
|
||||
@@ -15,8 +15,9 @@ LAT_PLAN_MIN_IDX = 5
|
||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||
|
||||
# from selfdrive/controls/lib/latcontrol_torque.py
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
KD = 0.0
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
@@ -63,7 +64,7 @@ class LatControlTorqueExtBase:
|
||||
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
|
||||
|
||||
self._ff = 0.0
|
||||
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI)
|
||||
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD)
|
||||
self._pid_log = None
|
||||
self._setpoint = 0.0
|
||||
self._measurement = 0.0
|
||||
|
||||
@@ -17,7 +17,6 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
|
||||
@@ -27,7 +26,6 @@ class LongitudinalPlannerSP:
|
||||
self.events_sp = EventsSP()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.dec = DynamicExperimentalController(CP, mpc)
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
self.scc = SmartCruiseControl()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
@@ -83,7 +81,6 @@ class LongitudinalPlannerSP:
|
||||
self.events_sp.clear()
|
||||
self.dec.update(sm)
|
||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||
self.accel_controller.update()
|
||||
|
||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||
|
||||
@@ -2,10 +2,9 @@ import json
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import jwt
|
||||
from typing import cast
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from openpilot.common.api.base import BaseApi
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
@@ -93,8 +92,7 @@ class SunnylinkApi(BaseApi):
|
||||
|
||||
backoff = 1
|
||||
while True:
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)},
|
||||
cast(str, private_key), algorithm=jwt_algo)
|
||||
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...")
|
||||
|
||||
@@ -1,26 +1,4 @@
|
||||
{
|
||||
"AccelPersonality": {
|
||||
"title": "Acceleration Personality",
|
||||
"description": "",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Sport"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Normal"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Eco"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AccelPersonalityEnabled": {
|
||||
"title": "Enable Acceleration Personality",
|
||||
"description": "Controls acceleration behavior: Eco (efficient), Normal (balanced), Sport (responsive)."
|
||||
},
|
||||
"AccessToken": {
|
||||
"title": "AccessTokenIsNice",
|
||||
"description": ""
|
||||
@@ -280,11 +258,11 @@
|
||||
"description": ""
|
||||
},
|
||||
"EnableCopyparty": {
|
||||
"title": "copyparty Service",
|
||||
"title": "Enable Copyparty",
|
||||
"description": ""
|
||||
},
|
||||
"EnableGithubRunner": {
|
||||
"title": "GitHub Runner Service",
|
||||
"title": "Enable GitHub Runner",
|
||||
"description": ""
|
||||
},
|
||||
"EnableSunnylinkUploader": {
|
||||
@@ -480,10 +458,6 @@
|
||||
"title": "Language",
|
||||
"description": ""
|
||||
},
|
||||
"LastAgnosPowerMonitorShutdown": {
|
||||
"title": "Last AGNOS Power Monitor Shutdown",
|
||||
"description": ""
|
||||
},
|
||||
"LastAthenaPingTime": {
|
||||
"title": "Last Athena Ping Time",
|
||||
"description": ""
|
||||
|
||||
@@ -17,7 +17,7 @@ from cereal.messaging import SubMaster
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.common.utils import atomic_write
|
||||
from openpilot.common.utils import atomic_write_in_dir
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S
|
||||
from openpilot.system.statsd import METRIC_TYPE, StatLogSP
|
||||
@@ -242,7 +242,7 @@ def stats_main(end_event):
|
||||
if len(os.listdir(STATS_DIR)) < STATS_DIR_FILE_LIMIT:
|
||||
if len(result) > 0:
|
||||
stats_path = os.path.join(STATS_DIR, f"{boot_uid}_{idx}")
|
||||
with atomic_write(stats_path) as f:
|
||||
with atomic_write_in_dir(stats_path) as f:
|
||||
f.write(result)
|
||||
idx += 1
|
||||
else:
|
||||
|
||||
0
sunnypilot/tools/__init__.py
Normal file
0
sunnypilot/tools/__init__.py
Normal file
171
sunnypilot/tools/profile_params.py
Executable file
171
sunnypilot/tools/profile_params.py
Executable file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import ctypes
|
||||
import csv
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import select
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as ticker
|
||||
from collections import defaultdict
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher, IN_CLOSE_WRITE, IN_MOVED_TO
|
||||
|
||||
IN_ACCESS = 0x00000001
|
||||
|
||||
|
||||
def get_linux_monitor(params_path, reads, writes):
|
||||
libc = ctypes.CDLL('libc.so.6')
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
return None
|
||||
|
||||
mask = IN_ACCESS | IN_MOVED_TO | IN_CLOSE_WRITE
|
||||
if libc.inotify_add_watch(fd, params_path.encode(), mask) < 0:
|
||||
return None
|
||||
|
||||
poll_obj = select.epoll()
|
||||
poll_obj.register(fd, select.EPOLLIN)
|
||||
|
||||
def monitor():
|
||||
for fileno, _ in poll_obj.poll(0.1):
|
||||
if fileno == fd:
|
||||
buffer = os.read(fd, 2048)
|
||||
i = 0
|
||||
while i + 16 <= len(buffer):
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buffer, i)
|
||||
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode('utf-8', 'ignore')
|
||||
if name and not name.startswith("."):
|
||||
if mask & IN_ACCESS:
|
||||
reads[name] += 1
|
||||
elif mask & (IN_MOVED_TO | IN_CLOSE_WRITE):
|
||||
writes[name] += 1
|
||||
i += 16 + name_len
|
||||
|
||||
def cleanup():
|
||||
os.close(fd)
|
||||
return monitor, cleanup
|
||||
|
||||
def get_darwin_monitor(params_path, reads, writes):
|
||||
print("WARNING: macOS only reports WRITES.")
|
||||
|
||||
def callback(name):
|
||||
writes[name] += 1
|
||||
|
||||
watcher = ParamWatcher()
|
||||
watcher.add_watcher(callback)
|
||||
|
||||
def monitor():
|
||||
time.sleep(0.1)
|
||||
|
||||
def cleanup():
|
||||
if callback in watcher._callbacks:
|
||||
watcher._callbacks.remove(callback)
|
||||
return monitor, cleanup
|
||||
|
||||
def profile_params():
|
||||
parser = argparse.ArgumentParser(description="Profile Params I/O")
|
||||
parser.add_argument("--timeout", type=int, default=30, help="Timeout in minutes (default: 30 mins)")
|
||||
default_out = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"params_profile_{random.randrange(99999)}.csv")
|
||||
parser.add_argument("--out", type=str, default=default_out, help="Output CSV file")
|
||||
args = parser.parse_args()
|
||||
|
||||
path = Paths.params_root()
|
||||
if not os.path.exists(path):
|
||||
return print(f"Error: {path} not found")
|
||||
|
||||
print(f"Profiling Params I/O at: {path}\nPress CTRL+C to stop.")
|
||||
reads, writes = defaultdict(int), defaultdict(int)
|
||||
|
||||
setup = get_linux_monitor if platform.system() == "Linux" else \
|
||||
get_darwin_monitor if platform.system() == "Darwin" else None
|
||||
|
||||
if not setup:
|
||||
return print("Unsupported platform")
|
||||
monitor, cleanup = setup(path, reads, writes) or (None, None)
|
||||
|
||||
if not monitor:
|
||||
return print("Failed to initialize monitor")
|
||||
|
||||
start_time = time.monotonic()
|
||||
timeout_seconds = args.timeout * 60
|
||||
last_print = start_time
|
||||
|
||||
try:
|
||||
while True:
|
||||
monitor()
|
||||
if time.monotonic() - last_print > 1.0:
|
||||
sys.stdout.write(".")
|
||||
sys.stdout.flush()
|
||||
last_print = time.monotonic()
|
||||
|
||||
if args.timeout > 0 and (time.monotonic() - start_time) > timeout_seconds:
|
||||
print("\nTimeout reached.")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nStopping...")
|
||||
finally:
|
||||
cleanup()
|
||||
|
||||
duration = time.monotonic() - start_time
|
||||
|
||||
|
||||
with open(args.out, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['Param Name', 'Reads/sec', 'Writes/sec', 'Total Reads', 'Total Writes'])
|
||||
for k in sorted(set(reads) | set(writes), key=lambda k: reads[k] + writes[k], reverse=True):
|
||||
writer.writerow([k, f"{reads[k]/duration:.1f}", f"{writes[k]/duration:.1f}", reads[k], writes[k]])
|
||||
print(f"CSV report saved to {args.out}")
|
||||
|
||||
|
||||
data = []
|
||||
for k in sorted(set(reads) | set(writes), key=lambda k: reads[k] + writes[k], reverse=True):
|
||||
data.append((k, reads[k]/duration, writes[k]/duration))
|
||||
|
||||
if data:
|
||||
data = data[:10]
|
||||
names = [x[0] for x in data]
|
||||
read_rates = [x[1] for x in data]
|
||||
write_rates = [x[2] for x in data]
|
||||
|
||||
bar_height = 0.35
|
||||
plt.figure(figsize=(12, len(names) * 0.5 + 2), dpi=150)
|
||||
y_pos = range(len(names))
|
||||
|
||||
y_pos_reads = [y - bar_height/2 for y in y_pos]
|
||||
y_pos_writes = [y + bar_height/2 for y in y_pos]
|
||||
|
||||
plt.barh(y_pos_reads, read_rates, height=bar_height, align='center', color='dodgerblue', alpha=0.8, label='Reads/sec')
|
||||
plt.barh(y_pos_writes, write_rates, height=bar_height, align='center', color='red', alpha=0.8, label='Writes/sec')
|
||||
|
||||
for i, (r_rate, w_rate) in enumerate(zip(read_rates, write_rates, strict=False)):
|
||||
if r_rate > 0:
|
||||
plt.text(r_rate, y_pos_reads[i], f"{r_rate:.2f}", va='center', fontsize=8, color='#005a9e', fontweight='bold')
|
||||
if w_rate > 0:
|
||||
plt.text(w_rate, y_pos_writes[i], f"{w_rate:.2f}", va='center', fontsize=8, color='#a30000', fontweight='bold')
|
||||
|
||||
max_val = max(max(read_rates), max(write_rates)) if read_rates else 0
|
||||
|
||||
plt.xlim(0, max_val * 1.15)
|
||||
plt.yticks(y_pos, names)
|
||||
plt.xlabel('Rate (Hz)')
|
||||
plt.title('Top 10 Params I/O Profile')
|
||||
plt.legend()
|
||||
plt.grid(axis='x', linestyle='--', alpha=0.5)
|
||||
plt.gca().xaxis.set_major_locator(ticker.MaxNLocator(integer=True, nbins='auto'))
|
||||
plt.tight_layout()
|
||||
plt.gca().invert_yaxis()
|
||||
|
||||
plot_filename = os.path.splitext(args.out)[0] + ".png"
|
||||
plt.savefig(plot_filename)
|
||||
print(f"Plot saved to {plot_filename}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
profile_params()
|
||||
@@ -31,7 +31,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
|
||||
import cereal.messaging as messaging
|
||||
from cereal import log
|
||||
from cereal.services import SERVICE_LIST
|
||||
from openpilot.common.api import Api, get_key_pair
|
||||
from openpilot.common.api import Api
|
||||
from openpilot.common.utils import CallbackReader, get_upload_stream
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import set_core_affinity
|
||||
@@ -554,8 +554,11 @@ def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, w
|
||||
|
||||
@dispatcher.add_method
|
||||
def getPublicKey() -> str | None:
|
||||
_, _, public_key = get_key_pair()
|
||||
return public_key
|
||||
if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'):
|
||||
return None
|
||||
|
||||
with open(Paths.persist_root() + '/comma/id_rsa.pub') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import time
|
||||
import json
|
||||
import jwt
|
||||
from typing import cast
|
||||
from pathlib import Path
|
||||
|
||||
from datetime import datetime, timedelta, UTC
|
||||
@@ -70,9 +69,7 @@ def register(show_spinner=False) -> str | None:
|
||||
start_time = time.monotonic()
|
||||
while True:
|
||||
try:
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)},
|
||||
cast(str, private_key), algorithm=jwt_algo)
|
||||
cloudlog.info("getting pilotauth")
|
||||
register_token = jwt.encode({'register': True, 'exp': datetime.now(UTC).replace(tzinfo=None) + timedelta(hours=1)}, private_key, algorithm=jwt_algo)
|
||||
cloudlog.info("getting pilotauth")
|
||||
resp = api_get("v2/pilotauth/", method='POST', timeout=15,
|
||||
imei=imei1, imei2=imei2, serial=serial, public_key=public_key, register_token=register_token)
|
||||
|
||||
@@ -1004,8 +1004,8 @@ bool SpectraCamera::openSensor() {
|
||||
};
|
||||
|
||||
// Figure out which sensor we have
|
||||
if (!init_sensor_lambda(new OS04C10) &&
|
||||
!init_sensor_lambda(new OX03C10)) {
|
||||
if (!init_sensor_lambda(new OX03C10) &&
|
||||
!init_sensor_lambda(new OS04C10)) {
|
||||
LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num);
|
||||
enabled = false;
|
||||
return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user