mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-11 04:43:43 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72deb91942 | |||
| d5b5652165 |
@@ -9,7 +9,6 @@
|
|||||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.otf filter=lfs diff=lfs merge=lfs -text
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text
|
|
||||||
|
|
||||||
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
||||||
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
* @sunnypilot/dev-internal
|
||||||
|
/.github/ @devtekve @sunnyhaibin
|
||||||
|
/release/ci/ @devtekve @sunnyhaibin
|
||||||
|
/tinygrad_repo @devtekve @Discountchubbs
|
||||||
|
/tinygrad/ @devtekve @Discountchubbs
|
||||||
|
/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs
|
||||||
|
/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs
|
||||||
|
/selfdrive/modeld/ @devtekve @Discountchubbs
|
||||||
|
/sunnypilot/model* @devtekve @Discountchubbs
|
||||||
|
/sunnypilot/sunnylink/ @devtekve
|
||||||
|
/system/athena/ @devtekve
|
||||||
@@ -78,7 +78,6 @@ jobs:
|
|||||||
- name: Get next recompiled dir number
|
- name: Get next recompiled dir number
|
||||||
id: create-recompiled-dir
|
id: create-recompiled-dir
|
||||||
env:
|
env:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
||||||
HF_REPO: ${{ github.event.inputs.hf_repo }}
|
HF_REPO: ${{ github.event.inputs.hf_repo }}
|
||||||
run: |
|
run: |
|
||||||
pip install huggingface_hub
|
pip install huggingface_hub
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
model_name: ${{ steps.resolve.outputs.model_name }}
|
model_name: ${{ steps.resolve.outputs.model_name }}
|
||||||
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
|
|
||||||
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
||||||
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
||||||
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
||||||
@@ -65,9 +64,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SAFE_NAME="${NAME// /-}"
|
|
||||||
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
||||||
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
|
|
||||||
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
||||||
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
||||||
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
||||||
@@ -138,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare output
|
- name: Prepare output
|
||||||
env:
|
env:
|
||||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||||
run: |
|
run: |
|
||||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||||
export PYTHONPATH=${{ github.workspace }}
|
export PYTHONPATH=${{ github.workspace }}
|
||||||
@@ -161,13 +158,13 @@ jobs:
|
|||||||
- name: Upload small model artifact
|
- name: Upload small model artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||||
path: ${{ github.workspace }}/small_output/
|
path: ${{ github.workspace }}/small_output/
|
||||||
|
|
||||||
- name: Upload artifact name file
|
- name: Upload artifact name file
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
||||||
|
|
||||||
- name: Re-enable powersave
|
- name: Re-enable powersave
|
||||||
@@ -257,7 +254,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare output
|
- name: Prepare output
|
||||||
env:
|
env:
|
||||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||||
run: |
|
run: |
|
||||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||||
export PYTHONPATH=${{ github.workspace }}
|
export PYTHONPATH=${{ github.workspace }}
|
||||||
@@ -280,13 +277,13 @@ jobs:
|
|||||||
- name: Upload big model artifact
|
- name: Upload big model artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||||
path: ${{ github.workspace }}/big_output/
|
path: ${{ github.workspace }}/big_output/
|
||||||
|
|
||||||
- name: Upload artifact name file
|
- name: Upload artifact name file
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
||||||
|
|
||||||
- name: Re-enable powersave
|
- name: Re-enable powersave
|
||||||
@@ -321,7 +318,7 @@ jobs:
|
|||||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: artifact_name
|
path: artifact_name
|
||||||
|
|
||||||
- name: Read artifact name
|
- name: Read artifact name
|
||||||
@@ -341,7 +338,7 @@ jobs:
|
|||||||
- name: Upload model to HF
|
- name: Upload model to HF
|
||||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||||
env:
|
env:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||||
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }}
|
||||||
run: |
|
run: |
|
||||||
rm -f output/artifact_name.txt
|
rm -f output/artifact_name.txt
|
||||||
@@ -367,7 +364,7 @@ jobs:
|
|||||||
- name: Generate DM metadata and upload to HF
|
- name: Generate DM metadata and upload to HF
|
||||||
if: ${{ inputs.target == 'dm' }}
|
if: ${{ inputs.target == 'dm' }}
|
||||||
env:
|
env:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }}
|
||||||
run: |
|
run: |
|
||||||
export PYTHONPATH=$(pwd)
|
export PYTHONPATH=$(pwd)
|
||||||
python3 -c "
|
python3 -c "
|
||||||
@@ -484,29 +481,11 @@ jobs:
|
|||||||
print(f'Chunked {pkl} into {len(targets)} chunks')
|
print(f'Chunked {pkl} into {len(targets)} chunks')
|
||||||
"
|
"
|
||||||
|
|
||||||
- name: Compile DM warp
|
|
||||||
run: |
|
|
||||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
|
||||||
export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}"
|
|
||||||
|
|
||||||
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
|
||||||
MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld"
|
|
||||||
DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
|
|
||||||
|
|
||||||
for res in $(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')"); do
|
|
||||||
WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl"
|
|
||||||
taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \
|
|
||||||
--camera-resolution ${res} \
|
|
||||||
--warp-to ${DM_SIZE} \
|
|
||||||
--output ${WARP_PKL}
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Prepare DM output
|
- name: Prepare DM output
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dm_output
|
mkdir -p dm_output
|
||||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
|
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/
|
||||||
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
|
cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/
|
||||||
cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/
|
|
||||||
|
|
||||||
- name: Upload DM artifact
|
- name: Upload DM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate hf_repo and JSON version
|
- name: Validate hf_repo and JSON version
|
||||||
env:
|
env:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f "$JSON_FILE" ]; then
|
if [ ! -f "$JSON_FILE" ]; then
|
||||||
echo "JSON file $JSON_FILE does not exist!"
|
echo "JSON file $JSON_FILE does not exist!"
|
||||||
@@ -155,8 +155,13 @@ jobs:
|
|||||||
python3 -c "
|
python3 -c "
|
||||||
import sys
|
import sys
|
||||||
from huggingface_hub import HfApi
|
from huggingface_hub import HfApi
|
||||||
HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
try:
|
||||||
|
api = HfApi()
|
||||||
|
api.repo_info(repo_id=sys.argv[1], repo_type='dataset')
|
||||||
print(f'Success: Repo {sys.argv[1]} exists.')
|
print(f'Success: Repo {sys.argv[1]} exists.')
|
||||||
|
except Exception as e:
|
||||||
|
print('HF validation failed:', e)
|
||||||
|
sys.exit(1)
|
||||||
" "${{ inputs.hf_repo }}"
|
" "${{ inputs.hf_repo }}"
|
||||||
|
|
||||||
- name: Download artifact name file
|
- name: Download artifact name file
|
||||||
@@ -187,7 +192,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to Hugging Face
|
- name: Upload to Hugging Face
|
||||||
env:
|
env:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }}
|
||||||
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }}
|
||||||
run: |
|
run: |
|
||||||
hf upload ${{ inputs.hf_repo }} \
|
hf upload ${{ inputs.hf_repo }} \
|
||||||
|
|||||||
@@ -46,13 +46,6 @@ runs:
|
|||||||
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST"
|
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST"
|
||||||
done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name')
|
done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name')
|
||||||
echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest"
|
echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest"
|
||||||
|
|
||||||
if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then
|
|
||||||
for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do
|
|
||||||
ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))")
|
|
||||||
printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
|
echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_model:
|
build_model:
|
||||||
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
|
runs-on: [self-hosted, chestnut]
|
||||||
needs: get_model
|
needs: get_model
|
||||||
env:
|
env:
|
||||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
|
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
|
||||||
echo "CHESTNUT build"
|
echo "CHESTNUT build"
|
||||||
export CHESTNUT=1
|
export CHESTNUT=1
|
||||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1"
|
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
|
||||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||||
else
|
else
|
||||||
echo "QCOM build"
|
echo "QCOM build"
|
||||||
|
|||||||
@@ -216,9 +216,6 @@ jobs:
|
|||||||
needs: [ prepare_strategy ]
|
needs: [ prepare_strategy ]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
|
if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }}
|
||||||
concurrency:
|
|
||||||
group: prepare-chestnut
|
|
||||||
cancel-in-progress: false
|
|
||||||
outputs:
|
outputs:
|
||||||
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
|
onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }}
|
||||||
env:
|
env:
|
||||||
@@ -231,10 +228,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
REF="${{ github.head_ref || github.ref_name }}"
|
REF="${{ github.head_ref || github.ref_name }}"
|
||||||
|
|
||||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||||
ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
|
||||||
echo "ONNX hash: $ONNX_HASH"
|
echo "ONNX hash: $ONNX_HASH"
|
||||||
[ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; }
|
|
||||||
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
|
echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||||
@@ -243,7 +238,7 @@ jobs:
|
|||||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||||
|
|
||||||
check_defaults() {
|
check_defaults() {
|
||||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||||
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||||
@@ -257,35 +252,18 @@ jobs:
|
|||||||
|
|
||||||
echo "No matching model on HF — dispatching build"
|
echo "No matching model on HF — dispatching build"
|
||||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
|
gh workflow run build-default-models.yaml --ref "$REF" -f target=big
|
||||||
sleep 10
|
|
||||||
|
|
||||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
echo "Polling HF for big model availability..."
|
||||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
|
||||||
|
|
||||||
echo "Waiting for build run to complete..."
|
|
||||||
for i in $(seq 1 90); do
|
for i in $(seq 1 90); do
|
||||||
sleep 30
|
sleep 30
|
||||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
|
||||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
|
||||||
echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION"
|
|
||||||
if [ "$STATUS" = "completed" ]; then
|
|
||||||
if [ "$CONCLUSION" = "success" ]; then
|
|
||||||
echo "Build run succeeded, verifying HF..."
|
|
||||||
sleep 10
|
|
||||||
if check_defaults; then
|
if check_defaults; then
|
||||||
echo "Big model verified on HF"
|
echo "Big model available on HF after $((i * 30))s"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "::error::Build succeeded but model not found on HF"
|
echo "Poll $i/90: not yet available"
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "::error::Build run did not complete within 45 minutes"
|
echo "::error::Big model not available on HF after 45 minutes"
|
||||||
exit 1
|
exit 1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -299,9 +277,6 @@ jobs:
|
|||||||
prepare_small_model:
|
prepare_small_model:
|
||||||
needs: [ prepare_strategy ]
|
needs: [ prepare_strategy ]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
concurrency:
|
|
||||||
group: prepare-small-model
|
|
||||||
cancel-in-progress: false
|
|
||||||
outputs:
|
outputs:
|
||||||
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
|
driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }}
|
||||||
env:
|
env:
|
||||||
@@ -314,10 +289,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
REF="${{ github.head_ref || github.ref_name }}"
|
REF="${{ github.head_ref || github.ref_name }}"
|
||||||
|
|
||||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha')
|
DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||||
DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
|
||||||
echo "Driving ONNX hash: $DRIVING_HASH"
|
echo "Driving ONNX hash: $DRIVING_HASH"
|
||||||
[ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; }
|
|
||||||
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
|
echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||||
@@ -326,7 +299,7 @@ jobs:
|
|||||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||||
|
|
||||||
check_defaults() {
|
check_defaults() {
|
||||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||||
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||||
@@ -340,35 +313,18 @@ jobs:
|
|||||||
|
|
||||||
echo "No matching model on HF — dispatching build"
|
echo "No matching model on HF — dispatching build"
|
||||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
|
gh workflow run build-default-models.yaml --ref "$REF" -f target=small
|
||||||
sleep 10
|
|
||||||
|
|
||||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
echo "Polling HF for model availability..."
|
||||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
|
||||||
|
|
||||||
echo "Waiting for build run to complete..."
|
|
||||||
for i in $(seq 1 60); do
|
for i in $(seq 1 60); do
|
||||||
sleep 30
|
sleep 30
|
||||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
|
||||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
|
||||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
|
||||||
if [ "$STATUS" = "completed" ]; then
|
|
||||||
if [ "$CONCLUSION" = "success" ]; then
|
|
||||||
echo "Build run succeeded, verifying HF..."
|
|
||||||
sleep 10
|
|
||||||
if check_defaults; then
|
if check_defaults; then
|
||||||
echo "Small model verified on HF"
|
echo "Model available on HF after $((i * 30))s"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "::error::Build succeeded but model not found on HF"
|
echo "Poll $i/60: not yet available"
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "::error::Small model build did not complete within 30 minutes"
|
echo "::error::Small driving model not available on HF after 30 minutes"
|
||||||
exit 1
|
exit 1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -382,9 +338,6 @@ jobs:
|
|||||||
prepare_dm_model:
|
prepare_dm_model:
|
||||||
needs: [ prepare_strategy ]
|
needs: [ prepare_strategy ]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
concurrency:
|
|
||||||
group: prepare-dm-model
|
|
||||||
cancel-in-progress: false
|
|
||||||
outputs:
|
outputs:
|
||||||
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
|
dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }}
|
||||||
env:
|
env:
|
||||||
@@ -397,10 +350,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
REF="${{ github.head_ref || github.ref_name }}"
|
REF="${{ github.head_ref || github.ref_name }}"
|
||||||
|
|
||||||
BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha')
|
DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
||||||
DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2)
|
|
||||||
echo "DM ONNX hash: $DM_HASH"
|
echo "DM ONNX hash: $DM_HASH"
|
||||||
[ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; }
|
|
||||||
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
|
echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha')
|
||||||
@@ -409,7 +360,7 @@ jobs:
|
|||||||
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json"
|
||||||
|
|
||||||
check_defaults() {
|
check_defaults() {
|
||||||
DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1
|
DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1
|
||||||
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null)
|
||||||
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
[ "$TINYGRAD_MATCH" = "true" ] || return 1
|
||||||
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null)
|
||||||
@@ -423,35 +374,18 @@ jobs:
|
|||||||
|
|
||||||
echo "No matching DM model on HF — dispatching build"
|
echo "No matching DM model on HF — dispatching build"
|
||||||
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
|
gh workflow run build-default-models.yaml --ref "$REF" -f target=dm
|
||||||
sleep 10
|
|
||||||
|
|
||||||
BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId')
|
echo "Polling HF for DM model availability..."
|
||||||
echo "Dispatched build run: $BUILD_RUN_ID"
|
|
||||||
|
|
||||||
echo "Waiting for build run to complete..."
|
|
||||||
for i in $(seq 1 60); do
|
for i in $(seq 1 60); do
|
||||||
sleep 30
|
sleep 30
|
||||||
STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status')
|
|
||||||
CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion')
|
|
||||||
echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION"
|
|
||||||
if [ "$STATUS" = "completed" ]; then
|
|
||||||
if [ "$CONCLUSION" = "success" ]; then
|
|
||||||
echo "Build run succeeded, verifying HF..."
|
|
||||||
sleep 10
|
|
||||||
if check_defaults; then
|
if check_defaults; then
|
||||||
echo "DM model verified on HF"
|
echo "DM model available on HF after $((i * 30))s"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "::error::Build succeeded but DM model not found on HF"
|
echo "Poll $i/60: not yet available"
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "::error::Build run failed with conclusion=$CONCLUSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "::error::DM model build did not complete within 30 minutes"
|
echo "::error::DM model not available on HF after 30 minutes"
|
||||||
exit 1
|
exit 1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# Ford C1 correction carryover experiment
|
|
||||||
|
|
||||||
The feedback controller at `5fbb583e5` can retain a correction from an earlier
|
|
||||||
turn that outweighs the new base C1. The measured curvature can already be
|
|
||||||
opposite the desired curvature, yet total C1 continues to request the old
|
|
||||||
direction while the integral works back toward zero.
|
|
||||||
|
|
||||||
This experiment keeps the existing 1:1 feedback strength and adds a conditional
|
|
||||||
reset of that correction. It is a command-policy experiment, not a demonstrated
|
|
||||||
improvement in physical steering response.
|
|
||||||
|
|
||||||
## Release rule
|
|
||||||
|
|
||||||
All of the following must be true on a valid, active cycle:
|
|
||||||
|
|
||||||
- Feedback is enabled and a fresh steering publication advances measurement time.
|
|
||||||
- Base C1 is nonzero by at least one DBC step (0.0005 rad).
|
|
||||||
- Both target C0 and the slewed C0 request agree with base C1's direction,
|
|
||||||
by at least one DBC step (0.01 m).
|
|
||||||
- Measured steering-derived curvature points opposite the desired curvature.
|
|
||||||
- The accumulated correction prevents total C1 from requesting the base direction:
|
|
||||||
the sum of base C1 and correction is zero or opposite base C1.
|
|
||||||
|
|
||||||
The stored correction is then set to zero before the usual feedback increment.
|
|
||||||
The final C1 command still passes through its existing ±0.5 rad amplitude and
|
|
||||||
0.5 rad/s slew limits. The reset cannot directly jump the transmitted command.
|
|
||||||
The DBC steps reject requests smaller than one representable step; they are
|
|
||||||
not new strength multipliers. This reset policy is itself an engineering choice.
|
|
||||||
|
|
||||||
There is no reset simply because steering error crosses zero, or because C1
|
|
||||||
and its correction have opposite signs. Matched curvature, neutral/conflicting
|
|
||||||
C0, a correction that does not outweigh base C1, and repeated measurements all
|
|
||||||
preserve normal integration. The condition can apply to small steering
|
|
||||||
corrections as well as large turns; it has no turn-size or speed threshold.
|
|
||||||
|
|
||||||
No previous-turn direction or timer is stored. Agreement between current path
|
|
||||||
requests and disagreement with measured curvature are the confirmation. This
|
|
||||||
does not establish which part of the combined C0/C1 request a PSCM physically
|
|
||||||
needs. In particular, when C0 still points into the previous turn, this rule
|
|
||||||
deliberately leaves the integral alone.
|
|
||||||
|
|
||||||
## Preserved behavior and diagnostics
|
|
||||||
|
|
||||||
C0's 7 m mapping, its limits, the base C1 mapping, upstream curvature limiting,
|
|
||||||
the original integral strength, driver/PSCM arbitration, C2=C3=0 and the 100 Hz
|
|
||||||
sender are unchanged. No fitted PSCM model, proportional term or gain schedule
|
|
||||||
is added. There are still three values used by the command law: C0, C1 and
|
|
||||||
the correction. A diagnostic-only `carryover_release_count` is added and resets
|
|
||||||
with the controller. It is included in the existing periodic diagnostic event.
|
|
||||||
|
|
||||||
The same default-off Sunnylink toggle selects this version. Its diagnostic
|
|
||||||
identity is `model-action-c1-feedback-v2`. See the [drive-test instructions](ford_model_action_drive_test.md).
|
|
||||||
|
|
||||||
## Offline evidence
|
|
||||||
|
|
||||||
The two mirrored command-regression tests failed before the change. After
|
|
||||||
building correction through actual feedback, the old controller still requested
|
|
||||||
the old C1 direction 0.4 s into a reversal. Both tests now pass with the original
|
|
||||||
output slew. Additional tests cover holding a steady curve, small error
|
|
||||||
crossings, neutral and conflicting C0, representable command boundaries,
|
|
||||||
freshness, driver override and PSCM limits. Integration tests execute the actual
|
|
||||||
controlsd selection and upstream limiter, Float32 publication and Ford CAN
|
|
||||||
builder, using both model and maneuver-plan requests and both turn directions.
|
|
||||||
|
|
||||||
The combined suite passes **567 tests and 9,146 subtests**, with the same 178
|
|
||||||
inherited/unsupported safety-test skips as the original feedback validation.
|
|
||||||
The randomized checks include mirrored inputs, zero-error compatibility, and
|
|
||||||
comparison against the exact previous controller from cloned pre-update states.
|
|
||||||
|
|
||||||
Frozen b8 replay triggers 11 releases; b9 triggers 14. Activation and C0 match
|
|
||||||
the previous controller exactly on every reconstructed cycle. In b9, most
|
|
||||||
releases concern small corrections; one follows the large turn around 13:28.
|
|
||||||
The C0/C1 disagreement at 14:36 is preserved. Numerical details and source
|
|
||||||
hashes are in `ford_c1_carryover_validation.json`.
|
|
||||||
|
|
||||||
At the release around 13:28, the candidate C1 crosses into the requested
|
|
||||||
direction 0.255 s earlier than the previous controller on identical frozen
|
|
||||||
inputs. This is a command zero-crossing comparison, not a measured improvement
|
|
||||||
in the truck's steering response. The lab checks total 669,343 Float32/CAN
|
|
||||||
round trips, in addition to the integration tests.
|
|
||||||
|
|
||||||
Replay preserves recorded model requests and measured motion. A difference
|
|
||||||
between candidate and baseline commands can persist because the recorded
|
|
||||||
steering does not respond to the changed command. Replay cannot predict wheel
|
|
||||||
angles, centering, oscillation, or how much earlier the vehicle would unwind.
|
|
||||||
No device build, boot, installation or physical validation was performed.
|
|
||||||
|
|
||||||
## Reproduction
|
|
||||||
|
|
||||||
Use the branch's native dependencies and pinned opendbc revision
|
|
||||||
`c21a9013700734dd20b09e05aa68329ad8cc20f9`. The route commands require the existing
|
|
||||||
full-rlog b8/b9 extracts and the baseline Git revision. Run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
export PYTHONPATH=.:opendbc_repo
|
|
||||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
|
||||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_carryover/stress.json
|
|
||||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_carryover/zero_error_stress.json
|
|
||||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb8
|
|
||||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb9 --baseline 5fbb583e592d30de266f8160a5d6b9c620c97f56 --output .cache/ford_c1_carryover/routeb9
|
|
||||||
```
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
{
|
|
||||||
"created_at_utc": "2026-09-10T14:00:37.853762+00:00",
|
|
||||||
"scope": "Conditional release of accumulated C1 correction; offline command behavior only, no predicted vehicle response.",
|
|
||||||
"baseline_commit": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
|
|
||||||
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
|
||||||
"deployment_target": {
|
|
||||||
"repository": "sunnypilot/sunnypilot",
|
|
||||||
"branch": "hiimisaac-dev"
|
|
||||||
},
|
|
||||||
"hypothesis": "model-action-c1-feedback-v2",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"toggle": {
|
|
||||||
"key": "FordModelActionController",
|
|
||||||
"default_enabled": false,
|
|
||||||
"activation": "Existing controlsd startup selection"
|
|
||||||
},
|
|
||||||
"release_rule": "Fresh enabled feedback; target and slewed C0 agree with base C1 by >= one DBC step; measured curvature is opposite; stored correction makes total C1 zero or opposite base. Clear correction, then apply original integration and output slew.",
|
|
||||||
"engineering_choices": "Conditional reset policy, using existing DBC steps (0.01 m, 0.0005 rad) to confirm nonzero commands. Original 1:1 integral strength is unchanged.",
|
|
||||||
"preserved": [
|
|
||||||
"C0 mapping and limits",
|
|
||||||
"Base C1 mapping",
|
|
||||||
"Original integral strength",
|
|
||||||
"Final C1 amplitude and slew limits",
|
|
||||||
"Driver and PSCM arbitration",
|
|
||||||
"Upstream selection and limiting",
|
|
||||||
"100 Hz sender",
|
|
||||||
"C2=C3=0"
|
|
||||||
],
|
|
||||||
"panda_safety_changed": false,
|
|
||||||
"opendbc_submodule_changed": false,
|
|
||||||
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
|
||||||
"controller_size": {
|
|
||||||
"total_lines": 194,
|
|
||||||
"code_lines_excluding_blanks_comments_docstrings": 131,
|
|
||||||
"core_command_state_values": 3,
|
|
||||||
"core_diagnostic_counters": 1
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"combined_suite": "567 passed, 178 skipped, 9146 subtests passed in 6.45s",
|
|
||||||
"safety_skips": "Same 178 inherited or unsupported variants recorded in ford_c1_feedback_validation.json.",
|
|
||||||
"regression": "Two mirrored carryover command tests fail on the exact baseline class and pass in the candidate suite.",
|
|
||||||
"ruff_changed_python": "pass",
|
|
||||||
"ty_controller": "pass",
|
|
||||||
"settings_compiler_check": "pass",
|
|
||||||
"carryover_controlsd_to_can_frames": 1120,
|
|
||||||
"existing_feedback_controlsd_to_can_frames": 1010,
|
|
||||||
"integration_scope": "Actual source selection, upstream limiting, controller, Float32 publication, Ford sender, both plan sources and signs, all counters and checksums."
|
|
||||||
},
|
|
||||||
"routes": {
|
|
||||||
"b8": {
|
|
||||||
"cycles": 160431,
|
|
||||||
"active_cycles": 68217,
|
|
||||||
"validity_and_c0_match_baseline_exactly": true,
|
|
||||||
"c1_changed_cycles": 6065,
|
|
||||||
"max_abs_c1_change_rad": 0.09250000000000003,
|
|
||||||
"can_round_trips": 160431,
|
|
||||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
|
||||||
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
|
|
||||||
"carryover_release_count": 11,
|
|
||||||
"input_sha256": {
|
|
||||||
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
|
||||||
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
|
||||||
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"b9": {
|
|
||||||
"cycles": 90774,
|
|
||||||
"active_cycles": 86474,
|
|
||||||
"validity_and_c0_match_baseline_exactly": true,
|
|
||||||
"c1_changed_cycles": 15208,
|
|
||||||
"max_abs_c1_change_rad": 0.10400000000000004,
|
|
||||||
"can_round_trips": 90774,
|
|
||||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
|
||||||
"reference_limit": "Uses exact consumed model publication as reference; b8 and b9 have no maneuver-plan messages.",
|
|
||||||
"carryover_release_count": 14,
|
|
||||||
"input_sha256": {
|
|
||||||
"route.npz": "b07c789d8155335f5d120d0262fced6e4d5803fe767b0ff49b6413dce4140b5c",
|
|
||||||
"model_paths.npz": "6b1f87897c050273fdc05af051307a049b6fc3a93072e7cda1721195ce7c3861",
|
|
||||||
"metadata.json": "9ce452220cab61b81883f32fc2fcaf5db6c78a674cb255a49cc77d5029580fee"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"command_timing_example": {
|
|
||||||
"event": {
|
|
||||||
"time_s": 808.286646083,
|
|
||||||
"correction_before_rad": -0.13089810321135922,
|
|
||||||
"correction_after_rad": 0.0,
|
|
||||||
"base_c1_rad": 0.06412824021622576,
|
|
||||||
"desired_angle_deg": -24.17155647277832,
|
|
||||||
"actual_angle_deg": -0.30000001192092896,
|
|
||||||
"speed_m_s": 11.804088592529297,
|
|
||||||
"baseline_c0_c1": [
|
|
||||||
0.15000000000000036,
|
|
||||||
-0.06600000000000006
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
0.15000000000000036,
|
|
||||||
-0.062000000000000055
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"scope": "Command zero crossing on identical frozen recorded inputs; not wheel response.",
|
|
||||||
"baseline_c1_rightward_at_s": 808.67241324,
|
|
||||||
"candidate_c1_rightward_at_s": 808.4169884780001,
|
|
||||||
"command_crossing_advance_s": 0.25542476199984776
|
|
||||||
},
|
|
||||||
"feedback_stress": {
|
|
||||||
"cycles": 200000,
|
|
||||||
"mirrored_updates": 200000,
|
|
||||||
"can_round_trips": 200000,
|
|
||||||
"carryover_release_count": 946,
|
|
||||||
"baseline_revision": "5fbb583e592d30de266f8160a5d6b9c620c97f56",
|
|
||||||
"baseline_source_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
|
||||||
"exact_unchanged_state_and_commands_without_release": 199054,
|
|
||||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, carryover direction/confirmation, integration, PSCM limits, CAN.",
|
|
||||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"controller_sha256": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552"
|
|
||||||
},
|
|
||||||
"zero_error_stress": {
|
|
||||||
"seed": 20260907,
|
|
||||||
"random_cycles": 200000,
|
|
||||||
"mirrored_core_updates": 200000,
|
|
||||||
"invalid_or_inactive_resets": 3537,
|
|
||||||
"field_boundary_cases": 18138,
|
|
||||||
"float32_can_round_trips": 218138,
|
|
||||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
|
||||||
"direct_raw_float32_packing_matches_host_output": true,
|
|
||||||
"max_continuous_step_c0_c1": [
|
|
||||||
0.40000000000000147,
|
|
||||||
0.05000000000000002
|
|
||||||
],
|
|
||||||
"calibration_approved": false,
|
|
||||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
|
||||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
|
||||||
},
|
|
||||||
"total_lab_float32_can_round_trips": 669343,
|
|
||||||
"source_sha256": {
|
|
||||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "6f40a05977253987a2c96e74c8c18d912367ed1e55630558ed7b28d52576e552",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "04935fb941a795cb243870a4c03f7073c68147b01da3cedf2872476aa5fb798e",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "e2e98d3a0a531235abd032fc4d3564796613ad51ff6ba22a230c48e36f6f6848",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "90fb42ce580f0085e349467086a2eef28c2512c671755d0771f6331d30b19035",
|
|
||||||
"tools/ford_pscm_lab/feedback_replay.py": "9bf145fbff6ed685aec2c0e5d0584e2dc7ff831021f15110f939e8a94c93280b",
|
|
||||||
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8",
|
|
||||||
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
|
|
||||||
"docs/ford_c1_carryover.md": "e8d08375963efc6d1ae6ce503bb580ba00cfa90adb71c941d56fe6e3701d5cbb",
|
|
||||||
"docs/ford_c1_feedback.md": "1b440a03082e5cec264a1d6693833ed0a7e07b6c6e2122f8e4455c5971121b57",
|
|
||||||
"docs/ford_model_action_drive_test.md": "7ac5ca0faf9690a23e7058d09b55f23e21e2ba74666001cacb56b67e8d8b4376",
|
|
||||||
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
|
|
||||||
"opendbc_repo/opendbc/car/ford/carcontroller.py": "b2d327a1833fb1f0d09ee17f54c9c8d45517fa29beb04a4543cfbf1b43f1a65e",
|
|
||||||
"opendbc_repo/opendbc/safety/modes/ford.h": "1d9d996292d6697ab4f02d55fae348d6aca1df94a07f7bdae48b68971b91afe7",
|
|
||||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401"
|
|
||||||
},
|
|
||||||
"artifact_sha256": {
|
|
||||||
".cache/ford_c1_carryover/tests.txt": "cd65146df93632e4a2c1e086781e1da4673db7d038c7e673124f227efefbb567",
|
|
||||||
".cache/ford_c1_carryover/baseline_regression.txt": "4469873f95ccf45a376a27fed05be3bdad5f808af7ceca472c6e2cb9d973eb7f",
|
|
||||||
".cache/ford_c1_carryover/stress.json": "7a8d20d7abd7cf444f55b7316e8bedbed0fbe8e9587e09f90d5b1946c3c2e98c",
|
|
||||||
".cache/ford_c1_carryover/zero_error_stress.json": "09e64eaac35df4ec324b41fabdc8baf91931106ac98b89c1ca71f4c8bf8796a4",
|
|
||||||
".cache/ford_c1_carryover/timing.json": "0d18ed4164803adedbbd660fe024f4c28de8eb7921caa60659346ff386d3847f",
|
|
||||||
".cache/ford_c1_carryover/routeb8/report.json": "d2c6f767cef29a74e292b6a16263d2da13b8c302e4653e419b0e232e1aaf762e",
|
|
||||||
".cache/ford_c1_carryover/routeb8/commands.npz": "89b5c3474940b61afce060111c27fd9bad9e24d703c59fca61adf4ce10473df3",
|
|
||||||
".cache/ford_c1_carryover/routeb9/report.json": "e3ff7bfa70e770eca763b125c283fd8a1d509ef1b6e7f26a81c398aca89a87da",
|
|
||||||
".cache/ford_c1_carryover/routeb9/commands.npz": "e4f5f341146e2897a479baf222d678fd16352c8da931876a2471c3719faf9edf"
|
|
||||||
},
|
|
||||||
"test_environment": {
|
|
||||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
|
||||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
|
||||||
"PYTHONDONTWRITEBYTECODE": "1",
|
|
||||||
"LOG_ROOT": "/private/tmp/ford-carryover-logs",
|
|
||||||
"PARAMS_ROOT": "/private/tmp/ford-carryover-params"
|
|
||||||
},
|
|
||||||
"limitations": [
|
|
||||||
"Frozen replay preserves recorded requests and measured motion; changed commands do not establish changed wheel angles, centering or stability.",
|
|
||||||
"C0/C1 agreement is a reset-policy choice, not an identified relationship between PSCM input and wheel angle.",
|
|
||||||
"The rule can release small corrections and does not promise unchanged centering during transients.",
|
|
||||||
"No device build, boot, installation or physical validation was performed."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# Ford C1 feedback experiment
|
|
||||||
|
|
||||||
This document records the original feedback change at `5fbb583e5`. The current
|
|
||||||
version retains its feedback law and adds [conditional carryover release](ford_c1_carryover.md).
|
|
||||||
The validation counts below describe the original change; current results are
|
|
||||||
recorded in `ford_c1_carryover_validation.json`.
|
|
||||||
|
|
||||||
The restored original v1 can leave a steering error while C0 and C1 still have
|
|
||||||
room. Its command law does not directly correct measured steering error. This
|
|
||||||
experiment keeps that mapping and adds one accumulated C1 correction:
|
|
||||||
|
|
||||||
```text
|
|
||||||
error = selected_limited_desired_curvature - measured_curvature
|
|
||||||
correction += error * speed * elapsed_measurement_time
|
|
||||||
C1_target = original_model_C1 + correction
|
|
||||||
```
|
|
||||||
|
|
||||||
Curvature (1/m) multiplied by traveled distance (m) gives heading mismatch in
|
|
||||||
radians. Applying that mismatch to C1 at **1:1 is an explicit feedback-strength
|
|
||||||
choice**. Dimensional consistency does not prove that every PSCM responds
|
|
||||||
correctly to that strength. There is no fitted PSCM response model or new
|
|
||||||
tunable multiplier.
|
|
||||||
|
|
||||||
For example, at 20 m/s, a constant curvature shortfall of 0.001/m adds 0.02 rad
|
|
||||||
to C1 over one second when the output can accept it. When measured curvature
|
|
||||||
matches the request, the correction holds. If the vehicle turns more than
|
|
||||||
requested, the correction moves in the unwind direction. Changing the model
|
|
||||||
request still changes the base immediately, subject to the existing slew.
|
|
||||||
|
|
||||||
## Preserved mapping and limits
|
|
||||||
|
|
||||||
- C0 is the current model path's lateral offset at 7 m of arc distance, holding
|
|
||||||
the available endpoint for shorter paths; its limits remain ±5.11 m and 4 m/s.
|
|
||||||
- Base C1 is `max(7 m, speed × 1 s) × selected_limited_desired_curvature`, clipped
|
|
||||||
to ±0.5 rad. Final C1 uses the same ±0.5 rad and 0.5 rad/s limits as v1.
|
|
||||||
- C2 and C3 are zero. Sign conversion, Float32/CAN rounding, upstream curvature
|
|
||||||
limiting and the 100 Hz sender retain their existing behavior.
|
|
||||||
|
|
||||||
The core holds three values: unquantized C0, unquantized C1 and the correction.
|
|
||||||
Zero error from a reset leaves the correction at zero and preserves the old
|
|
||||||
command arithmetic exactly. There is no separate percentage or distance cap
|
|
||||||
on the correction.
|
|
||||||
|
|
||||||
## Feedback measurement, timing and limits
|
|
||||||
|
|
||||||
The measurement is `controlsd.curvature`, computed from measured steering
|
|
||||||
angle with the existing live vehicle parameters. It matches the curvature
|
|
||||||
used for the desired-versus-actual steering comparison. It is not an independent
|
|
||||||
measurement of tire slip or the vehicle's actual ground path. CAN yaw remains
|
|
||||||
an input-health gate and does not drive this feedback.
|
|
||||||
|
|
||||||
The adapter integrates only elapsed time between fresh `carState` publications.
|
|
||||||
The first publication after reset integrates zero time. Duplicate timestamps
|
|
||||||
integrate zero; a fresh timestamp accounts for the elapsed measurement interval.
|
|
||||||
Output slew continues on valid control cycles. Existing service-age, speed,
|
|
||||||
model-geometry and clock-order gates remain, with the same finite/range check
|
|
||||||
also applied to measured curvature. Disengagement or invalid input clears all
|
|
||||||
three core states.
|
|
||||||
|
|
||||||
The correction cannot accumulate farther into an unavailable C1 amplitude or
|
|
||||||
slew request. Increments that move back toward the available output remain
|
|
||||||
allowed. Moving the base request does not itself rewrite the correction.
|
|
||||||
|
|
||||||
Fresh PSCM status means a valid message whose original CAN receipt timestamp
|
|
||||||
is within the existing −5 to +150 ms age allowance. Reached-limit status (2)
|
|
||||||
prevents extra accumulation in the measured turn direction. An old correction
|
|
||||||
opposing that direction can return to zero; it cannot be trapped below the
|
|
||||||
base request by the limit flag. Unwind and base model changes remain available.
|
|
||||||
Close-to-limit status (1) does not block feedback. Missing or stale status
|
|
||||||
does not gate it; local amplitude and slew anti-windup still apply.
|
|
||||||
|
|
||||||
Driver steering-pressed, torque above the existing 1 Nm allowance, nonfinite
|
|
||||||
torque, or fresh driver-limit status (3) clears the correction. Fresh denied
|
|
||||||
or inactive PSCM status also clears it. The base model request continues
|
|
||||||
through existing engagement and driver arbitration; clearing the correction
|
|
||||||
does not bypass the final output slew.
|
|
||||||
|
|
||||||
## Offline evidence and reproduction
|
|
||||||
|
|
||||||
`ford_c1_feedback_validation.json` records the source hashes and completed
|
|
||||||
checks. Tests exercise build, hold, unwind, saturation, limit flags, immediate
|
|
||||||
driver input, stale and repeated measurements, invalid inputs and both signs.
|
|
||||||
Integration tests execute actual controlsd selection and limiting, Float32
|
|
||||||
publication, CarControlSP conversion and the Ford CarController CAN builder.
|
|
||||||
Randomized runs check feedback invariants separately from zero-error
|
|
||||||
compatibility with the original independent scalar oracle.
|
|
||||||
|
|
||||||
The combined suite passes **511 tests and 9,146 subtests**. Its 178 skips are
|
|
||||||
in inherited safety base classes or unsupported safety-test variants. Ruff,
|
|
||||||
the controller's Ty check and settings compilation pass. Feedback stress,
|
|
||||||
zero-error stress and the b8 replay total **578,569 Float32/CAN round trips**;
|
|
||||||
the integration test separately verifies 1,010 transmitted packet constructions,
|
|
||||||
including every counter and checksum. No packets are sent to hardware.
|
|
||||||
|
|
||||||
The b8 replay retains recorded desired/measured curvature, model publications,
|
|
||||||
driver input and PSCM flags. It compares candidate commands with the restored
|
|
||||||
v1 at `a7d70e2b0890184636827351e4789d866f2a7c97`. All 160,431 reconstructed
|
|
||||||
activation decisions and C0 commands match. C1 changes on 58,106 cycles.
|
|
||||||
At 4:12.493, for example, reconstructed host C1 changes from −0.1625 to
|
|
||||||
−0.2035 rad; at 3:56.250 it changes from −0.1280 to −0.1080 rad. These are
|
|
||||||
changes to commands on frozen measurements, not predicted wheel angles.
|
|
||||||
|
|
||||||
Controls publication time proxies the unlogged computation clock, and the
|
|
||||||
full SubMaster health state cannot be reconstructed. This route uses the
|
|
||||||
consumed model publication as its reference and has no maneuver-plan messages.
|
|
||||||
Replay cannot show whether this feedback fixes weak turns, hanging turns or
|
|
||||||
oscillation. A new drive is needed to measure those outcomes.
|
|
||||||
|
|
||||||
Use the branch's native dependencies and pinned opendbc revision
|
|
||||||
`c21a9013700734dd20b09e05aa68329ad8cc20f9`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
export PYTHONPATH=.:opendbc_repo
|
|
||||||
python -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py openpilot/sunnypilot/sunnylink/tests openpilot/common/tests/test_params.py opendbc_repo/opendbc/car/ford/tests/test_ford.py opendbc_repo/opendbc/safety/tests/test_ford.py
|
|
||||||
python openpilot/sunnypilot/sunnylink/tools/compile_settings_ui.py --check
|
|
||||||
python -m tools.ford_pscm_lab.feedback_replay stress --cycles 200000 --output .cache/ford_c1_feedback/feedback_stress.json
|
|
||||||
python -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --opendbc-revision c21a9013700734dd20b09e05aa68329ad8cc20f9 --output .cache/ford_c1_feedback/zero_error_stress.json
|
|
||||||
python -m tools.ford_pscm_lab.feedback_replay route .cache/ford_routeb8 --output .cache/ford_c1_feedback/routeb8
|
|
||||||
```
|
|
||||||
|
|
||||||
The last command requires the existing full-rlog b8 extract (`route.npz`,
|
|
||||||
`model_paths.npz`, `metadata.json`), identified by hashes in the validation
|
|
||||||
record. The historical route90/95 replay deliberately sets measured curvature
|
|
||||||
equal to requested curvature to check zero-error compatibility; it does not
|
|
||||||
exercise recorded steering feedback.
|
|
||||||
|
|
||||||
Enable using the [existing Sunnylink toggle](ford_model_action_drive_test.md).
|
|
||||||
The diagnostic identity is `model-action-c1-feedback-v1`.
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
{
|
|
||||||
"created_at_utc": "2026-09-09T14:45:33.853345+00:00",
|
|
||||||
"baseline_commit": "a7d70e2b0890184636827351e4789d866f2a7c97",
|
|
||||||
"deployment_target": {
|
|
||||||
"repository": "sunnypilot/sunnypilot",
|
|
||||||
"branch": "hiimisaac-dev"
|
|
||||||
},
|
|
||||||
"scope": "C1 measured-curvature feedback on restored original v1. Offline software validation only; no predicted or measured physical improvement.",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"toggle": {
|
|
||||||
"key": "FordModelActionController",
|
|
||||||
"default_enabled": false,
|
|
||||||
"activation": "Existing startup selection after offroad-to-onroad cycle"
|
|
||||||
},
|
|
||||||
"feedback_law": "correction += (desired_curvature - measured_curvature) * speed * elapsed_measurement_time, subject to output and PSCM anti-windup",
|
|
||||||
"feedback_strength": "Explicit 1:1 heading-error-to-C1 choice; no fitted PSCM plant or new tunable multiplier",
|
|
||||||
"preserved": [
|
|
||||||
"C0 mapping and limits",
|
|
||||||
"C2=C3=0",
|
|
||||||
"C1 final amplitude and slew limits",
|
|
||||||
"100 Hz sender",
|
|
||||||
"upstream selection and limiting"
|
|
||||||
],
|
|
||||||
"panda_safety_changed": false,
|
|
||||||
"opendbc_submodule_changed": false,
|
|
||||||
"opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
|
||||||
"controller_size": {
|
|
||||||
"total_lines": 181,
|
|
||||||
"code_lines_excluding_blanks_comments_docstrings": 123,
|
|
||||||
"core_persistent_values": 3,
|
|
||||||
"adapter_timestamps": 3
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"combined_suite": "511 passed, 178 skipped, 9146 subtests passed in 5.14s",
|
|
||||||
"suite_log_sha256": "001ef6633b22513317593dd8debc160a0ca8aaf78ea53418c5f7a50c370cc818",
|
|
||||||
"ruff_changed_python": "pass",
|
|
||||||
"ty_controller": "pass",
|
|
||||||
"settings_compiler_check": "pass",
|
|
||||||
"safety_skip_reasons": [
|
|
||||||
"SKIPPED [145] ../../../../dev/sunnypilot/.venv/lib/python3.12/site-packages/_pytest/unittest.py:523: Skipped",
|
|
||||||
"SKIPPED [9] opendbc_repo/opendbc/safety/tests/common.py:64: Safety mode implements no _user_regen_msg",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:51: Skipping test because MADS button is not supported",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:254: Skipping test because MADS button is not supported",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:67: Skipping test because _acc_state_msg is not implemented for this car",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because MADS button is not supported",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:165: Skipping test because ACC main is not supported",
|
|
||||||
"SKIPPED [3] opendbc_repo/opendbc/safety/tests/mads_common.py:411: MADS button not supported",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:378: CAN FD only",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:361: CAN FD only",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:351: CAN FD only",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:327: CAN FD only",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:341: CAN FD only",
|
|
||||||
"SKIPPED [1] opendbc_repo/opendbc/safety/tests/test_ford.py:320: CAN FD only"
|
|
||||||
],
|
|
||||||
"safety_native_build": "Pinned safety C source is compiled locally by libsafety_py before testing.",
|
|
||||||
"controlsd_to_can_feedback_integration_frames": 1010,
|
|
||||||
"integration_checks": "Both signs: build, hold, unwind, rebuild, immediate driver override; actual 100 Hz sender, counter, checksum, fields and publication. Separate integration tests validate PSCM service forwarding.",
|
|
||||||
"regression_test_evidence": [
|
|
||||||
"Nonzero-error integration failed with zero correction before implementing feedback.",
|
|
||||||
"Both sign tests failed when a reached limit trapped an old opposing correction; they pass after allowing return to zero."
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"route_b8": {
|
|
||||||
"baseline_revision": "a7d70e2b0890184636827351e4789d866f2a7c97",
|
|
||||||
"baseline_source_sha256": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
|
|
||||||
"cycles": 160431,
|
|
||||||
"active_cycles": 68217,
|
|
||||||
"validity_and_c0_match_original_v1_exactly": true,
|
|
||||||
"status_counts": {
|
|
||||||
"inactive": 92214,
|
|
||||||
"active": 68217
|
|
||||||
},
|
|
||||||
"feedback_enabled_seconds": 598.256888772994,
|
|
||||||
"pscm_limit_2_seconds": 12.139079590997426,
|
|
||||||
"c1_changed_cycles": 58106,
|
|
||||||
"max_abs_c1_change_rad": 0.29800000000000004,
|
|
||||||
"max_abs_correction_rad": 0.29816844327770786,
|
|
||||||
"can_round_trips": 160431,
|
|
||||||
"timing_limit": "Controls publication time proxies the computation clock; full SubMaster checks are unavailable.",
|
|
||||||
"reference_limit": "Uses exact consumed model publication as reference; the b8 route has no maneuver-plan messages.",
|
|
||||||
"example_points": [
|
|
||||||
{
|
|
||||||
"time_s": 130.9368894940053,
|
|
||||||
"old_c0_c1": [
|
|
||||||
-0.7400000000000002,
|
|
||||||
-0.18700000000000006
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
-0.7400000000000002,
|
|
||||||
-0.22899999999999998
|
|
||||||
],
|
|
||||||
"correction_rad": -0.042171663052515254,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 235.3960996990063,
|
|
||||||
"old_c0_c1": [
|
|
||||||
-2.04,
|
|
||||||
-0.40449999999999997
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
-2.04,
|
|
||||||
-0.4145
|
|
||||||
],
|
|
||||||
"correction_rad": -0.00989648519895422,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 236.25034470800165,
|
|
||||||
"old_c0_c1": [
|
|
||||||
-1.46,
|
|
||||||
-0.128
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
-1.46,
|
|
||||||
-0.10799999999999998
|
|
||||||
],
|
|
||||||
"correction_rad": 0.01990758350705991,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 252.49320156300382,
|
|
||||||
"old_c0_c1": [
|
|
||||||
-0.6699999999999999,
|
|
||||||
-0.16249999999999998
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
-0.6699999999999999,
|
|
||||||
-0.20350000000000001
|
|
||||||
],
|
|
||||||
"correction_rad": -0.040907632902654506,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 674.430371745002,
|
|
||||||
"old_c0_c1": [
|
|
||||||
0.4299999999999997,
|
|
||||||
0.128
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
0.4299999999999997,
|
|
||||||
0.1345
|
|
||||||
],
|
|
||||||
"correction_rad": 0.00639271291315417,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 1534.5190040400048,
|
|
||||||
"old_c0_c1": [
|
|
||||||
2.62,
|
|
||||||
0.5
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
2.62,
|
|
||||||
0.5
|
|
||||||
],
|
|
||||||
"correction_rad": 0.0,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"time_s": 1562.5074677500015,
|
|
||||||
"old_c0_c1": [
|
|
||||||
-0.1200000000000001,
|
|
||||||
-0.051000000000000045
|
|
||||||
],
|
|
||||||
"candidate_c0_c1": [
|
|
||||||
-0.1200000000000001,
|
|
||||||
-0.046499999999999986
|
|
||||||
],
|
|
||||||
"correction_rad": 0.004453988923883501,
|
|
||||||
"feedback_enabled": true,
|
|
||||||
"pscm_limited": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"route_input_sha256": {
|
|
||||||
"route.npz": "6f5dd369b70eaed4b95b28c8b25c9f2e9b830fa07a334881a185505481667c8b",
|
|
||||||
"model_paths.npz": "939af6cf7e74251d8842581cc078d26d9fbfd22a0d7817cb0e368697d419b615",
|
|
||||||
"metadata.json": "73b439132d1de37ec187b544c04d2b05c80965065515a4b7dec29ba57ae37e7c"
|
|
||||||
},
|
|
||||||
"feedback_stress": {
|
|
||||||
"cycles": 200000,
|
|
||||||
"mirrored_updates": 200000,
|
|
||||||
"can_round_trips": 200000,
|
|
||||||
"checks": "Mirror symmetry, reset/override, amplitude, slew, correction bounds, integration direction/size, PSCM anti-windup, CAN fields.",
|
|
||||||
"scope": "Numerical software invariants only; no model of vehicle motion.",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"controller_sha256": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34"
|
|
||||||
},
|
|
||||||
"zero_error_stress": {
|
|
||||||
"seed": 20260907,
|
|
||||||
"random_cycles": 200000,
|
|
||||||
"mirrored_core_updates": 200000,
|
|
||||||
"invalid_or_inactive_resets": 3537,
|
|
||||||
"field_boundary_cases": 18138,
|
|
||||||
"float32_can_round_trips": 218138,
|
|
||||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
|
||||||
"direct_raw_float32_packing_matches_host_output": true,
|
|
||||||
"max_continuous_step_c0_c1": [
|
|
||||||
0.40000000000000147,
|
|
||||||
0.05000000000000002
|
|
||||||
],
|
|
||||||
"calibration_approved": false,
|
|
||||||
"scope": "Zero-error numerical construction: measured equals requested curvature. No PSCM response claims.",
|
|
||||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
|
||||||
},
|
|
||||||
"total_lab_float32_can_round_trips": 578569,
|
|
||||||
"artifact_sha256": {
|
|
||||||
".cache/ford_c1_feedback/routeb8/report.json": "648887eebb76260a60f7c0f0d9aaac83d0f1c06b443e28bc6fdf296bad4526c0",
|
|
||||||
".cache/ford_c1_feedback/routeb8/commands.npz": "1aef98365572b3fb3cb8ba2a93cf30041ff3be133718d469145b710f2c94dc32",
|
|
||||||
".cache/ford_c1_feedback/feedback_stress.json": "abf4e7bccc1e460008cc7450fcd92e9b2a6108bd71e53a01bdc24e31e5b5ad32",
|
|
||||||
".cache/ford_c1_feedback/zero_error_stress.json": "2a3f284e10e5054205a788cce59bcf57bd837e13afff327457244141ba5522f0",
|
|
||||||
".cache/ford_c1_feedback/safety_skip_reasons.txt": "5384c82b07b7cc20c6b22b8e94246cb104d53f8866af02b28fda7d4138cf377f"
|
|
||||||
},
|
|
||||||
"native_params": {
|
|
||||||
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
|
|
||||||
"sources_match_original_rebuild_record": true,
|
|
||||||
"provenance": "Same locally rebuilt native library and source hashes recorded in ford_model_action_drive_test_validation.json; verified for this run."
|
|
||||||
},
|
|
||||||
"test_environment": {
|
|
||||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
|
||||||
"PYTHONPATH": ".:opendbc_repo:.cache/ford_v6/test_deps",
|
|
||||||
"LOG_ROOT": "/private/tmp/ford-feedback-logs",
|
|
||||||
"PARAMS_ROOT": "/private/tmp/ford-feedback-params",
|
|
||||||
"PYTHONDONTWRITEBYTECODE": "1"
|
|
||||||
},
|
|
||||||
"source_sha256": {
|
|
||||||
"docs/ford_c1_feedback.md": "c1bc7f24c5ebe28679b4a04d09085d7b937926e63a38d43a3abfac93dfcfa0f9",
|
|
||||||
"docs/ford_model_action_candidate.md": "20cd8d10008cd796cc8719f5795ee80f50d8133d6e7684fb057a78fb05323fbe",
|
|
||||||
"docs/ford_model_action_drive_test.md": "7860ae26a61682aff86743ba302eb23c8f271d5700a2e616da1b6b38d438b57d",
|
|
||||||
"opendbc_repo/opendbc/car/vehicle_model.py": "ddc2a93d9c2b2ef6c9a913a5aef4c51e2bc387db1f7640473657e5ade4e50fac",
|
|
||||||
"openpilot/selfdrive/controls/controlsd.py": "2b7e246f00bccce3a2bb9f6f44009ca77690cadb8527cd2bdfe855e9ad72ad1e",
|
|
||||||
"openpilot/selfdrive/controls/lib/drive_helpers.py": "916bcd83c2a909a89795da58c7c43d7b168c9b82e1a6d281484bae45c667c01e",
|
|
||||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "4499defbb7fc5ddf5029ca42c549f0935b0758b08818c5bf0490fb52221f9a34",
|
|
||||||
"openpilot/selfdrive/controls/lib/ford_path.py": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "84113b1b7800c868117af0034278f53a1a6153c7bb5fadc1ea45958e62c4f0d0",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "cbe1b2aa1961deba3a42e1d82f5f75ae0c3d7a219428dea5f50cb70e1b27fd11",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "c7f5ffd650e804e6e02fa12d435e0867b56b13a49c3d9fa511993188d5cb625a",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_feedback.py": "f7a956c082a246d9506e21adbf348cbdc7f94d5342d832841058c71f7e264eeb",
|
|
||||||
"openpilot/sunnypilot/selfdrive/controls/controlsd_ext.py": "7a13dc5ce49b40e27e05e62cdb9ef1bb764de8ed8167f7e982d54a4dffe97ed4",
|
|
||||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "7d38f315a7c5ce6d46d01a06f7eaddd4933f85639e5325ff71fdce22866ef401",
|
|
||||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "410e306958ece12e49fc114707741c57a2dd927c6ba3410e160834e52a759ea9",
|
|
||||||
"tools/ford_pscm_lab/feedback_replay.py": "ca552217953f3cce35da0b1666252fd43b6f8ab6c067e5c9102da8ea8d97c2f3",
|
|
||||||
"tools/ford_pscm_lab/model_action_replay.py": "af97c665f342c66b1be2502e188c63e6f3ee106d0a0d5e80997bc3040373ff9f",
|
|
||||||
"tools/ford_pscm_lab/stress_model_action.py": "0b25188edf2b248ebe741173ce02ce75bd59f1f39fd5bd909d41a3dca2294aa8"
|
|
||||||
},
|
|
||||||
"limitations": [
|
|
||||||
"Frozen route replay changes commands only; it cannot establish tracking, unwind response or closed-loop stability.",
|
|
||||||
"Measured curvature uses the existing steering-angle vehicle model; it is not an independent ground-path measurement.",
|
|
||||||
"No full device build, device boot, installation or road validation was performed."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
# Offline Ford selected-action candidate
|
|
||||||
|
|
||||||
This document and `ford_model_action_validation.json` record the offline
|
|
||||||
stage committed as `7ca3c6e3b`. The candidate is now available behind a
|
|
||||||
separate default-off Sunnylink toggle; see
|
|
||||||
[drive-test setup and validation](ford_model_action_drive_test.md).
|
|
||||||
The counts, source hashes and selector status below describe that earlier
|
|
||||||
stage. The current experiment adds [measured-curvature C1 feedback](ford_c1_feedback.md)
|
|
||||||
to this original mapping; the historical no-feedback description below is
|
|
||||||
not the current controller specification.
|
|
||||||
|
|
||||||
The decision is `C0 = current model y(7 m)`,
|
|
||||||
`C1 = max(7 m, speed × 1 s) × selected upstream-limited desiredCurvature`,
|
|
||||||
with C2=C3=0. The 7 m station and one-second scale are engineering choices,
|
|
||||||
not identified PSCM gains. `calibration_approved=false`.
|
|
||||||
|
|
||||||
`openpilot/selfdrive/controls/lib/ford_model_action.py` contains the core
|
|
||||||
and a separate adapter compatible with the existing controlsd call.
|
|
||||||
At that stage, the production selector, v8 implementation, settings, opendbc
|
|
||||||
submodule and Panda safety remained unchanged. Tests injected the adapter
|
|
||||||
offline; there was no production setting. No hardware or CAN transmission
|
|
||||||
occurs in the lab tools.
|
|
||||||
|
|
||||||
## Construction and integration
|
|
||||||
|
|
||||||
Only the unquantized C0 and C1 slew positions persist in the core.
|
|
||||||
Each field is clipped independently (±5.11 m / ±0.5 rad), slewed independently
|
|
||||||
(4 m/s / 0.5 rad/s), then packed using the existing Float32/sign-negation
|
|
||||||
rounding contract (0.01 m / 0.0005 rad). Heading overflow is not transferred
|
|
||||||
to C0. No yaw integral, blend, additional curvature contribution, reference
|
|
||||||
filter, turn modes, or 10 m C1 cap is introduced.
|
|
||||||
|
|
||||||
The selected standalone implementation from worktree 3548 is the provenance
|
|
||||||
for this law. Its two-state packer has been moved into the library core so
|
|
||||||
the controller does not depend on experimental lab code. Invalid numeric
|
|
||||||
types, overflowing arc geometry and malformed paths reset the core instead
|
|
||||||
of throwing or retaining a command.
|
|
||||||
|
|
||||||
Arc stations use cumulative model x/y distance, not forward x. As in the
|
|
||||||
reviewed standalone core, a path ending before 7 m holds its available
|
|
||||||
endpoint instead of extrapolating. This matters: route95 contains 44 active
|
|
||||||
cycles with 5.45–6.94 m of path at 2.78–3.46 m/s. A tested strict 7 m
|
|
||||||
coverage gate would have introduced disengagements and was removed. There
|
|
||||||
is no speed-dependent C0 horizon beyond this existing endpoint behavior.
|
|
||||||
|
|
||||||
The adapter retains the existing input age allowance (−5 to +150 ms),
|
|
||||||
speed domain (0.3–55 m/s), yaw sanity bound (±3 rad/s), selected curvature
|
|
||||||
sanity bound (±1/m), and control interval (2–100 ms). It rejects backward
|
|
||||||
model/measurement timestamps and invalid services. Repeated timestamps may
|
|
||||||
continue slew, but geometry is validated again on each tick. Disengagement,
|
|
||||||
invalid inputs and timing faults clear all command and adapter timing state.
|
|
||||||
The first valid tick after reset uses 10 ms, as v8 does.
|
|
||||||
|
|
||||||
controlsd still owns reference selection, upstream curvature limiting,
|
|
||||||
service health and engagement. Tests execute its actual source-selection
|
|
||||||
and limiter code, its Ford call, Float32 publication in ControlsExt, conversion
|
|
||||||
to CarControlSP, and the pinned Ford CarController's in-memory CAN builder.
|
|
||||||
Both model-action and maneuver-planner selection are covered, including
|
|
||||||
disabling latActive after invalid output. Only the test chooses the adapter.
|
|
||||||
|
|
||||||
Yaw is not an input to the control law. The adapter checks it solely for the
|
|
||||||
inherited invalid-input policy. Driver override and optional PSCM status
|
|
||||||
do not modify the candidate base; existing engagement and downstream driver
|
|
||||||
arbitration remain responsible for authorization, as with v8's base request.
|
|
||||||
|
|
||||||
## Offline evidence
|
|
||||||
|
|
||||||
The checked-in `ford_model_action_validation.json` records the completed
|
|
||||||
checks and source hashes. Full arrays and detailed reports are generated
|
|
||||||
locally under `.cache/ford_model_action/`; original route files are read-only.
|
|
||||||
|
|
||||||
Completed validation: **264 Ford tests and 150 subtests pass**, including
|
|
||||||
120 new core/adapter/replay-validator cases. The candidate module has 100%
|
|
||||||
statement and branch coverage (78 statements, 24 branches). Ruff and Ty pass.
|
|
||||||
The 200,000-cycle numerical stress test also checks 200,000 mirrored core
|
|
||||||
updates and 18,138 field-boundary cases. Across route and stress runs,
|
|
||||||
485,238 Float32/CAN round trips pass. Eight deliberately injected faults
|
|
||||||
(heading gain/cap, erased C0, wrong C0 slew, retained invalid state, stale
|
|
||||||
model acceptance, model clock rollback and reversed C0 sign) are all caught
|
|
||||||
by the tests. Mutation runs replace code only inside isolated Python
|
|
||||||
processes; production source files are never modified by those probes.
|
|
||||||
|
|
||||||
Independent Standards and Spec reviews reported zero findings. The full
|
|
||||||
suite's Params setting test uses an existing local native library from
|
|
||||||
worktree 3548 after checking relevant source files are byte-identical;
|
|
||||||
its hash and provenance are in the manifest. That library is an ignored
|
|
||||||
test dependency, not part of this change. This is the full relevant Ford
|
|
||||||
suite, not the hardware-dependent test suite for every openpilot subsystem.
|
|
||||||
|
|
||||||
The replay has two separate passes:
|
|
||||||
|
|
||||||
* Core compatibility uses the archived eligibility mask and requires exact
|
|
||||||
equality with the independently implemented `action_heading` commands.
|
|
||||||
* Adapter reconstruction derives eligibility from recorded service streams
|
|
||||||
independently of the archived output mask. It retains original timestamps,
|
|
||||||
gaps and consumed model frames. Controls publication time proxies the
|
|
||||||
unlogged computation clock, and complete SubMaster health is unavailable.
|
|
||||||
|
|
||||||
All 54,738 route95 and 78,812 route90 core cycles match exactly, including
|
|
||||||
37,614 and 73,055 active cycles. The adapter preserves those active counts.
|
|
||||||
Its 59 / 19 changed commands arise solely from the fresh 10 ms engagement
|
|
||||||
tick instead of the archived harness's preceding publication interval;
|
|
||||||
the replay checks that attribution on every cycle. Maximum differences are
|
|
||||||
0.01 m / 0.001 rad (95) and 0.02 m / 0.002 rad (90).
|
|
||||||
|
|
||||||
Every core and adapter replay output is round-tripped through Float32 and
|
|
||||||
the real CAN packer/parser, including zero C2/C3, signs, mode and counter.
|
|
||||||
Continuous field slew and quantization allowance are checked separately
|
|
||||||
from immediate invalid-command resets. The original driver-clean cohorts,
|
|
||||||
speed strata and command RMS are reproduced without redoing the encoder search.
|
|
||||||
|
|
||||||
The numerical stress harness uses analytic rotated paths, scalar slew
|
|
||||||
arithmetic, mirrored requests, irregular intervals and invalid-input resets.
|
|
||||||
It also sweeps every representable host field value and the Float32 values
|
|
||||||
immediately below, at and above every half-quantum boundary. Direct CAN
|
|
||||||
packing of the continuous state must agree with the host's quantized output.
|
|
||||||
The unit tests cover releases, reversals, clipping, service freshness,
|
|
||||||
clock resets, malformed inputs, endpoint fallback and actual integration.
|
|
||||||
|
|
||||||
## Limits of the result
|
|
||||||
|
|
||||||
On turns at ≥15 m/s, candidate C0 RMS is 79%/81% below v8 on routes95/90,
|
|
||||||
while C1 is 33%/41% higher. Those are command changes, not evidence of
|
|
||||||
equivalent steering authority. The PSCM's independent C0/C1 response remains
|
|
||||||
unknown. Replay cannot establish physical model following, strong turns,
|
|
||||||
centering, overshoot, oscillation or closed-loop stability.
|
|
||||||
|
|
||||||
The release probe is intentionally explicit: a model bend can increase
|
|
||||||
while selected curvature decreases. At 20 m/s, one synthetic probe changes
|
|
||||||
C0/C1 from 0.24 m / 0.10 rad to 0.49 m / 0.08 rad. Zero selected curvature
|
|
||||||
sets the C1 target to zero but does not erase a nonzero current model C0.
|
|
||||||
Removing a yaw-integral tail does not prove that physical overshoot is solved.
|
|
||||||
No additional release policy or unsupported plant model is added to hide
|
|
||||||
that uncertainty.
|
|
||||||
|
|
||||||
## Reproduce
|
|
||||||
|
|
||||||
From this worktree, use the logged construction dependency explicitly:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
|
||||||
export PYTHONPATH=.:/Users/ibpersonal/.codex/worktrees/b926/sunnypilot/opendbc_repo
|
|
||||||
PY=/Users/ibpersonal/dev/sunnypilot/.venv/bin/python
|
|
||||||
EVIDENCE=/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/analysis/controller_search_20260904
|
|
||||||
$PY -m pytest -q -p no:cacheprovider openpilot/selfdrive/controls/tests/test_ford_*.py tools/ford_pscm_lab openpilot/selfdrive/car/tests/test_ford_pscm_status.py
|
|
||||||
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route95" --output .cache/ford_model_action/route95
|
|
||||||
$PY -m tools.ford_pscm_lab.model_action_replay "$EVIDENCE/route90" --output .cache/ford_model_action/route90
|
|
||||||
$PY -m tools.ford_pscm_lab.stress_model_action --cycles 200000 --seed 20260907 --output .cache/ford_model_action/stress.json
|
|
||||||
```
|
|
||||||
|
|
||||||
The route replay refuses an opendbc revision other than
|
|
||||||
`72a775d35e54c21ff5c5798acef22016eedcc0a7`. Stress defaults to this pin and
|
|
||||||
also accepts an explicitly required commit with `--opendbc-revision` for
|
|
||||||
deployment checks. A mismatch still fails. This historical pin reproduces
|
|
||||||
logged construction; it does not change the merge's submodule pointer.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Ford selected-action drive-test branch
|
|
||||||
|
|
||||||
The current experiment adds [measured-curvature C1 feedback](ford_c1_feedback.md)
|
|
||||||
and [conditional correction release](ford_c1_carryover.md) to the restored
|
|
||||||
original v1 mapping. It is selectable on **any Ford CAN FD vehicle**
|
|
||||||
through the existing persistent, default-off Sunnylink
|
|
||||||
toggle. Offline checks establish software behavior; physical tracking,
|
|
||||||
turn-exit behavior and closed-loop stability remain unvalidated.
|
|
||||||
|
|
||||||
## Select and restore
|
|
||||||
|
|
||||||
1. Install branch `hiimisaac-dev` from `sunnypilot/sunnypilot` using the device's
|
|
||||||
normal branch-switch process and allow its build to finish.
|
|
||||||
2. While offroad, open Sunnylink device settings → Vehicle → Ford and enable
|
|
||||||
**Selected-Action Path Tracking (Experimental)** (`FordModelActionController`).
|
|
||||||
3. Complete a real offroad-to-onroad cycle. Selection occurs when `controlsd`
|
|
||||||
starts; a stored toggle change or disengagement alone cannot swap an active
|
|
||||||
controller. Initial physical evaluation remains controlled testing.
|
|
||||||
|
|
||||||
The startup event `Ford path controller selected` should report
|
|
||||||
`FordModelActionController`. Periodic `Ford C2-free path tracking` events
|
|
||||||
identify **`hypothesis=model-action-c1-feedback-v2`**. They report desired and
|
|
||||||
measured curvature, base heading, accumulated correction, applied heading,
|
|
||||||
feedback timing and driver/PSCM gating. `carryover_release_count` counts
|
|
||||||
conditional releases since the last controller reset; it does not control
|
|
||||||
steering. `calibration_approved=false` remains.
|
|
||||||
|
|
||||||
Turning the toggle off and completing another offroad-to-onroad cycle restores
|
|
||||||
**PSCM Coefficient Observer** if selected, otherwise the original Ford path
|
|
||||||
controller. The stored observer selection is preserved. The experiment takes
|
|
||||||
priority on Ford CAN FD vehicles; other vehicles retain their existing
|
|
||||||
controller. A leftover `FordVirtualAngleController` parameter has no effect.
|
|
||||||
|
|
||||||
## Wiring and validation
|
|
||||||
|
|
||||||
`controlsd` supplies the selected, upstream-limited desired curvature and the
|
|
||||||
measured steering-derived curvature already used in its tracking diagnostics.
|
|
||||||
Fresh steering publications advance C1 feedback. Repeated publications may
|
|
||||||
advance output slew but cannot integrate the same elapsed interval twice.
|
|
||||||
Driver override clears the correction. A fresh PSCM reached-limit flag stops
|
|
||||||
extra outward accumulation while preserving unwind and base model changes.
|
|
||||||
With fresh feedback, the controller can discard an opposing correction when
|
|
||||||
it prevents C1 from following the direction shared by target C0, applied C0
|
|
||||||
and base C1, while measured curvature is still opposite. Neutral or conflicting
|
|
||||||
C0 and matched curvature preserve the correction. Final output slew still applies.
|
|
||||||
|
|
||||||
C0 retains the original 7 m model-path mapping. C2 and C3 remain zero. The
|
|
||||||
existing output limits, 100 Hz sender, Float32 publication and CAN builder
|
|
||||||
remain in place. No opendbc submodule or Panda safety change is required.
|
|
||||||
|
|
||||||
See [the carryover specification and validation](ford_c1_carryover.md) and
|
|
||||||
`ford_c1_carryover_validation.json` for current evidence and reproduction
|
|
||||||
commands. `ford_c1_feedback_validation.json` records the initial feedback
|
|
||||||
version at `5fbb583e5`. `ford_model_action_validation.json` and
|
|
||||||
`ford_model_action_drive_test_validation.json` are historical records for the
|
|
||||||
original offline candidate and its first wiring, respectively; their counts
|
|
||||||
and coverage are not claims about the feedback version.
|
|
||||||
|
|
||||||
The full hardware build and device boot are not performed by these offline
|
|
||||||
checks. Pushing the branch does not install it on the device or change its
|
|
||||||
stored toggle.
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
{
|
|
||||||
"date": "2026-09-07",
|
|
||||||
"baseline_commit": "7ca3c6e3b3e659c6f446039501c5826bbd14092e",
|
|
||||||
"branch": "codex/ford-model-action-drive-test",
|
|
||||||
"scope": "Default-off Sunnylink selection and v8 retirement; offline validation only. No device installation or physical performance validation.",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"production_selector_changed": true,
|
|
||||||
"toggle": "FordModelActionController",
|
|
||||||
"default_enabled": false,
|
|
||||||
"v8_removed": true,
|
|
||||||
"panda_safety_changed": false,
|
|
||||||
"opendbc_submodule_changed": false,
|
|
||||||
"deployment_opendbc_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9",
|
|
||||||
"controller_size": {
|
|
||||||
"total_lines": 145,
|
|
||||||
"code_lines_excluding_blanks_comments_docstrings": 95,
|
|
||||||
"core_persistent_values": 2,
|
|
||||||
"adapter_timestamps": 3,
|
|
||||||
"removed_v8_module_lines": 469
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"combined_ford_params_sunnylink_suite": "284 passed, 26 subtests passed in 2.63s",
|
|
||||||
"suite_log_sha256": "2e223a507f0630481cf6f83b9f8893d226f3f4273a79a09fc35905aa875b1d2c",
|
|
||||||
"coverage": {
|
|
||||||
"covered_lines": 87,
|
|
||||||
"num_statements": 87,
|
|
||||||
"percent_covered": 100.0,
|
|
||||||
"percent_covered_display": "100",
|
|
||||||
"missing_lines": 0,
|
|
||||||
"excluded_lines": 0,
|
|
||||||
"percent_statements_covered": 100.0,
|
|
||||||
"percent_statements_covered_display": "100",
|
|
||||||
"num_branches": 26,
|
|
||||||
"num_partial_branches": 0,
|
|
||||||
"covered_branches": 26,
|
|
||||||
"missing_branches": 0,
|
|
||||||
"percent_branches_covered": 100.0,
|
|
||||||
"percent_branches_covered_display": "100"
|
|
||||||
},
|
|
||||||
"ruff": "pass",
|
|
||||||
"ty_controller_and_lab": "pass",
|
|
||||||
"settings_compiler_check": "pass",
|
|
||||||
"standards_review_remaining_findings": 0,
|
|
||||||
"spec_review_remaining_findings": 0,
|
|
||||||
"resolved_review_finding": "Updated YAML authoring source and regenerated settings JSON before final compiler/schema suite."
|
|
||||||
},
|
|
||||||
"routes": {
|
|
||||||
"route95": {
|
|
||||||
"cycles": 54738,
|
|
||||||
"core_active_cycles": 37614,
|
|
||||||
"core_exact_archived_match": true,
|
|
||||||
"cohorts_reproduced": true,
|
|
||||||
"adapter_active_cycles": 37614,
|
|
||||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
|
||||||
"adapter_validity_differs_from_archive_cycles": 0,
|
|
||||||
"adapter_command_differs_from_archive_cycles": 59,
|
|
||||||
"adapter_max_absolute_command_difference_c0_c1": [
|
|
||||||
0.010000000000000675,
|
|
||||||
0.0010000000000000009
|
|
||||||
],
|
|
||||||
"field_slew_zero_c2_c3_pass": true,
|
|
||||||
"float32_can_round_trips": 109476,
|
|
||||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
|
||||||
"report_sha256": "72fab710dc81c8c7d9b75d371b97fec32402d3b01fa05517dfa814d8daff3134"
|
|
||||||
},
|
|
||||||
"route90": {
|
|
||||||
"cycles": 78812,
|
|
||||||
"core_active_cycles": 73055,
|
|
||||||
"core_exact_archived_match": true,
|
|
||||||
"cohorts_reproduced": true,
|
|
||||||
"adapter_active_cycles": 73055,
|
|
||||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
|
||||||
"adapter_validity_differs_from_archive_cycles": 0,
|
|
||||||
"adapter_command_differs_from_archive_cycles": 19,
|
|
||||||
"adapter_max_absolute_command_difference_c0_c1": [
|
|
||||||
0.020000000000000462,
|
|
||||||
0.0020000000000000018
|
|
||||||
],
|
|
||||||
"field_slew_zero_c2_c3_pass": true,
|
|
||||||
"float32_can_round_trips": 157624,
|
|
||||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
|
||||||
"report_sha256": "cc2224bae597a341697a7681560a077cb209d77e4d060c0850997691c57d32fb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stress": {
|
|
||||||
"seed": 20260907,
|
|
||||||
"random_cycles": 200000,
|
|
||||||
"mirrored_core_updates": 200000,
|
|
||||||
"invalid_or_inactive_resets": 3537,
|
|
||||||
"field_boundary_cases": 18138,
|
|
||||||
"float32_can_round_trips": 218138,
|
|
||||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
|
||||||
"direct_raw_float32_packing_matches_host_output": true,
|
|
||||||
"max_continuous_step_c0_c1": [
|
|
||||||
0.40000000000000147,
|
|
||||||
0.05000000000000002
|
|
||||||
],
|
|
||||||
"calibration_approved": false,
|
|
||||||
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
|
|
||||||
"opendbc_import_head": "c21a9013700734dd20b09e05aa68329ad8cc20f9"
|
|
||||||
},
|
|
||||||
"total_float32_can_round_trips": 485238,
|
|
||||||
"native_params": {
|
|
||||||
"source": "Rebuilt locally from this branch with clang++ and generated Capnp headers; ignored test dependency, not committed binary.",
|
|
||||||
"library_sha256": "270bf43241cf7c02cc432cf78ec9411a62d7653ca445695efe785ae82241aa09",
|
|
||||||
"sources_sha256": {
|
|
||||||
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
|
|
||||||
"openpilot/common/params.cc": "a5adacb1d47cb3bf6e0d87d44ce158b41982d7eaf2e8114e32c48d3a6631304c",
|
|
||||||
"openpilot/common/params.h": "ed03d137e126ecd6f1608016020af18c0339fb987e27d0a2aa6830bba396970c",
|
|
||||||
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
|
|
||||||
"openpilot/common/util.cc": "4479ecf72465e8f453d8af78447f7715f02d9397c58a49048f2bbc87a96d6b8a",
|
|
||||||
"openpilot/common/swaglog.cc": "9c2f88a2f1c3c4253b73defb264cc367a13ade23e02928e1d469b5c5833df176"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"test_dependency_notes": {
|
|
||||||
"python": "/Users/ibpersonal/dev/sunnypilot/.venv/bin/python",
|
|
||||||
"pyyaml": "6.0.3 from local uv cache",
|
|
||||||
"jsonschema": "Local cached package appended after venv to run schema validator without skips",
|
|
||||||
"hardware_build_and_device_boot": "not performed"
|
|
||||||
},
|
|
||||||
"source_sha256": {
|
|
||||||
"docs/ford_model_action_candidate.md": "c968132348d20891a9396f6e69db1315d570505796e19e09ffbb2da748e6e687",
|
|
||||||
"docs/ford_virtual_angle_experiment.md": "da6322f3c3d2d81463e44c50cc6cad1a962f97008ff9425c314da333ebe47a87",
|
|
||||||
"openpilot/common/params_c.cc": "57e3bcc7eba939bc91aadafb4ed1248b8123a8fe5c48fd8530298d966ea4db63",
|
|
||||||
"openpilot/common/params_keys.h": "39d36465f66405843b926ba18473fb6aee81c0f1c7bea87246aa08ffe3f67c58",
|
|
||||||
"openpilot/common/tests/test_params.py": "557a1f616af5fd9f5e623fbee0fd6f44cb059a30c290c68ce2b57e9bcceed081",
|
|
||||||
"openpilot/selfdrive/controls/controlsd.py": "102b383e5beff43b8dd7c219178bef62e8a4b54443ebe682694606862bcd4e7f",
|
|
||||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "8f3bc5d68e0051776f614a2ccffae84a88f7898dc95bdc12c23dcfe10dfe676a",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_controlsd_logging.py": "1c9448d88d8021e5d34a5dccd14a17c6c1bc64b5531342bfc6d15574d9d3e710",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "2c5b14f814e84e59d749f61a43ef1dcfe06253f6e185443fa123c37525ac8466",
|
|
||||||
"openpilot/sunnypilot/sunnylink/settings_ui.json": "9974df3ac4cc58ae78d47848cd18ef4aca1bcbb00edb257f28b4220d92890528",
|
|
||||||
"openpilot/sunnypilot/sunnylink/settings_ui_src/pages/vehicle.yaml": "7a3fa18e562d5a03a5b85c72ee3f3ebeb836c497285dcd9aaad24f8fb4dd6942",
|
|
||||||
"openpilot/sunnypilot/sunnylink/tests/test_settings_schema.py": "3db566612381fd87d3655a3ccff470be7da998c4ea7f6365f53e58cdb9c0ffb7",
|
|
||||||
"tools/ford_pscm_lab/model_action_replay.py": "827a6dc488d554bdf6e87438c6a2a985b3195bf6d01ab09002bfd6049d22a868",
|
|
||||||
"tools/ford_pscm_lab/stress_model_action.py": "2d5c72cc4b8ae214f2f5a19a050fe138f5c2ab0294d92d24e2481af5f8185613",
|
|
||||||
"tools/ford_pscm_lab/test_model_action_replay.py": "ebf6bcd9260745100311521f8e11e85b7aebdd5561ab0876bfc2e802429d6896",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_selection.py": "f826c6328f0abac2a61f1a0a6f8d119fdbab858e363cb466d84ba9a7783059cd",
|
|
||||||
"docs/ford_model_action_drive_test.md": "d825b177cd099efd797fe89b7041695d6d41e4b9e8bba6bcaeb32aece164262b"
|
|
||||||
},
|
|
||||||
"deployment_target": {
|
|
||||||
"repository": "sunnypilot/sunnypilot",
|
|
||||||
"branch": "hiimisaac-dev",
|
|
||||||
"validated_code_commit": "ea1ed70c718d32539ef6b9a89b89c0e297c92e06"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
{
|
|
||||||
"date": "2026-09-07",
|
|
||||||
"baseline_commit": "c4b3c55c826fca1ce09618e418e95f0a24478d96",
|
|
||||||
"calibration_approved": false,
|
|
||||||
"production_selector_changed": false,
|
|
||||||
"vehicle_settings_changed": false,
|
|
||||||
"panda_safety_changed": false,
|
|
||||||
"scope": "Offline command construction, adapter integration, numerical fault probes and Ford regression tests. Physical response remains unvalidated.",
|
|
||||||
"controller_size": {
|
|
||||||
"total_lines": 131,
|
|
||||||
"code_lines_excluding_blanks_comments_docstrings": 86,
|
|
||||||
"core_persistent_values": 2,
|
|
||||||
"adapter_timestamps": 3
|
|
||||||
},
|
|
||||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7",
|
|
||||||
"tests": {
|
|
||||||
"ford_suite": "264 passed, 150 subtests passed in 14.29s",
|
|
||||||
"new_core_adapter_tests": 107,
|
|
||||||
"new_replay_validator_tests": 13,
|
|
||||||
"controller_coverage": {
|
|
||||||
"statements": 78,
|
|
||||||
"missing_statements": 0,
|
|
||||||
"branches": 24,
|
|
||||||
"partial_branches": 0,
|
|
||||||
"percent": 100
|
|
||||||
},
|
|
||||||
"ruff": "pass",
|
|
||||||
"ty_controller_and_lab": "pass"
|
|
||||||
},
|
|
||||||
"routes": {
|
|
||||||
"route95": {
|
|
||||||
"cycles": 54738,
|
|
||||||
"core_active_cycles": 37614,
|
|
||||||
"core_exact_archived_match": true,
|
|
||||||
"cohorts_reproduced": true,
|
|
||||||
"adapter_active_cycles": 37614,
|
|
||||||
"adapter_status_counts": {
|
|
||||||
"inactive": 17124,
|
|
||||||
"active": 37614
|
|
||||||
},
|
|
||||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
|
||||||
"core_active_path_shorter_than_7m_cycles": 44,
|
|
||||||
"adapter_validity_differs_from_archive_cycles": 0,
|
|
||||||
"adapter_command_differs_from_archive_cycles": 59,
|
|
||||||
"adapter_max_absolute_command_difference_c0_c1": [
|
|
||||||
0.010000000000000675,
|
|
||||||
0.0010000000000000009
|
|
||||||
],
|
|
||||||
"field_slew_zero_c2_c3_pass": true,
|
|
||||||
"float32_can_round_trips": 109476,
|
|
||||||
"turn_speed_15_55": {
|
|
||||||
"seconds": 33.332073582999925,
|
|
||||||
"core_c0_c1_rms": [
|
|
||||||
0.07257996778378971,
|
|
||||||
0.03628926246224237
|
|
||||||
],
|
|
||||||
"recorded_v8_c0_c1_rms": [
|
|
||||||
0.3416081588451539,
|
|
||||||
0.027322786376822172
|
|
||||||
],
|
|
||||||
"adapter_eligible_seconds": 33.332073582999925,
|
|
||||||
"adapter_c0_c1_rms": [
|
|
||||||
0.07257996778378971,
|
|
||||||
0.03628926246224237
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"input_sha256": {
|
|
||||||
"route.npz": "6e5438867ea618e53ca395b60ff4b9b146256f1bf6f07dc8e92d663286074154",
|
|
||||||
"encoder_comparison.npz": "23bd05c4b6400299844acaba1d97051c96c23c682bf17b46e89e7f2cca5fce38",
|
|
||||||
"pose_replay.npz": "9cfbb3c6f0b1fdb7e3d38e6b64b8c94cffbcc9fabe41f6344d84a9b657b6af9b"
|
|
||||||
},
|
|
||||||
"report_sha256": "cf4e3804f2ebdb0e50f3c49636bf60a30e3c1180411b582826a115970ab972fc"
|
|
||||||
},
|
|
||||||
"route90": {
|
|
||||||
"cycles": 78812,
|
|
||||||
"core_active_cycles": 73055,
|
|
||||||
"core_exact_archived_match": true,
|
|
||||||
"cohorts_reproduced": true,
|
|
||||||
"adapter_active_cycles": 73055,
|
|
||||||
"adapter_status_counts": {
|
|
||||||
"inactive": 5757,
|
|
||||||
"active": 73055
|
|
||||||
},
|
|
||||||
"adapter_exact_match_with_fresh_engagement_dt": true,
|
|
||||||
"core_active_path_shorter_than_7m_cycles": 0,
|
|
||||||
"adapter_validity_differs_from_archive_cycles": 0,
|
|
||||||
"adapter_command_differs_from_archive_cycles": 19,
|
|
||||||
"adapter_max_absolute_command_difference_c0_c1": [
|
|
||||||
0.020000000000000462,
|
|
||||||
0.0020000000000000018
|
|
||||||
],
|
|
||||||
"field_slew_zero_c2_c3_pass": true,
|
|
||||||
"float32_can_round_trips": 157624,
|
|
||||||
"turn_speed_15_55": {
|
|
||||||
"seconds": 47.53485040600012,
|
|
||||||
"core_c0_c1_rms": [
|
|
||||||
0.08686835454709245,
|
|
||||||
0.043216347944464766
|
|
||||||
],
|
|
||||||
"recorded_v8_c0_c1_rms": [
|
|
||||||
0.4471065308273131,
|
|
||||||
0.030663943784303503
|
|
||||||
],
|
|
||||||
"adapter_eligible_seconds": 47.53485040600012,
|
|
||||||
"adapter_c0_c1_rms": [
|
|
||||||
0.08686835454709245,
|
|
||||||
0.043216347944464766
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"input_sha256": {
|
|
||||||
"route.npz": "51e8c26eedde253e171af47d704c1967ba45ae6825d883393bec1fb9e00251c1",
|
|
||||||
"encoder_comparison.npz": "7a625d3ed5cbd8013d1028aa3bc421740551dcae5c3d60981208bd047af9794c",
|
|
||||||
"pose_replay.npz": "4457ccc0868354749da5b72c1dea0faf750f783dfdc87038101288fdcba1e707"
|
|
||||||
},
|
|
||||||
"report_sha256": "b95247bdf6bbec16e5dc4781eaf7a678aca787418251cd161f6438e3173ad490"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stress": {
|
|
||||||
"seed": 20260907,
|
|
||||||
"random_cycles": 200000,
|
|
||||||
"mirrored_core_updates": 200000,
|
|
||||||
"invalid_or_inactive_resets": 3537,
|
|
||||||
"field_boundary_cases": 18138,
|
|
||||||
"float32_can_round_trips": 218138,
|
|
||||||
"analytic_targets_scalar_slew_and_mirror_checks_pass": true,
|
|
||||||
"direct_raw_float32_packing_matches_host_output": true,
|
|
||||||
"max_continuous_step_c0_c1": [
|
|
||||||
0.40000000000000147,
|
|
||||||
0.05000000000000002
|
|
||||||
],
|
|
||||||
"calibration_approved": false,
|
|
||||||
"scope": "Numerical construction only; no PSCM response or closed-loop performance claims.",
|
|
||||||
"opendbc_import_head": "72a775d35e54c21ff5c5798acef22016eedcc0a7"
|
|
||||||
},
|
|
||||||
"mutation_checks": {
|
|
||||||
"mutations": [
|
|
||||||
{
|
|
||||||
"mutation": "halve_heading",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "cap_heading_preview",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "erase_centering",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 15
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "slow_c0_to_c1_rate",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 11
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "retain_invalid_state",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 18
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "ignore_model_freshness",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "ignore_model_clock_rollback",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mutation": "reverse_c0_wire_sign",
|
|
||||||
"detected_by_tests": true,
|
|
||||||
"failed_tests": 11
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"all_detected": true
|
|
||||||
},
|
|
||||||
"native_test_dependency": {
|
|
||||||
"scope": "Native dependency for inherited Params selection test only; copied existing local build, not rebuilt.",
|
|
||||||
"source_library": "/Users/ibpersonal/.codex/worktrees/3548/sunnypilot/openpilot/common/libparams_c.dylib",
|
|
||||||
"sha256": "ddde738108eab18b75f085c865c43aa197fd79a0384b390b1515ff90116e20e0",
|
|
||||||
"byte_identical_source_files": [
|
|
||||||
"openpilot/common/params.cc",
|
|
||||||
"openpilot/common/params.h",
|
|
||||||
"openpilot/common/params_c.cc",
|
|
||||||
"openpilot/common/params.py",
|
|
||||||
"openpilot/common/params_keys.h",
|
|
||||||
"openpilot/common/queue.h",
|
|
||||||
"openpilot/common/util.cc",
|
|
||||||
"openpilot/common/util.h",
|
|
||||||
"openpilot/common/hardware/hw.h"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"review": {
|
|
||||||
"standards_findings": 0,
|
|
||||||
"spec_findings": 0,
|
|
||||||
"method": "Independent parallel read-only reviews; 120 focused tests independently passed."
|
|
||||||
},
|
|
||||||
"source_sha256": {
|
|
||||||
"openpilot/selfdrive/controls/lib/ford_model_action.py": "cb6353f00f2f5c84df4e606c6b7e20650f8c1e908b9fd71890f72aa4a5e42592",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action.py": "c3b971622cc4041575aeec1826d45b76cebab9a2f77b2b9525c85d4184961295",
|
|
||||||
"openpilot/selfdrive/controls/tests/test_ford_model_action_adapter.py": "5aaa29c1f11080b7df0165fd0202a29b065053809e5f9c4b9ed8abfcce47e41b",
|
|
||||||
"tools/ford_pscm_lab/__init__.py": "db4b8b7d2e317ed34ca0ec220bf9d53e7b80a23e766f4e1cb2c8224dada45f2e",
|
|
||||||
"tools/ford_pscm_lab/model_action_replay.py": "c114c479bd22e4fc61a3e8d3ee7fae5d71d1d80ec4b952faad8f8f3692fb1508",
|
|
||||||
"tools/ford_pscm_lab/stress_model_action.py": "a78a50eed1f801f3b096d694ab8c2fd70804b6c250465b4152f38d83770a982b",
|
|
||||||
"tools/ford_pscm_lab/test_model_action_replay.py": "09d024c59d44b83ec081d6416d0f946a7719a73f22213af1f4ddc03dc4e6f4ac"
|
|
||||||
},
|
|
||||||
"artifacts": {
|
|
||||||
"directory": ".cache/ford_model_action",
|
|
||||||
"route_reports": [
|
|
||||||
"route95/report.json",
|
|
||||||
"route90/report.json"
|
|
||||||
],
|
|
||||||
"stress_report": "stress.json",
|
|
||||||
"mutation_report": "mutations/report.json",
|
|
||||||
"test_log": "ford_suite.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
# Ford C2-free model-pose tracking with measured feedback
|
|
||||||
|
|
||||||
This experiment is retired. Its implementation, setting and dedicated tests
|
|
||||||
were removed from the selected-action drive-test branch. For current setup,
|
|
||||||
see [Ford selected-action drive testing](ford_model_action_drive_test.md).
|
|
||||||
The material below is historical; it does not describe an available toggle.
|
|
||||||
|
|
||||||
Hypothesis `model-pose-c0-c1-feedback-v8` retains the model-pose C0/C1 base
|
|
||||||
and adds two guarded release policies. When measured turning exceeds both
|
|
||||||
current and delayed requests, a separate output guard prevents same-direction
|
|
||||||
C0/C1 growth, including while feedback history rebuilds after driver input.
|
|
||||||
When turning instead falls below both requests and is no longer increasing,
|
|
||||||
bounded C1 tracking can use remaining release-entry command headroom.
|
|
||||||
Existing opposing-bias recovery still stops at zero bias. Geometry, blending,
|
|
||||||
feedback gain, slew rates and field limits are unchanged; C2/C3 remain zero.
|
|
||||||
|
|
||||||
This is an experimental outer controller around the multivariable PSCM.
|
|
||||||
Its geometry does not define a calibrated C0/C1-to-wheel mapping or an angle
|
|
||||||
servo. V8 has offline validation only. Command replay cannot establish the
|
|
||||||
truck's response, closed-loop stability, or an overshoot improvement.
|
|
||||||
|
|
||||||
## Evidence and scope
|
|
||||||
|
|
||||||
Route80 ran v3 and contains both sustained under-response and over-response.
|
|
||||||
Representative eligible windows had median CAN response/request ratios of
|
|
||||||
0.78, 1.77 and 0.69 with a declared 0.2-second comparison interval. These
|
|
||||||
are descriptive tracking ratios, not identified controller gains.
|
|
||||||
|
|
||||||
V4 replaced separate model-heading C1 with selected-curvature C1 and reduced
|
|
||||||
heading demand in several large maneuvers. The user subsequently reported
|
|
||||||
weak turning and steering repeatedly stopping near 85 degrees. Older logs
|
|
||||||
contain larger wheel angles; the inspected host code has no fixed 85-degree
|
|
||||||
wheel stop, although upstream curvature limits depend on speed.
|
|
||||||
|
|
||||||
Route83 had the Sunnylink toggle on, but omitted EPS firmware responses.
|
|
||||||
The former firmware gate selected the default `FordPathController`; replay
|
|
||||||
reproduced its recorded C0/C1/C2 requests. Its favorable turns are evidence
|
|
||||||
for the existing model-pose construction, not validation of v5 or v6.
|
|
||||||
V6 reuses that construction while replacing its remaining C2 request with
|
|
||||||
C0/C1 geometry. Removing C2 changes the request received by the PSCM, so
|
|
||||||
matching large C0/C1 commands does not guarantee matching vehicle motion.
|
|
||||||
|
|
||||||
Route8a ran v6 and was reported as the best drive. Route8e ran v7 throughout
|
|
||||||
with the experiment enabled; it includes entry lag and excessive turning
|
|
||||||
while requests release. Fixed-input v6/v7 replay produced identical commands
|
|
||||||
in the main reversal and over-response examples, so the v7 recovery change
|
|
||||||
does not directly explain their command behavior. In the over-response
|
|
||||||
example, model C0/C1 grew while selected curvature fell and driver resets
|
|
||||||
repeatedly removed feedback history. Another exit remained deficient after
|
|
||||||
opposing bias reached zero. These observations motivate the v8 guards; they
|
|
||||||
do not isolate an EPS transfer function or demonstrate the proposed response.
|
|
||||||
|
|
||||||
## Base request
|
|
||||||
|
|
||||||
controlsd selects valid `lateralManeuverPlan.desiredCurvature`, otherwise
|
|
||||||
`modelV2.action.desiredCurvature`, after the existing curvature limiter.
|
|
||||||
This action already includes upstream delay handling; it receives no extra
|
|
||||||
response advance here.
|
|
||||||
|
|
||||||
The model contribution uses the existing allocator's raw forward pose and
|
|
||||||
bounded short-pose correction. `_model_pose` advances 0.1 seconds, retains
|
|
||||||
the model's remaining forward geometry, and separately corrects the short
|
|
||||||
pose using measured curvature and its recent change. Its offset preview is
|
|
||||||
up to 7 m and its heading preview is up to max(7 m, speed × 1 s), bounded by
|
|
||||||
available path length. This raw pose is not passed through a second model
|
|
||||||
filter. The filtered, ego-aligned reference remains available for comparison
|
|
||||||
and the existing geometry-validity checks.
|
|
||||||
|
|
||||||
```text
|
|
||||||
share(k) = clip((k - 0.006/m) / (0.012/m - 0.006/m), 0, 1)
|
|
||||||
aligned = desired_curvature × model_forward_heading > 0
|
|
||||||
model_share = min(share(abs(desired_curvature)), share(model_curvature_demand))
|
|
||||||
if aligned, otherwise 0
|
|
||||||
model_pair = existing_pose_encoder(model_pose, model_share, C2=0)
|
|
||||||
|
|
||||||
remaining_curvature = desired_curvature × (1 - model_share)
|
|
||||||
L0 = max(8 m, speed × 1 s)
|
|
||||||
L1 = max(7 m, speed × 1 s)
|
|
||||||
curvature_C0 = 0.5 × remaining_curvature × L0²
|
|
||||||
curvature_C1 = remaining_curvature × L1
|
|
||||||
C0_base = clip(model_pair.C0 + curvature_C0, ±5.11 m)
|
|
||||||
C1_base = clip(model_pair.C1 + curvature_C1, ±0.5 rad)
|
|
||||||
```
|
|
||||||
|
|
||||||
`model_curvature_demand` is the larger absolute curvature implied by the
|
|
||||||
forward offset and heading previews. The share uses the existing allocator's
|
|
||||||
0.006–0.012/m thresholds. Both model and action must request a substantial
|
|
||||||
turn in the same direction before model pose supplies the full base.
|
|
||||||
Small, flat, opposed or zero requests use the curvature contribution; zero
|
|
||||||
action produces a zero base. Partial shares combine both contributions.
|
|
||||||
The existing pose encoder retains its quantization and field-allocation rules.
|
|
||||||
The residual-curvature lift is geometric, not a claim of EPS equivalence to C2.
|
|
||||||
|
|
||||||
The inherited pose encoder allocates heading overflow using its asymmetric
|
|
||||||
limits (+0.5235/−0.5 rad), before the symmetric final ±0.5 rad
|
|
||||||
heading bound. On clipped tails, this can leave mirrored C0 requests differing
|
|
||||||
by up to 0.0235 rad × 7 m = 0.1645 m. The favorable comparison anchors lie
|
|
||||||
below that heading cap; full model-base odd symmetry is not claimed.
|
|
||||||
|
|
||||||
## Measured feedback and limits
|
|
||||||
|
|
||||||
```text
|
|
||||||
past_request = selected curvature held at or before (measurement_time - delay)
|
|
||||||
yaw_error = measured_speed × past_request - measured_yaw_rate
|
|
||||||
bias_trial = released_bias + feedback_gain × yaw_error × measurement_dt
|
|
||||||
C1_unconstrained = clip(C1_base + accepted_bias, ±0.5 rad)
|
|
||||||
C1_target = temporary_backoff_ceiling(C1_unconstrained) if backoff_active
|
|
||||||
otherwise C1_unconstrained
|
|
||||||
```
|
|
||||||
|
|
||||||
Measured yaw is negated Ford CAN yaw, matching the control sign convention.
|
|
||||||
The historical request uses zero-order hold; it never interpolates toward a
|
|
||||||
future publication. Nominal comparison delay is `CP.steerActuatorDelay`
|
|
||||||
(0.2 seconds on the source vehicle). Feedback compares against selected
|
|
||||||
curvature, not curvature inferred from the model-pose coefficients.
|
|
||||||
|
|
||||||
| Quantity | Value |
|
|
||||||
|---|---:|
|
|
||||||
| C0 / C1 final bounds | ±5.11 m / ±0.5 rad |
|
|
||||||
| Independent C0 / C1 slew | 4 m/s / 0.5 rad/s |
|
|
||||||
| Feedback integration scale | 1.0 |
|
|
||||||
| Feedback minimum speed | 2 m/s |
|
|
||||||
| Maximum PSCM/core input age | 150 ms |
|
|
||||||
| Allowed timestamp lead | 5 ms |
|
|
||||||
| Release comparison tolerance | one C1 wire quantum, 0.0005 rad |
|
|
||||||
|
|
||||||
The integration scale, preview distances and blend thresholds are effective
|
|
||||||
gains; none establishes stability. No wheel-response gain is fitted.
|
|
||||||
Zero yaw error retains acquired bias while an eligible turn continues.
|
|
||||||
Host anti-windup admits reachable correction within the combined C1 field
|
|
||||||
and slew limits. Feedback overflow is not transferred into C0.
|
|
||||||
|
|
||||||
The release logic scales bias as the bounded base decreases and resets on
|
|
||||||
zero/reversal. When delayed curvature still represents a stronger or opposing
|
|
||||||
request, or PSCM reports LimitReached, new integration is normally frozen.
|
|
||||||
One exception permits measured-error backoff: measured turning must exceed
|
|
||||||
both the delayed and current selected yaw requests in the base's direction,
|
|
||||||
and total heading must still have the base's sign. Exceeding only an older,
|
|
||||||
smaller request during turn-in does not qualify. The accepted increment may
|
|
||||||
only reduce that existing total toward zero; it cannot grow the request or
|
|
||||||
carry it through zero. Existing host field and slew limits still apply.
|
|
||||||
|
|
||||||
The existing release-recovery exception requires fresh valid PSCM status with
|
|
||||||
limit below 2, retained bias opposing the base, and both current and delayed
|
|
||||||
requests aligned with that base. Measured turning must be below both requests
|
|
||||||
in their direction. It then uses the current yaw deficit × the existing
|
|
||||||
feedback gain × measurement interval to unwind only the opposing bias toward
|
|
||||||
zero. The increment is clipped so recovery cannot cross zero bias or create
|
|
||||||
demand beyond the existing base. Common host anti-windup still limits what
|
|
||||||
can be accepted. A separate release-tracking exception is described below;
|
|
||||||
other constrained cases remain frozen. PSCM limit 2 never permits either
|
|
||||||
request-increasing exception.
|
|
||||||
The no-new-bias restriction applies to `release_recovery`. It does not apply
|
|
||||||
to the separate bounded `release_tracking` branch. Once release ends,
|
|
||||||
ordinary eligible integration can add correction beyond the base as before;
|
|
||||||
its existing limits and guards are unchanged.
|
|
||||||
|
|
||||||
`release_recovery` and `feedback_recovery_active=true` indicate that the
|
|
||||||
recovery branch actually changed bias on that update. If host anti-windup
|
|
||||||
blocks the entire increment, the status remains `host_limit` and the flag is
|
|
||||||
false. Recovery is evaluated only on fresh measurements; the flag is false
|
|
||||||
on repeated-measurement updates and after reset.
|
|
||||||
|
|
||||||
Diagnostics distinguish `release_backoff` and `pscm_backoff`; a release takes
|
|
||||||
precedence when both conditions apply. While `feedback_backoff_active` is
|
|
||||||
true, total C1 is also capped at the preceding continuous heading request in
|
|
||||||
the current request direction and at zero in the opposite direction. This
|
|
||||||
ceiling affects the output only: it is not stored or projected into bias.
|
|
||||||
The measured-error increment can still update bias under the normal limits,
|
|
||||||
but a changing model base does not create persistent integral suppression.
|
|
||||||
The ceiling persists between repeated measurements; C1 cannot grow or reverse
|
|
||||||
while it applies. The next fresh measurement clears it unless backoff is
|
|
||||||
again warranted. It does not cap C0, and normal feedback has its own rules
|
|
||||||
outside backoff. Independent slew remains 0.5 rad/s for C1 and 4 m/s for C0.
|
|
||||||
Backoff still compares against the delayed reference, so response lag remains.
|
|
||||||
Reducing a request does not demonstrate that physical overshoot is resolved.
|
|
||||||
|
|
||||||
## V8 release guard and tracking
|
|
||||||
|
|
||||||
`ReleaseGuard` retains selected-request history independently of feedback
|
|
||||||
bias history. Driver-related feedback resets do not erase that reference,
|
|
||||||
but the guard still requires current fresh valid PSCM status, no current
|
|
||||||
driver override, and the existing input and speed eligibility. Invalid core
|
|
||||||
input or disengagement resets its history with the controller.
|
|
||||||
|
|
||||||
During release, measured yaw must exceed both the current and delay-matched
|
|
||||||
requests in the requested turn direction. Only then does the guard cap
|
|
||||||
same-direction C0/C1 growth at each preceding continuous request. Terms
|
|
||||||
already reducing the turn, including an opposing C0 centering offset, remain
|
|
||||||
available. The guard follows base allocation and C1 feedback, so changing
|
|
||||||
model geometry cannot bypass it. Its ceilings affect outputs, never stored
|
|
||||||
bias. No scalar-curvature cap replaces strong model geometry during turn-in
|
|
||||||
or undertracking. Existing independent slew and field limits still apply.
|
|
||||||
|
|
||||||
`release_tracking` addresses an eligible release deficit once bias is zero
|
|
||||||
or already in the base's direction. Both current and delayed requests must
|
|
||||||
align with that base, measured turning must be below both, and measured
|
|
||||||
curvature must not be rising in the turn direction across the response
|
|
||||||
interval by more than one C1 wire quantum after scaling by heading preview.
|
|
||||||
Fresh valid PSCM status with limit below 2 is required. The current yaw deficit
|
|
||||||
uses the existing integration gain and measurement interval;
|
|
||||||
new C1 tracking increments are limited by command headroom captured at
|
|
||||||
release entry, tapered with remaining desired curvature. The allowance is
|
|
||||||
`max(0, entry_command_magnitude - abs(base)) × min(1, abs(desired) / entry_reference)`
|
|
||||||
above the current base; any existing same-direction bias consumes it first.
|
|
||||||
This limits new tracking integration, not the existing model base or bias.
|
|
||||||
Only that additional allowance is tapered; strong model geometry remains
|
|
||||||
available. A brief pause does not reacquire a higher entry
|
|
||||||
ceiling; a full response interval without release ends the retained episode.
|
|
||||||
Common host anti-windup, field and slew bounds still apply. Opposing bias
|
|
||||||
continues through `release_recovery`, which stops at zero, before any separate
|
|
||||||
tracking exception can be considered.
|
|
||||||
|
|
||||||
Neither exception relaxes the PSCM LimitReached growth restriction. The
|
|
||||||
reference delay and finite response time remain; these output policies are
|
|
||||||
command-construction changes, not evidence of improved physical tracking.
|
|
||||||
|
|
||||||
## PSCM status and driver handling
|
|
||||||
|
|
||||||
card publishes `Lane_Assist_Data3_FD1` in `carStateSP.fordPscmStatus`, retaining
|
|
||||||
the original CAN receipt timestamp. Republishing carStateSP or receiving
|
|
||||||
unrelated frames cannot refresh it. The opendbc submodule is unchanged.
|
|
||||||
|
|
||||||
Feedback requires valid fresh status, InProgress lateral state (2), capability
|
|
||||||
LimitedModeAvailable or ExtendedModeAvailable (1 or 2), and no denial.
|
|
||||||
Missing, malformed, stale, backward-timestamped, denied or unavailable status
|
|
||||||
clears feedback bias/history and disables the separate release guard,
|
|
||||||
leaving the base subject to its core validity gates.
|
|
||||||
LimitReached (2) permits only the bounded request-reducing backoff described
|
|
||||||
above and otherwise freezes integration. LimitWithDriverActive (3) clears
|
|
||||||
feedback. Backoff still requires fresh, valid, InProgress status with an
|
|
||||||
available capability and no denial. These generic PSCM reports do not identify
|
|
||||||
a specific torque or rate limit.
|
|
||||||
|
|
||||||
`steeringPressed`, raw torque above the existing Ford driver allowance, or
|
|
||||||
nonfinite torque clear feedback. Below 2 m/s feedback also clears. A fresh
|
|
||||||
feedback reference interval is required after override; the independent
|
|
||||||
release guard can use retained valid request history once its current gates
|
|
||||||
are satisfied. Base requests retain normal
|
|
||||||
PSCM driver arbitration while lateral control remains authorized; an unset
|
|
||||||
override flag cannot rule out subthreshold driver influence.
|
|
||||||
|
|
||||||
## Gates and Sunnylink selection
|
|
||||||
|
|
||||||
Core model/action/car-state freshness, finite-value, clock and speed checks
|
|
||||||
remain in place. Invalid core inputs reset both commands and clear latActive.
|
|
||||||
Raw model geometry is validated on every update, including repeated model
|
|
||||||
timestamps; an invalid raw path cannot reuse the cached valid reference.
|
|
||||||
Missing PSCM status disables feedback, not an otherwise valid base request.
|
|
||||||
|
|
||||||
Vehicle → Ford → **C2-Free Path Tracking (Experimental)** retains the
|
|
||||||
`FordVirtualAngleController` key, default-off setting and offroad/onroad cycle
|
|
||||||
requirement. Enabled selects v8 on Ford CAN FD `FORD_F_150_LIGHTNING_MK1`
|
|
||||||
regardless of missing or different EPS firmware-query results. Other platforms
|
|
||||||
retain their existing controller. V8 takes priority over PSCM Coefficient
|
|
||||||
Observer while selected; disabling and cycling offroad/onroad restores the
|
|
||||||
previous selection. Controller selection does not force lateral engagement.
|
|
||||||
|
|
||||||
The analyzed firmware is `RL38-14D003-AA`; removing the eligibility check
|
|
||||||
is not validation of other firmware. No live device setting is changed.
|
|
||||||
|
|
||||||
## Diagnostics and verification
|
|
||||||
|
|
||||||
The 5 Hz `Ford C2-free path tracking` event keeps its name and identifies v8.
|
|
||||||
`model_offset_base` / `model_heading_base` report the already weighted and
|
|
||||||
encoded model contribution; `curvature_offset_base` / `curvature_heading_base`
|
|
||||||
report the residual-curvature contribution. `model_share` and `base_guard`
|
|
||||||
identify model-pose, blended, curvature-only, opposed-model and zero-request
|
|
||||||
cases. `heading_base` is the bounded pre-feedback C1. `offset_target` and
|
|
||||||
`heading_target` are the final targets after the independent release guard;
|
|
||||||
`offset_target_unguarded` and `heading_target_unguarded` retain the inputs to
|
|
||||||
that guard. The latter C1 already includes its normal feedback/backoff policy.
|
|
||||||
|
|
||||||
The event retains source timestamps, measured curvature/yaw, final commands,
|
|
||||||
slew scales, feedback bias/status/history, raw torque and PSCM status/age.
|
|
||||||
`feedback_backoff_active` records the persistent heading ceiling, including
|
|
||||||
cycles whose feedback status is `no_new_measurement`.
|
|
||||||
`release_guard_active` and `release_guard_reference_curvature` expose the
|
|
||||||
independent C0/C1 guard and its retained delayed reference.
|
|
||||||
`feedback_release_tracking_active`, `feedback_release_ceiling` and
|
|
||||||
`feedback_curvature_delta` identify accepted release
|
|
||||||
tracking, the total-heading threshold used to admit new bias, and the
|
|
||||||
measured-curvature change across the response interval (1/m). The tracking
|
|
||||||
flag is true only when the branch accepts a bias change on a new measurement;
|
|
||||||
it is false on repeated measurements. The ceiling/trend fields can describe
|
|
||||||
an evaluated condition even when no increment is accepted.
|
|
||||||
`feedback_recovery_active` records an accepted recovery increment on this
|
|
||||||
update only; it does not persist between measurements.
|
|
||||||
`feedback_yaw_error` retains its delayed-reference meaning. Recovery instead
|
|
||||||
uses current error, reconstructed from logged `desired_curvature`,
|
|
||||||
synchronized car-state speed and `yaw_rate`; those two errors can differ.
|
|
||||||
During backoff or the independent release guard, `heading_target` can be lower in the request direction than
|
|
||||||
the bounded sum of `heading_base` and `heading_bias`, because the temporary
|
|
||||||
ceiling is not part of the stored bias.
|
|
||||||
`model_heading_target` remains a filtered comparison reference; it is not the
|
|
||||||
weighted model contribution. `angleState.saturated` is not an EPS-limit signal.
|
|
||||||
|
|
||||||
Validation must cover large recorded maneuvers, flat-model centering, both
|
|
||||||
turn directions, model/action disagreement, share transitions, release and
|
|
||||||
reversal, release/limit backoff without growth or zero crossing, status/driver
|
|
||||||
resets, reference causality, bounds, slew and CAN packing with C2/C3 zero.
|
|
||||||
Recovery checks cover both directions, stopping at zero bias, repeated
|
|
||||||
measurements, current-and-delayed agreement, and rejection at PSCM limit 2.
|
|
||||||
Old v3/v4 command-equality expectations do not define
|
|
||||||
v8 success. Guard checks also cover driver reset/history rebuilding,
|
|
||||||
same-direction growth, opposing coefficients, repeated measurements,
|
|
||||||
undertracking and invalid-status inhibition. Tracking checks cover delayed
|
|
||||||
curvature trends and tapered release-entry headroom. Historical v5–v7 replay
|
|
||||||
results remain historical observations.
|
|
||||||
|
|
||||||
The v8 recorded-input fixture contains 15,273 cycles with 4,879 selected
|
|
||||||
evidence samples. Base allocation and output eligibility match v7. In the
|
|
||||||
clean deficient exit, median absolute C1 changes from 0.0665 to 0.0845 rad
|
|
||||||
while C0 stays unchanged. The growth guard also acts while feedback history
|
|
||||||
rebuilds; the largest over-growth witness includes nearby driver input and
|
|
||||||
is excluded from the strict autonomous tracking score. Both good comparison
|
|
||||||
curves in that fixture retain their median requests, and the older large-turn
|
|
||||||
fixtures retain their required command scale.
|
|
||||||
|
|
||||||
On the earlier good drive, one comparison curve retains extra C1 after
|
|
||||||
eligible release tracking: median magnitude changes from 0.121 to 0.128 rad.
|
|
||||||
In its 103–110 s interval, tracking increments occur only while measured
|
|
||||||
turning falls short, with a median current response/request ratio of 0.895.
|
|
||||||
Acquired bias can persist after matching, as with ordinary integral feedback.
|
|
||||||
This collateral command change remains a reason to compare new vehicle logs.
|
|
||||||
Replay fixes recorded motion and planner outputs, so enabled vehicle logs
|
|
||||||
are still required to assess tracking error, oscillation and interventions.
|
|
||||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
|||||||
export QCOM_PRIORITY=12
|
export QCOM_PRIORITY=12
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="19.7"
|
export AGNOS_VERSION="19.6"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: c21a901370...06743dfb39
@@ -353,7 +353,6 @@ struct OnroadEventSP @0xda96579883444c35 {
|
|||||||
speedLimitPending @22;
|
speedLimitPending @22;
|
||||||
e2eChime @23;
|
e2eChime @23;
|
||||||
laneChangeRoadEdge @24;
|
laneChangeRoadEdge @24;
|
||||||
bigModelReady @25;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +382,6 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
leadOne @2 :LeadData;
|
leadOne @2 :LeadData;
|
||||||
leadTwo @3 :LeadData;
|
leadTwo @3 :LeadData;
|
||||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||||
fordLateralPath @5 :FordLateralPath;
|
|
||||||
|
|
||||||
struct Param {
|
struct Param {
|
||||||
key @0 :Text;
|
key @0 :Text;
|
||||||
@@ -404,14 +402,6 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FordLateralPath {
|
|
||||||
pathOffset @0 :Float32; # c0 [m]
|
|
||||||
pathAngle @1 :Float32; # c1 [rad]
|
|
||||||
curvature @2 :Float32; # c2 [1/m]
|
|
||||||
curvatureRate @3 :Float32; # c3 [1/m^2]
|
|
||||||
valid @4 :Bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||||
backupStatus @0 :Status;
|
backupStatus @0 :Status;
|
||||||
restoreStatus @1 :Status;
|
restoreStatus @1 :Status;
|
||||||
@@ -456,16 +446,6 @@ struct BackupManagerSP @0xf98d843bfd7004a3 {
|
|||||||
|
|
||||||
struct CarStateSP @0xb86e6369214c01c8 {
|
struct CarStateSP @0xb86e6369214c01c8 {
|
||||||
speedLimit @0 :Float32;
|
speedLimit @0 :Float32;
|
||||||
fordPscmStatus @1 :FordPscmStatus;
|
|
||||||
|
|
||||||
struct FordPscmStatus {
|
|
||||||
valid @0 :Bool;
|
|
||||||
canMonoTime @1 :UInt64; # Last accepted Lane_Assist_Data3_FD1 CAN receipt, not carStateSP publication time.
|
|
||||||
lateralState @2 :UInt8; # LatCtlSte_D_Stat
|
|
||||||
limit @3 :UInt8; # LatCtlLim_D_Stat: generic lateral limit, not a torque/rate diagnosis.
|
|
||||||
capability @4 :UInt8; # LatCtlCpblty_D_Stat
|
|
||||||
denied @5 :Bool; # LaActDeny_B_Actl
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LiveMapDataSP @0xf416ec09499d9d19 {
|
struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||||
@@ -489,30 +469,7 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AssistedDrivingMilestoneState @0xcb9fd56c7057593a {
|
struct CustomReserved10 @0xcb9fd56c7057593a {
|
||||||
enabled @0 :Bool;
|
|
||||||
madsDistanceMeters @1 :Float64;
|
|
||||||
fullAssistDistanceMeters @2 :Float64;
|
|
||||||
event @3 :Event;
|
|
||||||
|
|
||||||
struct Event {
|
|
||||||
id @0 :UInt64;
|
|
||||||
category @1 :Category;
|
|
||||||
distanceMeters @2 :Float64;
|
|
||||||
previousDistanceMeters @3 :Float64;
|
|
||||||
unit @4 :Unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Category {
|
|
||||||
none @0;
|
|
||||||
mads @1;
|
|
||||||
fullAssist @2;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Unit {
|
|
||||||
imperial @0;
|
|
||||||
metric @1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||||
|
|||||||
@@ -725,7 +725,6 @@ struct ChestnutState {
|
|||||||
pcieLtssm @7 :UInt8;
|
pcieLtssm @7 :UInt8;
|
||||||
supplyVoltage @8 :UInt16; # mV
|
supplyVoltage @8 :UInt16; # mV
|
||||||
supplyCurrent @9 :Int16; # mA
|
supplyCurrent @9 :Int16; # mA
|
||||||
supplyFault @10 :Bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RadarState @0x9a185389d6fdd05f {
|
struct RadarState @0x9a185389d6fdd05f {
|
||||||
@@ -1005,7 +1004,6 @@ struct DrivingModelData {
|
|||||||
frameIdExtra @1 :UInt32;
|
frameIdExtra @1 :UInt32;
|
||||||
frameDropPerc @6 :Float32;
|
frameDropPerc @6 :Float32;
|
||||||
modelExecutionTime @7 :Float32;
|
modelExecutionTime @7 :Float32;
|
||||||
big @8 :Bool;
|
|
||||||
|
|
||||||
action @2 :ModelDataV2.Action;
|
action @2 :ModelDataV2.Action;
|
||||||
|
|
||||||
@@ -2642,7 +2640,7 @@ struct Event {
|
|||||||
carStateSP @114 :Custom.CarStateSP;
|
carStateSP @114 :Custom.CarStateSP;
|
||||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||||
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
|
customReserved10 @136 :Custom.CustomReserved10;
|
||||||
customReserved11 @137 :Custom.CustomReserved11;
|
customReserved11 @137 :Custom.CustomReserved11;
|
||||||
customReserved12 @138 :Custom.CustomReserved12;
|
customReserved12 @138 :Custom.CustomReserved12;
|
||||||
customReserved13 @139 :Custom.CustomReserved13;
|
customReserved13 @139 :Custom.CustomReserved13;
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ _services: dict[str, tuple] = {
|
|||||||
"carParamsSP": (True, 0.02, 1),
|
"carParamsSP": (True, 0.02, 1),
|
||||||
"carControlSP": (True, 100., 10),
|
"carControlSP": (True, 100., 10),
|
||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
"assistedDrivingMilestoneState": (True, 10., 1),
|
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||||
"liveLocationKalman": (True, 20.),
|
"liveLocationKalman": (True, 20.),
|
||||||
|
|||||||
@@ -56,28 +56,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "boot",
|
"name": "boot",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||||
"size": 46897152,
|
"size": 46897152,
|
||||||
"sparse": false,
|
"sparse": false,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||||
"size": 4718592000,
|
"size": 4718592000,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": false,
|
"full_check": false,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||||
"alt": {
|
"alt": {
|
||||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||||
"size": 4718592000
|
"size": 4718592000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
import string
|
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -355,7 +354,7 @@ class Modem:
|
|||||||
imei = ""
|
imei = ""
|
||||||
|
|
||||||
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
||||||
if not all(c in string.hexdigits for c in iccid):
|
if not iccid.isdigit():
|
||||||
iccid = ""
|
iccid = ""
|
||||||
|
|
||||||
imsi = first_line("AT+CIMI")
|
imsi = first_line("AT+CIMI")
|
||||||
|
|||||||
@@ -4,17 +4,11 @@ from pathlib import Path
|
|||||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
|
||||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||||
|
|
||||||
|
|
||||||
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
|
|
||||||
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
|
|
||||||
return (vendor_id, product_id) in ids
|
|
||||||
|
|
||||||
|
|
||||||
def get_usb_topology() -> set[str]:
|
def get_usb_topology() -> set[str]:
|
||||||
try:
|
try:
|
||||||
return set(os.listdir(USB_DEVICES_PATH))
|
return set(os.listdir(USB_DEVICES_PATH))
|
||||||
@@ -87,7 +81,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
|||||||
entry.linkErrorCount = device["linkErrorCount"]
|
entry.linkErrorCount = device["linkErrorCount"]
|
||||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||||
|
|
||||||
if is_chestnut_usb_id(entry.vendorId, entry.productId):
|
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||||
chestnut_present = True
|
chestnut_present = True
|
||||||
|
|
||||||
device_state.chestnutPresent = chestnut_present
|
device_state.chestnutPresent = chestnut_present
|
||||||
|
|||||||
@@ -97,10 +97,6 @@ Params::Params(const std::string &path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Params::~Params() {
|
Params::~Params() {
|
||||||
flushNonBlockingWrites();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Params::flushNonBlockingWrites() {
|
|
||||||
if (future.valid()) {
|
if (future.valid()) {
|
||||||
future.wait();
|
future.wait();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ public:
|
|||||||
return put(key.c_str(), val ? "1" : "0", 1);
|
return put(key.c_str(), val ? "1" : "0", 1);
|
||||||
}
|
}
|
||||||
void putNonBlocking(const std::string &key, const std::string &val);
|
void putNonBlocking(const std::string &key, const std::string &val);
|
||||||
void flushNonBlockingWrites();
|
|
||||||
inline void putBoolNonBlocking(const std::string &key, bool val) {
|
inline void putBoolNonBlocking(const std::string &key, bool val) {
|
||||||
putNonBlocking(key, val ? "1" : "0");
|
putNonBlocking(key, val ? "1" : "0");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool],
|
|||||||
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
||||||
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
||||||
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
||||||
params_flush = _bind("params_flush", [ParamsHandle])
|
|
||||||
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
||||||
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
||||||
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
||||||
@@ -179,10 +178,6 @@ class Params:
|
|||||||
def put_bool(self, key, val, block=False):
|
def put_bool(self, key, val, block=False):
|
||||||
params_put_bool(self.p, self.check_key(key), val, block)
|
params_put_bool(self.p, self.check_key(key), val, block)
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""Wait for all prior nonblocking writes from this Params instance."""
|
|
||||||
params_flush(self.p)
|
|
||||||
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
params_remove(self.p, self.check_key(key))
|
params_remove(self.p, self.check_key(key))
|
||||||
|
|
||||||
|
|||||||
@@ -133,12 +133,6 @@ int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool bloc
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void params_flush(ParamsHandle *handle) noexcept {
|
|
||||||
translate_exceptions([&]() {
|
|
||||||
handle->params.flushNonBlockingWrites();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
||||||
return translate_exceptions(-1, [&]() {
|
return translate_exceptions(-1, [&]() {
|
||||||
return handle->params.remove(key);
|
return handle->params.remove(key);
|
||||||
@@ -168,15 +162,12 @@ ParamsBuffer params_key_at(ParamsHandle *handle, size_t index) noexcept {
|
|||||||
|
|
||||||
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
|
size_t params_keys_by_flag(ParamsHandle *handle, uint32_t flag, ParamsBuffer *out, size_t out_size) noexcept {
|
||||||
return translate_exceptions(size_t{0}, [&]() {
|
return translate_exceptions(size_t{0}, [&]() {
|
||||||
size_t count = 0;
|
auto filtered = handle->params.allKeys(static_cast<ParamKeyFlag>(flag));
|
||||||
for (const auto &key : handle->keys) {
|
size_t count = std::min(filtered.size(), out_size);
|
||||||
if (flag == ALL || (handle->params.getKeyFlag(key) & flag)) {
|
for (size_t i = 0; i < count; i++) {
|
||||||
// Each buffer borrows a different string, stable for the handle's lifetime.
|
out[i] = return_string(filtered[i]);
|
||||||
if (count < out_size) out[count] = {key.data(), key.size()};
|
|
||||||
++count;
|
|
||||||
}
|
}
|
||||||
}
|
return filtered.size();
|
||||||
return count;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||||
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||||
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
|
|
||||||
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
|
|
||||||
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
||||||
@@ -138,13 +132,10 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||||
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||||
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||||
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
|
||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
{"AssistedDrivingMilestonesEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
|
||||||
{"AssistedDrivingMilestoneState", {PERSISTENT, JSON, "{}"}},
|
|
||||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
||||||
@@ -165,7 +156,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"FullAssistDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
|
||||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||||
@@ -175,9 +165,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
||||||
{"LastDriveAssistedDrivingSummary", {PERSISTENT, JSON, "{}"}},
|
|
||||||
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"MadsDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
|
||||||
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
||||||
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
||||||
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
@@ -237,8 +225,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
|
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// sunnypilot car specific params
|
// sunnypilot car specific params
|
||||||
{"FordPscmObserver", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"FordModelActionController", {PERSISTENT | BACKUP, BOOL, "0"}},
|
|
||||||
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ public:
|
|||||||
auto param_path = Params().getParamPath();
|
auto param_path = Params().getParamPath();
|
||||||
if (util::file_exists(param_path)) {
|
if (util::file_exists(param_path)) {
|
||||||
std::string real_path = util::readlink(param_path);
|
std::string real_path = util::readlink(param_path);
|
||||||
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
|
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
|
||||||
unlink(param_path.c_str());
|
unlink(param_path.c_str());
|
||||||
}
|
}
|
||||||
if (getenv("COMMA_CACHE") == nullptr) {
|
if (getenv("COMMA_CACHE") == nullptr) {
|
||||||
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
|
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
|
||||||
}
|
}
|
||||||
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
|
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
|
||||||
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
|
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
|
||||||
unsetenv("OPENPILOT_PREFIX");
|
unsetenv("OPENPILOT_PREFIX");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,13 +106,6 @@ class TestParams(OpenpilotTestCase):
|
|||||||
assert q.get("CarParams") is None
|
assert q.get("CarParams") is None
|
||||||
assert q.get("CarParams", True) == b"1"
|
assert q.get("CarParams", True) == b"1"
|
||||||
|
|
||||||
def test_flush_non_blocking_writes(self):
|
|
||||||
self.params.put("DongleId", "first")
|
|
||||||
self.params.put("DongleId", "last")
|
|
||||||
self.params.flush()
|
|
||||||
|
|
||||||
assert self.params.get("DongleId") == "last"
|
|
||||||
|
|
||||||
def test_params_all_keys(self):
|
def test_params_all_keys(self):
|
||||||
keys = Params().all_keys()
|
keys = Params().all_keys()
|
||||||
|
|
||||||
@@ -133,16 +126,6 @@ class TestParams(OpenpilotTestCase):
|
|||||||
assert self.params.get("LiveParametersV2") is None
|
assert self.params.get("LiveParametersV2") is None
|
||||||
assert self.params.get("LiveParametersV2", return_default=True) is None
|
assert self.params.get("LiveParametersV2", return_default=True) is None
|
||||||
|
|
||||||
def test_filtered_keys_are_distinct_registered_strings(self):
|
|
||||||
registered = set(self.params.all_keys())
|
|
||||||
for flag in (ParamKeyFlag.PERSISTENT, ParamKeyFlag.BACKUP, ParamKeyFlag.CLEAR_ON_MANAGER_START):
|
|
||||||
filtered = self.params.all_keys(flag)
|
|
||||||
assert len(filtered) > 1
|
|
||||||
assert len(filtered) == len(set(filtered))
|
|
||||||
assert set(filtered) <= registered
|
|
||||||
assert all(key.decode('utf-8') for key in filtered)
|
|
||||||
assert self.params.all_keys(flag) == filtered
|
|
||||||
|
|
||||||
def test_params_get_type(self):
|
def test_params_get_type(self):
|
||||||
# json
|
# json
|
||||||
self.params.put("ApiCache_FirehoseStats", {"a": 0}, block=True)
|
self.params.put("ApiCache_FirehoseStats", {"a": 0}, block=True)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
|
|
||||||
size 1544
|
|
||||||
Binary file not shown.
@@ -21,7 +21,6 @@ from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
|||||||
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||||
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
from openpilot.selfdrive.car.helpers import convert_carControlSP, convert_to_capnp
|
||||||
from openpilot.selfdrive.car.ford_pscm_status import populate_ford_pscm_status
|
|
||||||
|
|
||||||
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
|
from openpilot.sunnypilot.mads.helpers import set_alternative_experience, set_car_specific_params
|
||||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||||
@@ -199,7 +198,6 @@ class Car:
|
|||||||
# Update carState from CAN
|
# Update carState from CAN
|
||||||
CS, CS_SP = self.CI.update(can_list)
|
CS, CS_SP = self.CI.update(can_list)
|
||||||
CS_SP = convert_to_capnp(CS_SP)
|
CS_SP = convert_to_capnp(CS_SP)
|
||||||
populate_ford_pscm_status(self.CP, self.CI.can_parsers, CS_SP, CS.canValid)
|
|
||||||
|
|
||||||
# Update radar tracks from CAN
|
# Update radar tracks from CAN
|
||||||
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
"""Publish the Ford PSCM's actual CAN status without changing opendbc structs."""
|
|
||||||
import math
|
|
||||||
|
|
||||||
from opendbc.car import Bus
|
|
||||||
from opendbc.car.ford.values import FordFlags
|
|
||||||
|
|
||||||
|
|
||||||
MESSAGE = 'Lane_Assist_Data3_FD1'
|
|
||||||
SIGNALS = ('LatCtlSte_D_Stat', 'LatCtlLim_D_Stat', 'LatCtlCpblty_D_Stat', 'LaActDeny_B_Actl')
|
|
||||||
|
|
||||||
|
|
||||||
def populate_ford_pscm_status(CP, can_parsers, CS_SP, can_valid):
|
|
||||||
if CP.brand != 'ford' or not CP.flags & FordFlags.CANFD:
|
|
||||||
return
|
|
||||||
status = CS_SP.init('fordPscmStatus')
|
|
||||||
parser = can_parsers.get(Bus.pt)
|
|
||||||
if parser is None:
|
|
||||||
return
|
|
||||||
values = parser.vl.get(MESSAGE, {})
|
|
||||||
timestamps = parser.ts_nanos.get(MESSAGE, {})
|
|
||||||
if any(signal not in values or signal not in timestamps for signal in SIGNALS):
|
|
||||||
return
|
|
||||||
received = timestamps[SIGNALS[0]]
|
|
||||||
if received <= 0 or any(timestamps[signal] != received for signal in SIGNALS):
|
|
||||||
return
|
|
||||||
decoded = [values[signal] for signal in SIGNALS]
|
|
||||||
if any(not math.isfinite(value) or int(value) != value or not 0 <= value <= maximum
|
|
||||||
for value, maximum in zip(decoded, (7, 3, 3, 1), strict=True)):
|
|
||||||
return
|
|
||||||
status.canMonoTime = received
|
|
||||||
status.lateralState, status.limit, status.capability = map(int, decoded[:3])
|
|
||||||
status.denied = bool(decoded[3])
|
|
||||||
# CI.update already checked all parser validity. Reading can_valid again here
|
|
||||||
# would advance the parser's invalid-message counter a second time per tick.
|
|
||||||
# Age is evaluated by the feedback consumer using this original CAN timestamp.
|
|
||||||
status.valid = bool(can_valid)
|
|
||||||
@@ -63,6 +63,5 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
|
|||||||
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
||||||
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
|
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
|
||||||
)
|
)
|
||||||
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
|
|
||||||
|
|
||||||
return struct_dataclass
|
return struct_dataclass
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import ast
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.selfdrive.car.ford_pscm_status import MESSAGE, SIGNALS, populate_ford_pscm_status
|
|
||||||
from openpilot.selfdrive.car.helpers import convert_to_capnp
|
|
||||||
from opendbc.can import CANPacker, CANParser
|
|
||||||
from opendbc.car import Bus, structs
|
|
||||||
from opendbc.car.ford.values import FordFlags
|
|
||||||
|
|
||||||
|
|
||||||
class TestFordPscmStatus(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.cp = SimpleNamespace(brand='ford', flags=FordFlags.CANFD)
|
|
||||||
self.packer = CANPacker('ford_lincoln_base_pt')
|
|
||||||
self.parser = CANParser('ford_lincoln_base_pt', [(MESSAGE, 33), ('Yaw_Data_FD1', 100)], 0)
|
|
||||||
|
|
||||||
def update_status(self, timestamp, *, lateral_state=2, limit=0, capability=2, denied=False):
|
|
||||||
status = self.packer.make_can_msg(MESSAGE, 0, dict(zip(SIGNALS, (lateral_state, limit, capability, denied), strict=True)))
|
|
||||||
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 0.1})
|
|
||||||
self.parser.update([(timestamp, [status, yaw])])
|
|
||||||
|
|
||||||
def publish(self, *, can_valid=True):
|
|
||||||
state_sp = convert_to_capnp(structs.CarStateSP(speedLimit=13.5))
|
|
||||||
populate_ford_pscm_status(self.cp, {Bus.pt: self.parser}, state_sp, can_valid)
|
|
||||||
return state_sp
|
|
||||||
|
|
||||||
def test_decodes_status_and_preserves_receipt_time_across_other_can_messages(self):
|
|
||||||
self.update_status(1_000_000_000, limit=2, capability=1, denied=True)
|
|
||||||
original = self.publish()
|
|
||||||
self.assertEqual(original.speedLimit, 13.5)
|
|
||||||
status = original.fordPscmStatus
|
|
||||||
self.assertTrue(status.valid)
|
|
||||||
self.assertEqual(status.canMonoTime, 1_000_000_000)
|
|
||||||
self.assertEqual((status.lateralState, status.limit, status.capability, status.denied), (2, 2, 1, True))
|
|
||||||
|
|
||||||
# carStateSP may publish at 100 Hz while this 33 Hz message is absent. New
|
|
||||||
# unrelated CAN must not freshen the timestamp of an old PSCM status.
|
|
||||||
yaw = self.packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': .2})
|
|
||||||
self.parser.update([(1_080_000_000, [yaw])])
|
|
||||||
copied = self.publish().fordPscmStatus
|
|
||||||
self.assertEqual(copied.canMonoTime, 1_000_000_000)
|
|
||||||
self.assertEqual((copied.limit, copied.capability, copied.denied), (2, 1, True))
|
|
||||||
|
|
||||||
self.update_status(1_090_000_000, lateral_state=3, limit=3, capability=2)
|
|
||||||
next_state = self.publish()
|
|
||||||
with custom.CarStateSP.from_bytes(next_state.to_bytes()) as decoded:
|
|
||||||
latest = decoded.fordPscmStatus
|
|
||||||
self.assertTrue(latest.valid)
|
|
||||||
self.assertEqual(latest.canMonoTime, 1_090_000_000)
|
|
||||||
self.assertEqual((latest.lateralState, latest.limit, latest.capability, latest.denied), (3, 3, 2, False))
|
|
||||||
|
|
||||||
def test_absent_parser_unseen_message_and_invalid_can_do_not_claim_valid_status(self):
|
|
||||||
state = custom.CarStateSP.new_message()
|
|
||||||
populate_ford_pscm_status(self.cp, {}, state, True)
|
|
||||||
self.assertFalse(state.fordPscmStatus.valid)
|
|
||||||
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
|
|
||||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
|
||||||
self.update_status(1_000_000_000)
|
|
||||||
invalid = self.publish(can_valid=False).fordPscmStatus
|
|
||||||
self.assertFalse(invalid.valid)
|
|
||||||
self.assertEqual(invalid.canMonoTime, 1_000_000_000)
|
|
||||||
|
|
||||||
def test_mixed_timestamps_or_malformed_status_cannot_enable_feedback(self):
|
|
||||||
self.update_status(1_000_000_000)
|
|
||||||
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 990_000_000
|
|
||||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
|
||||||
self.parser.ts_nanos[MESSAGE][SIGNALS[-1]] = 1_000_000_000
|
|
||||||
for value in (float('nan'), -1, 1.5, 4):
|
|
||||||
self.parser.vl[MESSAGE]['LatCtlLim_D_Stat'] = value
|
|
||||||
self.assertFalse(self.publish().fordPscmStatus.valid)
|
|
||||||
|
|
||||||
def test_other_vehicles_and_legacy_messages_default_to_unavailable(self):
|
|
||||||
for cp in (SimpleNamespace(brand='toyota'), SimpleNamespace(brand='ford', flags=0)):
|
|
||||||
state = custom.CarStateSP.new_message(speedLimit=10.)
|
|
||||||
populate_ford_pscm_status(cp, {}, state, True)
|
|
||||||
self.assertFalse(state.fordPscmStatus.valid)
|
|
||||||
self.assertEqual(state.fordPscmStatus.canMonoTime, 0)
|
|
||||||
self.assertEqual(state.speedLimit, 10.)
|
|
||||||
# Old recordings/readers have no appended status pointer; defaults must
|
|
||||||
# remain unavailable rather than interpreting zeroed enums as fresh data.
|
|
||||||
self.assertFalse(custom.CarStateSP.new_message().fordPscmStatus.valid)
|
|
||||||
|
|
||||||
def test_actual_card_update_populates_status_after_dataclass_conversion(self):
|
|
||||||
self.update_status(1_000_000_000, limit=1)
|
|
||||||
source_path = Path(__file__).resolve().parents[1] / 'card.py'
|
|
||||||
source = ast.parse(source_path.read_text())
|
|
||||||
car_class = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == 'Car')
|
|
||||||
method = next(n for n in car_class.body if isinstance(n, ast.FunctionDef) and n.name == 'state_update')
|
|
||||||
statements = method.body
|
|
||||||
first = next(i for i, n in enumerate(statements) if isinstance(n, ast.Assign) and ast.unparse(n.value) == 'self.CI.update(can_list)')
|
|
||||||
last = next(i for i, n in enumerate(statements) if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call)
|
|
||||||
and isinstance(n.value.func, ast.Name) and n.value.func.id == 'populate_ford_pscm_status')
|
|
||||||
self.assertGreater(last, first)
|
|
||||||
code = compile(ast.Module(body=statements[first:last + 1], type_ignores=[]), str(source_path), 'exec')
|
|
||||||
ci = SimpleNamespace(update=lambda _: (SimpleNamespace(canValid=True), structs.CarStateSP(speedLimit=11.)),
|
|
||||||
can_parsers={Bus.pt: self.parser})
|
|
||||||
environment = {'self': SimpleNamespace(CP=self.cp, CI=ci), 'can_list': [], 'convert_to_capnp': convert_to_capnp,
|
|
||||||
'populate_ford_pscm_status': populate_ford_pscm_status}
|
|
||||||
exec(code, environment)
|
|
||||||
self.assertTrue(environment['CS_SP'].fordPscmStatus.valid)
|
|
||||||
self.assertEqual(environment['CS_SP'].fordPscmStatus.canMonoTime, 1_000_000_000)
|
|
||||||
self.assertEqual(environment['CS_SP'].fordPscmStatus.limit, 1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import math
|
import math
|
||||||
import time
|
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
|
||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
@@ -12,11 +11,8 @@ from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
from opendbc.car.car_helpers import interfaces
|
from opendbc.car.car_helpers import interfaces
|
||||||
from opendbc.car.ford.values import FordFlags
|
|
||||||
from opendbc.car.vehicle_model import VehicleModel
|
from opendbc.car.vehicle_model import VehicleModel
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController
|
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
||||||
@@ -48,7 +44,7 @@ class Controls(ControlsExt):
|
|||||||
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP)
|
||||||
|
|
||||||
self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
|
self.sm = messaging.SubMaster(['lateralDelay', 'vehicleParameters', 'lateralTorqueParameters', 'modelV2', 'selfdriveState',
|
||||||
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carStateSP', 'carOutput',
|
'extrinsicsCalibration', 'deviceMotion', 'longitudinalPlan', 'lateralManeuverPlan', 'carState', 'carOutput',
|
||||||
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
|
'driverMonitoringState', 'onroadEvents', 'driverAssistance'] + self.sm_services_ext,
|
||||||
poll='selfdriveState')
|
poll='selfdriveState')
|
||||||
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
|
self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext)
|
||||||
@@ -56,15 +52,6 @@ class Controls(ControlsExt):
|
|||||||
self.steer_limited_by_safety = False
|
self.steer_limited_by_safety = False
|
||||||
self.curvature = 0.0
|
self.curvature = 0.0
|
||||||
self.desired_curvature = 0.0
|
self.desired_curvature = 0.0
|
||||||
self.ford_pscm_observer = (self.CP.brand == "ford" and self.CP.flags & FordFlags.CANFD and
|
|
||||||
self.params.get_bool("FordPscmObserver"))
|
|
||||||
self.ford_path_controller = FordPscmObserverPathController() if self.ford_pscm_observer else FordPathController()
|
|
||||||
self.ford_path_controller = select_model_action_controller(self.CP, self.params.get_bool("FordModelActionController"),
|
|
||||||
self.ford_path_controller)
|
|
||||||
self.ford_model_action = isinstance(self.ford_path_controller, FordModelActionController)
|
|
||||||
if self.CP.brand == "ford":
|
|
||||||
cloudlog.event("Ford path controller selected", controller=type(self.ford_path_controller).__name__)
|
|
||||||
self.ford_path = FordPath()
|
|
||||||
|
|
||||||
self.pose_calibrator = PoseCalibrator()
|
self.pose_calibrator = PoseCalibrator()
|
||||||
self.calibrated_pose: Pose | None = None
|
self.calibrated_pose: Pose | None = None
|
||||||
@@ -168,36 +155,6 @@ class Controls(ControlsExt):
|
|||||||
actuators.curvature = float(lateral_output)
|
actuators.curvature = float(lateral_output)
|
||||||
else:
|
else:
|
||||||
actuators.steeringAngleDeg = float(lateral_output)
|
actuators.steeringAngleDeg = float(lateral_output)
|
||||||
if self.CP.brand == "ford":
|
|
||||||
ford_model = model_v2 if self.sm.valid['modelV2'] else None
|
|
||||||
if self.ford_model_action:
|
|
||||||
reference_service = 'lateralManeuverPlan' if self.sm.valid['lateralManeuverPlan'] else 'modelV2'
|
|
||||||
self.ford_path = self.ford_path_controller.update(
|
|
||||||
ford_model, self.desired_curvature, current_curvature=self.curvature, yaw_rate=-CS.yawRate, speed=CS.vEgo, now=time.monotonic(),
|
|
||||||
measurement_time=self.sm.logMonoTime['carState'] * 1e-9,
|
|
||||||
model_time=self.sm.logMonoTime['modelV2'] * 1e-9,
|
|
||||||
reference_time=self.sm.logMonoTime[reference_service] * 1e-9,
|
|
||||||
active=CC.latActive, valid=CS.canValid and self.sm.all_checks(['carState', 'vehicleParameters', 'modelV2', reference_service]),
|
|
||||||
driver_pressed=CS.steeringPressed, driver_torque=CS.steeringTorque,
|
|
||||||
pscm_status=self.sm['carStateSP'].fordPscmStatus if self.sm.valid['carStateSP'] else None,
|
|
||||||
)
|
|
||||||
if not self.ford_path.valid:
|
|
||||||
CC.latActive = False
|
|
||||||
if self.sm.frame % 20 == 0:
|
|
||||||
cloudlog.event("Ford C2-free path tracking", model_mono_time=self.sm.logMonoTime['modelV2'],
|
|
||||||
measurement_mono_time=self.sm.logMonoTime['carState'],
|
|
||||||
reference_service=reference_service, reference_mono_time=self.sm.logMonoTime[reference_service],
|
|
||||||
measured_curvature=self.curvature,
|
|
||||||
**self.ford_path_controller.diagnostics)
|
|
||||||
elif self.ford_pscm_observer:
|
|
||||||
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
|
|
||||||
current_curvature=self.curvature, v_ego=CS.vEgo,
|
|
||||||
v_ego_raw=CS.vEgoRaw, active=CC.latActive)
|
|
||||||
else:
|
|
||||||
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
|
|
||||||
current_curvature=self.curvature, v_ego=CS.vEgo,
|
|
||||||
active=CC.latActive)
|
|
||||||
actuators.curvature = float(self.ford_path.curvature)
|
|
||||||
# Ensure no NaNs/Infs
|
# Ensure no NaNs/Infs
|
||||||
for p in ACTUATOR_FIELDS:
|
for p in ACTUATOR_FIELDS:
|
||||||
attr = getattr(actuators, p)
|
attr = getattr(actuators, p)
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
"""Experimental Ford C2-free controller with measured-curvature C1 feedback.
|
|
||||||
|
|
||||||
Selected only by its explicit toggle. The 7 m station and one-second scale are
|
|
||||||
engineering choices. Feeding integrated heading mismatch into C1 at 1:1 is an
|
|
||||||
explicit feedback-strength choice, not an identified PSCM model or calibration.
|
|
||||||
Opposed correction may be released when both path commands confirm the turn.
|
|
||||||
"""
|
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from opendbc.car.ford.values import CarControllerParams, FordFlags
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath, _model_path
|
|
||||||
|
|
||||||
|
|
||||||
OFFSET_STATION_M = 7.0
|
|
||||||
HEADING_TIME_S = 1.0
|
|
||||||
CALIBRATION_APPROVED = False
|
|
||||||
|
|
||||||
|
|
||||||
def _packed(value, resolution, offset):
|
|
||||||
"""Mirror Float32 carControlSP and sign-reversed CANPacker rounding."""
|
|
||||||
value = struct.unpack("f", struct.pack("f", value))[0]
|
|
||||||
return -(math.floor((-value - offset) / resolution + 0.5) * resolution + offset)
|
|
||||||
|
|
||||||
|
|
||||||
def _finite(*values):
|
|
||||||
try:
|
|
||||||
return all(math.isfinite(value) for value in values)
|
|
||||||
except (TypeError, ValueError, OverflowError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def encode_model_action(model, desired_curvature, speed):
|
|
||||||
"""Encode y(7) and max(7, v*1s)*selected limited curvature.
|
|
||||||
|
|
||||||
Preserve the reviewed core's endpoint hold when the path ends before 7 m.
|
|
||||||
This samples the available geometry; it does not extrapolate an unseen path.
|
|
||||||
"""
|
|
||||||
if not _finite(desired_curvature, speed) or not .3 <= speed <= 55 or abs(desired_curvature) > 1:
|
|
||||||
return FordPath()
|
|
||||||
try:
|
|
||||||
path = _model_path(model)
|
|
||||||
except OverflowError:
|
|
||||||
return FordPath()
|
|
||||||
if path is None or not all(_finite(*values) for values in path):
|
|
||||||
return FordPath()
|
|
||||||
station, _, lateral, _ = path
|
|
||||||
c0 = float(np.interp(min(OFFSET_STATION_M, station[-1]), station, lateral))
|
|
||||||
c1 = max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature
|
|
||||||
return FordPath(True, c0, c1, 0., 0.) if _finite(c0, c1) else FordPath()
|
|
||||||
|
|
||||||
|
|
||||||
class ModelActionController:
|
|
||||||
"""Unquantized C0/C1 slew positions and one C1 feedback correction.
|
|
||||||
|
|
||||||
Feedback integrates requested minus measured curvature over traveled distance.
|
|
||||||
Freshness, measurement cadence and driver/PSCM arbitration belong to the caller.
|
|
||||||
"""
|
|
||||||
__slots__ = ('c0', 'c1', 'correction', 'carryover_release_count')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.c0 = self.c1 = self.correction = 0.
|
|
||||||
self.carryover_release_count = 0 # Diagnostic only; never feeds the command law.
|
|
||||||
|
|
||||||
def update(self, model, desired_curvature, *, current_curvature, speed, dt, active=True, valid=True,
|
|
||||||
feedback_dt=None, feedback_enabled=True, pscm_limited=False):
|
|
||||||
feedback_dt = dt if feedback_dt is None else feedback_dt
|
|
||||||
if (not active or not valid or not _finite(dt, feedback_dt, current_curvature) or not .002 <= dt <= .1
|
|
||||||
or not 0. <= feedback_dt <= .15 or abs(current_curvature) > 1.):
|
|
||||||
self.reset()
|
|
||||||
return FordPath()
|
|
||||||
target = encode_model_action(model, desired_curvature, speed)
|
|
||||||
if not target.valid:
|
|
||||||
self.reset()
|
|
||||||
return FordPath()
|
|
||||||
c0 = float(np.clip(target.path_offset, -5.11, 5.11))
|
|
||||||
self.c0 += float(np.clip(c0-self.c0, -4.*dt, 4.*dt))
|
|
||||||
base_c1 = float(np.clip(target.path_angle, -.5, .5))
|
|
||||||
lower = max(-.5, self.c1-.5*dt)
|
|
||||||
upper = min(.5, self.c1+.5*dt)
|
|
||||||
if not feedback_enabled:
|
|
||||||
self.correction = 0.
|
|
||||||
else:
|
|
||||||
direction = math.copysign(1., base_c1)
|
|
||||||
# Release only correction that prevents C1 from requesting the direction
|
|
||||||
# shared by target C0, slewed C0 and base C1, while measured steering is
|
|
||||||
# still opposite. One DBC step confirms each request is nonzero. Matched
|
|
||||||
# steering, neutral/conflicting centering and duplicate samples retain I.
|
|
||||||
if (feedback_dt > 0. and abs(base_c1) >= .0005 and current_curvature*direction < 0.
|
|
||||||
and min(c0*direction, self.c0*direction) >= .01
|
|
||||||
and (base_c1+self.correction)*direction <= 0.):
|
|
||||||
self.correction = 0.
|
|
||||||
self.carryover_release_count += 1
|
|
||||||
increment = (desired_curvature-current_curvature)*speed*feedback_dt
|
|
||||||
# LimitReached inhibits only extra demand in the measured turn direction.
|
|
||||||
# Opposing correction and changes to the model request remain available.
|
|
||||||
direction = current_curvature if current_curvature else self.c1
|
|
||||||
if pscm_limited and increment*direction > 0.:
|
|
||||||
# An old opposing correction may return to zero; don't trap it below
|
|
||||||
# the base request just because the PSCM now reports a limit.
|
|
||||||
increment = float(np.clip(increment, min(-self.correction, 0.), max(-self.correction, 0.)))
|
|
||||||
# Integrate only as far as this cycle's amplitude/slew envelope permits.
|
|
||||||
# If the base moved outside that envelope, allow increments toward it;
|
|
||||||
# never rewrite existing correction merely because the base changed.
|
|
||||||
request = base_c1+self.correction
|
|
||||||
self.correction += float(np.clip(increment, min(lower-request, 0.), max(upper-request, 0.)))
|
|
||||||
c1 = float(np.clip(base_c1+self.correction, -.5, .5))
|
|
||||||
self.c1 += float(np.clip(c1-self.c1, -.5*dt, .5*dt))
|
|
||||||
return FordPath(True, _packed(self.c0, .01, -5.12), _packed(self.c1, .0005, -.5), 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
class FordModelActionController:
|
|
||||||
"""Input adapter for the opt-in selected-action controller.
|
|
||||||
|
|
||||||
controlsd owns upstream selection/limiting and service health. This adapter
|
|
||||||
checks ages and clock order, then supplies elapsed time to the three-state
|
|
||||||
core. Feedback advances once per fresh steering measurement; repeated samples
|
|
||||||
can still advance output slew. Raw model geometry is checked on every cycle.
|
|
||||||
|
|
||||||
CAN yaw remains a health gate, not the feedback measurement. Driver override
|
|
||||||
clears the correction. Fresh PSCM limits only inhibit outward integration;
|
|
||||||
neither a limit nor a repeated measurement freezes the model request.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.core = ModelActionController()
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self, status='inactive'):
|
|
||||||
self.core.reset()
|
|
||||||
self.last_time = self.last_measurement_time = self.last_model_time = None
|
|
||||||
self.diagnostics = {'status': status, 'hypothesis': 'model-action-c1-feedback-v2',
|
|
||||||
'calibration_approved': CALIBRATION_APPROVED, 'command': (0., 0., 0., 0.)}
|
|
||||||
|
|
||||||
def update(self, model, desired_curvature, *, current_curvature, yaw_rate, speed, now, measurement_time, model_time,
|
|
||||||
reference_time, active, valid=True, driver_pressed=False, driver_torque=0., pscm_status=None):
|
|
||||||
reason = None
|
|
||||||
if not active:
|
|
||||||
reason = 'inactive'
|
|
||||||
elif not valid:
|
|
||||||
reason = 'invalid_service'
|
|
||||||
elif not _finite(desired_curvature, current_curvature, yaw_rate, speed, now, measurement_time, model_time, reference_time):
|
|
||||||
reason = 'nonfinite'
|
|
||||||
elif not all(-.005 <= now - timestamp <= .15 for timestamp in (measurement_time, model_time, reference_time)):
|
|
||||||
reason = 'stale_input'
|
|
||||||
elif not .3 <= speed <= 55 or abs(yaw_rate) > 3 or abs(desired_curvature) > 1 or abs(current_curvature) > 1:
|
|
||||||
reason = 'input_range'
|
|
||||||
if reason is not None:
|
|
||||||
self.reset(reason)
|
|
||||||
return FordPath()
|
|
||||||
|
|
||||||
dt = .01 if self.last_time is None else now - self.last_time
|
|
||||||
feedback_dt = 0. if self.last_measurement_time is None else measurement_time-self.last_measurement_time
|
|
||||||
if not .002 <= dt <= .1 or not 0. <= feedback_dt <= .15 or (
|
|
||||||
self.last_model_time is not None and model_time < self.last_model_time
|
|
||||||
):
|
|
||||||
self.reset('timing_reset')
|
|
||||||
return FordPath()
|
|
||||||
status_fresh = (pscm_status is not None and pscm_status.valid and pscm_status.canMonoTime > 0
|
|
||||||
and -.005 <= now-pscm_status.canMonoTime*1e-9 <= .15)
|
|
||||||
pscm_limited = bool(status_fresh and pscm_status.limit == 2)
|
|
||||||
driver_override = bool(driver_pressed or not _finite(driver_torque)
|
|
||||||
or abs(driver_torque) > CarControllerParams.STEER_DRIVER_ALLOWANCE
|
|
||||||
or (status_fresh and pscm_status.limit == 3))
|
|
||||||
feedback_enabled = not (driver_override or (status_fresh and (pscm_status.denied or pscm_status.lateralState != 2)))
|
|
||||||
command = self.core.update(model, desired_curvature, current_curvature=current_curvature, speed=speed, dt=dt,
|
|
||||||
feedback_dt=feedback_dt, feedback_enabled=feedback_enabled, pscm_limited=pscm_limited)
|
|
||||||
if not command.valid:
|
|
||||||
self.reset('invalid_path')
|
|
||||||
return command
|
|
||||||
self.last_time, self.last_measurement_time, self.last_model_time = now, measurement_time, model_time
|
|
||||||
self.diagnostics = {'status': 'active', 'hypothesis': 'model-action-c1-feedback-v2',
|
|
||||||
'calibration_approved': CALIBRATION_APPROVED, 'desired_curvature': desired_curvature,
|
|
||||||
'model_age': now - model_time, 'measurement_age': now - measurement_time, 'reference_age': now - reference_time,
|
|
||||||
'dt': dt, 'offset_request': self.core.c0, 'heading_request': self.core.c1,
|
|
||||||
'curvature_error': desired_curvature-current_curvature, 'feedback_dt': feedback_dt,
|
|
||||||
'heading_feedforward': float(np.clip(max(OFFSET_STATION_M, speed*HEADING_TIME_S)*desired_curvature, -.5, .5)),
|
|
||||||
'heading_correction': self.core.correction, 'feedback_enabled': feedback_enabled,
|
|
||||||
'carryover_release_count': self.core.carryover_release_count,
|
|
||||||
'driver_override': driver_override, 'pscm_limited': pscm_limited, 'pscm_status_fresh': bool(status_fresh),
|
|
||||||
'command': (command.path_offset, command.path_angle, 0., 0.)}
|
|
||||||
return command
|
|
||||||
|
|
||||||
|
|
||||||
def select_model_action_controller(CP, enabled, previous_controller):
|
|
||||||
"""The separate default-off toggle takes priority on any Ford CAN FD vehicle."""
|
|
||||||
compatible = CP.brand == 'ford' and CP.flags & FordFlags.CANFD
|
|
||||||
if enabled and compatible:
|
|
||||||
return FordModelActionController()
|
|
||||||
return previous_controller
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
from collections import deque
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import math
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from opendbc.car.ford.values import CarControllerParams
|
|
||||||
|
|
||||||
|
|
||||||
DBC_OFFSET = (-5.12, 5.11)
|
|
||||||
DBC_ANGLE = (-0.5, 0.5235)
|
|
||||||
DBC_CURVATURE = (-0.02, 0.02)
|
|
||||||
DBC_CURVATURE_RATE = (-0.001024, 0.001023)
|
|
||||||
|
|
||||||
DBC_OFFSET_RESOLUTION = 0.01
|
|
||||||
DBC_ANGLE_RESOLUTION = 0.0005
|
|
||||||
DBC_CURVATURE_RESOLUTION = 0.00002
|
|
||||||
DBC_CURVATURE_RATE_RESOLUTION = 0.000001
|
|
||||||
_PATH_MIN_LOOKAHEAD = 7.0
|
|
||||||
_POSE_PREDICTION_TIME = 0.1
|
|
||||||
_POSE_BLEND_CURVATURE = (0.006, 0.012)
|
|
||||||
_PATH_OFFSET_RATE = 4.0
|
|
||||||
_PATH_ANGLE_RATE = 1.0
|
|
||||||
|
|
||||||
_PSCM_DT = 0.004
|
|
||||||
_PSCM_C0_RATE = 1.5
|
|
||||||
_PSCM_C1_RATE = 0.100006103515625
|
|
||||||
_PSCM_C2_RATE = 0.0030059814453125
|
|
||||||
_PSCM_SPEED_KPH = (0.0, 15.0, 40.0, 70.0, 100.0, 150.0, 200.0, 250.0)
|
|
||||||
_PSCM_SPEED_GAIN = (32.0, 32.0, 32.0, 30.0, 30.0, 24.0, 12.0, 0.0)
|
|
||||||
_PSCM_C0_EFFECTIVE_LIMIT = 1.0
|
|
||||||
_PSCM_C1_EFFECTIVE_LIMIT = 0.349609375 / 10.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class FordPath:
|
|
||||||
valid: bool = False
|
|
||||||
path_offset: float = 0.0
|
|
||||||
path_angle: float = 0.0
|
|
||||||
curvature: float = 0.0
|
|
||||||
curvature_rate: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class FordPscmState:
|
|
||||||
path_offset: float = 0.0
|
|
||||||
path_angle: float = 0.0
|
|
||||||
curvature: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class FordModelPose:
|
|
||||||
path_offset: float
|
|
||||||
path_angle: float
|
|
||||||
offset_horizon: float
|
|
||||||
curvature_demand: float
|
|
||||||
forward_angle: float
|
|
||||||
|
|
||||||
|
|
||||||
def _finite(value: float) -> float:
|
|
||||||
return float(value) if math.isfinite(value) else 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _sample(distance: float, distances: list[float], values: list[float]) -> float:
|
|
||||||
return float(np.interp(distance, distances, values))
|
|
||||||
|
|
||||||
|
|
||||||
def _blend_share(demand: float) -> float:
|
|
||||||
lower, upper = _POSE_BLEND_CURVATURE
|
|
||||||
return float(np.clip((demand - lower) / (upper - lower), 0.0, 1.0))
|
|
||||||
|
|
||||||
|
|
||||||
def _model_path(model) -> tuple[list[float], list[float], list[float], list[float]] | None:
|
|
||||||
try:
|
|
||||||
x = [float(value) for value in model.position.x]
|
|
||||||
y = [float(value) for value in model.position.y]
|
|
||||||
heading = [float(value) for value in model.orientation.z]
|
|
||||||
except (AttributeError, TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
if len(x) < 2 or len(x) != len(y) or len(x) != len(heading):
|
|
||||||
return None
|
|
||||||
if not all(math.isfinite(value) for values in (x, y, heading) for value in values):
|
|
||||||
return None
|
|
||||||
|
|
||||||
distance = [0.0]
|
|
||||||
for i in range(1, len(x)):
|
|
||||||
distance.append(distance[-1] + math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]))
|
|
||||||
if distance[-1] <= 0.0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
unwrapped_heading = [heading[0]]
|
|
||||||
for value in heading[1:]:
|
|
||||||
delta = (value - unwrapped_heading[-1] + math.pi) % (2.0 * math.pi) - math.pi
|
|
||||||
unwrapped_heading.append(unwrapped_heading[-1] + delta)
|
|
||||||
return distance, x, y, unwrapped_heading
|
|
||||||
|
|
||||||
|
|
||||||
def _predicted_pose(distance: float, current_curvature: float,
|
|
||||||
curvature_delta: float) -> tuple[float, float, float]:
|
|
||||||
curvature = current_curvature + 0.5 * curvature_delta
|
|
||||||
heading = curvature * distance
|
|
||||||
if abs(curvature) < 1e-9:
|
|
||||||
return distance, 0.0, 0.0
|
|
||||||
return math.sin(heading) / curvature, (1.0 - math.cos(heading)) / curvature, heading
|
|
||||||
|
|
||||||
|
|
||||||
def _relative_pose(target_distance: float, path: tuple[list[float], list[float], list[float], list[float]],
|
|
||||||
vehicle_pose: tuple[float, float, float]) -> tuple[float, float]:
|
|
||||||
distance, x, y, heading = path
|
|
||||||
vehicle_x, vehicle_y, vehicle_heading = vehicle_pose
|
|
||||||
dx = _sample(target_distance, distance, x) - vehicle_x
|
|
||||||
dy = _sample(target_distance, distance, y) - vehicle_y
|
|
||||||
cosine = math.cos(vehicle_heading)
|
|
||||||
sine = math.sin(vehicle_heading)
|
|
||||||
offset = -sine * dx + cosine * dy
|
|
||||||
angle = math.atan2(math.sin(_sample(target_distance, distance, heading) - vehicle_heading),
|
|
||||||
math.cos(_sample(target_distance, distance, heading) - vehicle_heading))
|
|
||||||
return offset, angle
|
|
||||||
|
|
||||||
|
|
||||||
def _path_pose(target_distance: float,
|
|
||||||
path: tuple[list[float], list[float], list[float], list[float]]) -> tuple[float, float, float]:
|
|
||||||
distance, x, y, heading = path
|
|
||||||
return (_sample(target_distance, distance, x), _sample(target_distance, distance, y),
|
|
||||||
_sample(target_distance, distance, heading))
|
|
||||||
|
|
||||||
|
|
||||||
def _bounded_feedback(feedforward: float, feedback: float, resolution: float, zero_path_limit: float) -> float:
|
|
||||||
quantization_threshold = 0.5 * resolution
|
|
||||||
limit = max(abs(feedforward) - resolution, 0.0) if abs(feedforward) >= quantization_threshold else zero_path_limit
|
|
||||||
return float(np.clip(feedback, -limit, limit))
|
|
||||||
|
|
||||||
|
|
||||||
def _model_pose(path: tuple[list[float], list[float], list[float], list[float]],
|
|
||||||
current_curvature: float, curvature_delta: float, v_ego: float) -> FordModelPose:
|
|
||||||
distance, _, _, _ = path
|
|
||||||
advance = min(v_ego * _POSE_PREDICTION_TIME, distance[-1])
|
|
||||||
offset_horizon = min(_PATH_MIN_LOOKAHEAD, distance[-1] - advance)
|
|
||||||
angle_horizon = min(max(v_ego, _PATH_MIN_LOOKAHEAD), distance[-1] - advance)
|
|
||||||
|
|
||||||
# Keep the model's remaining path as feedforward. Measured vehicle motion is
|
|
||||||
# a separate, short delay-aligned correction, so catching the requested
|
|
||||||
# curvature cannot erase a turn that is still present in the model path.
|
|
||||||
model_pose = _path_pose(advance, path)
|
|
||||||
model_offset, _ = _relative_pose(advance + offset_horizon, path, model_pose)
|
|
||||||
_, model_angle = _relative_pose(advance + angle_horizon, path, model_pose)
|
|
||||||
vehicle_pose = _predicted_pose(advance, current_curvature, curvature_delta)
|
|
||||||
feedback_offset, feedback_angle = _relative_pose(advance, path, vehicle_pose)
|
|
||||||
gentle_curvature = _POSE_BLEND_CURVATURE[0]
|
|
||||||
feedback_offset = _bounded_feedback(model_offset, feedback_offset, DBC_OFFSET_RESOLUTION,
|
|
||||||
0.5 * gentle_curvature * advance ** 2)
|
|
||||||
feedback_angle = _bounded_feedback(model_angle, feedback_angle, DBC_ANGLE_RESOLUTION,
|
|
||||||
gentle_curvature * advance)
|
|
||||||
|
|
||||||
offset_curvature = 2.0 * model_offset / max(offset_horizon, 1e-3) ** 2
|
|
||||||
angle_curvature = model_angle / max(angle_horizon, 1e-3)
|
|
||||||
return FordModelPose(model_offset + feedback_offset, model_angle + feedback_angle, offset_horizon,
|
|
||||||
max(abs(offset_curvature), abs(angle_curvature)), model_angle)
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_pose(pose: FordModelPose, pose_share: float, curvature: float) -> FordPath:
|
|
||||||
path_offset = pose_share * pose.path_offset
|
|
||||||
path_angle = pose_share * pose.path_angle
|
|
||||||
if abs(path_offset) < 0.5 * DBC_OFFSET_RESOLUTION:
|
|
||||||
path_offset = 0.0
|
|
||||||
if abs(path_angle) < 0.5 * DBC_ANGLE_RESOLUTION:
|
|
||||||
path_angle = 0.0
|
|
||||||
limited_path_angle = float(np.clip(path_angle, *DBC_ANGLE))
|
|
||||||
path_offset += (path_angle - limited_path_angle) * pose.offset_horizon
|
|
||||||
return FordPath(
|
|
||||||
valid=True,
|
|
||||||
path_offset=float(np.clip(path_offset, *DBC_OFFSET)),
|
|
||||||
path_angle=limited_path_angle,
|
|
||||||
curvature=float(np.clip(curvature, *DBC_CURVATURE)),
|
|
||||||
curvature_rate=0.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _encode_path(path: tuple[list[float], list[float], list[float], list[float]], desired_curvature: float,
|
|
||||||
current_curvature: float, curvature_delta: float, v_ego: float) -> FordPath:
|
|
||||||
pose = _model_pose(path, current_curvature, curvature_delta, v_ego)
|
|
||||||
pose_share = _blend_share(max(pose.curvature_demand, abs(desired_curvature)))
|
|
||||||
|
|
||||||
# Match upstream's C2-only normal driving, then continuously transfer the
|
|
||||||
# command to the model pose for larger maneuvers. An opposing/finished model
|
|
||||||
# path must unload sticky C2 and retain the fast pose needed to unwind it.
|
|
||||||
c2_opposes_path = desired_curvature != 0.0 and desired_curvature * pose.forward_angle <= 0.0
|
|
||||||
if c2_opposes_path:
|
|
||||||
pose_share = 1.0
|
|
||||||
curvature = 0.0
|
|
||||||
else:
|
|
||||||
curvature = desired_curvature * (1.0 - pose_share)
|
|
||||||
|
|
||||||
return _encode_pose(pose, pose_share, curvature)
|
|
||||||
|
|
||||||
|
|
||||||
class FordPathController:
|
|
||||||
"""Blend normal C2 following into the model's forward C0/C1 pose."""
|
|
||||||
|
|
||||||
def __init__(self, dt: float = 0.01):
|
|
||||||
self.dt = dt
|
|
||||||
self._last_path = FordPath(valid=True)
|
|
||||||
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
|
|
||||||
|
|
||||||
def _limit(self, target: FordPath) -> FordPath:
|
|
||||||
offset_delta = target.path_offset - self._last_path.path_offset
|
|
||||||
angle_delta = target.path_angle - self._last_path.path_angle
|
|
||||||
scale = min(
|
|
||||||
1.0,
|
|
||||||
_PATH_OFFSET_RATE * self.dt / abs(offset_delta) if offset_delta else 1.0,
|
|
||||||
_PATH_ANGLE_RATE * self.dt / abs(angle_delta) if angle_delta else 1.0,
|
|
||||||
)
|
|
||||||
self._last_path = FordPath(
|
|
||||||
True,
|
|
||||||
self._last_path.path_offset + scale * offset_delta,
|
|
||||||
self._last_path.path_angle + scale * angle_delta,
|
|
||||||
self._last_path.curvature + scale * (target.curvature - self._last_path.curvature),
|
|
||||||
0.0,
|
|
||||||
)
|
|
||||||
return self._last_path
|
|
||||||
|
|
||||||
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
|
|
||||||
v_ego: float = 0.0, active: bool = True) -> FordPath:
|
|
||||||
if not active:
|
|
||||||
self._last_path = FordPath(valid=True)
|
|
||||||
self._curvature_history.clear()
|
|
||||||
return FordPath()
|
|
||||||
current_curvature = _finite(current_curvature)
|
|
||||||
self._curvature_history.append(current_curvature)
|
|
||||||
curvature_delta = (current_curvature - self._curvature_history[0]
|
|
||||||
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
|
|
||||||
path = _model_path(model) if model is not None else None
|
|
||||||
if path is None:
|
|
||||||
return self._limit(FordPath(valid=True))
|
|
||||||
return self._limit(_encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
|
|
||||||
max(_finite(v_ego), 0.0)))
|
|
||||||
|
|
||||||
|
|
||||||
def _pscm_slew(value: float, target: float, rate: float, ticks: int) -> float:
|
|
||||||
step = rate * _PSCM_DT * ticks
|
|
||||||
return float(np.clip(target, value - step, value + step))
|
|
||||||
|
|
||||||
|
|
||||||
def _pscm_speed_gain(v_ego: float) -> float:
|
|
||||||
return float(np.interp(max(v_ego, 0.0) * 3.6, _PSCM_SPEED_KPH, _PSCM_SPEED_GAIN))
|
|
||||||
|
|
||||||
|
|
||||||
def _wire_path(path: FordPath) -> FordPath:
|
|
||||||
return FordPath(
|
|
||||||
valid=path.valid,
|
|
||||||
path_offset=round(path.path_offset / DBC_OFFSET_RESOLUTION) * DBC_OFFSET_RESOLUTION,
|
|
||||||
path_angle=round(path.path_angle / DBC_ANGLE_RESOLUTION) * DBC_ANGLE_RESOLUTION,
|
|
||||||
curvature=round(path.curvature / DBC_CURVATURE_RESOLUTION) * DBC_CURVATURE_RESOLUTION,
|
|
||||||
curvature_rate=round(path.curvature_rate / DBC_CURVATURE_RATE_RESOLUTION) * DBC_CURVATURE_RATE_RESOLUTION,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _pscm_contributions(state: FordPscmState, v_ego: float) -> tuple[float, float, float]:
|
|
||||||
gain = _pscm_speed_gain(v_ego)
|
|
||||||
return (
|
|
||||||
float(np.clip(0.5 * gain * state.path_offset, -0.5 * gain, 0.5 * gain)),
|
|
||||||
float(np.clip(10.0 * gain * state.path_angle, -0.349609375 * gain, 0.349609375 * gain)),
|
|
||||||
float(np.clip(0.30078125 * gain * state.curvature * v_ego ** 2, -0.5 * gain, 0.5 * gain)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FordPscmObserver:
|
|
||||||
"""Mirror the firmware's held-command coefficient states at its 250 Hz step."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.state = FordPscmState()
|
|
||||||
self.command = FordPath(valid=True)
|
|
||||||
self._phase = 0.0
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self.state = FordPscmState()
|
|
||||||
self.command = FordPath(valid=True)
|
|
||||||
self._phase = 0.0
|
|
||||||
|
|
||||||
def advance(self, elapsed: float) -> None:
|
|
||||||
self._phase += max(elapsed, 0.0)
|
|
||||||
ticks = int((self._phase + 1e-12) / _PSCM_DT)
|
|
||||||
self._phase -= ticks * _PSCM_DT
|
|
||||||
if ticks == 0:
|
|
||||||
return
|
|
||||||
self.state = FordPscmState(
|
|
||||||
_pscm_slew(self.state.path_offset, self.command.path_offset, _PSCM_C0_RATE, ticks),
|
|
||||||
_pscm_slew(self.state.path_angle, self.command.path_angle, _PSCM_C1_RATE, ticks),
|
|
||||||
_pscm_slew(self.state.curvature, self.command.curvature + 10.0 * self.command.curvature_rate,
|
|
||||||
_PSCM_C2_RATE, ticks),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_command(self, command: FordPath) -> None:
|
|
||||||
self.command = _wire_path(command)
|
|
||||||
|
|
||||||
|
|
||||||
class FordPscmObserverPathController:
|
|
||||||
"""Compensate model-path commands for the PSCM coefficient state it still carries."""
|
|
||||||
|
|
||||||
def __init__(self, dt: float = 0.01):
|
|
||||||
self.dt = dt
|
|
||||||
self._last_path = FordPath(valid=True)
|
|
||||||
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
|
|
||||||
self.observer = FordPscmObserver()
|
|
||||||
self._sent_c2 = 0.0
|
|
||||||
|
|
||||||
def _reset(self) -> None:
|
|
||||||
self._last_path = FordPath(valid=True)
|
|
||||||
self._curvature_history.clear()
|
|
||||||
self.observer.reset()
|
|
||||||
self._sent_c2 = 0.0
|
|
||||||
|
|
||||||
def _command_for_state(self, target: FordPath, v_ego: float) -> FordPath:
|
|
||||||
# The target describes the desired fully-settled PSCM contribution. C0 keeps
|
|
||||||
# the remaining C1-saturated residual. C1 supplies the primary contribution
|
|
||||||
# that the known slow C2 state does not yet provide, without a guessed gain.
|
|
||||||
target_state = FordPscmState(target.path_offset, target.path_angle, target.curvature)
|
|
||||||
target_contribution = sum(_pscm_contributions(target_state, v_ego))
|
|
||||||
_, _, observed_c2 = _pscm_contributions(self.observer.state, v_ego)
|
|
||||||
gain = _pscm_speed_gain(v_ego)
|
|
||||||
required_fast = target_contribution - observed_c2
|
|
||||||
c1_contribution = float(np.clip(required_fast, -0.349609375 * gain, 0.349609375 * gain))
|
|
||||||
c0_contribution = required_fast - c1_contribution
|
|
||||||
path_offset = c0_contribution / (0.5 * gain) if gain > 0.0 else 0.0
|
|
||||||
path_angle = c1_contribution / (10.0 * gain) if gain > 0.0 else 0.0
|
|
||||||
return FordPath(
|
|
||||||
valid=True,
|
|
||||||
path_offset=float(np.clip(path_offset, -_PSCM_C0_EFFECTIVE_LIMIT, _PSCM_C0_EFFECTIVE_LIMIT)),
|
|
||||||
path_angle=float(np.clip(path_angle, -_PSCM_C1_EFFECTIVE_LIMIT, _PSCM_C1_EFFECTIVE_LIMIT)),
|
|
||||||
curvature=target.curvature,
|
|
||||||
curvature_rate=target.curvature_rate,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _limit(self, target: FordPath, v_ego_raw: float) -> FordPath:
|
|
||||||
path_offset = float(np.clip(target.path_offset,
|
|
||||||
self._last_path.path_offset - _PATH_OFFSET_RATE * self.dt,
|
|
||||||
self._last_path.path_offset + _PATH_OFFSET_RATE * self.dt))
|
|
||||||
path_angle = float(np.clip(target.path_angle,
|
|
||||||
self._last_path.path_angle - _PATH_ANGLE_RATE * self.dt,
|
|
||||||
self._last_path.path_angle + _PATH_ANGLE_RATE * self.dt))
|
|
||||||
curvature = CarControllerParams.CURVATURE_LIMITS.apply_limits(
|
|
||||||
target.curvature, self._sent_c2, v_ego_raw, 0.0, True, CarControllerParams.LMC2_STEP,
|
|
||||||
)
|
|
||||||
self._sent_c2 = curvature
|
|
||||||
self._last_path = FordPath(True, path_offset, path_angle, curvature, target.curvature_rate)
|
|
||||||
self.observer.set_command(self._last_path)
|
|
||||||
return self._last_path
|
|
||||||
|
|
||||||
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
|
|
||||||
v_ego: float = 0.0, v_ego_raw: float = 0.0, active: bool = True) -> FordPath:
|
|
||||||
if not active:
|
|
||||||
self._reset()
|
|
||||||
return FordPath()
|
|
||||||
|
|
||||||
self.observer.advance(self.dt)
|
|
||||||
current_curvature = _finite(current_curvature)
|
|
||||||
self._curvature_history.append(current_curvature)
|
|
||||||
curvature_delta = (current_curvature - self._curvature_history[0]
|
|
||||||
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
|
|
||||||
path = _model_path(model) if model is not None else None
|
|
||||||
if path is None:
|
|
||||||
target = FordPath(valid=True)
|
|
||||||
else:
|
|
||||||
target = _encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
|
|
||||||
max(_finite(v_ego), 0.0))
|
|
||||||
v_ego_raw = max(_finite(v_ego_raw), 0.0)
|
|
||||||
command = self._command_for_state(target, v_ego_raw)
|
|
||||||
return self._limit(command, v_ego_raw)
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Curvature-driven C0 and full-heading C1 command regression; does not predict counterfactual wheel response. Contains geometry and control signals only, no GPS.",
|
|
||||||
"fixture_sha256": "12782ac1b0d0637945f729a46ad03af16cd58188872b6a65f104e32c4db70e9b",
|
|
||||||
"episodes": [
|
|
||||||
{
|
|
||||||
"name": "left_large",
|
|
||||||
"route": "84865544361f55cb_00000077--4b55791ce6",
|
|
||||||
"range_seconds": [
|
|
||||||
809.5,
|
|
||||||
815.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
812.1,
|
|
||||||
814.0
|
|
||||||
],
|
|
||||||
"samples": 532
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "right_large",
|
|
||||||
"route": "84865544361f55cb_00000077--4b55791ce6",
|
|
||||||
"range_seconds": [
|
|
||||||
866.5,
|
|
||||||
872.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
869.5,
|
|
||||||
871.0
|
|
||||||
],
|
|
||||||
"samples": 547
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "left_very_large",
|
|
||||||
"route": "84865544361f55cb_00000077--4b55791ce6",
|
|
||||||
"range_seconds": [
|
|
||||||
880.0,
|
|
||||||
884.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
882.9,
|
|
||||||
883.32
|
|
||||||
],
|
|
||||||
"samples": 397
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "right_plateau",
|
|
||||||
"route": "84865544361f55cb_00000077--4b55791ce6",
|
|
||||||
"range_seconds": [
|
|
||||||
964.0,
|
|
||||||
970.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
967.2,
|
|
||||||
968.93
|
|
||||||
],
|
|
||||||
"samples": 583
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "oscillation",
|
|
||||||
"route": "84865544361f55cb_00000078--349f5b8695",
|
|
||||||
"range_seconds": [
|
|
||||||
29.0,
|
|
||||||
37.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
32.5,
|
|
||||||
36.1
|
|
||||||
],
|
|
||||||
"samples": 795
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "weak_first",
|
|
||||||
"route": "84865544361f55cb_0000007a--5a95fc717e",
|
|
||||||
"range_seconds": [
|
|
||||||
90.5,
|
|
||||||
95.2
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
93.5,
|
|
||||||
95.08
|
|
||||||
],
|
|
||||||
"samples": 467
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "weak_second",
|
|
||||||
"route": "84865544361f55cb_0000007a--5a95fc717e",
|
|
||||||
"range_seconds": [
|
|
||||||
119.5,
|
|
||||||
125.3
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
122.5,
|
|
||||||
125.2
|
|
||||||
],
|
|
||||||
"samples": 576
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sources": {
|
|
||||||
"84865544361f55cb_00000077--4b55791ce6": [
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--0--rlog.zst",
|
|
||||||
"bytes": 10342855,
|
|
||||||
"sha256": "2f072eff3076f4d32dbc1a077f24fc85818abeafd13a4b970b1ba558a0d2ca52"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--1--rlog.zst",
|
|
||||||
"bytes": 11688902,
|
|
||||||
"sha256": "bbbbf7fc79b1c7133b83678a5202ac41cd8b3dcdd0bb358843d43fdb38f25e1e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--2--rlog.zst",
|
|
||||||
"bytes": 11875525,
|
|
||||||
"sha256": "18d7e0224a61d068aeeef0e176da071f5e67d77fcb7ebaa7b5854756d4438add"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--3--rlog.zst",
|
|
||||||
"bytes": 12489313,
|
|
||||||
"sha256": "7869f95c87849018df07c680e0584145a31c407608f96f2fc77f35b729f02d2c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--4--rlog.zst",
|
|
||||||
"bytes": 12301118,
|
|
||||||
"sha256": "2c2125fb2320b9bc6620fc586cedb6558b33e8475d0cce3fb361db4eea6a1c28"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--5--rlog.zst",
|
|
||||||
"bytes": 12976655,
|
|
||||||
"sha256": "b18c448786daf46cfa05bf0352396ce5851a09603d34ee5b989e096da5ee5180"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--6--rlog.zst",
|
|
||||||
"bytes": 13223857,
|
|
||||||
"sha256": "8c08b8d47ebca38c70aa94a1cdda25443ed3be94727bb487406b518471c88dd1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--7--rlog.zst",
|
|
||||||
"bytes": 13043701,
|
|
||||||
"sha256": "638948d7e5853046773f82df8a531c518c42883c77f060e61ff683ccb59d52af"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--8--rlog.zst",
|
|
||||||
"bytes": 12569024,
|
|
||||||
"sha256": "41e5c83d2205964889579cf24967712dd0340da5f30f213409f2b1e04e6eb78a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--9--rlog.zst",
|
|
||||||
"bytes": 11926213,
|
|
||||||
"sha256": "6034a90e817424c02755c2c0d9bdbad088c4286edfb887d9f2acbd60d7818da7"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--10--rlog.zst",
|
|
||||||
"bytes": 13010961,
|
|
||||||
"sha256": "23958a1ac8277977952c73e889fbfd9245bc2cf82b3af58233c245dbf1e76545"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--11--rlog.zst",
|
|
||||||
"bytes": 13204208,
|
|
||||||
"sha256": "f78d65b7b5927a8570f52d765f9af45431f8ab72260987f718321ad1b03c70af"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--12--rlog.zst",
|
|
||||||
"bytes": 12562994,
|
|
||||||
"sha256": "485621d1cf71605fccc8c679cb146b1f0954078849db7c74b1b65b166d741025"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--13--rlog.zst",
|
|
||||||
"bytes": 12610836,
|
|
||||||
"sha256": "0b4b0c01caae39a7dc4ff2219ab168029ea12c43b1966b8c204972743627c24b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--14--rlog.zst",
|
|
||||||
"bytes": 13114068,
|
|
||||||
"sha256": "f18769800bd08c7614280916b50ec0274eca9b4765ad26d6549b72912321dac0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--15--rlog.zst",
|
|
||||||
"bytes": 13113162,
|
|
||||||
"sha256": "abf0adc702db6f5fdd78a145709504c7061255bc9b9a61329a575707d5bd2f74"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--16--rlog.zst",
|
|
||||||
"bytes": 13029294,
|
|
||||||
"sha256": "a76ed74b889bda60e2d929123186df83653591d1e058fbd1497551fbbf23941e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--17--rlog.zst",
|
|
||||||
"bytes": 12429846,
|
|
||||||
"sha256": "51d3aff2afa1a7ce3c5372a499b14394f22ac730e952273a69496c65ef726ed2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000077--4b55791ce6--18--rlog.zst",
|
|
||||||
"bytes": 9241845,
|
|
||||||
"sha256": "2a1646227f9cdb1ab7eaf79fa653b7f4443a629a703577ff6c38a715dbced3a4"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"84865544361f55cb_00000078--349f5b8695": [
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000078--349f5b8695--0--rlog.zst",
|
|
||||||
"bytes": 10919702,
|
|
||||||
"sha256": "2d35f6c9ac9b09f8b86b3fe2fbe864e4af0d50c411e3dd56572b5aa113bf1973"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000078--349f5b8695--1--rlog.zst",
|
|
||||||
"bytes": 10600112,
|
|
||||||
"sha256": "6cabb48ea0caeb2fddd58be35b5c7e7c42faf87fc01991c4603573bffe33eaeb"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"84865544361f55cb_0000007a--5a95fc717e": [
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_0000007a--5a95fc717e--0--rlog.zst",
|
|
||||||
"bytes": 10841486,
|
|
||||||
"sha256": "bfc9e3308e5241e76cba57f2441043710fe8b18ade314221ee5e423f640c211a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_0000007a--5a95fc717e--1--rlog.zst",
|
|
||||||
"bytes": 12567837,
|
|
||||||
"sha256": "ab9eb05c5805e286a6ec639bbbbc1cf086bfcf1b440801ad000713db95dfa7fc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_0000007a--5a95fc717e--2--rlog.zst",
|
|
||||||
"bytes": 11015857,
|
|
||||||
"sha256": "af4f87f39be37f0a5b23b58de50c2ed8801bdbda6544cd6cd292525fc3bd4fb3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pairing": "controlsState cycle time; causal carState speed/yaw/pressed; exact consumed model timestamp and geometry; nearest same-cycle carControl and carControlSP within 5ms.",
|
|
||||||
"yaw_rate": "Negative carState.yawRate, matching the model/control curvature coordinate sign; no wheel-to-curvature conversion.",
|
|
||||||
"desired_curvature": "Exact controlsState.desiredCurvature from the matching controlsState cycle. This is the post-selection, post-limiting request consumed by controlsd; it is not a wheel-angle-to-curvature fit.",
|
|
||||||
"reference_time": "Exact consumed modelV2 publication time, in the same relative seconds as each episode. The extraction cache does not retain consumed lateralManeuverPlan timestamps or validity; model time is an explicit replay assumption and cannot verify alternate-reference freshness."
|
|
||||||
}
|
|
||||||
Binary file not shown.
-67
@@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Real route80 turn-command regressions. Signal-only fixture; no GPS. Counterfactual commands do not predict physical vehicle response.",
|
|
||||||
"route": "84865544361f55cb_00000080--1643deea7e",
|
|
||||||
"source_commit": "98662df401217a00ec9fc8e73b16857b6c220150",
|
|
||||||
"frozen_v3_controller_sha256": "576f4ec6f2dbc93f7e6c93a69839f69447eb5a0c2f834bd48b24f84a163dc2eb",
|
|
||||||
"fixture_sha256": "c1460e2cf1d3fd52b1a036d923fec7835a7d361126ee0c2decbc3f101ee6653c",
|
|
||||||
"episodes": [
|
|
||||||
{
|
|
||||||
"name": "under_333_339",
|
|
||||||
"range_seconds": [
|
|
||||||
331.5,
|
|
||||||
339.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
333.0,
|
|
||||||
339.0
|
|
||||||
],
|
|
||||||
"samples": 745
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "over_417_420",
|
|
||||||
"range_seconds": [
|
|
||||||
415.5,
|
|
||||||
420.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
417.0,
|
|
||||||
420.0
|
|
||||||
],
|
|
||||||
"samples": 447
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "under_430_435",
|
|
||||||
"range_seconds": [
|
|
||||||
428.5,
|
|
||||||
435.0
|
|
||||||
],
|
|
||||||
"evidence_seconds": [
|
|
||||||
430.0,
|
|
||||||
435.0
|
|
||||||
],
|
|
||||||
"samples": 646
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000080--1643deea7e--5--rlog.zst",
|
|
||||||
"bytes": 12531711,
|
|
||||||
"sha256": "059482830794cb0eabe6069b75a9610b900bf2a93d7a6624f53c575cef997157"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000080--1643deea7e--6--rlog.zst",
|
|
||||||
"bytes": 12560505,
|
|
||||||
"sha256": "147276789f5b14913adc4cd16db18f3d4bd27ce8497c9ff96fdf0315c219339f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "84865544361f55cb_00000080--1643deea7e--7--rlog.zst",
|
|
||||||
"bytes": 12660797,
|
|
||||||
"sha256": "b311b6ace75819db52b9618154d68c7d12e2751d5046b6d174adb89ef87a223c"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pairing": "Exact controlsState desiredCurvature and consumed model publication timestamp; causal carState speed, negative CAN yaw, and steeringPressed; nearest same-cycle carControl/carControlSP within 5 ms.",
|
|
||||||
"reference_time": "Consumed modelV2 publication time. Controller audit confirms route80 used modelV2 as reference throughout.",
|
|
||||||
"preroll": "Each episode starts from reset 1.5 s before evidence; v3_replay stores those exact cold-start commands and gates, while recorded stores original live path fields.",
|
|
||||||
"benchmark_clean": "Existing route80 benchmark mask: whole interval request minus 0.5 s through response (0.2 s) plus 0.25 s active, unpressed, valid, fresh, and speed >= 2 m/s.",
|
|
||||||
"expected_common_c1": "Independent shadow: clip(desiredCurvature * max(7 m, vEgo * 1 s), +/-0.5 rad), independently slewed at 0.5 rad/s and packed to Float32/sign-reversed CAN semantics. No subtraction of measured curvature."
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
-13
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "PSCM status and raw driver-torque overlay for the existing three route80 request windows. No GPS. No counterfactual vehicle response.",
|
|
||||||
"fixture_sha256": "a9defdc5abdf26724358d606beb16becbdf30faa972974d49b179a9e004d7629",
|
|
||||||
"base_fixture": "ford_curvature_heading_route80.npz",
|
|
||||||
"base_fixture_sha256": "c1460e2cf1d3fd52b1a036d923fec7835a7d361126ee0c2decbc3f101ee6653c",
|
|
||||||
"source_route": "84865544361f55cb_00000080--1643deea7e",
|
|
||||||
"source_commit": "98662df401217a00ec9fc8e73b16857b6c220150",
|
|
||||||
"samples": 1838,
|
|
||||||
"source_cache_sha256": "1cd3e0c00805869ace1c5954dc682644f36f5eddb71785b69e4cb9da40f7f04f",
|
|
||||||
"pairing": "Latest actual bus-0 EPS 972 frame at or before each controlsState cycle; raw steering torque from the exact causal carState used by the base fixture.",
|
|
||||||
"timestamp_policy": "Actual CAN event logMonoTime in route-relative seconds, not the benchmark response-shifted status. The old route predates the new carStateSP status telemetry; source CAN timestamps are an explicit replay approximation.",
|
|
||||||
"validity": "Replay validity uses the paired carState valid and canValid values; enum validity, availability and age are checked by the production feedback controller."
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
-40
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Signal-only v6 turn-exit recovery regression; no location, device identity, or predicted new vehicle response.",
|
|
||||||
"recorded_controller_revision": "61dac4977bf9c36504398e8a4959dfed79cf6f05",
|
|
||||||
"baseline_revision": "61dac4977bf9c36504398e8a4959dfed79cf6f05",
|
|
||||||
"response_delay": 0.20000000298023224,
|
|
||||||
"samples": 9134,
|
|
||||||
"models": 1843,
|
|
||||||
"fixture_sha256": "41d5e3efcee03a9e02fcaf7bf456c050c6a671a5b7f7fc9706ddf4d27bad71b8",
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"name": "overturn_then_underturn",
|
|
||||||
"range_s": [
|
|
||||||
19.99615067150053,
|
|
||||||
29.48070058550053
|
|
||||||
],
|
|
||||||
"samples": 521
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "well_tracked_curve_a",
|
|
||||||
"range_s": [
|
|
||||||
56.19474309950053,
|
|
||||||
63.68808603150053
|
|
||||||
],
|
|
||||||
"samples": 729
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "well_tracked_curve_b",
|
|
||||||
"range_s": [
|
|
||||||
101.19780691350051,
|
|
||||||
116.33885445450052
|
|
||||||
],
|
|
||||||
"samples": 810
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"selection": "One previously identified overturn-then-underturn event and two previously reported well-tracked curves; selected before recovery implementation.",
|
|
||||||
"mask": "Whole t-0.5 through t+0.65 interval active, valid, fresh, unpressed, raw driver torque magnitude <=1 Nm; requested |curvature|*speed\u00b2 >=.5 m/s\u00b2.",
|
|
||||||
"timing": "Exact consumed model publication; causal CAN/PSCM at estimated control computation time. Subtract observed median computation-to-publication delay; unsampled tick timing remains approximate.",
|
|
||||||
"context": "At least 20 seconds prior context or the available start, extended before the latest observed reset. Overlapping episodes are merged.",
|
|
||||||
"coordinates": "Times are local elapsed seconds; models contain only relative position.x/y and orientation.z arrays."
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
-247
@@ -1,247 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Signal-only historical fallback evidence and frozen-v5 comparison; no GPS or inferred counterfactual vehicle response.",
|
|
||||||
"route": "route83",
|
|
||||||
"recorded_commit": "79a4caa1f6b71488949108aee9ae6ae6566347b1",
|
|
||||||
"fixture_sha256": "d00312c430ace47000c05b8284ee8d56df56ec24bb17a9ea8f4dce83133527c3",
|
|
||||||
"samples": 11744,
|
|
||||||
"model_count": 2367,
|
|
||||||
"source_cache_sha256": "53d786aff2e0b6338e1991320145305fda3101f7b76e50bd2929adbcaea95b28",
|
|
||||||
"response_delay": 0.20000000298023224,
|
|
||||||
"episodes": [
|
|
||||||
[
|
|
||||||
1861.2933736250002,
|
|
||||||
1874.756970279
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1878.07372132,
|
|
||||||
1892.07372132
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1950.874232409,
|
|
||||||
1964.874232409
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2440.9020600930003,
|
|
||||||
2456.964158177
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2580.722658577,
|
|
||||||
2611.364366768
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2734.478264791,
|
|
||||||
2764.574374172
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"name": "successful_large_early",
|
|
||||||
"role": "authority_target",
|
|
||||||
"range_s": [
|
|
||||||
1866.722720383,
|
|
||||||
1874.756970279
|
|
||||||
],
|
|
||||||
"samples": 426,
|
|
||||||
"substantial_demand_required": true,
|
|
||||||
"recorded_can_ratio_02s_median": 1.0233371460413845,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
1.6002928018569946,
|
|
||||||
0.2796146124601364
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
1.6002928018569946,
|
|
||||||
0.2796146124601364
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 15,
|
|
||||||
"phase_held": 122,
|
|
||||||
"phase_release": 402,
|
|
||||||
"phase_reversal": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "centering_reversal_positive_to_negative",
|
|
||||||
"role": "reversal",
|
|
||||||
"range_s": [
|
|
||||||
1888.07372132,
|
|
||||||
1892.07372132
|
|
||||||
],
|
|
||||||
"samples": 396,
|
|
||||||
"substantial_demand_required": false,
|
|
||||||
"recorded_can_ratio_02s_median": null,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 283,
|
|
||||||
"phase_held": 48,
|
|
||||||
"phase_release": 104,
|
|
||||||
"phase_reversal": 21
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "centering_reversal_negative_to_positive",
|
|
||||||
"role": "reversal",
|
|
||||||
"range_s": [
|
|
||||||
1960.874232409,
|
|
||||||
1964.874232409
|
|
||||||
],
|
|
||||||
"samples": 397,
|
|
||||||
"substantial_demand_required": false,
|
|
||||||
"recorded_can_ratio_02s_median": null,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 154,
|
|
||||||
"phase_held": 0,
|
|
||||||
"phase_release": 183,
|
|
||||||
"phase_reversal": 21
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "clean_release",
|
|
||||||
"role": "release",
|
|
||||||
"range_s": [
|
|
||||||
2453.714158177,
|
|
||||||
2456.964158177
|
|
||||||
],
|
|
||||||
"samples": 323,
|
|
||||||
"substantial_demand_required": false,
|
|
||||||
"recorded_can_ratio_02s_median": null,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 4,
|
|
||||||
"phase_held": 0,
|
|
||||||
"phase_release": 305,
|
|
||||||
"phase_reversal": 17
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "successful_smaller_positive",
|
|
||||||
"role": "sign_coverage_only",
|
|
||||||
"range_s": [
|
|
||||||
2590.722658577,
|
|
||||||
2600.918740146
|
|
||||||
],
|
|
||||||
"samples": 175,
|
|
||||||
"substantial_demand_required": true,
|
|
||||||
"recorded_can_ratio_02s_median": 1.0960646334373787,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
0.42173025012016296,
|
|
||||||
0.1222948431968689
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
0.42173025012016296,
|
|
||||||
0.1222948431968689
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 170,
|
|
||||||
"phase_held": 61,
|
|
||||||
"phase_release": 0,
|
|
||||||
"phase_reversal": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "large_under_response",
|
|
||||||
"role": "under_response_challenge",
|
|
||||||
"range_s": [
|
|
||||||
2604.2254721,
|
|
||||||
2611.364366768
|
|
||||||
],
|
|
||||||
"samples": 128,
|
|
||||||
"substantial_demand_required": true,
|
|
||||||
"recorded_can_ratio_02s_median": 0.7322859508492778,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
2.4204851388931274,
|
|
||||||
0.42145511507987976
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
2.4204851388931274,
|
|
||||||
0.42145511507987976
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 68,
|
|
||||||
"phase_held": 96,
|
|
||||||
"phase_release": 56,
|
|
||||||
"phase_reversal": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "successful_large_181deg",
|
|
||||||
"role": "authority_target",
|
|
||||||
"range_s": [
|
|
||||||
2744.478264791,
|
|
||||||
2750.573209708
|
|
||||||
],
|
|
||||||
"samples": 207,
|
|
||||||
"substantial_demand_required": true,
|
|
||||||
"recorded_can_ratio_02s_median": 1.0087938914780248,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
2.1044259071350098,
|
|
||||||
0.3815947473049164
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
2.1044259071350098,
|
|
||||||
0.3815947473049164
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 137,
|
|
||||||
"phase_held": 94,
|
|
||||||
"phase_release": 64,
|
|
||||||
"phase_reversal": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "large_over_response_290deg",
|
|
||||||
"role": "over_response_challenge_not_target",
|
|
||||||
"range_s": [
|
|
||||||
2760.493612962,
|
|
||||||
2764.574374172
|
|
||||||
],
|
|
||||||
"samples": 181,
|
|
||||||
"substantial_demand_required": true,
|
|
||||||
"recorded_can_ratio_02s_median": 1.2515789463064766,
|
|
||||||
"published_median_abs_c0_c1": [
|
|
||||||
4.737145900726318,
|
|
||||||
0.5235000252723694
|
|
||||||
],
|
|
||||||
"send_clamped_median_abs_c0_c1": [
|
|
||||||
4.737145900726318,
|
|
||||||
0.5
|
|
||||||
],
|
|
||||||
"phase_samples": {
|
|
||||||
"phase_turn_in": 139,
|
|
||||||
"phase_held": 90,
|
|
||||||
"phase_release": 41,
|
|
||||||
"phase_reversal": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"selection": "Authority targets require automatic turn windows with >=1 second strict torque eligibility, eligible |wheel|>=150 degrees, and whole-window CAN response ratio median 0.90..1.10 at fixed 0.2 s. No positive-request large turn qualifies.",
|
|
||||||
"non_targets": "Positive smaller turn supplies sign coverage only. Under/over response and release/reversal windows are regression challenges, not authority targets.",
|
|
||||||
"context": "At least 10 s pre-roll or available route start, extended to include the preceding feedback reset/sign reversal. Overlapping intervals are merged. First episode begins at the partial route boundary with unobserved earlier history.",
|
|
||||||
"phase_policy": "Held means request curvature range over +/-0.25 s times speed squared <0.15 m/s2 at demand>=0.5. Turn-in/release compare current absolute curvature with the historical held request at measurement_time-delay, scaled by max(7,speed), using +/-0.0005 rad. These masks can overlap held; reversal means opposing delayed/current signs.",
|
|
||||||
"wire_policy": "Published coefficients preserve Float32 values. Send-clamped copy caps C0 to +/-5.11 and C1 to +/-0.5 before packing. Actual decoded wire is normalized to controller sign, nearest within 15 ms; wire_time/fresh/mode expose timing approximation.",
|
|
||||||
"model_schema": "models[model_index] contains position.x, position.y, orientation.z; Float32 conversion preserves the original model payload precision.",
|
|
||||||
"v5_reference": "Frozen full sequential replay from command_replay.npz, whose source hash and limitations are recorded in command_replay.json.",
|
|
||||||
"frozen_v5_revision": "09acf8ec2f327769f00ee53563ad2dd9225e37a7",
|
|
||||||
"preroll_validation": "Compact reset replay exactly matches full sequential frozen-v5 C0/C1, gates and bias on all 2233 evidence samples."
|
|
||||||
}
|
|
||||||
BIN
Binary file not shown.
@@ -1,156 +0,0 @@
|
|||||||
{
|
|
||||||
"description": "Anonymous recorded-input turn-exit regression fixture; command construction only, not simulated vehicle response.",
|
|
||||||
"baseline_revision": "dfcfddb91ce2409511f5b2dbce25d06d5056b3d6",
|
|
||||||
"baseline_hypothesis": "model-pose-c0-c1-feedback-v7",
|
|
||||||
"baseline_source_hashes": {
|
|
||||||
"controller_sha256": "4951a6352d89fcd66277bbfe682bd22e935a31b5a4db33e617ad21189b6705fd",
|
|
||||||
"allocator_sha256": "383538fc7cdae3bc28dffb71fe12ac5f3f9866ffbe6adfb7457f3593e9fc903a"
|
|
||||||
},
|
|
||||||
"fixture_sha256": "87a030c309061b7dc218715d05440c2077e465a8138079b46e8e8cee94201e54",
|
|
||||||
"source_fixture_sha256": "d476110b83dc628ffbd094220e464d6d3114b709bda2977813c3217964d41086",
|
|
||||||
"response_delay": 0.20000000298023224,
|
|
||||||
"publication_latency_estimate_s": 0.0015483515003040793,
|
|
||||||
"samples": 15273,
|
|
||||||
"model_count": 3078,
|
|
||||||
"evidence_samples": 4879,
|
|
||||||
"context_policy": "At least twenty seconds prior context, extended before the last observed reset. Overlapping intervals are merged.",
|
|
||||||
"provenance": "Selected from a recorded drive running the pinned baseline; request, model, driver and PSCM observations stay fixed during replay.",
|
|
||||||
"baseline_policy": "Stored commands, validity and bias exactly match the complete baseline replay on evidence samples. Context outside evidence initializes state and is not an exact-output target.",
|
|
||||||
"compact_full_baseline_evidence_parity": {
|
|
||||||
"commands": {
|
|
||||||
"exact": true,
|
|
||||||
"max_difference": 0.0
|
|
||||||
},
|
|
||||||
"valid": {
|
|
||||||
"exact": true,
|
|
||||||
"max_difference": 0.0
|
|
||||||
},
|
|
||||||
"heading_bias": {
|
|
||||||
"exact": true,
|
|
||||||
"max_difference": 0.0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"measurement_policy": "Controller computation time is estimated from publication time using the recorded median latency; exact vehicle motion under changed commands is unknown.",
|
|
||||||
"clean_policy": "Every sample from request time minus 0.5 s through plus 0.65 s is active, valid, fresh, unpressed and within 1 Nm raw driver torque. Demand is absolute desired curvature times current speed squared; substantial means at least 0.5 m/s2.",
|
|
||||||
"driver_policy": "All replay inputs retain driver interference; only comparison metrics use the clean mask. History-reset failures intentionally retain nearby driver context.",
|
|
||||||
"coordinates": "Elapsed seconds shifted to the first fixture control cycle; model x/y/heading are vehicle-relative, not global position.",
|
|
||||||
"retained_fields": [
|
|
||||||
"t",
|
|
||||||
"episode",
|
|
||||||
"model_index",
|
|
||||||
"models",
|
|
||||||
"desired_curvature",
|
|
||||||
"yaw_rate",
|
|
||||||
"speed",
|
|
||||||
"measurement_time",
|
|
||||||
"model_time",
|
|
||||||
"reference_time",
|
|
||||||
"active",
|
|
||||||
"valid",
|
|
||||||
"pressed",
|
|
||||||
"steering_torque",
|
|
||||||
"pscm_timestamp",
|
|
||||||
"pscm_valid",
|
|
||||||
"pscm_lateral_state",
|
|
||||||
"pscm_limit",
|
|
||||||
"pscm_capability",
|
|
||||||
"pscm_denied",
|
|
||||||
"clean_rawtorque",
|
|
||||||
"demand",
|
|
||||||
"window_masks",
|
|
||||||
"evidence",
|
|
||||||
"baseline_commands",
|
|
||||||
"baseline_valid",
|
|
||||||
"baseline_heading_base",
|
|
||||||
"baseline_heading_target",
|
|
||||||
"baseline_heading_bias",
|
|
||||||
"baseline_feedback_yaw_error",
|
|
||||||
"baseline_feedback_reference_curvature",
|
|
||||||
"baseline_status",
|
|
||||||
"baseline_offset_target"
|
|
||||||
],
|
|
||||||
"omitted_data": "No route/device identifiers, VIN, GPS, private paths, raw wheel angle, wheel rate, EPS torque, or absolute clock origins.",
|
|
||||||
"baseline_status_meaning": "feedback_status from the pinned baseline",
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"name": "good_curve_a",
|
|
||||||
"role": "comparison",
|
|
||||||
"range_s": [
|
|
||||||
20.0002130975003,
|
|
||||||
25.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 496,
|
|
||||||
"clean_substantial_samples": 259
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "first_reversal",
|
|
||||||
"role": "reversal",
|
|
||||||
"range_s": [
|
|
||||||
83.0002130975003,
|
|
||||||
92.7002130975003
|
|
||||||
],
|
|
||||||
"samples": 964,
|
|
||||||
"clean_substantial_samples": 167
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "good_curve_b",
|
|
||||||
"role": "comparison",
|
|
||||||
"range_s": [
|
|
||||||
121.0002130975003,
|
|
||||||
128.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 695,
|
|
||||||
"clean_substantial_samples": 308
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "second_reversal",
|
|
||||||
"role": "reversal",
|
|
||||||
"range_s": [
|
|
||||||
133.5002130975003,
|
|
||||||
138.9002130975003
|
|
||||||
],
|
|
||||||
"samples": 537,
|
|
||||||
"clean_substantial_samples": 191
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "large_turn_driver_context_a",
|
|
||||||
"role": "driver_context",
|
|
||||||
"range_s": [
|
|
||||||
150.0002130975003,
|
|
||||||
157.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 695,
|
|
||||||
"clean_substantial_samples": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "over_growth",
|
|
||||||
"role": "over_response",
|
|
||||||
"range_s": [
|
|
||||||
182.0002130975003,
|
|
||||||
191.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 897,
|
|
||||||
"clean_substantial_samples": 66
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "large_turn_driver_context_b",
|
|
||||||
"role": "driver_context",
|
|
||||||
"range_s": [
|
|
||||||
199.0002130975003,
|
|
||||||
205.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 595,
|
|
||||||
"clean_substantial_samples": 281
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "zero_bias_release",
|
|
||||||
"role": "under_response",
|
|
||||||
"range_s": [
|
|
||||||
202.0002130975003,
|
|
||||||
205.0002130975003
|
|
||||||
],
|
|
||||||
"samples": 297,
|
|
||||||
"clean_substantial_samples": 279
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,59 +0,0 @@
|
|||||||
import ast
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from openpilot.common.logging_extra import SwagFormatter, SwagLogger
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPathController, FordPscmObserverPathController
|
|
||||||
from openpilot.selfdrive.controls.tests.test_ford_model_action import circle
|
|
||||||
|
|
||||||
|
|
||||||
class TestFordControlsLogging(unittest.TestCase):
|
|
||||||
def emit_controls_event(self, event, controls):
|
|
||||||
# Execute the actual controlsd call with the real logger and formatter,
|
|
||||||
# without launching hardware-dependent Controls or opening logging IPC.
|
|
||||||
source_path = Path(__file__).resolve().parents[1] / 'controlsd.py'
|
|
||||||
source = ast.parse(source_path.read_text())
|
|
||||||
calls = [node for node in ast.walk(source) if isinstance(node, ast.Call)
|
|
||||||
and isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name)
|
|
||||||
and node.func.value.id == 'cloudlog' and node.args
|
|
||||||
and isinstance(node.args[0], ast.Constant) and node.args[0].value == event]
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
logger = SwagLogger()
|
|
||||||
logger.setLevel(logging.INFO) # disabled INFO logging would hide this crash
|
|
||||||
stream = io.StringIO()
|
|
||||||
handler = logging.StreamHandler(stream)
|
|
||||||
handler.setFormatter(SwagFormatter(logger))
|
|
||||||
logger.addHandler(handler)
|
|
||||||
try:
|
|
||||||
expression = ast.Expression(body=calls[0])
|
|
||||||
eval(compile(expression, str(source_path), 'eval'), {'cloudlog': logger, 'self': controls, 'reference_service': 'modelV2'})
|
|
||||||
record = json.loads(stream.getvalue())
|
|
||||||
finally:
|
|
||||||
handler.close()
|
|
||||||
self.assertEqual(record['level'], 'INFO')
|
|
||||||
self.assertEqual(record['msg']['event'], event)
|
|
||||||
return record['msg']
|
|
||||||
|
|
||||||
def test_startup_logs_selected_controller_without_crashing(self):
|
|
||||||
for controller in (FordPathController(), FordPscmObserverPathController(), FordModelActionController()):
|
|
||||||
with self.subTest(controller=type(controller).__name__):
|
|
||||||
record = self.emit_controls_event('Ford path controller selected', SimpleNamespace(ford_path_controller=controller))
|
|
||||||
self.assertEqual(record['controller'], type(controller).__name__)
|
|
||||||
|
|
||||||
def test_candidate_diagnostics_identify_the_experiment_and_do_not_claim_calibration(self):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for active, valid in ((False, True), (True, True), (True, False)):
|
|
||||||
controller.update(circle(.01), .005, current_curvature=.0025, yaw_rate=.05, speed=20., now=1.,
|
|
||||||
measurement_time=1., model_time=1., reference_time=1., active=active, valid=valid)
|
|
||||||
controls = SimpleNamespace(ford_path_controller=controller, desired_curvature=.005, curvature=.0025,
|
|
||||||
sm=SimpleNamespace(logMonoTime={'modelV2': 123456789, 'carState': 123450000}))
|
|
||||||
record = self.emit_controls_event('Ford C2-free path tracking', controls)
|
|
||||||
self.assertEqual(record['hypothesis'], 'model-action-c1-feedback-v2')
|
|
||||||
self.assertIs(record['calibration_approved'], False)
|
|
||||||
self.assertEqual(record['command'][2:], [0., 0.])
|
|
||||||
self.assertEqual(record['status'], controller.diagnostics['status'])
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import math
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from opendbc.can import CANPacker, CANParser
|
|
||||||
from opendbc.car.ford.fordcan import CanBus, create_lat_ctl2_msg
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import ModelActionController, encode_model_action
|
|
||||||
|
|
||||||
|
|
||||||
def make_model(x, y, heading):
|
|
||||||
return SimpleNamespace(position=SimpleNamespace(x=x, y=y), orientation=SimpleNamespace(z=heading))
|
|
||||||
|
|
||||||
|
|
||||||
def circle(curvature):
|
|
||||||
s = np.linspace(0., 60., 601)
|
|
||||||
return make_model(np.sin(curvature*s)/curvature, (1-np.cos(curvature*s))/curvature, curvature*s)
|
|
||||||
|
|
||||||
|
|
||||||
def straight(offset=0.):
|
|
||||||
x = np.linspace(0., 60., 121)
|
|
||||||
return make_model(x, np.full_like(x, offset), np.zeros_like(x))
|
|
||||||
|
|
||||||
|
|
||||||
def test_selected_action_controls_heading_even_when_model_previews_another_turn():
|
|
||||||
model = circle(.02)
|
|
||||||
assert encode_model_action(model, 0., 20.).path_angle == 0.
|
|
||||||
assert encode_model_action(model, -.004, 20.).path_angle == pytest.approx(-.08)
|
|
||||||
assert encode_model_action(model, 0., 20.).path_offset > 0.
|
|
||||||
|
|
||||||
|
|
||||||
def test_centering_information_is_independent_of_action_and_not_scaled_with_speed():
|
|
||||||
for speed in (2., 7., 20., 35.):
|
|
||||||
target = encode_model_action(straight(.4), 0., speed)
|
|
||||||
assert target == FordPath(True, .4, 0., 0., 0.)
|
|
||||||
for sign in (-1, 1):
|
|
||||||
target = encode_model_action(circle(sign*.01), sign*.01, 20.)
|
|
||||||
assert target.path_offset == pytest.approx(sign*(1-math.cos(.07))/.01, abs=1e-6)
|
|
||||||
assert target.path_angle == pytest.approx(sign*.2) # No 10 m cap at highway speed.
|
|
||||||
|
|
||||||
|
|
||||||
def test_three_control_states_are_sufficient_for_every_next_output():
|
|
||||||
controller = ModelActionController()
|
|
||||||
assert not hasattr(controller, '__dict__')
|
|
||||||
for i in range(300):
|
|
||||||
copied = ModelActionController()
|
|
||||||
copied.c0, copied.c1, copied.correction = controller.c0, controller.c1, controller.correction
|
|
||||||
model = straight(.2*math.sin(i*.1))
|
|
||||||
kwargs = {'speed': 20., 'dt': .01}
|
|
||||||
desired = .005*math.cos(i*.03)
|
|
||||||
assert controller.update(model, desired, current_curvature=0., **kwargs) == copied.update(model, desired, current_curvature=0., **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def test_held_turn_releases_without_a_bias_tail_or_sign_reversal():
|
|
||||||
for sign in (-1., 1.):
|
|
||||||
controller = ModelActionController()
|
|
||||||
for _ in range(400):
|
|
||||||
out = controller.update(circle(sign*.01), sign*.01, current_curvature=sign*.01, speed=20., dt=.01)
|
|
||||||
assert out.path_angle == pytest.approx(sign*.2)
|
|
||||||
previous = np.array([out.path_offset, out.path_angle])
|
|
||||||
for desired in sign*np.linspace(.01, 0., 101):
|
|
||||||
out = controller.update(straight(), desired, current_curvature=desired, speed=20., dt=.01)
|
|
||||||
values = np.array([out.path_offset, out.path_angle])
|
|
||||||
assert (abs(values) <= abs(previous)+1e-8).all()
|
|
||||||
assert (sign*values >= -1e-8).all()
|
|
||||||
previous = values
|
|
||||||
assert out == FordPath(True, 0., 0., 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
def test_current_model_replacement_leaves_only_independent_actuator_slew():
|
|
||||||
controller = ModelActionController()
|
|
||||||
for _ in range(150):
|
|
||||||
controller.update(straight(1.), .04, current_curvature=.04, speed=20., dt=.01)
|
|
||||||
for _ in range(25):
|
|
||||||
out = controller.update(straight(), 0., current_curvature=0., speed=20., dt=.01)
|
|
||||||
assert out.path_offset == pytest.approx(0.)
|
|
||||||
assert out.path_angle > 0. # C1 cannot hold C0 during its longer release.
|
|
||||||
for _ in range(75):
|
|
||||||
out = controller.update(straight(), 0., current_curvature=0., speed=20., dt=.01)
|
|
||||||
assert out == FordPath(True, 0., 0., 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides', [{'active': False}, {'valid': False}, {'dt': .2}, {'speed': math.nan}])
|
|
||||||
def test_invalid_or_inactive_input_clears_state_before_reengagement(overrides):
|
|
||||||
controller = ModelActionController()
|
|
||||||
for _ in range(100):
|
|
||||||
controller.update(straight(.5), .01, current_curvature=.01, speed=20., dt=.01)
|
|
||||||
kwargs = {'speed': 20., 'dt': .01, 'active': True, 'valid': True}
|
|
||||||
kwargs.update(overrides)
|
|
||||||
assert controller.update(straight(), 0., current_curvature=0., **kwargs) == FordPath()
|
|
||||||
assert (controller.c0, controller.c1) == (0., 0.)
|
|
||||||
assert controller.update(straight(), 0., current_curvature=0., speed=20., dt=.01) == FordPath(True, 0., 0., 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
def test_malformed_geometry_and_nonfinite_action_never_create_an_active_command():
|
|
||||||
for model, desired in ((None, 0.), (straight(), math.nan), (straight(), math.inf)):
|
|
||||||
assert not encode_model_action(model, desired, 20.).valid
|
|
||||||
|
|
||||||
|
|
||||||
def test_selected_core_reversal_through_float32_and_wire_keeps_sign_and_zero_c2():
|
|
||||||
controller = ModelActionController()
|
|
||||||
packer = CANPacker('ford_lincoln_base_pt')
|
|
||||||
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], 0)
|
|
||||||
bus = CanBus(fingerprint={0: {}})
|
|
||||||
previous = np.zeros(2)
|
|
||||||
for i in range(600):
|
|
||||||
sign = 1. if i < 300 else -1.
|
|
||||||
out = controller.update(straight(sign*8.), sign*.1, current_curvature=sign*.1, speed=30., dt=.01)
|
|
||||||
fields = np.array([out.path_offset, out.path_angle])
|
|
||||||
assert (abs(fields) <= [5.1100001, .5000001]).all()
|
|
||||||
assert (abs(fields-previous) <= [.0500001, .0055001]).all()
|
|
||||||
previous = fields
|
|
||||||
message = custom.CarControlSP.new_message()
|
|
||||||
message.fordLateralPath.pathOffset = out.path_offset
|
|
||||||
message.fordLateralPath.pathAngle = out.path_angle
|
|
||||||
packet = create_lat_ctl2_msg(packer, bus, 2, -message.fordLateralPath.pathOffset,
|
|
||||||
-message.fordLateralPath.pathAngle, out.curvature, out.curvature_rate, i % 16)
|
|
||||||
parser.update([i*10_000_000, [packet]])
|
|
||||||
decoded = parser.vl['LateralMotionControl2']
|
|
||||||
assert decoded['LatCtlPathOffst_L_Actl'] == pytest.approx(-out.path_offset)
|
|
||||||
assert decoded['LatCtlPath_An_Actl'] == pytest.approx(-out.path_angle)
|
|
||||||
assert decoded['LatCtlCurv_No_Actl'] == decoded['LatCtlCrv_NoRate2_Actl'] == 0.
|
|
||||||
|
|
||||||
|
|
||||||
def test_short_path_holds_available_endpoint_without_extrapolation():
|
|
||||||
model = make_model([0., 1.], [0., .1], [0., 0.])
|
|
||||||
assert encode_model_action(model, .01, 20.) == FordPath(True, .1, .2, 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
def test_overflowing_arc_resets_instead_of_publishing_invalid_geometry():
|
|
||||||
model = make_model([0., 1e308, -1e308], [0., 0., 0.], [0., 0., 0.])
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.update(straight(.4), .01, current_curvature=.01, speed=20., dt=.01)
|
|
||||||
assert controller.update(model, .01, current_curvature=.01, speed=20., dt=.01) == FordPath()
|
|
||||||
assert (controller.c0, controller.c1) == (0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('value', [None, 'bad', 10**400])
|
|
||||||
@pytest.mark.parametrize('field', ['dt', 'speed', 'desired_curvature'])
|
|
||||||
def test_malformed_numeric_input_resets_without_throwing(field, value):
|
|
||||||
controller = ModelActionController()
|
|
||||||
kwargs = {'speed': 20., 'dt': .01, 'desired_curvature': .01}
|
|
||||||
controller.update(straight(.4), current_curvature=kwargs['desired_curvature'], **kwargs)
|
|
||||||
kwargs[field] = value
|
|
||||||
assert controller.update(straight(.4), current_curvature=kwargs['desired_curvature'], **kwargs) == FordPath()
|
|
||||||
assert (controller.c0, controller.c1) == (0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('model', [
|
|
||||||
make_model([], [], []), make_model([0.], [0.], [0.]),
|
|
||||||
make_model([0., 10.], [0.], [0., 0.]), make_model([0., 10.], [0., 0.], [0.]),
|
|
||||||
make_model([0., 0.], [0., 0.], [0., 0.]),
|
|
||||||
make_model([0., 10.], [0., math.nan], [0., 0.]), make_model([0., math.inf], [0., 0.], [0., 0.]),
|
|
||||||
make_model([0., 10.], [0., 0.], [0., math.inf]),
|
|
||||||
make_model([0., 10**400], [0., 0.], [0., 0.]),
|
|
||||||
make_model([0., 10.], [0., 0.], [1e308, -1e308]),
|
|
||||||
])
|
|
||||||
def test_malformed_model_arrays_cannot_reuse_a_previous_valid_command(model):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.update(straight(.4), .01, current_curvature=.01, speed=20., dt=.01)
|
|
||||||
assert controller.update(model, .01, current_curvature=.01, speed=20., dt=.01) == FordPath()
|
|
||||||
assert (controller.c0, controller.c1) == (0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('field,value,valid', [
|
|
||||||
('speed', .2999, False), ('speed', .3, True), ('speed', 55., True), ('speed', 55.0001, False),
|
|
||||||
('desired_curvature', -1., True), ('desired_curvature', 1., True), ('desired_curvature', -1.0001, False),
|
|
||||||
('dt', .001999, False), ('dt', .002, True), ('dt', .1, True), ('dt', .100001, False), ('dt', 0., False),
|
|
||||||
])
|
|
||||||
def test_domain_and_elapsed_time_boundaries(field, value, valid):
|
|
||||||
kwargs = {'speed': 20., 'desired_curvature': .01, 'dt': .01}
|
|
||||||
kwargs[field] = value
|
|
||||||
assert ModelActionController().update(straight(.4), current_curvature=kwargs['desired_curvature'], **kwargs).valid == valid
|
|
||||||
|
|
||||||
|
|
||||||
def test_arc_station_not_forward_x_or_model_heading_determines_offset():
|
|
||||||
x = np.array([0., 6., 12.])
|
|
||||||
y = .4+x*.75
|
|
||||||
target = encode_model_action(make_model(x, y, [2., -2., 1.]), -.01, 20.)
|
|
||||||
# Arc length is 1.25*x on this line, so y(arc=7)=.4+.75*(7/1.25).
|
|
||||||
assert target.path_offset == pytest.approx(4.6)
|
|
||||||
assert target.path_angle == pytest.approx(-.2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_duplicate_stations_keep_valid_geometry_and_first_cycle_slew():
|
|
||||||
model = make_model([0., 0., 10.], [.4, .4, .4], [0., 0., 0.])
|
|
||||||
assert encode_model_action(model, .01, 20.) == FordPath(True, .4, .2, 0., 0.)
|
|
||||||
out = ModelActionController().update(model, .01, current_curvature=.01, speed=20., dt=.002)
|
|
||||||
assert out.path_offset == pytest.approx(.01)
|
|
||||||
assert out.path_angle == pytest.approx(.001)
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
"""Exercise the candidate through existing selection, publication and CAN code.
|
|
||||||
|
|
||||||
Tests enable the candidate through controlsd's real startup selection.
|
|
||||||
No hardware, IPC or CAN transmission is involved.
|
|
||||||
"""
|
|
||||||
import ast
|
|
||||||
from collections import defaultdict
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from opendbc.can import CANParser
|
|
||||||
from opendbc.car import Bus, structs
|
|
||||||
from opendbc.car.ford.carcontroller import CarController
|
|
||||||
from opendbc.car.ford.fordcan import calculate_lat_ctl2_checksum
|
|
||||||
from opendbc.car.ford.values import FordFlags
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.selfdrive.car.helpers import convert_carControlSP
|
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath
|
|
||||||
from openpilot.selfdrive.controls.tests.test_ford_model_action import circle, straight
|
|
||||||
from openpilot.selfdrive.controls.tests.test_ford_model_action_selection import CANFD_CARS, car_params, startup
|
|
||||||
|
|
||||||
|
|
||||||
def update(controller, now=1., **overrides):
|
|
||||||
kwargs = {'model': straight(.4), 'desired_curvature': .01, 'speed': 20., 'yaw_rate': 0., 'now': now,
|
|
||||||
'model_time': now, 'measurement_time': now, 'reference_time': now, 'active': True}
|
|
||||||
kwargs.update(overrides)
|
|
||||||
kwargs.setdefault('current_curvature', kwargs['desired_curvature']) # Preserve feedforward-only compatibility probes.
|
|
||||||
return controller.update(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('field', ['model_time', 'measurement_time', 'reference_time'])
|
|
||||||
@pytest.mark.parametrize('age', [.151, -.006])
|
|
||||||
def test_stale_or_future_service_clears_commands_and_reengages_from_zero(field, age):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
update(controller)
|
|
||||||
assert update(controller, 1.01, **{field: 1.01-age}) == FordPath()
|
|
||||||
assert controller.diagnostics['status'] == 'stale_input'
|
|
||||||
assert update(controller, 1.02).path_offset == pytest.approx(.04)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('change,reason', [
|
|
||||||
({'now': 1.}, 'timing_reset'),
|
|
||||||
({'now': .99}, 'timing_reset'),
|
|
||||||
({'now': 1.001}, 'timing_reset'),
|
|
||||||
({'now': 1.101}, 'timing_reset'),
|
|
||||||
({'model_time': .999}, 'timing_reset'),
|
|
||||||
({'measurement_time': .999}, 'timing_reset'),
|
|
||||||
({'active': False}, 'inactive'),
|
|
||||||
({'valid': False}, 'invalid_service'),
|
|
||||||
({'model': None}, 'invalid_path'),
|
|
||||||
({'yaw_rate': math.nan}, 'nonfinite'),
|
|
||||||
({'yaw_rate': 3.01}, 'input_range'),
|
|
||||||
({'speed': 55.01}, 'input_range'),
|
|
||||||
({'desired_curvature': 1.01}, 'input_range'),
|
|
||||||
])
|
|
||||||
def test_invalid_cycle_never_keeps_a_previous_active_request(change, reason):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
update(controller)
|
|
||||||
now = change.get('now', 1.01)
|
|
||||||
assert update(controller, **dict(change, now=now)) == FordPath()
|
|
||||||
assert controller.diagnostics['status'] == reason
|
|
||||||
assert (controller.core.c0, controller.core.c1) == (0., 0.)
|
|
||||||
assert update(controller, now+1.).path_angle == pytest.approx(.005)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('field', ['now', 'measurement_time', 'model_time', 'reference_time', 'speed', 'yaw_rate', 'desired_curvature'])
|
|
||||||
@pytest.mark.parametrize('value', [math.nan, math.inf, -math.inf, None])
|
|
||||||
def test_nonfinite_input_never_raises_or_leaks_into_diagnostics(field, value):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
update(controller)
|
|
||||||
assert update(controller, **{field: value}) == FordPath()
|
|
||||||
assert controller.diagnostics['status'] == 'nonfinite'
|
|
||||||
json.dumps(controller.diagnostics, allow_nan=False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_repeated_measurements_do_not_freeze_slew_or_cache_invalid_model_geometry():
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(10):
|
|
||||||
result = update(controller, 1.+i*.01, measurement_time=1., model_time=1., reference_time=1.)
|
|
||||||
assert result.path_offset == pytest.approx(.4)
|
|
||||||
assert result.path_angle == pytest.approx(.05)
|
|
||||||
broken = straight(.4)
|
|
||||||
broken.position.y[5] = math.nan
|
|
||||||
assert update(controller, 1.1, model=broken, model_time=1., measurement_time=1.) == FordPath()
|
|
||||||
assert controller.diagnostics['status'] == 'invalid_path'
|
|
||||||
|
|
||||||
|
|
||||||
def test_yaw_offset_does_not_change_the_base():
|
|
||||||
controllers = [FordModelActionController() for _ in range(3)]
|
|
||||||
variants = [{}, {'yaw_rate': .0072}, {'yaw_rate': -.0072}]
|
|
||||||
for i in range(100):
|
|
||||||
outputs = [update(c, 1.+i*.01, **kwargs) for c, kwargs in zip(controllers, variants, strict=True)]
|
|
||||||
assert all(out == outputs[0] for out in outputs)
|
|
||||||
assert outputs[0].path_angle == pytest.approx(.2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_reference_source_can_change_to_an_older_but_fresh_publication():
|
|
||||||
controller = FordModelActionController()
|
|
||||||
update(controller, reference_time=.99)
|
|
||||||
assert update(controller, 1.01, reference_time=.98).valid
|
|
||||||
|
|
||||||
|
|
||||||
def test_release_keeps_current_geometry_and_may_grow_c0_while_c1_decreases():
|
|
||||||
for sign in (-1., 1.):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(100):
|
|
||||||
before = update(controller, 1.+i*.01, model=circle(sign*.01), desired_curvature=sign*.005)
|
|
||||||
for i in range(100):
|
|
||||||
after = update(controller, 2.+i*.01, model=circle(sign*.02), desired_curvature=sign*.004)
|
|
||||||
assert abs(after.path_offset) > abs(before.path_offset)
|
|
||||||
assert abs(after.path_angle) < abs(before.path_angle)
|
|
||||||
for i in range(100):
|
|
||||||
released = update(controller, 3.+i*.01, model=circle(sign*.02), desired_curvature=0.)
|
|
||||||
assert released.path_offset == after.path_offset
|
|
||||||
assert released.path_angle == pytest.approx(0.)
|
|
||||||
|
|
||||||
|
|
||||||
def _method(filename, class_name, method):
|
|
||||||
tree = ast.parse(filename.read_text())
|
|
||||||
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == class_name)
|
|
||||||
return next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == method)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def pipeline():
|
|
||||||
root = Path(__file__).resolve().parents[3]
|
|
||||||
controls_file = root/'selfdrive/controls/controlsd.py'
|
|
||||||
body = _method(controls_file, 'Controls', 'state_control').body
|
|
||||||
# Execute the actual source choice, upstream limiter and Ford integration.
|
|
||||||
selection = next(n for n in body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.sm.valid['lateralManeuverPlan']")
|
|
||||||
limiter = next(n for n in body if isinstance(n, ast.Assign) and isinstance(n.value, ast.Call) and
|
|
||||||
isinstance(n.value.func, ast.Name) and n.value.func.id == 'clip_curvature')
|
|
||||||
branch = next(n for n in body if isinstance(n, ast.If) and ast.unparse(n.test) == "self.CP.brand == 'ford'")
|
|
||||||
call = compile(ast.Module(body=[selection, limiter, branch], type_ignores=[]), str(controls_file), 'exec')
|
|
||||||
publication_file = root/'sunnypilot/selfdrive/controls/controlsd_ext.py'
|
|
||||||
body = _method(publication_file, 'ControlsExt', 'state_control_ext').body
|
|
||||||
publish = [n for n in body if (isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'ford_path') or
|
|
||||||
(isinstance(n, ast.If) and ast.unparse(n.test) == 'ford_path is not None')]
|
|
||||||
assert len(publish) == 2
|
|
||||||
publication = compile(ast.Module(body=publish, type_ignores=[]), str(publication_file), 'exec')
|
|
||||||
return call, publication
|
|
||||||
|
|
||||||
|
|
||||||
class Subscriptions:
|
|
||||||
frame = 1
|
|
||||||
|
|
||||||
def __init__(self, maneuver):
|
|
||||||
self.valid = {'lateralManeuverPlan': maneuver, 'modelV2': True, 'carStateSP': True}
|
|
||||||
self.logMonoTime = {'carState': 995_000_000, 'modelV2': 980_000_000, 'lateralManeuverPlan': 990_000_000}
|
|
||||||
self.failed = set()
|
|
||||||
self.messages = {'carStateSP': custom.CarStateSP.new_message(), 'lateralManeuverPlan': SimpleNamespace(desiredCurvature=-.1)}
|
|
||||||
|
|
||||||
def __getitem__(self, service):
|
|
||||||
return self.messages[service]
|
|
||||||
|
|
||||||
def all_checks(self, services):
|
|
||||||
return not self.failed.intersection(services) and all(self.valid.get(s, True) for s in services)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('maneuver', [False, True])
|
|
||||||
@pytest.mark.parametrize('fingerprint', CANFD_CARS)
|
|
||||||
def test_actual_controlsd_selection_limiting_publication_and_downstream_can(pipeline, maneuver, fingerprint):
|
|
||||||
call, publication = pipeline
|
|
||||||
sm = Subscriptions(maneuver)
|
|
||||||
controls = startup(car_params(carFingerprint=fingerprint))
|
|
||||||
controller = controls.ford_path_controller
|
|
||||||
controls.sm, controls.desired_curvature, controls.curvature = sm, 0., 0.
|
|
||||||
model = straight(.4)
|
|
||||||
model.action = SimpleNamespace(desiredCurvature=.1)
|
|
||||||
cc = structs.CarControl(latActive=True)
|
|
||||||
cs = SimpleNamespace(vEgo=20., yawRate=-.0072, canValid=True, steeringPressed=False, steeringTorque=0.)
|
|
||||||
environment = {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.),
|
|
||||||
'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)}
|
|
||||||
exec(call, environment)
|
|
||||||
expected_curvature = (-1 if maneuver else 1)*.000125
|
|
||||||
assert controls.desired_curvature == pytest.approx(expected_curvature)
|
|
||||||
assert controls.ford_path.path_angle == pytest.approx(20.*expected_curvature)
|
|
||||||
assert controls.ford_path.path_offset == pytest.approx(.04)
|
|
||||||
assert cc.latActive and cc.actuators.curvature == 0.
|
|
||||||
assert controller.diagnostics['reference_age'] == pytest.approx(.01 if maneuver else .02)
|
|
||||||
|
|
||||||
cp = structs.CarParams(flags=int(FordFlags.CANFD), carFingerprint=fingerprint)
|
|
||||||
downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, structs.CarParamsSP())
|
|
||||||
vehicle = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int),
|
|
||||||
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
|
|
||||||
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], downstream.CAN.main)
|
|
||||||
for i, fail in enumerate((False, True)):
|
|
||||||
if fail:
|
|
||||||
sm.failed.add('modelV2')
|
|
||||||
exec(call, environment)
|
|
||||||
assert not cc.latActive and controls.ford_path == FordPath()
|
|
||||||
msg = custom.CarControlSP.new_message()
|
|
||||||
exec(publication, {'self': controls, 'CC_SP': msg})
|
|
||||||
_, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, (i+1)*10_000_000)
|
|
||||||
parser.update([(i+1)*10_000_000, packets])
|
|
||||||
wire = parser.vl['LateralMotionControl2']
|
|
||||||
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-controls.ford_path.path_offset)
|
|
||||||
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-controls.ford_path.path_angle)
|
|
||||||
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
|
|
||||||
assert wire['LatCtl_D2_Rq'] == (0 if fail else 2)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('maneuver', [False, True])
|
|
||||||
@pytest.mark.parametrize('failed', ['carState', 'modelV2', 'vehicleParameters', 'lateralManeuverPlan'])
|
|
||||||
def test_actual_controlsd_service_gates(pipeline, maneuver, failed):
|
|
||||||
sm = Subscriptions(maneuver)
|
|
||||||
sm.failed.add(failed)
|
|
||||||
controls = startup()
|
|
||||||
controls.sm, controls.desired_curvature, controls.curvature = sm, 0., 0.
|
|
||||||
cc = structs.CarControl(latActive=True)
|
|
||||||
cs = SimpleNamespace(vEgo=20., yawRate=0., canValid=True, steeringPressed=False, steeringTorque=0.)
|
|
||||||
model = straight()
|
|
||||||
model.action = SimpleNamespace(desiredCurvature=.1)
|
|
||||||
exec(pipeline[0], {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model, 'lp': SimpleNamespace(roll=0.),
|
|
||||||
'clip_curvature': clip_curvature, 'time': SimpleNamespace(monotonic=lambda: 1.)})
|
|
||||||
assert controls.ford_path.valid == cc.latActive == (failed == 'lateralManeuverPlan' and not maneuver)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_feedback_through_actual_controlsd_publication_and_100hz_sender(pipeline, sign):
|
|
||||||
call, publication = pipeline
|
|
||||||
controls, sm = startup(), Subscriptions(False)
|
|
||||||
controls.sm, controls.desired_curvature = sm, sign*.004
|
|
||||||
model = straight(.4)
|
|
||||||
model.action = SimpleNamespace(desiredCurvature=sign*.004)
|
|
||||||
cc = structs.CarControl(latActive=True)
|
|
||||||
cs = SimpleNamespace(vEgo=20., yawRate=.2, canValid=True, steeringPressed=False, steeringTorque=0.)
|
|
||||||
cp = structs.CarParams(flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1')
|
|
||||||
downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, structs.CarParamsSP())
|
|
||||||
vehicle = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int),
|
|
||||||
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
|
|
||||||
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], downstream.CAN.main)
|
|
||||||
frame = 0
|
|
||||||
for measured, torque, count, expected in [(sign*.004, 0., 100, 0.), (sign*.003, 0., 100, sign*.02),
|
|
||||||
(sign*.004, 0., 100, sign*.02), (sign*.005, 0., 100, 0.),
|
|
||||||
(sign*.003, 0., 100, sign*.02), (0., 1.0625, 5, 0.)]:
|
|
||||||
for _ in range(count):
|
|
||||||
now = 1.+frame*.01
|
|
||||||
controls.curvature, cs.steeringTorque = measured, torque
|
|
||||||
sm.logMonoTime.update(carState=round(now*1e9), modelV2=round(now*1e9))
|
|
||||||
environment = {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model,
|
|
||||||
'lp': SimpleNamespace(roll=0.), 'clip_curvature': clip_curvature,
|
|
||||||
'time': SimpleNamespace(monotonic=lambda now=now: now)}
|
|
||||||
exec(call, environment)
|
|
||||||
msg = custom.CarControlSP.new_message()
|
|
||||||
exec(publication, {'self': controls, 'CC_SP': msg})
|
|
||||||
_, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, round(now*1e9))
|
|
||||||
received = parser.update([round(now*1e9), packets])
|
|
||||||
assert parser.dbc.name_to_msg['LateralMotionControl2'].address in received
|
|
||||||
wire = parser.vl['LateralMotionControl2']
|
|
||||||
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-controls.ford_path.path_angle)
|
|
||||||
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-controls.ford_path.path_offset)
|
|
||||||
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
|
|
||||||
assert wire['LatCtl_D2_Rq'] == 2
|
|
||||||
assert wire['LatCtlPath_No_Cnt'] == frame % 16
|
|
||||||
address = parser.dbc.name_to_msg['LateralMotionControl2'].address
|
|
||||||
packet = next(packet for packet in packets if packet[0] == address)
|
|
||||||
assert wire['LatCtlPath_No_Cs'] == calculate_lat_ctl2_checksum(2, frame % 16, packet[1])
|
|
||||||
frame += 1
|
|
||||||
assert controls.ford_path_controller.core.correction == pytest.approx(expected)
|
|
||||||
assert controls.ford_path.path_angle == pytest.approx(sign*.08+expected)
|
|
||||||
assert controls.ford_path.path_offset == pytest.approx(.4)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('service_valid', [False, True])
|
|
||||||
def test_actual_controlsd_passes_only_valid_pscm_service_to_feedback(pipeline, service_valid):
|
|
||||||
controls, sm = startup(), Subscriptions(False)
|
|
||||||
controls.sm, controls.desired_curvature = sm, .004
|
|
||||||
model = straight(.4)
|
|
||||||
model.action = SimpleNamespace(desiredCurvature=.004)
|
|
||||||
cc = structs.CarControl(latActive=True)
|
|
||||||
cs = SimpleNamespace(vEgo=20., yawRate=0., canValid=True, steeringPressed=False, steeringTorque=0.)
|
|
||||||
for frame in range(101):
|
|
||||||
now = 1.+frame*.01
|
|
||||||
controls.curvature = .004 if frame < 100 else .003
|
|
||||||
sm.logMonoTime.update(carState=round(now*1e9), modelV2=round(now*1e9))
|
|
||||||
sm.valid['carStateSP'] = service_valid
|
|
||||||
status = sm['carStateSP'].fordPscmStatus
|
|
||||||
status.valid, status.canMonoTime, status.limit, status.lateralState = True, round(now*1e9), 2, 2
|
|
||||||
exec(pipeline[0], {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model,
|
|
||||||
'lp': SimpleNamespace(roll=0.), 'clip_curvature': clip_curvature,
|
|
||||||
'time': SimpleNamespace(monotonic=lambda now=now: now)})
|
|
||||||
controller = controls.ford_path_controller
|
|
||||||
assert controller.diagnostics['pscm_limited'] is service_valid
|
|
||||||
assert controller.core.correction == pytest.approx(0. if service_valid else .0002)
|
|
||||||
assert cc.latActive and controls.ford_path.valid
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
@pytest.mark.parametrize('maneuver', [False, True])
|
|
||||||
def test_carryover_release_through_selected_limited_request_and_actual_can(pipeline, sign, maneuver):
|
|
||||||
call, publication = pipeline
|
|
||||||
controls, sm = startup(), Subscriptions(maneuver)
|
|
||||||
controls.sm, controls.desired_curvature = sm, sign*.004
|
|
||||||
core = controls.ford_path_controller.core
|
|
||||||
cc = structs.CarControl(latActive=True)
|
|
||||||
cs = SimpleNamespace(vEgo=20., yawRate=0., canValid=True, steeringPressed=False, steeringTorque=0.)
|
|
||||||
cp = structs.CarParams(flags=int(FordFlags.CANFD), carFingerprint='FORD_F_150_LIGHTNING_MK1')
|
|
||||||
downstream = CarController({Bus.pt: 'ford_lincoln_base_pt'}, cp, structs.CarParamsSP())
|
|
||||||
vehicle = SimpleNamespace(out=structs.CarState(vEgo=20., vEgoRaw=20.), acc_tja_status_stock_values=defaultdict(int),
|
|
||||||
lkas_status_stock_values=defaultdict(int), buttons_stock_values=defaultdict(int))
|
|
||||||
parser = CANParser('ford_lincoln_base_pt', [('LateralMotionControl2', 100)], downstream.CAN.main)
|
|
||||||
for frame in range(280):
|
|
||||||
now = 1.+frame*.01
|
|
||||||
desired = sign*(.004 if frame < 200 else -.001)
|
|
||||||
model = straight(sign*(.2 if frame < 200 else -.2))
|
|
||||||
model.action = SimpleNamespace(desiredCurvature=-desired if maneuver else desired)
|
|
||||||
sm.messages['lateralManeuverPlan'].desiredCurvature = desired
|
|
||||||
controls.curvature = sign*(.004 if frame < 100 else .001 if frame < 200 else .003)
|
|
||||||
sm.logMonoTime.update(carState=round(now*1e9), modelV2=round(now*1e9), lateralManeuverPlan=round(now*1e9))
|
|
||||||
before = core.c0, core.c1
|
|
||||||
exec(call, {'self': controls, 'CS': cs, 'CC': cc, 'actuators': cc.actuators, 'model_v2': model,
|
|
||||||
'lp': SimpleNamespace(roll=0.), 'clip_curvature': clip_curvature,
|
|
||||||
'time': SimpleNamespace(monotonic=lambda now=now: now)})
|
|
||||||
assert abs(core.c0-before[0]) <= .0400000001 and abs(core.c1-before[1]) <= .0050000001
|
|
||||||
msg = custom.CarControlSP.new_message()
|
|
||||||
exec(publication, {'self': controls, 'CC_SP': msg})
|
|
||||||
_, packets = downstream.update(cc.as_reader(), convert_carControlSP(msg.as_reader()), vehicle, round(now*1e9))
|
|
||||||
received = parser.update([round(now*1e9), packets])
|
|
||||||
address = parser.dbc.name_to_msg['LateralMotionControl2'].address
|
|
||||||
assert address in received
|
|
||||||
wire = parser.vl['LateralMotionControl2']
|
|
||||||
assert wire['LatCtlPath_An_Actl'] == pytest.approx(-controls.ford_path.path_angle)
|
|
||||||
assert wire['LatCtlPathOffst_L_Actl'] == pytest.approx(-controls.ford_path.path_offset)
|
|
||||||
assert wire['LatCtlCurv_No_Actl'] == wire['LatCtlCrv_NoRate2_Actl'] == 0.
|
|
||||||
assert wire['LatCtl_D2_Rq'] == 2 and wire['LatCtlPath_No_Cnt'] == frame % 16
|
|
||||||
packet = next(packet for packet in packets if packet[0] == address)
|
|
||||||
assert wire['LatCtlPath_No_Cs'] == calculate_lat_ctl2_checksum(2, frame % 16, packet[1])
|
|
||||||
if frame == 199:
|
|
||||||
assert core.correction == pytest.approx(sign*.06)
|
|
||||||
assert core.carryover_release_count == 0
|
|
||||||
assert core.carryover_release_count == 1
|
|
||||||
assert controls.ford_path_controller.diagnostics['carryover_release_count'] == 1
|
|
||||||
assert controls.ford_path_controller.diagnostics['hypothesis'] == 'model-action-c1-feedback-v2'
|
|
||||||
assert sign*controls.ford_path.path_angle < 0.
|
|
||||||
assert controls.ford_path.path_offset == pytest.approx(-sign*.2)
|
|
||||||
controls.ford_path_controller.reset()
|
|
||||||
assert core.carryover_release_count == 0
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
"""C1 feedback behavior; these tests do not simulate a Ford steering plant."""
|
|
||||||
import math
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, ModelActionController
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath
|
|
||||||
from openpilot.selfdrive.controls.tests.test_ford_model_action import straight
|
|
||||||
|
|
||||||
|
|
||||||
def tick(controller, desired, measured, **overrides):
|
|
||||||
kwargs = {'current_curvature': measured, 'speed': 20., 'dt': .01}
|
|
||||||
kwargs.update(overrides)
|
|
||||||
return controller.update(straight(.4), desired, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_old_turn_correction_does_not_keep_c1_in_old_direction_after_reversal(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
# Build an actual correction through feedback, rather than injecting a state.
|
|
||||||
for _ in range(100):
|
|
||||||
controller.update(straight(), sign*.004, current_curvature=sign*.004, speed=20., dt=.01)
|
|
||||||
for _ in range(100):
|
|
||||||
controller.update(straight(), sign*.004, current_curvature=sign*.001, speed=20., dt=.01)
|
|
||||||
assert controller.correction == pytest.approx(sign*.06)
|
|
||||||
# The request has reversed, but measured steering still points into the old
|
|
||||||
# turn. C0 also confirms the new direction. The existing slew permits
|
|
||||||
# crossing zero within these 0.4 seconds.
|
|
||||||
for _ in range(40):
|
|
||||||
before = controller.c1
|
|
||||||
out = controller.update(straight(-sign*.2), -sign*.001, current_curvature=sign*.003, speed=20., dt=.01)
|
|
||||||
assert abs(controller.c1-before) <= .0050000001
|
|
||||||
assert sign*out.path_angle < 0., 'Stored old-turn correction still overrides the new C1 direction'
|
|
||||||
assert out.path_offset == pytest.approx(-sign*.2)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
@pytest.mark.parametrize('limited', [False, True])
|
|
||||||
def test_carryover_release_uses_fresh_feedback_and_preserves_final_slew(sign, limited):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.c0, controller.c1, controller.correction = -sign*.2, sign*.03, sign*.05
|
|
||||||
kwargs = {'speed': 20., 'dt': .01, 'current_curvature': sign*.003, 'pscm_limited': limited}
|
|
||||||
controller.update(straight(-sign*.2), -sign*.001, feedback_dt=0., **kwargs)
|
|
||||||
assert controller.correction == pytest.approx(sign*.05)
|
|
||||||
before = controller.c1
|
|
||||||
out = controller.update(straight(-sign*.2), -sign*.001, **kwargs)
|
|
||||||
assert controller.correction == 0.
|
|
||||||
assert controller.carryover_release_count == 1
|
|
||||||
assert out.path_angle == pytest.approx(before-sign*.005)
|
|
||||||
for _ in range(30):
|
|
||||||
controller.update(straight(-sign*.2), -sign*.001, **kwargs)
|
|
||||||
assert controller.carryover_release_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
@pytest.mark.parametrize('case', ['matched', 'same_turn', 'already_turning_new_way', 'c0_opposes', 'c0_neutral',
|
|
||||||
'c0_sub_resolution', 'target_c0_opposes', 'c1_sub_resolution',
|
|
||||||
'neutral_request', 'neutral_measurement', 'correction_helps', 'correction_not_dominant'])
|
|
||||||
def test_carryover_release_preserves_steady_correction_and_ambiguous_requests(sign, case):
|
|
||||||
controller = ModelActionController()
|
|
||||||
offset, desired, measured, correction = -.2, -.001, .003, .05
|
|
||||||
if case == 'matched':
|
|
||||||
measured = desired
|
|
||||||
elif case == 'same_turn':
|
|
||||||
measured = -.0005
|
|
||||||
elif case == 'already_turning_new_way':
|
|
||||||
measured = -.003
|
|
||||||
elif case == 'c0_opposes':
|
|
||||||
offset = .2
|
|
||||||
elif case == 'c0_neutral':
|
|
||||||
offset = 0.
|
|
||||||
elif case == 'c0_sub_resolution':
|
|
||||||
offset = -.001
|
|
||||||
elif case == 'c1_sub_resolution':
|
|
||||||
desired = -.000001
|
|
||||||
elif case == 'neutral_request':
|
|
||||||
desired = 0.
|
|
||||||
elif case == 'neutral_measurement':
|
|
||||||
measured = 0.
|
|
||||||
elif case == 'correction_helps':
|
|
||||||
correction = -.05
|
|
||||||
elif case == 'correction_not_dominant':
|
|
||||||
correction = .01
|
|
||||||
controller.c0, controller.c1, controller.correction = sign*offset, sign*(20.*desired+correction), sign*correction
|
|
||||||
if case == 'target_c0_opposes':
|
|
||||||
offset = .2 # The old slewed C0 alone is not enough to confirm the request.
|
|
||||||
controller.update(straight(sign*offset), sign*desired, current_curvature=sign*measured, speed=20., dt=.01)
|
|
||||||
assert controller.carryover_release_count == 0
|
|
||||||
assert abs(controller.correction-sign*correction) <= abs(desired-measured)*20.*.01+1e-10
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_carryover_release_waits_for_c0_to_finish_opposing_the_new_request(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.c0, controller.c1, controller.correction = sign*.2, sign*.03, sign*.05
|
|
||||||
for _ in range(4):
|
|
||||||
controller.update(straight(-sign*.2), -sign*.001, current_curvature=sign*.003, speed=20., dt=.01)
|
|
||||||
assert controller.carryover_release_count == 0
|
|
||||||
for _ in range(3):
|
|
||||||
controller.update(straight(-sign*.2), -sign*.001, current_curvature=sign*.003, speed=20., dt=.01)
|
|
||||||
assert controller.carryover_release_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_steady_opposing_correction_is_preserved_through_small_error_crossings(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.c0, controller.c1, controller.correction = -sign*.2, sign*.03, sign*.05
|
|
||||||
for i in range(200):
|
|
||||||
measured = sign*(-.001+(-1 if i % 2 else 1)*.000001)
|
|
||||||
controller.update(straight(-sign*.2), -sign*.001, current_curvature=measured, speed=20., dt=.01)
|
|
||||||
assert controller.carryover_release_count == 0
|
|
||||||
assert controller.correction == pytest.approx(sign*.05)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
@pytest.mark.parametrize('offset', [.009999, .01, .010001])
|
|
||||||
@pytest.mark.parametrize('heading', [.0004999, .0005, .0005001])
|
|
||||||
def test_carryover_direction_confirmation_uses_existing_dbc_steps(sign, offset, heading):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.c0, controller.c1, controller.correction = -sign*offset, sign*.03, sign*.05
|
|
||||||
controller.update(straight(-sign*offset), -sign*heading/20., current_curvature=sign*.003, speed=20., dt=.01)
|
|
||||||
assert controller.carryover_release_count == int(offset >= .01 and heading >= .0005)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_feedback_builds_holds_and_unwinds_without_changing_c0(sign):
|
|
||||||
controller, matched = ModelActionController(), ModelActionController()
|
|
||||||
for _ in range(100):
|
|
||||||
tick(controller, sign*.004, sign*.004)
|
|
||||||
for _ in range(100):
|
|
||||||
out = tick(controller, sign*.004, sign*.003)
|
|
||||||
baseline = tick(matched, sign*.004, sign*.004)
|
|
||||||
assert controller.correction == pytest.approx(sign*.02)
|
|
||||||
assert out.path_angle == pytest.approx(sign*.1)
|
|
||||||
assert out.path_offset == baseline.path_offset == pytest.approx(.4)
|
|
||||||
for _ in range(100):
|
|
||||||
out = tick(controller, sign*.004, sign*.004)
|
|
||||||
assert controller.correction == pytest.approx(sign*.02)
|
|
||||||
assert out.path_angle == pytest.approx(sign*.1)
|
|
||||||
for _ in range(200):
|
|
||||||
out = tick(controller, sign*.004, sign*.005)
|
|
||||||
assert controller.correction == pytest.approx(-sign*.02)
|
|
||||||
assert out.path_angle == pytest.approx(sign*.06)
|
|
||||||
assert out.curvature == out.curvature_rate == 0.
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_amplitude_and_slew_limits_do_not_store_unavailable_feedback(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
# The unchanged model request is already ahead of the output slew.
|
|
||||||
for _ in range(10):
|
|
||||||
tick(controller, sign*.01, 0.)
|
|
||||||
assert controller.correction == 0.
|
|
||||||
for _ in range(1000):
|
|
||||||
before = controller.c1
|
|
||||||
tick(controller, sign*.01, -sign*.9)
|
|
||||||
assert abs(controller.c1-before) <= .0050000001
|
|
||||||
assert abs(controller.correction) <= .3000000001
|
|
||||||
assert controller.c1 == pytest.approx(sign*.5)
|
|
||||||
assert controller.correction == pytest.approx(sign*.3)
|
|
||||||
for _ in range(200):
|
|
||||||
tick(controller, sign*.01, 0.)
|
|
||||||
assert controller.correction == pytest.approx(sign*.3)
|
|
||||||
tick(controller, sign*.01, sign*.02)
|
|
||||||
assert sign*controller.correction < .3 # Unwind is allowed at the cap.
|
|
||||||
assert sign*controller.c1 < .5
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_pscm_limit_only_blocks_feedback_further_into_measured_turn(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
for _ in range(100):
|
|
||||||
tick(controller, sign*.004, sign*.004)
|
|
||||||
for _ in range(100):
|
|
||||||
tick(controller, sign*.004, sign*.003, pscm_limited=True)
|
|
||||||
assert controller.correction == 0.
|
|
||||||
out = tick(controller, sign*.004, sign*.005, pscm_limited=True)
|
|
||||||
assert sign*controller.correction < 0.
|
|
||||||
# A limit cannot stall the new model request itself or its unwind slew.
|
|
||||||
for _ in range(100):
|
|
||||||
out = tick(controller, 0., 0., pscm_limited=True)
|
|
||||||
assert abs(out.path_angle) < .001
|
|
||||||
assert out.path_offset == pytest.approx(.4)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('sign', [-1., 1.])
|
|
||||||
def test_pscm_limit_cannot_trap_old_correction_below_the_model_request(sign):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.correction = -sign*.02
|
|
||||||
controller.c1 = sign*.06
|
|
||||||
for _ in range(200):
|
|
||||||
out = tick(controller, sign*.004, sign*.003, pscm_limited=True)
|
|
||||||
assert controller.correction == pytest.approx(0.)
|
|
||||||
assert out.path_angle == pytest.approx(sign*.08)
|
|
||||||
|
|
||||||
|
|
||||||
def test_driver_intervention_clears_feedback_through_existing_output_slew():
|
|
||||||
controller = ModelActionController()
|
|
||||||
for _ in range(100):
|
|
||||||
tick(controller, .004, .004)
|
|
||||||
for _ in range(100):
|
|
||||||
tick(controller, .004, .003)
|
|
||||||
assert controller.correction > 0.
|
|
||||||
previous = controller.c1
|
|
||||||
tick(controller, .004, -.01, feedback_enabled=False)
|
|
||||||
assert controller.correction == 0.
|
|
||||||
assert abs(controller.c1-previous) <= .0050000001
|
|
||||||
for _ in range(100):
|
|
||||||
out = tick(controller, .004, -.01, feedback_enabled=False)
|
|
||||||
assert controller.correction == 0.
|
|
||||||
assert out.path_angle == pytest.approx(.08)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('field,value', [('current_curvature', math.nan), ('current_curvature', None),
|
|
||||||
('current_curvature', 1.01), ('feedback_dt', math.nan),
|
|
||||||
('feedback_dt', -.001), ('feedback_dt', .151), ('active', False)])
|
|
||||||
def test_bad_feedback_inputs_and_disengagement_clear_every_control_state(field, value):
|
|
||||||
controller = ModelActionController()
|
|
||||||
controller.correction = .03
|
|
||||||
out = tick(controller, .004, .003, **{field: value})
|
|
||||||
assert out == FordPath()
|
|
||||||
assert (controller.c0, controller.c1, controller.correction) == (0., 0., 0.)
|
|
||||||
|
|
||||||
|
|
||||||
def adapter_tick(controller, now, **overrides):
|
|
||||||
kwargs = {'current_curvature': .003, 'speed': 20., 'yaw_rate': 0., 'now': now,
|
|
||||||
'measurement_time': now, 'model_time': now, 'reference_time': now, 'active': True}
|
|
||||||
kwargs.update(overrides)
|
|
||||||
return controller.update(straight(.4), .004, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def status(now, **overrides):
|
|
||||||
fields = {'valid': True, 'canMonoTime': round(now*1e9), 'limit': 0, 'lateralState': 2, 'denied': False}
|
|
||||||
fields.update(overrides)
|
|
||||||
return SimpleNamespace(**fields)
|
|
||||||
|
|
||||||
|
|
||||||
def test_repeated_steering_samples_only_advance_output_slew():
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(100):
|
|
||||||
adapter_tick(controller, 1.+i*.01, current_curvature=.004)
|
|
||||||
before = controller.core.correction
|
|
||||||
for i in range(1, 6):
|
|
||||||
adapter_tick(controller, 1.99+i*.01, measurement_time=1.99)
|
|
||||||
assert controller.core.correction == before
|
|
||||||
adapter_tick(controller, 2.05)
|
|
||||||
assert controller.core.correction == pytest.approx(.02*.06)
|
|
||||||
assert controller.diagnostics['feedback_dt'] == pytest.approx(.06)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides', [{'driver_pressed': True}, {'driver_torque': 1.01},
|
|
||||||
{'driver_torque': -1.01}, {'driver_torque': math.nan},
|
|
||||||
{'pscm_status': status(2.01, limit=3)},
|
|
||||||
{'pscm_status': status(2.01, denied=True)},
|
|
||||||
{'pscm_status': status(2.01, lateralState=1)}])
|
|
||||||
def test_adapter_clears_feedback_when_driver_or_pscm_overrides(overrides):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(101):
|
|
||||||
adapter_tick(controller, 1.+i*.01)
|
|
||||||
assert controller.core.correction > 0.
|
|
||||||
assert adapter_tick(controller, 2.01, **overrides).valid
|
|
||||||
assert controller.core.correction == 0.
|
|
||||||
assert not controller.diagnostics['feedback_enabled']
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides,limited', [({}, True), ({'valid': False}, False),
|
|
||||||
({'canMonoTime': 0}, False), ({'canMonoTime': 1_800_000_000}, False),
|
|
||||||
({'canMonoTime': 2_020_000_000}, False), ({'limit': 1}, False)])
|
|
||||||
def test_only_fresh_reached_pscm_limit_blocks_outward_integration(overrides, limited):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(100):
|
|
||||||
adapter_tick(controller, 1.+i*.01, current_curvature=.004)
|
|
||||||
adapter_tick(controller, 2., pscm_status=status(2., **{'limit': 2, **overrides}))
|
|
||||||
assert controller.diagnostics['pscm_limited'] is limited
|
|
||||||
assert (controller.core.correction == 0.) is limited
|
|
||||||
|
|
||||||
|
|
||||||
def test_measurement_cadence_preserves_elapsed_distance_integration():
|
|
||||||
results = []
|
|
||||||
for period in (1, 2, 5):
|
|
||||||
controller = FordModelActionController()
|
|
||||||
for i in range(101):
|
|
||||||
now = 1.+i*.01
|
|
||||||
adapter_tick(controller, now, current_curvature=.004)
|
|
||||||
for i in range(1, 101):
|
|
||||||
now = 2.+i*.01
|
|
||||||
adapter_tick(controller, now, measurement_time=2.+(i//period)*period*.01)
|
|
||||||
results.append(controller.core.correction)
|
|
||||||
assert results == pytest.approx([.02, .02, .02])
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
"""Exercise real startup selection and Sunnylink writes without starting hardware."""
|
|
||||||
import ast
|
|
||||||
import base64
|
|
||||||
import itertools
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from opendbc.car.ford.values import CAR, FordFlags
|
|
||||||
from openpilot.common.params import Params, ParamKeyFlag, ParamKeyType
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_model_action import FordModelActionController, select_model_action_controller
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController
|
|
||||||
|
|
||||||
|
|
||||||
CANFD_CARS = [car for car in CAR if car.config.flags & FordFlags.CANFD]
|
|
||||||
|
|
||||||
|
|
||||||
def car_params(**overrides):
|
|
||||||
return SimpleNamespace(**({'brand': 'ford', 'flags': FordFlags.CANFD, 'carFingerprint': 'FORD_F_150_LIGHTNING_MK1',
|
|
||||||
'carFw': []} | overrides))
|
|
||||||
|
|
||||||
|
|
||||||
def startup(cp=None, params=None):
|
|
||||||
filename = Path(__file__).resolve().parents[1]/'controlsd.py'
|
|
||||||
tree = ast.parse(filename.read_text())
|
|
||||||
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Controls')
|
|
||||||
body = next(n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == '__init__').body
|
|
||||||
start = next(i for i, n in enumerate(body) if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'self.ford_pscm_observer')
|
|
||||||
end = next(i for i, n in enumerate(body) if isinstance(n, ast.Assign) and ast.unparse(n.targets[0]) == 'self.ford_path')
|
|
||||||
if params is None:
|
|
||||||
params = SimpleNamespace(get_bool=lambda key: key == 'FordModelActionController')
|
|
||||||
controls = SimpleNamespace(CP=cp or car_params(), params=params)
|
|
||||||
environment = {'self': controls, 'FordFlags': FordFlags, 'FordPath': FordPath,
|
|
||||||
'FordPathController': FordPathController, 'FordPscmObserverPathController': FordPscmObserverPathController,
|
|
||||||
'FordModelActionController': FordModelActionController,
|
|
||||||
'select_model_action_controller': select_model_action_controller,
|
|
||||||
'cloudlog': SimpleNamespace(event=lambda *args, **kwargs: None)}
|
|
||||||
exec(compile(ast.Module(body=body[start:end+1], type_ignores=[]), str(filename), 'exec'), environment)
|
|
||||||
return controls
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('candidate,observer', list(itertools.product((False, True), repeat=2)))
|
|
||||||
@pytest.mark.parametrize('fingerprint', [*CANFD_CARS, 'FORD_FUTURE_CANFD'])
|
|
||||||
def test_actual_startup_priority(candidate, observer, fingerprint):
|
|
||||||
settings = {'FordModelActionController': candidate, 'FordPscmObserver': observer}
|
|
||||||
selected = startup(car_params(carFingerprint=fingerprint), params=SimpleNamespace(get_bool=settings.__getitem__))
|
|
||||||
previous = FordPscmObserverPathController if observer else FordPathController
|
|
||||||
expected = FordModelActionController if candidate else previous
|
|
||||||
assert type(selected.ford_path_controller) is expected
|
|
||||||
assert selected.ford_model_action == candidate
|
|
||||||
assert selected.ford_path == FordPath()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('overrides', [{'brand': 'tesla'}, {'flags': 0}, {'flags': 8}])
|
|
||||||
@pytest.mark.parametrize('observer', [False, True])
|
|
||||||
def test_other_vehicles_keep_their_previous_selection(overrides, observer):
|
|
||||||
settings = {'FordModelActionController': False, 'FordPscmObserver': observer}
|
|
||||||
params = SimpleNamespace(get_bool=settings.__getitem__)
|
|
||||||
before = startup(car_params(**overrides), params)
|
|
||||||
settings['FordModelActionController'] = True
|
|
||||||
after = startup(car_params(**overrides), params)
|
|
||||||
assert type(after.ford_path_controller) is type(before.ford_path_controller)
|
|
||||||
assert not after.ford_model_action
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('firmware', [[], [SimpleNamespace(ecu='eps', fwVersion=b'other')]])
|
|
||||||
def test_candidate_does_not_depend_on_eps_firmware_query(firmware):
|
|
||||||
assert isinstance(startup(car_params(carFw=firmware)).ford_path_controller, FordModelActionController)
|
|
||||||
|
|
||||||
|
|
||||||
def test_candidate_accepts_canfd_with_additional_flags():
|
|
||||||
assert isinstance(startup(car_params(flags=FordFlags.CANFD | 8)).ford_path_controller, FordModelActionController)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('observer', [False, True])
|
|
||||||
@pytest.mark.parametrize('fingerprint', CANFD_CARS)
|
|
||||||
def test_sunnylink_write_takes_effect_on_restart_and_restores_stored_selection(tmp_path, monkeypatch, observer, fingerprint):
|
|
||||||
from openpilot.sunnypilot.sunnylink import utils
|
|
||||||
|
|
||||||
params = Params(str(tmp_path))
|
|
||||||
monkeypatch.setattr(utils, 'Params', lambda: params)
|
|
||||||
assert params.get_default_value('FordModelActionController') is False
|
|
||||||
assert params.get_type('FordModelActionController') == ParamKeyType.BOOL
|
|
||||||
assert b'FordModelActionController' in params.all_keys(ParamKeyFlag.PERSISTENT)
|
|
||||||
assert b'FordModelActionController' in params.all_keys(ParamKeyFlag.BACKUP)
|
|
||||||
params.put_bool('FordPscmObserver', observer, block=True)
|
|
||||||
cp = car_params(carFingerprint=fingerprint)
|
|
||||||
old = startup(cp, params=params)
|
|
||||||
assert not isinstance(old.ford_path_controller, FordModelActionController)
|
|
||||||
utils.save_param_from_base64_encoded_string('FordModelActionController', base64.b64encode(b'true').decode())
|
|
||||||
enabled = startup(cp, params=params)
|
|
||||||
assert isinstance(enabled.ford_path_controller, FordModelActionController)
|
|
||||||
assert not isinstance(old.ford_path_controller, FordModelActionController)
|
|
||||||
utils.save_param_from_base64_encoded_string('FordModelActionController', base64.b64encode(b'false').decode())
|
|
||||||
assert isinstance(enabled.ford_path_controller, FordModelActionController)
|
|
||||||
assert type(startup(cp, params=params).ford_path_controller) is type(old.ford_path_controller)
|
|
||||||
assert params.get_bool('FordPscmObserver') == observer
|
|
||||||
|
|
||||||
|
|
||||||
def test_stored_retired_toggle_cannot_enable_the_candidate(tmp_path):
|
|
||||||
params = Params(str(tmp_path))
|
|
||||||
Path(params.get_param_path('FordVirtualAngleController')).write_text('1')
|
|
||||||
assert b'FordVirtualAngleController' not in params.all_keys()
|
|
||||||
assert params.get_bool('FordModelActionController') is False
|
|
||||||
assert type(startup(params=params).ford_path_controller) is FordPathController
|
|
||||||
params.put_bool('FordModelActionController', True, block=True)
|
|
||||||
params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START)
|
|
||||||
assert not Path(params.get_param_path('FordVirtualAngleController')).exists()
|
|
||||||
assert params.get_bool('FordModelActionController') is True
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
import math
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.selfdrive.car.helpers import convert_carControlSP
|
|
||||||
from openpilot.selfdrive.controls.lib.ford_path import (DBC_ANGLE, DBC_CURVATURE, DBC_OFFSET, FordPath, FordPathController,
|
|
||||||
FordPscmObserver, FordPscmObserverPathController, FordPscmState,
|
|
||||||
_bounded_feedback, _encode_path, _model_path, _predicted_pose,
|
|
||||||
_pscm_contributions, _relative_pose)
|
|
||||||
|
|
||||||
|
|
||||||
def _path(curvature: float, speed: float = 8.0):
|
|
||||||
t = np.linspace(0.0, 3.0, 61)
|
|
||||||
distance = speed * t
|
|
||||||
heading = curvature * distance
|
|
||||||
x = np.zeros_like(distance)
|
|
||||||
y = np.zeros_like(distance)
|
|
||||||
for i in range(1, len(distance)):
|
|
||||||
ds = distance[i] - distance[i - 1]
|
|
||||||
average_heading = 0.5 * (heading[i] + heading[i - 1])
|
|
||||||
x[i] = x[i - 1] + ds * math.cos(average_heading)
|
|
||||||
y[i] = y[i - 1] + ds * math.sin(average_heading)
|
|
||||||
return SimpleNamespace(
|
|
||||||
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
|
|
||||||
orientation=SimpleNamespace(z=heading.tolist()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _changing_path(start_curvature: float, end_curvature: float, speed: float = 8.0):
|
|
||||||
t = np.linspace(0.0, 3.0, 61)
|
|
||||||
distance = speed * t
|
|
||||||
curvature = np.interp(distance, [distance[0], min(distance[-1], 7.0)], [start_curvature, end_curvature])
|
|
||||||
heading = np.zeros_like(distance)
|
|
||||||
x = np.zeros_like(distance)
|
|
||||||
y = np.zeros_like(distance)
|
|
||||||
for i in range(1, len(distance)):
|
|
||||||
ds = distance[i] - distance[i - 1]
|
|
||||||
heading[i] = heading[i - 1] + 0.5 * (curvature[i] + curvature[i - 1]) * ds
|
|
||||||
average_heading = 0.5 * (heading[i] + heading[i - 1])
|
|
||||||
x[i] = x[i - 1] + ds * math.cos(average_heading)
|
|
||||||
y[i] = y[i - 1] + ds * math.sin(average_heading)
|
|
||||||
return SimpleNamespace(
|
|
||||||
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
|
|
||||||
orientation=SimpleNamespace(z=heading.tolist()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _command(model, desired_curvature: float, *, current_curvature: float = 0.0, v_ego: float = 8.0):
|
|
||||||
return FordPathController(dt=1.0).update(model, desired_curvature, current_curvature=current_curvature, v_ego=v_ego)
|
|
||||||
|
|
||||||
|
|
||||||
def _equivalent_curvature(command) -> float:
|
|
||||||
return 2.0 * command.path_offset / 7.0 ** 2 + 2.0 * command.path_angle / 7.0 + command.curvature
|
|
||||||
|
|
||||||
|
|
||||||
def test_gentle_path_uses_only_c2():
|
|
||||||
command = _command(_path(0.004, speed=20.0), 0.004, current_curvature=0.004, v_ego=20.0)
|
|
||||||
assert command.valid
|
|
||||||
assert command.path_offset == 0.0
|
|
||||||
assert command.path_angle == 0.0
|
|
||||||
assert np.isclose(command.curvature, 0.004, atol=1e-6)
|
|
||||||
assert command.curvature_rate == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_gentle_path_uses_only_c2_when_model_and_action_disagree():
|
|
||||||
command = _command(_path(0.005), 0.002, current_curvature=0.005)
|
|
||||||
assert command.path_offset == 0.0
|
|
||||||
assert command.path_angle == 0.0
|
|
||||||
assert np.isclose(command.curvature, 0.002, atol=1e-6)
|
|
||||||
|
|
||||||
|
|
||||||
def test_spatially_growing_path_adds_fast_pose_before_action_becomes_large():
|
|
||||||
controller = FordPathController(dt=1.0)
|
|
||||||
command = controller.update(_changing_path(0.0, 0.04), 0.012, current_curvature=0.0, v_ego=8.0)
|
|
||||||
assert command.path_offset > 0.0
|
|
||||||
assert command.path_angle > 0.0
|
|
||||||
assert command.curvature < 0.012
|
|
||||||
assert command.curvature_rate == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_growing_model_pose_adds_authority_but_c3_is_never_transmitted():
|
|
||||||
constant = _command(_path(0.012), 0.012)
|
|
||||||
growing = _command(_changing_path(0.0, 0.04), 0.012)
|
|
||||||
assert _equivalent_curvature(growing) > _equivalent_curvature(constant)
|
|
||||||
assert constant.curvature_rate == 0.0
|
|
||||||
assert growing.curvature_rate == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_local_tracking_error_corrects_without_replacing_forward_pose():
|
|
||||||
model = _changing_path(0.0, 0.04)
|
|
||||||
local_curvature = 0.5 * 0.04 * 2.0 / 7.0
|
|
||||||
aligned = _command(model, 0.012, current_curvature=local_curvature)
|
|
||||||
under = _command(model, 0.012, current_curvature=0.0)
|
|
||||||
assert aligned.path_offset > 0.0
|
|
||||||
assert aligned.path_angle > 0.0
|
|
||||||
assert under.path_offset > aligned.path_offset
|
|
||||||
assert under.path_angle > aligned.path_angle
|
|
||||||
|
|
||||||
|
|
||||||
def test_large_maneuver_uses_fast_pose_and_zeros_c2():
|
|
||||||
command = _command(_path(0.04), 0.04)
|
|
||||||
assert command.path_offset > 0.5
|
|
||||||
assert command.path_angle > 0.2
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
assert command.curvature_rate == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_pose_can_trigger_maneuver_when_action_is_late():
|
|
||||||
command = _command(_path(0.04), 0.002)
|
|
||||||
assert command.path_offset > 0.5
|
|
||||||
assert command.path_angle > 0.2
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_gentle_model_pose_does_not_replace_a_collapsed_action():
|
|
||||||
command = _command(_path(0.005), 0.0, current_curvature=0.005)
|
|
||||||
assert command.path_offset == 0.0
|
|
||||||
assert command.path_angle == 0.0
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_changing_gentle_curve_keeps_upstream_strength_c2():
|
|
||||||
command = _command(_changing_path(0.0, 0.008), 0.004, current_curvature=0.0)
|
|
||||||
assert np.isclose(command.curvature, 0.004)
|
|
||||||
assert command.path_offset == 0.0
|
|
||||||
assert command.path_angle == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_action_only_maneuver_cannot_invent_large_model_pose():
|
|
||||||
command = _command(_path(0.002), 0.04)
|
|
||||||
assert 0.0 < command.path_offset < 0.1
|
|
||||||
assert 0.0 < command.path_angle < 0.03
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_nearby_demands_blend_continuously_without_a_mode_threshold():
|
|
||||||
low = _command(_path(0.0119), 0.0119)
|
|
||||||
high = _command(_path(0.0121), 0.0121)
|
|
||||||
assert abs(high.path_offset - low.path_offset) < 0.05
|
|
||||||
assert abs(high.path_angle - low.path_angle) < 0.03
|
|
||||||
assert abs(high.curvature - low.curvature) < 0.001
|
|
||||||
|
|
||||||
|
|
||||||
def test_leaving_c2_normal_band_does_not_drop_total_authority():
|
|
||||||
normal = _command(_path(0.006), 0.006)
|
|
||||||
transition = _command(_path(0.0061), 0.0061)
|
|
||||||
assert transition.curvature <= normal.curvature
|
|
||||||
assert _equivalent_curvature(transition) >= _equivalent_curvature(normal)
|
|
||||||
|
|
||||||
|
|
||||||
def test_low_speed_still_uses_available_model_pose():
|
|
||||||
command = _command(_path(0.04, speed=2.0), 0.04, v_ego=2.0)
|
|
||||||
assert command.path_offset > 0.0
|
|
||||||
assert command.path_angle > 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_higher_speed_advances_predicted_pose_and_extends_heading_horizon():
|
|
||||||
model = _changing_path(0.0, 0.015, speed=20.0)
|
|
||||||
slow = _command(model, 0.012, v_ego=7.0)
|
|
||||||
fast = _command(model, 0.012, v_ego=20.0)
|
|
||||||
assert fast.path_offset > slow.path_offset
|
|
||||||
assert fast.path_angle > slow.path_angle
|
|
||||||
|
|
||||||
|
|
||||||
def test_short_model_uses_available_endpoint():
|
|
||||||
model = _path(0.04, speed=1.0)
|
|
||||||
command = _command(model, 0.04, v_ego=1.0)
|
|
||||||
assert command.valid
|
|
||||||
assert command.path_offset > 0.0
|
|
||||||
assert command.path_angle > 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_entry_coordinates_c2_release_with_fast_pose_attack():
|
|
||||||
controller = FordPathController(dt=0.01)
|
|
||||||
for _ in range(20):
|
|
||||||
assert controller.update(_path(0.004), 0.004, v_ego=8.0).curvature > 0.0
|
|
||||||
outputs = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
|
|
||||||
assert 0.0 < outputs[0].curvature < 0.004
|
|
||||||
assert outputs[0].path_offset > 0.0
|
|
||||||
assert outputs[0].path_angle > 0.0
|
|
||||||
assert outputs[-1].curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_exit_allows_c2_to_take_over_while_fast_pose_drains():
|
|
||||||
controller = FordPathController(dt=0.01)
|
|
||||||
for _ in range(20):
|
|
||||||
controller.update(_path(0.04), 0.04, current_curvature=0.02, v_ego=8.0)
|
|
||||||
outputs = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
|
|
||||||
assert 0.0 < outputs[0].curvature < 0.004
|
|
||||||
assert outputs[0].path_offset != 0.0 or outputs[0].path_angle != 0.0
|
|
||||||
assert outputs[-1].path_offset == 0.0
|
|
||||||
assert outputs[-1].path_angle == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_100hz_handoff_preserves_total_authority_without_entry_drop_or_exit_overshoot():
|
|
||||||
controller = FordPathController(dt=0.01)
|
|
||||||
normal = controller.update(_path(0.006), 0.006, current_curvature=0.006, v_ego=8.0)
|
|
||||||
entries = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
|
|
||||||
entry_authority = np.asarray([_equivalent_curvature(command) for command in entries])
|
|
||||||
assert np.all(np.diff(entry_authority) >= -1e-9)
|
|
||||||
assert entry_authority[0] >= _equivalent_curvature(normal)
|
|
||||||
|
|
||||||
exits = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
|
|
||||||
exit_authority = np.asarray([_equivalent_curvature(command) for command in exits])
|
|
||||||
assert np.all(np.diff(exit_authority) <= 1e-9)
|
|
||||||
assert np.all(exit_authority >= 0.004 - 1e-9)
|
|
||||||
|
|
||||||
|
|
||||||
def test_measured_tracking_error_closes_bidirectionally_without_abandoning_the_turn():
|
|
||||||
model = _path(0.04)
|
|
||||||
under = _command(model, 0.04, current_curvature=0.005)
|
|
||||||
on_target = _command(model, 0.04, current_curvature=0.04)
|
|
||||||
over = _command(model, 0.04, current_curvature=0.05)
|
|
||||||
assert under.path_offset > on_target.path_offset
|
|
||||||
assert under.path_angle > on_target.path_angle
|
|
||||||
assert 0.0 < over.path_offset < on_target.path_offset
|
|
||||||
assert 0.0 < over.path_angle < on_target.path_angle
|
|
||||||
|
|
||||||
|
|
||||||
def test_gentle_curve_does_not_add_fast_tracking_trim():
|
|
||||||
model = _path(0.004)
|
|
||||||
under = _command(model, 0.004, current_curvature=0.002)
|
|
||||||
on_target = _command(model, 0.004, current_curvature=0.004)
|
|
||||||
over = _command(model, 0.004, current_curvature=0.006)
|
|
||||||
assert under.path_offset == on_target.path_offset == over.path_offset == 0.0
|
|
||||||
assert under.path_angle == on_target.path_angle == over.path_angle == 0.0
|
|
||||||
assert np.allclose([under.curvature, on_target.curvature, over.curvature], 0.004, atol=2e-6)
|
|
||||||
|
|
||||||
|
|
||||||
def test_overshoot_trim_cannot_erase_a_modeled_turn():
|
|
||||||
model = _path(0.04)
|
|
||||||
on_target = _command(model, 0.04, current_curvature=0.04)
|
|
||||||
over = _command(model, 0.04, current_curvature=0.06)
|
|
||||||
assert over.path_offset > 0.95 * on_target.path_offset
|
|
||||||
assert over.path_angle > 0.9 * on_target.path_angle
|
|
||||||
|
|
||||||
|
|
||||||
def test_corrupt_measured_curvature_cannot_reverse_a_modeled_turn():
|
|
||||||
command = _command(_path(0.04), 0.04, current_curvature=0.5)
|
|
||||||
assert command.path_offset > 0.0
|
|
||||||
assert command.path_angle > 0.0
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_feedback_preserves_half_lsb_feedforward_direction():
|
|
||||||
for feedforward, resolution in ((0.006, 0.01), (0.0004, 0.0005)):
|
|
||||||
result = feedforward + _bounded_feedback(feedforward, -1.0, resolution, 1.0)
|
|
||||||
assert result >= 0.5 * resolution
|
|
||||||
|
|
||||||
|
|
||||||
def test_recent_curvature_trend_advances_vehicle_pose_without_a_response_gain():
|
|
||||||
model = _model_path(_path(0.04))
|
|
||||||
assert model is not None
|
|
||||||
constant = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.0, v_ego=8.0)
|
|
||||||
rising = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.01, v_ego=8.0)
|
|
||||||
assert 0.0 < rising.path_offset < constant.path_offset
|
|
||||||
assert 0.0 < rising.path_angle < constant.path_angle
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_path_exit_zeros_lingering_c2_and_countersteers():
|
|
||||||
command = _command(_path(0.0), 0.004, current_curvature=0.006)
|
|
||||||
assert command.path_offset <= 0.0
|
|
||||||
assert command.path_angle < 0.0
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_model_path_reversal_zeros_opposing_lingering_c2():
|
|
||||||
command = _command(_path(-0.004), 0.004, current_curvature=0.002)
|
|
||||||
assert command.path_offset < 0.0
|
|
||||||
assert command.path_angle < 0.0
|
|
||||||
assert command.curvature == 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_s_turn_reverses_model_pose_without_slow_c2():
|
|
||||||
controller = FordPathController(dt=0.05)
|
|
||||||
for _ in range(10):
|
|
||||||
controller.update(_path(0.04), 0.04, v_ego=8.0)
|
|
||||||
outputs = [controller.update(_path(-0.04), -0.04, v_ego=8.0) for _ in range(10)]
|
|
||||||
assert all(command.curvature == 0.0 for command in outputs)
|
|
||||||
assert np.all(np.diff([command.path_offset for command in outputs]) < 0.0)
|
|
||||||
assert np.all(np.diff([command.path_angle for command in outputs]) < 0.0)
|
|
||||||
assert outputs[-1].path_offset < 0.0
|
|
||||||
assert outputs[-1].path_angle < 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_output_limits_and_rates_are_bounded():
|
|
||||||
controller = FordPathController()
|
|
||||||
outputs = [controller.update(_path(0.2), 0.2, v_ego=8.0) for _ in range(100)]
|
|
||||||
assert all(DBC_OFFSET[0] <= command.path_offset <= DBC_OFFSET[1] for command in outputs)
|
|
||||||
assert all(DBC_ANGLE[0] <= command.path_angle <= DBC_ANGLE[1] for command in outputs)
|
|
||||||
assert all(DBC_CURVATURE[0] <= command.curvature <= DBC_CURVATURE[1] for command in outputs)
|
|
||||||
assert np.max(np.abs(np.diff([command.path_offset for command in outputs]))) <= 0.04 + 1e-9
|
|
||||||
assert np.max(np.abs(np.diff([command.path_angle for command in outputs]))) <= 0.01 + 1e-9
|
|
||||||
|
|
||||||
|
|
||||||
def test_clipped_path_angle_uses_available_offset_to_preserve_endpoint():
|
|
||||||
horizon = 7.0
|
|
||||||
for curvature, angle_limit in ((-0.1, DBC_ANGLE[0]), (0.1, DBC_ANGLE[1])):
|
|
||||||
model = _path(curvature)
|
|
||||||
command = _command(model, curvature, current_curvature=curvature, v_ego=horizon)
|
|
||||||
path = _model_path(model)
|
|
||||||
assert path is not None
|
|
||||||
advance = 0.1 * horizon
|
|
||||||
model_offset, model_angle = _relative_pose(advance + horizon, path,
|
|
||||||
_predicted_pose(advance, curvature, 0.0))
|
|
||||||
|
|
||||||
assert command.path_angle == angle_limit
|
|
||||||
assert np.isclose(command.path_offset + horizon * command.path_angle,
|
|
||||||
model_offset + horizon * model_angle)
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_model_ramps_pose_to_zero_and_inactive_resets():
|
|
||||||
controller = FordPathController(dt=0.01)
|
|
||||||
for _ in range(20):
|
|
||||||
active = controller.update(_path(0.04), 0.04, v_ego=8.0)
|
|
||||||
invalid = controller.update(None, 0.0, v_ego=8.0)
|
|
||||||
assert invalid.valid
|
|
||||||
assert abs(invalid.path_offset) < abs(active.path_offset)
|
|
||||||
assert abs(invalid.path_angle) < abs(active.path_angle)
|
|
||||||
assert not controller.update(_path(0.0), 0.0, v_ego=8.0, active=False).valid
|
|
||||||
|
|
||||||
|
|
||||||
def test_sunnypilot_path_message_round_trip():
|
|
||||||
message = custom.CarControlSP.new_message()
|
|
||||||
message.fordLateralPath.pathOffset = 0.3
|
|
||||||
message.fordLateralPath.pathAngle = -0.2
|
|
||||||
message.fordLateralPath.curvature = 0.008
|
|
||||||
message.fordLateralPath.curvatureRate = -0.0004
|
|
||||||
message.fordLateralPath.valid = True
|
|
||||||
path = convert_carControlSP(message.as_reader()).fordLateralPath
|
|
||||||
assert np.isclose(path.pathOffset, 0.3)
|
|
||||||
assert np.isclose(path.pathAngle, -0.2)
|
|
||||||
assert np.isclose(path.curvature, 0.008)
|
|
||||||
assert np.isclose(path.curvatureRate, -0.0004)
|
|
||||||
assert path.valid
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_mirrors_exact_250hz_slew_and_c3_target():
|
|
||||||
observer = FordPscmObserver()
|
|
||||||
observer.set_command(FordPath(True, 1.0, 0.5, 0.0, 0.001))
|
|
||||||
observer.advance(1.0)
|
|
||||||
assert np.isclose(observer.state.path_offset, 1.0)
|
|
||||||
assert np.isclose(observer.state.path_angle, 0.100006103515625)
|
|
||||||
assert np.isclose(observer.state.curvature, 0.0030059814453125)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_tracks_wire_quantized_commands():
|
|
||||||
observer = FordPscmObserver()
|
|
||||||
observer.set_command(FordPath(True, 0.006, 0.0004, 0.000011, 0.0))
|
|
||||||
assert observer.command.path_offset == 0.01
|
|
||||||
assert observer.command.path_angle == 0.0005
|
|
||||||
assert observer.command.curvature == 0.00002
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_c2_contribution_is_speed_scheduled():
|
|
||||||
state = FordPscmObserver().state
|
|
||||||
state = type(state)(curvature=0.004)
|
|
||||||
low = _pscm_contributions(state, 5.0)[2]
|
|
||||||
high = _pscm_contributions(state, 20.0)[2]
|
|
||||||
assert high > low * 10.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_fills_missing_gentle_c2_with_fast_fields():
|
|
||||||
controller = FordPscmObserverPathController(dt=0.01)
|
|
||||||
command = controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
|
||||||
v_ego=20.0, v_ego_raw=20.0)
|
|
||||||
assert command.path_offset > 0.0
|
|
||||||
assert command.path_angle > 0.0
|
|
||||||
assert command.curvature > 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_uses_c0_only_after_c1_reaches_its_effective_limit():
|
|
||||||
controller = FordPscmObserverPathController(dt=0.01)
|
|
||||||
small = controller._command_for_state(FordPath(True, 0.2, 0.0, 0.0, 0.0), 8.0)
|
|
||||||
large = controller._command_for_state(FordPath(True, 1.0, 0.5, 0.0, 0.0), 8.0)
|
|
||||||
assert small.path_offset == 0.0
|
|
||||||
assert small.path_angle > 0.0
|
|
||||||
assert large.path_offset > 0.0
|
|
||||||
assert large.path_angle == 0.349609375 / 10.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_preserves_c2_residual_across_c0_c1_headroom():
|
|
||||||
controller = FordPscmObserverPathController(dt=0.01)
|
|
||||||
target = FordPath(True, 0.0, 0.0, 0.004, 0.0)
|
|
||||||
command = controller._command_for_state(target, 20.0)
|
|
||||||
target_contribution = sum(_pscm_contributions(FordPscmState(curvature=target.curvature), 20.0))
|
|
||||||
command_contributions = _pscm_contributions(FordPscmState(command.path_offset, command.path_angle), 20.0)
|
|
||||||
assert np.isclose(sum(command_contributions), target_contribution)
|
|
||||||
|
|
||||||
controller.observer.state = FordPscmState(curvature=0.004)
|
|
||||||
unwind = controller._command_for_state(FordPath(valid=True), 20.0)
|
|
||||||
unwind_contributions = _pscm_contributions(FordPscmState(unwind.path_offset, unwind.path_angle), 20.0)
|
|
||||||
lingering_c2 = _pscm_contributions(controller.observer.state, 20.0)[2]
|
|
||||||
assert np.isclose(sum(unwind_contributions) + lingering_c2, 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_unloads_fast_residual_as_c2_loads():
|
|
||||||
controller = FordPscmObserverPathController(dt=0.01)
|
|
||||||
outputs = [controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
|
||||||
v_ego=20.0, v_ego_raw=20.0) for _ in range(200)]
|
|
||||||
assert outputs[0].path_angle > outputs[-1].path_angle >= 0.0
|
|
||||||
assert controller.observer.state.curvature > 0.003
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_counters_lingering_c2_during_model_exit():
|
|
||||||
controller = FordPscmObserverPathController(dt=0.01)
|
|
||||||
for _ in range(200):
|
|
||||||
controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
|
||||||
v_ego=20.0, v_ego_raw=20.0)
|
|
||||||
command = controller.update(_path(0.0, speed=20.0), 0.0, current_curvature=0.004,
|
|
||||||
v_ego=20.0, v_ego_raw=20.0)
|
|
||||||
assert command.path_angle < 0.0
|
|
||||||
assert command.curvature < controller.observer.state.curvature
|
|
||||||
|
|
||||||
|
|
||||||
def test_pscm_observer_avoids_ineffective_c0_c1_windup():
|
|
||||||
controller = FordPscmObserverPathController(dt=1.0)
|
|
||||||
command = controller.update(_path(0.2), 0.2, v_ego=8.0, v_ego_raw=8.0)
|
|
||||||
assert abs(command.path_offset) <= 1.0
|
|
||||||
assert abs(command.path_angle) <= 0.349609375 / 10.0
|
|
||||||
@@ -10,6 +10,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
|||||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||||
|
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||||
|
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||||
|
]
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
chunker_file = File("#openpilot/common/file_chunker.py")
|
chunker_file = File("#openpilot/common/file_chunker.py")
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -19,22 +24,20 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
|||||||
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
||||||
|
|
||||||
def estimate_pickle_max_size(onnx_size):
|
def estimate_pickle_max_size(onnx_size):
|
||||||
# QCOM programs for models with spatial recurrent features can approach 2x
|
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||||
# the ONNX size. Overestimating only adds an empty trailing chunk.
|
|
||||||
return 2.0 * onnx_size + 10 * 1024 * 1024
|
|
||||||
|
|
||||||
if arch == 'comma_arm64':
|
if arch == 'comma_arm64':
|
||||||
from openpilot.common.hardware import HARDWARE
|
|
||||||
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
|
|
||||||
camera_configs = [(camera.width, camera.height)]
|
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||||
else:
|
else:
|
||||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
|
||||||
tg_backend = 'CPU'
|
tg_backend = 'CPU'
|
||||||
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
||||||
|
|
||||||
tg_devices = { # which device to put jit inputs to at runtime
|
tg_devices = { # which device to put jit inputs to at runtime
|
||||||
|
'openpilot.selfdrive.modeld.modeld': {
|
||||||
|
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
|
||||||
|
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||||
|
},
|
||||||
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
||||||
'default': {'DEV': tg_backend}
|
'default': {'DEV': tg_backend}
|
||||||
},
|
},
|
||||||
@@ -42,7 +45,7 @@ tg_devices = { # which device to put jit inputs to at runtime
|
|||||||
|
|
||||||
CHESTNUT = chestnut_present()
|
CHESTNUT = chestnut_present()
|
||||||
if CHESTNUT:
|
if CHESTNUT:
|
||||||
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
|
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
|
||||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||||
chestnut_lock = File("models/.chestnut.lock").abspath
|
chestnut_lock = File("models/.chestnut.lock").abspath
|
||||||
|
|
||||||
@@ -73,9 +76,10 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
|||||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||||
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
||||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
|
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut
|
||||||
|
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
|
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||||
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
||||||
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
||||||
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
||||||
@@ -103,7 +107,7 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
|||||||
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
||||||
node = lenv.Command(
|
node = lenv.Command(
|
||||||
chunk_targets,
|
chunk_targets,
|
||||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
|
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||||
actions,
|
actions,
|
||||||
)
|
)
|
||||||
if chestnut:
|
if chestnut:
|
||||||
@@ -117,7 +121,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
|
|||||||
|
|
||||||
dm_w, dm_h = DM_INPUT_SIZE
|
dm_w, dm_h = DM_INPUT_SIZE
|
||||||
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
||||||
for cam_w, cam_h in camera_configs:
|
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||||
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
||||||
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
||||||
@@ -139,4 +143,5 @@ def tg_compile(flags, model_name):
|
|||||||
Action(do_chunk, " [CHUNK] $TARGET")],
|
Action(do_chunk, " [CHUNK] $TARGET")],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||||
tg_compile(tg_flags, 'dmonitoring_model')
|
tg_compile(tg_flags, 'dmonitoring_model')
|
||||||
|
|||||||
@@ -37,12 +37,17 @@ from tinygrad.engine.jit import TinyJit
|
|||||||
|
|
||||||
|
|
||||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||||
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
WARP_INPUTS = ['tfm', 'big_tfm']
|
||||||
|
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||||
|
|
||||||
|
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||||
|
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||||
|
|
||||||
|
WARP_DEV = os.getenv('WARP_DEV')
|
||||||
|
|
||||||
|
|
||||||
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
|
def make_random_images(keys, shape, device=None):
|
||||||
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
|
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
|
||||||
return stride * (y_height + uv_height)
|
|
||||||
|
|
||||||
|
|
||||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||||
@@ -94,7 +99,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
|||||||
|
|
||||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
|
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
|
||||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||||
with Context(SPLIT_REDUCEOP=0):
|
with Context(SPLIT_REDUCEOP=0):
|
||||||
@@ -113,43 +118,49 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
|||||||
return frame_prepare_tinygrad
|
return frame_prepare_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||||
|
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||||
|
n_frames = img[1] // 6
|
||||||
|
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||||
|
|
||||||
|
npy = {
|
||||||
|
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
|
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
|
}
|
||||||
|
input_queues = {
|
||||||
|
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||||
|
}
|
||||||
|
return input_queues, npy
|
||||||
|
|
||||||
|
|
||||||
def get_policy_npy_shapes(input_shapes):
|
def get_policy_npy_shapes(input_shapes):
|
||||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||||
at = input_shapes['action_t'] # (1, 2)
|
at = input_shapes['action_t'] # (1, 2)
|
||||||
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
|
fb = input_shapes['features_buffer'] # (1, 24, 512)
|
||||||
feat_dim = math.prod(fb[2:])
|
|
||||||
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
||||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
|
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
|
||||||
return shapes, [math.prod(s) for s in shapes.values()]
|
return shapes, [math.prod(s) for s in shapes.values()]
|
||||||
|
|
||||||
|
|
||||||
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
|
def make_input_queues(input_shapes, frame_skip, device):
|
||||||
img = input_shapes['img'] # (1, 12, 128, 256)
|
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||||
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
|
|
||||||
feat_dim = math.prod(fb[2:])
|
|
||||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
|
||||||
n_frames = img[1] // 6
|
|
||||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
|
||||||
|
|
||||||
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
|
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
|
||||||
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
|
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||||
sizes = [math.prod(s) for s in shapes.values()]
|
|
||||||
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
|
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||||
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
|
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||||
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
|
|
||||||
frames = packed_input[packed_npy_size:]
|
|
||||||
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
|
|
||||||
# views into the packed inputs, to be refilled at runtime
|
# views into the packed inputs, to be refilled at runtime
|
||||||
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
|
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
|
||||||
input_queues = {
|
input_queues.update({
|
||||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
|
||||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
|
|
||||||
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
|
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
|
||||||
}
|
})
|
||||||
return input_queues, npy, frame_views
|
return input_queues, npy
|
||||||
|
|
||||||
|
|
||||||
def shift_and_sample(buf, new_val, sample_fn):
|
def shift_and_sample(buf, new_val, sample_fn):
|
||||||
@@ -165,15 +176,13 @@ def sample_desire(buf, frame_skip):
|
|||||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
def make_warp(nv12, model_w, model_h):
|
def make_warp(nv12, model_w, model_h, frame_skip):
|
||||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||||
|
|
||||||
def warp(tfm, big_tfm, frame, big_frame):
|
def warp(tfm, big_tfm, frame, big_frame):
|
||||||
tfm = tfm.to(Device.DEFAULT)
|
tfm = tfm.to(WARP_DEV)
|
||||||
big_tfm = big_tfm.to(Device.DEFAULT)
|
big_tfm = big_tfm.to(WARP_DEV)
|
||||||
frame = frame.to(Device.DEFAULT)
|
Tensor.realize(tfm, big_tfm)
|
||||||
big_frame = big_frame.to(Device.DEFAULT)
|
|
||||||
Tensor.realize(tfm, big_tfm, frame, big_frame)
|
|
||||||
|
|
||||||
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
||||||
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
||||||
@@ -186,10 +195,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
|||||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||||
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||||
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
|
|
||||||
|
|
||||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||||
|
warped = warped.to(Device.DEFAULT)
|
||||||
Tensor.realize(packed_npy_inputs, warped)
|
Tensor.realize(packed_npy_inputs, warped)
|
||||||
|
|
||||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||||
@@ -202,50 +211,33 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
|||||||
inputs = {
|
inputs = {
|
||||||
'img': img,
|
'img': img,
|
||||||
'big_img': big_img,
|
'big_img': big_img,
|
||||||
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
|
'features_buffer': feat_buf,
|
||||||
'desire_pulse': desire_buf,
|
'desire_pulse': desire_buf,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
'action_t': action_t,
|
'action_t': action_t,
|
||||||
}
|
}
|
||||||
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
|
|
||||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||||
return out,
|
return out,
|
||||||
return run_policy
|
return run_policy
|
||||||
|
|
||||||
|
|
||||||
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
|
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||||
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
|
||||||
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
|
|
||||||
|
|
||||||
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
|
||||||
packed_input = packed_npy_inputs.to(Device.DEFAULT)
|
|
||||||
Tensor.realize(packed_input)
|
|
||||||
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
|
|
||||||
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
|
|
||||||
big_frame = packed_input[packed_npy_size + frame_copy_size:]
|
|
||||||
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
|
|
||||||
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
|
|
||||||
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
|
|
||||||
return run_model
|
|
||||||
|
|
||||||
|
|
||||||
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
|
||||||
if benchmark_runs < 1:
|
|
||||||
raise ValueError("benchmark_runs must be at least 1")
|
|
||||||
|
|
||||||
SEED = 42
|
SEED = 42
|
||||||
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
|
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||||
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
|
input_queues, npy = make_queues(Device.DEFAULT)
|
||||||
rng = np.random.default_rng(seed)
|
rng = np.random.default_rng(seed)
|
||||||
|
Tensor.manual_seed(seed)
|
||||||
|
|
||||||
|
testing = test_val is not None or test_buffers is not None
|
||||||
|
n_runs = 1 if testing else 3
|
||||||
|
|
||||||
for i in range(n_runs):
|
for i in range(n_runs):
|
||||||
for v in npy.values():
|
for v in npy.values():
|
||||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||||
for v in frame_views.values():
|
|
||||||
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
|
|
||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
|
random_inputs = make_random_inputs()
|
||||||
st = time.perf_counter()
|
st = time.perf_counter()
|
||||||
outs = fn(**{k: input_queues[k] for k in input_keys})
|
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
|
||||||
mt = time.perf_counter()
|
mt = time.perf_counter()
|
||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
et = time.perf_counter()
|
et = time.perf_counter()
|
||||||
@@ -264,15 +256,14 @@ def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
|||||||
return val, buffers
|
return val, buffers
|
||||||
|
|
||||||
print('capture + replay')
|
print('capture + replay')
|
||||||
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
|
test_val, test_buffers = random_inputs_run(jit, SEED)
|
||||||
print(f'pickle round trip ({benchmark_runs} runs per seed)')
|
print('pickle round trip')
|
||||||
with tempfile.TemporaryFile(dir=".") as f:
|
with tempfile.TemporaryFile(dir=".") as f:
|
||||||
dump_oob(jit, f)
|
dump_oob(jit, f)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
loaded_jit = load_oob(f)
|
jit = load_oob(f)
|
||||||
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
|
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
|
||||||
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
|
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||||
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
|
|
||||||
return jit
|
return jit
|
||||||
|
|
||||||
|
|
||||||
@@ -301,31 +292,27 @@ if __name__ == "__main__":
|
|||||||
p.add_argument('--onnx', required=True)
|
p.add_argument('--onnx', required=True)
|
||||||
p.add_argument('--output', required=True)
|
p.add_argument('--output', required=True)
|
||||||
p.add_argument('--frame-skip', type=int, required=True)
|
p.add_argument('--frame-skip', type=int, required=True)
|
||||||
p.add_argument('--benchmark-runs', type=int, default=1,
|
|
||||||
help='timed loaded-JIT runs for each correctness seed')
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
model_path = read_file_chunked_to_disk(args.onnx)
|
model_path = read_file_chunked_to_disk(args.onnx)
|
||||||
model_w, model_h = args.model_size
|
model_w, model_h = args.model_size
|
||||||
|
|
||||||
model_runner = OnnxRunner(model_path)
|
model_runner = OnnxRunner(model_path)
|
||||||
out = {
|
out = {'metadata': make_metadata_dict(model_path)}
|
||||||
'metadata': make_metadata_dict(model_path),
|
|
||||||
'input_devices': {'model': Device.DEFAULT},
|
|
||||||
'run_model': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
|
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
|
||||||
|
|
||||||
|
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||||
|
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
|
||||||
|
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
|
||||||
|
make_policy_queues)
|
||||||
|
|
||||||
for cam_w, cam_h in args.camera_resolutions:
|
for cam_w, cam_h in args.camera_resolutions:
|
||||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
|
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
|
||||||
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
|
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||||
frame_copy_size=frame_copy_size)
|
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||||
warp = make_warp(nv12, model_w, model_h)
|
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||||
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
|
|
||||||
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
|
|
||||||
args.benchmark_runs)
|
|
||||||
|
|
||||||
with open(args.output, "wb") as f:
|
with open(args.output, "wb") as f:
|
||||||
dump_oob(out, f)
|
dump_oob(out, f)
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
|
|||||||
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
||||||
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
||||||
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
||||||
driving_model_data.big = modelV2.big
|
|
||||||
driving_model_data.action = modelV2.action
|
driving_model_data.action = modelV2.action
|
||||||
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
||||||
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openpilot.common.file_chunker import get_manifest_path
|
from openpilot.common.file_chunker import get_manifest_path
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
|
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||||
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
||||||
CHESTNUT_POWERED_VOLTAGE = 5000
|
|
||||||
CHESTNUT_PCIE_READY = 0x78
|
|
||||||
|
|
||||||
|
|
||||||
def get_tg_input_devices(process_name: str, chestnut: bool):
|
def get_tg_input_devices(process_name: str, chestnut: bool):
|
||||||
@@ -52,7 +50,7 @@ def chestnut_present() -> bool:
|
|||||||
try:
|
try:
|
||||||
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
||||||
product = (d / "product").read_text().strip()
|
product = (d / "product").read_text().strip()
|
||||||
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
|
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -60,7 +58,3 @@ def chestnut_present() -> bool:
|
|||||||
|
|
||||||
def chestnut_compiled() -> bool:
|
def chestnut_compiled() -> bool:
|
||||||
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
||||||
|
|
||||||
|
|
||||||
def chestnut_ready(state) -> bool:
|
|
||||||
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import ctypes
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
import os
|
import os
|
||||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
from tinygrad.device import Device
|
from tinygrad.device import Device
|
||||||
import usb1
|
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -28,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
|
|||||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
||||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
|
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
|
||||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
||||||
from openpilot.common.file_chunker import open_file_chunked
|
from openpilot.common.file_chunker import open_file_chunked
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
|
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
|
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||||
|
|
||||||
|
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
|
|
||||||
LAT_SMOOTH_SECONDS = 0.0
|
LAT_SMOOTH_SECONDS = 0.0
|
||||||
@@ -83,37 +83,6 @@ class ChestnutState:
|
|||||||
self.valid = True
|
self.valid = True
|
||||||
self.sends = 0
|
self.sends = 0
|
||||||
self.metrics = {}
|
self.metrics = {}
|
||||||
self._asm_usb = None
|
|
||||||
|
|
||||||
def _close_asm_usb(self) -> None:
|
|
||||||
if self._asm_usb is not None:
|
|
||||||
self._asm_usb.close()
|
|
||||||
self._asm_usb = None
|
|
||||||
|
|
||||||
def _open_asm_usb(self):
|
|
||||||
context = usb1.USBContext()
|
|
||||||
for vendor_id, product_id in CHESTNUT_USB_IDS:
|
|
||||||
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
|
|
||||||
return handle
|
|
||||||
context.close()
|
|
||||||
|
|
||||||
def _read_ina(self) -> tuple[int, int, bool]:
|
|
||||||
if "AMD" in Device._opened_devices and self._asm_usb is None:
|
|
||||||
try:
|
|
||||||
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
|
|
||||||
return struct.unpack('<Hh?', bytes(raw))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self._asm_usb is None:
|
|
||||||
self._asm_usb = self._open_asm_usb()
|
|
||||||
if self._asm_usb is None:
|
|
||||||
raise usb1.USBErrorNoDevice
|
|
||||||
try:
|
|
||||||
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
|
|
||||||
except usb1.USBError:
|
|
||||||
self._close_asm_usb()
|
|
||||||
raise
|
|
||||||
return struct.unpack('<Hh?', bytes(raw))
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def power_limit(self) -> int:
|
def power_limit(self) -> int:
|
||||||
@@ -149,15 +118,13 @@ class ChestnutState:
|
|||||||
setattr(state, k, v)
|
setattr(state, k, v)
|
||||||
|
|
||||||
asm_valid = False
|
asm_valid = False
|
||||||
try:
|
|
||||||
# ASM runs on USB-C power, these still read without a gpu
|
|
||||||
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
|
|
||||||
asm_valid = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if "AMD" in Device._opened_devices:
|
if "AMD" in Device._opened_devices:
|
||||||
try:
|
try:
|
||||||
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
|
# ASM runs on USB-C power, these still read without a gpu
|
||||||
|
asm = Device["AMD"].iface.pci_dev.usb
|
||||||
|
state.pcieLtssm = asm.read(0xB450, 1)[0]
|
||||||
|
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
|
||||||
|
asm_valid = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -180,9 +147,9 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
|
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
|
||||||
|
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||||
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
||||||
input_devices = jits['input_devices']
|
|
||||||
self.model_device = input_devices['model']
|
|
||||||
metadata = jits['metadata']
|
metadata = jits['metadata']
|
||||||
self.input_shapes = metadata['input_shapes']
|
self.input_shapes = metadata['input_shapes']
|
||||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||||
@@ -192,11 +159,13 @@ class ModelState(ModelStateBase):
|
|||||||
self.chestnut = chestnut
|
self.chestnut = chestnut
|
||||||
|
|
||||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
self.full_frames: dict[str, Tensor] = {}
|
||||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.run_model = jits['run_model'][(cam_w,cam_h)]
|
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||||
|
self.run_policy = jits['run_policy']
|
||||||
|
self.warp = jits[(cam_w,cam_h)]
|
||||||
|
|
||||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||||
@@ -204,8 +173,14 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
||||||
for key, buf in bufs.items():
|
for key in bufs.keys():
|
||||||
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
|
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||||
|
yuv_size = self.frame_buf_params[key][3]
|
||||||
|
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||||
|
cache_key = (key, ptr)
|
||||||
|
if cache_key not in self._blob_cache:
|
||||||
|
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||||
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
|
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire_pulse'][0] = 0
|
inputs['desire_pulse'][0] = 0
|
||||||
@@ -216,7 +191,11 @@ class ModelState(ModelStateBase):
|
|||||||
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
||||||
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
||||||
|
|
||||||
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
|
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
||||||
|
|
||||||
|
outs, = self.run_policy(
|
||||||
|
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
|
||||||
|
)
|
||||||
if after_enqueue is not None:
|
if after_enqueue is not None:
|
||||||
after_enqueue()
|
after_enqueue()
|
||||||
model_output = outs.numpy()[0]
|
model_output = outs.numpy()[0]
|
||||||
@@ -230,36 +209,24 @@ class ModelState(ModelStateBase):
|
|||||||
return outputs_dict
|
return outputs_dict
|
||||||
|
|
||||||
def warmup(self) -> None:
|
def warmup(self) -> None:
|
||||||
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
|
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
|
||||||
eye = np.eye(3, dtype=np.float32)
|
eye = np.eye(3, dtype=np.float32)
|
||||||
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
||||||
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
||||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
|
||||||
self.prev_desire[:] = 0
|
self.prev_desire[:] = 0
|
||||||
|
self.full_frames.clear()
|
||||||
|
self._blob_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
def main(demo=False):
|
def main(demo=False):
|
||||||
cloudlog.warning("modeld init")
|
cloudlog.warning("modeld init")
|
||||||
|
|
||||||
chestnut_available = chestnut_present() and chestnut_compiled()
|
CHESTNUT = chestnut_present() and chestnut_compiled()
|
||||||
CHESTNUT = False
|
|
||||||
if chestnut_available:
|
|
||||||
poller = messaging.Poller()
|
|
||||||
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
|
|
||||||
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
|
|
||||||
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
|
|
||||||
if not poller.poll(round(remaining * 1000)):
|
|
||||||
break
|
|
||||||
msg = messaging.recv_one_or_none(sock)
|
|
||||||
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
|
|
||||||
if CHESTNUT:
|
if CHESTNUT:
|
||||||
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
||||||
params = Params()
|
params = Params()
|
||||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||||
if chestnut_available and not CHESTNUT:
|
|
||||||
params.put_bool("ChestnutActive", False)
|
|
||||||
else:
|
|
||||||
params.remove("ChestnutActive")
|
params.remove("ChestnutActive")
|
||||||
|
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
@@ -304,11 +271,7 @@ def main(demo=False):
|
|||||||
loader.start()
|
loader.start()
|
||||||
loader.join(BIG_MODEL_TIMEOUT)
|
loader.join(BIG_MODEL_TIMEOUT)
|
||||||
model = big_model
|
model = big_model
|
||||||
if model is None:
|
|
||||||
params.put_bool("ChestnutModelError", True)
|
|
||||||
params.put_bool("ChestnutActive", model is not None)
|
params.put_bool("ChestnutActive", model is not None)
|
||||||
if model is not None:
|
|
||||||
params.remove("ChestnutModelError")
|
|
||||||
|
|
||||||
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
|
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
|
||||||
if model is None:
|
if model is None:
|
||||||
@@ -442,7 +405,6 @@ def main(demo=False):
|
|||||||
raise
|
raise
|
||||||
# fallback to small model
|
# fallback to small model
|
||||||
cloudlog.exception("big model failed, fall back to small")
|
cloudlog.exception("big model failed, fall back to small")
|
||||||
params.put_bool("ChestnutModelError", True)
|
|
||||||
params.put_bool("ChestnutActive", False)
|
params.put_bool("ChestnutActive", False)
|
||||||
assert small_model is not None
|
assert small_model is not None
|
||||||
model = small_model
|
model = small_model
|
||||||
@@ -469,11 +431,12 @@ def main(demo=False):
|
|||||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||||
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||||
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
|
||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
|
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
|
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
||||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
|
|
||||||
fill_driving_model_data(drivingdata_send, modelv2_send)
|
fill_driving_model_data(drivingdata_send, modelv2_send)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
|
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
|
||||||
size 765950064
|
size 1757355221
|
||||||
|
|||||||
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
|||||||
ps.setUptime(health.uptime_pkt);
|
ps.setUptime(health.uptime_pkt);
|
||||||
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
||||||
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
||||||
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
|
ps.setIgnitionLine(health.ignition_line_pkt);
|
||||||
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
|
ps.setIgnitionCan(health.ignition_can_pkt);
|
||||||
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
|
ps.setControlsAllowed(health.controls_allowed_pkt);
|
||||||
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
||||||
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
||||||
ps.setPandaType(hw_type);
|
ps.setPandaType(hw_type);
|
||||||
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
||||||
ps.setSafetyParam(health.safety_param_pkt);
|
ps.setSafetyParam(health.safety_param_pkt);
|
||||||
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
||||||
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
|
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
|
||||||
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
|
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
|
||||||
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
||||||
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
||||||
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
|
ps.setInterruptLoad(health.interrupt_load_pkt);
|
||||||
ps.setFanPower(health.fan_power);
|
ps.setFanPower(health.fan_power);
|
||||||
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
|
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
|
||||||
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
||||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||||
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spoofing_started) {
|
if (spoofing_started) {
|
||||||
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
|
health.ignition_line_pkt = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
|
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
|
||||||
|
|
||||||
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
||||||
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
||||||
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool power_save_desired = !ignition_local;
|
bool power_save_desired = !ignition_local;
|
||||||
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
|
if (health.power_save_enabled_pkt != power_save_desired) {
|
||||||
panda->set_power_saving(power_save_desired);
|
panda->set_power_saving(power_save_desired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,30 +19,6 @@
|
|||||||
},
|
},
|
||||||
"Offroad_ChestnutBranch": {
|
"Offroad_ChestnutBranch": {
|
||||||
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
||||||
"severity": -1
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutNotDetected": {
|
|
||||||
"text": "Chestnut not detected. Check USB and 12V connections.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutOverheated": {
|
|
||||||
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutPcieUnavailable": {
|
|
||||||
"text": "%1",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUncompiled": {
|
|
||||||
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUpdateFailed": {
|
|
||||||
"text": "Chestnut update failed. Check the USB cable.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUsbSlow": {
|
|
||||||
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
|
|
||||||
"severity": 0
|
"severity": 0
|
||||||
},
|
},
|
||||||
"Offroad_UnregisteredHardware": {
|
"Offroad_UnregisteredHardware": {
|
||||||
|
|||||||
@@ -32,14 +32,7 @@ from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
|||||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.assisted_driving_milestones import (
|
|
||||||
AssistCategory,
|
|
||||||
AssistedDrivingMilestones,
|
|
||||||
MilestoneEvent,
|
|
||||||
MilestoneStore,
|
|
||||||
)
|
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
from openpilot.sunnypilot.system.statsd import statlog
|
|
||||||
|
|
||||||
REPLAY = "REPLAY" in os.environ
|
REPLAY = "REPLAY" in os.environ
|
||||||
SIMULATION = "SIMULATION" in os.environ
|
SIMULATION = "SIMULATION" in os.environ
|
||||||
@@ -95,8 +88,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.big_model_ready_t = 0.
|
self.big_model_ready_t = 0.
|
||||||
|
|
||||||
# Setup sockets
|
# Setup sockets
|
||||||
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] +
|
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP'])
|
||||||
['selfdriveStateSP', 'onroadEventsSP', 'assistedDrivingMilestoneState'])
|
|
||||||
|
|
||||||
self.gps_location_service = get_gps_location_service(self.params)
|
self.gps_location_service = get_gps_location_service(self.params)
|
||||||
self.gps_packets = [self.gps_location_service]
|
self.gps_packets = [self.gps_location_service]
|
||||||
@@ -135,7 +127,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.params.remove("ExperimentalMode")
|
self.params.remove("ExperimentalMode")
|
||||||
|
|
||||||
self.CS_prev = car.CarState.new_message()
|
self.CS_prev = car.CarState.new_message()
|
||||||
self.car_state_log_mono_time = 0
|
|
||||||
self.AM = AlertManager()
|
self.AM = AlertManager()
|
||||||
self.events = Events()
|
self.events = Events()
|
||||||
|
|
||||||
@@ -146,11 +137,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.cruise_mismatch_counter = 0
|
self.cruise_mismatch_counter = 0
|
||||||
self.last_steering_pressed_frame = 0
|
self.last_steering_pressed_frame = 0
|
||||||
self.distance_traveled = 0
|
self.distance_traveled = 0
|
||||||
self.assisted_driving_milestones = AssistedDrivingMilestones(MilestoneStore(self.params))
|
|
||||||
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
|
|
||||||
self.assisted_driving_milestone_drive_id = ""
|
|
||||||
self._milestone_event: MilestoneEvent | None = None
|
|
||||||
self._milestone_event_expires_ns = 0
|
|
||||||
self.last_functional_fan_frame = 0
|
self.last_functional_fan_frame = 0
|
||||||
self.events_prev = []
|
self.events_prev = []
|
||||||
self.logged_comm_issue = None
|
self.logged_comm_issue = None
|
||||||
@@ -212,7 +198,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
loading = self.params.get_bool("ChestnutLoading")
|
loading = self.params.get_bool("ChestnutLoading")
|
||||||
if self.big_model_loading and not loading:
|
if self.big_model_loading and not loading:
|
||||||
self.big_model_ready_t = time.monotonic()
|
self.big_model_ready_t = time.monotonic()
|
||||||
self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady)
|
|
||||||
self.big_model_loading = loading
|
self.big_model_loading = loading
|
||||||
if self.big_model_loading:
|
if self.big_model_loading:
|
||||||
self.events.add(EventName.bigModelLoading)
|
self.events.add(EventName.bigModelLoading)
|
||||||
@@ -542,8 +527,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
def data_sample(self):
|
def data_sample(self):
|
||||||
_car_state = messaging.recv_one(self.car_state_sock)
|
_car_state = messaging.recv_one(self.car_state_sock)
|
||||||
CS = _car_state.carState if _car_state else self.CS_prev
|
CS = _car_state.carState if _car_state else self.CS_prev
|
||||||
if _car_state is not None:
|
|
||||||
self.car_state_log_mono_time = _car_state.logMonoTime
|
|
||||||
|
|
||||||
self.sm.update(0)
|
self.sm.update(0)
|
||||||
|
|
||||||
@@ -662,31 +645,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.pm.send('onroadEventsSP', ce_send_sp)
|
self.pm.send('onroadEventsSP', ce_send_sp)
|
||||||
self.events_sp_prev = self.events_sp.names.copy()
|
self.events_sp_prev = self.events_sp.names.copy()
|
||||||
|
|
||||||
def publish_assisted_driving_milestones(self, now_ns: int, event: MilestoneEvent | None) -> None:
|
|
||||||
if event is not None:
|
|
||||||
self._milestone_event = event
|
|
||||||
self._milestone_event_expires_ns = now_ns + 1_000_000_000
|
|
||||||
elif now_ns >= self._milestone_event_expires_ns:
|
|
||||||
self._milestone_event = None
|
|
||||||
|
|
||||||
if event is None and self.sm.frame % 10 != 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
snapshot = self.assisted_driving_milestones.snapshot()
|
|
||||||
msg = messaging.new_message("assistedDrivingMilestoneState")
|
|
||||||
msg.valid = True
|
|
||||||
state = msg.assistedDrivingMilestoneState
|
|
||||||
state.enabled = self.assisted_driving_milestones_enabled
|
|
||||||
state.madsDistanceMeters = snapshot.distances_meters[AssistCategory.MADS]
|
|
||||||
state.fullAssistDistanceMeters = snapshot.distances_meters[AssistCategory.FULL_ASSIST]
|
|
||||||
if self._milestone_event is not None:
|
|
||||||
state.event.id = self._milestone_event.event_id
|
|
||||||
state.event.category = self._milestone_event.category.value
|
|
||||||
state.event.distanceMeters = self._milestone_event.distance_meters
|
|
||||||
state.event.previousDistanceMeters = self._milestone_event.previous_distance_meters
|
|
||||||
state.event.unit = self._milestone_event.unit.value
|
|
||||||
self.pm.send("assistedDrivingMilestoneState", msg)
|
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
CS = self.data_sample()
|
CS = self.data_sample()
|
||||||
self.update_events(CS)
|
self.update_events(CS)
|
||||||
@@ -696,28 +654,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.mads.update(CS)
|
self.mads.update(CS)
|
||||||
self.update_alerts(CS)
|
self.update_alerts(CS)
|
||||||
|
|
||||||
now_ns = time.monotonic_ns()
|
|
||||||
if not self.assisted_driving_milestone_drive_id:
|
|
||||||
self.assisted_driving_milestone_drive_id = self.params.get("CurrentRoute") or ""
|
|
||||||
self.assisted_driving_milestones.set_drive_id(self.assisted_driving_milestone_drive_id)
|
|
||||||
car_control = self.sm['carControl']
|
|
||||||
milestone_event = self.assisted_driving_milestones.update(
|
|
||||||
self.car_state_log_mono_time,
|
|
||||||
CS.vEgo,
|
|
||||||
lat_active=car_control.latActive,
|
|
||||||
long_active=car_control.longActive,
|
|
||||||
is_metric=self.is_metric,
|
|
||||||
enabled=self.assisted_driving_milestones_enabled,
|
|
||||||
)
|
|
||||||
if milestone_event is not None:
|
|
||||||
cloudlog.event("assisted_driving_milestone_reached",
|
|
||||||
event_id=milestone_event.event_id,
|
|
||||||
category=milestone_event.category.value,
|
|
||||||
distance_meters=milestone_event.distance_meters)
|
|
||||||
statlog.gauge(f"assisted_driving_milestone.{milestone_event.category.value}.meters",
|
|
||||||
milestone_event.distance_meters)
|
|
||||||
self.publish_assisted_driving_milestones(now_ns, milestone_event)
|
|
||||||
|
|
||||||
self.button_state_tracker.update(CS)
|
self.button_state_tracker.update(CS)
|
||||||
self.publish_selfdriveState(CS)
|
self.publish_selfdriveState(CS)
|
||||||
|
|
||||||
@@ -730,7 +666,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||||
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
|
|
||||||
|
|
||||||
self.mads.read_params()
|
self.mads.read_params()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -744,7 +679,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.step()
|
self.step()
|
||||||
self.rk.monitor_time()
|
self.rk.monitor_time()
|
||||||
finally:
|
finally:
|
||||||
self.assisted_driving_milestones.close()
|
|
||||||
e.set()
|
e.set()
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
|
|||||||
add_ops = []
|
add_ops = []
|
||||||
for _, msg in msgs:
|
for _, msg in msgs:
|
||||||
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
||||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
|
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
|
||||||
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
||||||
for meta_field in ["laneChangeState", "laneChangeState"]:
|
for meta_field in ["laneChangeState", "laneChangeState"]:
|
||||||
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
|
|||||||
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
||||||
|
|
||||||
EXEC_TIMINGS = [
|
EXEC_TIMINGS = [
|
||||||
# model, instant max, average max, chestnut average max
|
# model, instant max, average max
|
||||||
("modelV2", 0.05, 0.03, 0.05),
|
("modelV2", 0.05, 0.028),
|
||||||
("driverStateV2", 0.05, 0.018, 0.018),
|
("driverStateV2", 0.05, 0.018),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_log_fn(test_route, ref="master"):
|
def get_log_fn(test_route, ref="master"):
|
||||||
@@ -169,13 +169,11 @@ def model_replay(lr, frs):
|
|||||||
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
||||||
|
|
||||||
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
||||||
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
|
|
||||||
|
|
||||||
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
||||||
rows = []
|
rows = []
|
||||||
timings_ok = True
|
timings_ok = True
|
||||||
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
|
for (s, instant_max, avg_max) in EXEC_TIMINGS:
|
||||||
avg_max = chestnut_avg_max if chestnut else avg_max
|
|
||||||
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
||||||
# TODO some init can happen in first iteration
|
# TODO some init can happen in first iteration
|
||||||
ts = ts[1:]
|
ts = ts[1:]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
|
|||||||
self._last_input_time = time.monotonic()
|
self._last_input_time = time.monotonic()
|
||||||
self._was_active = False
|
self._was_active = False
|
||||||
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
||||||
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
|||||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.system.ui.lib.multilang import tr, trn
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
|
|||||||
|
|
||||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||||
version_text_width, self.header_rect.height)
|
version_text_width, self.header_rect.height)
|
||||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
|
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
|
|
||||||
def _render_home_content(self):
|
def _render_home_content(self):
|
||||||
self._render_left_column()
|
self._render_left_column()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from enum import IntEnum
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
@@ -115,9 +115,9 @@ class TermsPage(Widget):
|
|||||||
self._on_accept = on_accept
|
self._on_accept = on_accept
|
||||||
self._on_decline = on_decline
|
self._on_decline = on_decline
|
||||||
|
|
||||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
|
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
|
|
||||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||||
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
|
|||||||
def __init__(self, back_callback=None):
|
def __init__(self, back_callback=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||||
click_callback=self._on_uninstall_clicked)
|
click_callback=self._on_uninstall_clicked)
|
||||||
|
|||||||
@@ -199,9 +199,6 @@ class SoftwareLayout(Widget):
|
|||||||
selection = self._branch_dialog.selection
|
selection = self._branch_dialog.selection
|
||||||
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
||||||
self._branch_btn.action_item.set_value(selection)
|
self._branch_btn.action_item.set_value(selection)
|
||||||
self._download_btn.action_item.set_enabled(False)
|
|
||||||
self._waiting_for_updater = True
|
|
||||||
self._waiting_start_ts = time.monotonic()
|
|
||||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
self._branch_dialog = None
|
self._branch_dialog = None
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
@@ -9,7 +8,7 @@ from openpilot.system.ui.widgets import Widget
|
|||||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||||
from openpilot.common.version import RELEASE_BRANCHES
|
from openpilot.common.version import RELEASE_BRANCHES
|
||||||
|
|
||||||
@@ -70,8 +69,8 @@ class AlertsPill(Widget):
|
|||||||
|
|
||||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||||
gui_label(count_rect, str(alert_count), font_size=36,
|
gui_label(count_rect, str(alert_count), font_size=36,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
|
|
||||||
class NetworkIcon(Widget):
|
class NetworkIcon(Widget):
|
||||||
@@ -140,9 +139,7 @@ class MiciHomeLayout(Widget):
|
|||||||
self._version_text = self._get_version_text()
|
self._version_text = self._get_version_text()
|
||||||
|
|
||||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||||
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
|
|
||||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
||||||
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
|
||||||
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
||||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||||
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
||||||
@@ -153,15 +150,13 @@ class MiciHomeLayout(Widget):
|
|||||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||||
NetworkIcon(),
|
NetworkIcon(),
|
||||||
self._experimental_icon,
|
self._experimental_icon,
|
||||||
self._usb_icon,
|
|
||||||
self._chestnut_icon,
|
self._chestnut_icon,
|
||||||
self._chestnut_loading_icon,
|
|
||||||
self._chestnut_failed_icon,
|
self._chestnut_failed_icon,
|
||||||
self._body_icon,
|
self._body_icon,
|
||||||
self._mic_icon,
|
self._mic_icon,
|
||||||
], spacing=18)
|
], spacing=18)
|
||||||
|
|
||||||
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
@@ -252,20 +247,12 @@ class MiciHomeLayout(Widget):
|
|||||||
self._version_commit_label.render()
|
self._version_commit_label.render()
|
||||||
|
|
||||||
# ***** Center-aligned bottom section icons *****
|
# ***** Center-aligned bottom section icons *****
|
||||||
usb_connected = ui_state.usb_connected
|
|
||||||
usb_unknown = ui_state.usb_unknown
|
|
||||||
chestnut_state = ui_state.chestnut_state
|
|
||||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
self._set_chestnut_visibility()
|
self._set_chestnut_visibility()
|
||||||
else:
|
else:
|
||||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
|
||||||
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
|
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||||
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
|
|
||||||
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
|
|
||||||
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
|
|
||||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
|
||||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
|
||||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
import openpilot.cereal.messaging as messaging
|
import openpilot.cereal.messaging as messaging
|
||||||
from openpilot.common.hardware import PC
|
|
||||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||||
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
||||||
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
||||||
@@ -64,8 +61,7 @@ class MiciMainLayout(Scroller):
|
|||||||
|
|
||||||
# Start onboarding if terms or training not completed, make sure to push after self
|
# Start onboarding if terms or training not completed, make sure to push after self
|
||||||
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
||||||
skip_onboarding_for_milestone_preview = PC and os.getenv("SP_MILESTONE_PREVIEW") == "1"
|
if not self._onboarding_window.completed:
|
||||||
if not self._onboarding_window.completed and not skip_onboarding_for_milestone_preview:
|
|
||||||
gui_app.push_widget(self._onboarding_window)
|
gui_app.push_widget(self._onboarding_window)
|
||||||
|
|
||||||
# initialize correct onroad layout
|
# initialize correct onroad layout
|
||||||
@@ -123,8 +119,6 @@ class MiciMainLayout(Scroller):
|
|||||||
self._onroad_time_delay = rl.get_time()
|
self._onroad_time_delay = rl.get_time()
|
||||||
else:
|
else:
|
||||||
self._scroll_to(self._home_layout)
|
self._scroll_to(self._home_layout)
|
||||||
if hasattr(self._home_layout, "request_drive_summary"):
|
|
||||||
self._home_layout.request_drive_summary()
|
|
||||||
|
|
||||||
# FIXME: these two pops can interrupt user interacting in the settings
|
# FIXME: these two pops can interrupt user interacting in the settings
|
||||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
|
|||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
|
||||||
REFRESH_INTERVAL = 5.0 # seconds
|
REFRESH_INTERVAL = 5.0 # seconds
|
||||||
@@ -62,12 +62,12 @@ class AlertItem(Widget):
|
|||||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||||
|
|
||||||
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
|
||||||
|
|
||||||
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
|
||||||
|
|
||||||
self._title_text = ""
|
self._title_text = ""
|
||||||
self._body_text = ""
|
self._body_text = ""
|
||||||
@@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
|
|||||||
|
|
||||||
# Create empty state label
|
# Create empty state label
|
||||||
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
# Build initial alert list
|
# Build initial alert list
|
||||||
self._build_alerts()
|
self._build_alerts()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pyray as rl
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.common.qrcode import make_texture
|
from openpilot.common.qrcode import make_texture
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||||
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
|
|||||||
|
|
||||||
if not self._camera_view.frame:
|
if not self._camera_view.frame:
|
||||||
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER)
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ class SoftwareInfoLayoutMici(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class CheckUpdateButton(BigButton):
|
class CheckUpdateButton(BigButton):
|
||||||
UPDATER_PROC = "openpilot.system.updated.updated"
|
|
||||||
CHECK_FOR_UPDATE = "SIGUSR1"
|
|
||||||
DOWNLOAD_UPDATE = "SIGHUP"
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||||
@@ -101,20 +97,15 @@ class CheckUpdateButton(BigButton):
|
|||||||
gui_app.push_widget(dlg)
|
gui_app.push_widget(dlg)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
|
|
||||||
|
|
||||||
def check_for_update(self):
|
|
||||||
self._signal_updater(self.CHECK_FOR_UPDATE)
|
|
||||||
|
|
||||||
def _signal_updater(self, sig: str):
|
|
||||||
self.set_enabled(False)
|
self.set_enabled(False)
|
||||||
self._state = UpdaterState.WAITING_FOR_UPDATER
|
self._state = UpdaterState.WAITING_FOR_UPDATER
|
||||||
self._hide_value_t = None
|
|
||||||
self.set_value("")
|
|
||||||
self.set_icon(self._txt_update_icon)
|
self.set_icon(self._txt_update_icon)
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
|
if self.get_value() == "download update":
|
||||||
|
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
|
||||||
|
else:
|
||||||
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
@@ -193,7 +184,7 @@ class CheckUpdateButton(BigButton):
|
|||||||
|
|
||||||
class InstallUpdateButton(BigButton):
|
class InstallUpdateButton(BigButton):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||||
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
@@ -241,9 +232,8 @@ class BranchSelectPage(NavScroller):
|
|||||||
|
|
||||||
|
|
||||||
class TargetBranchButton(BigButton):
|
class TargetBranchButton(BigButton):
|
||||||
def __init__(self, check_update_btn: CheckUpdateButton):
|
def __init__(self):
|
||||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
||||||
self._check_update_btn = check_update_btn
|
|
||||||
self.set_click_callback(self._on_click)
|
self.set_click_callback(self._on_click)
|
||||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||||
self.set_enabled(lambda: ui_state.is_offroad())
|
self.set_enabled(lambda: ui_state.is_offroad())
|
||||||
@@ -256,15 +246,12 @@ class TargetBranchButton(BigButton):
|
|||||||
self.set_value(target)
|
self.set_value(target)
|
||||||
|
|
||||||
def _on_click(self):
|
def _on_click(self):
|
||||||
if not ui_state.params.get("UpdaterAvailableBranches"):
|
|
||||||
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
|
|
||||||
return
|
|
||||||
gui_app.push_widget(BranchSelectPage(self._on_select))
|
gui_app.push_widget(BranchSelectPage(self._on_select))
|
||||||
|
|
||||||
def _on_select(self, branch: str):
|
def _on_select(self, branch: str):
|
||||||
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
||||||
self.set_value(branch)
|
self.set_value(branch)
|
||||||
self._check_update_btn.check_for_update()
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLayoutMici(NavScroller):
|
class SoftwareLayoutMici(NavScroller):
|
||||||
@@ -278,11 +265,10 @@ class SoftwareLayoutMici(NavScroller):
|
|||||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||||
|
|
||||||
check_update_btn = CheckUpdateButton()
|
|
||||||
self._scroller.add_widgets([
|
self._scroller.add_widgets([
|
||||||
SoftwareInfoLayoutMici(),
|
SoftwareInfoLayoutMici(),
|
||||||
check_update_btn,
|
CheckUpdateButton(),
|
||||||
InstallUpdateButton(),
|
InstallUpdateButton(),
|
||||||
TargetBranchButton(check_update_btn),
|
TargetBranchButton(),
|
||||||
uninstall_openpilot_btn,
|
uninstall_openpilot_btn,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
||||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||||
milestone_celebrations_toggle = BigParamControl("assisted driving milestones", "AssistedDrivingMilestonesEnabled")
|
|
||||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||||
@@ -58,7 +57,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
is_metric_toggle,
|
is_metric_toggle,
|
||||||
ldw_toggle,
|
ldw_toggle,
|
||||||
always_on_dm_toggle,
|
always_on_dm_toggle,
|
||||||
milestone_celebrations_toggle,
|
|
||||||
record_front,
|
record_front,
|
||||||
record_mic,
|
record_mic,
|
||||||
enable_openpilot,
|
enable_openpilot,
|
||||||
@@ -70,7 +68,6 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
("IsMetric", is_metric_toggle),
|
("IsMetric", is_metric_toggle),
|
||||||
("IsLdwEnabled", ldw_toggle),
|
("IsLdwEnabled", ldw_toggle),
|
||||||
("AlwaysOnDM", always_on_dm_toggle),
|
("AlwaysOnDM", always_on_dm_toggle),
|
||||||
("AssistedDrivingMilestonesEnabled", milestone_celebrations_toggle),
|
|
||||||
("RecordFront", record_front),
|
("RecordFront", record_front),
|
||||||
("RecordAudio", record_mic),
|
("RecordAudio", record_mic),
|
||||||
("OpenpilotEnabledToggle", enable_openpilot),
|
("OpenpilotEnabledToggle", enable_openpilot),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||||
from openpilot.common.hardware import COMMA_HARDWARE
|
from openpilot.common.hardware import COMMA_HARDWARE
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ AlertSize = log.SelfdriveState.AlertSize
|
|||||||
AlertStatus = log.SelfdriveState.AlertStatus
|
AlertStatus = log.SelfdriveState.AlertStatus
|
||||||
|
|
||||||
ALERT_MARGIN = 18
|
ALERT_MARGIN = 18
|
||||||
ALERT_BACKGROUND_OPACITY = 0.90
|
|
||||||
|
|
||||||
ALERT_FONT_SMALL = 66 - 50
|
ALERT_FONT_SMALL = 66 - 50
|
||||||
ALERT_FONT_BIG = 88 - 40
|
ALERT_FONT_BIG = 88 - 40
|
||||||
@@ -280,7 +279,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
def _draw_background(self, alert: Alert) -> None:
|
def _draw_background(self, alert: Alert) -> None:
|
||||||
# draw top gradient for alert text at top
|
# draw top gradient for alert text at top
|
||||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
||||||
color = rl.Color(color.r, color.g, color.b, int(255 * ALERT_BACKGROUND_OPACITY * self._alpha_filter.x))
|
color = rl.Color(color.r, color.g, color.b, int(255 * 0.90 * self._alpha_filter.x))
|
||||||
translucent_color = rl.Color(color.r, color.g, color.b, int(0 * self._alpha_filter.x))
|
translucent_color = rl.Color(color.r, color.g, color.b, int(0 * self._alpha_filter.x))
|
||||||
|
|
||||||
small_alert_height = round(self._rect.height * 0.583) # 140px at mici height
|
small_alert_height = round(self._rect.height * 0.583) # 140px at mici height
|
||||||
@@ -334,7 +333,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
self._alert_text1_label.set_text(alert_text1)
|
self._alert_text1_label.set_text(alert_text1)
|
||||||
self._alert_text1_label.set_text_color(color)
|
self._alert_text1_label.set_text_color(color)
|
||||||
self._alert_text1_label.set_font_size(font_size)
|
self._alert_text1_label.set_font_size(font_size)
|
||||||
self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
self._alert_text1_label.render(text_rect1)
|
self._alert_text1_label.render(text_rect1)
|
||||||
|
|
||||||
alert_text2 = alert.text2.lower()
|
alert_text2 = alert.text2.lower()
|
||||||
@@ -366,5 +365,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
self._alert_text2_label.set_text(alert_text2)
|
self._alert_text2_label.set_text(alert_text2)
|
||||||
self._alert_text2_label.set_text_color(color)
|
self._alert_text2_label.set_text_color(color)
|
||||||
self._alert_text2_label.set_font_size(small_font_size)
|
self._alert_text2_label.set_font_size(small_font_size)
|
||||||
self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
self._alert_text2_label.render(text_rect2)
|
self._alert_text2_label.render(text_rect2)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
|||||||
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
||||||
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
||||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.common.filter_simple import BounceFilter
|
from openpilot.common.filter_simple import BounceFilter
|
||||||
@@ -19,15 +19,10 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
|
|||||||
from openpilot.common.transformations.orientation import rot_from_euler
|
from openpilot.common.transformations.orientation import rot_from_euler
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
MILESTONE_CELEBRATION_ENABLED = gui_app.sunnypilot_ui()
|
|
||||||
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
||||||
|
|
||||||
if MILESTONE_CELEBRATION_ENABLED:
|
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_celebration import MilestoneCelebration
|
|
||||||
|
|
||||||
OpState = log.SelfdriveState.OpenpilotState
|
OpState = log.SelfdriveState.OpenpilotState
|
||||||
CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated
|
CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated
|
||||||
NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD
|
NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD
|
||||||
@@ -161,11 +156,10 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._alert_renderer = AlertRenderer()
|
self._alert_renderer = AlertRenderer()
|
||||||
self._driver_state_renderer = DriverStateRenderer()
|
self._driver_state_renderer = DriverStateRenderer()
|
||||||
self._confidence_ball = ConfidenceBall()
|
self._confidence_ball = ConfidenceBall()
|
||||||
self._milestone_celebration = self._child(MilestoneCelebration()) if MILESTONE_CELEBRATION_ENABLED else None
|
|
||||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||||
|
|
||||||
@@ -229,12 +223,6 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
||||||
|
|
||||||
if self._milestone_celebration is not None:
|
|
||||||
if alert_to_render is not None:
|
|
||||||
self._milestone_celebration.cancel_for_alert()
|
|
||||||
else:
|
|
||||||
self._milestone_celebration.render(self._content_rect)
|
|
||||||
|
|
||||||
# Hide DMoji when disengaged unless AlwaysOnDM is enabled
|
# Hide DMoji when disengaged unless AlwaysOnDM is enabled
|
||||||
should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and
|
should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and
|
||||||
(ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm))
|
(ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm))
|
||||||
@@ -259,6 +247,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._confidence_ball.render(self.rect)
|
self._confidence_ball.render(self.rect)
|
||||||
|
|
||||||
self._bookmark_icon.render(self.rect)
|
self._bookmark_icon.render(self.rect)
|
||||||
|
|
||||||
def _switch_stream_if_needed(self, sm):
|
def _switch_stream_if_needed(self, sm):
|
||||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
v_ego = sm['carState'].vEgo
|
v_ego = sm['carState'].vEgo
|
||||||
@@ -366,12 +355,10 @@ class AugmentedRoadView(CameraView):
|
|||||||
return self._cached_matrix
|
return self._cached_matrix
|
||||||
|
|
||||||
def show_event(self):
|
def show_event(self):
|
||||||
super().show_event()
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
|
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
|
||||||
|
|
||||||
def hide_event(self):
|
def hide_event(self):
|
||||||
super().hide_event()
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
|
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
|||||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||||
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
|
|
||||||
if not self._camera_view.frame:
|
if not self._camera_view.frame:
|
||||||
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER)
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
self._publish_alert_sound(None)
|
self._publish_alert_sound(None)
|
||||||
return
|
return
|
||||||
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
||||||
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
||||||
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=TextAlignment.RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
color=rl.Color(0, 0, 0, 180))
|
color=rl.Color(0, 0, 0, 180))
|
||||||
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=TextAlignment.RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
|
|
||||||
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
||||||
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
|
|
||||||
# Show alert level
|
# Show alert level
|
||||||
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
||||||
alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
|
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
||||||
|
|
||||||
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
||||||
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||||
color=rl.Color(0, 0, 0, 180))
|
color=rl.Color(0, 0, 0, 180))
|
||||||
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
|
|
||||||
def _load_eye_textures(self):
|
def _load_eye_textures(self):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.common.filter_simple import BounceFilter
|
from openpilot.common.filter_simple import BounceFilter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -125,10 +125,10 @@ class BigButton(Widget):
|
|||||||
self._rotate_icon_t: float | None = None
|
self._rotate_icon_t: float | None = None
|
||||||
|
|
||||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||||
text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
|
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||||
line_height=0.9)
|
line_height=0.9)
|
||||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||||
text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
self._update_label_layout()
|
self._update_label_layout()
|
||||||
|
|
||||||
self._load_images()
|
self._load_images()
|
||||||
@@ -167,9 +167,9 @@ class BigButton(Widget):
|
|||||||
def _update_label_layout(self):
|
def _update_label_layout(self):
|
||||||
self._label.set_font_size(self._get_label_font_size())
|
self._label.set_font_size(self._get_label_font_size())
|
||||||
if self.value:
|
if self.value:
|
||||||
self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
|
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
else:
|
else:
|
||||||
self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
|
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
|
|
||||||
def set_text(self, text: str):
|
def set_text(self, text: str):
|
||||||
self.text = text
|
self.text = text
|
||||||
@@ -356,8 +356,8 @@ class GreyBigButton(BigButton):
|
|||||||
self._sub_label.set_font_size(36)
|
self._sub_label.set_font_size(36)
|
||||||
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||||
self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
|
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
|
||||||
TextAlignmentVertical.BOTTOM)
|
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
self._sub_label.set_line_height(0.95)
|
self._sub_label.set_line_height(0.95)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|||||||
from openpilot.cereal import messaging, log
|
from openpilot.cereal import messaging, log
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.common.hardware import COMMA_HARDWARE
|
from openpilot.common.hardware import COMMA_HARDWARE
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
|
|||||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
# font size is set dynamically
|
# font size is set dynamically
|
||||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
|
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
|
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
|
|
||||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||||
"""Generate the current alert based on selfdrive state."""
|
"""Generate the current alert based on selfdrive state."""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
|||||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
|
|||||||
tr("camera starting"),
|
tr("camera starting"),
|
||||||
font_size=100,
|
font_size=100,
|
||||||
font_weight=FontWeight.BOLD,
|
font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
)
|
)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate
|
|||||||
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
|
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
|
||||||
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
||||||
|
|
||||||
|
AMBIENT_DB = 26 # DB where MIN_VOLUME is applied
|
||||||
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
||||||
|
|
||||||
|
VOLUME_BASE = 20
|
||||||
|
if HARDWARE.get_device_type() == "tizi":
|
||||||
|
AMBIENT_DB = 30
|
||||||
|
VOLUME_BASE = 10
|
||||||
|
|
||||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||||
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
||||||
|
|
||||||
@@ -47,7 +53,6 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
|||||||
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
|
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
|
||||||
|
|
||||||
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
|
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
|
||||||
AudibleAlert.complete: ("milestone.wav", 1, MAX_VOLUME),
|
|
||||||
|
|
||||||
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
|
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
|
||||||
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
|
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
|
||||||
@@ -55,14 +60,6 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
|||||||
**sound_list_sp,
|
**sound_list_sp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def calculate_volume_for_device(weighted_db: float, device_type: str) -> float:
|
|
||||||
ambient_db = 30 if device_type in ("mici", "tizi") else 26
|
|
||||||
volume_base = 10 if device_type in ("mici", "tizi") else 20
|
|
||||||
volume_boost = 1.5 if device_type == "mici" else 1.0
|
|
||||||
volume = ((weighted_db - ambient_db) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME
|
|
||||||
return min(MAX_VOLUME, volume_boost * math.pow(volume_base, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1)))
|
|
||||||
|
|
||||||
def check_selfdrive_timeout_alert(sm):
|
def check_selfdrive_timeout_alert(sm):
|
||||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||||
|
|
||||||
@@ -77,7 +74,6 @@ class Soundd(QuietMode):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.device_type = HARDWARE.get_device_type()
|
|
||||||
self.load_sounds()
|
self.load_sounds()
|
||||||
|
|
||||||
self.current_alert = AudibleAlert.none
|
self.current_alert = AudibleAlert.none
|
||||||
@@ -89,7 +85,6 @@ class Soundd(QuietMode):
|
|||||||
|
|
||||||
self.selfdrive_timeout_alert = False
|
self.selfdrive_timeout_alert = False
|
||||||
self.pending_stop = False
|
self.pending_stop = False
|
||||||
self.last_milestone_event_id = 0
|
|
||||||
|
|
||||||
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
||||||
|
|
||||||
@@ -169,19 +164,9 @@ class Soundd(QuietMode):
|
|||||||
self.update_alert(AudibleAlert.none)
|
self.update_alert(AudibleAlert.none)
|
||||||
self.selfdrive_timeout_alert = False
|
self.selfdrive_timeout_alert = False
|
||||||
|
|
||||||
def update_milestone_alert(self, sm):
|
|
||||||
if not sm.updated['assistedDrivingMilestoneState']:
|
|
||||||
return
|
|
||||||
milestone_state = sm['assistedDrivingMilestoneState']
|
|
||||||
event_id = milestone_state.event.id
|
|
||||||
if not milestone_state.enabled or event_id == 0 or event_id == self.last_milestone_event_id:
|
|
||||||
return
|
|
||||||
self.last_milestone_event_id = event_id
|
|
||||||
if self.current_alert == AudibleAlert.none and not self.enabled:
|
|
||||||
self.update_alert(AudibleAlert.complete)
|
|
||||||
|
|
||||||
def calculate_volume(self, weighted_db):
|
def calculate_volume(self, weighted_db):
|
||||||
return calculate_volume_for_device(weighted_db, self.device_type)
|
volume = ((weighted_db - AMBIENT_DB) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME
|
||||||
|
return math.pow(VOLUME_BASE, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1))
|
||||||
|
|
||||||
@retry(attempts=10, delay=3)
|
@retry(attempts=10, delay=3)
|
||||||
def get_stream(self, sd):
|
def get_stream(self, sd):
|
||||||
@@ -195,7 +180,7 @@ class Soundd(QuietMode):
|
|||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
micd.patch_sounddevice(sd)
|
micd.patch_sounddevice(sd)
|
||||||
|
|
||||||
sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure', 'assistedDrivingMilestoneState'])
|
sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure'])
|
||||||
|
|
||||||
with self.get_stream(sd) as stream:
|
with self.get_stream(sd) as stream:
|
||||||
rk = Ratekeeper(20)
|
rk = Ratekeeper(20)
|
||||||
@@ -213,7 +198,6 @@ class Soundd(QuietMode):
|
|||||||
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
||||||
|
|
||||||
self.get_audible_alert(sm)
|
self.get_audible_alert(sm)
|
||||||
self.update_milestone_alert(sm)
|
|
||||||
|
|
||||||
# Ramp up immediate warning sound over 4s
|
# Ramp up immediate warning sound over 4s
|
||||||
if self.current_alert == AudibleAlert.warningImmediate:
|
if self.current_alert == AudibleAlert.warningImmediate:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.multilang import tr, trn
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
@@ -59,7 +59,7 @@ class HomeLayoutSP(HomeLayout):
|
|||||||
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
||||||
desc_width = desc_size.x
|
desc_width = desc_size.x
|
||||||
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
||||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=TextAlignment.RIGHT)
|
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
|
|
||||||
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
||||||
spacing = BRAND_DESC_SPACING if description else 0
|
spacing = BRAND_DESC_SPACING if description else 0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
|||||||
self._done_callback = done_callback
|
self._done_callback = done_callback
|
||||||
self._step = 0
|
self._step = 0
|
||||||
|
|
||||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=TextAlignment.LEFT))
|
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||||
|
|
||||||
self._content = [
|
self._content = [
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ class SunnylinkConsentPage(Widget):
|
|||||||
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
|
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
|
||||||
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
|
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
|
||||||
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
|
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
|
||||||
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT))
|
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||||
|
|
||||||
def _handle_choice(self, choice):
|
def _handle_choice(self, choice):
|
||||||
if choice == "enable":
|
if choice == "enable":
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from openpilot.system.ui.widgets.list_view import button_item
|
|||||||
|
|
||||||
from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP
|
from openpilot.system.ui.sunnypilot.widgets.html_render import HtmlModalSP
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.external_storage import external_storage_item
|
||||||
|
|
||||||
PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt"
|
PREBUILT_PATH = os.path.join(Paths.comma_home(), "prebuilt") if PC else "/data/openpilot/prebuilt"
|
||||||
|
|
||||||
@@ -52,7 +53,11 @@ class DeveloperLayoutSP(DeveloperLayout):
|
|||||||
|
|
||||||
self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked)
|
self.error_log_btn = button_item(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes."), callback=self._on_error_log_clicked)
|
||||||
|
|
||||||
self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle, self.error_log_btn,]
|
self.external_storage = external_storage_item(tr("External Storage"), description=tr("Extend your comma device's storage by inserting a USB drive " +
|
||||||
|
"into the aux port."))
|
||||||
|
|
||||||
|
self.items: list = [self.show_advanced_controls, self.enable_github_runner_toggle, self.enable_copyparty_toggle, self.prebuilt_toggle,
|
||||||
|
self.external_storage, self.error_log_btn,]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _on_prebuilt_toggled(state):
|
def _on_prebuilt_toggled(state):
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
import pyray as rl
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import copy
|
||||||
|
from enum import Enum
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.hardware import PC
|
||||||
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
|
from openpilot.system.ui.widgets import DialogResult
|
||||||
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
|
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||||
|
from openpilot.system.ui.widgets.list_view import (
|
||||||
|
ItemAction,
|
||||||
|
ListItem,
|
||||||
|
BUTTON_HEIGHT,
|
||||||
|
BUTTON_BORDER_RADIUS,
|
||||||
|
BUTTON_FONT_SIZE,
|
||||||
|
BUTTON_WIDTH,
|
||||||
|
)
|
||||||
|
|
||||||
|
VALUE_FONT_SIZE = 48
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalStorageState(Enum):
|
||||||
|
DISABLED = tr_noop("DISABLED")
|
||||||
|
LOADING = tr_noop("LOADING")
|
||||||
|
CHECK = tr_noop("CHECK")
|
||||||
|
MOUNT = tr_noop("MOUNT")
|
||||||
|
UNMOUNT = tr_noop("UNMOUNT")
|
||||||
|
FORMAT = tr_noop("FORMAT")
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalStorageAction(ItemAction):
|
||||||
|
MAX_WIDTH = 500
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(self.MAX_WIDTH, True)
|
||||||
|
|
||||||
|
self._params = Params()
|
||||||
|
self._error_message = ""
|
||||||
|
self._text_font = gui_app.font(FontWeight.NORMAL)
|
||||||
|
|
||||||
|
self._button = Button(
|
||||||
|
"",
|
||||||
|
click_callback=self._handle_button_click,
|
||||||
|
button_style=ButtonStyle.LIST_ACTION,
|
||||||
|
border_radius=BUTTON_BORDER_RADIUS,
|
||||||
|
font_size=BUTTON_FONT_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._value_text = ""
|
||||||
|
self._formatting = False
|
||||||
|
self._refresh_pending = False
|
||||||
|
|
||||||
|
self._state = ExternalStorageState.CHECK
|
||||||
|
self._refresh_state()
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def set_touch_valid_callback(self, callback):
|
||||||
|
def wrapped():
|
||||||
|
if self._state == ExternalStorageState.DISABLED:
|
||||||
|
return False
|
||||||
|
return callback()
|
||||||
|
super().set_touch_valid_callback(wrapped)
|
||||||
|
self._button.set_touch_valid_callback(wrapped)
|
||||||
|
|
||||||
|
def _run(self, cmd: str) -> bool:
|
||||||
|
return subprocess.call(["sh", "-c", cmd]) == 0
|
||||||
|
|
||||||
|
def _run_output(self, cmd: str) -> str:
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(["sh", "-c", cmd], universal_newlines=True)
|
||||||
|
return out.strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle) -> bool:
|
||||||
|
if self._error_message:
|
||||||
|
msg = copy.copy(self._error_message)
|
||||||
|
gui_app.set_modal_overlay(alert_dialog(msg))
|
||||||
|
self._error_message = ""
|
||||||
|
|
||||||
|
if self._value_text:
|
||||||
|
text_size = measure_text_cached(self._text_font, self._value_text, VALUE_FONT_SIZE)
|
||||||
|
rl.draw_text_ex(
|
||||||
|
self._text_font,
|
||||||
|
self._value_text,
|
||||||
|
(rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30,
|
||||||
|
rect.y + (rect.height - text_size.y) / 2),
|
||||||
|
VALUE_FONT_SIZE,
|
||||||
|
1.0,
|
||||||
|
rl.Color(170, 170, 170, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
button_rect = rl.Rectangle(
|
||||||
|
rect.x + rect.width - BUTTON_WIDTH,
|
||||||
|
rect.y + (rect.height - BUTTON_HEIGHT) / 2,
|
||||||
|
BUTTON_WIDTH,
|
||||||
|
BUTTON_HEIGHT
|
||||||
|
)
|
||||||
|
self._button.set_rect(button_rect)
|
||||||
|
self._button.set_text(tr(self._state.value))
|
||||||
|
self._button.set_enabled(self._state not in (ExternalStorageState.LOADING,
|
||||||
|
ExternalStorageState.DISABLED))
|
||||||
|
self._button.render(button_rect)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _refresh_state(self):
|
||||||
|
if PC:
|
||||||
|
self._state = ExternalStorageState.DISABLED
|
||||||
|
self._button.set_enabled(False)
|
||||||
|
self._value_text = ""
|
||||||
|
|
||||||
|
def debounced_refresh(self):
|
||||||
|
if self._refresh_pending:
|
||||||
|
return
|
||||||
|
self._refresh_pending = True
|
||||||
|
|
||||||
|
def _timer():
|
||||||
|
import time
|
||||||
|
time.sleep(0.25)
|
||||||
|
self._refresh_pending = False
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
threading.Thread(target=_timer, daemon=True).start()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
def _work():
|
||||||
|
is_mounted = self._run("findmnt -n /mnt/external_realdata")
|
||||||
|
has_drive = self._run("lsblk -f /dev/sdg")
|
||||||
|
has_fs = self._run("lsblk -f /dev/sdg1 | grep -q ext4")
|
||||||
|
has_label = self._run("blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'")
|
||||||
|
|
||||||
|
info = ""
|
||||||
|
if is_mounted and has_label:
|
||||||
|
info = self._run_output(
|
||||||
|
"df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply():
|
||||||
|
if self._formatting:
|
||||||
|
self._value_text = tr("formatting")
|
||||||
|
self._state = ExternalStorageState.FORMAT
|
||||||
|
return
|
||||||
|
|
||||||
|
if not has_drive:
|
||||||
|
self._value_text = tr("insert drive")
|
||||||
|
self._state = ExternalStorageState.CHECK
|
||||||
|
|
||||||
|
elif not has_fs or not has_label:
|
||||||
|
self._value_text = tr("needs format")
|
||||||
|
self._state = ExternalStorageState.FORMAT
|
||||||
|
|
||||||
|
elif is_mounted:
|
||||||
|
self._value_text = info
|
||||||
|
self._state = ExternalStorageState.UNMOUNT
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._value_text = tr("drive detected")
|
||||||
|
self._state = ExternalStorageState.MOUNT
|
||||||
|
|
||||||
|
apply()
|
||||||
|
|
||||||
|
threading.Thread(target=_work, daemon=True).start()
|
||||||
|
|
||||||
|
def _handle_button_click(self):
|
||||||
|
st = self._state
|
||||||
|
|
||||||
|
if st == ExternalStorageState.DISABLED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if st in (ExternalStorageState.CHECK, ExternalStorageState.MOUNT):
|
||||||
|
self.mount_storage()
|
||||||
|
|
||||||
|
elif st == ExternalStorageState.UNMOUNT:
|
||||||
|
self.unmount_storage()
|
||||||
|
|
||||||
|
elif st == ExternalStorageState.FORMAT:
|
||||||
|
dialog = ConfirmDialog(
|
||||||
|
tr("Are you sure you want to format this drive? This will erase all data."),
|
||||||
|
confirm_text=tr("Format"),
|
||||||
|
cancel_text=tr("Cancel"),
|
||||||
|
)
|
||||||
|
gui_app.set_modal_overlay(dialog, callback=self._confirm_format)
|
||||||
|
|
||||||
|
def _confirm_format(self, result: DialogResult):
|
||||||
|
if result == DialogResult.CONFIRM:
|
||||||
|
self.format_storage()
|
||||||
|
|
||||||
|
def mount_storage(self):
|
||||||
|
self._value_text = tr("mounting")
|
||||||
|
self._state = ExternalStorageState.LOADING
|
||||||
|
|
||||||
|
def _work():
|
||||||
|
cmd = """
|
||||||
|
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' >> /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 /
|
||||||
|
"""
|
||||||
|
subprocess.call(["sh", "-c", cmd])
|
||||||
|
self.debounced_refresh()
|
||||||
|
|
||||||
|
threading.Thread(target=_work, daemon=True).start()
|
||||||
|
|
||||||
|
def unmount_storage(self):
|
||||||
|
self._value_text = tr("unmounting")
|
||||||
|
self._state = ExternalStorageState.LOADING
|
||||||
|
|
||||||
|
def _work():
|
||||||
|
subprocess.call(["sh", "-c", "sudo umount /mnt/external_realdata"])
|
||||||
|
self.debounced_refresh()
|
||||||
|
|
||||||
|
threading.Thread(target=_work, daemon=True).start()
|
||||||
|
|
||||||
|
def format_storage(self):
|
||||||
|
self._formatting = True
|
||||||
|
self._value_text = tr("formatting")
|
||||||
|
self._state = ExternalStorageState.LOADING
|
||||||
|
|
||||||
|
def _work():
|
||||||
|
cmd = """
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
exitcode = subprocess.call(["sh", "-c", cmd])
|
||||||
|
|
||||||
|
def apply():
|
||||||
|
self._formatting = False
|
||||||
|
if exitcode == 0:
|
||||||
|
self.mount_storage()
|
||||||
|
else:
|
||||||
|
self._value_text = tr("needs format")
|
||||||
|
self._state = ExternalStorageState.FORMAT
|
||||||
|
|
||||||
|
apply()
|
||||||
|
|
||||||
|
threading.Thread(target=_work, daemon=True).start()
|
||||||
|
|
||||||
|
def external_storage_item(title: str | Callable[[], str], description: str | Callable[[], str]) -> ListItem:
|
||||||
|
return ListItem(
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
action_item=ExternalStorageAction()
|
||||||
|
)
|
||||||
@@ -9,7 +9,7 @@ from openpilot.cereal import custom
|
|||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||||
@@ -32,8 +32,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=90,
|
font_size=90,
|
||||||
font_weight=FontWeight.AUDIOWIDE,
|
font_weight=FontWeight.AUDIOWIDE,
|
||||||
text_color=rl.WHITE,
|
text_color=rl.WHITE,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=False,
|
wrap_text=False,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -43,8 +43,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=40,
|
font_size=40,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.Color(0, 255, 0, 255), # Green
|
text_color=rl.Color(0, 255, 0, 255), # Green
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -55,8 +55,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=35,
|
font_size=35,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.Color(255, 165, 0, 255), # Orange
|
text_color=rl.Color(255, 165, 0, 255), # Orange
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -109,8 +109,8 @@ class SunnylinkDescriptionItem(Widget):
|
|||||||
font_size=40,
|
font_size=40,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.WHITE,
|
text_color=rl.WHITE,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False,
|
elide=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,89 +5,30 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
|||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
import time
|
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
|
|
||||||
METERS_PER_MILE = 1609.344
|
|
||||||
METERS_PER_KILOMETER = 1000.0
|
|
||||||
SUMMARY_DURATION_SECONDS = 10.0
|
|
||||||
SUMMARY_WAIT_SECONDS = 3.0
|
|
||||||
|
|
||||||
|
|
||||||
def _nonnegative_float(value) -> float:
|
|
||||||
try:
|
|
||||||
return max(0.0, float(value))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class MiciHomeLayoutSP(MiciHomeLayout):
|
class MiciHomeLayoutSP(MiciHomeLayout):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||||
initial_summary = ui_state.params.get("LastDriveAssistedDrivingSummary", return_default=True) or {}
|
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
||||||
self._last_summary_id = initial_summary.get("id", 0)
|
self._chestnut_loading_icon.set_visible(False)
|
||||||
self._summary_wait_until = 0.0
|
failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon)
|
||||||
self._summary_visible_until = 0.0
|
self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon)
|
||||||
self._drive_summary = {}
|
|
||||||
|
|
||||||
def request_drive_summary(self) -> None:
|
|
||||||
self._summary_wait_until = time.monotonic() + SUMMARY_WAIT_SECONDS
|
|
||||||
|
|
||||||
def _render(self, _: rl.Rectangle) -> None:
|
|
||||||
super()._render(_)
|
|
||||||
now = time.monotonic()
|
|
||||||
if now < self._summary_wait_until:
|
|
||||||
summary = ui_state.params.get("LastDriveAssistedDrivingSummary", return_default=True) or {}
|
|
||||||
summary_id = summary.get("id", 0)
|
|
||||||
if summary_id and summary_id != self._last_summary_id:
|
|
||||||
self._last_summary_id = summary_id
|
|
||||||
distances = summary.get("distancesMeters", {})
|
|
||||||
enabled = ui_state.params.get_bool("AssistedDrivingMilestonesEnabled")
|
|
||||||
if enabled and any(_nonnegative_float(distances.get(category, 0.0)) > 0.0 for category in ("mads", "fullAssist")):
|
|
||||||
self._drive_summary = summary
|
|
||||||
self._summary_visible_until = now + SUMMARY_DURATION_SECONDS
|
|
||||||
self._summary_wait_until = 0.0
|
|
||||||
|
|
||||||
if now < self._summary_visible_until:
|
|
||||||
self._draw_drive_summary(_)
|
|
||||||
|
|
||||||
def _draw_drive_summary(self, rect: rl.Rectangle) -> None:
|
|
||||||
distances = self._drive_summary.get("distancesMeters", {})
|
|
||||||
metric = self._drive_summary.get("unit") == "metric"
|
|
||||||
meters_per_unit = METERS_PER_KILOMETER if metric else METERS_PER_MILE
|
|
||||||
unit = "KM" if metric else "MI"
|
|
||||||
mads = _nonnegative_float(distances.get("mads", 0.0)) / meters_per_unit
|
|
||||||
full_assist = _nonnegative_float(distances.get("fullAssist", 0.0)) / meters_per_unit
|
|
||||||
|
|
||||||
rl.draw_rectangle_rec(rect, rl.Color(0, 0, 0, 235))
|
|
||||||
gui_label(rl.Rectangle(rect.x, rect.y + 14, rect.width, 52), tr("DRIVE COMPLETE"), 42,
|
|
||||||
font_weight=FontWeight.SEMI_BOLD, alignment=TextAlignment.CENTER)
|
|
||||||
gui_label(rl.Rectangle(rect.x + 20, rect.y + 78, rect.width / 2 - 30, 42), tr("MADS"), 28,
|
|
||||||
color=rl.Color(255, 255, 255, 184), alignment=TextAlignment.CENTER)
|
|
||||||
gui_label(rl.Rectangle(rect.x + rect.width / 2 + 10, rect.y + 78, rect.width / 2 - 30, 42), tr("FULL ASSIST"), 28,
|
|
||||||
color=rl.Color(255, 255, 255, 184), alignment=TextAlignment.CENTER)
|
|
||||||
gui_label(rl.Rectangle(rect.x + 20, rect.y + 116, rect.width / 2 - 30, 72), f"{mads:.1f} {unit}", 48,
|
|
||||||
font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER)
|
|
||||||
gui_label(rl.Rectangle(rect.x + rect.width / 2 + 10, rect.y + 116, rect.width / 2 - 30, 72), f"{full_assist:.1f} {unit}", 48,
|
|
||||||
font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER)
|
|
||||||
|
|
||||||
def _set_chestnut_visibility(self):
|
def _set_chestnut_visibility(self):
|
||||||
usb_connected = ui_state.usb_connected
|
# stock has no loading tier: it shows green from the moment a big model is available. keep the
|
||||||
usb_unknown = ui_state.usb_unknown
|
# pulse so the status bar and the onroad HUD agree on what loading looks like.
|
||||||
chestnut_state = ui_state.chestnut_state
|
loading = ui_state.chestnut_state == ChestnutState.LOADING
|
||||||
loading = chestnut_state == ChestnutState.LOADING
|
self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||||
|
self._chestnut_loading_icon.set_visible(loading)
|
||||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
||||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||||
self._chestnut_loading_icon.set_visible(not usb_unknown and loading)
|
|
||||||
self._chestnut_icon.set_visible(not usb_unknown and not loading and
|
|
||||||
chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
|
||||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
"""Render assisted-driving milestone celebrations over the on-road view."""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
from collections import deque
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
import pyray as rl
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
|
||||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import ALERT_BACKGROUND_OPACITY
|
|
||||||
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import FONT_SIZES
|
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
|
||||||
from openpilot.system.ui.widgets import Widget
|
|
||||||
|
|
||||||
|
|
||||||
CELEBRATION_DURATION = 4.5
|
|
||||||
PARTICLE_COUNT = 150
|
|
||||||
METERS_PER_MILE = 1609.344
|
|
||||||
METERS_PER_KILOMETER = 1000.0
|
|
||||||
|
|
||||||
CONFETTI_COLORS = (
|
|
||||||
rl.Color(255, 55, 95, 255),
|
|
||||||
rl.Color(255, 183, 3, 255),
|
|
||||||
rl.Color(48, 209, 88, 255),
|
|
||||||
rl.Color(36, 179, 255, 255),
|
|
||||||
rl.Color(112, 72, 232, 255),
|
|
||||||
rl.Color(255, 45, 196, 255),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ConfettiParticle:
|
|
||||||
x: float
|
|
||||||
y: float
|
|
||||||
width: float
|
|
||||||
height: float
|
|
||||||
speed: float
|
|
||||||
drift: float
|
|
||||||
angle: float
|
|
||||||
spin: float
|
|
||||||
phase: float
|
|
||||||
color: rl.Color
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CelebrationMilestone:
|
|
||||||
event_id: int
|
|
||||||
full_assist: bool
|
|
||||||
distance_meters: float
|
|
||||||
previous_distance_meters: float
|
|
||||||
metric: bool
|
|
||||||
|
|
||||||
|
|
||||||
class MilestoneCelebration(Widget):
|
|
||||||
"""Pure renderer for typed assisted-driving milestone events."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._drive_started_time = -1.0
|
|
||||||
self._celebration_started_time: float | None = None
|
|
||||||
self._current_milestone: CelebrationMilestone | None = None
|
|
||||||
self._pending_milestones: deque[CelebrationMilestone] = deque()
|
|
||||||
self._last_event_id = 0
|
|
||||||
self._particles = self._make_particles()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _make_particles() -> list[ConfettiParticle]:
|
|
||||||
rng = random.Random(20260828)
|
|
||||||
return [
|
|
||||||
ConfettiParticle(
|
|
||||||
x=rng.random(),
|
|
||||||
y=rng.uniform(-0.25, 0.95),
|
|
||||||
width=rng.uniform(10, 24),
|
|
||||||
height=rng.uniform(24, 58),
|
|
||||||
speed=rng.uniform(0.12, 0.34),
|
|
||||||
drift=rng.uniform(-0.035, 0.035),
|
|
||||||
angle=rng.uniform(0, 360),
|
|
||||||
spin=rng.uniform(-150, 150),
|
|
||||||
phase=rng.uniform(0, math.tau),
|
|
||||||
color=CONFETTI_COLORS[rng.randrange(len(CONFETTI_COLORS))],
|
|
||||||
)
|
|
||||||
for _ in range(PARTICLE_COUNT)
|
|
||||||
]
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle, /) -> None:
|
|
||||||
now = time.monotonic()
|
|
||||||
if ui_state.started_time != self._drive_started_time:
|
|
||||||
self._drive_started_time = ui_state.started_time
|
|
||||||
self._celebration_started_time = None
|
|
||||||
self._current_milestone = None
|
|
||||||
self._pending_milestones.clear()
|
|
||||||
|
|
||||||
self._consume_event(suppress=False)
|
|
||||||
|
|
||||||
if self._current_milestone is None and self._pending_milestones:
|
|
||||||
self._current_milestone = self._pending_milestones.popleft()
|
|
||||||
self._celebration_started_time = now
|
|
||||||
|
|
||||||
if self._celebration_started_time is None or self._current_milestone is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
elapsed = now - self._celebration_started_time
|
|
||||||
if elapsed >= CELEBRATION_DURATION:
|
|
||||||
self._celebration_started_time = None
|
|
||||||
self._current_milestone = None
|
|
||||||
return
|
|
||||||
|
|
||||||
alpha = min(1.0, elapsed / 0.2, (CELEBRATION_DURATION - elapsed) / 0.8)
|
|
||||||
self._draw_background_scrim(rect, alpha)
|
|
||||||
self._draw_confetti(rect, elapsed, alpha)
|
|
||||||
self._draw_milestone(rect, elapsed, alpha, self._current_milestone)
|
|
||||||
|
|
||||||
def cancel_for_alert(self) -> None:
|
|
||||||
self._consume_event(suppress=True)
|
|
||||||
self._celebration_started_time = None
|
|
||||||
self._current_milestone = None
|
|
||||||
self._pending_milestones.clear()
|
|
||||||
|
|
||||||
def _consume_event(self, suppress: bool) -> None:
|
|
||||||
if not ui_state.sm.updated["assistedDrivingMilestoneState"]:
|
|
||||||
return
|
|
||||||
state = ui_state.sm["assistedDrivingMilestoneState"]
|
|
||||||
event = state.event
|
|
||||||
if not state.enabled:
|
|
||||||
self._celebration_started_time = None
|
|
||||||
self._current_milestone = None
|
|
||||||
self._pending_milestones.clear()
|
|
||||||
return
|
|
||||||
if event.id == 0 or event.id == self._last_event_id:
|
|
||||||
return
|
|
||||||
self._last_event_id = event.id
|
|
||||||
if suppress:
|
|
||||||
return
|
|
||||||
self._pending_milestones.append(CelebrationMilestone(
|
|
||||||
event_id=event.id,
|
|
||||||
full_assist=event.category == custom.AssistedDrivingMilestoneState.Category.fullAssist,
|
|
||||||
distance_meters=event.distanceMeters,
|
|
||||||
previous_distance_meters=event.previousDistanceMeters,
|
|
||||||
metric=event.unit == custom.AssistedDrivingMilestoneState.Unit.metric,
|
|
||||||
))
|
|
||||||
|
|
||||||
def _draw_confetti(self, rect: rl.Rectangle, elapsed: float, alpha: float) -> None:
|
|
||||||
travel_height = rect.height * 1.45
|
|
||||||
compact = rect.height <= 300
|
|
||||||
particle_scale = rect.height / 1080.0
|
|
||||||
particles = self._particles[:100] if compact else self._particles
|
|
||||||
for particle in particles:
|
|
||||||
x = rect.x + rect.width * (particle.x + particle.drift * elapsed + 0.012 * math.sin(elapsed * 3 + particle.phase))
|
|
||||||
y = rect.y - rect.height * 0.2 + (particle.y * travel_height + particle.speed * rect.height * elapsed) % travel_height
|
|
||||||
flip = 0.2 + 0.8 * abs(math.sin(elapsed * 5 + particle.phase))
|
|
||||||
particle_rect = rl.Rectangle(x, y, particle.width * particle_scale * flip, particle.height * particle_scale)
|
|
||||||
origin = rl.Vector2(particle_rect.width / 2, particle_rect.height / 2)
|
|
||||||
color = rl.Color(particle.color.r, particle.color.g, particle.color.b, int(255 * alpha))
|
|
||||||
rl.draw_rectangle_pro(particle_rect, origin, particle.angle + particle.spin * elapsed, color)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _draw_milestone(rect: rl.Rectangle, elapsed: float, alpha: float, milestone: CelebrationMilestone) -> None:
|
|
||||||
# Match the comma four set-speed hierarchy: DISPLAY number with a MAX-sized label.
|
|
||||||
scale = rect.height / 240.0
|
|
||||||
pulse = 1.0 + 0.025 * math.sin(min(elapsed, 0.6) / 0.6 * math.pi)
|
|
||||||
number_size = int(FONT_SIZES.set_speed * scale * pulse)
|
|
||||||
milestone_size = int(FONT_SIZES.max_speed * scale * pulse)
|
|
||||||
category_size = int(22 * scale * pulse)
|
|
||||||
unit_size = category_size
|
|
||||||
|
|
||||||
display_font = gui_app.font(FontWeight.DISPLAY)
|
|
||||||
semibold_font = gui_app.font(FontWeight.SEMI_BOLD)
|
|
||||||
tween_progress = min(elapsed / 0.85, 1.0)
|
|
||||||
tween_progress = 1.0 - (1.0 - tween_progress) ** 3
|
|
||||||
meters_per_unit = METERS_PER_KILOMETER if milestone.metric else METERS_PER_MILE
|
|
||||||
previous_distance = milestone.previous_distance_meters / meters_per_unit
|
|
||||||
milestone_distance = milestone.distance_meters / meters_per_unit
|
|
||||||
displayed_distance = previous_distance + (milestone_distance - previous_distance) * tween_progress
|
|
||||||
if tween_progress >= 1.0:
|
|
||||||
number = f"{round(milestone_distance):,}"
|
|
||||||
else:
|
|
||||||
number = f"{displayed_distance:,.1f}"
|
|
||||||
unit = tr("KM") if milestone.metric else tr("MI")
|
|
||||||
category = tr("FULL ASSIST") if milestone.full_assist else tr("MADS")
|
|
||||||
milestone_label = tr("MILESTONE")
|
|
||||||
|
|
||||||
unit_bounds = measure_text_cached(semibold_font, unit, unit_size)
|
|
||||||
number_bounds = measure_text_cached(display_font, number, number_size)
|
|
||||||
max_number_width = rect.width * 0.72 - unit_bounds.x - 8 * scale
|
|
||||||
if number_bounds.x > max_number_width:
|
|
||||||
number_size = max(1, int(number_size * max_number_width / number_bounds.x))
|
|
||||||
number_bounds = measure_text_cached(display_font, number, number_size)
|
|
||||||
category_bounds = measure_text_cached(semibold_font, category, category_size)
|
|
||||||
milestone_bounds = measure_text_cached(semibold_font, milestone_label, milestone_size)
|
|
||||||
|
|
||||||
center_x = rect.x + rect.width / 2
|
|
||||||
center_y = rect.y + rect.height / 2
|
|
||||||
text_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
|
||||||
secondary_color = rl.Color(255, 255, 255, int(255 * 0.72 * alpha))
|
|
||||||
number_line_width = number_bounds.x + 8 * scale + unit_bounds.x
|
|
||||||
number_x = center_x - number_line_width / 2
|
|
||||||
number_y = center_y - 76 * scale
|
|
||||||
unit_y = center_y + 14 * scale
|
|
||||||
category_y = center_y - 91 * scale
|
|
||||||
milestone_y = center_y + 50 * scale
|
|
||||||
|
|
||||||
rl.draw_text_ex(semibold_font, category, rl.Vector2(center_x - category_bounds.x / 2, category_y),
|
|
||||||
category_size, 0, secondary_color)
|
|
||||||
rl.draw_text_ex(display_font, number, rl.Vector2(number_x, number_y), number_size, 0, text_color)
|
|
||||||
rl.draw_text_ex(semibold_font, unit, rl.Vector2(number_x + number_bounds.x + 8 * scale, unit_y),
|
|
||||||
unit_size, 0, secondary_color)
|
|
||||||
rl.draw_text_ex(semibold_font, milestone_label, rl.Vector2(center_x - milestone_bounds.x / 2, milestone_y),
|
|
||||||
milestone_size, 0, text_color)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _draw_background_scrim(rect: rl.Rectangle, alpha: float) -> None:
|
|
||||||
# Match the alert background: a mostly opaque black core fading to transparent.
|
|
||||||
fade_height = round(rect.height * 0.25)
|
|
||||||
solid_height = round(rect.height * 0.50)
|
|
||||||
solid_color = rl.Color(0, 0, 0, int(255 * ALERT_BACKGROUND_OPACITY * alpha))
|
|
||||||
transparent = rl.Color(0, 0, 0, 0)
|
|
||||||
x = int(rect.x)
|
|
||||||
y = int(rect.y)
|
|
||||||
width = int(rect.width)
|
|
||||||
|
|
||||||
rl.draw_rectangle_gradient_v(x, y, width, fade_height, transparent, solid_color)
|
|
||||||
rl.draw_rectangle(x, y + fade_height, width, solid_height, solid_color)
|
|
||||||
rl.draw_rectangle_gradient_v(x, y + fade_height + solid_height, width, fade_height, solid_color, transparent)
|
|
||||||
@@ -35,8 +35,7 @@ class UIStateSP:
|
|||||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||||
self.sm_services_ext = [
|
self.sm_services_ext = [
|
||||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||||
"gpsLocation", "lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay",
|
"gpsLocation", "lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay"
|
||||||
"assistedDrivingMilestoneState",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.sunnylink_state = SunnylinkState()
|
self.sunnylink_state = SunnylinkState()
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate the assisted-driving milestone celebration chime."""
|
|
||||||
|
|
||||||
import math
|
|
||||||
import wave
|
|
||||||
from array import array
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
SAMPLE_RATE = 48_000
|
|
||||||
DURATION_SECONDS = 0.82
|
|
||||||
NOTES = (
|
|
||||||
(0.00, 523.25),
|
|
||||||
(0.11, 659.25),
|
|
||||||
(0.22, 783.99),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def note_sample(age: float, frequency: float) -> float:
|
|
||||||
if not 0 <= age <= 0.58:
|
|
||||||
return 0.0
|
|
||||||
attack = min(age / 0.008, 1.0)
|
|
||||||
release = min((0.58 - age) / 0.15, 1.0)
|
|
||||||
envelope = attack * release * math.exp(-3.8 * age)
|
|
||||||
tone = math.sin(math.tau * frequency * age) + 0.16 * math.sin(math.tau * frequency * 2 * age)
|
|
||||||
return envelope * tone
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
output = Path(__file__).parents[4] / "openpilot/selfdrive/assets/sounds/milestone.wav"
|
|
||||||
samples = array('h')
|
|
||||||
for frame in range(round(SAMPLE_RATE * DURATION_SECONDS)):
|
|
||||||
t = frame / SAMPLE_RATE
|
|
||||||
value = 0.38 * sum(note_sample(t - start, frequency) for start, frequency in NOTES)
|
|
||||||
samples.append(round(max(-1.0, min(1.0, value)) * 32767))
|
|
||||||
|
|
||||||
with wave.open(str(output), "wb") as wav:
|
|
||||||
wav.setnchannels(1)
|
|
||||||
wav.setsampwidth(2)
|
|
||||||
wav.setframerate(SAMPLE_RATE)
|
|
||||||
wav.writeframes(samples.tobytes())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Publish deterministic milestone events for the local comma-four UI preview."""
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import time
|
|
||||||
|
|
||||||
from openpilot.cereal import messaging
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
pm = messaging.PubMaster(["assistedDrivingMilestoneState"])
|
|
||||||
milestones = itertools.cycle(((1, 0, "mads"), (2, 1, "fullAssist"), (5, 2, "mads"), (10, 5, "fullAssist")))
|
|
||||||
event_id = 0
|
|
||||||
milestone, previous_milestone, category = 0, 0, "mads"
|
|
||||||
next_event_time = time.monotonic() + 1.0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
now = time.monotonic()
|
|
||||||
if now >= next_event_time:
|
|
||||||
event_id += 1
|
|
||||||
milestone, previous_milestone, category = next(milestones)
|
|
||||||
next_event_time = now + 6.0
|
|
||||||
|
|
||||||
msg = messaging.new_message("assistedDrivingMilestoneState")
|
|
||||||
state = msg.assistedDrivingMilestoneState
|
|
||||||
state.enabled = True
|
|
||||||
if event_id:
|
|
||||||
state.event.id = event_id
|
|
||||||
state.event.category = category
|
|
||||||
state.event.distanceMeters = milestone * 1609.344
|
|
||||||
state.event.previousDistanceMeters = previous_milestone * 1609.344
|
|
||||||
state.event.unit = "imperial"
|
|
||||||
pm.send("assistedDrivingMilestoneState", msg)
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
|
||||||
replay_pid=""
|
|
||||||
preview_pid=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
for pid in "$preview_pid" "$replay_pid"; do
|
|
||||||
if [[ -n "$pid" ]]; then
|
|
||||||
kill "$pid" 2>/dev/null || true
|
|
||||||
wait "$pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
trap cleanup EXIT INT TERM
|
|
||||||
|
|
||||||
export PATH="$repo_root/.venv/bin:$PATH"
|
|
||||||
export SP_MILESTONE_PREVIEW=1
|
|
||||||
playback="${SP_MILESTONE_PLAYBACK:-1}"
|
|
||||||
|
|
||||||
"$repo_root/openpilot/tools/replay/replay" --demo --playback "$playback" &
|
|
||||||
replay_pid=$!
|
|
||||||
"$repo_root/.venv/bin/python" "$repo_root/openpilot/selfdrive/ui/tests/milestone_preview.py" &
|
|
||||||
preview_pid=$!
|
|
||||||
|
|
||||||
"$repo_root/.venv/bin/python" "$repo_root/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py"
|
|
||||||
@@ -4,63 +4,12 @@ import time
|
|||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
from openpilot.cereal import log, messaging
|
from openpilot.cereal import log, messaging
|
||||||
from openpilot.cereal.messaging import SubMaster, PubMaster
|
from openpilot.cereal.messaging import SubMaster, PubMaster
|
||||||
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, Soundd, calculate_volume_for_device, check_selfdrive_timeout_alert
|
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
|
||||||
|
|
||||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||||
|
|
||||||
|
|
||||||
class TestSoundd(OpenpilotTestCase):
|
class TestSoundd(OpenpilotTestCase):
|
||||||
@staticmethod
|
|
||||||
def milestone_submaster(event_id=42):
|
|
||||||
class SubMasterStub:
|
|
||||||
def __init__(self):
|
|
||||||
self.updated = {'assistedDrivingMilestoneState': True}
|
|
||||||
msg = messaging.new_message('assistedDrivingMilestoneState')
|
|
||||||
msg.assistedDrivingMilestoneState.enabled = True
|
|
||||||
msg.assistedDrivingMilestoneState.event.id = event_id
|
|
||||||
self.data = {'assistedDrivingMilestoneState': msg.assistedDrivingMilestoneState}
|
|
||||||
|
|
||||||
def __getitem__(self, service):
|
|
||||||
return self.data[service]
|
|
||||||
|
|
||||||
return SubMasterStub()
|
|
||||||
|
|
||||||
def test_comma_four_volume_is_50_percent_louder_than_comma_three_x(self):
|
|
||||||
for weighted_db in (20.0, 30.0, 40.0, 50.0):
|
|
||||||
with self.subTest(weighted_db=weighted_db):
|
|
||||||
comma_three_x_volume = calculate_volume_for_device(weighted_db, "tizi")
|
|
||||||
comma_four_volume = calculate_volume_for_device(weighted_db, "mici")
|
|
||||||
assert comma_four_volume == min(1.0, comma_three_x_volume * 1.5)
|
|
||||||
|
|
||||||
def test_milestone_chime_uses_typed_milestone_event_once(self):
|
|
||||||
soundd = Soundd()
|
|
||||||
sm = self.milestone_submaster()
|
|
||||||
soundd.update_milestone_alert(sm)
|
|
||||||
|
|
||||||
assert soundd.current_alert == AudibleAlert.complete
|
|
||||||
soundd.current_alert = AudibleAlert.none
|
|
||||||
soundd.update_milestone_alert(sm)
|
|
||||||
assert soundd.current_alert == AudibleAlert.none
|
|
||||||
|
|
||||||
def test_safety_alert_consumes_milestone_without_replaying_it(self):
|
|
||||||
soundd = Soundd()
|
|
||||||
sm = self.milestone_submaster()
|
|
||||||
soundd.current_alert = AudibleAlert.warningImmediate
|
|
||||||
|
|
||||||
soundd.update_milestone_alert(sm)
|
|
||||||
soundd.current_alert = AudibleAlert.none
|
|
||||||
soundd.update_milestone_alert(sm)
|
|
||||||
|
|
||||||
assert soundd.current_alert == AudibleAlert.none
|
|
||||||
|
|
||||||
def test_quiet_mode_consumes_milestone_without_playing_it(self):
|
|
||||||
soundd = Soundd()
|
|
||||||
soundd.enabled = True
|
|
||||||
|
|
||||||
soundd.update_milestone_alert(self.milestone_submaster())
|
|
||||||
|
|
||||||
assert soundd.current_alert == AudibleAlert.none
|
|
||||||
|
|
||||||
def test_check_selfdrive_timeout_alert(self, mocker):
|
def test_check_selfdrive_timeout_alert(self, mocker):
|
||||||
sm = SubMaster(['selfdriveState', 'selfdriveStateSP'])
|
sm = SubMaster(['selfdriveState', 'selfdriveStateSP'])
|
||||||
pm = PubMaster(['selfdriveState', 'selfdriveStateSP'])
|
pm = PubMaster(['selfdriveState', 'selfdriveStateSP'])
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from openpilot.common.swaglog import cloudlog
|
|||||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.common.hardware import HARDWARE, PC
|
from openpilot.common.hardware import HARDWARE, PC
|
||||||
from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int
|
|
||||||
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
||||||
|
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||||
@@ -96,10 +95,6 @@ class UIState(UIStateSP):
|
|||||||
self.chestnut_compiled: bool = chestnut_compiled()
|
self.chestnut_compiled: bool = chestnut_compiled()
|
||||||
self.chestnut_active: bool | None = None
|
self.chestnut_active: bool | None = None
|
||||||
self.chestnut_loading: bool = False
|
self.chestnut_loading: bool = False
|
||||||
self.usb_connected: bool = False
|
|
||||||
self.usb_connected_ts: float | None = None
|
|
||||||
self.usb_disconnected_ts: float | None = None
|
|
||||||
self.usb_unknown: bool = False
|
|
||||||
self.chestnut_state = ChestnutState.DISCONNECTED
|
self.chestnut_state = ChestnutState.DISCONNECTED
|
||||||
self.started: bool = False
|
self.started: bool = False
|
||||||
self.ignition: bool = False
|
self.ignition: bool = False
|
||||||
@@ -259,23 +254,6 @@ class UIState(UIStateSP):
|
|||||||
self.chestnut_compiled = chestnut_compiled()
|
self.chestnut_compiled = chestnut_compiled()
|
||||||
self.chestnut_active = self.params.get("ChestnutActive")
|
self.chestnut_active = self.params.get("ChestnutActive")
|
||||||
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
||||||
now = time.monotonic()
|
|
||||||
if read_int(TYPEC_CC_ORIENTATION_PATH) != 0:
|
|
||||||
self.usb_disconnected_ts = None
|
|
||||||
if not self.usb_connected:
|
|
||||||
self.usb_connected = True
|
|
||||||
self.usb_connected_ts = now
|
|
||||||
self.usb_unknown = False
|
|
||||||
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
|
|
||||||
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
|
|
||||||
self.usb_connected_ts = None
|
|
||||||
elif self.usb_connected:
|
|
||||||
if self.usb_disconnected_ts is None:
|
|
||||||
self.usb_disconnected_ts = now
|
|
||||||
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
|
|
||||||
self.usb_connected = False
|
|
||||||
self.usb_connected_ts = None
|
|
||||||
self.usb_unknown = False
|
|
||||||
|
|
||||||
UIStateSP.update_params(self)
|
UIStateSP.update_params(self)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user