mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-21 03:52:08 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38bb1407c9 | |||
| 613e8d27b5 | |||
| 3af7841a5d | |||
| 5c2c6a9c75 | |||
| acfbfea41d | |||
| 1dee05cb1f | |||
| 3ff8ccfc23 | |||
| c1f251a34f | |||
| 29e76a1e38 | |||
| 3384771117 | |||
| a6dfe14487 | |||
| b9c3ef57d1 | |||
| 9b5228ec08 |
@@ -39,4 +39,4 @@ jobs:
|
||||
git config --global --add safe.directory '*'
|
||||
git lfs pull
|
||||
- name: Push __nightly
|
||||
run: BRANCH=__nightly release/build_stripped.sh
|
||||
run: BRANCH=__nightly release/build_devel.sh
|
||||
|
||||
@@ -27,7 +27,7 @@ env:
|
||||
|
||||
RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c
|
||||
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical
|
||||
PYTEST: pytest --continue-on-collection-errors --durations=0 --durations-min=5 -n logical
|
||||
|
||||
jobs:
|
||||
build_release:
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
command: git lfs pull
|
||||
- name: Build devel
|
||||
timeout-minutes: 1
|
||||
run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh
|
||||
run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh
|
||||
- uses: ./.github/workflows/setup-with-retry
|
||||
- name: Build openpilot and run checks
|
||||
timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache
|
||||
@@ -190,8 +190,7 @@ jobs:
|
||||
timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 999 }}
|
||||
run: |
|
||||
${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \
|
||||
# Pre-compile Python bytecode so each pytest worker doesn't need to
|
||||
$PYTEST --collect-only -m 'not slow' -qq && \
|
||||
$PYTEST --collect-only -m 'not slow' &> /dev/null && \
|
||||
MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \
|
||||
./selfdrive/ui/tests/create_test_translations.sh && \
|
||||
QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \
|
||||
|
||||
@@ -9,6 +9,12 @@ env:
|
||||
|
||||
# Branch configurations
|
||||
STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings.
|
||||
DEV_C3_SOURCE_BRANCH: ${{ vars.DEV_C3_SOURCE_BRANCH || 'master-dev-c3-new' }} # vars are set on repo settings.
|
||||
|
||||
# Target branch configurations
|
||||
STAGING_TARGET_BRANCH: ${{ vars.STAGING_TARGET_BRANCH || 'staging-c3-new' }} # vars are set on repo settings.
|
||||
DEV_TARGET_BRANCH: ${{ vars.DEV_TARGET_BRANCH || 'dev-c3-new' }} # vars are set on repo settings.
|
||||
RELEASE_TARGET_BRANCH: ${{ vars.RELEASE_TARGET_BRANCH || 'release-c3-new' }} # vars are set on repo settings.
|
||||
|
||||
# Runtime configuration
|
||||
SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}"
|
||||
@@ -16,7 +22,7 @@ env:
|
||||
on:
|
||||
push:
|
||||
branches: [ master, master-dev-c3-new ]
|
||||
tags: [ 'release/*' ]
|
||||
tags: [ '*' ]
|
||||
pull_request_target:
|
||||
types: [ labeled ]
|
||||
workflow_dispatch:
|
||||
@@ -28,72 +34,9 @@ on:
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
prepare_strategy:
|
||||
runs-on: ubuntu-24.04
|
||||
if: (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
outputs:
|
||||
environment: ${{ steps.strategy.outputs.environment }}
|
||||
new_branch: ${{ steps.strategy.outputs.new_branch }}
|
||||
extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }}
|
||||
version: ${{ steps.strategy.outputs.version }}
|
||||
cancel_publish_in_progress: ${{ steps.strategy.outputs.cancel_publish_in_progress }}
|
||||
publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }}
|
||||
is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }}
|
||||
build: ${{ steps.strategy.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Extract deploy strategy
|
||||
id: strategy
|
||||
run: |
|
||||
echo '::group::Strategy Extraction'
|
||||
BRANCH="${{ github.head_ref || github.ref_name }}"
|
||||
echo "Current branch: $BRANCH"
|
||||
|
||||
STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}'
|
||||
CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" '
|
||||
.configs[] | select(.branch == $branch)
|
||||
')
|
||||
|
||||
BUILD="$(date '+%Y.%m.%d')-${{ github.run_number }}"
|
||||
if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then
|
||||
echo "No exact strategy match found. Falling back to feature/fork logic."
|
||||
IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}"
|
||||
FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" )
|
||||
NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt"
|
||||
|
||||
echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
|
||||
echo "version=$BUILD" >> $GITHUB_OUTPUT
|
||||
echo "cancel_publish_in_progress=true" >> $GITHUB_OUTPUT
|
||||
echo "publish_concurrency_group=publish-${BRANCH}" >> $GITHUB_OUTPUT
|
||||
echo "environment=feature-branch" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=feature-branch" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Matched config: $CONFIG"
|
||||
environment=$(echo "$CONFIG" | jq -r '.environment')
|
||||
echo "environment=$environment" >> $GITHUB_OUTPUT
|
||||
echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
|
||||
cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')";
|
||||
echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT
|
||||
echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT
|
||||
|
||||
is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')";
|
||||
echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
stable_version=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g');
|
||||
echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "build=$BUILD" >> $GITHUB_OUTPUT
|
||||
cat $GITHUB_OUTPUT
|
||||
|
||||
validate_tests:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: [ prepare_strategy ]
|
||||
if: ${{
|
||||
((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) ||
|
||||
(github.event_name == 'push' && needs.prepare_strategy.outputs.is_stable_branch == 'true') ||
|
||||
contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
}}
|
||||
if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Wait for Tests
|
||||
@@ -101,26 +44,19 @@ jobs:
|
||||
with:
|
||||
workflow: selfdrive_tests.yaml # The workflow file to monitor
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
should-wait-for-start: ${{ github.event_name == 'push' && 'true' || 'false' }}
|
||||
|
||||
build:
|
||||
needs: [ validate_tests, prepare_strategy ]
|
||||
needs: [ validate_tests ]
|
||||
concurrency:
|
||||
group: build-${{ github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
runs-on: [self-hosted, tici]
|
||||
outputs:
|
||||
new_branch: ${{ needs.prepare_strategy.outputs.new_branch }}
|
||||
version: ${{ needs.prepare_strategy.outputs.version }}
|
||||
extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
if: ${{
|
||||
(always() && !cancelled() && !failure()) &&
|
||||
needs.prepare_strategy.result == 'success' &&
|
||||
(needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') &&
|
||||
(!contains(github.event_name, 'pull_request') ||
|
||||
(github.event.action == 'labeled' && github.event.label.name == 'prebuilt'))
|
||||
}}
|
||||
new_branch: ${{ steps.set-env.outputs.new_branch }}
|
||||
version: ${{ steps.set-env.outputs.version }}
|
||||
extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }}
|
||||
commit_sha: ${{ steps.set-env.outputs.commit_sha }}
|
||||
if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -141,12 +77,61 @@ jobs:
|
||||
scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }}
|
||||
scons-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Set Feature Branch Prebuilt Configuration
|
||||
id: set_feature_configuration
|
||||
if: (
|
||||
!(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) &&
|
||||
!(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) &&
|
||||
!(startsWith(github.ref, 'refs/tags/'))
|
||||
)
|
||||
run: |
|
||||
echo "NEW_BRANCH=${{ env.SOURCE_BRANCH }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-prebuilt" >> $GITHUB_ENV
|
||||
echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set dev-c3-new prebuilt Configuration
|
||||
id: set_dev_configuration
|
||||
if: (
|
||||
steps.set_feature_configuration.outcome == 'skipped' &&
|
||||
env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH
|
||||
)
|
||||
run: |
|
||||
echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV
|
||||
echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV
|
||||
echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set staging-c3-new prebuilt Configuration
|
||||
id: set_staging_configuration
|
||||
if: (
|
||||
steps.set_feature_configuration.outcome == 'skipped' &&
|
||||
!contains(github.event_name, 'pull_request') &&
|
||||
steps.set_dev_configuration.outcome == 'skipped' &&
|
||||
(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH)
|
||||
)
|
||||
run: |
|
||||
echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV
|
||||
echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV
|
||||
echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV
|
||||
|
||||
- name: Set release-c3-new prebuilt Configuration
|
||||
id: set_tag_configuration
|
||||
if: (
|
||||
steps.set_feature_configuration.outcome == 'skipped' &&
|
||||
!contains(github.event_name, 'pull_request') &&
|
||||
steps.set_staging_configuration.outcome == 'skipped' &&
|
||||
startsWith(github.ref, 'refs/tags/')
|
||||
)
|
||||
run: |
|
||||
echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV
|
||||
echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV
|
||||
echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV
|
||||
|
||||
- name: Set environment variables
|
||||
id: set-env
|
||||
run: |
|
||||
echo "new_branch=${{ needs.prepare_strategy.outputs.new_branch }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT
|
||||
# Write to GITHUB_OUTPUT from environment variables
|
||||
echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT
|
||||
[[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT
|
||||
[[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Set up common environment
|
||||
@@ -241,19 +226,15 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
|
||||
|
||||
|
||||
|
||||
publish:
|
||||
concurrency:
|
||||
# We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name.
|
||||
# This means that if multiple commits come in while we're publishing, they will be queued up and publish one after the other.
|
||||
# Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time.
|
||||
group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }}
|
||||
cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }}
|
||||
if: ${{ (always() && !cancelled() && !failure()) && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
needs: [ build, prepare_strategy ]
|
||||
group: publish-${{ github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-24.04
|
||||
environment: ${{ needs.prepare_strategy.outputs.environment }}
|
||||
environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -285,7 +266,7 @@ jobs:
|
||||
"${{ needs.build.outputs.new_branch }}" \
|
||||
"${{ needs.build.outputs.version }}" \
|
||||
"https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \
|
||||
"${{ needs.build.outputs.extra_version_identifier }}"
|
||||
"-${{ needs.build.outputs.extra_version_identifier }}"
|
||||
|
||||
echo ""
|
||||
echo "---- ℹ️ To update the list of branches that auto deploy prebuilts -----"
|
||||
@@ -293,18 +274,11 @@ jobs:
|
||||
echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES"
|
||||
echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}"
|
||||
echo "3. Update as needed (JSON array with no spaces)"
|
||||
|
||||
- name: Tag ${{ needs.prepare_strategy.outputs.environment }}
|
||||
if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }}
|
||||
run: |
|
||||
TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}"
|
||||
git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.build.outputs.build }}."
|
||||
git push -f origin ${TAG}
|
||||
|
||||
notify:
|
||||
needs: [ build, publish ]
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' && !failure() && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Alpine Linux environment
|
||||
|
||||
Vendored
+1
-1
@@ -167,7 +167,7 @@ node {
|
||||
env.GIT_COMMIT = checkout(scm).GIT_COMMIT
|
||||
|
||||
def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging',
|
||||
'release-tici', 'testing-closet*', 'hotfix-*']
|
||||
'testing-closet*', 'hotfix-*']
|
||||
def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*')
|
||||
|
||||
if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) {
|
||||
|
||||
+5
-9
@@ -1,22 +1,18 @@
|
||||
Version 0.10.1 (2025-09-08)
|
||||
========================
|
||||
* Record driving feedback using LKAS button
|
||||
* Honda City 2023 support thanks to drFritz!
|
||||
|
||||
Version 0.10.0 (2025-08-05)
|
||||
========================
|
||||
* New driving model
|
||||
* New training architecture
|
||||
* Described in our CVPR paper: "Learning to Drive from a World Model"
|
||||
* Longitudinal MPC replaced by E2E planning from World Model in Experimental Mode
|
||||
* Action from lateral MPC as training objective replaced by E2E planning from World Model
|
||||
* Architecture outlined in CVPR paper: "Learning to Drive from a World Model"
|
||||
* Longitudinal MPC replaced by E2E planning from worldmodel in experimental mode
|
||||
* Action from lateral MPC as training objective replaced by E2E planning from worldmodel
|
||||
* Low-speed lead car ground-truth fixes
|
||||
|
||||
* Enable live-learned steering actuation delay
|
||||
* Record driving feedback using LKAS button when MADS is disabled
|
||||
* Opt-in audio recording for dashcam video
|
||||
* Acura MDX 2025 support thanks to vanillagorillaa and MVL!
|
||||
* Honda Accord 2023-25 support thanks to vanillagorillaa and MVL!
|
||||
* Honda CR-V 2023-25 support thanks to vanillagorillaa and MVL!
|
||||
* Honda Pilot 2023-25 support thanks to vanillagorillaa and MVL!
|
||||
|
||||
Version 0.9.9 (2025-05-23)
|
||||
========================
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "Steam Powered (Default)"
|
||||
#define DEFAULT_MODEL "Down To Ride (Default)"
|
||||
|
||||
@@ -73,9 +73,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}},
|
||||
{"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}},
|
||||
{"LastUpdateRouteCount", {PERSISTENT, INT, "0"}},
|
||||
{"LastUpdateRouteCount", {PERSISTENT, INT}},
|
||||
{"LastUpdateTime", {PERSISTENT, TIME}},
|
||||
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT}},
|
||||
{"LiveDelay", {PERSISTENT | BACKUP, BYTES}},
|
||||
{"LiveParameters", {PERSISTENT, JSON}},
|
||||
{"LiveParametersV2", {PERSISTENT, BYTES}},
|
||||
@@ -156,6 +156,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"sunnypilot_ui", {PERSISTENT, BOOL, "1"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -199,7 +200,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||
{"MapdVersion", {PERSISTENT, STRING}},
|
||||
{"MapdVersion", {PERSISTENT, STRING, ""}},
|
||||
{"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}},
|
||||
{"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||
{"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
@@ -215,5 +216,5 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"OsmStateName", {PERSISTENT, STRING, "All"}},
|
||||
{"OsmStateTitle", {PERSISTENT, STRING}},
|
||||
{"OsmWayTest", {PERSISTENT, STRING}},
|
||||
{"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
{"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING, ""}},
|
||||
};
|
||||
|
||||
+2
-5
@@ -14,7 +14,8 @@ class PIDController:
|
||||
if isinstance(self._k_d, Number):
|
||||
self._k_d = [[0], [self._k_d]]
|
||||
|
||||
self.set_limits(pos_limit, neg_limit)
|
||||
self.pos_limit = pos_limit
|
||||
self.neg_limit = neg_limit
|
||||
|
||||
self.i_rate = 1.0 / rate
|
||||
self.speed = 0.0
|
||||
@@ -40,10 +41,6 @@ class PIDController:
|
||||
self.f = 0.0
|
||||
self.control = 0
|
||||
|
||||
def set_limits(self, pos_limit, neg_limit):
|
||||
self.pos_limit = pos_limit
|
||||
self.neg_limit = neg_limit
|
||||
|
||||
def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False):
|
||||
self.speed = speed
|
||||
self.p = float(error) * self.k_p
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from subprocess import Popen, PIPE, TimeoutExpired
|
||||
|
||||
|
||||
def run_cmd(cmd: list[str], cwd=None, env=None) -> str:
|
||||
@@ -13,16 +11,3 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st
|
||||
except subprocess.CalledProcessError:
|
||||
return default
|
||||
|
||||
|
||||
@contextmanager
|
||||
def managed_proc(cmd: list[str], env: dict[str, str]):
|
||||
proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE)
|
||||
try:
|
||||
yield proc
|
||||
finally:
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except TimeoutExpired:
|
||||
proc.kill()
|
||||
|
||||
+4
-13
@@ -4,13 +4,12 @@
|
||||
|
||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||
|
||||
# 334 Supported Cars
|
||||
# 325 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura ILX 2019">Buy Here</a></sub></details>|||
|
||||
|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura MDX 2025">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
@@ -73,25 +72,19 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Genesis|GV80 2023[<sup>6</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis GV80 2023">Buy Here</a></sub></details>|||
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|14 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>5</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hybrid 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hybrid 2025">Buy Here</a></sub></details>|||
|
||||
|Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector + Honda Clarity Proxy Board<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://shop.retropilot.org/product/honda-clarity-proxy-board-kit">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2015-16">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|15 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda CR-V Hybrid 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|e 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda e 2020">Buy Here</a></sub></details>|||
|
||||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Freed 2020">Buy Here</a></sub></details>|||
|
||||
@@ -102,7 +95,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Odyssey 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Passport 2019-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 angled mount (8 degrees)<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Pilot 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Ridgeline 2017-25">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Azera 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera 2022">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Azera Hybrid 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Azera Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
@@ -127,7 +119,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq Plug-in Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|6 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2022">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023[<sup>6</sup>](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
@@ -305,7 +297,6 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Toyota A connector<br>- 1 comma 3X<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Wildlander PHEV 2021">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma 3X<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br>- 1 right angle OBD-C cable (1.5 ft)<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|
||||
@@ -6,7 +6,7 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu
|
||||
|
||||
### Getting Started
|
||||
|
||||
* Set up your [development environment](/tools/)
|
||||
* Setup your [development environment](../tools/)
|
||||
* Join our [Discord](https://discord.comma.ai)
|
||||
* Docs are at https://docs.comma.ai and https://blog.comma.ai
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Turn the speed blue
|
||||
*A getting started guide for openpilot development*
|
||||
|
||||
In 30 minutes, we'll get an openpilot development environment set up on your computer and make some changes to openpilot's UI.
|
||||
In 30 minutes, we'll get an openpilot development environment setup on your computer and make some changes to openpilot's UI.
|
||||
|
||||
And if you have a comma 3/3X, we'll deploy the change to your device for testing.
|
||||
|
||||
## 1. Set up your development environment
|
||||
## 1. Setup your development environment
|
||||
|
||||
Run this to clone openpilot and install all the dependencies:
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
|
||||
export VECLIB_MAXIMUM_THREADS=1
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="12.8"
|
||||
export AGNOS_VERSION="12.6"
|
||||
fi
|
||||
|
||||
export STAGING_ROOT="/data/safe_staging"
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
# On any failure, run the fallback launcher
|
||||
trap 'exec ./launch_chffrplus.sh' ERR
|
||||
C3_LAUNCH_SH="./sunnypilot/system/hardware/c3/launch_chffrplus.sh"
|
||||
|
||||
MODEL="$(tr -d '\0' < "/sys/firmware/devicetree/base/model")"
|
||||
export MODEL
|
||||
|
||||
if [ "$MODEL" = "comma tici" ]; then
|
||||
# Force a failure if the launcher doesn't exist
|
||||
[ -x "$C3_LAUNCH_SH" ] || false
|
||||
|
||||
# If it exists, run it
|
||||
exec "$C3_LAUNCH_SH"
|
||||
fi
|
||||
|
||||
exec ./launch_chffrplus.sh
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 004fa8df07...23d37a87ec
+1
-1
Submodule panda updated: f10ddc6a89...0e7a3fd8cf
+12
-13
@@ -3,34 +3,33 @@
|
||||
```
|
||||
## release checklist
|
||||
|
||||
### Go to staging
|
||||
- [ ] make a GitHub issue to track release
|
||||
- [ ] create release master branch
|
||||
**Go to `devel-staging`**
|
||||
- [ ] make issue to track release
|
||||
- [ ] update RELEASES.md
|
||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||
- [ ] trigger new nightly build: https://github.com/commaai/openpilot/actions/workflows/release.yaml
|
||||
- [ ] update `devel-staging`: `git reset --hard origin/__nightly`
|
||||
- [ ] build new userdata partition from `release3-staging`
|
||||
- [ ] open a pull request from `devel-staging` to `devel`
|
||||
- [ ] post on Discord, tag `@release crew`
|
||||
|
||||
Updating staging:
|
||||
1. either rebase on master or cherry-pick changes
|
||||
2. run this to update: `BRANCH=devel-staging release/build_devel.sh`
|
||||
3. build new userdata partition from `release3-staging`
|
||||
|
||||
### Go to release
|
||||
- [ ] before going to release, test the following:
|
||||
**Go to `devel`**
|
||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||
- [ ] before merging the pull request, test the following:
|
||||
- [ ] update from previous release -> new release
|
||||
- [ ] update from new release -> previous release
|
||||
- [ ] fresh install with `openpilot-test.comma.ai`
|
||||
- [ ] drive on fresh install
|
||||
- [ ] no submodules or LFS
|
||||
- [ ] check sentry, MTBF, etc.
|
||||
- [ ] stress test passes in production
|
||||
- [ ] stress test in production
|
||||
|
||||
**Go to `release3`**
|
||||
- [ ] publish the blog post
|
||||
- [ ] `git reset --hard origin/release3-staging`
|
||||
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
||||
- [ ] create GitHub release
|
||||
- [ ] final test install on `openpilot.comma.ai`
|
||||
- [ ] update factory provisioning
|
||||
- [ ] close out milestone and issue
|
||||
- [ ] close out milestone
|
||||
- [ ] post on Discord, X, etc.
|
||||
```
|
||||
|
||||
@@ -17,23 +17,28 @@ rm -rf $TARGET_DIR
|
||||
mkdir -p $TARGET_DIR
|
||||
cd $TARGET_DIR
|
||||
cp -r $SOURCE_DIR/.git $TARGET_DIR
|
||||
pre-commit uninstall || true
|
||||
|
||||
echo "[-] setting up stripped branch sync T=$SECONDS"
|
||||
echo "[-] bringing __nightly and devel in sync T=$SECONDS"
|
||||
cd $TARGET_DIR
|
||||
|
||||
# tmp branch
|
||||
git checkout --orphan tmp
|
||||
git fetch --depth 1 origin __nightly
|
||||
git fetch --depth 1 origin devel
|
||||
|
||||
git checkout -f --track origin/__nightly
|
||||
git reset --hard __nightly
|
||||
git checkout __nightly
|
||||
git reset --hard origin/devel
|
||||
git clean -xdff
|
||||
git lfs uninstall
|
||||
|
||||
# remove everything except .git
|
||||
echo "[-] erasing old sunnypilot T=$SECONDS"
|
||||
git submodule deinit -f --all
|
||||
git rm -rf --cached .
|
||||
find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \;
|
||||
|
||||
# cleanup before the copy
|
||||
# reset source tree
|
||||
cd $SOURCE_DIR
|
||||
git clean -xdff
|
||||
git submodule foreach --recursive git clean -xdff
|
||||
|
||||
# do the files copy
|
||||
echo "[-] copying files T=$SECONDS"
|
||||
@@ -42,7 +47,6 @@ cp -pR --parents $(./release/release_files.py) $TARGET_DIR/
|
||||
|
||||
# in the directory
|
||||
cd $TARGET_DIR
|
||||
rm -rf .git/modules/
|
||||
rm -f panda/board/obj/panda.bin.signed
|
||||
|
||||
# include source commit hash and build date in commit
|
||||
@@ -81,7 +85,7 @@ fi
|
||||
|
||||
if [ ! -z "$BRANCH" ]; then
|
||||
echo "[-] Pushing to $BRANCH T=$SECONDS"
|
||||
git push -f origin tmp:$BRANCH
|
||||
git push -f origin __nightly:$BRANCH
|
||||
fi
|
||||
|
||||
echo "[-] done T=$SECONDS, ready at $TARGET_DIR"
|
||||
+15
-8
@@ -51,19 +51,26 @@ git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --all
|
||||
|
||||
echo "[-] committing version $VERSION T=$SECONDS"
|
||||
git add -f .
|
||||
git commit -a -m "sunnypilot v$VERSION release"
|
||||
git branch --set-upstream-to=origin/$DEV_BRANCH
|
||||
|
||||
# include source commit hash and build date in commit
|
||||
GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD)
|
||||
DATETIME=$(date '+%Y-%m-%dT%H:%M:%S')
|
||||
SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/common/version.h)
|
||||
SP_VERSION=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}')
|
||||
|
||||
# Commit with detailed message
|
||||
git commit -a -m "sunnypilot v$VERSION
|
||||
version: sunnypilot v$SP_VERSION (${EXTRA_VERSION_IDENTIFIER})
|
||||
date: $DATETIME
|
||||
master commit: $GIT_HASH
|
||||
"
|
||||
git branch --set-upstream-to=origin/$DEV_BRANCH
|
||||
# Add built files to git
|
||||
git add -f .
|
||||
if [ "$EXTRA_VERSION_IDENTIFIER" = "-release" ] || [ "$EXTRA_VERSION_IDENTIFIER" = "-staging" ]; then
|
||||
export VERSION=${VERSION%"$EXTRA_VERSION_IDENTIFIER"}
|
||||
git commit --amend -m "sunnypilot v$VERSION"
|
||||
else
|
||||
git commit --amend -m "sunnypilot v$VERSION
|
||||
version: sunnypilot v$SP_VERSION release
|
||||
date: $DATETIME
|
||||
master commit: $GIT_HASH
|
||||
"
|
||||
fi
|
||||
git branch -m $DEV_BRANCH
|
||||
|
||||
# Push!
|
||||
|
||||
@@ -95,8 +95,6 @@ class Controls(ControlsExt, ModelStateBase):
|
||||
self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered,
|
||||
torque_params.frictionCoefficientFiltered)
|
||||
|
||||
self.LaC.extension.update_limits()
|
||||
|
||||
self.LaC.extension.update_model_v2(self.sm['modelV2'])
|
||||
|
||||
self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay)
|
||||
|
||||
@@ -3,6 +3,7 @@ import numpy as np
|
||||
|
||||
from cereal import log
|
||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||
from opendbc.car.interfaces import LatControlInputs
|
||||
from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY
|
||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||
from openpilot.common.pid import PIDController
|
||||
@@ -28,24 +29,17 @@ class LatControlTorque(LatControl):
|
||||
def __init__(self, CP, CP_SP, CI):
|
||||
super().__init__(CP, CP_SP, CI)
|
||||
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(self.torque_params.kp, self.torque_params.ki,
|
||||
k_f=self.torque_params.kf)
|
||||
self.update_limits()
|
||||
k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max)
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg
|
||||
|
||||
self.extension = LatControlTorqueExt(self, CP, CP_SP, CI)
|
||||
self.extension = LatControlTorqueExt(self, CP, CP_SP)
|
||||
|
||||
def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction):
|
||||
self.torque_params.latAccelFactor = latAccelFactor
|
||||
self.torque_params.latAccelOffset = latAccelOffset
|
||||
self.torque_params.friction = friction
|
||||
self.update_limits()
|
||||
|
||||
def update_limits(self):
|
||||
self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params),
|
||||
self.lateral_accel_from_torque(-self.steer_max, self.torque_params))
|
||||
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited):
|
||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||
@@ -67,31 +61,33 @@ class LatControlTorque(LatControl):
|
||||
setpoint = desired_lateral_accel + low_speed_factor * desired_curvature
|
||||
measurement = actual_lateral_accel + low_speed_factor * actual_curvature
|
||||
gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation
|
||||
|
||||
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
||||
pid_log.error = float(setpoint - measurement)
|
||||
ff = gravity_adjusted_lateral_accel
|
||||
torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
|
||||
gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
|
||||
gravity_adjusted=False)
|
||||
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params,
|
||||
gravity_adjusted=True)
|
||||
ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
|
||||
# Lateral acceleration torque controller extension updates
|
||||
# Overrides stock ff and pid_log.error
|
||||
ff, pid_log = self.extension.update(CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
|
||||
desired_curvature, actual_curvature)
|
||||
|
||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
output_lataccel = self.pid.update(pid_log.error,
|
||||
output_torque = self.pid.update(pid_log.error,
|
||||
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
|
||||
# Overrides pid_log.error and output_torque
|
||||
pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
|
||||
desired_curvature, actual_curvature, steer_limited_by_safety, output_torque)
|
||||
|
||||
pid_log.active = True
|
||||
pid_log.p = float(self.pid.p)
|
||||
pid_log.i = float(self.pid.i)
|
||||
pid_log.d = float(self.pid.d)
|
||||
pid_log.f = float(self.pid.f)
|
||||
pid_log.output = float(-output_torque) # TODO: log lat accel?
|
||||
pid_log.output = float(-output_torque)
|
||||
pid_log.actualLateralAccel = float(actual_lateral_accel)
|
||||
pid_log.desiredLateralAccel = float(desired_lateral_accel)
|
||||
pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited))
|
||||
|
||||
@@ -93,12 +93,12 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
return x, v, a, j, throttle_prob
|
||||
|
||||
def update(self, sm):
|
||||
mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
||||
self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
||||
if not self.mlsim:
|
||||
self.mpc.mode = mode
|
||||
self.mpc.mode = self.mode
|
||||
LongitudinalPlannerSP.update(self, sm)
|
||||
if dec_mpc_mode := self.get_mpc_mode():
|
||||
mode = dec_mpc_mode
|
||||
self.mode = dec_mpc_mode
|
||||
if not self.mlsim:
|
||||
self.mpc.mode = dec_mpc_mode
|
||||
|
||||
@@ -123,7 +123,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# No change cost when user is controlling the speed, or when standstill
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
if self.mode == 'acc':
|
||||
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)
|
||||
@@ -173,7 +173,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
if mode == 'acc' or not self.mlsim:
|
||||
if self.mode == 'acc' or not self.mlsim:
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
else:
|
||||
|
||||
+1
-3
@@ -5,7 +5,6 @@ from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
from opendbc.car.nissan.values import CAR as NISSAN
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.vehicle_model import VehicleModel
|
||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||
@@ -18,8 +17,7 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac
|
||||
|
||||
class TestLatControl:
|
||||
|
||||
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque),
|
||||
(NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)])
|
||||
@parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle)])
|
||||
def test_saturation(self, car_name, controller):
|
||||
CarInterface = interfaces[car_name]
|
||||
CP = CarInterface.get_non_essential_params(car_name)
|
||||
@@ -107,12 +107,15 @@ class ModelState(ModelStateBase):
|
||||
|
||||
self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32)
|
||||
self.full_desire = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32)
|
||||
self.full_prev_desired_curv = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32)
|
||||
self.temporal_idxs = slice(-1-(ModelConstants.TEMPORAL_SKIP*(ModelConstants.INPUT_HISTORY_BUFFER_LEN-1)), None, ModelConstants.TEMPORAL_SKIP)
|
||||
|
||||
# policy inputs
|
||||
self.numpy_inputs = {
|
||||
'desire': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32),
|
||||
'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32),
|
||||
'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32),
|
||||
'prev_desired_curv': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32),
|
||||
'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32),
|
||||
}
|
||||
|
||||
@@ -145,6 +148,7 @@ class ModelState(ModelStateBase):
|
||||
self.numpy_inputs['desire'][:] = self.full_desire.reshape((1,ModelConstants.INPUT_HISTORY_BUFFER_LEN,ModelConstants.TEMPORAL_SKIP,-1)).max(axis=2)
|
||||
|
||||
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||
self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params']
|
||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names}
|
||||
|
||||
if TICI and not USBGPU:
|
||||
@@ -170,6 +174,11 @@ class ModelState(ModelStateBase):
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||
|
||||
# TODO model only uses last value now
|
||||
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
|
||||
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
|
||||
self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
if SEND_RAW_PRED:
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
||||
@@ -290,6 +299,7 @@ def main(demo=False):
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS
|
||||
lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
@@ -322,6 +332,7 @@ def main(demo=False):
|
||||
inputs:dict[str, np.ndarray] = {
|
||||
'desire': vec_desire,
|
||||
'traffic_convention': traffic_convention,
|
||||
'lateral_control_params': lateral_control_params,
|
||||
}
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04b763fb71efe57a8a4c4168a8043ecd58939015026ded0dc755ded6905ac251
|
||||
size 12343523
|
||||
oid sha256:1af87c38492444521632a0e75839b5684ee46bf255b3474773784bffb9fe4f57
|
||||
size 15583374
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e66bb8d53eced3786ed71a59b55ffc6810944cb217f0518621cc76303260a1ef
|
||||
size 46271991
|
||||
oid sha256:c824f68646a3b94f117f01c70dc8316fb466e05fbd42ccdba440b8a8dc86914b
|
||||
size 46265993
|
||||
|
||||
@@ -22,10 +22,9 @@ class Parser:
|
||||
self.ignore_missing = ignore_missing
|
||||
|
||||
def check_missing(self, outs, name):
|
||||
missing = name not in outs
|
||||
if missing and not self.ignore_missing:
|
||||
if name not in outs and not self.ignore_missing:
|
||||
raise ValueError(f"Missing output {name}")
|
||||
return missing
|
||||
return name not in outs
|
||||
|
||||
def parse_categorical_crossentropy(self, name, outs, out_shape=None):
|
||||
if self.check_missing(outs, name):
|
||||
@@ -85,13 +84,6 @@ class Parser:
|
||||
outs[name] = pred_mu_final.reshape(final_shape)
|
||||
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
|
||||
|
||||
def is_mhp(self, outs, name, shape):
|
||||
if self.check_missing(outs, name):
|
||||
return False
|
||||
if outs[name].shape[1] == 2 * shape:
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
@@ -102,17 +94,23 @@ class Parser:
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
self.parse_binary_crossentropy('lead_prob', outs)
|
||||
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
|
||||
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
|
||||
lead_out_shape = (ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH) if lead_mhp else \
|
||||
(ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH)
|
||||
self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape)
|
||||
if outs['lead'].shape[1] == 2 * ModelConstants.LEAD_MHP_SELECTION *ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH:
|
||||
self.parse_mdn('lead', outs, in_N=0, out_N=0,
|
||||
out_shape=(ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
|
||||
else:
|
||||
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
|
||||
return outs
|
||||
|
||||
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.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=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
if outs['plan'].shape[1] == 2 * ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH:
|
||||
self.parse_mdn('plan', outs, in_N=0, out_N=0,
|
||||
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
|
||||
else:
|
||||
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
|
||||
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
|
||||
if 'desired_curvature' in outs:
|
||||
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
|
||||
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
|
||||
return outs
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ def main() -> None:
|
||||
|
||||
# TODO: remove this in the next AGNOS
|
||||
# wait until USB is up before counting
|
||||
if time.monotonic() < 60.:
|
||||
if time.monotonic() < 35.:
|
||||
no_internal_panda_count = 0
|
||||
|
||||
# Handle missing internal panda
|
||||
|
||||
@@ -265,10 +265,7 @@ class SelfdriveD(CruiseHelper):
|
||||
if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture:
|
||||
self.events.add(EventName.ldw)
|
||||
|
||||
# ******************************************************************************************
|
||||
# NOTE: To fork maintainers.
|
||||
# Disabling or nerfing safety features will get you and your users banned from our servers.
|
||||
# We recommend that you do not change these numbers from the defaults.
|
||||
# Check for excessive actuation
|
||||
if self.sm.updated['liveCalibration']:
|
||||
self.pose_calibrator.feed_live_calib(self.sm['liveCalibration'])
|
||||
if self.sm.updated['livePose']:
|
||||
@@ -283,7 +280,6 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
if self.excessive_actuation:
|
||||
self.events.add(EventName.excessiveActuation)
|
||||
# ******************************************************************************************
|
||||
|
||||
# Handle lane change
|
||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||
|
||||
@@ -1 +1 @@
|
||||
6d3219bca9f66a229b38a5382d301a92b0147edb
|
||||
543bd2347fa35f8300478a3893fdd0a03a7c1fe6
|
||||
@@ -63,7 +63,7 @@ segments = [
|
||||
]
|
||||
|
||||
# dashcamOnly makes don't need to be tested until a full port is done
|
||||
excluded_interfaces = ["mock", "body", "psa"]
|
||||
excluded_interfaces = ["mock", "body"]
|
||||
|
||||
BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/"
|
||||
REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit")
|
||||
|
||||
@@ -333,18 +333,20 @@ class TestOnroad:
|
||||
assert np.all(eof_sof_diff > 0)
|
||||
assert np.all(eof_sof_diff < 50*1e6)
|
||||
|
||||
first_fid = {min(self.ts[c]['frameId']) for c in cams}
|
||||
assert len(first_fid) == 1, "Cameras don't start on same frame ID"
|
||||
first_fid = {c: min(self.ts[c]['frameId']) for c in cams}
|
||||
if cam.endswith('CameraState'):
|
||||
# camerad guarantees that all cams start on frame ID 0
|
||||
# (note loggerd also needs to start up fast enough to catch it)
|
||||
assert next(iter(first_fid)) < 100, "Cameras start on frame ID too high"
|
||||
assert set(first_fid.values()) == {0, }, "Cameras don't start on frame ID 0"
|
||||
else:
|
||||
# encoder guarantees all cams start on the same frame ID
|
||||
assert len(set(first_fid.values())) == 1, "Cameras don't start on same frame ID"
|
||||
|
||||
# we don't do a full segment rotation, so these might not match exactly
|
||||
last_fid = {max(self.ts[c]['frameId']) for c in cams}
|
||||
assert max(last_fid) - min(last_fid) < 10
|
||||
last_fid = {c: max(self.ts[c]['frameId']) for c in cams}
|
||||
assert max(last_fid.values()) - min(last_fid.values()) < 10
|
||||
|
||||
start, end = min(first_fid), min(last_fid)
|
||||
start, end = min(first_fid.values()), min(last_fid.values())
|
||||
for i in range(end-start):
|
||||
ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams}
|
||||
diff = (max(ts.values()) - min(ts.values()))
|
||||
|
||||
@@ -22,9 +22,8 @@ def main():
|
||||
sm.update()
|
||||
should_send_bookmark = False
|
||||
|
||||
# TODO: https://github.com/commaai/openpilot/issues/36015
|
||||
# only allow the LKAS button to record feedback when MADS is disabled
|
||||
if False and sm.updated['carState'] and sm['carState'].canValid and not sm['selfdriveStateSP'].mads.available:
|
||||
if sm.updated['carState'] and sm['carState'].canValid and not sm['selfdriveStateSP'].mads.available:
|
||||
for be in sm['carState'].buttonEvents:
|
||||
if be.type == ButtonType.lkas:
|
||||
if be.pressed:
|
||||
|
||||
@@ -24,13 +24,11 @@ const std::string BRANCH_STR = get_str(BRANCH "?
|
||||
#define GIT_SSH_URL "git@github.com:commaai/openpilot.git"
|
||||
#define CONTINUE_PATH "/data/continue.sh"
|
||||
|
||||
const std::string INSTALL_PATH = "/data/openpilot";
|
||||
const std::string VALID_CACHE_PATH = "/data/.openpilot_cache";
|
||||
const std::string CACHE_PATH = "/data/openpilot.cache";
|
||||
|
||||
#define INSTALL_PATH "/data/openpilot"
|
||||
#define TMP_INSTALL_PATH "/data/tmppilot"
|
||||
|
||||
const int FONT_SIZE = 120;
|
||||
|
||||
extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_start");
|
||||
extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_end");
|
||||
extern const uint8_t inter_ttf[] asm("_binary_selfdrive_ui_installer_inter_ascii_ttf_start");
|
||||
@@ -43,16 +41,6 @@ void run(const char* cmd) {
|
||||
assert(err == 0);
|
||||
}
|
||||
|
||||
void finishInstall() {
|
||||
BeginDrawing();
|
||||
ClearBackground(BLACK);
|
||||
const char *m = "Finishing install...";
|
||||
int text_width = MeasureText(m, FONT_SIZE);
|
||||
DrawTextEx(font, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE);
|
||||
EndDrawing();
|
||||
util::sleep_for(60 * 1000);
|
||||
}
|
||||
|
||||
void renderProgress(int progress) {
|
||||
BeginDrawing();
|
||||
ClearBackground(BLACK);
|
||||
@@ -74,11 +62,11 @@ int doInstall() {
|
||||
}
|
||||
|
||||
// cleanup previous install attempts
|
||||
run("rm -rf " TMP_INSTALL_PATH);
|
||||
run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH);
|
||||
|
||||
// do the install
|
||||
if (util::file_exists(INSTALL_PATH) && util::file_exists(VALID_CACHE_PATH)) {
|
||||
return cachedFetch(INSTALL_PATH);
|
||||
if (util::file_exists(CACHE_PATH)) {
|
||||
return cachedFetch(CACHE_PATH);
|
||||
} else {
|
||||
return freshClone();
|
||||
}
|
||||
@@ -147,9 +135,7 @@ void cloneFinished(int exitCode) {
|
||||
run("git submodule update --init");
|
||||
|
||||
// move into place
|
||||
run(("rm -f " + VALID_CACHE_PATH).c_str());
|
||||
run(("rm -rf " + INSTALL_PATH).c_str());
|
||||
run(util::string_format("mv %s %s", TMP_INSTALL_PATH, INSTALL_PATH.c_str()).c_str());
|
||||
run("mv " TMP_INSTALL_PATH " " INSTALL_PATH);
|
||||
|
||||
#ifdef INTERNAL
|
||||
run("mkdir -p /data/params/d/");
|
||||
@@ -167,9 +153,9 @@ void cloneFinished(int exitCode) {
|
||||
param << value;
|
||||
param.close();
|
||||
}
|
||||
run(("cd " + INSTALL_PATH + " && "
|
||||
run("cd " INSTALL_PATH " && "
|
||||
"git remote set-url origin --push " GIT_SSH_URL " && "
|
||||
"git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\"").c_str());
|
||||
"git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\"");
|
||||
#endif
|
||||
|
||||
// write continue.sh
|
||||
@@ -185,22 +171,16 @@ void cloneFinished(int exitCode) {
|
||||
run("mv /data/continue.sh.new " CONTINUE_PATH);
|
||||
|
||||
// wait for the installed software's UI to take over
|
||||
finishInstall();
|
||||
util::sleep_for(60 * 1000);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
InitWindow(2160, 1080, "Installer");
|
||||
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0);
|
||||
font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, 120, NULL, 0);
|
||||
SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
|
||||
|
||||
if (util::file_exists(CONTINUE_PATH)) {
|
||||
finishInstall();
|
||||
} else {
|
||||
renderProgress(0);
|
||||
int result = doInstall();
|
||||
cloneFinished(result);
|
||||
}
|
||||
|
||||
renderProgress(0);
|
||||
int result = doInstall();
|
||||
cloneFinished(result);
|
||||
CloseWindow();
|
||||
UnloadFont(font);
|
||||
return 0;
|
||||
|
||||
@@ -4,6 +4,9 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import toggle_item
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
|
||||
if Params().get_bool("sunnypilot_ui"):
|
||||
from openpilot.system.ui.sunnypilot.lib.list_view import toggle_item_sp as toggle_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
'enable_adb': (
|
||||
|
||||
@@ -3,6 +3,9 @@ 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 import Scroller
|
||||
|
||||
if Params().get_bool("sunnypilot_ui"):
|
||||
from openpilot.system.ui.sunnypilot.lib.list_view import toggle_item_sp as toggle_item
|
||||
|
||||
# Description constants
|
||||
DESCRIPTIONS = {
|
||||
"OpenpilotEnabledToggle": (
|
||||
@@ -23,6 +26,10 @@ DESCRIPTIONS = {
|
||||
'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.",
|
||||
"IsMetric": "Display speed in km/h instead of mph.",
|
||||
"RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.",
|
||||
"RecordAudioFeedback": (
|
||||
"Press the LKAS button to record audio feedback about openpilot. When this toggle is disabled, the button acts as a bookmark button. " +
|
||||
"The event will be highlighted in comma connect and the segment will be preserved on your device's storage."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +88,12 @@ class TogglesLayout(Widget):
|
||||
self._params.get_bool("RecordAudio"),
|
||||
icon="microphone.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Record Audio Feedback with LKAS button",
|
||||
DESCRIPTIONS["RecordAudioFeedback"],
|
||||
self._params.get_bool("RecordAudioFeedback"),
|
||||
icon="microphone.png",
|
||||
),
|
||||
toggle_item(
|
||||
"Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png"
|
||||
),
|
||||
|
||||
@@ -2,15 +2,12 @@ from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from functools import lru_cache
|
||||
|
||||
from openpilot.common.api import Api, api_get
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
|
||||
TOKEN_EXPIRY_HOURS = 2
|
||||
|
||||
|
||||
class PrimeType(IntEnum):
|
||||
UNKNOWN = -2,
|
||||
@@ -23,12 +20,6 @@ class PrimeType(IntEnum):
|
||||
PURPLE = 5,
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_token(dongle_id: str, t: int):
|
||||
print('getting token')
|
||||
return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS)
|
||||
|
||||
|
||||
class PrimeState:
|
||||
FETCH_INTERVAL = 5.0 # seconds between API calls
|
||||
API_TIMEOUT = 10.0 # seconds for API requests
|
||||
@@ -58,15 +49,13 @@ class PrimeState:
|
||||
return
|
||||
|
||||
try:
|
||||
identity_token = get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60)))
|
||||
identity_token = Api(dongle_id).get_token()
|
||||
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
is_paired = data.get("is_paired", False)
|
||||
prime_type = data.get("prime_type", 0)
|
||||
self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED)
|
||||
elif response.status_code == 401:
|
||||
get_token.cache_clear()
|
||||
except Exception as e:
|
||||
cloudlog.error(f"Failed to fetch prime status: {e}")
|
||||
|
||||
|
||||
@@ -187,9 +187,9 @@ class ModelRenderer(Widget):
|
||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False
|
||||
)
|
||||
|
||||
self._update_experimental_gradient()
|
||||
self._update_experimental_gradient(self._rect.height)
|
||||
|
||||
def _update_experimental_gradient(self):
|
||||
def _update_experimental_gradient(self, height):
|
||||
"""Pre-calculate experimental mode gradient colors"""
|
||||
if not self._experimental_mode:
|
||||
return
|
||||
@@ -201,21 +201,22 @@ class ModelRenderer(Widget):
|
||||
|
||||
i = 0
|
||||
while i < max_len:
|
||||
# Some points (screen space) are out of frame (rect space)
|
||||
track_y = self._path.projected_points[i][1]
|
||||
if track_y < self._rect.y or track_y > (self._rect.y + self._rect.height):
|
||||
track_idx = max_len - i - 1 # flip idx to start from bottom right
|
||||
track_y = self._path.projected_points[track_idx][1]
|
||||
if track_y < 0 or track_y > height:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Calculate color based on acceleration (0 is bottom, 1 is top)
|
||||
lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height
|
||||
# Calculate color based on acceleration
|
||||
lin_grad_point = (height - track_y) / height
|
||||
|
||||
# speed up: 120, slow down: 0
|
||||
path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120)
|
||||
path_hue = max(min(60 + self._acceleration_x[i] * 35, 120), 0)
|
||||
path_hue = int(path_hue * 100 + 0.5) / 100
|
||||
|
||||
saturation = min(abs(self._acceleration_x[i] * 1.5), 1)
|
||||
lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62])
|
||||
alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0])
|
||||
lightness = self._map_val(saturation, 0.0, 1.0, 0.95, 0.62)
|
||||
alpha = self._map_val(lin_grad_point, 0.75 / 2.0, 0.75, 0.4, 0.0)
|
||||
|
||||
# Use HSL to RGB conversion
|
||||
color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha)
|
||||
@@ -279,7 +280,7 @@ class ModelRenderer(Widget):
|
||||
|
||||
if self._experimental_mode:
|
||||
# Draw with acceleration coloring
|
||||
if len(self._exp_gradient['colors']) > 1:
|
||||
if len(self._exp_gradient['colors']) > 2:
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
|
||||
else:
|
||||
draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30))
|
||||
@@ -408,6 +409,13 @@ class ModelRenderer(Widget):
|
||||
|
||||
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||
|
||||
@staticmethod
|
||||
def _map_val(x, x0, x1, y0, y1):
|
||||
x = np.clip(x, x0, x1)
|
||||
ra = x1 - x0
|
||||
rb = y1 - y0
|
||||
return (x - x0) * rb / ra + y0 if ra != 0 else y0
|
||||
|
||||
@staticmethod
|
||||
def _hsla_to_color(h, s, l, a):
|
||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||
|
||||
@@ -75,6 +75,13 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) {
|
||||
"../assets/icons/microphone.png",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"RecordAudioFeedback",
|
||||
tr("Record Audio Feedback with LKAS button"),
|
||||
tr("Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.\n\nNote that this feature is only compatible with select cars."),
|
||||
"../assets/icons/microphone.png",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"IsMetric",
|
||||
tr("Use Metric System"),
|
||||
|
||||
@@ -4,7 +4,6 @@ widgets_src = [
|
||||
"sunnypilot/qt/widgets/controls.cc",
|
||||
"sunnypilot/qt/widgets/drive_stats.cc",
|
||||
"sunnypilot/qt/widgets/expandable_row.cc",
|
||||
"sunnypilot/qt/widgets/external_storage.cc",
|
||||
"sunnypilot/qt/widgets/prime.cc",
|
||||
"sunnypilot/qt/widgets/scrollview.cc",
|
||||
"sunnypilot/qt/network/networking.cc",
|
||||
|
||||
@@ -5,14 +5,9 @@
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h"
|
||||
|
||||
DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) {
|
||||
|
||||
#ifndef __APPLE__
|
||||
addItem(new ExternalStorageControl());
|
||||
#endif
|
||||
|
||||
// Advanced Controls Toggle
|
||||
showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), "");
|
||||
addItem(showAdvancedControls);
|
||||
|
||||
@@ -34,27 +34,6 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
vlayout->setContentsMargins(50, 20, 50, 20);
|
||||
|
||||
auto *list = new ListWidget(this, false);
|
||||
|
||||
QVBoxLayout *titleLayout = new QVBoxLayout;
|
||||
QLabel *title = new QLabel(tr("🚀 sunnylink 🚀"));
|
||||
title->setStyleSheet("font-size: 90px; font-weight: 500; font-family: 'Noto Color Emoji';");
|
||||
titleLayout->addWidget(title, 0, Qt::AlignCenter);
|
||||
|
||||
QLabel *sunnylinkDesc = new QLabel("<div align='center'><font color='green'>"+
|
||||
tr("For secure backup, restore, and remote configuration")+ "</font></div>");
|
||||
|
||||
QLabel *sponsorMsg = new QLabel("<div align='center'><font color='orange'>"+
|
||||
tr("Sponsorship isn't required for basic backup/restore") + "<br>" +
|
||||
tr("Click the sponsor button for more details")+ "</font></div>");
|
||||
|
||||
sunnylinkDesc->setStyleSheet("font-size: 40px; font-weight: 100; font-family: 'Noto';");
|
||||
sponsorMsg->setStyleSheet("font-size: 35px; font-weight: 100; font-family: 'Noto';");
|
||||
|
||||
titleLayout->addWidget(sunnylinkDesc, 0, Qt::AlignCenter);
|
||||
titleLayout->addWidget(sponsorMsg, 0, Qt::AlignCenter);
|
||||
|
||||
list->addItem(titleLayout);
|
||||
|
||||
QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.");
|
||||
sunnylinkEnabledBtn = new ParamControl(
|
||||
"SunnylinkEnabled",
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QCoreApplication>
|
||||
#include <QShowEvent>
|
||||
#include <QTimer>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
|
||||
ExternalStorageControl::ExternalStorageControl() :
|
||||
ButtonControl(tr("External Storage"), "", tr("Extend your comma device's storage by inserting a USB drive into the aux port.")) {
|
||||
|
||||
QObject::connect(this, &ButtonControl::clicked, [=]() {
|
||||
if (text() == tr("CHECK") || text() == tr("MOUNT")) {
|
||||
mountStorage();
|
||||
} else if (text() == tr("UNMOUNT")) {
|
||||
unmountStorage();
|
||||
} else if (text() == tr("FORMAT")) {
|
||||
if (ConfirmationDialog::confirm(tr("Are you sure you want to format this drive? This will erase all data."), tr("Format"), this)) {
|
||||
formatStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &ExternalStorageControl::updateState);
|
||||
updateState(!uiState()->scene.started);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ExternalStorageControl::updateState(bool offroad) {
|
||||
setEnabled(offroad);
|
||||
}
|
||||
|
||||
void ExternalStorageControl::debouncedRefresh() {
|
||||
if (refreshPending) return;
|
||||
refreshPending = true;
|
||||
|
||||
QTimer::singleShot(250, this, [=]() {
|
||||
refreshPending = false;
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
void ExternalStorageControl::refresh() {
|
||||
QtConcurrent::run([=]() {
|
||||
auto run = [](const QString &cmd) {
|
||||
QProcess p;
|
||||
p.start("sh", QStringList() << "-c" << cmd);
|
||||
p.waitForFinished();
|
||||
return p.exitCode() == 0;
|
||||
};
|
||||
|
||||
bool isMounted = run("findmnt -n /mnt/external_realdata");
|
||||
bool hasDrive = run("lsblk -f /dev/sdg");
|
||||
bool hasFs = run("lsblk -f /dev/sdg1 | grep -q ext4");
|
||||
bool hasLabel = run("sudo blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'");
|
||||
|
||||
QString info;
|
||||
if (isMounted && hasLabel) {
|
||||
QProcess df;
|
||||
df.start("sh", QStringList() << "-c" << "df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'");
|
||||
df.waitForFinished();
|
||||
info = df.readAllStandardOutput().trimmed();
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(this, [=]() {
|
||||
if (formatting) {
|
||||
setValue(tr("formatting"));
|
||||
setText(tr("FORMAT"));
|
||||
setEnabled(false);
|
||||
} else {
|
||||
if (!hasDrive) {
|
||||
setValue(tr("insert drive"));
|
||||
setText(tr("CHECK"));
|
||||
} else if (!hasFs || !hasLabel) {
|
||||
setValue(tr("needs format"));
|
||||
setText(tr("FORMAT"));
|
||||
} else if (isMounted) {
|
||||
setValue(info);
|
||||
setText(tr("UNMOUNT"));
|
||||
} else {
|
||||
setValue("drive detected");
|
||||
setText(tr("MOUNT"));
|
||||
}
|
||||
updateState(!uiState()->scene.started);
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void ExternalStorageControl::mountStorage() {
|
||||
setValue(tr("mounting"));
|
||||
setEnabled(false);
|
||||
|
||||
QtConcurrent::run([=]() {
|
||||
QProcess process;
|
||||
process.start("sh", QStringList() << "-c" <<
|
||||
"sudo mount -o remount,rw / && "
|
||||
"sudo mkdir -p /mnt/external_realdata && "
|
||||
"grep -q '/dev/sdg1 /mnt/external_realdata' /etc/fstab || "
|
||||
"echo '/dev/sdg1 /mnt/external_realdata ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab && "
|
||||
"sudo systemctl daemon-reexec && "
|
||||
"sudo mount /mnt/external_realdata && "
|
||||
"sudo chown -R comma:comma /mnt/external_realdata && "
|
||||
"sudo chmod -R 775 /mnt/external_realdata && "
|
||||
"sudo mount -o remount,ro /");
|
||||
process.waitForFinished();
|
||||
|
||||
QMetaObject::invokeMethod(this, [=]() {
|
||||
debouncedRefresh();
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void ExternalStorageControl::unmountStorage() {
|
||||
setValue(tr("unmounting"));
|
||||
setEnabled(false);
|
||||
|
||||
QtConcurrent::run([=]() {
|
||||
QProcess process;
|
||||
process.start("sh", QStringList() << "-c" << "sudo umount /mnt/external_realdata");
|
||||
process.waitForFinished();
|
||||
|
||||
QMetaObject::invokeMethod(this, [=]() {
|
||||
debouncedRefresh();
|
||||
}, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void ExternalStorageControl::formatStorage() {
|
||||
unmountStorage();
|
||||
formatting = true;
|
||||
setValue(tr("formatting"));
|
||||
setEnabled(false);
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
connect(process, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
|
||||
this, [=](int exitCode, QProcess::ExitStatus status) {
|
||||
process->deleteLater();
|
||||
formatting = false;
|
||||
if (exitCode == 0 && status == QProcess::NormalExit) {
|
||||
mountStorage();
|
||||
} else {
|
||||
setValue(tr("needs format"));
|
||||
updateState(!uiState()->scene.started);
|
||||
}
|
||||
});
|
||||
process->start("sh", QStringList() << "-c" <<
|
||||
"sudo wipefs -a /dev/sdg && "
|
||||
"sudo parted -s /dev/sdg mklabel gpt mkpart primary ext4 0% 100% && "
|
||||
"sudo mkfs.ext4 -F -L openpilot /dev/sdg1"
|
||||
);
|
||||
}
|
||||
|
||||
void ExternalStorageControl::showEvent(QShowEvent *event) {
|
||||
ButtonControl::showEvent(event);
|
||||
QTimer::singleShot(100, this, &ExternalStorageControl::debouncedRefresh);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
#define ButtonControl ButtonControlSP
|
||||
|
||||
class ExternalStorageControl : public ButtonControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ExternalStorageControl();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *event) override;
|
||||
|
||||
private:
|
||||
Params params;
|
||||
|
||||
bool refreshPending = false;
|
||||
bool formatting = false;
|
||||
void updateState(bool offroad);
|
||||
void refresh();
|
||||
void debouncedRefresh();
|
||||
void mountStorage();
|
||||
void unmountStorage();
|
||||
void formatStorage();
|
||||
};
|
||||
@@ -5,7 +5,6 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
@pytest.mark.skip("tmp disabled")
|
||||
class TestFeedbackd:
|
||||
def setup_method(self):
|
||||
self.pm = messaging.PubMaster(['carState', 'rawAudioData'])
|
||||
|
||||
@@ -972,6 +972,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1016,38 +1020,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">إلغاء</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2095,30 +2067,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2264,6 +2212,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -964,6 +964,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1008,38 +1012,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">Abbrechen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2077,30 +2049,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2246,6 +2194,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -462,7 +462,7 @@ La calibración del retraso de la dirección está completa.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select a language</source>
|
||||
<translation type="unfinished">Seleccione el idioma</translation>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Wake-Up Behavior</source>
|
||||
@@ -968,6 +968,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1012,38 +1016,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">Cancelar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2079,30 +2051,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2248,6 +2196,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -964,6 +964,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1008,38 +1012,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">Annuler</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2075,30 +2047,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2244,6 +2192,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -966,6 +966,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1010,38 +1014,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">キャンセル</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2074,30 +2046,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2243,6 +2191,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>運転中にマイク音声を録音・保存します。音声は comma connect のドライブレコーダー映像に含まれます。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -974,6 +974,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation>기본값</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation>이 기능을 켜면 차량이 스스로 핸들 반응 속도를 학습하고 맞춥니다. 끄면 고정된 핸들 반응 속도를 사용합니다. 이 기능을 켜두는 것이 기본 openpilot 경험을 제공합니다. 차량이 주행 중일 때 현재 값이 자동으로 업데이트됩니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation>모델 다운로드가 백그라운드에서 시작되었습니다.</translation>
|
||||
@@ -1018,38 +1022,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation>취소</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished">새로 고침</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2088,30 +2060,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation>페어링되지 않음</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2257,6 +2205,18 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>운전 중 마이크 오디오를 녹음하고 저장합니다. 이 오디오는 comma connect의 대시캠 영상에 포함됩니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation>LKAS 버튼으로 오디오 피드백 녹음</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation>LKAS 버튼을 눌러 openpilot 팀과 주행 피드백을 녹음하고 공유하세요. 이 기능을 비활성화하면, 해당 버튼은 북마크 버튼 역할을 합니다. 이 이벤트는 comma connect에서 강조되며, 해당 구간 영상은 기기 저장소에 보존됩니다.
|
||||
|
||||
참고: 이 기능은 일부 차량에서만 호환됩니다.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation>sunnypilot 사용</translation>
|
||||
|
||||
@@ -968,6 +968,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1012,38 +1016,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">Cancelar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2079,30 +2051,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2248,6 +2196,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -962,6 +962,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1006,38 +1010,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">ยกเลิก</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2070,30 +2042,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2239,6 +2187,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -962,6 +962,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1006,38 +1010,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2069,30 +2041,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2238,6 +2186,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -966,6 +966,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1010,38 +1014,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">取消</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2074,30 +2046,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2243,6 +2191,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>在驾驶时录制并存储麦克风音频。该音频将会包含在 comma connect 的行车记录仪视频中。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -966,6 +966,10 @@ The default software delay value is 0.2</source>
|
||||
<source>Default</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. The Current value is updated automatically when the vehicle is Onroad.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Model download has started in the background.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@@ -1010,38 +1014,6 @@ The default software delay value is 0.2</source>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished">取消</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Refresh Model List</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>REFRESH</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fetching Latest Models</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Live Steer Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Actuator Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Software Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Total Delay:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MultiOptionDialog</name>
|
||||
@@ -2074,30 +2046,6 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>[Don't use] Enable sunnylink uploader</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>🚀 sunnylink 🚀</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>For secure backup, restore, and remote configuration</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsorship isn't required for basic backup/restore</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Click the sponsor button for more details</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
@@ -2243,6 +2191,16 @@ Warning: You are on a metered connection!</source>
|
||||
<source>Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.</source>
|
||||
<translation>在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Record Audio Feedback with LKAS button</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage.
|
||||
|
||||
Note that this feature is only compatible with select cars.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enable sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -34,7 +34,7 @@ class OsmMapData(BaseMapData):
|
||||
return float(self.mem_params.get("MapSpeedLimit") or 0.0)
|
||||
|
||||
def get_current_road_name(self) -> str:
|
||||
return str(self.mem_params.get("RoadName") or "")
|
||||
return str(self.mem_params.get("RoadName"))
|
||||
|
||||
def get_next_speed_limit_and_distance(self) -> tuple[float, float]:
|
||||
next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit")
|
||||
|
||||
@@ -90,7 +90,7 @@ class MapdInstallManager:
|
||||
logging.error("Failed to download file after all retries")
|
||||
|
||||
def get_installed_version(self) -> str:
|
||||
return str(self._params.get("MapdVersion") or "")
|
||||
return str(self._params.get("MapdVersion"))
|
||||
|
||||
def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool:
|
||||
max_retries = 10
|
||||
|
||||
@@ -28,30 +28,3 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
commonmodel_lib = lenv.Library('commonmodel', common_src)
|
||||
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
|
||||
|
||||
# Get model metadata
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
script_files = [File(Dir("#sunnypilot/modeld_v2").File("get_model_metadata.py").abspath)]
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd)
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
return lenv.Command(
|
||||
fn + "_tinygrad.pkl",
|
||||
[fn + ".onnx"] + tinygrad_files,
|
||||
f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl'
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': 'DEV=CPU IMAGE=0',
|
||||
}.get(arch, 'DEV=LLVM IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
@@ -82,32 +82,3 @@ class Meta:
|
||||
BRAKE_PRESS = slice(32, 55, 4)
|
||||
LEFT_BLINKER = slice(33, 55, 4)
|
||||
RIGHT_BLINKER = slice(34, 55, 4)
|
||||
|
||||
class MetaTombRaider:
|
||||
ENGAGED = slice(0, 1)
|
||||
# next 2, 4, 6, 8, 10 seconds
|
||||
GAS_DISENGAGE = slice(1, 41, 8)
|
||||
BRAKE_DISENGAGE = slice(2, 41, 8)
|
||||
STEER_OVERRIDE = slice(3, 41, 8)
|
||||
HARD_BRAKE_3 = slice(4, 41, 8)
|
||||
HARD_BRAKE_4 = slice(5, 41, 8)
|
||||
HARD_BRAKE_5 = slice(6, 41, 8)
|
||||
GAS_PRESS = slice(7, 41, 8)
|
||||
BRAKE_PRESS = slice(8, 41, 8)
|
||||
# next 0, 2, 4, 6, 8, 10 seconds
|
||||
LEFT_BLINKER = slice(41, 53, 2)
|
||||
RIGHT_BLINKER = slice(42, 53, 2)
|
||||
|
||||
class MetaSimPose:
|
||||
ENGAGED = slice(0, 1)
|
||||
# next 2, 4, 6, 8, 10 seconds
|
||||
GAS_DISENGAGE = slice(1, 36, 7)
|
||||
BRAKE_DISENGAGE = slice(2, 36, 7)
|
||||
STEER_OVERRIDE = slice(3, 36, 7)
|
||||
HARD_BRAKE_3 = slice(4, 36, 7)
|
||||
HARD_BRAKE_4 = slice(5, 36, 7)
|
||||
HARD_BRAKE_5 = slice(6, 36, 7)
|
||||
GAS_PRESS = slice(7, 36, 7)
|
||||
# next 0, 2, 4, 6, 8, 10 seconds
|
||||
LEFT_BLINKER = slice(36, 48, 2)
|
||||
RIGHT_BLINKER = slice(37, 48, 2)
|
||||
|
||||
@@ -4,7 +4,6 @@ import numpy as np
|
||||
from cereal import log
|
||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_lag_adjusted_curvature, MIN_SPEED
|
||||
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
|
||||
@@ -13,16 +12,8 @@ ConfidenceClass = log.ModelDataV2.ConfidenceClass
|
||||
|
||||
def get_curvature_from_output(output, vego, lat_action_t, mlsim):
|
||||
if not mlsim:
|
||||
if 'lat_planner_solution' in output:
|
||||
x, y, yaw, yawRate = [output['lat_planner_solution'][0, :, i].tolist() for i in range(4)]
|
||||
x_sol = np.column_stack([x, y, yaw, yawRate])
|
||||
v_ego = max(MIN_SPEED, vego)
|
||||
psis = x_sol[0:CONTROL_N, 2].tolist()
|
||||
curvatures = (x_sol[0:CONTROL_N, 3] / v_ego).tolist()
|
||||
desired_curvature = get_lag_adjusted_curvature(lat_action_t, v_ego, psis, curvatures)
|
||||
else:
|
||||
desired_curvature = float(output.get('desired_curvature')[0, 0])
|
||||
return desired_curvature
|
||||
if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly
|
||||
return float(desired_curv[0, 0])
|
||||
|
||||
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],
|
||||
@@ -127,31 +118,14 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
|
||||
# action (includes lateral planning now)
|
||||
modelV2.action = action
|
||||
|
||||
# times at X_IDXS according to model plan
|
||||
PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N
|
||||
PLAN_T_IDXS[0] = 0.0
|
||||
plan_x = net_output_data['plan'][0, :, Plan.POSITION][:, 0].tolist()
|
||||
for xidx in range(1, ModelConstants.IDX_N):
|
||||
tidx = 0
|
||||
# increment tidx until we find an element that's further away than the current xidx
|
||||
while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx + 1] < ModelConstants.X_IDXS[xidx]:
|
||||
tidx += 1
|
||||
if tidx == ModelConstants.IDX_N - 1:
|
||||
# if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break
|
||||
PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1]
|
||||
break
|
||||
# interpolate to find `t` for the current xidx
|
||||
current_x_val = plan_x[tidx]
|
||||
next_x_val = plan_x[tidx + 1]
|
||||
p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(
|
||||
next_x_val - current_x_val) > 1e-9 else float('nan')
|
||||
PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx + 1] + (1 - p) * ModelConstants.T_IDXS[tidx]
|
||||
# times at X_IDXS of edges and lines aren't used
|
||||
LINE_T_IDXS: list[float] = []
|
||||
|
||||
# lane lines
|
||||
modelV2.init('laneLines', 4)
|
||||
for i in range(4):
|
||||
lane_line = modelV2.laneLines[i]
|
||||
fill_xyzt(lane_line, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1])
|
||||
fill_xyzt(lane_line, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['lane_lines'][0,i,:,0], net_output_data['lane_lines'][0,i,:,1])
|
||||
modelV2.laneLineStds = net_output_data['lane_lines_stds'][0,:,0,0].tolist()
|
||||
modelV2.laneLineProbs = net_output_data['lane_lines_prob'][0,1::2].tolist()
|
||||
|
||||
@@ -161,7 +135,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
|
||||
modelV2.init('roadEdges', 2)
|
||||
for i in range(2):
|
||||
road_edge = modelV2.roadEdges[i]
|
||||
fill_xyzt(road_edge, PLAN_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1])
|
||||
fill_xyzt(road_edge, LINE_T_IDXS, np.array(ModelConstants.X_IDXS), net_output_data['road_edges'][0,i,:,0], net_output_data['road_edges'][0,i,:,1])
|
||||
modelV2.roadEdgeStds = net_output_data['road_edges_stds'][0,:,0,0].tolist()
|
||||
|
||||
# leads
|
||||
|
||||
@@ -1,310 +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 argparse
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_metadata_file(metadata_path: str, type_key: str = None) -> dict:
|
||||
if not os.path.exists(metadata_path):
|
||||
print(f"Error: File not found: {metadata_path}")
|
||||
sys.exit(1)
|
||||
with open(metadata_path, 'rb') as f:
|
||||
metadata = pickle.load(f)
|
||||
result = {
|
||||
"metadata_path": metadata_path,
|
||||
"model_checkpoint": metadata.get("model_checkpoint"),
|
||||
**{k: metadata[k] for k in ("input_shapes", "output_shapes", "output_slices") if k in metadata}
|
||||
}
|
||||
if type_key:
|
||||
result[type_key] = True
|
||||
return result
|
||||
|
||||
def add_to_model_metadata(new_data: dict, key: str) -> None:
|
||||
file_path = os.path.abspath(__file__)
|
||||
with open(file_path) as f:
|
||||
lines = f.readlines()
|
||||
# Find insertion point for new entry
|
||||
for i in range(len(lines)-1, -1, -1):
|
||||
if lines[i].strip() == '}' and any('MODEL_METADATA' in l for l in lines[:i]):
|
||||
insert_idx = i
|
||||
break
|
||||
else:
|
||||
print("Could not find where to insert new entry in MODEL_METADATA.")
|
||||
sys.exit(1)
|
||||
|
||||
def format_val(val: Any, indent: int = 2) -> str:
|
||||
if isinstance(val, dict):
|
||||
items = [f'"{k}": {format_val(v, indent+2)}' for k, v in val.items()]
|
||||
return '{\n' + ',\n'.join(' '*(indent+2) + item for item in items) + '\n' + ' '*indent + '}'
|
||||
if isinstance(val, str):
|
||||
return f'"{val}"'
|
||||
if isinstance(val, slice):
|
||||
if val.step is None:
|
||||
return f'slice({val.start}, {val.stop})'
|
||||
return f'slice({val.start}, {val.stop}, {val.step})'
|
||||
if isinstance(val, (tuple, list)):
|
||||
open_s, close_s = ('(', ')') if isinstance(val, tuple) else ('[', ']')
|
||||
return open_s + ', '.join(format_val(x, 0) for x in val) + close_s
|
||||
return repr(val)
|
||||
|
||||
entry_str = f' "{key}": {format_val(new_data, 2)},\n'
|
||||
lines.insert(insert_idx, entry_str)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.writelines(lines)
|
||||
print(f"Added entry to MODEL_METADATA with key: {key}")
|
||||
|
||||
|
||||
MODEL_METADATA = {
|
||||
"supercombo_wd40": {
|
||||
"metadata_path": "/Users/james/Downloads/model-WD40 (April 09, 2024)-558/supercombo_wd40_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"non20hz": True,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 100, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"lateral_control_params": (1, 2),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
"features_buffer": (1, 99, 512),
|
||||
},
|
||||
"output_shapes": {"outputs": (1, 6504)},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5916),
|
||||
"desire_pred": slice(5916, 5948),
|
||||
"pose": slice(5948, 5960),
|
||||
"wide_from_device_euler": slice(5960, 5966),
|
||||
"sim_pose": slice(5966, 5978),
|
||||
"road_transform": slice(5978, 5990),
|
||||
"desired_curvature": slice(5990, 5992),
|
||||
"hidden_state": slice(5992, None),
|
||||
},
|
||||
},
|
||||
"supercombo_farmville": {
|
||||
"metadata_path": "/Users/james/Downloads/model-Farmville (November 07, 2023)-557/supercombo_farmville_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"non20hz": True,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 100, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"lat_planner_state": (1, 4),
|
||||
"nav_features": (1, 256),
|
||||
"nav_instructions": (1, 150),
|
||||
"features_buffer": (1, 99, 512),
|
||||
},
|
||||
"output_shapes": {"outputs": (1, 6768)},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5916),
|
||||
"desire_pred": slice(5916, 5948),
|
||||
"pose": slice(5948, 5960),
|
||||
"wide_from_device_euler": slice(5960, 5966),
|
||||
"sim_pose": slice(5966, 5978),
|
||||
"road_transform": slice(5978, 5990),
|
||||
"lat_planner_solution": slice(5990, 6254),
|
||||
"hidden_state": slice(6254, -2),
|
||||
"pad": slice(-2, None),
|
||||
},
|
||||
},
|
||||
"driving_policy_steam_powered": {
|
||||
"metadata_path": "selfdrive/modeld/models/driving_policy_metadata.pkl",
|
||||
"model_checkpoint": "a8f96b93-bde2-4e28-a732-4df21ebba968/400",
|
||||
"split": True,
|
||||
"input_shapes": {
|
||||
"desire": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"features_buffer": (1, 25, 512),
|
||||
},
|
||||
"output_shapes": {"outputs": (1, 1000)},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 990),
|
||||
"desire_state": slice(990, 998),
|
||||
"pad": slice(-2, None),
|
||||
},
|
||||
},
|
||||
"supercombo_op": {
|
||||
"metadata_path": "/Users/james/Downloads/model-Optimus Prime (September 21, 2023)-559/supercombo_op_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"non20hz": True,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 100, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"nav_features": (1, 256),
|
||||
"nav_instructions": (1, 150),
|
||||
"features_buffer": (1, 99, 512),
|
||||
},
|
||||
"output_shapes": {"outputs": (1, 6504)},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5916),
|
||||
"desire_pred": slice(5916, 5948),
|
||||
"pose": slice(5948, 5960),
|
||||
"wide_from_device_euler": slice(5960, 5966),
|
||||
"sim_pose": slice(5966, 5978),
|
||||
"road_transform": slice(5978, 5990),
|
||||
"hidden_state": slice(5990, -2),
|
||||
"pad": slice(-2, None),
|
||||
},
|
||||
},
|
||||
"supercombo_nd": {
|
||||
"metadata_path": "/Users/james/Downloads/model-Notre Dame (July 01, 2024)-568/supercombo_nd_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"non20hz": True,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 100, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"lateral_control_params": (1, 2),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
"features_buffer": (1, 99, 512),
|
||||
},
|
||||
"output_shapes": {"outputs": (1, 6512)},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5921),
|
||||
"desire_pred": slice(5921, 5953),
|
||||
"pose": slice(5953, 5965),
|
||||
"wide_from_device_euler": slice(5965, 5971),
|
||||
"sim_pose": slice(5971, 5983),
|
||||
"road_transform": slice(5983, 5995),
|
||||
"desired_curvature": slice(5995, 5997),
|
||||
"hidden_state": slice(5997, -3),
|
||||
"pad": slice(-3, None),
|
||||
},
|
||||
},
|
||||
"supercombo_npr": {
|
||||
"metadata_path": "/Users/james/Downloads/supercombo_npr_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"features_buffer": (1, 24, 512)
|
||||
},
|
||||
"output_shapes": {
|
||||
"outputs": (1, 6500)
|
||||
},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5923),
|
||||
"desire_pred": slice(5923, 5955),
|
||||
"pose": slice(5955, 5967),
|
||||
"wide_from_device_euler": slice(5967, 5973),
|
||||
"road_transform": slice(5973, 5985),
|
||||
"hidden_state": slice(5985, -3),
|
||||
"pad": slice(-3, None)
|
||||
},
|
||||
"20hz": True
|
||||
},
|
||||
"driving_policy_renamed_desire": {
|
||||
"metadata_path": "/Users/james/Downloads/model-ugh (August 27, 2025)-575/driving_policy_ugh_metadata.pkl",
|
||||
"model_checkpoint": "a8f96b93-bde2-4e28-a732-4df21ebba968/400",
|
||||
"split": True,
|
||||
"input_shapes": {
|
||||
"desire_pulse": (1, 25, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"features_buffer": (1, 25, 512)
|
||||
},
|
||||
"output_shapes": {
|
||||
"outputs": (1, 1000)
|
||||
},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 990),
|
||||
"desire_state": slice(990, 998),
|
||||
"pad": slice(-2, None)
|
||||
}
|
||||
},
|
||||
"supercombo_nts": { # released in January of this year, so its not 20hz, but it is modern logic..
|
||||
"metadata_path": "/Users/james/Downloads/supercombo_nts_metadata.pkl",
|
||||
"model_checkpoint": None,
|
||||
"non20hz": True,
|
||||
"input_shapes": {
|
||||
"input_imgs": (1, 12, 128, 256),
|
||||
"big_input_imgs": (1, 12, 128, 256),
|
||||
"desire": (1, 100, 8),
|
||||
"traffic_convention": (1, 2),
|
||||
"lateral_control_params": (1, 2),
|
||||
"prev_desired_curv": (1, 100, 1),
|
||||
"features_buffer": (1, 99, 512)
|
||||
},
|
||||
"output_shapes": {
|
||||
"outputs": (1, 6512)
|
||||
},
|
||||
"output_slices": {
|
||||
"plan": slice(0, 4955),
|
||||
"lane_lines": slice(4955, 5483),
|
||||
"lane_lines_prob": slice(5483, 5491),
|
||||
"road_edges": slice(5491, 5755),
|
||||
"lead": slice(5755, 5857),
|
||||
"lead_prob": slice(5857, 5860),
|
||||
"desire_state": slice(5860, 5868),
|
||||
"meta": slice(5868, 5923),
|
||||
"desire_pred": slice(5923, 5955),
|
||||
"pose": slice(5955, 5967),
|
||||
"wide_from_device_euler": slice(5967, 5973),
|
||||
"sim_pose": slice(5973, 5985),
|
||||
"road_transform": slice(5985, 5997),
|
||||
"desired_curvature": slice(5997, 5999),
|
||||
"hidden_state": slice(5999, -1),
|
||||
"pad": slice(-1, None)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Add model metadata from .pkl file to lookup dictionary")
|
||||
parser.add_argument("metadata_file", help="Path to *_metadata.pkl file to parse")
|
||||
parser.add_argument("--type", dest="type_key", default=None,
|
||||
help="Type key to set (e.g., non20hz, split, 20hz)")
|
||||
args = parser.parse_args()
|
||||
basename = os.path.basename(args.metadata_file)
|
||||
dict_key = basename.replace("_metadata.pkl", "")
|
||||
|
||||
metadata = parse_metadata_file(args.metadata_file, args.type_key)
|
||||
add_to_model_metadata(metadata, dict_key)
|
||||
+86
-120
@@ -27,7 +27,7 @@ from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
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"
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -40,76 +40,11 @@ class FrameMeta:
|
||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||
|
||||
|
||||
class InputQueues:
|
||||
def __init__(self, input_shapes: dict, input_dtypes: dict):
|
||||
self.input_shapes = input_shapes
|
||||
self.input_dtypes = input_dtypes
|
||||
self.buffers: dict[str, np.ndarray | None] = {}
|
||||
self.indices: dict[str, np.ndarray | None] = {}
|
||||
for key, shape in input_shapes.items():
|
||||
self._setup_buffer_for_key(key, shape, input_dtypes[key])
|
||||
|
||||
def _setup_buffer_for_key(self, key, shape, dtype):
|
||||
# Temporal input: shape is [batch, history, features]
|
||||
if len(shape) == 3 and shape[1] > 1:
|
||||
buffer_history_len = max(100, shape[1] * 4 if shape[1] < 100 else shape[1])
|
||||
self.buffers[key] = np.zeros((1, buffer_history_len, shape[2]), dtype=dtype)
|
||||
features_buffer_shape = self.input_shapes.get('features_buffer')
|
||||
if shape[1] in (24, 25) and features_buffer_shape and features_buffer_shape[1] == 24:
|
||||
step = int(-buffer_history_len / shape[1])
|
||||
self.indices[key] = np.arange(step, step * (shape[1] + 1), step)[::-1]
|
||||
elif shape[1] == 25:
|
||||
skip = buffer_history_len // shape[1]
|
||||
self.indices[key] = np.arange(buffer_history_len)[-1 - (skip * (shape[1] - 1))::skip]
|
||||
elif shape[1] == buffer_history_len:
|
||||
self.indices[key] = np.arange(buffer_history_len)
|
||||
else:
|
||||
self.indices[key] = None
|
||||
|
||||
def update_dtypes_and_shapes(self, input_dtypes: dict, input_shapes: dict) -> None:
|
||||
self.input_dtypes.update(input_dtypes)
|
||||
self.input_shapes.update(input_shapes)
|
||||
for key in input_dtypes:
|
||||
if key in self.buffers and self.buffers[key] is not None:
|
||||
shape = input_shapes[key]
|
||||
self._setup_buffer_for_key(key, shape, input_dtypes[key])
|
||||
|
||||
def enqueue(self, inputs: dict[str, np.ndarray]) -> None:
|
||||
for key, new_val in inputs.items():
|
||||
if key not in self.buffers or self.buffers[key] is None:
|
||||
continue
|
||||
if new_val.dtype != self.input_dtypes[key]:
|
||||
raise ValueError(f'Input {key} has wrong dtype {new_val.dtype}, expected {self.input_dtypes[key]}')
|
||||
buf = self.buffers[key]
|
||||
if buf is not None:
|
||||
if buf.shape[1] == new_val.shape[0]:
|
||||
buf[0, -new_val.shape[0]:] = new_val
|
||||
buf[0, :-new_val.shape[0]] = buf[0, new_val.shape[0]:]
|
||||
else:
|
||||
buf[0, :-1] = buf[0, 1:]
|
||||
buf[0, -1] = new_val
|
||||
|
||||
def get(self, *names) -> dict[str, np.ndarray]:
|
||||
result: dict[str, np.ndarray] = {}
|
||||
for key in names:
|
||||
buf = self.buffers.get(key, None)
|
||||
if buf is not None:
|
||||
out_shape = self.input_shapes.get(key)
|
||||
# Roll buffer and assign based on desire.shape[1] value
|
||||
if out_shape is not None and key.startswith('desire') and buf.shape[1] > out_shape[1]:
|
||||
skip = buf.shape[1] // out_shape[1]
|
||||
result[key] = buf.reshape((out_shape[0], out_shape[1], skip, -1)).max(axis=2)
|
||||
elif self.indices[key] is not None and buf.shape[1] > 1:
|
||||
result[key] = buf[0, self.indices[key]]
|
||||
elif out_shape is not None and buf.shape[1] >= out_shape[1]:
|
||||
result[key] = buf[0, -out_shape[1]:]
|
||||
return result
|
||||
|
||||
|
||||
class ModelState(ModelStateBase):
|
||||
frames: dict[str, DrivingModelFrame]
|
||||
inputs: dict[str, np.ndarray]
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
temporal_idxs: slice | np.ndarray
|
||||
|
||||
def __init__(self, context: CLContext):
|
||||
ModelStateBase.__init__(self)
|
||||
@@ -121,47 +56,64 @@ class ModelState(ModelStateBase):
|
||||
raise
|
||||
|
||||
model_bundle = get_active_bundle()
|
||||
self.generation = model_bundle.generation if model_bundle else None
|
||||
overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {}
|
||||
self.generation = model_bundle.generation if model_bundle is not None else None
|
||||
overrides = {override.key: override.value for override in model_bundle.overrides}
|
||||
|
||||
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0"))
|
||||
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
|
||||
buffer_length = 4 if self.model_runner.is_20hz else 2
|
||||
buffer_length = 5 if self.model_runner.is_20hz else 2
|
||||
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
|
||||
|
||||
input_dtypes = dict.fromkeys(self.model_runner.input_shapes, np.float32)
|
||||
self.numpy_inputs = {k: np.zeros(shape, dtype=input_dtypes[k]) for k, shape in self.model_runner.input_shapes.items() if k not in self.frames}
|
||||
# img buffers are managed in openCL transform code
|
||||
self.numpy_inputs = {}
|
||||
|
||||
temporal_inputs = {k: v for k, v in self.model_runner.input_shapes.items() if len(v) == 3 and v[1] > 1}
|
||||
self.input_queues = InputQueues(temporal_inputs, dict.fromkeys(temporal_inputs, np.float32))
|
||||
self.prev_desire = np.zeros(self.numpy_inputs[self.desire_key].shape[2], dtype=np.float32)
|
||||
for key, shape in self.model_runner.input_shapes.items():
|
||||
if key not in self.frames: # Managed by opencl
|
||||
self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
if self.model_runner.is_20hz_3d: # split models
|
||||
self.full_features_buffer = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.FEATURE_LEN), dtype=np.float32)
|
||||
self.full_desire = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.DESIRE_LEN), dtype=np.float32)
|
||||
self.full_prev_desired_curv = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.PREV_DESIRED_CURV_LEN), dtype=np.float32)
|
||||
self.temporal_idxs = slice(-1-(self.constants.TEMPORAL_SKIP*(self.constants.INPUT_HISTORY_BUFFER_LEN-1)), None, self.constants.TEMPORAL_SKIP)
|
||||
elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d:
|
||||
self.full_features_buffer = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.FEATURE_LEN), dtype=np.float32)
|
||||
self.full_desire = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.DESIRE_LEN), dtype=np.float32)
|
||||
num_elements = self.numpy_inputs['features_buffer'].shape[1]
|
||||
step_size = int(-100 / num_elements)
|
||||
self.temporal_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
|
||||
self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1,
|
||||
self.numpy_inputs['desire'].shape[2])
|
||||
|
||||
@property
|
||||
def mlsim(self) -> bool:
|
||||
return bool(self.generation is not None and self.generation >= 11)
|
||||
|
||||
@property
|
||||
def desire_key(self) -> str:
|
||||
return next(key for key in self.numpy_inputs if key.startswith('desire'))
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs[self.desire_key][0] = 0
|
||||
new_desire = np.where(inputs[self.desire_key] - self.prev_desire > .99, inputs[self.desire_key], 0)
|
||||
self.prev_desire[:] = inputs[self.desire_key]
|
||||
inputs['desire'][0] = 0
|
||||
new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0)
|
||||
self.prev_desire[:] = inputs['desire']
|
||||
|
||||
batch_inputs = {key: (new_desire if key == self.desire_key else inputs[key])
|
||||
for key in self.input_queues.buffers
|
||||
if not (key == 'features_buffer' and 'hidden_state' in self.numpy_inputs) and (key == self.desire_key or key in inputs)}
|
||||
self.input_queues.enqueue(batch_inputs)
|
||||
if self.model_runner.is_20hz_3d: # split models
|
||||
self.full_desire[0,:-1] = self.full_desire[0,1:]
|
||||
self.full_desire[0,-1] = new_desire
|
||||
self.numpy_inputs['desire'][:] = self.full_desire.reshape((1, self.constants.INPUT_HISTORY_BUFFER_LEN, self.constants.TEMPORAL_SKIP, -1)).max(axis=2)
|
||||
elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo
|
||||
self.full_desire[:-1] = self.full_desire[1:]
|
||||
self.full_desire[-1] = new_desire
|
||||
self.numpy_inputs['desire'][:] = self.full_desire.reshape(self.desire_reshape_dims).max(axis=2)
|
||||
else: # not 20hz
|
||||
length = inputs['desire'].shape[0]
|
||||
self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:]
|
||||
self.numpy_inputs['desire'][0, -1, :length] = new_desire[:length]
|
||||
|
||||
for key in self.numpy_inputs:
|
||||
if key in self.input_queues.buffers:
|
||||
self.numpy_inputs[key][:] = self.input_queues.get(key)[key]
|
||||
elif key in inputs:
|
||||
if key in inputs and key not in ['desire']:
|
||||
self.numpy_inputs[key][:] = inputs[key]
|
||||
|
||||
imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names}
|
||||
@@ -175,27 +127,42 @@ class ModelState(ModelStateBase):
|
||||
# Run model inference
|
||||
outputs = self.model_runner.run_model()
|
||||
|
||||
if "lat_planner_solution" in outputs and "lat_planner_state" in self.numpy_inputs:
|
||||
idx_n = outputs['lat_planner_solution'].shape[1]
|
||||
t_idxs = [10.0 * ((i / (idx_n - 1))**2) for i in range(idx_n)]
|
||||
self.numpy_inputs['lat_planner_state'][2] = np.interp(DT_MDL, t_idxs, outputs['lat_planner_solution'][0, :, 2])
|
||||
self.numpy_inputs['lat_planner_state'][3] = np.interp(DT_MDL, t_idxs, outputs['lat_planner_solution'][0, :, 3])
|
||||
if self.model_runner.is_20hz_3d: # split models
|
||||
self.full_features_buffer[0, :-1] = self.full_features_buffer[0, 1:]
|
||||
self.full_features_buffer[0, -1] = outputs['hidden_state'][0, :]
|
||||
self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[0, self.temporal_idxs]
|
||||
elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo
|
||||
self.full_features_buffer[:-1] = self.full_features_buffer[1:]
|
||||
self.full_features_buffer[-1] = outputs['hidden_state'][0, :]
|
||||
self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[self.temporal_idxs]
|
||||
else: # not 20hz
|
||||
feature_len = outputs['hidden_state'].shape[1]
|
||||
self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:]
|
||||
self.numpy_inputs['features_buffer'][0, -1, :feature_len] = outputs['hidden_state'][0, :feature_len]
|
||||
|
||||
# Enqueue features buffer
|
||||
self.input_queues.enqueue({'features_buffer': outputs['hidden_state'][0, :]})
|
||||
self.numpy_inputs['features_buffer'][:] = self.input_queues.get('features_buffer')['features_buffer']
|
||||
if "desired_curvature" in outputs:
|
||||
input_name_prev = None
|
||||
|
||||
if "desired_curvature" in outputs and "prev_desired_curv" in self.numpy_inputs:
|
||||
self.process_desired_curvature(outputs, 'prev_desired_curv')
|
||||
if "prev_desired_curvs" in self.numpy_inputs.keys():
|
||||
input_name_prev = 'prev_desired_curvs'
|
||||
elif "prev_desired_curv" in self.numpy_inputs.keys():
|
||||
input_name_prev = 'prev_desired_curv'
|
||||
|
||||
if input_name_prev is not None:
|
||||
self.process_desired_curvature(outputs, input_name_prev)
|
||||
return outputs
|
||||
|
||||
def process_desired_curvature(self, outputs, input_name):
|
||||
self.input_queues.enqueue({input_name: outputs['desired_curvature'][0, :]})
|
||||
self.numpy_inputs[input_name][:] = self.input_queues.get(input_name)[input_name]
|
||||
if self.mlsim:
|
||||
self.numpy_inputs[input_name][:] = 0 * self.input_queues.get(input_name)[input_name]
|
||||
|
||||
def process_desired_curvature(self, outputs, input_name_prev):
|
||||
if self.model_runner.is_20hz_3d: # split models
|
||||
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
|
||||
self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :]
|
||||
self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs]
|
||||
if self.mlsim:
|
||||
self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
|
||||
else:
|
||||
length = outputs['desired_curvature'][0].size
|
||||
self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0]
|
||||
self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0]
|
||||
|
||||
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:
|
||||
@@ -256,13 +223,19 @@ def main(demo=False):
|
||||
|
||||
publish_state = PublishState()
|
||||
params = Params()
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
|
||||
frame_id = last_vipc_frame_id = run_count = 0
|
||||
|
||||
model_transform_main = model_transform_extra = np.zeros((3, 3), dtype=np.float32)
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
|
||||
frame_id = 0
|
||||
last_vipc_frame_id = 0
|
||||
run_count = 0
|
||||
|
||||
model_transform_main = np.zeros((3, 3), dtype=np.float32)
|
||||
model_transform_extra = np.zeros((3, 3), dtype=np.float32)
|
||||
live_calib_seen = False
|
||||
buf_main = buf_extra = None
|
||||
meta_main = meta_extra = FrameMeta()
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -349,19 +322,12 @@ def main(demo=False):
|
||||
bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names}
|
||||
transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names}
|
||||
inputs:dict[str, np.ndarray] = {
|
||||
model.desire_key: vec_desire,
|
||||
'desire': vec_desire,
|
||||
'traffic_convention': traffic_convention,
|
||||
}
|
||||
|
||||
conditional_inputs = {
|
||||
"lateral_control_params": lambda v_ego=v_ego, lat_delay=lat_delay: np.array([v_ego, lat_delay], dtype=np.float32),
|
||||
"driving_style": lambda: np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32),
|
||||
"nav_features": lambda: np.zeros(model.model_runner.input_shapes.get('nav_features')[1], dtype=np.float32),
|
||||
"nav_instructions": lambda: np.zeros(model.model_runner.input_shapes.get('nav_instructions')[1], dtype=np.float32),
|
||||
}
|
||||
for key, value in conditional_inputs.items():
|
||||
if key in model.numpy_inputs:
|
||||
inputs[key] = value()
|
||||
if "lateral_control_params" in model.numpy_inputs.keys():
|
||||
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
model_output = model.run(bufs, transforms, inputs, prepare_only)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
|
||||
|
||||
def safe_exp(x, out=None):
|
||||
@@ -24,6 +25,8 @@ def softmax(x, axis=-1):
|
||||
class Parser:
|
||||
def __init__(self, ignore_missing=False):
|
||||
self.ignore_missing = ignore_missing
|
||||
model_bundle = get_active_bundle()
|
||||
self.generation = model_bundle.generation if model_bundle is not None else None
|
||||
|
||||
def check_missing(self, outs, name):
|
||||
if name not in outs and not self.ignore_missing:
|
||||
@@ -88,26 +91,26 @@ class Parser:
|
||||
outs[name] = pred_mu_final.reshape(final_shape)
|
||||
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
|
||||
|
||||
def is_mhp(self, outs, name, shape):
|
||||
if self.check_missing(outs, name):
|
||||
return False
|
||||
if outs[name].shape[1] == 2 * shape:
|
||||
return False
|
||||
return True
|
||||
def _parse_plan_mhp(self, outs):
|
||||
self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION,
|
||||
out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH))
|
||||
|
||||
def parse_dynamic_outputs(self, outs: dict[str, np.ndarray]) -> None:
|
||||
if 'lead' in outs:
|
||||
lead_mhp = self.is_mhp(outs, 'lead',
|
||||
SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH)
|
||||
lead_in_N, lead_out_N = (SplitModelConstants.LEAD_MHP_N, SplitModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
|
||||
lead_out_shape = (SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH) if lead_mhp else \
|
||||
(SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH)
|
||||
self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape)
|
||||
if self.generation >= 12 and \
|
||||
outs['lead'].shape[1] == 2 * SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH:
|
||||
self.parse_mdn('lead', outs, in_N=0, out_N=0,
|
||||
out_shape=(SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH))
|
||||
else:
|
||||
self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION,
|
||||
out_shape=(SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH))
|
||||
if 'plan' in outs:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', SplitModelConstants.IDX_N * SplitModelConstants.PLAN_WIDTH)
|
||||
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 self.generation >= 12 and \
|
||||
outs['plan'].shape[1] == 2 * SplitModelConstants.IDX_N * SplitModelConstants.PLAN_WIDTH:
|
||||
self.parse_mdn('plan', outs, in_N=0, out_N=0,
|
||||
out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH))
|
||||
else:
|
||||
self._parse_plan_mhp(outs)
|
||||
|
||||
def split_outputs(self, outs: dict[str, np.ndarray]) -> None:
|
||||
if 'desired_curvature' in outs:
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from typing import Any
|
||||
|
||||
import openpilot.sunnypilot.models.helpers as helpers
|
||||
import openpilot.sunnypilot.models.runners.helpers as runner_helpers
|
||||
import openpilot.sunnypilot.modeld_v2.modeld as modeld_module
|
||||
from openpilot.sunnypilot.modeld_v2.model_metadata_lookup import MODEL_METADATA
|
||||
|
||||
ModelState = modeld_module.ModelState
|
||||
SHAPE_MODE_PARAMS = []
|
||||
for _, meta in MODEL_METADATA.items():
|
||||
mode = ''
|
||||
if isinstance(meta, dict):
|
||||
if meta.get('split'):
|
||||
mode = 'split'
|
||||
elif meta.get('non20hz'):
|
||||
mode = 'non20hz'
|
||||
elif meta.get('20hz'):
|
||||
mode = '20hz'
|
||||
|
||||
input_shapes = {}
|
||||
for k, v in meta.get('input_shapes', {}).items():
|
||||
if k not in ["input_imgs", "big_input_imgs"]:
|
||||
input_shapes[k] = v
|
||||
if input_shapes:
|
||||
SHAPE_MODE_PARAMS.append((input_shapes, mode))
|
||||
|
||||
|
||||
# This creates a dummy runner, override, and bundle instance for the tests to run, without actually trying to load a physical model.
|
||||
class DummyOverride:
|
||||
def __init__(self, key: str, value: str) -> None:
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
class DummyBundle:
|
||||
def __init__(self) -> None:
|
||||
self.overrides = [DummyOverride('lat', '.1'), DummyOverride('long', '.3')]
|
||||
self.generation = 10 # default to non-mlsim for buffer-update tests, as raising to 11 here will zero curvature buffer
|
||||
|
||||
|
||||
class DummyModelRunner:
|
||||
def __init__(self, input_shapes: dict[str, tuple[int, int, int]], constants: Any = None) -> None:
|
||||
self.input_shapes = input_shapes
|
||||
self.constants = constants or type('C', (), {
|
||||
'FULL_HISTORY_BUFFER_LEN': 100,
|
||||
'FEATURE_LEN': 512,
|
||||
'DESIRE_LEN': 8,
|
||||
'PREV_DESIRED_CURV_LEN': 1,
|
||||
'INPUT_HISTORY_BUFFER_LEN': 25,
|
||||
'TEMPORAL_SKIP': 4,
|
||||
})()
|
||||
self.vision_input_names: list[str] = []
|
||||
shape = input_shapes.get('desire', (1, 0, 0)) # [batch, history, features]
|
||||
if shape[1] == 25:
|
||||
self.is_20hz = True
|
||||
else:
|
||||
self.is_20hz = False
|
||||
|
||||
# Minimal prepare/run methods so ModelState can be run without actually running the model
|
||||
def prepare_inputs(self, imgs_cl, numpy_inputs, frames):
|
||||
return None
|
||||
|
||||
def run_model(self):
|
||||
return {
|
||||
'hidden_state': np.zeros((1, self.constants.FEATURE_LEN), dtype=np.float32),
|
||||
'desired_curvature': np.zeros((1, 1), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shapes(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bundle() -> DummyBundle:
|
||||
return DummyBundle()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner(shapes) -> DummyModelRunner:
|
||||
return DummyModelRunner(shapes)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def apply_patches(monkeypatch: pytest.MonkeyPatch, bundle: DummyBundle, runner: DummyModelRunner):
|
||||
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False)
|
||||
monkeypatch.setattr(runner_helpers, 'get_model_runner', lambda: runner, raising=False)
|
||||
monkeypatch.setattr(modeld_module, 'get_model_runner', lambda: runner, raising=False)
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False)
|
||||
|
||||
|
||||
# These are expected shapes and indices based on the time the model was presented
|
||||
def get_expected_indices(shape, constants, mode, key=None):
|
||||
if mode == 'split':
|
||||
start = -1 - (constants.TEMPORAL_SKIP * (constants.INPUT_HISTORY_BUFFER_LEN - 1))
|
||||
arr = np.arange(constants.FULL_HISTORY_BUFFER_LEN)
|
||||
idxs = arr[start::constants.TEMPORAL_SKIP]
|
||||
return idxs
|
||||
elif mode == '20hz':
|
||||
num_elements = shape[1]
|
||||
step_size = int(-100 / num_elements)
|
||||
idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1]
|
||||
return idxs
|
||||
elif mode == 'non20hz':
|
||||
if key and shape[1] == constants.FULL_HISTORY_BUFFER_LEN:
|
||||
return np.arange(constants.FULL_HISTORY_BUFFER_LEN)
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"])
|
||||
def test_buffer_shapes_and_indices(shapes, mode, apply_patches):
|
||||
state = ModelState(None)
|
||||
constants = DummyModelRunner(shapes).constants
|
||||
for key in shapes:
|
||||
buf = state.input_queues.buffers.get(key, None)
|
||||
idxs = state.input_queues.indices.get(key, None)
|
||||
if buf is None:
|
||||
continue # not all shapes are 3D, and the non-3D ones are not buffered
|
||||
# Buffer shape logic
|
||||
if mode == 'split':
|
||||
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
||||
expected_idxs = get_expected_indices(shapes[key], constants, 'split', key)
|
||||
elif mode == '20hz':
|
||||
expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2])
|
||||
expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key)
|
||||
elif mode == 'non20hz':
|
||||
if key == 'features_buffer':
|
||||
expected_shape = (1, shapes[key][1]*4, shapes[key][2])
|
||||
else:
|
||||
expected_shape = (1, shapes[key][1], shapes[key][2])
|
||||
expected_idxs = get_expected_indices(shapes[key], constants, 'non20hz', key)
|
||||
|
||||
assert buf is not None, f"{key}: buffer not found"
|
||||
assert buf.shape == expected_shape, f"{key}: buffer shape {buf.shape} != expected {expected_shape}"
|
||||
if expected_idxs is not None:
|
||||
assert np.all(idxs == expected_idxs), f"{key}: buffer idxs {idxs} != expected {expected_idxs}"
|
||||
else:
|
||||
assert idxs is None or idxs.size == 0, f"{key}: buffer idxs should be None or empty"
|
||||
|
||||
|
||||
def legacy_buffer_update(buf, new_val, mode, key, constants, idxs, input_shape, prev_desire=None):
|
||||
# This is what we compare the new dynamic logic to, to ensure it does the same thing
|
||||
if mode == 'split':
|
||||
if key == 'desire' or key.startswith('desire'):
|
||||
buf[0,:-1] = buf[0,1:]
|
||||
buf[0,-1] = new_val
|
||||
return buf.reshape((1, constants.INPUT_HISTORY_BUFFER_LEN, constants.TEMPORAL_SKIP, -1)).max(axis=2)
|
||||
elif key == 'features_buffer':
|
||||
buf[0,:-1] = buf[0,1:]
|
||||
buf[0,-1] = new_val
|
||||
return buf[0, idxs]
|
||||
elif key == 'prev_desired_curv':
|
||||
buf[0,:-1] = buf[0,1:]
|
||||
buf[0,-1,:] = new_val
|
||||
return buf[0, idxs]
|
||||
elif mode == '20hz':
|
||||
if key == 'desire':
|
||||
buf[:-1] = buf[1:]
|
||||
buf[-1] = new_val
|
||||
reshape_dims = (1, buf.shape[1], -1, buf.shape[2])
|
||||
reshaped = buf.reshape(reshape_dims).max(axis=2)
|
||||
# Slice to last shape[1] elements to match model input shape
|
||||
input_len = reshaped.shape[1]
|
||||
model_input_len = 25 # For 20hz mode, desire shape[1] is 25
|
||||
if input_len > model_input_len:
|
||||
reshaped = reshaped[:, -model_input_len:, :]
|
||||
return reshaped
|
||||
elif key == 'features_buffer':
|
||||
buffer_history_len = buf.shape[1]
|
||||
legacy_buf = np.zeros((buffer_history_len, buf.shape[2]), dtype=np.float32)
|
||||
legacy_buf[:] = buf[0]
|
||||
legacy_buf[:-1] = legacy_buf[1:]
|
||||
legacy_buf[-1] = new_val
|
||||
return legacy_buf[idxs]
|
||||
elif key == 'prev_desired_curv':
|
||||
buffer_history_len = buf.shape[1]
|
||||
legacy_buf = np.zeros((buffer_history_len, buf.shape[2]), dtype=np.float32)
|
||||
legacy_buf[:] = buf[0]
|
||||
legacy_buf[:-1] = legacy_buf[1:]
|
||||
legacy_buf[-1,:] = new_val
|
||||
return legacy_buf[idxs]
|
||||
elif mode == 'non20hz':
|
||||
if key == 'desire':
|
||||
desire_len = constants.DESIRE_LEN
|
||||
if prev_desire is None:
|
||||
prev_desire = np.zeros(desire_len, dtype=np.float32)
|
||||
# Set first element to zero
|
||||
new_val = new_val.copy()
|
||||
new_val[0] = 0
|
||||
# Shift buffer by desire len
|
||||
buf[0][:-desire_len] = buf[0][desire_len:]
|
||||
# Only insert new desire if rising edge
|
||||
buf[0][-desire_len:] = np.where(new_val - prev_desire > 0.99, new_val, 0)
|
||||
prev_desire[:] = new_val
|
||||
return buf[0]
|
||||
elif key == 'features_buffer':
|
||||
feature_len = constants.FEATURE_LEN
|
||||
buf[0, :-feature_len] = buf[0, feature_len:]
|
||||
buf[0, -feature_len:] = new_val
|
||||
return buf[0, -input_shape[1]:]
|
||||
elif key == 'prev_desired_curv':
|
||||
length = new_val.shape[0]
|
||||
buf[0,:-length,0] = buf[0,length:,0]
|
||||
buf[0,-length:,0] = new_val[:length]
|
||||
return buf[0]
|
||||
return None
|
||||
|
||||
|
||||
def dynamic_buffer_update(state, key, new_val, mode):
|
||||
if key == 'desire' or key.startswith('desire'):
|
||||
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||
for k, v in state.model_runner.input_shapes.items() if k != key}
|
||||
inputs[key] = new_val.copy()
|
||||
# ModelState.run expects desire as a pulse, so we zero the first element.
|
||||
inputs[key][0] = 0
|
||||
state.run({}, {}, inputs, prepare_only=False)
|
||||
return state.numpy_inputs[key]
|
||||
|
||||
if key == 'features_buffer':
|
||||
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||
for k, v in state.model_runner.input_shapes.items() if k != 'features_buffer'}
|
||||
def run_model_stub():
|
||||
return {
|
||||
'hidden_state': np.asarray(new_val, dtype=np.float32).reshape(1, -1),
|
||||
}
|
||||
state.model_runner.run_model = run_model_stub
|
||||
state.run({}, {}, inputs, prepare_only=False)
|
||||
return state.numpy_inputs['features_buffer'][0]
|
||||
|
||||
if key == 'prev_desired_curv':
|
||||
inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32)
|
||||
for k, v in state.model_runner.input_shapes.items() if k != 'prev_desired_curv'}
|
||||
def run_model_stub():
|
||||
return {
|
||||
'hidden_state': np.zeros((1, state.constants.FEATURE_LEN), dtype=np.float32),
|
||||
'desired_curvature': np.asarray(new_val, dtype=np.float32).reshape(1, -1),
|
||||
}
|
||||
state.model_runner.run_model = run_model_stub
|
||||
state.run({}, {}, inputs, prepare_only=False)
|
||||
return state.numpy_inputs['prev_desired_curv'][0]
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"])
|
||||
@pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"])
|
||||
def test_buffer_update_equivalence(shapes, mode, key, apply_patches):
|
||||
state = ModelState(None)
|
||||
if key == "desire":
|
||||
desire_keys = [k for k in shapes.keys() if k.startswith('desire')]
|
||||
if desire_keys:
|
||||
actual_key = desire_keys[0] # Use the first (and likely only) desire key
|
||||
else:
|
||||
actual_key = key
|
||||
|
||||
if actual_key not in state.numpy_inputs:
|
||||
pytest.skip()
|
||||
|
||||
constants = DummyModelRunner(shapes).constants
|
||||
buf = state.input_queues.buffers.get(actual_key, None)
|
||||
idxs = state.input_queues.indices.get(actual_key, None)
|
||||
input_shape = shapes[actual_key]
|
||||
prev_desire = np.zeros(constants.DESIRE_LEN, dtype=np.float32) if key == 'desire' else None
|
||||
|
||||
for step in range(20): # multiple steps to ensure history is built up
|
||||
new_val = np.full((input_shape[2],), step, dtype=np.float32)
|
||||
expected = legacy_buffer_update(buf, new_val, mode, actual_key, constants, idxs, input_shape, prev_desire)
|
||||
actual = dynamic_buffer_update(state, actual_key, new_val, mode)
|
||||
if expected is not None and actual is not None and expected.shape != actual.shape:
|
||||
if expected.ndim == 2 and actual.ndim == 2 and expected.shape[1] == actual.shape[1]:
|
||||
expected = expected[-actual.shape[0]:]
|
||||
assert np.allclose(actual, expected), f"{mode} {actual_key}: dynamic buffer update does not match legacy logic"
|
||||
@@ -11,15 +11,16 @@ import pickle
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
|
||||
|
||||
# Set device environment variable for hardware acceleration
|
||||
# Set QCOM environment variable for TICI devices, potentially enabling hardware acceleration
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
os.environ['AMD'] = '1'
|
||||
os.environ['AMD_IFACE'] = 'USB'
|
||||
elif TICI:
|
||||
os.environ['DEV'] = 'QCOM'
|
||||
os.environ['QCOM'] = '1'
|
||||
else:
|
||||
os.environ['DEV'] = 'LLVM'
|
||||
os.environ['LLVM'] = '1'
|
||||
os.environ['JIT'] = '2' # TODO: This may cause issues
|
||||
|
||||
|
||||
class ModelData:
|
||||
|
||||
@@ -83,7 +83,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
|
||||
|
||||
def _run_model(self) -> NumpyDict:
|
||||
"""Runs the Tinygrad model inference and parses the outputs."""
|
||||
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
outputs = self.model_run(**self.inputs).numpy().flatten()
|
||||
return self._parse_outputs(outputs)
|
||||
|
||||
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:
|
||||
|
||||
@@ -1 +1 @@
|
||||
2ff2f49176a13bc7f856645d785b3b838a5c7ecf7f6cb37699fa0459ebf12453
|
||||
cee4a5f34c3c741fd67e4f130a7c21fd92258c9abfc0416c4d619d94e08a72eb
|
||||
@@ -9,28 +9,23 @@ from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkL
|
||||
|
||||
|
||||
class LatControlTorqueExt(NeuralNetworkLateralControl):
|
||||
def __init__(self, lac_torque, CP, CP_SP, CI):
|
||||
super().__init__(lac_torque, CP, CP_SP, CI)
|
||||
def __init__(self, lac_torque, CP, CP_SP):
|
||||
super().__init__(lac_torque, CP, CP_SP)
|
||||
|
||||
def update(self, CS, VM, pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||
def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation,
|
||||
desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel,
|
||||
desired_curvature, actual_curvature, steer_limited_by_safety, output_torque):
|
||||
desired_curvature, actual_curvature):
|
||||
self._ff = ff
|
||||
self._pid = pid
|
||||
self._pid_log = pid_log
|
||||
self._setpoint = setpoint
|
||||
self._measurement = measurement
|
||||
self._roll_compensation = roll_compensation
|
||||
self._lateral_accel_deadzone = lateral_accel_deadzone
|
||||
self._desired_lateral_accel = desired_lateral_accel
|
||||
self._actual_lateral_accel = actual_lateral_accel
|
||||
self._desired_curvature = desired_curvature
|
||||
self._actual_curvature = actual_curvature
|
||||
self._gravity_adjusted_lateral_accel = gravity_adjusted_lateral_accel
|
||||
self._steer_limited_by_safety = steer_limited_by_safety
|
||||
self._output_torque = output_torque
|
||||
|
||||
self.update_calculations(CS, VM, desired_lateral_accel)
|
||||
self.update_neural_network_feedforward(CS, params, calibrated_pose)
|
||||
|
||||
return self._pid_log, self._output_torque
|
||||
return self._ff, self._pid_log
|
||||
|
||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.pid import PIDController
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
@@ -44,10 +43,9 @@ def get_lookahead_value(future_vals, current_val):
|
||||
|
||||
|
||||
class LatControlTorqueExtBase:
|
||||
def __init__(self, lac_torque, CP, CP_SP, CI):
|
||||
def __init__(self, lac_torque, CP, CP_SP):
|
||||
self.model_v2 = None
|
||||
self.model_valid = False
|
||||
self.lac_torque = lac_torque
|
||||
self.torque_params = lac_torque.torque_params
|
||||
|
||||
self.actual_lateral_jerk: float = 0.0
|
||||
@@ -55,22 +53,17 @@ class LatControlTorqueExtBase:
|
||||
self.lateral_jerk_measurement: float = 0.0
|
||||
self.lookahead_lateral_jerk: float = 0.0
|
||||
|
||||
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
|
||||
self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel
|
||||
|
||||
self._ff = 0.0
|
||||
self._pid = PIDController(0.0, 0.0, k_f=0.0)
|
||||
self._pid_log = None
|
||||
self._setpoint = 0.0
|
||||
self._measurement = 0.0
|
||||
self._roll_compensation = 0.0
|
||||
self._lateral_accel_deadzone = 0.0
|
||||
self._desired_lateral_accel = 0.0
|
||||
self._actual_lateral_accel = 0.0
|
||||
self._desired_curvature = 0.0
|
||||
self._actual_curvature = 0.0
|
||||
self._gravity_adjusted_lateral_accel = 0.0
|
||||
self._steer_limited_by_safety = False
|
||||
self._output_torque = 0.0
|
||||
|
||||
# twilsonco's Lateral Neural Network Feedforward
|
||||
# Instantaneous lateral jerk changes very rapidly, making it not useful on its own,
|
||||
|
||||
@@ -9,8 +9,6 @@ import math
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction
|
||||
from opendbc.sunnypilot.car.interfaces import LatControlInputs
|
||||
from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
@@ -32,8 +30,8 @@ def roll_pitch_adjust(roll, pitch):
|
||||
|
||||
|
||||
class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
def __init__(self, lac_torque, CP, CP_SP, CI):
|
||||
super().__init__(lac_torque, CP, CP_SP, CI)
|
||||
def __init__(self, lac_torque, CP, CP_SP):
|
||||
super().__init__(lac_torque, CP, CP_SP)
|
||||
self.params = Params()
|
||||
self.enabled = self.params.get_bool("NeuralNetworkLateralControl")
|
||||
self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH
|
||||
@@ -59,44 +57,14 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
self.error_deque = deque(maxlen=history_check_frames[0])
|
||||
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
|
||||
|
||||
@property
|
||||
def _nnlc_enabled(self):
|
||||
return self.enabled and self.model_valid and self.has_nn_model
|
||||
|
||||
def update_limits(self):
|
||||
if not self._nnlc_enabled:
|
||||
return
|
||||
|
||||
self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max)
|
||||
|
||||
def update_lateral_lag(self, lag):
|
||||
super().update_lateral_lag(lag)
|
||||
self.nn_future_times = [t + self.desired_lat_jerk_time for t in self.future_times]
|
||||
|
||||
def update_feedforward_torque_space(self, CS):
|
||||
torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self._pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
|
||||
CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True)
|
||||
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
|
||||
FRICTION_THRESHOLD, self.torque_params)
|
||||
|
||||
def update_output_torque(self, CS):
|
||||
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
self._output_torque = self._pid.update(self._pid_log.error,
|
||||
feedforward=self._ff,
|
||||
speed=CS.vEgo,
|
||||
freeze_integrator=freeze_integrator)
|
||||
|
||||
def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None:
|
||||
if not self._nnlc_enabled:
|
||||
if not self.enabled or not self.model_valid or not self.has_nn_model:
|
||||
return
|
||||
|
||||
self.update_feedforward_torque_space(CS)
|
||||
|
||||
low_speed_factor = float(np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)) ** 2
|
||||
self._setpoint = self._desired_lateral_accel + low_speed_factor * self._desired_curvature
|
||||
self._measurement = self._actual_lateral_accel + low_speed_factor * self._actual_curvature
|
||||
@@ -160,5 +128,3 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.model.friction_override:
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
|
||||
self.update_output_torque(CS)
|
||||
|
||||
@@ -3,7 +3,6 @@ from parameterized import parameterized
|
||||
|
||||
from cereal import car, log, messaging
|
||||
from opendbc.car.car_helpers import interfaces
|
||||
from opendbc.car.gm.values import CAR as GM
|
||||
from opendbc.car.honda.values import CAR as HONDA
|
||||
from opendbc.car.hyundai.values import CAR as HYUNDAI
|
||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||
@@ -42,7 +41,7 @@ def generate_modelV2():
|
||||
|
||||
class TestNeuralNetworkLateralControl:
|
||||
|
||||
@parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, GM.CHEVROLET_BOLT_EUV])
|
||||
@parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN])
|
||||
def test_saturation(self, car_name):
|
||||
params = Params()
|
||||
params.put_bool("NeuralNetworkLateralControl", True)
|
||||
@@ -58,7 +57,6 @@ class TestNeuralNetworkLateralControl:
|
||||
VM = VehicleModel(CP)
|
||||
|
||||
controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI)
|
||||
torque_params = CP.lateralTuning.torque
|
||||
|
||||
CS = car.CarState.new_message()
|
||||
CS.vEgo = 30
|
||||
@@ -79,23 +77,17 @@ class TestNeuralNetworkLateralControl:
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
|
||||
controller.extension.update_limits()
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True)
|
||||
assert lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
|
||||
controller.extension.update_limits()
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False)
|
||||
assert not lac_log.saturated
|
||||
|
||||
for _ in range(1000):
|
||||
controller.extension.update_model_v2(model_v2)
|
||||
controller.extension.update_lateral_lag(test_lag)
|
||||
controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction)
|
||||
controller.extension.update_limits()
|
||||
_, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False)
|
||||
assert lac_log.saturated
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# C3 specific hardware code
|
||||
|
||||
`c3` is known as `tici` and comma three by comma. Not to confuse it with `c3x` which is known as `tizi`.
|
||||
@@ -1,84 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "xbl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz",
|
||||
"hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b",
|
||||
"size": 3282256,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89"
|
||||
},
|
||||
{
|
||||
"name": "xbl_config",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz",
|
||||
"hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c",
|
||||
"size": 98124,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f"
|
||||
},
|
||||
{
|
||||
"name": "abl",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz",
|
||||
"hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6",
|
||||
"size": 274432,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6"
|
||||
},
|
||||
{
|
||||
"name": "aop",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz",
|
||||
"hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9",
|
||||
"size": 184364,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180"
|
||||
},
|
||||
{
|
||||
"name": "devcfg",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz",
|
||||
"hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620",
|
||||
"size": 40336,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f"
|
||||
},
|
||||
{
|
||||
"name": "boot",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz",
|
||||
"hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4",
|
||||
"size": 18515968,
|
||||
"sparse": false,
|
||||
"full_check": true,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
|
||||
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
|
||||
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
|
||||
"alt": {
|
||||
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SP_C3_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
DIR="$( cd "$SP_C3_DIR/../../../.." >/dev/null 2>&1 && pwd )"
|
||||
|
||||
source "$SP_C3_DIR/launch_env.sh"
|
||||
|
||||
function agnos_init {
|
||||
# TODO: move this to agnos
|
||||
sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta
|
||||
|
||||
# set success flag for current boot slot
|
||||
sudo abctl --set_success
|
||||
|
||||
# TODO: do this without udev in AGNOS
|
||||
# udev does this, but sometimes we startup faster
|
||||
sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
|
||||
sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
|
||||
|
||||
|
||||
if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then
|
||||
AGNOS_PY="$DIR/system/hardware/tici/agnos.py"
|
||||
MANIFEST="$SP_C3_DIR/agnos.json"
|
||||
if $AGNOS_PY --verify $MANIFEST; then
|
||||
sudo reboot
|
||||
fi
|
||||
$DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST
|
||||
fi
|
||||
}
|
||||
|
||||
function launch {
|
||||
# Remove orphaned git lock if it exists on boot
|
||||
[ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock
|
||||
|
||||
# Check to see if there's a valid overlay-based update available. Conditions
|
||||
# are as follows:
|
||||
#
|
||||
# 1. The DIR init file has to exist, with a newer modtime than anything in
|
||||
# the DIR Git repo. This checks for local development work or the user
|
||||
# switching branches/forks, which should not be overwritten.
|
||||
# 2. The FINALIZED consistent file has to exist, indicating there's an update
|
||||
# that completed successfully and synced to disk.
|
||||
|
||||
if [ -f "${DIR}/.overlay_init" ]; then
|
||||
find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${DIR} has been modified, skipping overlay update installation"
|
||||
else
|
||||
if [ -f "${STAGING_ROOT}/finalized/.overlay_consistent" ]; then
|
||||
if [ ! -d /data/safe_staging/old_openpilot ]; then
|
||||
echo "Valid overlay update found, installing"
|
||||
LAUNCHER_LOCATION="${BASH_SOURCE[0]}"
|
||||
|
||||
mv $DIR /data/safe_staging/old_openpilot
|
||||
mv "${STAGING_ROOT}/finalized" $DIR
|
||||
cd $DIR
|
||||
|
||||
echo "Restarting launch script ${LAUNCHER_LOCATION}"
|
||||
unset AGNOS_VERSION
|
||||
exec "${LAUNCHER_LOCATION}"
|
||||
else
|
||||
echo "openpilot backup found, not updating"
|
||||
# TODO: restore backup? This means the updater didn't start after swapping
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# handle pythonpath
|
||||
ln -sfn $(pwd) /data/pythonpath
|
||||
export PYTHONPATH="$PWD"
|
||||
|
||||
# hardware specific init
|
||||
if [ -f /AGNOS ]; then
|
||||
agnos_init
|
||||
fi
|
||||
|
||||
# write tmux scrollback to a file
|
||||
tmux capture-pane -pq -S-1000 > /tmp/launch_log
|
||||
|
||||
# start manager
|
||||
cd $DIR/system/manager
|
||||
if [ ! -f $DIR/prebuilt ]; then
|
||||
./build.py
|
||||
fi
|
||||
./manager.py
|
||||
|
||||
# if broken, keep on screen error
|
||||
while true; do sleep 1; done
|
||||
}
|
||||
|
||||
launch
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export OMP_NUM_THREADS=1
|
||||
export MKL_NUM_THREADS=1
|
||||
export NUMEXPR_NUM_THREADS=1
|
||||
export OPENBLAS_NUM_THREADS=1
|
||||
export VECLIB_MAXIMUM_THREADS=1
|
||||
|
||||
if [ -z "$AGNOS_VERSION" ]; then
|
||||
export AGNOS_VERSION="12.8"
|
||||
fi
|
||||
@@ -381,22 +381,20 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = N
|
||||
return {"success": 1}
|
||||
|
||||
|
||||
def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]:
|
||||
if base is None:
|
||||
base = path
|
||||
def scan_dir(path: str, prefix: str) -> list[str]:
|
||||
files = []
|
||||
# only walk directories that match the prefix
|
||||
# (glob and friends traverse entire dir tree)
|
||||
with os.scandir(path) as i:
|
||||
for e in i:
|
||||
rel_path = os.path.relpath(e.path, base)
|
||||
rel_path = os.path.relpath(e.path, Paths.log_root())
|
||||
if e.is_dir(follow_symlinks=False):
|
||||
# add trailing slash
|
||||
rel_path = os.path.join(rel_path, '')
|
||||
# if prefix is a partial dir name, current dir will start with prefix
|
||||
# if prefix is a partial file name, prefix with start with dir name
|
||||
if rel_path.startswith(prefix) or prefix.startswith(rel_path):
|
||||
files.extend(scan_dir(e.path, prefix, base))
|
||||
files.extend(scan_dir(e.path, prefix))
|
||||
else:
|
||||
if rel_path.startswith(prefix):
|
||||
files.append(rel_path)
|
||||
@@ -404,12 +402,7 @@ def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]:
|
||||
|
||||
@dispatcher.add_method
|
||||
def listDataDirectory(prefix='') -> list[str]:
|
||||
internal_files = scan_dir(Paths.log_root(), prefix, Paths.log_root())
|
||||
try:
|
||||
external_files = scan_dir(Paths.log_root_external(), prefix, Paths.log_root_external())
|
||||
except FileNotFoundError:
|
||||
external_files = []
|
||||
return sorted(set(internal_files + external_files))
|
||||
return scan_dir(Paths.log_root(), prefix)
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
@@ -434,13 +427,8 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo
|
||||
failed.append(file.fn)
|
||||
continue
|
||||
|
||||
path_internal = os.path.join(Paths.log_root(), file.fn)
|
||||
path_external = os.path.join(Paths.log_root_external(), file.fn)
|
||||
if os.path.exists(path_internal) or os.path.exists(strip_zst_extension(path_internal)):
|
||||
path = path_internal
|
||||
elif os.path.exists(path_external) or os.path.exists(strip_zst_extension(path_external)):
|
||||
path = path_external
|
||||
else:
|
||||
path = os.path.join(Paths.log_root(), file.fn)
|
||||
if not os.path.exists(path) and not os.path.exists(strip_zst_extension(path)):
|
||||
failed.append(file.fn)
|
||||
continue
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#if SENSOR_ID == 1
|
||||
|
||||
#define VIGNETTE_PROFILE_8DT0MM
|
||||
|
||||
#define BIT_DEPTH 12
|
||||
#define PV_MAX 4096
|
||||
#define BLACK_LVL 168
|
||||
|
||||
float4 normalize_pv(int4 parsed, float vignette_factor) {
|
||||
float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX - BLACK_LVL);
|
||||
return clamp(pv*vignette_factor, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float3 color_correct(float3 rgb) {
|
||||
float3 corrected = rgb.x * (float3)(1.82717181, -0.31231438, 0.07307673);
|
||||
corrected += rgb.y * (float3)(-0.5743977, 1.36858544, -0.53183455);
|
||||
corrected += rgb.z * (float3)(-0.25277411, -0.05627105, 1.45875782);
|
||||
return corrected;
|
||||
}
|
||||
|
||||
float3 apply_gamma(float3 rgb, int expo_time) {
|
||||
// tone mapping params
|
||||
const float gamma_k = 0.75;
|
||||
const float gamma_b = 0.125;
|
||||
const float mp = 0.01; // ideally midpoint should be adaptive
|
||||
const float rk = 9 - 100*mp;
|
||||
|
||||
// poly approximation for s curve
|
||||
return (rgb > mp) ?
|
||||
((rk * (rgb-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(rgb-mp))) + gamma_k*mp + gamma_b) :
|
||||
((rk * (rgb-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(rgb-mp))) + gamma_k*mp + gamma_b);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
#if SENSOR_ID == 3
|
||||
|
||||
#define BGGR
|
||||
#define VIGNETTE_PROFILE_4DT6MM
|
||||
|
||||
#define BIT_DEPTH 12
|
||||
#define PV_MAX10 1023
|
||||
#define PV_MAX12 4095
|
||||
#define PV_MAX16 65536 // gamma curve is calibrated to 16bit
|
||||
#define BLACK_LVL 48
|
||||
|
||||
float combine_dual_pvs(float lv, float sv, int expo_time) {
|
||||
float svc = fmax(sv * expo_time, (float)(64 * (PV_MAX10 - BLACK_LVL)));
|
||||
float svd = sv * fmin(expo_time, 8.0) / 8;
|
||||
|
||||
if (expo_time > 64) {
|
||||
if (lv < PV_MAX10 - BLACK_LVL) {
|
||||
return lv / (PV_MAX16 - BLACK_LVL);
|
||||
} else {
|
||||
return (svc / 64) / (PV_MAX16 - BLACK_LVL);
|
||||
}
|
||||
} else {
|
||||
if (lv > 32) {
|
||||
return (lv * 64 / fmax(expo_time, 8.0)) / (PV_MAX16 - BLACK_LVL);
|
||||
} else {
|
||||
return svd / (PV_MAX16 - BLACK_LVL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float4 normalize_pv_hdr(int4 parsed, int4 short_parsed, float vignette_factor, int expo_time) {
|
||||
float4 pl = convert_float4(parsed - BLACK_LVL);
|
||||
float4 ps = convert_float4(short_parsed - BLACK_LVL);
|
||||
float4 pv;
|
||||
pv.s0 = combine_dual_pvs(pl.s0, ps.s0, expo_time);
|
||||
pv.s1 = combine_dual_pvs(pl.s1, ps.s1, expo_time);
|
||||
pv.s2 = combine_dual_pvs(pl.s2, ps.s2, expo_time);
|
||||
pv.s3 = combine_dual_pvs(pl.s3, ps.s3, expo_time);
|
||||
return clamp(pv*vignette_factor, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 normalize_pv(int4 parsed, float vignette_factor) {
|
||||
float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX12 - BLACK_LVL);
|
||||
return clamp(pv*vignette_factor, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float3 color_correct(float3 rgb) {
|
||||
float3 corrected = rgb.x * (float3)(1.55361989, -0.268894615, -0.000593219);
|
||||
corrected += rgb.y * (float3)(-0.421217301, 1.51883144, -0.69760146);
|
||||
corrected += rgb.z * (float3)(-0.132402589, -0.249936825, 1.69819468);
|
||||
return corrected;
|
||||
}
|
||||
|
||||
float3 apply_gamma(float3 rgb, int expo_time) {
|
||||
return (10 * rgb) / (1 + 9 * rgb);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#if SENSOR_ID == 2
|
||||
|
||||
#define VIGNETTE_PROFILE_8DT0MM
|
||||
|
||||
#define BIT_DEPTH 12
|
||||
#define BLACK_LVL 64
|
||||
|
||||
float ox_lut_func(int x) {
|
||||
if (x < 512) {
|
||||
return x * 5.94873e-8;
|
||||
} else if (512 <= x && x < 768) {
|
||||
return 3.0458e-05 + (x-512) * 1.19913e-7;
|
||||
} else if (768 <= x && x < 1536) {
|
||||
return 6.1154e-05 + (x-768) * 2.38493e-7;
|
||||
} else if (1536 <= x && x < 1792) {
|
||||
return 0.0002448 + (x-1536) * 9.56930e-7;
|
||||
} else if (1792 <= x && x < 2048) {
|
||||
return 0.00048977 + (x-1792) * 1.91441e-6;
|
||||
} else if (2048 <= x && x < 2304) {
|
||||
return 0.00097984 + (x-2048) * 3.82937e-6;
|
||||
} else if (2304 <= x && x < 2560) {
|
||||
return 0.0019601 + (x-2304) * 7.659055e-6;
|
||||
} else if (2560 <= x && x < 2816) {
|
||||
return 0.0039207 + (x-2560) * 1.525e-5;
|
||||
} else {
|
||||
return 0.0078421 + (exp((x-2816)/273.0) - 1) * 0.0092421;
|
||||
}
|
||||
}
|
||||
|
||||
float4 normalize_pv(int4 parsed, float vignette_factor) {
|
||||
// PWL
|
||||
float4 pv = {ox_lut_func(parsed.s0), ox_lut_func(parsed.s1), ox_lut_func(parsed.s2), ox_lut_func(parsed.s3)};
|
||||
return clamp(pv*vignette_factor*256.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
float3 color_correct(float3 rgb) {
|
||||
float3 corrected = rgb.x * (float3)(1.5664815, -0.29808738, -0.03973474);
|
||||
corrected += rgb.y * (float3)(-0.48672447, 1.41914433, -0.40295248);
|
||||
corrected += rgb.z * (float3)(-0.07975703, -0.12105695, 1.44268722);
|
||||
return corrected;
|
||||
}
|
||||
|
||||
float3 apply_gamma(float3 rgb, int expo_time) {
|
||||
return -0.507089*exp(-12.54124638*rgb) + 0.9655*powr(rgb, 0.5) - 0.472597*rgb + 0.507089;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -21,16 +21,16 @@ class TiciFanController(BaseFanController):
|
||||
self.controller = PIDController(k_p=0, k_i=4e-3, k_f=1, rate=(1 / DT_HW))
|
||||
|
||||
def update(self, cur_temp: float, ignition: bool) -> int:
|
||||
self.controller.pos_limit = 100 if ignition else 30
|
||||
self.controller.neg_limit = 30 if ignition else 0
|
||||
self.controller.neg_limit = -(100 if ignition else 30)
|
||||
self.controller.pos_limit = -(30 if ignition else 0)
|
||||
|
||||
if ignition != self.last_ignition:
|
||||
self.controller.reset()
|
||||
|
||||
error = cur_temp - 75
|
||||
fan_pwr_out = int(self.controller.update(
|
||||
error = 75 - cur_temp
|
||||
fan_pwr_out = -int(self.controller.update(
|
||||
error=error,
|
||||
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, 100])
|
||||
feedforward=np.interp(cur_temp, [60.0, 100.0], [0, -100])
|
||||
))
|
||||
|
||||
self.last_ignition = ignition
|
||||
|
||||
@@ -20,10 +20,6 @@ class Paths:
|
||||
else:
|
||||
return '/data/media/0/realdata/'
|
||||
|
||||
@staticmethod
|
||||
def log_root_external() -> str:
|
||||
return '/mnt/external_realdata/'
|
||||
|
||||
@staticmethod
|
||||
def swaglog_root() -> str:
|
||||
if PC:
|
||||
|
||||
@@ -67,17 +67,17 @@
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
|
||||
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
|
||||
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz",
|
||||
"hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1",
|
||||
"hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
|
||||
"ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f",
|
||||
"alt": {
|
||||
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
|
||||
"hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,51 +350,51 @@
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz",
|
||||
"hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126",
|
||||
"hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img.xz",
|
||||
"hash": "49faee0e9b084abf0ea46f87722e3366bbd0435fb6b25cce189295c1ff368da1",
|
||||
"hash_raw": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643",
|
||||
"size": 5368709120,
|
||||
"sparse": true,
|
||||
"full_check": false,
|
||||
"has_ab": true,
|
||||
"ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7",
|
||||
"ondevice_hash": "db07761be0130e35a9d3ea6bec8df231260d3e767ae770850f18f10e14d0ab3f",
|
||||
"alt": {
|
||||
"hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img",
|
||||
"hash": "18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/system-18100d9065bb44a315262041b9fb6bfd9e59179981876e442200cc1284d43643.img",
|
||||
"size": 5368709120
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userdata_90",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a.img.xz",
|
||||
"hash": "6a11d448bac50467791809339051eed2894aae971c37bf6284b3b972a99ba3ac",
|
||||
"hash_raw": "602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_90-02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda.img.xz",
|
||||
"hash": "1eda66d4e31222fc2e792a62ae8e7d322fc643f0b23785e7527bb51a9fee97c7",
|
||||
"hash_raw": "02f7abb4b667c04043c0c6950145aaebd704851261f32256d0f7e84a52059dda",
|
||||
"size": 96636764160,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "e014d92940a696bf8582807259820ab73948b950656ed83a45da738f26083705"
|
||||
"ondevice_hash": "679b650ee04b7b1ef610b63fde9b43569fded39ceacf88789b564de99c221ea1"
|
||||
},
|
||||
{
|
||||
"name": "userdata_89",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48.img.xz",
|
||||
"hash": "748e31a5fc01fc256c012e359c3382d1f98cce98feafe8ecc0fca3e47caef116",
|
||||
"hash_raw": "4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_89-bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588.img.xz",
|
||||
"hash": "e63d3277285aae1f04fd7f4f48429ce35010f4843ab755f10d360c3aa788e484",
|
||||
"hash_raw": "bab8399bbe3968f3c496f7bc83c2541b33acc1f47814c4ad95801bf5cb7e7588",
|
||||
"size": 95563022336,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "c181b93050787adcfef730c086bcb780f28508d84e6376d9b80d37e5dc02b55e"
|
||||
"ondevice_hash": "2947374fc5980ffe3c5b94b61cc1c81bc55214f494153ed234164801731f5dc0"
|
||||
},
|
||||
{
|
||||
"name": "userdata_30",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668.img.xz",
|
||||
"hash": "09ff390e639e4373d772e1688d05a5ac77a573463ed1deeff86390686fa686f9",
|
||||
"hash_raw": "80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668",
|
||||
"url": "https://commadist.azureedge.net/agnosupdate/userdata_30-22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0.img.xz",
|
||||
"hash": "12d9245711e8c49c51ff2c7b82d7301f2fcb1911edcddb35a105a80911859113",
|
||||
"hash_raw": "22c874b4b66bbc000f3219abede8d62cb307f5786fd526a8473c61422765dea0",
|
||||
"size": 32212254720,
|
||||
"sparse": true,
|
||||
"full_check": true,
|
||||
"has_ab": false,
|
||||
"ondevice_hash": "2c01ab470c02121c721ff6afc25582437e821686207f3afef659387afb69c507"
|
||||
"ondevice_hash": "03c8b65c945207f887ed6c52d38b53d53d71c8597dcb0b63dfbb11f7cfff8d2b"
|
||||
}
|
||||
]
|
||||
@@ -9,26 +9,21 @@ STATS_DIR_FILE_LIMIT = 10000
|
||||
STATS_SOCKET = "ipc:///tmp/stats"
|
||||
STATS_FLUSH_TIME_S = 60
|
||||
|
||||
PATH_DICT = {
|
||||
"internal": Paths.log_root(),
|
||||
"external": Paths.log_root_external()
|
||||
}
|
||||
|
||||
def get_available_percent(default: float, path_type="internal") -> float:
|
||||
def get_available_percent(default: float) -> float:
|
||||
try:
|
||||
statvfs = os.statvfs(PATH_DICT[path_type])
|
||||
statvfs = os.statvfs(Paths.log_root())
|
||||
available_percent = 100.0 * statvfs.f_bavail / statvfs.f_blocks
|
||||
except (OSError, KeyError):
|
||||
except OSError:
|
||||
available_percent = default
|
||||
|
||||
return available_percent
|
||||
|
||||
|
||||
def get_available_bytes(default: int, path_type="internal") -> int:
|
||||
def get_available_bytes(default: int) -> int:
|
||||
try:
|
||||
statvfs = os.statvfs(PATH_DICT[path_type])
|
||||
statvfs = os.statvfs(Paths.log_root())
|
||||
available_bytes = statvfs.f_bavail * statvfs.f_frsize
|
||||
except (OSError, KeyError):
|
||||
except OSError:
|
||||
available_bytes = default
|
||||
|
||||
return available_bytes
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.loggerd.config import get_available_bytes, get_available_percent
|
||||
@@ -63,41 +61,6 @@ def deleter_thread(exit_event: threading.Event):
|
||||
if any(name.endswith(".lock") for name in os.listdir(delete_path)):
|
||||
continue
|
||||
|
||||
if Path(Paths.log_root_external()).is_mount():
|
||||
out_of_bytes_external = get_available_bytes(default=MIN_BYTES + 1, path_type="external") < MIN_BYTES
|
||||
out_of_percent_external = get_available_percent(default=MIN_PERCENT + 1, path_type="external") < MIN_PERCENT
|
||||
|
||||
if out_of_percent_external or out_of_bytes_external:
|
||||
dirs_external = listdir_by_creation(Paths.log_root_external())
|
||||
|
||||
# remove the earliest external directory we can
|
||||
for delete_dir_external in sorted(dirs_external):
|
||||
delete_path_external = os.path.join(Paths.log_root_external(), delete_dir_external)
|
||||
try:
|
||||
cloudlog.warning(f"deleting {delete_path_external}")
|
||||
shutil.rmtree(delete_path_external)
|
||||
break
|
||||
except OSError:
|
||||
cloudlog.exception(f"issue deleting {delete_path_external}")
|
||||
|
||||
# move directory from internal to external
|
||||
path_external = os.path.join(Paths.log_root_external(), delete_dir)
|
||||
try:
|
||||
cloudlog.warning(f"moving {delete_path} to {path_external}")
|
||||
start = time.monotonic()
|
||||
shutil.move(delete_path, path_external)
|
||||
cloudlog.warning(f"moved {delete_path} to {path_external} in {time.monotonic() - start:.2f}s")
|
||||
break
|
||||
except Exception:
|
||||
cloudlog.error(f"issue moving {delete_path} to {path_external}")
|
||||
try:
|
||||
cloudlog.warning(f"deleting {delete_path}")
|
||||
shutil.rmtree(delete_path)
|
||||
break
|
||||
except OSError:
|
||||
cloudlog.exception(f"issue deleting {delete_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
cloudlog.info(f"deleting {delete_path}")
|
||||
shutil.rmtree(delete_path)
|
||||
|
||||
@@ -44,7 +44,7 @@ def manager_init() -> None:
|
||||
# set unset params to their default value
|
||||
for k in params.all_keys():
|
||||
default_value = params.get_default_value(k)
|
||||
if default_value is not None and params.get(k) is None:
|
||||
if default_value and params.get(k) is None:
|
||||
params.put(k, default_value)
|
||||
|
||||
# Create folders needed for msgq
|
||||
|
||||
@@ -46,10 +46,9 @@ class TestManager:
|
||||
manager.main()
|
||||
for k in params.all_keys():
|
||||
default_value = params.get_default_value(k)
|
||||
if default_value is not None:
|
||||
if default_value:
|
||||
assert params.get(k) == default_value
|
||||
assert params.get("OpenpilotEnabledToggle")
|
||||
assert params.get("RouteCount") == 0
|
||||
|
||||
@pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad")
|
||||
def test_clean_exit(self, subtests):
|
||||
|
||||
@@ -67,8 +67,7 @@ class MouseEvent(NamedTuple):
|
||||
|
||||
|
||||
class MouseState:
|
||||
def __init__(self, scale: float = 1.0):
|
||||
self._scale = scale
|
||||
def __init__(self):
|
||||
self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list
|
||||
self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS
|
||||
|
||||
@@ -103,10 +102,8 @@ class MouseState:
|
||||
def _handle_mouse_event(self):
|
||||
for slot in range(MAX_TOUCH_SLOTS):
|
||||
mouse_pos = rl.get_touch_position(slot)
|
||||
x = mouse_pos.x / self._scale if self._scale != 1.0 else mouse_pos.x
|
||||
y = mouse_pos.y / self._scale if self._scale != 1.0 else mouse_pos.y
|
||||
ev = MouseEvent(
|
||||
MousePos(x, y),
|
||||
MousePos(mouse_pos.x, mouse_pos.y),
|
||||
slot,
|
||||
rl.is_mouse_button_pressed(slot),
|
||||
rl.is_mouse_button_released(slot),
|
||||
@@ -136,7 +133,7 @@ class GuiApplication:
|
||||
self._trace_log_callback = None
|
||||
self._modal_overlay = ModalOverlay()
|
||||
|
||||
self._mouse = MouseState(self._scale)
|
||||
self._mouse = MouseState()
|
||||
self._mouse_events: list[MouseEvent] = []
|
||||
|
||||
# Debug variables
|
||||
|
||||
@@ -39,10 +39,6 @@ vec4 getGradientColor(vec2 pos) {
|
||||
float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0);
|
||||
|
||||
if (gradientColorCount <= 1) return gradientColors[0];
|
||||
|
||||
// handle t before first / after last stop
|
||||
if (t <= gradientStops[0]) return gradientColors[0];
|
||||
if (t >= gradientStops[gradientColorCount-1]) return gradientColors[gradientColorCount-1];
|
||||
for (int i = 0; i < gradientColorCount - 1; i++) {
|
||||
if (t >= gradientStops[i] && t <= gradientStops[i+1]) {
|
||||
float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]);
|
||||
|
||||
@@ -37,7 +37,7 @@ NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device"
|
||||
NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8
|
||||
|
||||
TETHERING_IP_ADDRESS = "192.168.43.1"
|
||||
DEFAULT_TETHERING_PASSWORD = "swagswagcomma"
|
||||
DEFAULT_TETHERING_PASSWORD = "12345678"
|
||||
|
||||
|
||||
# NetworkManager device states
|
||||
|
||||
+18
-74
@@ -6,14 +6,10 @@ import time
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse
|
||||
from enum import IntEnum
|
||||
import shutil
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from cereal import log
|
||||
from openpilot.common.run import run_cmd
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
|
||||
@@ -34,19 +30,6 @@ BUTTON_SPACING = 50
|
||||
OPENPILOT_URL = "https://openpilot.comma.ai"
|
||||
USER_AGENT = f"AGNOSSetup-{HARDWARE.get_os_version()}"
|
||||
|
||||
CONTINUE_PATH = "/data/continue.sh"
|
||||
TMP_CONTINUE_PATH = "/data/continue.sh.new"
|
||||
INSTALL_PATH = "/data/openpilot"
|
||||
VALID_CACHE_PATH = "/data/.openpilot_cache"
|
||||
INSTALLER_SOURCE_PATH = "/usr/comma/installer"
|
||||
INSTALLER_DESTINATION_PATH = "/tmp/installer"
|
||||
INSTALLER_URL_PATH = "/tmp/installer_url"
|
||||
|
||||
CONTINUE = """#!/usr/bin/env bash
|
||||
|
||||
cd /data/openpilot
|
||||
exec ./launch_openpilot.sh
|
||||
"""
|
||||
|
||||
class SetupState(IntEnum):
|
||||
LOW_VOLTAGE = 0
|
||||
@@ -110,21 +93,14 @@ class Setup(Widget):
|
||||
self._network_setup_continue_button.set_enabled(False)
|
||||
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT)
|
||||
|
||||
self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback,
|
||||
button_style=ButtonStyle.PRIMARY)
|
||||
self._custom_software_warning_continue_button.set_enabled(False)
|
||||
self._custom_software_warning_continue_button = Button("Continue", self._custom_software_warning_continue_button_callback)
|
||||
self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback)
|
||||
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 100, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255,89,79,255),
|
||||
text_padding=60)
|
||||
self._custom_software_warning_body_label = Label("Use caution when installing third-party software.\n\n"
|
||||
+ "⚠️ It has not been tested by comma.\n\n"
|
||||
+ "⚠️ It may not comply with relevant safety standards.\n\n"
|
||||
+ "⚠️ It may cause damage to your device and/or vehicle.\n\n"
|
||||
+ "If you'd like to proceed, use https://flash.comma.ai "
|
||||
self._custom_software_warning_body_label = Label("Use caution when installing third-party software. Third-party software has not been tested by comma,"
|
||||
+ " and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai "
|
||||
+ "to restore your device to a factory state later.",
|
||||
85, text_alignment=TextAlignment.LEFT, text_padding=60)
|
||||
self._custom_software_warning_body_scroll_panel = GuiScrollPanel()
|
||||
|
||||
self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM)
|
||||
|
||||
try:
|
||||
@@ -160,19 +136,21 @@ class Setup(Widget):
|
||||
self.state = SetupState.SOFTWARE_SELECTION
|
||||
|
||||
def _custom_software_warning_continue_button_callback(self):
|
||||
self.state = SetupState.CUSTOM_SOFTWARE
|
||||
|
||||
def _getting_started_button_callback(self):
|
||||
self.state = SetupState.NETWORK_SETUP
|
||||
self.stop_network_check_thread.clear()
|
||||
self.start_network_check()
|
||||
|
||||
def _getting_started_button_callback(self):
|
||||
self.state = SetupState.SOFTWARE_SELECTION
|
||||
|
||||
def _software_selection_back_button_callback(self):
|
||||
self.state = SetupState.GETTING_STARTED
|
||||
self.state = SetupState.NETWORK_SETUP
|
||||
self.stop_network_check_thread.clear()
|
||||
self.start_network_check()
|
||||
|
||||
def _software_selection_continue_button_callback(self):
|
||||
if self._software_selection_openpilot_button.selected:
|
||||
self.use_openpilot()
|
||||
self.download(OPENPILOT_URL)
|
||||
else:
|
||||
self.state = SetupState.CUSTOM_SOFTWARE_WARNING
|
||||
|
||||
@@ -180,14 +158,11 @@ class Setup(Widget):
|
||||
self.state = SetupState.GETTING_STARTED
|
||||
|
||||
def _network_setup_back_button_callback(self):
|
||||
self.state = SetupState.SOFTWARE_SELECTION
|
||||
self.state = SetupState.GETTING_STARTED
|
||||
|
||||
def _network_setup_continue_button_callback(self):
|
||||
self.state = SetupState.SOFTWARE_SELECTION
|
||||
self.stop_network_check_thread.set()
|
||||
if self._software_selection_openpilot_button.selected:
|
||||
self.download(OPENPILOT_URL)
|
||||
else:
|
||||
self.state = SetupState.CUSTOM_SOFTWARE
|
||||
|
||||
def render_low_voltage(self, rect: rl.Rectangle):
|
||||
rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE)
|
||||
@@ -299,23 +274,13 @@ class Setup(Widget):
|
||||
self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT))
|
||||
|
||||
def render_custom_software_warning(self, rect: rl.Rectangle):
|
||||
warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500)
|
||||
offset = self._custom_software_warning_body_scroll_panel.handle_scroll(rect, warn_rect)
|
||||
self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, rect.y + 150, rect.width - 265, TITLE_FONT_SIZE))
|
||||
self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, rect.y + 200 , rect.width - 50, BODY_FONT_SIZE * 3))
|
||||
|
||||
button_width = (rect.width - MARGIN * 3) / 2
|
||||
button_y = rect.height - MARGIN - BUTTON_HEIGHT
|
||||
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE))
|
||||
y_offset = rect.y + offset.y
|
||||
self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE))
|
||||
self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 200 , rect.width - 50, BODY_FONT_SIZE * 3))
|
||||
rl.end_scissor_mode()
|
||||
|
||||
self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT))
|
||||
self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT))
|
||||
if offset.y < (rect.height - warn_rect.height):
|
||||
self._custom_software_warning_continue_button.set_enabled(True)
|
||||
self._custom_software_warning_continue_button.set_text("Continue")
|
||||
|
||||
def render_custom_software(self):
|
||||
def handle_keyboard_result(result):
|
||||
@@ -334,23 +299,6 @@ class Setup(Widget):
|
||||
self.keyboard.set_title("Enter URL", "for Custom Software")
|
||||
gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result)
|
||||
|
||||
def use_openpilot(self):
|
||||
if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH):
|
||||
os.remove(VALID_CACHE_PATH)
|
||||
with open(TMP_CONTINUE_PATH, "w") as f:
|
||||
f.write(CONTINUE)
|
||||
run_cmd(["chmod", "+x", TMP_CONTINUE_PATH])
|
||||
shutil.move(TMP_CONTINUE_PATH, CONTINUE_PATH)
|
||||
shutil.copyfile(INSTALLER_SOURCE_PATH, INSTALLER_DESTINATION_PATH)
|
||||
|
||||
# give time for installer UI to take over
|
||||
time.sleep(1)
|
||||
gui_app.request_close()
|
||||
else:
|
||||
self.state = SetupState.NETWORK_SETUP
|
||||
self.stop_network_check_thread.clear()
|
||||
self.start_network_check()
|
||||
|
||||
def download(self, url: str):
|
||||
# autocomplete incomplete URLs
|
||||
if re.match("^([^/.]+)/([^/]+)$", url):
|
||||
@@ -368,7 +316,7 @@ class Setup(Widget):
|
||||
try:
|
||||
import tempfile
|
||||
|
||||
fd, tmpfile = tempfile.mkstemp(prefix="installer_")
|
||||
_, tmpfile = tempfile.mkstemp(prefix="installer_")
|
||||
|
||||
headers = {"User-Agent": USER_AGENT, "X-openpilot-serial": HARDWARE.get_serial()}
|
||||
req = urllib.request.Request(self.download_url, headers=headers)
|
||||
@@ -398,16 +346,12 @@ class Setup(Widget):
|
||||
self.download_failed(self.download_url, "No custom software found at this URL.")
|
||||
return
|
||||
|
||||
# AGNOS might try to execute the installer before this process exits.
|
||||
# Therefore, important to close the fd before renaming the installer.
|
||||
os.close(fd)
|
||||
os.rename(tmpfile, INSTALLER_DESTINATION_PATH)
|
||||
os.rename(tmpfile, "/tmp/installer")
|
||||
os.chmod("/tmp/installer", 0o755)
|
||||
|
||||
with open(INSTALLER_URL_PATH, "w") as f:
|
||||
with open("/tmp/installer_url", "w") as f:
|
||||
f.write(self.download_url)
|
||||
|
||||
# give time for installer UI to take over
|
||||
time.sleep(5)
|
||||
gui_app.request_close()
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.widgets.list_view import ToggleAction, ListItem, ItemAction
|
||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.sunnypilot.lib.toggle import ToggleSP
|
||||
import openpilot.system.ui.sunnypilot.lib.styles as styles
|
||||
|
||||
style = styles.Default
|
||||
|
||||
|
||||
class ToggleActionSP(ToggleAction):
|
||||
def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True):
|
||||
ToggleAction.__init__(self, initial_state, width, enabled)
|
||||
self.toggle = ToggleSP(initial_state=initial_state)
|
||||
|
||||
class ListItemSP(ListItem):
|
||||
def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None,
|
||||
description_visible: bool = False, callback: Callable | None = None,
|
||||
action_item: ItemAction | None = None):
|
||||
ListItem.__init__(self, title, icon, description, description_visible, callback, action_item)
|
||||
|
||||
def get_item_height(self, font: rl.Font, max_width: int) -> float:
|
||||
if not self.is_visible:
|
||||
return 0
|
||||
|
||||
total_width = self._rect.width - (2 * style.ITEM_PADDING) # Full width minus padding
|
||||
max_width = int(total_width - (2 * style.ITEM_PADDING))
|
||||
|
||||
current_description = self.get_description()
|
||||
if self.description_visible and current_description:
|
||||
if (
|
||||
not self._wrapped_description
|
||||
or current_description != self._prev_description
|
||||
or max_width != self._prev_max_width
|
||||
):
|
||||
self._prev_max_width = max_width
|
||||
self._prev_description = current_description
|
||||
|
||||
wrapped_lines = wrap_text(self._font, current_description, style.ITEM_DESC_FONT_SIZE, max_width)
|
||||
self._wrapped_description = "\n".join(wrapped_lines)
|
||||
self._description_height = len(wrapped_lines) * style.ITEM_DESC_FONT_SIZE + 10
|
||||
return style.ITEM_BASE_HEIGHT + self._description_height - (style.ITEM_BASE_HEIGHT - style.ITEM_DESC_V_OFFSET) + style.ITEM_PADDING
|
||||
return style.ITEM_BASE_HEIGHT
|
||||
|
||||
def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle:
|
||||
if not self.action_item:
|
||||
return rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
right_width = self.action_item.rect.width
|
||||
if right_width == 0: # Full width action (like DualButtonAction)
|
||||
return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y,
|
||||
item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT)
|
||||
|
||||
action_width = self.action_item.rect.width
|
||||
if isinstance(self.action_item, ToggleAction):
|
||||
action_x = item_rect.x
|
||||
else:
|
||||
action_x = item_rect.x + item_rect.width - action_width
|
||||
action_y = item_rect.y
|
||||
return rl.Rectangle(action_x, action_y, action_width, style.ITEM_BASE_HEIGHT)
|
||||
|
||||
def _render(self, _):
|
||||
content_x = self._rect.x + style.ITEM_PADDING
|
||||
text_x = content_x
|
||||
left_action_item = isinstance(self.action_item, ToggleAction)
|
||||
|
||||
if left_action_item:
|
||||
left_rect = rl.Rectangle(
|
||||
content_x,
|
||||
self._rect.y + (style.ITEM_BASE_HEIGHT - style.TOGGLE_HEIGHT) // 2,
|
||||
style.TOGGLE_WIDTH,
|
||||
style.TOGGLE_HEIGHT
|
||||
)
|
||||
text_x = left_rect.x + left_rect.width + style.ITEM_PADDING
|
||||
|
||||
# Draw title
|
||||
if self.title:
|
||||
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
|
||||
# Render toggle and handle callback
|
||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||
if self.callback:
|
||||
self.callback()
|
||||
|
||||
else:
|
||||
if self.title:
|
||||
# Draw main text
|
||||
text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE)
|
||||
item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2
|
||||
rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR)
|
||||
|
||||
# Draw right item if present
|
||||
if self.action_item:
|
||||
right_rect = self.get_right_item_rect(self._rect)
|
||||
right_rect.y = self._rect.y
|
||||
if self.action_item.render(right_rect) and self.action_item.enabled:
|
||||
# Right item was clicked/activated
|
||||
if self.callback:
|
||||
self.callback()
|
||||
|
||||
# Draw description if visible
|
||||
current_description = self.get_description()
|
||||
if self.description_visible and current_description and self._wrapped_description:
|
||||
rl.draw_text_ex(
|
||||
self._font,
|
||||
self._wrapped_description,
|
||||
rl.Vector2(content_x, self._rect.y + style.ITEM_DESC_V_OFFSET),
|
||||
style.ITEM_DESC_FONT_SIZE,
|
||||
0,
|
||||
style.ITEM_DESC_TEXT_COLOR,
|
||||
)
|
||||
|
||||
def toggle_item_sp(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False,
|
||||
callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True) -> ListItem:
|
||||
action = ToggleActionSP(initial_state=initial_state, enabled=enabled)
|
||||
return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback)
|
||||
@@ -0,0 +1,34 @@
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Default:
|
||||
# Base Colors
|
||||
ON_BG_COLOR = rl.Color(28, 101, 186, 255) # Blue
|
||||
OFF_BG_COLOR = rl.Color(39, 39, 39, 255) # Grey
|
||||
DISABLED_ON_BG_COLOR = rl.Color(37, 70, 107, 255) # Dull Blue
|
||||
DISABLED_OFF_BG_COLOR = rl.Color(39, 39, 39, 255) # Grey
|
||||
ITEM_TEXT_COLOR = rl.WHITE
|
||||
ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255)
|
||||
|
||||
# Widget/Control Dimensions
|
||||
ITEM_BASE_HEIGHT = 170
|
||||
ITEM_PADDING = 30
|
||||
ITEM_TEXT_FONT_SIZE = 50
|
||||
ITEM_DESC_FONT_SIZE = 40
|
||||
ITEM_DESC_V_OFFSET = 150
|
||||
|
||||
TOGGLE_HEIGHT = 100
|
||||
TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75)
|
||||
TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20
|
||||
|
||||
BUTTON_WIDTH = 250
|
||||
BUTTON_HEIGHT = 100
|
||||
|
||||
# Toggle Colors
|
||||
TOGGLE_ON_COLOR = ON_BG_COLOR
|
||||
TOGGLE_OFF_COLOR = OFF_BG_COLOR
|
||||
TOGGLE_KNOB_COLOR = rl.WHITE
|
||||
TOGGLE_DISABLED_ON_COLOR = DISABLED_ON_BG_COLOR
|
||||
TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR
|
||||
TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey
|
||||
@@ -0,0 +1,89 @@
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.widgets.toggle import Toggle
|
||||
import openpilot.system.ui.sunnypilot.lib.styles as styles
|
||||
|
||||
style = styles.Default
|
||||
|
||||
class ToggleSP(Toggle):
|
||||
def __init__(self, initial_state=False):
|
||||
Toggle.__init__(self, initial_state)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.update()
|
||||
if self._enabled:
|
||||
bg_color = self._blend_color(style.TOGGLE_OFF_COLOR, style.TOGGLE_ON_COLOR, self._progress)
|
||||
knob_color = style.TOGGLE_KNOB_COLOR
|
||||
else:
|
||||
bg_color = self._blend_color(style.TOGGLE_DISABLED_OFF_COLOR, style.TOGGLE_DISABLED_ON_COLOR, self._progress)
|
||||
knob_color = style.TOGGLE_DISABLED_KNOB_COLOR
|
||||
|
||||
# Draw background
|
||||
bg_rect = rl.Rectangle(self._rect.x, self._rect.y, style.TOGGLE_WIDTH, style.TOGGLE_BG_HEIGHT)
|
||||
|
||||
# Draw outline first
|
||||
outline_color = style.TOGGLE_ON_COLOR
|
||||
if not self._enabled:
|
||||
# Use a more subtle color for disabled state
|
||||
outline_color = rl.Color(outline_color.r // 2, outline_color.g // 2, outline_color.b // 2, 255)
|
||||
|
||||
# Draw outline by drawing a slightly larger rounded rectangle behind the background
|
||||
outline_rect = rl.Rectangle(bg_rect.x - 2, bg_rect.y - 2, bg_rect.width + 4, bg_rect.height + 4)
|
||||
rl.draw_rectangle_rounded(outline_rect, 1.0, 10, outline_color)
|
||||
|
||||
# Draw actual background
|
||||
rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color)
|
||||
|
||||
# Draw knob to sit inside the background
|
||||
knob_padding = 5
|
||||
knob_radius = style.TOGGLE_BG_HEIGHT / 2 - knob_padding
|
||||
|
||||
left_edge = bg_rect.x + knob_padding
|
||||
right_edge = bg_rect.x + bg_rect.width - knob_padding
|
||||
|
||||
knob_travel_distance = right_edge - left_edge - 2 * knob_radius
|
||||
min_knob_x = left_edge + knob_radius
|
||||
knob_x = min_knob_x + knob_travel_distance * self._progress
|
||||
knob_y = self._rect.y + style.TOGGLE_BG_HEIGHT / 2
|
||||
|
||||
rl.draw_circle(int(knob_x), int(knob_y), knob_radius, knob_color)
|
||||
|
||||
symbol_size = knob_radius / 2
|
||||
|
||||
if self._state and (self._enabled or self._progress > 0.5):
|
||||
# Draw checkmark when toggle is ON
|
||||
start_x = knob_x - symbol_size * 0.8
|
||||
start_y = knob_y
|
||||
mid_x = knob_x - symbol_size * 0.1
|
||||
mid_y = knob_y + symbol_size * 0.6
|
||||
end_x = knob_x + symbol_size * 0.8
|
||||
end_y = knob_y - symbol_size * 0.5
|
||||
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(int(start_x), int(start_y)),
|
||||
rl.Vector2(int(mid_x), int(mid_y)),
|
||||
3,
|
||||
style.TOGGLE_ON_COLOR
|
||||
)
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(int(mid_x), int(mid_y)),
|
||||
rl.Vector2(int(end_x), int(end_y)),
|
||||
3,
|
||||
style.TOGGLE_ON_COLOR
|
||||
)
|
||||
else:
|
||||
# Draw X when toggle is OFF
|
||||
x_size_factor = 0.65
|
||||
x_offset = symbol_size * x_size_factor
|
||||
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(int(knob_x - x_offset), int(knob_y - x_offset)),
|
||||
rl.Vector2(int(knob_x + x_offset), int(knob_y + x_offset)),
|
||||
3,
|
||||
style.TOGGLE_OFF_COLOR
|
||||
)
|
||||
rl.draw_line_ex(
|
||||
rl.Vector2(int(knob_x + x_offset), int(knob_y - x_offset)),
|
||||
rl.Vector2(int(knob_x - x_offset), int(knob_y + x_offset)),
|
||||
3,
|
||||
style.TOGGLE_OFF_COLOR
|
||||
)
|
||||
@@ -1,17 +1,17 @@
|
||||
from enum import IntEnum
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from threading import Lock
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||
from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button, TextAlignment
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
from openpilot.system.ui.widgets.label import TextAlignment, gui_label
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
NM_DEVICE_STATE_NEED_AUTH = 60
|
||||
MIN_PASSWORD_LENGTH = 8
|
||||
@@ -27,20 +27,43 @@ STRENGTH_ICONS = [
|
||||
]
|
||||
|
||||
|
||||
class UIState(IntEnum):
|
||||
IDLE = 0
|
||||
CONNECTING = 1
|
||||
NEEDS_AUTH = 2
|
||||
SHOW_FORGET_CONFIRM = 3
|
||||
FORGETTING = 4
|
||||
@dataclass
|
||||
class StateIdle:
|
||||
action: Literal["idle"] = "idle"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateConnecting:
|
||||
network: NetworkInfo
|
||||
action: Literal["connecting"] = "connecting"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateNeedsAuth:
|
||||
network: NetworkInfo
|
||||
retry: bool
|
||||
action: Literal["needs_auth"] = "needs_auth"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateShowForgetConfirm:
|
||||
network: NetworkInfo
|
||||
action: Literal["show_forget_confirm"] = "show_forget_confirm"
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateForgetting:
|
||||
network: NetworkInfo
|
||||
action: Literal["forgetting"] = "forgetting"
|
||||
|
||||
|
||||
UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting
|
||||
|
||||
|
||||
class WifiManagerUI(Widget):
|
||||
def __init__(self, wifi_manager: WifiManagerWrapper):
|
||||
super().__init__()
|
||||
self.state: UIState = UIState.IDLE
|
||||
self._state_network: NetworkInfo | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING
|
||||
self._password_retry: bool = False # for NEEDS_AUTH
|
||||
self.state: UIState = StateIdle()
|
||||
self.btn_width: int = 200
|
||||
self.scroll_panel = GuiScrollPanel()
|
||||
self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True)
|
||||
@@ -70,16 +93,17 @@ class WifiManagerUI(Widget):
|
||||
gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
return
|
||||
|
||||
if self.state == UIState.NEEDS_AUTH and self._state_network:
|
||||
self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}")
|
||||
self.keyboard.reset()
|
||||
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(NetworkInfo, self._state_network), result))
|
||||
elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network:
|
||||
self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?')
|
||||
self._confirm_dialog.reset()
|
||||
gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result))
|
||||
else:
|
||||
self._draw_network_list(rect)
|
||||
match self.state:
|
||||
case StateNeedsAuth(network, retry):
|
||||
self.keyboard.set_title("Wrong password" if retry else "Enter password", f"for {network.ssid}")
|
||||
self.keyboard.reset()
|
||||
gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(network, result))
|
||||
case StateShowForgetConfirm(network):
|
||||
self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{network.ssid}"?')
|
||||
self._confirm_dialog.reset()
|
||||
gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(network, result))
|
||||
case _:
|
||||
self._draw_network_list(rect)
|
||||
|
||||
def _on_password_entered(self, network: NetworkInfo, result: int):
|
||||
if result == 1:
|
||||
@@ -89,13 +113,13 @@ class WifiManagerUI(Widget):
|
||||
if len(password) >= MIN_PASSWORD_LENGTH:
|
||||
self.connect_to_network(network, password)
|
||||
elif result == 0:
|
||||
self.state = UIState.IDLE
|
||||
self.state = StateIdle()
|
||||
|
||||
def on_forgot_confirm_finished(self, network, result: int):
|
||||
if result == 1:
|
||||
self.forget_network(network)
|
||||
elif result == 0:
|
||||
self.state = UIState.IDLE
|
||||
self.state = StateIdle()
|
||||
|
||||
def _draw_network_list(self, rect: rl.Rectangle):
|
||||
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT)
|
||||
@@ -123,18 +147,17 @@ class WifiManagerUI(Widget):
|
||||
security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE)
|
||||
|
||||
status_text = ""
|
||||
if self.state == UIState.CONNECTING and self._state_network:
|
||||
if self._state_network.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = "CONNECTING..."
|
||||
elif self.state == UIState.FORGETTING and self._state_network:
|
||||
if self._state_network.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = "FORGETTING..."
|
||||
elif network.security_type == SecurityType.UNSUPPORTED:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
else:
|
||||
self._networks_buttons[network.ssid].set_enabled(True)
|
||||
match self.state:
|
||||
case StateConnecting(network=connecting):
|
||||
if connecting.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = "CONNECTING..."
|
||||
case StateForgetting(network=forgetting):
|
||||
if forgetting.ssid == network.ssid:
|
||||
self._networks_buttons[network.ssid].set_enabled(False)
|
||||
status_text = "FORGETTING..."
|
||||
case _:
|
||||
self._networks_buttons[network.ssid].set_enabled(True)
|
||||
|
||||
self._networks_buttons[network.ssid].render(ssid_rect)
|
||||
|
||||
@@ -158,16 +181,13 @@ class WifiManagerUI(Widget):
|
||||
def _networks_buttons_callback(self, network):
|
||||
if self.scroll_panel.is_touch_valid():
|
||||
if not network.is_saved and network.security_type != SecurityType.OPEN:
|
||||
self.state = UIState.NEEDS_AUTH
|
||||
self._state_network = network
|
||||
self._password_retry = False
|
||||
self.state = StateNeedsAuth(network, False)
|
||||
elif not network.is_connected:
|
||||
self.connect_to_network(network)
|
||||
|
||||
def _forget_networks_buttons_callback(self, network):
|
||||
if self.scroll_panel.is_touch_valid():
|
||||
self.state = UIState.SHOW_FORGET_CONFIRM
|
||||
self._state_network = network
|
||||
self.state = StateShowForgetConfirm(network)
|
||||
|
||||
def _draw_status_icon(self, rect, network: NetworkInfo):
|
||||
"""Draw the status icon based on network's connection state"""
|
||||
@@ -192,16 +212,14 @@ class WifiManagerUI(Widget):
|
||||
rl.draw_texture_v(gui_app.texture(STRENGTH_ICONS[strength_level], ICON_SIZE, ICON_SIZE), rl.Vector2(rect.x, rect.y), rl.WHITE)
|
||||
|
||||
def connect_to_network(self, network: NetworkInfo, password=''):
|
||||
self.state = UIState.CONNECTING
|
||||
self._state_network = network
|
||||
self.state = StateConnecting(network)
|
||||
if network.is_saved and not password:
|
||||
self.wifi_manager.activate_connection(network.ssid)
|
||||
else:
|
||||
self.wifi_manager.connect_to_network(network.ssid, password)
|
||||
|
||||
def forget_network(self, network: NetworkInfo):
|
||||
self.state = UIState.FORGETTING
|
||||
self._state_network = network
|
||||
self.state = StateForgetting(network)
|
||||
network.is_saved = False
|
||||
self.wifi_manager.forget_connection(network.ssid)
|
||||
|
||||
@@ -218,24 +236,22 @@ class WifiManagerUI(Widget):
|
||||
with self._lock:
|
||||
network = next((n for n in self._networks if n.ssid == ssid), None)
|
||||
if network:
|
||||
self.state = UIState.NEEDS_AUTH
|
||||
self._state_network = network
|
||||
self._password_retry = True
|
||||
self.state = StateNeedsAuth(network, True)
|
||||
|
||||
def _on_activated(self):
|
||||
with self._lock:
|
||||
if self.state == UIState.CONNECTING:
|
||||
self.state = UIState.IDLE
|
||||
if isinstance(self.state, StateConnecting):
|
||||
self.state = StateIdle()
|
||||
|
||||
def _on_forgotten(self, ssid):
|
||||
with self._lock:
|
||||
if self.state == UIState.FORGETTING:
|
||||
self.state = UIState.IDLE
|
||||
if isinstance(self.state, StateForgetting):
|
||||
self.state = StateIdle()
|
||||
|
||||
def _on_connection_failed(self, ssid: str, error: str):
|
||||
with self._lock:
|
||||
if self.state == UIState.CONNECTING:
|
||||
self.state = UIState.IDLE
|
||||
if isinstance(self.state, StateConnecting):
|
||||
self.state = StateIdle()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -242,9 +242,6 @@ class Updater:
|
||||
b: str | None = self.params.get("UpdaterTargetBranch")
|
||||
if b is None:
|
||||
b = self.get_branch(BASEDIR)
|
||||
b = {
|
||||
("tici", "release3"): "release-tici"
|
||||
}.get((HARDWARE.get_device_type(), b), b)
|
||||
return b
|
||||
|
||||
@property
|
||||
@@ -286,8 +283,8 @@ class Updater:
|
||||
self.params.put("LastUpdateUptimeOnroad", last_uptime_onroad)
|
||||
self.params.put("LastUpdateRouteCount", last_route_count)
|
||||
else:
|
||||
last_uptime_onroad = self.params.get("LastUpdateUptimeOnroad", return_default=True)
|
||||
last_route_count = self.params.get("LastUpdateRouteCount", return_default=True)
|
||||
last_uptime_onroad = self.params.get("LastUpdateUptimeOnroad") or last_uptime_onroad
|
||||
last_route_count = self.params.get("LastUpdateRouteCount") or last_route_count
|
||||
|
||||
if exception is None:
|
||||
self.params.remove("LastUpdateException")
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ from openpilot.common.git import get_commit, get_origin, get_branch, get_short_b
|
||||
RELEASE_SP_BRANCHES = ['release-c3']
|
||||
TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new']
|
||||
MASTER_SP_BRANCHES = ['master']
|
||||
RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES
|
||||
RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] + RELEASE_SP_BRANCHES
|
||||
TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES
|
||||
|
||||
BUILD_METADATA_FILENAME = "build.json"
|
||||
|
||||
@@ -107,7 +107,7 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False):
|
||||
|
||||
|
||||
class CompressedVipc:
|
||||
def __init__(self, addr, vision_streams, server_name, nvidia=False, debug=False):
|
||||
def __init__(self, addr, vision_streams, nvidia=False, debug=False):
|
||||
print("getting frame sizes")
|
||||
os.environ["ZMQ"] = "1"
|
||||
messaging.reset_context()
|
||||
@@ -117,7 +117,7 @@ class CompressedVipc:
|
||||
os.environ.pop("ZMQ")
|
||||
messaging.reset_context()
|
||||
|
||||
self.vipc_server = VisionIpcServer(server_name)
|
||||
self.vipc_server = VisionIpcServer("camerad")
|
||||
for vst in vision_streams:
|
||||
ed = sm[ENCODE_SOCKETS[vst]]
|
||||
self.vipc_server.create_buffers(vst, 4, ed.width, ed.height)
|
||||
@@ -144,7 +144,6 @@ if __name__ == "__main__":
|
||||
parser.add_argument("addr", help="Address of comma three")
|
||||
parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg")
|
||||
parser.add_argument("--cams", default="0,1,2", help="Cameras to decode")
|
||||
parser.add_argument("--server", default="camerad", help="choose vipc server name")
|
||||
parser.add_argument("--silent", action="store_true", help="Suppress debug output")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -155,7 +154,7 @@ if __name__ == "__main__":
|
||||
]
|
||||
|
||||
vsts = [vision_streams[int(x)] for x in args.cams.split(",")]
|
||||
cvipc = CompressedVipc(args.addr, vsts, args.server, args.nvidia, debug=(not args.silent))
|
||||
cvipc = CompressedVipc(args.addr, vsts, args.nvidia, debug=(not args.silent))
|
||||
|
||||
# register exit handler
|
||||
signal.signal(signal.SIGINT, lambda sig, frame: cvipc.kill())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user