mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-25 03:22:07 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41ce29af86 | |||
| dfc3c98b22 | |||
| 107a6f4c00 | |||
| 059d0b6c4c | |||
| c51ffe3808 | |||
| a15aed1a79 | |||
| 78007e82e0 | |||
| b1a6223b14 | |||
| e771dfa007 | |||
| c28eb95874 | |||
| 7ed960f713 | |||
| 7e2b8430c5 | |||
| 521fa09b0d | |||
| b9aa1962ca | |||
| 6b1b6aca05 | |||
| 41a8bc3fc4 | |||
| 540f4f5933 | |||
| 53e5ae0578 | |||
| 2182be05ea | |||
| 3e44c90c68 | |||
| 2d35bd895f | |||
| 855d5022ad | |||
| 6a363365ab | |||
| ddb9039493 | |||
| 0b7df7df10 | |||
| dd3feac854 |
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement
|
||||||
|
about: For openpilot enhancement suggestions
|
||||||
|
title: ''
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
@@ -23,56 +23,43 @@ env:
|
|||||||
CI: 1
|
CI: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate_cereal_artifact:
|
|
||||||
name: Generate cereal validation artifacts
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- run: ./tools/op.sh setup
|
|
||||||
- name: Build openpilot
|
|
||||||
run: scons -j$(nproc) cereal
|
|
||||||
- name: Dump sunnypilot schema
|
|
||||||
run: |
|
|
||||||
export PYTHONPATH=${{ github.workspace }}
|
|
||||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema.json
|
|
||||||
- name: 'Prepare artifact'
|
|
||||||
run: |
|
|
||||||
mkdir -p "cereal/messaging/tests/cereal_validations"
|
|
||||||
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
|
|
||||||
cp schema.json "cereal/messaging/tests/cereal_validations/schema.json"
|
|
||||||
- name: 'Upload Artifact'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cereal_validations
|
|
||||||
path: cereal/messaging/tests/cereal_validations
|
|
||||||
|
|
||||||
validate_cereal_with_upstream:
|
validate_cereal_with_upstream:
|
||||||
name: Validate cereal with Upstream
|
name: Validate cereal with Upstream
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: generate_cereal_artifact
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sunnypilot
|
- name: Checkout sunnypilot cereal
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Checkout upstream openpilot
|
with:
|
||||||
|
sparse-checkout: cereal
|
||||||
|
|
||||||
|
- name: Init sunnypilot opendbc submodule
|
||||||
|
run: git submodule update --init --depth 1 opendbc_repo
|
||||||
|
|
||||||
|
- name: Checkout upstream openpilot cereal
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'commaai/openpilot'
|
repository: 'commaai/openpilot'
|
||||||
path: openpilot
|
path: upstream_openpilot
|
||||||
submodules: true
|
sparse-checkout: cereal
|
||||||
ref: "refs/heads/master"
|
ref: "refs/heads/master"
|
||||||
- run: ./tools/op.sh setup
|
|
||||||
- name: Build openpilot
|
- name: Init upstream opendbc submodule
|
||||||
working-directory: openpilot
|
working-directory: upstream_openpilot
|
||||||
run: scons -j$(nproc) cereal
|
run: git submodule update --init --depth 1 opendbc_repo
|
||||||
- name: Download build artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
- name: Install uv
|
||||||
with:
|
run: pip install uv
|
||||||
name: cereal_validations
|
|
||||||
path: openpilot/cereal/messaging/tests/cereal_validations
|
- name: Generate sunnypilot schema
|
||||||
- name: 'Validate sunnypilot schema against upstream'
|
|
||||||
run: |
|
run: |
|
||||||
export PYTHONPATH=${{ github.workspace }}/openpilot
|
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||||
chmod +x openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
|
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||||
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema.json
|
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||||
|
-g -f /tmp/sp_schema.json --cereal-dir cereal
|
||||||
|
|
||||||
|
- name: Validate against upstream
|
||||||
|
run: |
|
||||||
|
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||||
|
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||||
|
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||||
|
-r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ jobs:
|
|||||||
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
|
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
|
||||||
|
|
||||||
echo "Compiling: $onnx_file -> $output_file"
|
echo "Compiling: $onnx_file -> $output_file"
|
||||||
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 IMAGE=2 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
|
QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
|
||||||
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Validate Model Outputs
|
- name: Validate Model Outputs
|
||||||
|
|||||||
+89
-1
@@ -1,4 +1,7 @@
|
|||||||
sunnypilot Version 2026.001.000 (2026-03-xx)
|
sunnypilot Version 2026.002.000 (2026-xx-xx)
|
||||||
|
========================
|
||||||
|
|
||||||
|
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||||
========================
|
========================
|
||||||
* What's Changed (sunnypilot/sunnypilot)
|
* What's Changed (sunnypilot/sunnypilot)
|
||||||
* Complete rewrite of the user interface from Qt C++ to Raylib Python
|
* Complete rewrite of the user interface from Qt C++ to Raylib Python
|
||||||
@@ -66,6 +69,64 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
|
|||||||
* Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89
|
* Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89
|
||||||
* SCC-V: Use p97 for predicted lateral accel by @yasu-oh
|
* SCC-V: Use p97 for predicted lateral accel by @yasu-oh
|
||||||
* Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin
|
* Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin
|
||||||
|
* [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP` by @sunnyhaibin
|
||||||
|
* [TIZI/TICI] ui: use `vCruiseCluster` and `vEgoCluster` for SLA `preActive` by @sunnyhaibin
|
||||||
|
* Fix display of values when using use_float_scaling by @CHaucke89
|
||||||
|
* models: fix default & index "0" by @nayan8teen
|
||||||
|
* [TIZI/TICI] visuals: Improved speed limit by @angaz
|
||||||
|
* ICBM: ensure button timers update on disable to clear stale presses by @jamesmikesell
|
||||||
|
* [TIZI/TICI] ui: simplify Smart Cruise Control text rendering by @sunnyhaibin
|
||||||
|
* controlsd: fix steer_limited_by_safety not updating under MADS by @zephleggett
|
||||||
|
* soundd: trigger timeout warning during MADS lateral-only by @zephleggett
|
||||||
|
* pandad: flasher for Rivian long upgrade module by @lukasloetkolben
|
||||||
|
* modeld_v2: tinygrad transformation warp by @Discountchubbs
|
||||||
|
* tools: block `manage_sunnylinkd` in sim startup script by @sunnyhaibin
|
||||||
|
* [MICI] ui: need superclass `_render` in `HudRendererSP` by @sunnyhaibin
|
||||||
|
* [TIZI/TICI] ui: Speed Limit Assist active status by @sunnyhaibin
|
||||||
|
* ui: reimplement "Screen Off" option to Onroad Brightness by @sunnyhaibin
|
||||||
|
* ui: don't hide steering wheel when blindspot disabled by @royjr
|
||||||
|
* ui: Speed Limit Assist `preActive` improvements by @sunnyhaibin
|
||||||
|
* ui: consolidate Speed Limit Assist `preActive` status rendering by @sunnyhaibin
|
||||||
|
* [MICI] ui: Speed Limit Assist `preActive` status by @sunnyhaibin
|
||||||
|
* sunnypilot modeld: remove thneed modeld by @Discountchubbs
|
||||||
|
* modeld_v2: decouple planplus scaling from accel by @Discountchubbs
|
||||||
|
* sunnylink: Handle exceptions in `getParamsAllKeysV1` to log crashes by @devtekve
|
||||||
|
* [TIZI/TICI] ui: Developer UI cleanup by @sunnyhaibin
|
||||||
|
* [TIZI/TICI] ui: dynamic alert size by @nayan8teen
|
||||||
|
* i18n(fr): Add French translations by @didlawowo
|
||||||
|
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
|
||||||
|
* ui: `AlertFadeAnimator` for longitudinal-related statuses by @sunnyhaibin
|
||||||
|
* pandad: gate unsupported pandas before flashing by @sunnyhaibin
|
||||||
|
* Rivian: Flash xnor's Longitudinal Upgrade Kit prior supported panda check by @lukasloetkolben
|
||||||
|
* [TIZI/TICI] ui: add back gate steering arc behind toggle by @sunnyhaibin
|
||||||
|
* ui: gate Onroad Brightness Delay on readiness by @sunnyhaibin
|
||||||
|
* ui: add new timer options for Onroad Brightness Delay by @sunnyhaibin
|
||||||
|
* [TIZI/TICI] ui: branch switcher is always available by @sunnyhaibin
|
||||||
|
* pandad: always prioritize internal panda by @sunnyhaibin
|
||||||
|
* sunnylinkd: fetch compressed params schema by @sunnyhaibin
|
||||||
|
* sunnypilot locationd: remove unused car_ekf filter by @sunnyhaibin
|
||||||
|
* modeld_v2: update deprecated temporalPose ref by @sunnyhaibin
|
||||||
|
* NNLC: restore pre-v1 PID gains in torque extension by @mmmorks
|
||||||
|
* MADS safety: enable heartbeat and lateral controls mismatch checks by @sunnyhaibin
|
||||||
|
* [MICI] ui: models panel enhancements by @nayan8teen
|
||||||
|
* [TIZI/TICI] ui: fix unintended selection while scrolling in TreeOptionDialog by @TheSecurityDev
|
||||||
|
* tools: script for video concatenation by @Discountchubbs
|
||||||
|
* tools: profile memory usage by @Discountchubbs
|
||||||
|
* [TIZI/TICI] ui: remove per-frame param sync by @sunnyhaibin
|
||||||
|
* [MICI] ui: always offroad by @nayan8teen
|
||||||
|
* controls: always default Torque Lateral Control to v0 Tune by @sunnyhaibin
|
||||||
|
* Revert "controls: always default Torque Lateral Control to v0 Tune" by @sunnyhaibin
|
||||||
|
* Reapply "controls: always default Torque Lateral Control to v0 Tune" (#1806) by @sunnyhaibin
|
||||||
|
* [MICI] ui: add sunnylink info & connectivity check by @nayan8teen
|
||||||
|
* sunnylink: Remove unused API endpoint by @devtekve
|
||||||
|
* DM: wheel touch enforcement in MADS by @sunnyhaibin
|
||||||
|
* torque: show static override values in Dev UI & gate `useParams` on custom torque tune by @sunnyhaibin
|
||||||
|
* MADS: suppress espActive event when long is not engaged by @sunnyhaibin
|
||||||
|
* sunnylink: SDUI by @sunnyhaibin
|
||||||
|
* [MICI] ui: align upstream changes with sunnypilot settings buttons by @nayan8teen
|
||||||
|
* ui: fix cellular toggles by @AmyJeanes
|
||||||
|
* sunnylink: switch athena domain by @devtekve
|
||||||
|
* Platform List: dynamically migrate CarPlatformBundle by @sunnyhaibin
|
||||||
* What's Changed (sunnypilot/opendbc)
|
* What's Changed (sunnypilot/opendbc)
|
||||||
* Honda: DBC for Accord 9th Generation by @mvl-boston
|
* Honda: DBC for Accord 9th Generation by @mvl-boston
|
||||||
* FCA: update tire stiffness values for `RAM_HD` by @dparring
|
* FCA: update tire stiffness values for `RAM_HD` by @dparring
|
||||||
@@ -84,12 +145,25 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
|
|||||||
* Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston
|
* Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston
|
||||||
* GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin
|
* GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin
|
||||||
* GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin
|
* GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin
|
||||||
|
* Hyundai Longitudinal: deprecate ramp update for dynamic tune by @Discountchubbs
|
||||||
|
* Rivian: long upgrade messages on bus 1 by @lukasloetkolben
|
||||||
|
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
|
||||||
|
* Toyota: gate Smart DSU behind Alpha Longitudinal by @sunnyhaibin
|
||||||
|
* Toyota: Gas Interceptor always set `standstill_req` by @sunnyhaibin
|
||||||
|
* MADS safety: dedicated `controls_allowed_lateral` by @sunnyhaibin
|
||||||
|
* Platform List: include community supported platforms by @sunnyhaibin
|
||||||
* New Contributors (sunnypilot/sunnypilot)
|
* New Contributors (sunnypilot/sunnypilot)
|
||||||
* @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots"
|
* @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots"
|
||||||
* @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters"
|
* @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters"
|
||||||
* @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar"
|
* @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar"
|
||||||
* @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay"
|
* @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay"
|
||||||
* @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel"
|
* @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel"
|
||||||
|
* @angaz made their first contribution in "[TIZI/TICI] visuals: Improved speed limit"
|
||||||
|
* @jamesmikesell made their first contribution in "ICBM: ensure button timers update on disable to clear stale presses"
|
||||||
|
* @zephleggett made their first contribution in "controlsd: fix steer_limited_by_safety not updating under MADS"
|
||||||
|
* @lukasloetkolben made their first contribution in "pandad: flasher for Rivian long upgrade module"
|
||||||
|
* @didlawowo made their first contribution in "i18n(fr): Add French translations"
|
||||||
|
* @mmmorks made their first contribution in "NNLC: restore pre-v1 PID gains in torque extension"
|
||||||
* New Contributors (sunnypilot/opendbc)
|
* New Contributors (sunnypilot/opendbc)
|
||||||
* @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled"
|
* @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled"
|
||||||
* @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name"
|
* @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name"
|
||||||
@@ -99,6 +173,20 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
|
|||||||
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
|
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
|
||||||
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
|
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
|
||||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
|
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
|
||||||
|
************************
|
||||||
|
* Synced with commaai's openpilot (v0.11.1)
|
||||||
|
* master commit c001f3c9b490a80e69539f0af6022f6e07ceb721 (April 16, 2026)
|
||||||
|
* New driver monitoring model
|
||||||
|
* Improved image processing pipeline for driver camera
|
||||||
|
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
|
||||||
|
* New driving model #36798
|
||||||
|
* Fully trained using a learned simulator
|
||||||
|
* Improved longitudinal performance in Experimental mode
|
||||||
|
* Reduce comma four standby power usage by 77% to 52 mW
|
||||||
|
* Kia K7 2017 support thanks to royjr!
|
||||||
|
* Lexus LS 2018 support thanks to Hacheoy!
|
||||||
|
* Improved inter-process communication memory efficiency
|
||||||
|
* comma four support
|
||||||
|
|
||||||
sunnypilot Version 2025.002.000 (2025-11-06)
|
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||||
========================
|
========================
|
||||||
|
|||||||
@@ -194,6 +194,13 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
|||||||
aTarget @5 :Float32;
|
aTarget @5 :Float32;
|
||||||
events @6 :List(OnroadEventSP.Event);
|
events @6 :List(OnroadEventSP.Event);
|
||||||
e2eAlerts @7 :E2eAlerts;
|
e2eAlerts @7 :E2eAlerts;
|
||||||
|
accelPersonality @8 :AccelerationPersonality;
|
||||||
|
|
||||||
|
enum AccelerationPersonality {
|
||||||
|
sport @0;
|
||||||
|
normal @1;
|
||||||
|
eco @2;
|
||||||
|
}
|
||||||
|
|
||||||
struct DynamicExperimentalControl {
|
struct DynamicExperimentalControl {
|
||||||
state @0 :DynamicExperimentalControlState;
|
state @0 :DynamicExperimentalControlState;
|
||||||
|
|||||||
+25
-98
@@ -273,7 +273,11 @@ struct GPSNMEAData {
|
|||||||
nmea @2 :Text;
|
nmea @2 :Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# android sensor_event_t
|
||||||
struct SensorEventData {
|
struct SensorEventData {
|
||||||
|
version @0 :Int32;
|
||||||
|
sensor @1 :Int32;
|
||||||
|
type @2 :Int32;
|
||||||
timestamp @3 :Int64;
|
timestamp @3 :Int64;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
@@ -292,10 +296,7 @@ struct SensorEventData {
|
|||||||
|
|
||||||
struct SensorVec {
|
struct SensorVec {
|
||||||
v @0 :List(Float32);
|
v @0 :List(Float32);
|
||||||
|
status @1 :Int8;
|
||||||
deprecated :group {
|
|
||||||
status @1 :Int8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SensorSource {
|
enum SensorSource {
|
||||||
@@ -313,11 +314,7 @@ struct SensorEventData {
|
|||||||
mmc5603nj @11;
|
mmc5603nj @11;
|
||||||
}
|
}
|
||||||
|
|
||||||
# formerly based on android sensor_event_t
|
|
||||||
deprecated :group {
|
deprecated :group {
|
||||||
version @0 :Int32;
|
|
||||||
sensor @1 :Int32;
|
|
||||||
type @2 :Int32;
|
|
||||||
uncalibrated @10 :Bool;
|
uncalibrated @10 :Bool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,10 +457,10 @@ struct DeviceState @0xa4d8b5af2aa492eb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum ThermalStatus {
|
enum ThermalStatus {
|
||||||
ok @0;
|
green @0;
|
||||||
warmDEPRECATED @1;
|
yellow @1;
|
||||||
overheated @2;
|
red @2;
|
||||||
critical @3;
|
danger @3;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NetworkType {
|
enum NetworkType {
|
||||||
@@ -2057,16 +2054,14 @@ struct DriverStateV2 {
|
|||||||
facePosition @2 :List(Float32);
|
facePosition @2 :List(Float32);
|
||||||
facePositionStd @3 :List(Float32);
|
facePositionStd @3 :List(Float32);
|
||||||
faceProb @4 :Float32;
|
faceProb @4 :Float32;
|
||||||
eyesVisibleProb @14 :Float32;
|
leftEyeProb @5 :Float32;
|
||||||
eyesClosedProb @15 :Float32;
|
rightEyeProb @6 :Float32;
|
||||||
|
leftBlinkProb @7 :Float32;
|
||||||
|
rightBlinkProb @8 :Float32;
|
||||||
|
sunglassesProb @9 :Float32;
|
||||||
phoneProb @13 :Float32;
|
phoneProb @13 :Float32;
|
||||||
|
|
||||||
deprecated :group {
|
deprecated :group {
|
||||||
leftEyeProb @5 :Float32;
|
|
||||||
rightEyeProb @6 :Float32;
|
|
||||||
leftBlinkProb @7 :Float32;
|
|
||||||
rightBlinkProb @8 :Float32;
|
|
||||||
sunglassesProb @9 :Float32;
|
|
||||||
notReadyProb @12 :List(Float32);
|
notReadyProb @12 :List(Float32);
|
||||||
occludedProb @10 :Float32;
|
occludedProb @10 :Float32;
|
||||||
readyProb @11 :List(Float32);
|
readyProb @11 :List(Float32);
|
||||||
@@ -2079,7 +2074,7 @@ struct DriverStateV2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
|
struct DriverMonitoringState @0xb83cda094a1da284 {
|
||||||
events @18 :List(OnroadEvent);
|
events @18 :List(OnroadEvent);
|
||||||
faceDetected @1 :Bool;
|
faceDetected @1 :Bool;
|
||||||
isDistracted @2 :Bool;
|
isDistracted @2 :Bool;
|
||||||
@@ -2107,75 +2102,6 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DriverMonitoringState {
|
|
||||||
lockout @0 :Bool;
|
|
||||||
alertCountLockoutPercent @1 :Int8;
|
|
||||||
alertTimeLockoutPercent @2 :Int8;
|
|
||||||
|
|
||||||
alwaysOn @3 :Bool;
|
|
||||||
alwaysOnLockout @4 :Bool;
|
|
||||||
|
|
||||||
alertLevel @5 :AlertLevel;
|
|
||||||
activePolicy @6 :MonitoringPolicy;
|
|
||||||
isRHD @7 :Bool;
|
|
||||||
rhdCalibration @8 :CalibrationState;
|
|
||||||
|
|
||||||
visionPolicyState @9 :VisionPolicyState;
|
|
||||||
wheeltouchPolicyState @10 :WheeltouchPolicyState;
|
|
||||||
|
|
||||||
enum AlertLevel {
|
|
||||||
# ordinal must match the name to prevent bugs
|
|
||||||
# comparing against the raw ordinal value
|
|
||||||
none @0;
|
|
||||||
one @1;
|
|
||||||
two @2;
|
|
||||||
three @3;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MonitoringPolicy {
|
|
||||||
wheeltouch @0;
|
|
||||||
vision @1;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VisionPolicyState {
|
|
||||||
awarenessPercent @0 :Int8;
|
|
||||||
awarenessStep @1 :Float32;
|
|
||||||
isDistracted @2 :Bool;
|
|
||||||
distractedTypes @3 :DistractedTypes;
|
|
||||||
|
|
||||||
faceDetected @4 :Bool;
|
|
||||||
pose @5 :Pose;
|
|
||||||
wheeltouchFallbackPercent @6 :Int8;
|
|
||||||
uncertainOffroadAlertPercent @7 :Int8;
|
|
||||||
|
|
||||||
struct DistractedTypes {
|
|
||||||
pose @0: Bool;
|
|
||||||
eye @1: Bool;
|
|
||||||
phone @2: Bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Pose {
|
|
||||||
pitch @0 :Float32;
|
|
||||||
yaw @1 :Float32;
|
|
||||||
pitchCalib @2 :CalibrationState;
|
|
||||||
yawCalib @3 :CalibrationState;
|
|
||||||
calibrated @4 :Bool;
|
|
||||||
uncertainty @5 :Float32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WheeltouchPolicyState {
|
|
||||||
awarenessPercent @0 :Int8;
|
|
||||||
awarenessStep @1 :Float32;
|
|
||||||
driverInteracting @2 :Bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CalibrationState {
|
|
||||||
calibratedPercent @0 :Int8;
|
|
||||||
offset @1 :Float32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Boot {
|
struct Boot {
|
||||||
wallTimeNanos @0 :UInt64;
|
wallTimeNanos @0 :UInt64;
|
||||||
pstore @4 :Map(Text, Data);
|
pstore @4 :Map(Text, Data);
|
||||||
@@ -2449,6 +2375,7 @@ struct Event {
|
|||||||
boot @60 :Boot;
|
boot @60 :Boot;
|
||||||
|
|
||||||
# ********** openpilot daemon msgs **********
|
# ********** openpilot daemon msgs **********
|
||||||
|
gpsNMEA @3 :GPSNMEAData;
|
||||||
can @5 :List(CanData);
|
can @5 :List(CanData);
|
||||||
controlsState @7 :ControlsState;
|
controlsState @7 :ControlsState;
|
||||||
selfdriveState @130 :SelfdriveState;
|
selfdriveState @130 :SelfdriveState;
|
||||||
@@ -2473,6 +2400,7 @@ struct Event {
|
|||||||
qcomGnss @31 :QcomGnss;
|
qcomGnss @31 :QcomGnss;
|
||||||
gpsLocationExternal @48 :GpsLocationData;
|
gpsLocationExternal @48 :GpsLocationData;
|
||||||
gpsLocation @21 :GpsLocationData;
|
gpsLocation @21 :GpsLocationData;
|
||||||
|
gnssMeasurements @91 :GnssMeasurements;
|
||||||
liveParameters @61 :LiveParametersData;
|
liveParameters @61 :LiveParametersData;
|
||||||
liveTorqueParameters @94 :LiveTorqueParametersData;
|
liveTorqueParameters @94 :LiveTorqueParametersData;
|
||||||
liveDelay @146 : LiveDelayData;
|
liveDelay @146 : LiveDelayData;
|
||||||
@@ -2480,7 +2408,7 @@ struct Event {
|
|||||||
thumbnail @66: Thumbnail;
|
thumbnail @66: Thumbnail;
|
||||||
onroadEvents @134: List(OnroadEvent);
|
onroadEvents @134: List(OnroadEvent);
|
||||||
carParams @69: Car.CarParams;
|
carParams @69: Car.CarParams;
|
||||||
driverMonitoringState @151 :DriverMonitoringState;
|
driverMonitoringState @71: DriverMonitoringState;
|
||||||
livePose @129 :LivePose;
|
livePose @129 :LivePose;
|
||||||
modelV2 @75 :ModelDataV2;
|
modelV2 @75 :ModelDataV2;
|
||||||
drivingModelData @128 :DrivingModelData;
|
drivingModelData @128 :DrivingModelData;
|
||||||
@@ -2506,6 +2434,7 @@ struct Event {
|
|||||||
# systems stuff
|
# systems stuff
|
||||||
androidLog @20 :AndroidLogEntry;
|
androidLog @20 :AndroidLogEntry;
|
||||||
managerState @78 :ManagerState;
|
managerState @78 :ManagerState;
|
||||||
|
uploaderState @79 :UploaderState;
|
||||||
procLog @33 :ProcLog;
|
procLog @33 :ProcLog;
|
||||||
clocks @35 :Clocks;
|
clocks @35 :Clocks;
|
||||||
deviceState @6 :DeviceState;
|
deviceState @6 :DeviceState;
|
||||||
@@ -2515,6 +2444,12 @@ struct Event {
|
|||||||
# touch frame
|
# touch frame
|
||||||
touch @135 :List(Touch);
|
touch @135 :List(Touch);
|
||||||
|
|
||||||
|
# navigation
|
||||||
|
navInstruction @82 :NavInstruction;
|
||||||
|
navRoute @83 :NavRoute;
|
||||||
|
navThumbnail @84: Thumbnail;
|
||||||
|
mapRenderState @105: MapRenderState;
|
||||||
|
|
||||||
# UI services
|
# UI services
|
||||||
uiDebug @102 :UIDebug;
|
uiDebug @102 :UIDebug;
|
||||||
|
|
||||||
@@ -2616,13 +2551,5 @@ struct Event {
|
|||||||
gyroscope2DEPRECATED @100 :SensorEventData;
|
gyroscope2DEPRECATED @100 :SensorEventData;
|
||||||
accelerometer2DEPRECATED @101 :SensorEventData;
|
accelerometer2DEPRECATED @101 :SensorEventData;
|
||||||
temperatureSensor2DEPRECATED @123 :SensorEventData;
|
temperatureSensor2DEPRECATED @123 :SensorEventData;
|
||||||
driverMonitoringStateDEPRECATED @71 :DriverMonitoringStateDEPRECATED;
|
|
||||||
gpsNMEADEPRECATED @3 :GPSNMEAData;
|
|
||||||
uploaderStateDEPRECATED @79 :UploaderState;
|
|
||||||
navInstructionDEPRECATED @82 :NavInstruction;
|
|
||||||
navRouteDEPRECATED @83 :NavRoute;
|
|
||||||
navThumbnailDEPRECATED @84 :Thumbnail;
|
|
||||||
gnssMeasurementsDEPRECATED @91 :GnssMeasurements;
|
|
||||||
mapRenderStateDEPRECATED @105: MapRenderState;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -104,8 +105,15 @@ def collect_schema(root: Any) -> dict[str, dict]:
|
|||||||
return structs
|
return structs
|
||||||
|
|
||||||
|
|
||||||
def dump_schema(path: str) -> None:
|
def load_log(cereal_dir: str) -> Any:
|
||||||
from cereal import log
|
import capnp
|
||||||
|
cereal_dir = os.path.abspath(cereal_dir)
|
||||||
|
capnp.remove_import_hook()
|
||||||
|
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
|
||||||
|
|
||||||
|
|
||||||
|
def dump_schema(cereal_dir: str, path: str) -> None:
|
||||||
|
log = load_log(cereal_dir)
|
||||||
payload = {
|
payload = {
|
||||||
"root": hex_id(log.Event.schema.node.id),
|
"root": hex_id(log.Event.schema.node.id),
|
||||||
"structs": collect_schema(log.Event.schema),
|
"structs": collect_schema(log.Event.schema),
|
||||||
@@ -206,8 +214,8 @@ def load_peer(path: str) -> dict:
|
|||||||
return json.load(handle)
|
return json.load(handle)
|
||||||
|
|
||||||
|
|
||||||
def run_read(peer_path: str) -> int:
|
def run_read(cereal_dir: str, peer_path: str) -> int:
|
||||||
from cereal import log
|
log = load_log(cereal_dir)
|
||||||
peer_dump = load_peer(peer_path)
|
peer_dump = load_peer(peer_path)
|
||||||
local_dump = {
|
local_dump = {
|
||||||
"root": hex_id(log.Event.schema.node.id),
|
"root": hex_id(log.Event.schema.node.id),
|
||||||
@@ -235,16 +243,13 @@ def main() -> int:
|
|||||||
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
|
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
|
||||||
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
|
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
|
||||||
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
|
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
|
||||||
|
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
if args.generate:
|
||||||
if args.generate:
|
dump_schema(args.cereal_dir, args.file)
|
||||||
dump_schema(args.file)
|
return 0
|
||||||
return 0
|
return run_read(args.cereal_dir, args.file)
|
||||||
return run_read(args.file)
|
|
||||||
except ImportError as exc:
|
|
||||||
print(f"error: cannot import cereal ({exc}). did scons build cereal?")
|
|
||||||
return 2
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ _services: dict[str, tuple] = {
|
|||||||
# note: the "EncodeIdx" packets will still be in the log
|
# note: the "EncodeIdx" packets will still be in the log
|
||||||
"gyroscope": (True, 104., 104),
|
"gyroscope": (True, 104., 104),
|
||||||
"accelerometer": (True, 104., 104),
|
"accelerometer": (True, 104., 104),
|
||||||
|
"magnetometer": (True, 25.),
|
||||||
|
"lightSensor": (True, 100., 100),
|
||||||
"temperatureSensor": (True, 2., 200),
|
"temperatureSensor": (True, 2., 200),
|
||||||
|
"gpsNMEA": (True, 9.),
|
||||||
"deviceState": (True, 2., 1),
|
"deviceState": (True, 2., 1),
|
||||||
"touch": (True, 20., 1),
|
"touch": (True, 20., 1),
|
||||||
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
|
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
|
||||||
@@ -53,6 +56,7 @@ _services: dict[str, tuple] = {
|
|||||||
"gpsLocation": (True, 1., 1),
|
"gpsLocation": (True, 1., 1),
|
||||||
"ubloxGnss": (True, 10.),
|
"ubloxGnss": (True, 10.),
|
||||||
"qcomGnss": (True, 2.),
|
"qcomGnss": (True, 2.),
|
||||||
|
"gnssMeasurements": (True, 10., 10),
|
||||||
"clocks": (True, 0.1, 1),
|
"clocks": (True, 0.1, 1),
|
||||||
"ubloxRaw": (True, 20.),
|
"ubloxRaw": (True, 20.),
|
||||||
"livePose": (True, 20., 4),
|
"livePose": (True, 20., 4),
|
||||||
@@ -71,6 +75,10 @@ _services: dict[str, tuple] = {
|
|||||||
"drivingModelData": (True, 20., 10),
|
"drivingModelData": (True, 20., 10),
|
||||||
"modelV2": (True, 20., None, QueueSize.BIG),
|
"modelV2": (True, 20., None, QueueSize.BIG),
|
||||||
"managerState": (True, 2., 1),
|
"managerState": (True, 2., 1),
|
||||||
|
"uploaderState": (True, 0., 1),
|
||||||
|
"navInstruction": (True, 1., 10),
|
||||||
|
"navRoute": (True, 0.),
|
||||||
|
"navThumbnail": (True, 0.),
|
||||||
"qRoadEncodeIdx": (False, 20.),
|
"qRoadEncodeIdx": (False, 20.),
|
||||||
"userBookmark": (True, 0., 1),
|
"userBookmark": (True, 0., 1),
|
||||||
"soundPressure": (True, 10., 10),
|
"soundPressure": (True, 10., 10),
|
||||||
@@ -106,6 +114,8 @@ _services: dict[str, tuple] = {
|
|||||||
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||||
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||||
"customReservedRawData0": (True, 0.),
|
"customReservedRawData0": (True, 0.),
|
||||||
|
"customReservedRawData1": (True, 0.),
|
||||||
|
"customReservedRawData2": (True, 0.),
|
||||||
}
|
}
|
||||||
SERVICE_LIST = {name: Service(*vals) for
|
SERVICE_LIST = {name: Service(*vals) for
|
||||||
idx, (name, vals) in enumerate(_services.items())}
|
idx, (name, vals) in enumerate(_services.items())}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
#define DEFAULT_MODEL "CD210 (Default)"
|
|
||||||
@@ -135,6 +135,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
|
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||||
|
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
@@ -204,6 +206,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
// sunnylink params
|
// sunnylink params
|
||||||
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
|
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
|
||||||
|
{"ParamsVersion", {PERSISTENT, INT}},
|
||||||
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
|
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
|
||||||
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
|
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
|
||||||
{"SunnylinkDongleId", {PERSISTENT, STRING}},
|
{"SunnylinkDongleId", {PERSISTENT, STRING}},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from markdown.extensions import Extension
|
|||||||
from markdown.preprocessors import Preprocessor
|
from markdown.preprocessors import Preprocessor
|
||||||
from markdown.treeprocessors import Treeprocessor
|
from markdown.treeprocessors import Treeprocessor
|
||||||
|
|
||||||
from zensical.extensions.links import LinksTreeprocessor
|
from zensical.extensions.links import LinksProcessor
|
||||||
|
|
||||||
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ class GlossaryTreeprocessor(Treeprocessor):
|
|||||||
def run(self, root: ET.Element) -> None:
|
def run(self, root: ET.Element) -> None:
|
||||||
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
||||||
processor = self.md.treeprocessors[at]
|
processor = self.md.treeprocessors[at]
|
||||||
if not isinstance(processor, LinksTreeprocessor):
|
if not isinstance(processor, LinksProcessor):
|
||||||
raise TypeError("Links processor not registered")
|
raise TypeError("Links processor not registered")
|
||||||
if processor.path == GLOSSARY_PAGE:
|
if processor.path == GLOSSARY_PAGE:
|
||||||
return
|
return
|
||||||
|
|||||||
+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="18.1"
|
export AGNOS_VERSION="17.2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 81b7b3a2d2...4dad7b09dd
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:6b45f88128430fb50ef51c8e08b8e2a1c8fbe0b5c3a08de9f5d9d59bc1edc82e
|
|
||||||
size 4545
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c
|
||||||
|
size 3688
|
||||||
@@ -86,7 +86,7 @@ class Car:
|
|||||||
|
|
||||||
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
||||||
|
|
||||||
is_release = self.params.get_bool("IsReleaseBranch")
|
is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||||
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
|
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
|
||||||
|
|
||||||
if CI is None:
|
if CI is None:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class TestVCruiseHelper:
|
|||||||
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
self.enable(V_CRUISE_INITIAL * CV.KPH_TO_MS, False, False)
|
||||||
|
|
||||||
# Expected diff on enabling. Speed should not change on falling edge of pressed
|
# Expected diff on enabling. Speed should not change on falling edge of pressed
|
||||||
assert (not pressed) == (self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
|
assert not pressed == self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last
|
||||||
|
|
||||||
def test_resume_in_standstill(self):
|
def test_resume_in_standstill(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class Controls(ControlsExt):
|
|||||||
cs.upAccelCmd = float(self.LoC.pid.p)
|
cs.upAccelCmd = float(self.LoC.pid.p)
|
||||||
cs.uiAccelCmd = float(self.LoC.pid.i)
|
cs.uiAccelCmd = float(self.LoC.pid.i)
|
||||||
cs.ufAccelCmd = float(self.LoC.pid.f)
|
cs.ufAccelCmd = float(self.LoC.pid.f)
|
||||||
cs.forceDecel = bool((self.sm['driverMonitoringState'].alertLevel == log.DriverMonitoringState.AlertLevel.three) or
|
cs.forceDecel = bool((self.sm['driverMonitoringState'].awarenessStatus < 0.) or
|
||||||
(self.sm['selfdriveState'].state == State.softDisabling))
|
(self.sm['selfdriveState'].state == State.softDisabling))
|
||||||
|
|
||||||
lat_tuning = self.CP.lateralTuning.which()
|
lat_tuning = self.CP.lateralTuning.which()
|
||||||
|
|||||||
@@ -313,11 +313,14 @@ class LongitudinalMpc:
|
|||||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||||
return lead_xv
|
return lead_xv
|
||||||
|
|
||||||
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
|
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard, a_cruise_min=None):
|
||||||
t_follow = get_T_FOLLOW(personality)
|
t_follow = get_T_FOLLOW(personality)
|
||||||
v_ego = self.x0[1]
|
v_ego = self.x0[1]
|
||||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||||
|
|
||||||
|
if a_cruise_min is None:
|
||||||
|
a_cruise_min = CRUISE_MIN_ACCEL
|
||||||
|
|
||||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||||
|
|
||||||
@@ -329,7 +332,7 @@ class LongitudinalMpc:
|
|||||||
|
|
||||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||||
# when the leads are no factor.
|
# when the leads are no factor.
|
||||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
||||||
# TODO does this make sense when max_a is negative?
|
# TODO does this make sense when max_a is negative?
|
||||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
# No change cost when user is controlling the speed, or when standstill
|
# No change cost when user is controlling the speed, or when standstill
|
||||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||||
|
|
||||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
accel_clip = self.get_accel_clip(v_ego) or [ACCEL_MIN, get_max_accel(v_ego)]
|
||||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||||
|
|
||||||
@@ -138,7 +138,8 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
|
|
||||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||||
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
|
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality,
|
||||||
|
a_cruise_min=self.get_cruise_min_accel(v_ego))
|
||||||
|
|
||||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||||
|
|||||||
+17
-47
@@ -1,23 +1,10 @@
|
|||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from itertools import product
|
|
||||||
from SCons.Script import Value
|
from SCons.Script import Value
|
||||||
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
|
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
|
||||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
|
||||||
from openpilot.selfdrive.modeld.helpers import CompileConfig
|
|
||||||
from tinygrad import Device
|
from tinygrad import Device
|
||||||
|
|
||||||
CAMERA_CONFIGS = [
|
|
||||||
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
|
||||||
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
|
||||||
]
|
|
||||||
MODELD_CONFIGS = [CompileConfig(cam_w, cam_h, prepare_only, 'driving_')
|
|
||||||
for (cam_w, cam_h), prepare_only in product(CAMERA_CONFIGS, [True, False])]
|
|
||||||
DM_WARP_CONFIGS = [CompileConfig(cam_w, cam_h, True, 'dm_') for cam_w, cam_h in CAMERA_CONFIGS]
|
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
chunker_file = File("#common/file_chunker.py")
|
chunker_file = File("#common/file_chunker.py")
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -29,17 +16,18 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
|||||||
def estimate_pickle_max_size(onnx_size):
|
def estimate_pickle_max_size(onnx_size):
|
||||||
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||||
|
|
||||||
|
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
||||||
# get fastest TG config
|
# get fastest TG config
|
||||||
available = set(Device.get_available_devices())
|
available = set(Device.get_available_devices())
|
||||||
if 'CUDA' in available:
|
# FIXME-SP: reset when we bump tg
|
||||||
|
if False: # 'CUDA' in available:
|
||||||
tg_backend = 'CUDA'
|
tg_backend = 'CUDA'
|
||||||
tg_flags = f'DEV={tg_backend}'
|
tg_flags = f'DEV={tg_backend}'
|
||||||
elif 'QCOM' in available:
|
elif 'QCOM' in available:
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0'
|
||||||
else:
|
else:
|
||||||
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
|
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU CPU_LLVM=1' # FIXME-SP: reset when we bump tg
|
||||||
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
|
||||||
tg_flags = f'DEV={tg_backend} THREADS=0'
|
tg_flags = f'DEV={tg_backend} THREADS=0'
|
||||||
|
|
||||||
def write_tg_compiled_flags(target, source, env):
|
def write_tg_compiled_flags(target, source, env):
|
||||||
@@ -66,35 +54,14 @@ for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
|||||||
image_flag = {
|
image_flag = {
|
||||||
'larch64': 'IMAGE=2',
|
'larch64': 'IMAGE=2',
|
||||||
}.get(arch, 'IMAGE=0')
|
}.get(arch, 'IMAGE=0')
|
||||||
modeld_dir = Dir("#selfdrive/modeld").abspath
|
script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
|
||||||
compile_modeld_script = [File(f"{modeld_dir}/compile_modeld.py")]
|
compile_warp_cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py '
|
||||||
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
driving_onnx_deps = [File(f"models/{m}.onnx").abspath for m in ['driving_vision', 'driving_policy']]
|
warp_targets = []
|
||||||
driving_metadata_deps = [File(f"models/{m}_metadata.pkl").abspath for m in ['driving_vision', 'driving_policy']]
|
for cam in [_ar_ox_fisheye, _os_fisheye]:
|
||||||
|
w, h = cam.width, cam.height
|
||||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath]
|
||||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
lenv.Command(warp_targets, tinygrad_files + script_files + [compiled_flags_node], compile_warp_cmd)
|
||||||
for cfg in MODELD_CONFIGS:
|
|
||||||
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_modeld.py '
|
|
||||||
f'--model-size {model_w}x{model_h} '
|
|
||||||
f'--nv12 {",".join(str(x) for x in cfg.nv12)} '
|
|
||||||
f'--vision-onnx {File("models/driving_vision.onnx").abspath} '
|
|
||||||
f'--policy-onnx {File("models/driving_policy.onnx").abspath} '
|
|
||||||
f'--output {cfg.pkl_path} --frame-skip {frame_skip}'
|
|
||||||
+ (' --prepare-only' if cfg.prepare_only else ''))
|
|
||||||
node = lenv.Command(cfg.pkl_path, tinygrad_files + compile_modeld_script + driving_onnx_deps + driving_metadata_deps + [chunker_file, compiled_flags_node], cmd)
|
|
||||||
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
|
|
||||||
chunk_targets = get_chunk_paths(cfg.pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
|
|
||||||
def do_chunk(target, source, env, pkl=cfg.pkl_path, chunks=chunk_targets):
|
|
||||||
chunk_file(pkl, chunks)
|
|
||||||
lenv.Command(chunk_targets, node, do_chunk)
|
|
||||||
|
|
||||||
dm_w, dm_h = DM_INPUT_SIZE
|
|
||||||
for cfg in DM_WARP_CONFIGS:
|
|
||||||
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_dm_warp.py '
|
|
||||||
f'--nv12 {",".join(str(x) for x in cfg.nv12)} --warp-to {dm_w}x{dm_h} '
|
|
||||||
f'--output {cfg.pkl_path}')
|
|
||||||
lenv.Command(cfg.pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [compiled_flags_node], cmd)
|
|
||||||
|
|
||||||
def tg_compile(flags, model_name):
|
def tg_compile(flags, model_name):
|
||||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||||
@@ -115,4 +82,7 @@ def tg_compile(flags, model_name):
|
|||||||
do_chunk,
|
do_chunk,
|
||||||
)
|
)
|
||||||
|
|
||||||
tg_compile(tg_flags, 'dmonitoring_model')
|
# Compile small models
|
||||||
|
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||||
|
tg_compile(tg_flags, model_name)
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import pickle
|
|
||||||
import time
|
|
||||||
|
|
||||||
from tinygrad.tensor import Tensor
|
|
||||||
from tinygrad.device import Device
|
|
||||||
from tinygrad.engine.jit import TinyJit
|
|
||||||
|
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, warp_perspective_tinygrad, _parse_size, _parse_nv12
|
|
||||||
|
|
||||||
|
|
||||||
def make_warp_dm(nv12: NV12Frame, dm_w, dm_h):
|
|
||||||
cam_w, cam_h, stride, _, _, _ = nv12
|
|
||||||
stride_pad = stride - cam_w
|
|
||||||
|
|
||||||
def warp_dm(input_frame, M_inv):
|
|
||||||
M_inv = M_inv.to(Device.DEFAULT).realize()
|
|
||||||
return warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv,
|
|
||||||
(dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
|
|
||||||
return warp_dm
|
|
||||||
|
|
||||||
|
|
||||||
def compile_dm_warp(nv12: NV12Frame, dm_w, dm_h, pkl_path):
|
|
||||||
print(f"Compiling DM warp for {nv12.width}x{nv12.height} -> {dm_w}x{dm_h}...")
|
|
||||||
|
|
||||||
warp_dm_jit = TinyJit(make_warp_dm(nv12, dm_w, dm_h), prune=True)
|
|
||||||
|
|
||||||
for i in range(10):
|
|
||||||
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
|
||||||
M_inv = Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')
|
|
||||||
Device.default.synchronize()
|
|
||||||
st = time.perf_counter()
|
|
||||||
warp_dm_jit(frame, M_inv).realize()
|
|
||||||
mt = time.perf_counter()
|
|
||||||
Device.default.synchronize()
|
|
||||||
et = time.perf_counter()
|
|
||||||
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
|
||||||
|
|
||||||
with open(pkl_path, "wb") as f:
|
|
||||||
pickle.dump(warp_dm_jit, f)
|
|
||||||
print(f" Saved to {pkl_path}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
p = argparse.ArgumentParser()
|
|
||||||
p.add_argument('--nv12', type=_parse_nv12, required=True,
|
|
||||||
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
|
|
||||||
p.add_argument('--warp-to', type=_parse_size, required=True, help='DM input WxH')
|
|
||||||
p.add_argument('--output', required=True)
|
|
||||||
args = p.parse_args()
|
|
||||||
|
|
||||||
dm_w, dm_h = args.warp_to
|
|
||||||
compile_dm_warp(args.nv12, dm_w, dm_h, args.output)
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import pickle
|
|
||||||
import time
|
|
||||||
from functools import partial
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from tinygrad.tensor import Tensor
|
|
||||||
from tinygrad.helpers import Context
|
|
||||||
from tinygrad.device import Device
|
|
||||||
from tinygrad.engine.jit import TinyJit
|
|
||||||
from tinygrad.nn.onnx import OnnxRunner
|
|
||||||
|
|
||||||
# https://github.com/tinygrad/tinygrad/issues/15682
|
|
||||||
from tinygrad.uop.ops import UOp, Ops
|
|
||||||
_orig = UOp.__reduce__
|
|
||||||
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
|
|
||||||
|
|
||||||
|
|
||||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
|
|
||||||
w_dst, h_dst = dst_shape
|
|
||||||
h_src, w_src = src_shape
|
|
||||||
|
|
||||||
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
|
||||||
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
|
||||||
|
|
||||||
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
|
||||||
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
|
||||||
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
|
||||||
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
|
||||||
|
|
||||||
src_x = src_x / src_w
|
|
||||||
src_y = src_y / src_w
|
|
||||||
|
|
||||||
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
|
|
||||||
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
|
|
||||||
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
|
||||||
|
|
||||||
return src_flat[idx]
|
|
||||||
|
|
||||||
|
|
||||||
def frames_to_tensor(frames):
|
|
||||||
H = (frames.shape[0] * 2) // 3
|
|
||||||
W = frames.shape[1]
|
|
||||||
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
|
||||||
frames[1:H:2, 0::2],
|
|
||||||
frames[0:H:2, 1::2],
|
|
||||||
frames[1:H:2, 1::2],
|
|
||||||
frames[H:H+H//4].reshape((H//2, W//2)),
|
|
||||||
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
|
||||||
return in_img1
|
|
||||||
|
|
||||||
|
|
||||||
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
|
||||||
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
|
|
||||||
uv_offset = stride * y_height
|
|
||||||
stride_pad = stride - cam_w
|
|
||||||
|
|
||||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
|
||||||
# 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]])
|
|
||||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
|
||||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
|
||||||
with Context(SPLIT_REDUCEOP=0):
|
|
||||||
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
|
|
||||||
M_inv, (model_w, model_h),
|
|
||||||
(cam_h, cam_w), stride_pad).realize()
|
|
||||||
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
|
||||||
M_inv_uv, (model_w//2, model_h//2),
|
|
||||||
(cam_h//2, cam_w//2), 0).realize()
|
|
||||||
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
|
|
||||||
M_inv_uv, (model_w//2, model_h//2),
|
|
||||||
(cam_h//2, cam_w//2), 0).realize()
|
|
||||||
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
|
|
||||||
tensor = frames_to_tensor(yuv)
|
|
||||||
return tensor
|
|
||||||
return frame_prepare_tinygrad
|
|
||||||
|
|
||||||
|
|
||||||
def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip):
|
|
||||||
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])
|
|
||||||
|
|
||||||
fb = policy_input_shapes['features_buffer'] # (1, 25, 512)
|
|
||||||
dp = policy_input_shapes['desire_pulse'] # (1, 25, 8)
|
|
||||||
tc = policy_input_shapes['traffic_convention'] # (1, 2)
|
|
||||||
|
|
||||||
npy = {
|
|
||||||
'desire': np.zeros(dp[2], dtype=np.float32),
|
|
||||||
'traffic_convention': np.zeros(tc, dtype=np.float32),
|
|
||||||
'tfm': np.zeros((3, 3), dtype=np.float32),
|
|
||||||
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
|
||||||
}
|
|
||||||
input_queues = {
|
|
||||||
'img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
|
|
||||||
'big_img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
|
|
||||||
'feat_q': Tensor.zeros(frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]).contiguous().realize(),
|
|
||||||
'desire_q': Tensor.zeros(frame_skip * dp[1], dp[0], dp[2]).contiguous().realize(),
|
|
||||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
|
||||||
}
|
|
||||||
return input_queues, npy
|
|
||||||
|
|
||||||
|
|
||||||
def shift_and_sample(buf, new_val, sample_fn):
|
|
||||||
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
|
|
||||||
return sample_fn(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def sample_skip(buf, frame_skip):
|
|
||||||
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
|
|
||||||
|
|
||||||
|
|
||||||
def sample_desire(buf, frame_skip):
|
|
||||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
|
||||||
|
|
||||||
|
|
||||||
def make_run_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
|
|
||||||
vision_features_slice, frame_skip, prepare_only=False):
|
|
||||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
|
||||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
|
||||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
|
||||||
|
|
||||||
def run_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame):
|
|
||||||
tfm = tfm.to(Device.DEFAULT)
|
|
||||||
big_tfm = big_tfm.to(Device.DEFAULT)
|
|
||||||
desire = desire.to(Device.DEFAULT)
|
|
||||||
traffic_convention = traffic_convention.to(Device.DEFAULT)
|
|
||||||
Tensor.realize(tfm, big_tfm, desire, traffic_convention)
|
|
||||||
|
|
||||||
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
|
|
||||||
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
|
|
||||||
|
|
||||||
if prepare_only:
|
|
||||||
return img, big_img
|
|
||||||
|
|
||||||
vision_out = next(iter(vision_runner({'img': img, 'big_img': big_img}).values())).cast('float32')
|
|
||||||
|
|
||||||
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
|
|
||||||
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
|
|
||||||
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
|
|
||||||
|
|
||||||
inputs = {'features_buffer': feat_buf, 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention}
|
|
||||||
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
|
|
||||||
|
|
||||||
return vision_out, policy_out
|
|
||||||
return run_policy
|
|
||||||
|
|
||||||
|
|
||||||
def compile_modeld(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
|
|
||||||
vision_onnx, policy_onnx, pkl_path):
|
|
||||||
from get_model_metadata import metadata_path_for
|
|
||||||
|
|
||||||
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
|
|
||||||
|
|
||||||
vision_runner = OnnxRunner(vision_onnx)
|
|
||||||
policy_runner = OnnxRunner(policy_onnx)
|
|
||||||
|
|
||||||
with open(metadata_path_for(vision_onnx), 'rb') as f:
|
|
||||||
vision_metadata = pickle.load(f)
|
|
||||||
vision_features_slice = vision_metadata['output_slices']['hidden_state']
|
|
||||||
vision_input_shapes = vision_metadata['input_shapes']
|
|
||||||
with open(metadata_path_for(policy_onnx), 'rb') as f:
|
|
||||||
policy_input_shapes = pickle.load(f)['input_shapes']
|
|
||||||
|
|
||||||
_run = make_run_policy(vision_runner, policy_runner, nv12, model_w, model_h,
|
|
||||||
vision_features_slice, frame_skip, prepare_only)
|
|
||||||
run_policy_jit = TinyJit(_run, prune=True)
|
|
||||||
|
|
||||||
N_RUNS = 3
|
|
||||||
SEED = 42
|
|
||||||
|
|
||||||
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
|
||||||
input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip)
|
|
||||||
np.random.seed(seed)
|
|
||||||
Tensor.manual_seed(seed)
|
|
||||||
|
|
||||||
for i in range(N_RUNS):
|
|
||||||
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
|
||||||
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
|
|
||||||
for v in npy.values():
|
|
||||||
v[:] = np.random.randn(*v.shape).astype(v.dtype)
|
|
||||||
Device.default.synchronize()
|
|
||||||
st = time.perf_counter()
|
|
||||||
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
|
|
||||||
mt = time.perf_counter()
|
|
||||||
for o in outs:
|
|
||||||
# .realize() not needed once jitted, but needed for unjitted fn
|
|
||||||
o.realize()
|
|
||||||
Device.default.synchronize()
|
|
||||||
et = time.perf_counter()
|
|
||||||
print(f" [{i+1}/{N_RUNS}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
|
||||||
|
|
||||||
val = [np.copy(v.numpy()) for v in outs]
|
|
||||||
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
|
|
||||||
|
|
||||||
if test_val is not None:
|
|
||||||
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
|
|
||||||
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
|
||||||
if test_buffers is not None:
|
|
||||||
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
|
|
||||||
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
|
|
||||||
return fn, val, buffers
|
|
||||||
|
|
||||||
print('run unjitted')
|
|
||||||
_, test_val, test_buffers = random_inputs_run_fn(_run, seed=SEED)
|
|
||||||
print('capture + replay')
|
|
||||||
run_policy_jit, _, _ = random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers)
|
|
||||||
|
|
||||||
print('pickle round trip')
|
|
||||||
with open(pkl_path, "wb") as f:
|
|
||||||
pickle.dump(run_policy_jit, f)
|
|
||||||
print(f" Saved to {pkl_path}")
|
|
||||||
with open(pkl_path, "rb") as f:
|
|
||||||
run_policy_jit = pickle.load(f)
|
|
||||||
random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers, expect_match=True)
|
|
||||||
random_inputs_run_fn(run_policy_jit, SEED+1, test_val, test_buffers, expect_match=False)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_size(s):
|
|
||||||
w, h = s.lower().split('x')
|
|
||||||
return int(w), int(h)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_nv12(s):
|
|
||||||
parts = s.split(',')
|
|
||||||
assert len(parts) == len(NV12Frame._fields), \
|
|
||||||
f"--nv12 expects {','.join(NV12Frame._fields)} (got {s!r})"
|
|
||||||
return NV12Frame(*(int(x) for x in parts))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
p = argparse.ArgumentParser()
|
|
||||||
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
|
|
||||||
p.add_argument('--nv12', type=_parse_nv12, required=True,
|
|
||||||
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
|
|
||||||
p.add_argument('--vision-onnx', required=True)
|
|
||||||
p.add_argument('--policy-onnx', required=True)
|
|
||||||
p.add_argument('--output', required=True)
|
|
||||||
p.add_argument('--prepare-only', action='store_true')
|
|
||||||
p.add_argument('--frame-skip', type=int, required=True)
|
|
||||||
args = p.parse_args()
|
|
||||||
|
|
||||||
model_w, model_h = args.model_size
|
|
||||||
compile_modeld(args.nv12, model_w, model_h, args.prepare_only, args.frame_skip,
|
|
||||||
args.vision_onnx, args.policy_onnx, args.output)
|
|
||||||
Executable
+201
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
import pickle
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
|
from tinygrad.helpers import Context
|
||||||
|
from tinygrad.device import Device
|
||||||
|
from tinygrad.engine.jit import TinyJit
|
||||||
|
|
||||||
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
|
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
||||||
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
|
|
||||||
|
MODELS_DIR = Path(__file__).parent / 'models'
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||||
|
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||||
|
]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2)
|
||||||
|
|
||||||
|
|
||||||
|
def warp_pkl_path(w, h):
|
||||||
|
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
|
def dm_warp_pkl_path(w, h):
|
||||||
|
return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
|
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
|
||||||
|
w_dst, h_dst = dst_shape
|
||||||
|
h_src, w_src = src_shape
|
||||||
|
|
||||||
|
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
|
||||||
|
|
||||||
|
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
|
||||||
|
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
|
||||||
|
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
|
||||||
|
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
|
||||||
|
|
||||||
|
src_x = src_x / src_w
|
||||||
|
src_y = src_y / src_w
|
||||||
|
|
||||||
|
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
|
||||||
|
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
|
||||||
|
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
|
||||||
|
|
||||||
|
return src_flat[idx]
|
||||||
|
|
||||||
|
|
||||||
|
def frames_to_tensor(frames, model_w, model_h):
|
||||||
|
H = (frames.shape[0] * 2) // 3
|
||||||
|
W = frames.shape[1]
|
||||||
|
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
|
||||||
|
frames[1:H:2, 0::2],
|
||||||
|
frames[0:H:2, 1::2],
|
||||||
|
frames[1:H:2, 1::2],
|
||||||
|
frames[H:H+H//4].reshape((H//2, W//2)),
|
||||||
|
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
|
||||||
|
return in_img1
|
||||||
|
|
||||||
|
|
||||||
|
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
|
||||||
|
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
|
||||||
|
uv_offset = stride * y_height
|
||||||
|
stride_pad = stride - cam_w
|
||||||
|
|
||||||
|
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||||
|
# 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]])
|
||||||
|
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||||
|
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||||
|
with Context(SPLIT_REDUCEOP=0):
|
||||||
|
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
|
||||||
|
M_inv, (model_w, model_h),
|
||||||
|
(cam_h, cam_w), stride_pad).realize()
|
||||||
|
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
|
||||||
|
M_inv_uv, (model_w//2, model_h//2),
|
||||||
|
(cam_h//2, cam_w//2), 0).realize()
|
||||||
|
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
|
||||||
|
M_inv_uv, (model_w//2, model_h//2),
|
||||||
|
(cam_h//2, cam_w//2), 0).realize()
|
||||||
|
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
|
||||||
|
tensor = frames_to_tensor(yuv, model_w, model_h)
|
||||||
|
return tensor
|
||||||
|
return frame_prepare_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_update_img_input(frame_prepare, model_w, model_h):
|
||||||
|
def update_img_input_tinygrad(tensor, frame, M_inv):
|
||||||
|
M_inv = M_inv.to(Device.DEFAULT)
|
||||||
|
new_img = frame_prepare(frame, M_inv)
|
||||||
|
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
||||||
|
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
||||||
|
return update_img_input_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_update_both_imgs(frame_prepare, model_w, model_h):
|
||||||
|
update_img = make_update_img_input(frame_prepare, model_w, model_h)
|
||||||
|
|
||||||
|
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
||||||
|
calib_big_img_buffer, new_big_img, M_inv_big):
|
||||||
|
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
||||||
|
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
||||||
|
return calib_img_pair, calib_big_img_pair
|
||||||
|
return update_both_imgs_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_warp_dm(cam_w, cam_h, dm_w, dm_h):
|
||||||
|
stride, y_height, _, _ = get_nv12_info(cam_w, cam_h)
|
||||||
|
stride_pad = stride - cam_w
|
||||||
|
|
||||||
|
def warp_dm(input_frame, M_inv):
|
||||||
|
M_inv = M_inv.to(Device.DEFAULT)
|
||||||
|
result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
|
||||||
|
return result
|
||||||
|
return warp_dm
|
||||||
|
|
||||||
|
|
||||||
|
def compile_modeld_warp(cam_w, cam_h):
|
||||||
|
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||||
|
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
||||||
|
|
||||||
|
print(f"Compiling modeld warp for {cam_w}x{cam_h}...")
|
||||||
|
|
||||||
|
frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h)
|
||||||
|
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
|
||||||
|
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
|
full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
||||||
|
big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
||||||
|
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
for i in range(10):
|
||||||
|
img_inputs = [full_buffer,
|
||||||
|
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||||
|
big_img_inputs = [big_full_buffer,
|
||||||
|
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||||
|
inputs = img_inputs + big_img_inputs
|
||||||
|
Device.default.synchronize()
|
||||||
|
|
||||||
|
st = time.perf_counter()
|
||||||
|
_ = update_img_jit(*inputs)
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
|
pkl_path = warp_pkl_path(cam_w, cam_h)
|
||||||
|
with open(pkl_path, "wb") as f:
|
||||||
|
pickle.dump(update_img_jit, f)
|
||||||
|
print(f" Saved to {pkl_path}")
|
||||||
|
|
||||||
|
jit = pickle.load(open(pkl_path, "rb"))
|
||||||
|
jit(*inputs)
|
||||||
|
|
||||||
|
|
||||||
|
def compile_dm_warp(cam_w, cam_h):
|
||||||
|
dm_w, dm_h = DM_INPUT_SIZE
|
||||||
|
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
||||||
|
|
||||||
|
print(f"Compiling DM warp for {cam_w}x{cam_h}...")
|
||||||
|
|
||||||
|
warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h)
|
||||||
|
warp_dm_jit = TinyJit(warp_dm, prune=True)
|
||||||
|
|
||||||
|
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||||
|
for i in range(10):
|
||||||
|
inputs = [Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||||
|
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||||
|
Device.default.synchronize()
|
||||||
|
st = time.perf_counter()
|
||||||
|
warp_dm_jit(*inputs)
|
||||||
|
mt = time.perf_counter()
|
||||||
|
Device.default.synchronize()
|
||||||
|
et = time.perf_counter()
|
||||||
|
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
|
||||||
|
|
||||||
|
pkl_path = dm_warp_pkl_path(cam_w, cam_h)
|
||||||
|
with open(pkl_path, "wb") as f:
|
||||||
|
pickle.dump(warp_dm_jit, f)
|
||||||
|
print(f" Saved to {pkl_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_and_save_pickle():
|
||||||
|
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||||
|
compile_modeld_warp(cam_w, cam_h)
|
||||||
|
compile_dm_warp(cam_w, cam_h)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_and_save_pickle()
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
|
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
||||||
set_tinygrad_backend_from_compiled_flags()
|
set_tinygrad_backend_from_compiled_flags()
|
||||||
|
|
||||||
|
# FIXME-SP: remove once we bump tg
|
||||||
|
from openpilot.system.hardware import TICI
|
||||||
|
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||||
|
|
||||||
from tinygrad.tensor import Tensor
|
from tinygrad.tensor import Tensor
|
||||||
import time
|
import time
|
||||||
import pickle
|
import pickle
|
||||||
@@ -28,7 +32,7 @@ class ModelState:
|
|||||||
inputs: dict[str, np.ndarray]
|
inputs: dict[str, np.ndarray]
|
||||||
output: np.ndarray
|
output: np.ndarray
|
||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int):
|
def __init__(self):
|
||||||
with open(METADATA_PATH, 'rb') as f:
|
with open(METADATA_PATH, 'rb') as f:
|
||||||
model_metadata = pickle.load(f)
|
model_metadata = pickle.load(f)
|
||||||
self.input_shapes = model_metadata['input_shapes']
|
self.input_shapes = model_metadata['input_shapes']
|
||||||
@@ -40,18 +44,22 @@ class ModelState:
|
|||||||
|
|
||||||
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
|
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
|
||||||
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
|
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
|
||||||
self.frame_buf_params = get_nv12_info(cam_w, cam_h)
|
self.frame_buf_params = None
|
||||||
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||||
self._blob_cache : dict[int, Tensor] = {}
|
self._blob_cache : dict[int, Tensor] = {}
|
||||||
|
self.image_warp = None
|
||||||
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
|
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
|
||||||
with open(CompileConfig(cam_w, cam_h, prefix='dm_', prepare_only=True).pkl_path, "rb") as f:
|
|
||||||
self.image_warp = pickle.load(f)
|
|
||||||
|
|
||||||
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
|
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
|
||||||
self.numpy_inputs['calib'][0,:] = calib
|
self.numpy_inputs['calib'][0,:] = calib
|
||||||
|
|
||||||
t1 = time.perf_counter()
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
if self.image_warp is None:
|
||||||
|
self.frame_buf_params = get_nv12_info(buf.width, buf.height)
|
||||||
|
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
|
||||||
|
with open(warp_path, "rb") as f:
|
||||||
|
self.image_warp = pickle.load(f)
|
||||||
ptr = buf.data.ctypes.data
|
ptr = buf.data.ctypes.data
|
||||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||||
if ptr not in self._blob_cache:
|
if ptr not in self._blob_cache:
|
||||||
@@ -75,7 +83,7 @@ def parse_model_output(model_output):
|
|||||||
face_descs = model_output[f'face_descs_{ds_suffix}']
|
face_descs = model_output[f'face_descs_{ds_suffix}']
|
||||||
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
||||||
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
||||||
for key in ['face_prob', 'eyes_visible_prob', 'eyes_closed_prob', 'using_phone_prob']:
|
for key in ['face_prob', 'left_eye_prob', 'right_eye_prob','left_blink_prob', 'right_blink_prob', 'sunglasses_prob', 'using_phone_prob']:
|
||||||
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
@@ -85,8 +93,11 @@ def fill_driver_data(msg, model_output, ds_suffix):
|
|||||||
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
||||||
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
||||||
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.eyesVisibleProb = model_output[f'eyes_visible_prob_{ds_suffix}'][0, 0].item()
|
msg.leftEyeProb = model_output[f'left_eye_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.eyesClosedProb = model_output[f'eyes_closed_prob_{ds_suffix}'][0, 0].item()
|
msg.rightEyeProb = model_output[f'right_eye_prob_{ds_suffix}'][0, 0].item()
|
||||||
|
msg.leftBlinkProb = model_output[f'left_blink_prob_{ds_suffix}'][0, 0].item()
|
||||||
|
msg.rightBlinkProb = model_output[f'right_blink_prob_{ds_suffix}'][0, 0].item()
|
||||||
|
msg.sunglassesProb = model_output[f'sunglasses_prob_{ds_suffix}'][0, 0].item()
|
||||||
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
||||||
|
|
||||||
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
||||||
@@ -105,6 +116,9 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t
|
|||||||
def main():
|
def main():
|
||||||
config_realtime_process(7, 5)
|
config_realtime_process(7, 5)
|
||||||
|
|
||||||
|
model = ModelState()
|
||||||
|
cloudlog.warning("models loaded, dmonitoringmodeld starting")
|
||||||
|
|
||||||
cloudlog.warning("connecting to driver stream")
|
cloudlog.warning("connecting to driver stream")
|
||||||
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
|
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
|
||||||
while not vipc_client.connect(False):
|
while not vipc_client.connect(False):
|
||||||
@@ -112,9 +126,6 @@ def main():
|
|||||||
assert vipc_client.is_connected()
|
assert vipc_client.is_connected()
|
||||||
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
|
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
|
||||||
|
|
||||||
model = ModelState(vipc_client.width, vipc_client.height)
|
|
||||||
cloudlog.warning("models loaded, dmonitoringmodeld starting")
|
|
||||||
|
|
||||||
sm = SubMaster(["liveCalibration"])
|
sm = SubMaster(["liveCalibration"])
|
||||||
pm = PubMaster(["driverStateV2"])
|
pm = PubMaster(["driverStateV2"])
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ from typing import Any
|
|||||||
|
|
||||||
from tinygrad.nn.onnx import OnnxPBParser
|
from tinygrad.nn.onnx import OnnxPBParser
|
||||||
|
|
||||||
def metadata_path_for(onnx_path) -> pathlib.Path:
|
|
||||||
p = pathlib.Path(onnx_path)
|
|
||||||
return p.parent / (p.stem + '_metadata.pkl')
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataOnnxPBParser(OnnxPBParser):
|
class MetadataOnnxPBParser(OnnxPBParser):
|
||||||
def _parse_ModelProto(self) -> dict:
|
def _parse_ModelProto(self) -> dict:
|
||||||
@@ -52,7 +48,7 @@ if __name__ == "__main__":
|
|||||||
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
|
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata_path = metadata_path_for(model_path)
|
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
|
||||||
with open(metadata_path, 'wb') as f:
|
with open(metadata_path, 'wb') as f:
|
||||||
pickle.dump(metadata, f)
|
pickle.dump(metadata, f)
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
|
||||||
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
|
|
||||||
|
|
||||||
|
|
||||||
def set_tinygrad_backend_from_compiled_flags() -> None:
|
|
||||||
if os.path.isfile(COMPILED_FLAGS_PATH):
|
|
||||||
with open(COMPILED_FLAGS_PATH) as f:
|
|
||||||
os.environ['DEV'] = str(json.load(f)['DEV'])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CompileConfig:
|
|
||||||
cam_w: int
|
|
||||||
cam_h: int
|
|
||||||
prepare_only: bool
|
|
||||||
prefix: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pkl_path(self):
|
|
||||||
return str(MODELS_DIR / f'{self.prefix}{"warp_" if self.prepare_only else ""}{self.cam_w}x{self.cam_h}_tinygrad.pkl')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nv12(self):
|
|
||||||
return (self.cam_w, self.cam_h, *get_nv12_info(self.cam_w, self.cam_h))
|
|
||||||
+122
-35
@@ -1,8 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
|
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
||||||
set_tinygrad_backend_from_compiled_flags()
|
set_tinygrad_backend_from_compiled_flags()
|
||||||
|
|
||||||
|
# FIXME-SP: remove once we bump tg
|
||||||
|
from openpilot.system.hardware import TICI
|
||||||
|
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||||
|
|
||||||
USBGPU = "USBGPU" in os.environ
|
USBGPU = "USBGPU" in os.environ
|
||||||
if USBGPU:
|
if USBGPU:
|
||||||
os.environ['DEV'] = 'AMD'
|
os.environ['DEV'] = 'AMD'
|
||||||
@@ -26,7 +30,6 @@ 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, smooth_value, get_curvature_from_plan
|
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, 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
|
|
||||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||||
from openpilot.common.file_chunker import read_file_chunked
|
from openpilot.common.file_chunker import read_file_chunked
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
@@ -38,13 +41,17 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
|||||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
|
|
||||||
|
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
|
||||||
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
||||||
|
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
|
||||||
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
||||||
|
|
||||||
LAT_SMOOTH_SECONDS = 0.0
|
LAT_SMOOTH_SECONDS = 0.0
|
||||||
LONG_SMOOTH_SECONDS = 0.3
|
LONG_SMOOTH_SECONDS = 0.3
|
||||||
MIN_LAT_CONTROL_SPEED = 0.3
|
MIN_LAT_CONTROL_SPEED = 0.3
|
||||||
|
|
||||||
|
IMG_QUEUE_SHAPE = (6*(ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ + 1), 128, 256)
|
||||||
|
assert IMG_QUEUE_SHAPE[0] == 30
|
||||||
|
|
||||||
|
|
||||||
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||||
@@ -79,39 +86,108 @@ class FrameMeta:
|
|||||||
if vipc is not None:
|
if vipc is not None:
|
||||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||||
|
|
||||||
|
class InputQueues:
|
||||||
|
def __init__ (self, model_fps, env_fps, n_frames_input):
|
||||||
|
assert env_fps % model_fps == 0
|
||||||
|
assert env_fps >= model_fps
|
||||||
|
self.model_fps = model_fps
|
||||||
|
self.env_fps = env_fps
|
||||||
|
self.n_frames_input = n_frames_input
|
||||||
|
|
||||||
|
self.dtypes = {}
|
||||||
|
self.shapes = {}
|
||||||
|
self.q = {}
|
||||||
|
|
||||||
|
def update_dtypes_and_shapes(self, input_dtypes, input_shapes) -> None:
|
||||||
|
self.dtypes.update(input_dtypes)
|
||||||
|
if self.env_fps == self.model_fps:
|
||||||
|
self.shapes.update(input_shapes)
|
||||||
|
else:
|
||||||
|
for k in input_shapes:
|
||||||
|
shape = list(input_shapes[k])
|
||||||
|
if 'img' in k:
|
||||||
|
n_channels = shape[1] // self.n_frames_input
|
||||||
|
shape[1] = (self.env_fps // self.model_fps + (self.n_frames_input - 1)) * n_channels
|
||||||
|
else:
|
||||||
|
shape[1] = (self.env_fps // self.model_fps) * shape[1]
|
||||||
|
self.shapes[k] = tuple(shape)
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.q = {k: np.zeros(self.shapes[k], dtype=self.dtypes[k]) for k in self.dtypes.keys()}
|
||||||
|
|
||||||
|
def enqueue(self, inputs:dict[str, np.ndarray]) -> None:
|
||||||
|
for k in inputs.keys():
|
||||||
|
if inputs[k].dtype != self.dtypes[k]:
|
||||||
|
raise ValueError(f'supplied input <{k}({inputs[k].dtype})> has wrong dtype, expected {self.dtypes[k]}')
|
||||||
|
input_shape = list(self.shapes[k])
|
||||||
|
input_shape[1] = -1
|
||||||
|
single_input = inputs[k].reshape(tuple(input_shape))
|
||||||
|
sz = single_input.shape[1]
|
||||||
|
self.q[k][:,:-sz] = self.q[k][:,sz:]
|
||||||
|
self.q[k][:,-sz:] = single_input
|
||||||
|
|
||||||
|
def get(self, *names) -> dict[str, np.ndarray]:
|
||||||
|
if self.env_fps == self.model_fps:
|
||||||
|
return {k: self.q[k] for k in names}
|
||||||
|
else:
|
||||||
|
out = {}
|
||||||
|
for k in names:
|
||||||
|
shape = self.shapes[k]
|
||||||
|
if 'img' in k:
|
||||||
|
n_channels = shape[1] // (self.env_fps // self.model_fps + (self.n_frames_input - 1))
|
||||||
|
out[k] = np.concatenate([self.q[k][:, s:s+n_channels] for s in np.linspace(0, shape[1] - n_channels, self.n_frames_input, dtype=int)], axis=1)
|
||||||
|
elif 'pulse' in k:
|
||||||
|
# any pulse within interval counts
|
||||||
|
out[k] = self.q[k].reshape((shape[0], shape[1] * self.model_fps // self.env_fps, self.env_fps // self.model_fps, -1)).max(axis=2)
|
||||||
|
else:
|
||||||
|
idxs = np.arange(-1, -shape[1], -self.env_fps // self.model_fps)[::-1]
|
||||||
|
out[k] = self.q[k][:, idxs]
|
||||||
|
return out
|
||||||
|
|
||||||
class ModelState(ModelStateBase):
|
class ModelState(ModelStateBase):
|
||||||
|
inputs: dict[str, np.ndarray]
|
||||||
|
output: np.ndarray
|
||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int):
|
def __init__(self):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
||||||
|
|
||||||
with open(VISION_METADATA_PATH, 'rb') as f:
|
with open(VISION_METADATA_PATH, 'rb') as f:
|
||||||
vision_metadata = pickle.load(f)
|
vision_metadata = pickle.load(f)
|
||||||
self.vision_input_shapes = vision_metadata['input_shapes']
|
self.vision_input_shapes = vision_metadata['input_shapes']
|
||||||
self.vision_input_names = list(self.vision_input_shapes.keys())
|
self.vision_input_names = list(self.vision_input_shapes.keys())
|
||||||
self.vision_output_slices = vision_metadata['output_slices']
|
self.vision_output_slices = vision_metadata['output_slices']
|
||||||
|
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||||
|
|
||||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||||
policy_metadata = pickle.load(f)
|
policy_metadata = pickle.load(f)
|
||||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||||
self.policy_output_slices = policy_metadata['output_slices']
|
self.policy_output_slices = policy_metadata['output_slices']
|
||||||
|
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
|
||||||
|
|
||||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||||
|
|
||||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
# policy inputs
|
||||||
self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip)
|
self.numpy_inputs = {k: np.zeros(self.policy_input_shapes[k], dtype=np.float32) for k in self.policy_input_shapes}
|
||||||
|
self.full_input_queues = InputQueues(ModelConstants.MODEL_CONTEXT_FREQ, ModelConstants.MODEL_RUN_FREQ, ModelConstants.N_FRAMES)
|
||||||
|
for k in ['desire_pulse', 'features_buffer']:
|
||||||
|
self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape})
|
||||||
|
self.full_input_queues.reset()
|
||||||
|
|
||||||
|
self.img_queues = {'img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize(),
|
||||||
|
'big_img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize()}
|
||||||
self.full_frames : dict[str, Tensor] = {}
|
self.full_frames : dict[str, Tensor] = {}
|
||||||
self._blob_cache : dict[int, Tensor] = {}
|
self._blob_cache : dict[int, Tensor] = {}
|
||||||
|
self.transforms_np = {k: np.zeros((3,3), dtype=np.float32) for k in self.img_queues}
|
||||||
|
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
|
||||||
|
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||||
|
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||||
|
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
|
||||||
self.run_policy = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=False).pkl_path))
|
self.update_imgs = None
|
||||||
self.warp_enqueue = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=True).pkl_path))
|
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
|
||||||
self.warp_enqueue(
|
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
|
||||||
**self.input_queues,
|
|
||||||
frame=Tensor.zeros(self.frame_buf_params['img'][3], dtype='uint8').contiguous().realize(),
|
|
||||||
big_frame=Tensor.zeros(self.frame_buf_params['big_img'][3], dtype='uint8').contiguous().realize())
|
|
||||||
|
|
||||||
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()}
|
||||||
@@ -119,6 +195,18 @@ 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], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||||
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
|
inputs['desire_pulse'][0] = 0
|
||||||
|
new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
|
||||||
|
self.prev_desire[:] = inputs['desire_pulse']
|
||||||
|
if self.update_imgs is None:
|
||||||
|
for key in bufs.keys():
|
||||||
|
w, h = bufs[key].width, bufs[key].height
|
||||||
|
self.frame_buf_params[key] = get_nv12_info(w, h)
|
||||||
|
warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
|
||||||
|
with open(warp_path, "rb") as f:
|
||||||
|
self.update_imgs = pickle.load(f)
|
||||||
|
|
||||||
for key in bufs.keys():
|
for key in bufs.keys():
|
||||||
ptr = bufs[key].data.ctypes.data
|
ptr = bufs[key].data.ctypes.data
|
||||||
yuv_size = self.frame_buf_params[key][3]
|
yuv_size = self.frame_buf_params[key][3]
|
||||||
@@ -127,31 +215,30 @@ class ModelState(ModelStateBase):
|
|||||||
if cache_key not in self._blob_cache:
|
if cache_key not in self._blob_cache:
|
||||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
|
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
|
||||||
self.full_frames[key] = self._blob_cache[cache_key]
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
|
for key in bufs.keys():
|
||||||
|
self.transforms_np[key][:,:] = transforms[key][:,:]
|
||||||
|
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
out = self.update_imgs(self.img_queues['img'], self.full_frames['img'], self.transforms['img'],
|
||||||
inputs['desire_pulse'][0] = 0
|
self.img_queues['big_img'], self.full_frames['big_img'], self.transforms['big_img'])
|
||||||
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
|
vision_inputs = {'img': out[0], 'big_img': out[1]}
|
||||||
self.prev_desire[:] = inputs['desire_pulse']
|
|
||||||
self.npy['traffic_convention'][:] = inputs['traffic_convention']
|
|
||||||
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
|
||||||
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
|
||||||
|
|
||||||
if prepare_only:
|
if prepare_only:
|
||||||
self.warp_enqueue(**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
vision_output, policy_output = self.run_policy(
|
self.vision_output = self.vision_run(**vision_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
||||||
**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']
|
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices))
|
||||||
)
|
|
||||||
|
|
||||||
vision_output = vision_output.numpy().flatten()
|
self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire})
|
||||||
policy_output = policy_output.numpy().flatten()
|
for k in ['desire_pulse', 'features_buffer']:
|
||||||
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices))
|
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
|
||||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(policy_output, self.policy_output_slices))
|
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
|
||||||
|
|
||||||
|
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
||||||
|
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||||
|
|
||||||
if SEND_RAW_PRED:
|
if SEND_RAW_PRED:
|
||||||
combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), policy_output.copy()])
|
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
||||||
|
|
||||||
return combined_outputs_dict
|
return combined_outputs_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -163,6 +250,11 @@ def main(demo=False):
|
|||||||
# also need to move the aux USB interrupts for good timings
|
# also need to move the aux USB interrupts for good timings
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
|
|
||||||
|
st = time.monotonic()
|
||||||
|
cloudlog.warning("loading model")
|
||||||
|
model = ModelState()
|
||||||
|
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||||
|
|
||||||
# visionipc clients
|
# visionipc clients
|
||||||
while True:
|
while True:
|
||||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||||
@@ -186,11 +278,6 @@ def main(demo=False):
|
|||||||
if use_extra_client:
|
if use_extra_client:
|
||||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||||
|
|
||||||
st = time.monotonic()
|
|
||||||
cloudlog.warning("loading model")
|
|
||||||
model = ModelState(vipc_client_main.width, vipc_client_main.height)
|
|
||||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2fd471febb6e973313ac0d0c6755f6410c1937ba92230b58a433761e8c883072
|
oid sha256:7aff7ff1dc08bbaf562a8f77380ab5e5914f8557dba2f760d87e4d953f5604b0
|
||||||
size 7364290
|
size 7307246
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15
|
oid sha256:853c6634746ff439a848349d00e4d5581cd941f13f7c1862c31b72a31cc24858
|
||||||
size 14060847
|
size 14061595
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66
|
oid sha256:940e9006a25f27f0b6e85da798e6a8fd1f6dd492dd7d0b9ff1a9436460f46129
|
||||||
size 46877473
|
size 46887794
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
MODELS_DIR = Path(__file__).parent / 'models'
|
||||||
|
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
|
||||||
|
|
||||||
|
|
||||||
|
def set_tinygrad_backend_from_compiled_flags() -> None:
|
||||||
|
if os.path.isfile(COMPILED_FLAGS_PATH):
|
||||||
|
with open(COMPILED_FLAGS_PATH) as f:
|
||||||
|
os.environ['DEV'] = str(json.load(f)['DEV'])
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# driver monitoring (DM)
|
||||||
|
|
||||||
|
Uploading driver-facing camera footage is opt-in, but it is encouraged to opt-in to improve the DM model. You can always change your preference using the "Record and Upload Driver Camera" toggle.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Before creating a bug report, go through these troubleshooting steps.
|
||||||
|
|
||||||
|
* Ensure the driver-facing camera has a good view of the driver in normal driving positions.
|
||||||
|
* This can be checked in Settings -> Device -> Preview Driver Camera (when car is off).
|
||||||
|
* If the camera can't see the driver, the device should be re-mounted.
|
||||||
|
|
||||||
|
## Bug report
|
||||||
|
|
||||||
|
In order for us to look into DM bug reports, we'll need the driver-facing camera footage. If you don't normally have this enabled, simply enable the toggle for a single drive. Also ensure the "Upload Raw Logs" toggle is enabled before going for a drive.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import config_realtime_process
|
from openpilot.common.realtime import config_realtime_process
|
||||||
from openpilot.selfdrive.monitoring.policy import DriverMonitoring
|
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring
|
||||||
|
|
||||||
|
|
||||||
def dmonitoringd_thread():
|
def dmonitoringd_thread():
|
||||||
@@ -25,7 +25,7 @@ def dmonitoringd_thread():
|
|||||||
|
|
||||||
valid = sm.all_checks()
|
valid = sm.all_checks()
|
||||||
if demo_mode and sm.valid['driverStateV2']:
|
if demo_mode and sm.valid['driverStateV2']:
|
||||||
DM.run_step(sm, demo=True)
|
DM.run_step(sm, demo=demo_mode)
|
||||||
elif valid:
|
elif valid:
|
||||||
DM.run_step(sm, demo=demo_mode)
|
DM.run_step(sm, demo=demo_mode)
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ def dmonitoringd_thread():
|
|||||||
|
|
||||||
# save rhd virtual toggle every 5 mins
|
# save rhd virtual toggle every 5 mins
|
||||||
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
|
if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and
|
||||||
DM.wheelpos_offsetter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and
|
||||||
DM.wheel_on_right == (DM.wheelpos_offsetter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)):
|
||||||
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
|
params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -0,0 +1,472 @@
|
|||||||
|
from math import atan2, radians
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from cereal import car, log
|
||||||
|
import cereal.messaging as messaging
|
||||||
|
from openpilot.selfdrive.selfdrived.events import Events
|
||||||
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
|
from openpilot.common.realtime import DT_DMON
|
||||||
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.stat_live import RunningStatFilter
|
||||||
|
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
|
||||||
|
EventName = log.OnroadEvent.EventName
|
||||||
|
|
||||||
|
# ******************************************************************************************
|
||||||
|
# NOTE: To fork maintainers.
|
||||||
|
# Disabling or nerfing safety features will get you and your users banned from our servers.
|
||||||
|
# We recommend that you do not change these numbers from the defaults.
|
||||||
|
# ******************************************************************************************
|
||||||
|
|
||||||
|
class DRIVER_MONITOR_SETTINGS:
|
||||||
|
def __init__(self, device_type):
|
||||||
|
self._DT_DMON = DT_DMON
|
||||||
|
# ref (page15-16): https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2
|
||||||
|
self._AWARENESS_TIME = 30. # passive wheeltouch total timeout
|
||||||
|
self._AWARENESS_PRE_TIME_TILL_TERMINAL = 15.
|
||||||
|
self._AWARENESS_PROMPT_TIME_TILL_TERMINAL = 6.
|
||||||
|
self._DISTRACTED_TIME = 11. # active monitoring total timeout
|
||||||
|
self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8.
|
||||||
|
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
|
||||||
|
|
||||||
|
self._FACE_THRESHOLD = 0.7
|
||||||
|
self._EYE_THRESHOLD = 0.65
|
||||||
|
self._SG_THRESHOLD = 0.9
|
||||||
|
self._BLINK_THRESHOLD = 0.865
|
||||||
|
self._PHONE_THRESH = 0.5
|
||||||
|
|
||||||
|
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||||
|
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||||
|
self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD
|
||||||
|
self._POSE_YAW_THRESHOLD = 0.4020
|
||||||
|
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
|
||||||
|
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
|
||||||
|
self._POSE_YAW_MIN_STEER_DEG = 30
|
||||||
|
self._POSE_YAW_STEER_FACTOR = 0.15
|
||||||
|
self._POSE_YAW_STEER_MAX_OFFSET = 0.3927
|
||||||
|
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
|
||||||
|
self._PITCH_NATURAL_THRESHOLD = 0.449
|
||||||
|
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
|
||||||
|
self._PITCH_NATURAL_VAR = 3*0.01
|
||||||
|
self._YAW_NATURAL_VAR = 3*0.05
|
||||||
|
self._PITCH_MAX_OFFSET = 0.124
|
||||||
|
self._PITCH_MIN_OFFSET = -0.0881
|
||||||
|
self._YAW_MAX_OFFSET = 0.289
|
||||||
|
self._YAW_MIN_OFFSET = -0.0246
|
||||||
|
|
||||||
|
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
|
||||||
|
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / self._DT_DMON)
|
||||||
|
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / self._DT_DMON)
|
||||||
|
self._POSESTD_THRESHOLD = 0.3
|
||||||
|
self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s
|
||||||
|
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
|
||||||
|
|
||||||
|
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
|
||||||
|
self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative
|
||||||
|
self._POSE_OFFSET_MAX_COUNT = int(360 / self._DT_DMON) # stop deweighting new data after 6 min, aka "short term memory"
|
||||||
|
|
||||||
|
self._WHEELPOS_CALIB_MIN_SPEED = 11
|
||||||
|
self._WHEELPOS_THRESHOLD = 0.5
|
||||||
|
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side
|
||||||
|
self._WHEELPOS_DATA_AVG = 0.03
|
||||||
|
self._WHEELPOS_DATA_VAR = 3*5.5e-5
|
||||||
|
self._WHEELPOS_MAX_COUNT = -1
|
||||||
|
|
||||||
|
self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change
|
||||||
|
self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change
|
||||||
|
|
||||||
|
self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts
|
||||||
|
self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts
|
||||||
|
|
||||||
|
class DistractedType:
|
||||||
|
|
||||||
|
NOT_DISTRACTED = 0
|
||||||
|
DISTRACTED_POSE = 1 << 0
|
||||||
|
DISTRACTED_BLINK = 1 << 1
|
||||||
|
DISTRACTED_PHONE = 1 << 2
|
||||||
|
|
||||||
|
class DriverPose:
|
||||||
|
def __init__(self, settings):
|
||||||
|
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
|
||||||
|
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
|
||||||
|
self.yaw = 0.
|
||||||
|
self.pitch = 0.
|
||||||
|
self.roll = 0.
|
||||||
|
self.yaw_std = 0.
|
||||||
|
self.pitch_std = 0.
|
||||||
|
self.roll_std = 0.
|
||||||
|
self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||||
|
self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
||||||
|
self.calibrated = False
|
||||||
|
self.low_std = True
|
||||||
|
self.cfactor_pitch = 1.
|
||||||
|
self.cfactor_yaw = 1.
|
||||||
|
self.steer_yaw_offset = 0.
|
||||||
|
|
||||||
|
class DriverProb:
|
||||||
|
def __init__(self, raw_priors, max_trackable):
|
||||||
|
self.prob = 0.
|
||||||
|
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||||
|
self.prob_calibrated = False
|
||||||
|
|
||||||
|
class DriverBlink:
|
||||||
|
def __init__(self):
|
||||||
|
self.left = 0.
|
||||||
|
self.right = 0.
|
||||||
|
|
||||||
|
|
||||||
|
# model output refers to center of undistorted+leveled image
|
||||||
|
EFL = 598.0 # focal length in K
|
||||||
|
cam = DEVICE_CAMERAS[("tici", "ar0231")] # corrected image has same size as raw
|
||||||
|
W, H = (cam.dcam.width, cam.dcam.height) # corrected image has same size as raw
|
||||||
|
|
||||||
|
def face_orientation_from_net(angles_desc, pos_desc, rpy_calib):
|
||||||
|
# the output of these angles are in device frame
|
||||||
|
# so from driver's perspective, pitch is up and yaw is right
|
||||||
|
|
||||||
|
pitch_net, yaw_net, roll_net = angles_desc
|
||||||
|
|
||||||
|
face_pixel_position = ((pos_desc[0]+0.5)*W, (pos_desc[1]+0.5)*H)
|
||||||
|
yaw_focal_angle = atan2(face_pixel_position[0] - W//2, EFL)
|
||||||
|
pitch_focal_angle = atan2(face_pixel_position[1] - H//2, EFL)
|
||||||
|
|
||||||
|
pitch = pitch_net + pitch_focal_angle
|
||||||
|
yaw = -yaw_net + yaw_focal_angle
|
||||||
|
|
||||||
|
# no calib for roll
|
||||||
|
pitch -= rpy_calib[1]
|
||||||
|
yaw -= rpy_calib[2]
|
||||||
|
return roll_net, pitch, yaw
|
||||||
|
|
||||||
|
|
||||||
|
class DriverMonitoring:
|
||||||
|
def __init__(self, rhd_saved=False, settings=None, always_on=False):
|
||||||
|
# init policy settings
|
||||||
|
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
|
||||||
|
|
||||||
|
# init driver status
|
||||||
|
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||||
|
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||||
|
self.pose = DriverPose(settings=self.settings)
|
||||||
|
self.blink = DriverBlink()
|
||||||
|
self.phone_prob = 0.
|
||||||
|
|
||||||
|
self.always_on = always_on
|
||||||
|
self.distracted_types = []
|
||||||
|
self.driver_distracted = False
|
||||||
|
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON)
|
||||||
|
self.wheel_on_right = False
|
||||||
|
self.wheel_on_right_last = None
|
||||||
|
self.wheel_on_right_default = rhd_saved
|
||||||
|
self.face_detected = False
|
||||||
|
self.terminal_alert_cnt = 0
|
||||||
|
self.terminal_time = 0
|
||||||
|
self.step_change = 0.
|
||||||
|
self.active_monitoring_mode = True
|
||||||
|
self.is_model_uncertain = False
|
||||||
|
self.hi_stds = 0
|
||||||
|
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
|
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
|
self.dcam_uncertain_cnt = 0
|
||||||
|
self.dcam_uncertain_alerted = False # once per drive
|
||||||
|
self.dcam_reset_cnt = 0
|
||||||
|
|
||||||
|
self.params = Params()
|
||||||
|
self.too_distracted = self.params.get_bool("DriverTooDistracted")
|
||||||
|
|
||||||
|
self._reset_awareness()
|
||||||
|
self._set_timers(active_monitoring=True)
|
||||||
|
self._reset_events()
|
||||||
|
|
||||||
|
def _reset_awareness(self):
|
||||||
|
self.awareness = 1.
|
||||||
|
self.awareness_active = 1.
|
||||||
|
self.awareness_passive = 1.
|
||||||
|
|
||||||
|
def _reset_events(self):
|
||||||
|
self.current_events = Events()
|
||||||
|
|
||||||
|
def _set_timers(self, active_monitoring):
|
||||||
|
if self.active_monitoring_mode and self.awareness <= self.threshold_prompt:
|
||||||
|
if active_monitoring:
|
||||||
|
self.step_change = self.settings._DT_DMON / self.settings._DISTRACTED_TIME
|
||||||
|
else:
|
||||||
|
self.step_change = 0.
|
||||||
|
return # no exploit after orange alert
|
||||||
|
elif self.awareness <= 0.:
|
||||||
|
return
|
||||||
|
|
||||||
|
if active_monitoring:
|
||||||
|
# when falling back from passive mode to active mode, reset awareness to avoid false alert
|
||||||
|
if not self.active_monitoring_mode:
|
||||||
|
self.awareness_passive = self.awareness
|
||||||
|
self.awareness = self.awareness_active
|
||||||
|
|
||||||
|
self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
|
self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME
|
||||||
|
self.step_change = self.settings._DT_DMON / self.settings._DISTRACTED_TIME
|
||||||
|
self.active_monitoring_mode = True
|
||||||
|
else:
|
||||||
|
if self.active_monitoring_mode:
|
||||||
|
self.awareness_active = self.awareness
|
||||||
|
self.awareness = self.awareness_passive
|
||||||
|
|
||||||
|
self.threshold_pre = self.settings._AWARENESS_PRE_TIME_TILL_TERMINAL / self.settings._AWARENESS_TIME
|
||||||
|
self.threshold_prompt = self.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL / self.settings._AWARENESS_TIME
|
||||||
|
self.step_change = self.settings._DT_DMON / self.settings._AWARENESS_TIME
|
||||||
|
self.active_monitoring_mode = False
|
||||||
|
|
||||||
|
def _set_policy(self, brake_disengage_prob, car_speed):
|
||||||
|
bp = brake_disengage_prob
|
||||||
|
k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2)
|
||||||
|
bp_normal = max(min(bp / k1, 0.5),0)
|
||||||
|
self.pose.cfactor_pitch = np.interp(bp_normal, [0, 0.5],
|
||||||
|
[self.settings._POSE_PITCH_THRESHOLD_SLACK,
|
||||||
|
self.settings._POSE_PITCH_THRESHOLD_STRICT]) / self.settings._POSE_PITCH_THRESHOLD
|
||||||
|
self.pose.cfactor_yaw = np.interp(bp_normal, [0, 0.5],
|
||||||
|
[self.settings._POSE_YAW_THRESHOLD_SLACK,
|
||||||
|
self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD
|
||||||
|
|
||||||
|
def _get_distracted_types(self):
|
||||||
|
distracted_types = []
|
||||||
|
|
||||||
|
if not self.pose.calibrated:
|
||||||
|
pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
|
||||||
|
yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
|
||||||
|
else:
|
||||||
|
pitch_error = self.pose.pitch - min(max(self.pose.pitch_offseter.filtered_stat.mean(),
|
||||||
|
self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
|
||||||
|
yaw_error = self.pose.yaw - min(max(self.pose.yaw_offseter.filtered_stat.mean(),
|
||||||
|
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
|
||||||
|
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
|
||||||
|
|
||||||
|
if yaw_error * self.pose.steer_yaw_offset > 0: # unidirectional
|
||||||
|
yaw_error = max(abs(yaw_error) - min(abs(self.pose.steer_yaw_offset), self.settings._POSE_YAW_STEER_MAX_OFFSET), 0.)
|
||||||
|
else:
|
||||||
|
yaw_error = abs(yaw_error)
|
||||||
|
|
||||||
|
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
|
||||||
|
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
|
||||||
|
|
||||||
|
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||||
|
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||||
|
|
||||||
|
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||||
|
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||||
|
|
||||||
|
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||||
|
distracted_types.append(DistractedType.DISTRACTED_PHONE)
|
||||||
|
|
||||||
|
return distracted_types
|
||||||
|
|
||||||
|
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.):
|
||||||
|
rhd_pred = driver_state.wheelOnRightProb
|
||||||
|
# calibrates only when there's movement and either face detected
|
||||||
|
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
|
||||||
|
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
|
||||||
|
self.wheelpos.prob_offseter.push_and_update(rhd_pred)
|
||||||
|
|
||||||
|
self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT
|
||||||
|
|
||||||
|
if self.wheelpos.prob_calibrated or demo_mode:
|
||||||
|
self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
||||||
|
else:
|
||||||
|
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
|
||||||
|
# make sure no switching when engaged
|
||||||
|
if op_engaged and self.wheel_on_right_last is not None and self.wheel_on_right_last != self.wheel_on_right and not demo_mode:
|
||||||
|
self.wheel_on_right = self.wheel_on_right_last
|
||||||
|
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
|
||||||
|
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
|
||||||
|
driver_data.faceOrientationStd, driver_data.facePositionStd)):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
|
||||||
|
self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy)
|
||||||
|
steer_d = max(abs(steering_angle_deg) - self.settings._POSE_YAW_MIN_STEER_DEG, 0.)
|
||||||
|
self.pose.steer_yaw_offset = radians(steer_d) * -np.sign(steering_angle_deg) * self.settings._POSE_YAW_STEER_FACTOR
|
||||||
|
if self.wheel_on_right:
|
||||||
|
self.pose.yaw *= -1
|
||||||
|
self.pose.steer_yaw_offset *= -1
|
||||||
|
self.wheel_on_right_last = self.wheel_on_right
|
||||||
|
self.pose.pitch_std = driver_data.faceOrientationStd[0]
|
||||||
|
self.pose.yaw_std = driver_data.faceOrientationStd[1]
|
||||||
|
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
|
||||||
|
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
|
||||||
|
self.blink.left = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
|
||||||
|
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||||
|
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||||
|
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||||
|
self.phone_prob = driver_data.phoneProb
|
||||||
|
|
||||||
|
self.distracted_types = self._get_distracted_types()
|
||||||
|
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
|
||||||
|
or DistractedType.DISTRACTED_POSE in self.distracted_types
|
||||||
|
or DistractedType.DISTRACTED_BLINK in self.distracted_types) \
|
||||||
|
and driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std
|
||||||
|
self.driver_distraction_filter.update(self.driver_distracted)
|
||||||
|
|
||||||
|
# update offseter
|
||||||
|
# only update when driver is actively driving the car above a certain speed
|
||||||
|
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
|
||||||
|
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
||||||
|
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
||||||
|
|
||||||
|
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
||||||
|
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||||
|
|
||||||
|
if self.face_detected and not self.driver_distracted:
|
||||||
|
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
||||||
|
if not standstill:
|
||||||
|
self.dcam_uncertain_cnt += 1
|
||||||
|
self.dcam_reset_cnt = 0
|
||||||
|
else:
|
||||||
|
self.dcam_reset_cnt += 1
|
||||||
|
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
|
||||||
|
self.dcam_uncertain_cnt = 0
|
||||||
|
|
||||||
|
self.is_model_uncertain = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME
|
||||||
|
self._set_timers(self.face_detected and not self.is_model_uncertain)
|
||||||
|
if self.face_detected and not self.pose.low_std and not self.driver_distracted:
|
||||||
|
self.hi_stds += 1
|
||||||
|
elif self.face_detected and self.pose.low_std:
|
||||||
|
self.hi_stds = 0
|
||||||
|
|
||||||
|
def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||||
|
self._reset_events()
|
||||||
|
# Block engaging until ignition cycle after max number or time of distractions
|
||||||
|
if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \
|
||||||
|
self.terminal_time >= self.settings._MAX_TERMINAL_DURATION:
|
||||||
|
if not self.too_distracted:
|
||||||
|
self.params.put_bool_nonblocking("DriverTooDistracted", True)
|
||||||
|
self.too_distracted = True
|
||||||
|
|
||||||
|
# Always-on distraction lockout is temporary
|
||||||
|
if self.too_distracted or (self.always_on and self.awareness <= self.threshold_prompt):
|
||||||
|
self.current_events.add(EventName.tooDistracted)
|
||||||
|
|
||||||
|
always_on_valid = self.always_on and not wrong_gear
|
||||||
|
if (driver_engaged and self.awareness > 0 and not self.active_monitoring_mode) or \
|
||||||
|
(not always_on_valid and not op_engaged) or \
|
||||||
|
(always_on_valid and not op_engaged and self.awareness <= 0):
|
||||||
|
# always reset on disengage with normal mode; disengage resets only on red if always on
|
||||||
|
self._reset_awareness()
|
||||||
|
return
|
||||||
|
|
||||||
|
awareness_prev = self.awareness
|
||||||
|
_reaching_pre = self.awareness - self.step_change <= self.threshold_pre
|
||||||
|
_reaching_terminal = self.awareness - self.step_change <= 0
|
||||||
|
standstill_orange_exemption = standstill and _reaching_pre
|
||||||
|
always_on_red_exemption = always_on_valid and not op_engaged and _reaching_terminal
|
||||||
|
|
||||||
|
if self.awareness > 0 and \
|
||||||
|
((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_orange_exemption):
|
||||||
|
if driver_engaged:
|
||||||
|
self._reset_awareness()
|
||||||
|
return
|
||||||
|
# only restore awareness when paying attention and alert is not red
|
||||||
|
self.awareness = min(self.awareness + ((self.settings._RECOVERY_FACTOR_MAX-self.settings._RECOVERY_FACTOR_MIN)*
|
||||||
|
(1.-self.awareness)+self.settings._RECOVERY_FACTOR_MIN)*self.step_change, 1.)
|
||||||
|
if self.awareness == 1.:
|
||||||
|
self.awareness_passive = min(self.awareness_passive + self.step_change, 1.)
|
||||||
|
# don't display alert banner when awareness is recovering and has cleared orange
|
||||||
|
if self.awareness > self.threshold_prompt:
|
||||||
|
return
|
||||||
|
|
||||||
|
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
|
||||||
|
maybe_distracted = self.hi_stds > self.settings._HI_STD_FALLBACK_TIME or not self.face_detected
|
||||||
|
|
||||||
|
if certainly_distracted or maybe_distracted:
|
||||||
|
# should always be counting if distracted unless at standstill and reaching green
|
||||||
|
# also will not be reaching 0 if DM is active when not engaged
|
||||||
|
if not (standstill_orange_exemption or always_on_red_exemption):
|
||||||
|
self.awareness = max(self.awareness - self.step_change, -0.1)
|
||||||
|
|
||||||
|
alert = None
|
||||||
|
if self.awareness <= 0.:
|
||||||
|
# terminal red alert: disengagement required
|
||||||
|
alert = EventName.driverDistracted3 if self.active_monitoring_mode else EventName.driverUnresponsive3
|
||||||
|
self.terminal_time += 1
|
||||||
|
if awareness_prev > 0.:
|
||||||
|
self.terminal_alert_cnt += 1
|
||||||
|
elif self.awareness <= self.threshold_prompt:
|
||||||
|
# prompt orange alert
|
||||||
|
alert = EventName.driverDistracted2 if self.active_monitoring_mode else EventName.driverUnresponsive2
|
||||||
|
elif self.awareness <= self.threshold_pre:
|
||||||
|
# pre green alert
|
||||||
|
alert = EventName.driverDistracted1 if self.active_monitoring_mode else EventName.driverUnresponsive1
|
||||||
|
|
||||||
|
if alert is not None:
|
||||||
|
self.current_events.add(alert)
|
||||||
|
|
||||||
|
if self.dcam_uncertain_cnt > self.settings._DCAM_UNCERTAIN_ALERT_COUNT and not self.dcam_uncertain_alerted:
|
||||||
|
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
|
||||||
|
self.dcam_uncertain_alerted = True
|
||||||
|
|
||||||
|
|
||||||
|
def get_state_packet(self, valid=True):
|
||||||
|
# build driverMonitoringState packet
|
||||||
|
dat = messaging.new_message('driverMonitoringState', valid=valid)
|
||||||
|
dat.driverMonitoringState = {
|
||||||
|
"events": self.current_events.to_msg(),
|
||||||
|
"faceDetected": self.face_detected,
|
||||||
|
"isDistracted": self.driver_distracted,
|
||||||
|
"distractedType": sum(self.distracted_types),
|
||||||
|
"awarenessStatus": self.awareness,
|
||||||
|
"posePitchOffset": self.pose.pitch_offseter.filtered_stat.mean(),
|
||||||
|
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
|
||||||
|
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
|
||||||
|
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
|
||||||
|
"stepChange": self.step_change,
|
||||||
|
"awarenessActive": self.awareness_active,
|
||||||
|
"awarenessPassive": self.awareness_passive,
|
||||||
|
"isLowStd": self.pose.low_std,
|
||||||
|
"hiStdCount": self.hi_stds,
|
||||||
|
"isActiveMode": self.active_monitoring_mode,
|
||||||
|
"isRHD": self.wheel_on_right,
|
||||||
|
"uncertainCount": self.dcam_uncertain_cnt,
|
||||||
|
}
|
||||||
|
return dat
|
||||||
|
|
||||||
|
def run_step(self, sm, demo=False):
|
||||||
|
if demo:
|
||||||
|
highway_speed = 30
|
||||||
|
enabled = True
|
||||||
|
wrong_gear = False
|
||||||
|
standstill = False
|
||||||
|
driver_engaged = False
|
||||||
|
brake_disengage_prob = 1.0
|
||||||
|
rpyCalib = [0., 0., 0.]
|
||||||
|
else:
|
||||||
|
highway_speed = sm['carState'].vEgo
|
||||||
|
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
|
||||||
|
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
|
||||||
|
standstill = sm['carState'].standstill
|
||||||
|
driver_engaged = sm['carState'].steeringPressed or (sm['selfdriveState'].enabled and sm['carState'].gasPressed)
|
||||||
|
brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
|
||||||
|
rpyCalib = sm['liveCalibration'].rpyCalib
|
||||||
|
self._set_policy(
|
||||||
|
brake_disengage_prob=brake_disengage_prob,
|
||||||
|
car_speed=highway_speed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse data from dmonitoringmodeld
|
||||||
|
self._update_states(
|
||||||
|
driver_state=sm['driverStateV2'],
|
||||||
|
cal_rpy=rpyCalib,
|
||||||
|
car_speed=highway_speed,
|
||||||
|
op_engaged=enabled,
|
||||||
|
standstill=standstill,
|
||||||
|
demo_mode=demo,
|
||||||
|
steering_angle_deg=sm['carState'].steeringAngleDeg,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update distraction events
|
||||||
|
self._update_events(
|
||||||
|
driver_engaged=driver_engaged,
|
||||||
|
op_engaged=enabled,
|
||||||
|
standstill=standstill,
|
||||||
|
wrong_gear=wrong_gear,
|
||||||
|
car_speed=highway_speed
|
||||||
|
)
|
||||||
@@ -1,426 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from math import atan2, radians
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from cereal import car, log
|
|
||||||
import cereal.messaging as messaging
|
|
||||||
from openpilot.common.realtime import DT_DMON
|
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
|
||||||
from openpilot.common.params import Params
|
|
||||||
from openpilot.common.stat_live import RunningStatFilter
|
|
||||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
|
||||||
|
|
||||||
AlertLevel = log.DriverMonitoringState.AlertLevel
|
|
||||||
MonitoringPolicy = log.DriverMonitoringState.MonitoringPolicy
|
|
||||||
|
|
||||||
def to_percent(v):
|
|
||||||
return int(min(max(v * 100., 0.), 100.))
|
|
||||||
|
|
||||||
# ******************************************************************************************
|
|
||||||
# NOTE: To fork maintainers.
|
|
||||||
# Disabling or nerfing safety features will get you and your users banned from our servers.
|
|
||||||
# We recommend that you do not change these numbers from the defaults.
|
|
||||||
# ******************************************************************************************
|
|
||||||
|
|
||||||
class DRIVER_MONITOR_SETTINGS:
|
|
||||||
def __init__(self):
|
|
||||||
# https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:42018X1947&rid=2
|
|
||||||
self._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT = 15.
|
|
||||||
self._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT = 24.
|
|
||||||
self._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT = 30.
|
|
||||||
# https://cdn.euroncap.com/cars/assets/euro_ncap_protocol_safe_driving_driver_engagement_v11_a30e874152.pdf
|
|
||||||
self._VISION_POLICY_ALERT_1_TIMEOUT = 3.
|
|
||||||
self._VISION_POLICY_ALERT_2_TIMEOUT = 5.
|
|
||||||
self._VISION_POLICY_ALERT_3_TIMEOUT = 11.
|
|
||||||
|
|
||||||
self._TIMEOUT_RECOVERY_FACTOR_MAX = 5.
|
|
||||||
self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25
|
|
||||||
|
|
||||||
self._MAX_TERMINAL_ALERTS = 3 # not allowed to engage after 3 terminal alerts
|
|
||||||
self._MAX_TERMINAL_DURATION = int(30 / DT_DMON) # not allowed to engage after 30s of terminal alerts
|
|
||||||
|
|
||||||
self._FACE_THRESHOLD = 0.7
|
|
||||||
self._EYE_THRESHOLD = 0.5
|
|
||||||
self._BLINK_THRESHOLD = 0.5
|
|
||||||
self._PHONE_THRESH = 0.5
|
|
||||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
|
||||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
|
||||||
self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD
|
|
||||||
self._POSE_YAW_THRESHOLD = 0.4020
|
|
||||||
self._POSE_YAW_THRESHOLD_SLACK = 0.5042
|
|
||||||
self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD
|
|
||||||
self._POSE_YAW_MIN_STEER_DEG = 30
|
|
||||||
self._POSE_YAW_STEER_FACTOR = 0.15
|
|
||||||
self._POSE_YAW_STEER_MAX_OFFSET = 0.3927
|
|
||||||
self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned
|
|
||||||
self._PITCH_NATURAL_THRESHOLD = 0.449
|
|
||||||
self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned
|
|
||||||
self._PITCH_NATURAL_VAR = 3*0.01
|
|
||||||
self._YAW_NATURAL_VAR = 3*0.05
|
|
||||||
self._PITCH_MAX_OFFSET = 0.124
|
|
||||||
self._PITCH_MIN_OFFSET = -0.0881
|
|
||||||
self._YAW_MAX_OFFSET = 0.289
|
|
||||||
self._YAW_MIN_OFFSET = -0.0246
|
|
||||||
|
|
||||||
self._DCAM_UNCERTAIN_ALERT_THRESHOLD = 0.1
|
|
||||||
self._DCAM_UNCERTAIN_ALERT_COUNT = int(60 / DT_DMON)
|
|
||||||
self._DCAM_UNCERTAIN_RESET_COUNT = int(20 / DT_DMON)
|
|
||||||
self._HI_STD_THRESHOLD = 0.3
|
|
||||||
self._HI_STD_FALLBACK_TIME = int(10 / DT_DMON) # fall back to wheel touch if model is uncertain for 10s
|
|
||||||
self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz
|
|
||||||
|
|
||||||
self._POSE_CALIB_MIN_SPEED = 13 # 30 mph
|
|
||||||
self._POSE_OFFSET_MIN_COUNT = int(60 / DT_DMON) # valid data counts before calibration completes, 1min cumulative
|
|
||||||
self._POSE_OFFSET_MAX_COUNT = int(360 / DT_DMON) # stop deweighting new data after 6 min, aka "short term memory"
|
|
||||||
self._WHEELPOS_CALIB_MIN_SPEED = 11
|
|
||||||
self._WHEELPOS_THRESHOLD = 0.5
|
|
||||||
self._WHEELPOS_FILTER_MIN_COUNT = int(15 / DT_DMON) # allow 15 seconds to converge wheel side
|
|
||||||
self._WHEELPOS_DATA_AVG = 0.03
|
|
||||||
self._WHEELPOS_DATA_VAR = 3*5.5e-5
|
|
||||||
self._WHEELPOS_MAX_COUNT = -1
|
|
||||||
|
|
||||||
class DriverPose:
|
|
||||||
def __init__(self, settings):
|
|
||||||
pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2)
|
|
||||||
yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2)
|
|
||||||
self.yaw = 0.
|
|
||||||
self.pitch = 0.
|
|
||||||
self.pitch_offsetter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
|
||||||
self.yaw_offsetter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT)
|
|
||||||
self.calibrated = False
|
|
||||||
self.low_std = True
|
|
||||||
self.cfactor_pitch = 1.
|
|
||||||
self.cfactor_yaw = 1.
|
|
||||||
self.steer_yaw_offset = 0.
|
|
||||||
|
|
||||||
# model output refers to center of undistorted+leveled image
|
|
||||||
ref_undistorted_cam = DEVICE_CAMERAS[("tici", "ar0231")].dcam
|
|
||||||
dcam_undistorted_FL = 598.0
|
|
||||||
dcam_undistorted_W, dcam_undistorted_H = (ref_undistorted_cam.width, ref_undistorted_cam.height)
|
|
||||||
|
|
||||||
def face_orientation_from_model(orient_model, pos_model, rpy_calib):
|
|
||||||
pitch_model = orient_model[0]
|
|
||||||
yaw_model = orient_model[1]
|
|
||||||
|
|
||||||
face_pixel_position = ((pos_model[0]+0.5)*dcam_undistorted_W, (pos_model[1]+0.5)*dcam_undistorted_H)
|
|
||||||
yaw_focal_angle = atan2(face_pixel_position[0] - dcam_undistorted_W//2, dcam_undistorted_FL)
|
|
||||||
pitch_focal_angle = atan2(face_pixel_position[1] - dcam_undistorted_H//2, dcam_undistorted_FL)
|
|
||||||
|
|
||||||
pitch = pitch_model + pitch_focal_angle
|
|
||||||
yaw = -yaw_model + yaw_focal_angle
|
|
||||||
|
|
||||||
pitch -= rpy_calib[1]
|
|
||||||
yaw -= rpy_calib[2]
|
|
||||||
return pitch, yaw
|
|
||||||
|
|
||||||
|
|
||||||
class DriverMonitoring:
|
|
||||||
def __init__(self, rhd_saved=False, settings=None, always_on=False):
|
|
||||||
# init policy settings
|
|
||||||
self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS()
|
|
||||||
|
|
||||||
# init driver status
|
|
||||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
|
||||||
self.wheelpos_offsetter = RunningStatFilter(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
|
||||||
self.pose = DriverPose(settings=self.settings)
|
|
||||||
self.blink_prob = 0.
|
|
||||||
self.phone_prob = 0.
|
|
||||||
|
|
||||||
self.alert_level = AlertLevel.none
|
|
||||||
self.always_on = always_on
|
|
||||||
self.distracted_types = defaultdict(bool)
|
|
||||||
self.driver_distracted = False
|
|
||||||
self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, DT_DMON)
|
|
||||||
self.wheel_on_right = False
|
|
||||||
self.wheel_on_right_last = None
|
|
||||||
self.wheel_on_right_default = rhd_saved
|
|
||||||
self.face_detected = False
|
|
||||||
self.terminal_alert_cnt = 0
|
|
||||||
self.terminal_time = 0
|
|
||||||
self.step_change = 0.
|
|
||||||
self.active_policy = MonitoringPolicy.vision
|
|
||||||
self.driver_interacting = False
|
|
||||||
self.is_model_uncertain = False
|
|
||||||
self.hi_stds = 0
|
|
||||||
self.model_std_max = 0.
|
|
||||||
self.threshold_alert_1 = 0.
|
|
||||||
self.threshold_alert_2 = 0.
|
|
||||||
self.dcam_uncertain_cnt = 0
|
|
||||||
self.dcam_reset_cnt = 0
|
|
||||||
self.too_distracted = Params().get_bool("DriverTooDistracted")
|
|
||||||
|
|
||||||
self._reset_awareness()
|
|
||||||
self._set_policy(MonitoringPolicy.vision)
|
|
||||||
|
|
||||||
def _reset_awareness(self):
|
|
||||||
self.awareness = 1.
|
|
||||||
self.last_vision_awareness = 1.
|
|
||||||
self.last_wheeltouch_awareness = 1.
|
|
||||||
|
|
||||||
def _set_policy(self, target_policy):
|
|
||||||
if self.active_policy == MonitoringPolicy.vision and self.awareness <= self.threshold_alert_2:
|
|
||||||
if target_policy == MonitoringPolicy.vision:
|
|
||||||
self.step_change = DT_DMON / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
|
|
||||||
else:
|
|
||||||
self.step_change = 0.
|
|
||||||
return # no exploit after orange alert
|
|
||||||
elif self.awareness <= 0.:
|
|
||||||
return
|
|
||||||
|
|
||||||
if target_policy == MonitoringPolicy.vision:
|
|
||||||
# when falling back from passive mode to active mode, reset awareness to avoid false alert
|
|
||||||
if self.active_policy != MonitoringPolicy.vision:
|
|
||||||
self.last_wheeltouch_awareness = self.awareness
|
|
||||||
self.awareness = self.last_vision_awareness
|
|
||||||
|
|
||||||
self.threshold_alert_1 = 1. - self.settings._VISION_POLICY_ALERT_1_TIMEOUT / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.threshold_alert_2 = 1. - self.settings._VISION_POLICY_ALERT_2_TIMEOUT / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.step_change = DT_DMON / self.settings._VISION_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.active_policy = MonitoringPolicy.vision
|
|
||||||
else:
|
|
||||||
if self.active_policy == MonitoringPolicy.vision:
|
|
||||||
self.last_vision_awareness = self.awareness
|
|
||||||
self.awareness = self.last_wheeltouch_awareness
|
|
||||||
|
|
||||||
self.threshold_alert_1 = 1. - self.settings._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.threshold_alert_2 = 1. - self.settings._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.step_change = DT_DMON / self.settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT
|
|
||||||
self.active_policy = MonitoringPolicy.wheeltouch
|
|
||||||
|
|
||||||
def _set_pose_strictness(self, brake_disengage_prob, car_speed):
|
|
||||||
bp = brake_disengage_prob
|
|
||||||
k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2)
|
|
||||||
bp_normal = max(min(bp / k1, 0.5),0)
|
|
||||||
self.pose.cfactor_pitch = np.interp(bp_normal, [0, 0.5],
|
|
||||||
[self.settings._POSE_PITCH_THRESHOLD_SLACK,
|
|
||||||
self.settings._POSE_PITCH_THRESHOLD_STRICT]) / self.settings._POSE_PITCH_THRESHOLD
|
|
||||||
self.pose.cfactor_yaw = np.interp(bp_normal, [0, 0.5],
|
|
||||||
[self.settings._POSE_YAW_THRESHOLD_SLACK,
|
|
||||||
self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD
|
|
||||||
|
|
||||||
def _get_distracted_types(self):
|
|
||||||
self.distracted_types = defaultdict(bool)
|
|
||||||
|
|
||||||
if not self.pose.calibrated:
|
|
||||||
pitch_error = self.pose.pitch - self.settings._PITCH_NATURAL_OFFSET
|
|
||||||
yaw_error = self.pose.yaw - self.settings._YAW_NATURAL_OFFSET
|
|
||||||
else:
|
|
||||||
pitch_error = self.pose.pitch - min(max(self.pose.pitch_offsetter.filtered_stat.mean(),
|
|
||||||
self.settings._PITCH_MIN_OFFSET), self.settings._PITCH_MAX_OFFSET)
|
|
||||||
yaw_error = self.pose.yaw - min(max(self.pose.yaw_offsetter.filtered_stat.mean(),
|
|
||||||
self.settings._YAW_MIN_OFFSET), self.settings._YAW_MAX_OFFSET)
|
|
||||||
pitch_error = 0 if pitch_error > 0 else abs(pitch_error) # no positive pitch limit
|
|
||||||
|
|
||||||
if yaw_error * self.pose.steer_yaw_offset > 0: # unidirectional
|
|
||||||
yaw_error = max(abs(yaw_error) - min(abs(self.pose.steer_yaw_offset), self.settings._POSE_YAW_STEER_MAX_OFFSET), 0.)
|
|
||||||
else:
|
|
||||||
yaw_error = abs(yaw_error)
|
|
||||||
|
|
||||||
pitch_threshold = self.settings._POSE_PITCH_THRESHOLD * self.pose.cfactor_pitch if self.pose.calibrated else self.settings._PITCH_NATURAL_THRESHOLD
|
|
||||||
yaw_threshold = self.settings._POSE_YAW_THRESHOLD * self.pose.cfactor_yaw
|
|
||||||
|
|
||||||
self.distracted_types['pose'] = bool((pitch_error > pitch_threshold) or (yaw_error > yaw_threshold))
|
|
||||||
self.distracted_types['eye'] = bool(self.blink_prob > self.settings._BLINK_THRESHOLD)
|
|
||||||
self.distracted_types['phone'] = bool(self.phone_prob > self.settings._PHONE_THRESH)
|
|
||||||
|
|
||||||
def _update_states(self, driver_state, cal_rpy, car_speed, op_engaged, standstill, demo_mode=False, steering_angle_deg=0.):
|
|
||||||
rhd_pred = driver_state.wheelOnRightProb
|
|
||||||
# calibrates only when there's movement and either face detected
|
|
||||||
if car_speed > self.settings._WHEELPOS_CALIB_MIN_SPEED and (driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD or
|
|
||||||
driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD):
|
|
||||||
self.wheelpos_offsetter.push_and_update(rhd_pred)
|
|
||||||
|
|
||||||
wheelpos_calibrated = self.wheelpos_offsetter.filtered_stat.n >= self.settings._WHEELPOS_FILTER_MIN_COUNT
|
|
||||||
|
|
||||||
if wheelpos_calibrated or demo_mode:
|
|
||||||
self.wheel_on_right = self.wheelpos_offsetter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD
|
|
||||||
else:
|
|
||||||
self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished
|
|
||||||
# make sure no switching when engaged
|
|
||||||
if op_engaged and self.wheel_on_right_last is not None and self.wheel_on_right_last != self.wheel_on_right and not demo_mode:
|
|
||||||
self.wheel_on_right = self.wheel_on_right_last
|
|
||||||
driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData
|
|
||||||
if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition,
|
|
||||||
driver_data.faceOrientationStd, driver_data.facePositionStd)):
|
|
||||||
return
|
|
||||||
|
|
||||||
self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD
|
|
||||||
self.pose.pitch, self.pose.yaw = face_orientation_from_model(driver_data.faceOrientation, driver_data.facePosition, cal_rpy)
|
|
||||||
steer_d = max(abs(steering_angle_deg) - self.settings._POSE_YAW_MIN_STEER_DEG, 0.)
|
|
||||||
self.pose.steer_yaw_offset = radians(steer_d) * -np.sign(steering_angle_deg) * self.settings._POSE_YAW_STEER_FACTOR
|
|
||||||
if self.wheel_on_right:
|
|
||||||
self.pose.yaw *= -1
|
|
||||||
self.pose.steer_yaw_offset *= -1
|
|
||||||
self.wheel_on_right_last = self.wheel_on_right
|
|
||||||
self.model_std_max = max(driver_data.faceOrientationStd[0], driver_data.faceOrientationStd[1])
|
|
||||||
self.pose.low_std = self.model_std_max < self.settings._HI_STD_THRESHOLD
|
|
||||||
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
|
|
||||||
self.phone_prob = driver_data.phoneProb
|
|
||||||
|
|
||||||
self._get_distracted_types()
|
|
||||||
self.driver_distracted = any(self.distracted_types.values()) and driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std
|
|
||||||
self.driver_distraction_filter.update(self.driver_distracted)
|
|
||||||
|
|
||||||
# only update offsetter when driver is actively driving the car above a certain speed
|
|
||||||
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
|
|
||||||
self.pose.pitch_offsetter.push_and_update(self.pose.pitch)
|
|
||||||
self.pose.yaw_offsetter.push_and_update(self.pose.yaw)
|
|
||||||
|
|
||||||
self.pose.calibrated = self.pose.pitch_offsetter.filtered_stat.n >= self.settings._POSE_OFFSET_MIN_COUNT and \
|
|
||||||
self.pose.yaw_offsetter.filtered_stat.n >= self.settings._POSE_OFFSET_MIN_COUNT
|
|
||||||
|
|
||||||
if self.face_detected and not self.driver_distracted:
|
|
||||||
dcam_uncertain = self.model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD
|
|
||||||
if dcam_uncertain and not standstill:
|
|
||||||
self.dcam_uncertain_cnt += 1
|
|
||||||
self.dcam_reset_cnt = 0
|
|
||||||
else:
|
|
||||||
self.dcam_reset_cnt += 1
|
|
||||||
if self.dcam_reset_cnt > self.settings._DCAM_UNCERTAIN_RESET_COUNT:
|
|
||||||
self.dcam_uncertain_cnt = 0
|
|
||||||
|
|
||||||
self.is_model_uncertain = self.hi_stds >= self.settings._HI_STD_FALLBACK_TIME
|
|
||||||
self._set_policy(MonitoringPolicy.vision if self.face_detected and not self.is_model_uncertain else MonitoringPolicy.wheeltouch)
|
|
||||||
if self.face_detected and not self.pose.low_std and not self.driver_distracted:
|
|
||||||
self.hi_stds += 1
|
|
||||||
elif self.face_detected and self.pose.low_std:
|
|
||||||
self.hi_stds = 0
|
|
||||||
|
|
||||||
def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear):
|
|
||||||
self.alert_level = AlertLevel.none
|
|
||||||
self.driver_interacting = driver_engaged
|
|
||||||
|
|
||||||
if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \
|
|
||||||
self.terminal_time >= self.settings._MAX_TERMINAL_DURATION:
|
|
||||||
self.too_distracted = True
|
|
||||||
|
|
||||||
always_on_valid = self.always_on and not wrong_gear
|
|
||||||
if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \
|
|
||||||
(not always_on_valid and not op_engaged) or \
|
|
||||||
(always_on_valid and not op_engaged and self.awareness <= 0):
|
|
||||||
# always reset on disengage with normal mode; disengage resets only on red if always on
|
|
||||||
self._reset_awareness()
|
|
||||||
return
|
|
||||||
|
|
||||||
awareness_prev = self.awareness
|
|
||||||
_reaching_alert_1 = self.awareness - self.step_change <= self.threshold_alert_1
|
|
||||||
_reaching_alert_3 = self.awareness - self.step_change <= 0
|
|
||||||
standstill_exemption = standstill and _reaching_alert_1
|
|
||||||
always_on_exemption = always_on_valid and not op_engaged and _reaching_alert_3
|
|
||||||
|
|
||||||
if self.awareness > 0 and \
|
|
||||||
((self.driver_distraction_filter.x < 0.37 and self.face_detected and self.pose.low_std) or standstill_exemption):
|
|
||||||
if self.driver_interacting:
|
|
||||||
self._reset_awareness()
|
|
||||||
return
|
|
||||||
# only restore awareness when paying attention and alert is not red
|
|
||||||
self.awareness = min(self.awareness + ((self.settings._TIMEOUT_RECOVERY_FACTOR_MAX-self.settings._TIMEOUT_RECOVERY_FACTOR_MIN)*
|
|
||||||
(1.-self.awareness)+self.settings._TIMEOUT_RECOVERY_FACTOR_MIN)*self.step_change, 1.)
|
|
||||||
if self.awareness == 1.:
|
|
||||||
self.last_wheeltouch_awareness = min(self.last_wheeltouch_awareness + self.step_change, 1.)
|
|
||||||
# don't display alert banner when awareness is recovering and has cleared orange
|
|
||||||
if self.awareness > self.threshold_alert_2:
|
|
||||||
return
|
|
||||||
|
|
||||||
certainly_distracted = self.driver_distraction_filter.x > 0.63 and self.driver_distracted and self.face_detected
|
|
||||||
maybe_distracted = self.is_model_uncertain or not self.face_detected
|
|
||||||
|
|
||||||
if certainly_distracted or maybe_distracted:
|
|
||||||
# should always be counting if distracted unless at standstill and reaching green
|
|
||||||
# also will not be reaching 0 if DM is active when not engaged
|
|
||||||
if not (standstill_exemption or always_on_exemption):
|
|
||||||
self.awareness = max(self.awareness - self.step_change, -0.1)
|
|
||||||
|
|
||||||
if self.awareness <= 0.:
|
|
||||||
# terminal alert: disengagement required
|
|
||||||
self.alert_level = AlertLevel.three
|
|
||||||
self.terminal_time += 1
|
|
||||||
if awareness_prev > 0.:
|
|
||||||
self.terminal_alert_cnt += 1
|
|
||||||
elif self.awareness <= self.threshold_alert_2:
|
|
||||||
self.alert_level = AlertLevel.two
|
|
||||||
elif self.awareness <= self.threshold_alert_1:
|
|
||||||
self.alert_level = AlertLevel.one
|
|
||||||
|
|
||||||
def get_state_packet(self, valid=True):
|
|
||||||
# build driverMonitoringState packet
|
|
||||||
dat = messaging.new_message('driverMonitoringState', valid=valid)
|
|
||||||
dm = dat.driverMonitoringState
|
|
||||||
|
|
||||||
dm.lockout = self.too_distracted
|
|
||||||
dm.alertCountLockoutPercent = to_percent(self.terminal_alert_cnt / self.settings._MAX_TERMINAL_ALERTS)
|
|
||||||
dm.alertTimeLockoutPercent = to_percent(self.terminal_time / self.settings._MAX_TERMINAL_DURATION)
|
|
||||||
dm.alwaysOn = self.always_on
|
|
||||||
dm.alwaysOnLockout = self.always_on and self.awareness <= self.threshold_alert_2
|
|
||||||
dm.alertLevel = self.alert_level
|
|
||||||
dm.activePolicy = self.active_policy
|
|
||||||
dm.isRHD = self.wheel_on_right
|
|
||||||
dm.rhdCalibration.calibratedPercent = to_percent(self.wheelpos_offsetter.filtered_stat.n / self.settings._WHEELPOS_FILTER_MIN_COUNT)
|
|
||||||
dm.rhdCalibration.offset = self.wheelpos_offsetter.filtered_stat.M
|
|
||||||
|
|
||||||
dm.visionPolicyState.awarenessPercent = to_percent(self.last_vision_awareness if self.active_policy != MonitoringPolicy.vision else self.awareness)
|
|
||||||
dm.visionPolicyState.awarenessStep = self.step_change if self.active_policy == MonitoringPolicy.vision else 0.
|
|
||||||
dm.visionPolicyState.isDistracted = self.driver_distracted
|
|
||||||
dm.visionPolicyState.distractedTypes.pose = self.distracted_types['pose']
|
|
||||||
dm.visionPolicyState.distractedTypes.eye = self.distracted_types['eye']
|
|
||||||
dm.visionPolicyState.distractedTypes.phone = self.distracted_types['phone']
|
|
||||||
dm.visionPolicyState.faceDetected = self.face_detected
|
|
||||||
dm.visionPolicyState.pose.pitch = self.pose.pitch
|
|
||||||
dm.visionPolicyState.pose.yaw = self.pose.yaw
|
|
||||||
dm.visionPolicyState.pose.calibrated = self.pose.calibrated
|
|
||||||
dm.visionPolicyState.pose.pitchCalib.calibratedPercent = to_percent(self.pose.pitch_offsetter.filtered_stat.n / self.settings._POSE_OFFSET_MIN_COUNT)
|
|
||||||
dm.visionPolicyState.pose.pitchCalib.offset = self.pose.pitch_offsetter.filtered_stat.M
|
|
||||||
dm.visionPolicyState.pose.yawCalib.calibratedPercent = to_percent(self.pose.yaw_offsetter.filtered_stat.n / self.settings._POSE_OFFSET_MIN_COUNT)
|
|
||||||
dm.visionPolicyState.pose.yawCalib.offset = self.pose.yaw_offsetter.filtered_stat.M
|
|
||||||
dm.visionPolicyState.pose.uncertainty = self.model_std_max
|
|
||||||
dm.visionPolicyState.wheeltouchFallbackPercent = to_percent(self.hi_stds / self.settings._HI_STD_FALLBACK_TIME)
|
|
||||||
dm.visionPolicyState.uncertainOffroadAlertPercent = to_percent(self.dcam_uncertain_cnt / self.settings._DCAM_UNCERTAIN_ALERT_COUNT)
|
|
||||||
|
|
||||||
dm.wheeltouchPolicyState.awarenessPercent = to_percent(self.last_wheeltouch_awareness if self.active_policy == MonitoringPolicy.vision else self.awareness)
|
|
||||||
dm.wheeltouchPolicyState.awarenessStep = 0. if self.active_policy == MonitoringPolicy.vision else self.step_change
|
|
||||||
dm.wheeltouchPolicyState.driverInteracting = self.driver_interacting
|
|
||||||
return dat
|
|
||||||
|
|
||||||
def run_step(self, sm, demo=False):
|
|
||||||
if demo:
|
|
||||||
car_speed = 30
|
|
||||||
enabled = True
|
|
||||||
wrong_gear = False
|
|
||||||
standstill = False
|
|
||||||
driver_engaged = False
|
|
||||||
brake_disengage_prob = 1.0
|
|
||||||
steering_angle_deg = 0.0
|
|
||||||
rpyCalib = [0., 0., 0.]
|
|
||||||
else:
|
|
||||||
car_speed = sm['carState'].vEgo
|
|
||||||
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
|
|
||||||
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
|
|
||||||
standstill = sm['carState'].standstill
|
|
||||||
driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed
|
|
||||||
brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
|
|
||||||
steering_angle_deg = sm['carState'].steeringAngleDeg
|
|
||||||
rpyCalib = sm['liveCalibration'].rpyCalib
|
|
||||||
|
|
||||||
self._set_pose_strictness(
|
|
||||||
brake_disengage_prob=brake_disengage_prob,
|
|
||||||
car_speed=car_speed,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse data from dmonitoringmodeld
|
|
||||||
self._update_states(
|
|
||||||
driver_state=sm['driverStateV2'],
|
|
||||||
cal_rpy=rpyCalib,
|
|
||||||
car_speed=car_speed,
|
|
||||||
op_engaged=enabled,
|
|
||||||
standstill=standstill,
|
|
||||||
demo_mode=demo,
|
|
||||||
steering_angle_deg=steering_angle_deg,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update distraction events
|
|
||||||
self._update_events(
|
|
||||||
driver_engaged=driver_engaged,
|
|
||||||
op_engaged=enabled,
|
|
||||||
standstill=standstill,
|
|
||||||
wrong_gear=wrong_gear,
|
|
||||||
)
|
|
||||||
@@ -3,24 +3,27 @@ import pytest
|
|||||||
|
|
||||||
from cereal import log, car
|
from cereal import log, car
|
||||||
from openpilot.common.realtime import DT_DMON
|
from openpilot.common.realtime import DT_DMON
|
||||||
from openpilot.selfdrive.monitoring.policy import DriverMonitoring, DRIVER_MONITOR_SETTINGS
|
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
|
||||||
EventName = log.OnroadEvent.EventName
|
EventName = log.OnroadEvent.EventName
|
||||||
dm_settings = DRIVER_MONITOR_SETTINGS()
|
dm_settings = DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type())
|
||||||
|
|
||||||
TEST_TIMESPAN = 120 # seconds
|
TEST_TIMESPAN = 120 # seconds
|
||||||
DISTRACTED_SECONDS_TO_ORANGE = dm_settings._VISION_POLICY_ALERT_2_TIMEOUT + 1
|
DISTRACTED_SECONDS_TO_ORANGE = dm_settings._DISTRACTED_TIME - dm_settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + 1
|
||||||
DISTRACTED_SECONDS_TO_RED = dm_settings._VISION_POLICY_ALERT_3_TIMEOUT + 1
|
DISTRACTED_SECONDS_TO_RED = dm_settings._DISTRACTED_TIME + 1
|
||||||
INVISIBLE_SECONDS_TO_ORANGE = dm_settings._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT + 1
|
INVISIBLE_SECONDS_TO_ORANGE = dm_settings._AWARENESS_TIME - dm_settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + 1
|
||||||
INVISIBLE_SECONDS_TO_RED = dm_settings._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT + 1
|
INVISIBLE_SECONDS_TO_RED = dm_settings._AWARENESS_TIME + 1
|
||||||
|
|
||||||
def make_msg(face_detected, distracted=False, model_uncertain=False):
|
def make_msg(face_detected, distracted=False, model_uncertain=False):
|
||||||
ds = log.DriverStateV2.new_message()
|
ds = log.DriverStateV2.new_message()
|
||||||
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
||||||
ds.leftDriverData.facePosition = [0., 0.]
|
ds.leftDriverData.facePosition = [0., 0.]
|
||||||
ds.leftDriverData.faceProb = 1. * face_detected
|
ds.leftDriverData.faceProb = 1. * face_detected
|
||||||
ds.leftDriverData.eyesVisibleProb = 1.
|
ds.leftDriverData.leftEyeProb = 1.
|
||||||
ds.leftDriverData.eyesClosedProb = 1. * distracted
|
ds.leftDriverData.rightEyeProb = 1.
|
||||||
|
ds.leftDriverData.leftBlinkProb = 1. * distracted
|
||||||
|
ds.leftDriverData.rightBlinkProb = 1. * distracted
|
||||||
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
||||||
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
||||||
# TODO: test both separately when e2e is used
|
# TODO: test both separately when e2e is used
|
||||||
@@ -34,7 +37,7 @@ msg_ATTENTIVE = make_msg(True)
|
|||||||
msg_DISTRACTED = make_msg(True, distracted=True)
|
msg_DISTRACTED = make_msg(True, distracted=True)
|
||||||
msg_ATTENTIVE_UNCERTAIN = make_msg(True, model_uncertain=True)
|
msg_ATTENTIVE_UNCERTAIN = make_msg(True, model_uncertain=True)
|
||||||
msg_DISTRACTED_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=True)
|
msg_DISTRACTED_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=True)
|
||||||
msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=dm_settings._HI_STD_THRESHOLD*1.5)
|
msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN = make_msg(True, distracted=True, model_uncertain=dm_settings._POSESTD_THRESHOLD*1.5)
|
||||||
|
|
||||||
# driver interaction with car
|
# driver interaction with car
|
||||||
car_interaction_DETECTED = True
|
car_interaction_DETECTED = True
|
||||||
@@ -50,49 +53,49 @@ always_false = [False] * int(TEST_TIMESPAN / DT_DMON)
|
|||||||
class TestMonitoring:
|
class TestMonitoring:
|
||||||
def _run_seq(self, msgs, interaction, engaged, standstill):
|
def _run_seq(self, msgs, interaction, engaged, standstill):
|
||||||
DM = DriverMonitoring()
|
DM = DriverMonitoring()
|
||||||
alert_lvls = []
|
events = []
|
||||||
for idx in range(len(msgs)):
|
for idx in range(len(msgs)):
|
||||||
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
|
DM._update_states(msgs[idx], [0, 0, 0], 0, engaged[idx], standstill[idx])
|
||||||
# cal_rpy and car_speed don't matter here
|
# cal_rpy and car_speed don't matter here
|
||||||
|
|
||||||
# evaluate events at 10Hz for tests
|
# evaluate events at 10Hz for tests
|
||||||
DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0)
|
DM._update_events(interaction[idx], engaged[idx], standstill[idx], 0, 0)
|
||||||
alert_lvls.append(DM.alert_level)
|
events.append(DM.current_events)
|
||||||
assert len(alert_lvls) == len(msgs), f"got {len(alert_lvls)} for {len(msgs)} driverState input msgs"
|
assert len(events) == len(msgs), f"got {len(events)} for {len(msgs)} driverState input msgs"
|
||||||
return alert_lvls, DM
|
return events, DM
|
||||||
|
|
||||||
|
def _assert_no_events(self, events):
|
||||||
|
assert all(not len(e) for e in events)
|
||||||
|
|
||||||
# engaged, driver is attentive all the time
|
# engaged, driver is attentive all the time
|
||||||
def test_fully_aware_driver(self):
|
def test_fully_aware_driver(self):
|
||||||
alert_lvls, d_status = self._run_seq(always_attentive, always_false, always_true, always_false)
|
events, _ = self._run_seq(always_attentive, always_false, always_true, always_false)
|
||||||
assert all(a == 0 for a in alert_lvls)
|
self._assert_no_events(events)
|
||||||
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.vision
|
|
||||||
|
|
||||||
# engaged, driver is distracted and does nothing
|
# engaged, driver is distracted and does nothing
|
||||||
def test_fully_distracted_driver(self):
|
def test_fully_distracted_driver(self):
|
||||||
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
|
events, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
|
||||||
s = d_status.settings
|
assert len(events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int(s._VISION_POLICY_ALERT_1_TIMEOUT / 2 / DT_DMON)] == 0
|
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL + \
|
||||||
assert alert_lvls[int((s._VISION_POLICY_ALERT_1_TIMEOUT + \
|
((d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
|
||||||
(s._VISION_POLICY_ALERT_2_TIMEOUT - s._VISION_POLICY_ALERT_1_TIMEOUT) / 2) / DT_DMON)] == 1
|
EventName.driverDistracted1
|
||||||
assert alert_lvls[int((s._VISION_POLICY_ALERT_2_TIMEOUT + \
|
assert events[int((d_status.settings._DISTRACTED_TIME-d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL + \
|
||||||
(s._VISION_POLICY_ALERT_3_TIMEOUT - s._VISION_POLICY_ALERT_2_TIMEOUT) / 2) / DT_DMON)] == 2
|
((d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((s._VISION_POLICY_ALERT_3_TIMEOUT + \
|
assert events[int((d_status.settings._DISTRACTED_TIME + \
|
||||||
(TEST_TIMESPAN - 10 - s._VISION_POLICY_ALERT_3_TIMEOUT) / 2) / DT_DMON)] == 3
|
((TEST_TIMESPAN-10-d_status.settings._DISTRACTED_TIME)/2))/DT_DMON)].names[0] == EventName.driverDistracted3
|
||||||
assert isinstance(d_status.awareness, float)
|
assert isinstance(d_status.awareness, float)
|
||||||
|
|
||||||
# engaged, no face detected the whole time, no action
|
# engaged, no face detected the whole time, no action
|
||||||
def test_fully_invisible_driver(self):
|
def test_fully_invisible_driver(self):
|
||||||
alert_lvls, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
|
events, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
|
||||||
s = d_status.settings
|
assert len(events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL)/2/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int(s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT / 2 / DT_DMON)] == 0
|
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL + \
|
||||||
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT + \
|
((d_status.settings._AWARENESS_PRE_TIME_TILL_TERMINAL-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == \
|
||||||
(s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT - s._WHEELTOUCH_POLICY_ALERT_1_TIMEOUT) / 2) / DT_DMON)] == 1
|
EventName.driverUnresponsive1
|
||||||
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT + \
|
assert events[int((d_status.settings._AWARENESS_TIME-d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL + \
|
||||||
(s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT - s._WHEELTOUCH_POLICY_ALERT_2_TIMEOUT) / 2) / DT_DMON)] == 2
|
((d_status.settings._AWARENESS_PROMPT_TIME_TILL_TERMINAL)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive2
|
||||||
assert alert_lvls[int((s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT + \
|
assert events[int((d_status.settings._AWARENESS_TIME + \
|
||||||
(TEST_TIMESPAN - 10 - s._WHEELTOUCH_POLICY_ALERT_3_TIMEOUT) / 2) / DT_DMON)] == 3
|
((TEST_TIMESPAN-10-d_status.settings._AWARENESS_TIME)/2))/DT_DMON)].names[0] == EventName.driverUnresponsive3
|
||||||
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch
|
|
||||||
|
|
||||||
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
|
# engaged, down to orange, driver pays attention, back to normal; then down to orange, driver touches wheel
|
||||||
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
|
# - should have short orange recovery time and no green afterwards; wheel touch only recovers when paying attention
|
||||||
@@ -103,13 +106,13 @@ class TestMonitoring:
|
|||||||
[msg_ATTENTIVE] * (int(TEST_TIMESPAN/DT_DMON)-int((DISTRACTED_SECONDS_TO_ORANGE*3+2)/DT_DMON))
|
[msg_ATTENTIVE] * (int(TEST_TIMESPAN/DT_DMON)-int((DISTRACTED_SECONDS_TO_ORANGE*3+2)/DT_DMON))
|
||||||
interaction_vector = [car_interaction_NOT_DETECTED] * int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON) + \
|
interaction_vector = [car_interaction_NOT_DETECTED] * int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON) + \
|
||||||
[car_interaction_DETECTED] * (int(TEST_TIMESPAN/DT_DMON)-int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON))
|
[car_interaction_DETECTED] * (int(TEST_TIMESPAN/DT_DMON)-int(DISTRACTED_SECONDS_TO_ORANGE*3/DT_DMON))
|
||||||
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
|
events, _ = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
|
||||||
assert alert_lvls[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
|
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
|
assert events[int((DISTRACTED_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)] == 0
|
assert len(events[int(DISTRACTED_SECONDS_TO_ORANGE*1.5/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)] == 2
|
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3-0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)] == 2
|
assert events[int((DISTRACTED_SECONDS_TO_ORANGE*3+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)] == 0
|
assert len(events[int((DISTRACTED_SECONDS_TO_ORANGE*3+2.5)/DT_DMON)]) == 0
|
||||||
|
|
||||||
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
|
# engaged, down to orange, driver dodges camera, then comes back still distracted, down to red, \
|
||||||
# driver dodges, and then touches wheel to no avail, disengages and reengages
|
# driver dodges, and then touches wheel to no avail, disengages and reengages
|
||||||
@@ -127,11 +130,11 @@ class TestMonitoring:
|
|||||||
= [True] * int(1/DT_DMON)
|
= [True] * int(1/DT_DMON)
|
||||||
op_vector[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+2.5)/DT_DMON):int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3)/DT_DMON)] \
|
op_vector[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+2.5)/DT_DMON):int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3)/DT_DMON)] \
|
||||||
= [False] * int(0.5/DT_DMON)
|
= [False] * int(0.5/DT_DMON)
|
||||||
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
|
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)] == 2
|
assert events[int((DISTRACTED_SECONDS_TO_ORANGE+0.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)] == 3
|
assert events[int((DISTRACTED_SECONDS_TO_RED+1.5*_invisible_time)/DT_DMON)].names[0] == EventName.driverDistracted3
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)] == 3
|
assert events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+1.5)/DT_DMON)].names[0] == EventName.driverDistracted3
|
||||||
assert alert_lvls[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)] == 0
|
assert len(events[int((DISTRACTED_SECONDS_TO_RED+2*_invisible_time+3.5)/DT_DMON)]) == 0
|
||||||
|
|
||||||
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
|
# engaged, invisible driver, down to orange, driver touches wheel; then down to orange again, driver appears
|
||||||
# - both actions should clear the alert, but momentary appearance should not
|
# - both actions should clear the alert, but momentary appearance should not
|
||||||
@@ -142,16 +145,16 @@ class TestMonitoring:
|
|||||||
ds_vector[int((2*INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON):int((2*INVISIBLE_SECONDS_TO_ORANGE+1+_visible_time)/DT_DMON)] = \
|
ds_vector[int((2*INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON):int((2*INVISIBLE_SECONDS_TO_ORANGE+1+_visible_time)/DT_DMON)] = \
|
||||||
[msg_ATTENTIVE] * int(_visible_time/DT_DMON)
|
[msg_ATTENTIVE] * int(_visible_time/DT_DMON)
|
||||||
interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON)
|
interaction_vector[int((INVISIBLE_SECONDS_TO_ORANGE)/DT_DMON):int((INVISIBLE_SECONDS_TO_ORANGE+1)/DT_DMON)] = [True] * int(1/DT_DMON)
|
||||||
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
|
events, _ = self._run_seq(ds_vector, interaction_vector, 2*always_true, 2*always_false)
|
||||||
assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
|
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
|
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)] == 0
|
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE+0.1)/DT_DMON)]) == 0
|
||||||
if _visible_time == 0.5:
|
if _visible_time == 0.5:
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2
|
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 1
|
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive1
|
||||||
elif _visible_time == 10:
|
elif _visible_time == 10:
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)] == 2
|
assert events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)] == 0
|
assert len(events[int((INVISIBLE_SECONDS_TO_ORANGE*2+1+0.1+_visible_time)/DT_DMON)]) == 0
|
||||||
|
|
||||||
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
|
# engaged, invisible driver, down to red, driver appears and then touches wheel, then disengages/reengages
|
||||||
# - only disengage will clear the alert
|
# - only disengage will clear the alert
|
||||||
@@ -163,19 +166,19 @@ class TestMonitoring:
|
|||||||
ds_vector[int(INVISIBLE_SECONDS_TO_RED/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON)] = [msg_ATTENTIVE] * int(_visible_time/DT_DMON)
|
ds_vector[int(INVISIBLE_SECONDS_TO_RED/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON)] = [msg_ATTENTIVE] * int(_visible_time/DT_DMON)
|
||||||
interaction_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON)] = [True] * int(1/DT_DMON)
|
interaction_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON)] = [True] * int(1/DT_DMON)
|
||||||
op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON)
|
op_vector[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1)/DT_DMON):int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] = [False] * int(0.5/DT_DMON)
|
||||||
alert_lvls, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
|
events, _ = self._run_seq(ds_vector, interaction_vector, op_vector, always_false)
|
||||||
assert alert_lvls[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)] == 0
|
assert len(events[int(INVISIBLE_SECONDS_TO_ORANGE*0.5/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)] == 2
|
assert events[int((INVISIBLE_SECONDS_TO_ORANGE-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive2
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)] == 3
|
assert events[int((INVISIBLE_SECONDS_TO_RED-0.1)/DT_DMON)].names[0] == EventName.driverUnresponsive3
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)] == 3
|
assert events[int((INVISIBLE_SECONDS_TO_RED+0.5*_visible_time)/DT_DMON)].names[0] == EventName.driverUnresponsive3
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)] == 3
|
assert events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+0.5)/DT_DMON)].names[0] == EventName.driverUnresponsive3
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)] == 0
|
assert len(events[int((INVISIBLE_SECONDS_TO_RED+_visible_time+1+0.1)/DT_DMON)]) == 0
|
||||||
|
|
||||||
# disengaged, always distracted driver
|
# disengaged, always distracted driver
|
||||||
# - dm should stay quiet when not engaged
|
# - dm should stay quiet when not engaged
|
||||||
def test_pure_dashcam_user(self):
|
def test_pure_dashcam_user(self):
|
||||||
alert_lvls, _ = self._run_seq(always_distracted, always_false, always_false, always_false)
|
events, _ = self._run_seq(always_distracted, always_false, always_false, always_false)
|
||||||
assert all(a == 0 for a in alert_lvls)
|
assert sum(len(event) for event in events) == 0
|
||||||
|
|
||||||
# engaged, car stops at traffic light, down to orange, no action, then car starts moving
|
# engaged, car stops at traffic light, down to orange, no action, then car starts moving
|
||||||
# - should only reach green when stopped, but continues counting down on launch
|
# - should only reach green when stopped, but continues counting down on launch
|
||||||
@@ -183,12 +186,11 @@ class TestMonitoring:
|
|||||||
_redlight_time = 60 # seconds
|
_redlight_time = 60 # seconds
|
||||||
standstill_vector = always_true[:]
|
standstill_vector = always_true[:]
|
||||||
standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON)
|
standstill_vector[int(_redlight_time/DT_DMON):] = [False] * int((TEST_TIMESPAN-_redlight_time)/DT_DMON)
|
||||||
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
|
events, d_status = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
|
||||||
s = d_status.settings
|
assert len(events[int((_redlight_time-0.1)/DT_DMON)]) == 0
|
||||||
assert alert_lvls[int((_redlight_time-0.1)/DT_DMON)] == 0
|
_pre_to_prompt = d_status.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL - d_status.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL
|
||||||
_alert_1_to_2 = s._VISION_POLICY_ALERT_2_TIMEOUT - s._VISION_POLICY_ALERT_1_TIMEOUT
|
assert events[int((_redlight_time+0.5)/DT_DMON)].names[0] == EventName.driverDistracted1
|
||||||
assert alert_lvls[int((_redlight_time+0.5)/DT_DMON)] == 1
|
assert events[int((_redlight_time+_pre_to_prompt+0.5)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((_redlight_time+_alert_1_to_2+0.5)/DT_DMON)] == 2
|
|
||||||
|
|
||||||
# engaged, distracted while moving, then car stops after reaching orange
|
# engaged, distracted while moving, then car stops after reaching orange
|
||||||
# - should reset timer to pre green at standstill
|
# - should reset timer to pre green at standstill
|
||||||
@@ -196,81 +198,67 @@ class TestMonitoring:
|
|||||||
_stop_time = DISTRACTED_SECONDS_TO_ORANGE + 1 # stop 1 second after reaching orange
|
_stop_time = DISTRACTED_SECONDS_TO_ORANGE + 1 # stop 1 second after reaching orange
|
||||||
standstill_vector = always_false[:]
|
standstill_vector = always_false[:]
|
||||||
standstill_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON)
|
standstill_vector[int(_stop_time/DT_DMON):] = [True] * int((TEST_TIMESPAN-_stop_time)/DT_DMON)
|
||||||
alert_lvls, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
|
events, _ = self._run_seq(always_distracted, always_false, always_true, standstill_vector)
|
||||||
# just before and briefly after stopping: orange alert; goes away quickly after stopped
|
# just before and briefly after stopping: orange alert; goes away quickly after stopped
|
||||||
assert alert_lvls[int((_stop_time+0.1)/DT_DMON)] == 2
|
assert events[int((_stop_time+0.1)/DT_DMON)].names[0] == EventName.driverDistracted2
|
||||||
assert alert_lvls[int((_stop_time+0.5)/DT_DMON)] == 0
|
assert len(events[int((_stop_time+0.5)/DT_DMON)]) == 0
|
||||||
|
|
||||||
# engaged, model is somehow uncertain and driver is distracted
|
# engaged, model is somehow uncertain and driver is distracted
|
||||||
# - should fall back to wheel touch after uncertain alert
|
# - should fall back to wheel touch after uncertain alert
|
||||||
def test_somehow_indecisive_model(self):
|
def test_somehow_indecisive_model(self):
|
||||||
ds_vector = [msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN] * int(TEST_TIMESPAN/DT_DMON)
|
ds_vector = [msg_DISTRACTED_BUT_SOMEHOW_UNCERTAIN] * int(TEST_TIMESPAN/DT_DMON)
|
||||||
interaction_vector = always_false[:]
|
interaction_vector = always_false[:]
|
||||||
alert_lvls, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
|
events, d_status = self._run_seq(ds_vector, interaction_vector, always_true, always_false)
|
||||||
s = d_status.settings
|
assert EventName.driverUnresponsive1 in \
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*s._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)] == 1
|
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME-0.1)/DT_DMON)].names
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*s._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)] == 2
|
assert EventName.driverUnresponsive2 in \
|
||||||
assert alert_lvls[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*s._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)] == 3
|
events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
|
||||||
|
assert EventName.driverUnresponsive3 in \
|
||||||
|
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
|
def _build_sm(selfdrive_enabled, lat_active, steering_pressed, gas_pressed):
|
||||||
(False, False, False), # Both Disabled
|
|
||||||
(True, False, True), # OP Enabled, Lat Inactive
|
|
||||||
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
|
|
||||||
(True, True, True) # Both Active
|
|
||||||
])
|
|
||||||
def test_enabled_states(enabled_state, lat_active_state, expected):
|
|
||||||
"""
|
|
||||||
Test DriverMonitoring.run_step with all 4 combinations of:
|
|
||||||
- selfdriveState.enabled (True/False)
|
|
||||||
- carControl.latActive (True/False)
|
|
||||||
"""
|
|
||||||
cs = car.CarState.new_message()
|
cs = car.CarState.new_message()
|
||||||
cs.vEgo = 30.0
|
cs.vEgo = 30.0
|
||||||
cs.gearShifter = car.CarState.GearShifter.drive
|
cs.gearShifter = car.CarState.GearShifter.drive
|
||||||
cs.standstill = False
|
cs.steeringPressed = steering_pressed
|
||||||
cs.steeringPressed = False
|
cs.gasPressed = gas_pressed
|
||||||
cs.gasPressed = False
|
|
||||||
|
|
||||||
ss = log.SelfdriveState.new_message()
|
ss = log.SelfdriveState.new_message()
|
||||||
ss.enabled = enabled_state
|
ss.enabled = selfdrive_enabled
|
||||||
|
|
||||||
cc = car.CarControl.new_message()
|
cc = car.CarControl.new_message()
|
||||||
cc.latActive = lat_active_state
|
cc.latActive = lat_active
|
||||||
|
|
||||||
mv2 = log.ModelDataV2.new_message()
|
mv2 = log.ModelDataV2.new_message()
|
||||||
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
|
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
|
||||||
|
|
||||||
lc = log.LiveCalibrationData.new_message()
|
lc = log.LiveCalibrationData.new_message()
|
||||||
lc.rpyCalib = [0.0, 0.0, 0.0]
|
lc.rpyCalib = [0.0, 0.0, 0.0]
|
||||||
|
return {
|
||||||
ds = make_msg(False)
|
'carState': cs, 'selfdriveState': ss, 'carControl': cc,
|
||||||
|
'modelV2': mv2, 'liveCalibration': lc, 'driverStateV2': make_msg(False),
|
||||||
sm = {
|
|
||||||
'carState': cs,
|
|
||||||
'selfdriveState': ss,
|
|
||||||
'carControl': cc,
|
|
||||||
'modelV2': mv2,
|
|
||||||
'liveCalibration': lc,
|
|
||||||
'driverStateV2': ds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
driver_monitoring = DriverMonitoring()
|
|
||||||
|
|
||||||
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
|
@pytest.mark.parametrize("selfdrive_enabled, lat_active, steering, gas, expected_op_engaged, expected_driver_engaged", [
|
||||||
captured_args = []
|
(False, False, False, False, False, False), # disabled
|
||||||
original_update_events = driver_monitoring._update_events
|
(True, False, False, False, True, False), # OP enabled
|
||||||
|
(False, True, False, False, True, False), # MADS lat-only
|
||||||
|
(True, True, False, False, True, False), # both active
|
||||||
|
(False, True, False, True, True, False), # MADS lat-only + gas
|
||||||
|
(True, True, False, True, True, True), # full op + gas: override
|
||||||
|
(False, True, True, False, True, True), # MADS lat-only + wheel touch: override
|
||||||
|
])
|
||||||
|
def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas,
|
||||||
|
expected_op_engaged, expected_driver_engaged):
|
||||||
|
sm = _build_sm(selfdrive_enabled, lat_active, steering, gas)
|
||||||
|
dm = DriverMonitoring()
|
||||||
|
captured = {}
|
||||||
|
orig = dm._update_events
|
||||||
|
|
||||||
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear):
|
def spy(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||||
captured_args.append(op_engaged)
|
captured['driver_engaged'] = driver_engaged
|
||||||
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear)
|
captured['op_engaged'] = op_engaged
|
||||||
|
return orig(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
|
||||||
|
|
||||||
driver_monitoring._update_events = spy_update_events
|
dm._update_events = spy
|
||||||
|
dm.run_step(sm, demo=False)
|
||||||
driver_monitoring.run_step(sm, demo=False)
|
assert captured['op_engaged'] == expected_op_engaged
|
||||||
|
assert captured['driver_engaged'] == expected_driver_engaged
|
||||||
# Assertion
|
|
||||||
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
|
|
||||||
actual_enabled = captured_args[0]
|
|
||||||
|
|
||||||
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
|
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ LaneChangeDirection = log.LaneChangeDirection
|
|||||||
EventName = log.OnroadEvent.EventName
|
EventName = log.OnroadEvent.EventName
|
||||||
ButtonType = car.CarState.ButtonEvent.Type
|
ButtonType = car.CarState.ButtonEvent.Type
|
||||||
SafetyModel = car.CarParams.SafetyModel
|
SafetyModel = car.CarParams.SafetyModel
|
||||||
AlertLevel = log.DriverMonitoringState.AlertLevel
|
|
||||||
MonitoringPolicy = log.DriverMonitoringState.MonitoringPolicy
|
|
||||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||||
|
|
||||||
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
|
IGNORED_SAFETY_MODES = (SafetyModel.silent, SafetyModel.noOutput)
|
||||||
@@ -142,8 +140,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.params
|
self.params
|
||||||
)
|
)
|
||||||
self.recalibrating_seen = False
|
self.recalibrating_seen = False
|
||||||
self.dm_lockout_set = False
|
|
||||||
self.dm_uncertain_alerted = False
|
|
||||||
self.state_machine = StateMachine()
|
self.state_machine = StateMachine()
|
||||||
self.rk = Ratekeeper(100, print_delay_threshold=None)
|
self.rk = Ratekeeper(100, print_delay_threshold=None)
|
||||||
|
|
||||||
@@ -220,27 +216,8 @@ class SelfdriveD(CruiseHelper):
|
|||||||
if not self.CP.pcmCruise and CS.vCruise > 250 and resume_pressed:
|
if not self.CP.pcmCruise and CS.vCruise > 250 and resume_pressed:
|
||||||
self.events.add(EventName.resumeBlocked)
|
self.events.add(EventName.resumeBlocked)
|
||||||
|
|
||||||
# Handle DM
|
|
||||||
if not self.CP.notCar:
|
if not self.CP.notCar:
|
||||||
# Block engaging until ignition cycle after max number or time of distractions
|
self.events.add_from_msg(self.sm['driverMonitoringState'].events)
|
||||||
if self.sm['driverMonitoringState'].lockout and not self.dm_lockout_set:
|
|
||||||
self.params.put_bool_nonblocking("DriverTooDistracted", True)
|
|
||||||
self.dm_lockout_set = True
|
|
||||||
# No entry conditions
|
|
||||||
if self.sm['driverMonitoringState'].lockout or self.sm['driverMonitoringState'].alwaysOnLockout:
|
|
||||||
self.events.add(EventName.tooDistracted)
|
|
||||||
# Alerts
|
|
||||||
vision_dm = self.sm['driverMonitoringState'].activePolicy == MonitoringPolicy.vision
|
|
||||||
if self.sm['driverMonitoringState'].alertLevel == AlertLevel.one:
|
|
||||||
self.events.add(EventName.driverDistracted1 if vision_dm else EventName.driverUnresponsive1)
|
|
||||||
elif self.sm['driverMonitoringState'].alertLevel == AlertLevel.two:
|
|
||||||
self.events.add(EventName.driverDistracted2 if vision_dm else EventName.driverUnresponsive2)
|
|
||||||
elif self.sm['driverMonitoringState'].alertLevel == AlertLevel.three:
|
|
||||||
self.events.add(EventName.driverDistracted3 if vision_dm else EventName.driverUnresponsive3)
|
|
||||||
# Warn consistent DM uncertainty
|
|
||||||
if self.sm['driverMonitoringState'].visionPolicyState.uncertainOffroadAlertPercent >= 100 and not self.dm_uncertain_alerted:
|
|
||||||
set_offroad_alert("Offroad_DriverMonitoringUncertain", True)
|
|
||||||
self.dm_uncertain_alerted = True
|
|
||||||
self.events_sp.add_from_msg(self.sm['longitudinalPlanSP'].events)
|
self.events_sp.add_from_msg(self.sm['longitudinalPlanSP'].events)
|
||||||
|
|
||||||
# Add car events, ignore if CAN isn't valid
|
# Add car events, ignore if CAN isn't valid
|
||||||
@@ -264,7 +241,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.events.add(EventName.pedalPressed)
|
self.events.add(EventName.pedalPressed)
|
||||||
|
|
||||||
# Create events for temperature, disk space, and memory
|
# Create events for temperature, disk space, and memory
|
||||||
if self.sm['deviceState'].thermalStatus >= ThermalStatus.overheated:
|
if self.sm['deviceState'].thermalStatus >= ThermalStatus.red:
|
||||||
self.events.add(EventName.overheat)
|
self.events.add(EventName.overheat)
|
||||||
if self.sm['deviceState'].freeSpacePercent < 7 and not SIMULATION:
|
if self.sm['deviceState'].freeSpacePercent < 7 and not SIMULATION:
|
||||||
self.events.add(EventName.outOfSpace)
|
self.events.add(EventName.outOfSpace)
|
||||||
|
|||||||
@@ -449,6 +449,9 @@ def migrate_sensorEvents(msgs):
|
|||||||
m.logMonoTime = msg.logMonoTime
|
m.logMonoTime = msg.logMonoTime
|
||||||
|
|
||||||
m_dat = getattr(m, sensor_service)
|
m_dat = getattr(m, sensor_service)
|
||||||
|
m_dat.version = evt.version
|
||||||
|
m_dat.sensor = evt.sensor
|
||||||
|
m_dat.type = evt.type
|
||||||
m_dat.source = evt.source
|
m_dat.source = evt.source
|
||||||
m_dat.timestamp = evt.timestamp
|
m_dat.timestamp = evt.timestamp
|
||||||
setattr(m_dat, evt.which(), getattr(evt, evt.which()))
|
setattr(m_dat, evt.which(), getattr(evt, evt.which()))
|
||||||
@@ -481,41 +484,22 @@ def migrate_onroadEvents(msgs):
|
|||||||
return ops, [], []
|
return ops, [], []
|
||||||
|
|
||||||
|
|
||||||
@migration(inputs=["driverMonitoringStateDEPRECATED"])
|
@migration(inputs=["driverMonitoringState"])
|
||||||
def migrate_driverMonitoringState(msgs):
|
def migrate_driverMonitoringState(msgs):
|
||||||
ops = []
|
ops = []
|
||||||
for index, msg in msgs:
|
for index, msg in msgs:
|
||||||
old = msg.driverMonitoringStateDEPRECATED
|
msg = msg.as_builder()
|
||||||
new_msg = messaging.new_message('driverMonitoringState', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
events = []
|
||||||
dm = new_msg.driverMonitoringState
|
for event in msg.driverMonitoringState.deprecated.events:
|
||||||
dm.isRHD = old.isRHD
|
try:
|
||||||
dm.activePolicy = log.DriverMonitoringState.MonitoringPolicy.vision if old.isActiveMode else \
|
if not str(event.name).endswith('DEPRECATED'):
|
||||||
log.DriverMonitoringState.MonitoringPolicy.wheeltouch
|
migrated_event = migrate_onroad_event(event)
|
||||||
|
if migrated_event is not None:
|
||||||
|
events.append(migrated_event)
|
||||||
|
except RuntimeError: # Member was null
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
AlertLevel = log.DriverMonitoringState.AlertLevel
|
msg.driverMonitoringState.events = events
|
||||||
event_to_alert_level = {
|
ops.append((index, msg.as_reader()))
|
||||||
'driverDistracted1': AlertLevel.one, 'driverUnresponsive1': AlertLevel.one,
|
|
||||||
'driverDistracted2': AlertLevel.two, 'driverUnresponsive2': AlertLevel.two,
|
|
||||||
'driverDistracted3': AlertLevel.three, 'driverUnresponsive3': AlertLevel.three,
|
|
||||||
'tooDistracted': AlertLevel.three,
|
|
||||||
}
|
|
||||||
for event in old.events:
|
|
||||||
level = event_to_alert_level.get(str(event.name))
|
|
||||||
if level is not None:
|
|
||||||
dm.alertLevel = level
|
|
||||||
break
|
|
||||||
|
|
||||||
dm.visionPolicyState.awarenessPercent = int(max(0, min(100, (old.awarenessStatus if old.isActiveMode else old.awarenessActive) * 100)))
|
|
||||||
dm.visionPolicyState.awarenessStep = old.stepChange if old.isActiveMode else 0.
|
|
||||||
dm.visionPolicyState.isDistracted = old.isDistracted
|
|
||||||
dm.visionPolicyState.faceDetected = old.faceDetected
|
|
||||||
dm.visionPolicyState.pose.pitchCalib.offset = old.posePitchOffset
|
|
||||||
dm.visionPolicyState.pose.pitchCalib.calibratedPercent = int(min(100, old.posePitchValidCount / 600 * 100))
|
|
||||||
dm.visionPolicyState.pose.yawCalib.offset = old.poseYawOffset
|
|
||||||
dm.visionPolicyState.pose.yawCalib.calibratedPercent = int(min(100, old.poseYawValidCount / 600 * 100))
|
|
||||||
dm.visionPolicyState.pose.calibrated = old.posePitchValidCount >= 600 and old.poseYawValidCount >= 600
|
|
||||||
dm.wheeltouchPolicyState.awarenessPercent = int(max(0, min(100, (old.awarenessPassive if old.isActiveMode else old.awarenessStatus) * 100)))
|
|
||||||
dm.wheeltouchPolicyState.awarenessStep = 0. if old.isActiveMode else old.stepChange
|
|
||||||
ops.append((index, new_msg.as_reader()))
|
|
||||||
|
|
||||||
return ops, [], []
|
return ops, [], []
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
|||||||
EXEC_TIMINGS = [
|
EXEC_TIMINGS = [
|
||||||
# model, instant max, average max
|
# model, instant max, average max
|
||||||
("modelV2", 0.05, 0.028),
|
("modelV2", 0.05, 0.028),
|
||||||
("driverStateV2", 0.05, 0.018),
|
("driverStateV2", 0.05, 0.016),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_log_fn(test_route, ref="master"):
|
def get_log_fn(test_route, ref="master"):
|
||||||
@@ -76,7 +76,7 @@ def generate_report(proposed, master, tmp, commit):
|
|||||||
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.eyesClosedProb), "leftDriverData.eyesClosedProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.leftBlinkProb), "leftDriverData.leftBlinkProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
||||||
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
||||||
], "driverStateV2")
|
], "driverStateV2")
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class AnimationMode(Enum):
|
|
||||||
ONCE_FORWARD = 1
|
|
||||||
ONCE_FORWARD_BACKWARD = 2
|
|
||||||
REPEAT_FORWARD = 3
|
|
||||||
REPEAT_FORWARD_BACKWARD = 4
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Animation:
|
|
||||||
frames: list[list[tuple[int, int]]]
|
|
||||||
starting_frames: list[list[tuple[int, int]]] | None = None # played once before the main loop
|
|
||||||
frame_duration: float = 0.15 # seconds each frame is shown
|
|
||||||
mode: AnimationMode = AnimationMode.REPEAT_FORWARD_BACKWARD
|
|
||||||
repeat_interval: float = 5.0 # seconds between animation restarts (only for REPEAT modes)
|
|
||||||
hold_end: float = 0.0 # seconds to hold the last frame before playing backward (only for *_BACKWARD modes)
|
|
||||||
left_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning left
|
|
||||||
right_turn_remove: list[tuple[int, int]] | None = None # dots to remove from frame when turning right
|
|
||||||
|
|
||||||
|
|
||||||
# --- Animation Helper Functions ---
|
|
||||||
|
|
||||||
def _mirror(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
||||||
"""Mirror a component from the left side of the face to the right"""
|
|
||||||
return [(r, 15 - c) for r, c in dots]
|
|
||||||
|
|
||||||
|
|
||||||
def _mirror_no_flip(dots: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
||||||
"""Move a component to the mirrored position on the right half without flipping its shape."""
|
|
||||||
min_c = min(c for _, c in dots)
|
|
||||||
max_c = max(c for _, c in dots)
|
|
||||||
return [(r, 15 - max_c - min_c + c) for r, c in dots]
|
|
||||||
|
|
||||||
|
|
||||||
def _shift(dots: list[tuple[int, int]], rc: tuple[int, int]) -> list[tuple[int, int]]:
|
|
||||||
dr, dc = rc
|
|
||||||
return [(r + dr, c + dc) for r, c in dots]
|
|
||||||
|
|
||||||
|
|
||||||
def _make_frame(left_eye: list[tuple[int, int]], right_eye: list[tuple[int, int]],
|
|
||||||
left_brow: list[tuple[int, int]], right_brow: list[tuple[int, int]],
|
|
||||||
mouth: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
||||||
return left_eye + left_brow + right_eye + right_brow + mouth
|
|
||||||
|
|
||||||
|
|
||||||
# --- Animation Helper Components ---
|
|
||||||
|
|
||||||
# Eyes (left side)
|
|
||||||
EYE_OPEN = [
|
|
||||||
(2, 2), (2, 3),
|
|
||||||
(3, 1), (3, 2), (3, 3), (3, 4),
|
|
||||||
(4, 1), (4, 2), (4, 3), (4, 4),
|
|
||||||
(5, 2), (5, 3)
|
|
||||||
]
|
|
||||||
EYE_HALF = [
|
|
||||||
(4, 1), (4, 2), (4, 3), (4, 4),
|
|
||||||
(5, 2), (5, 3)
|
|
||||||
]
|
|
||||||
EYE_CLOSED = [
|
|
||||||
(4, 1), (4, 4),
|
|
||||||
(5, 2), (5, 3),
|
|
||||||
]
|
|
||||||
EYE_LEFT_LOOK = [
|
|
||||||
(2, 2), (2, 3),
|
|
||||||
(3, 1), (3, 2),
|
|
||||||
(4, 1), (4, 2),
|
|
||||||
(5, 2), (5, 3),
|
|
||||||
]
|
|
||||||
EYE_RIGHT_LOOK = [
|
|
||||||
(2, 2), (2, 3),
|
|
||||||
(3, 3), (3, 4),
|
|
||||||
(4, 3), (4, 4),
|
|
||||||
(5, 2), (5, 3),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Eyebrows (left side)
|
|
||||||
BROW_HIGH = [
|
|
||||||
(0, 1), (0, 2),
|
|
||||||
(1, 0),
|
|
||||||
]
|
|
||||||
BROW_LOWERED = [
|
|
||||||
(1, 1), (1, 2),
|
|
||||||
(2, 0)
|
|
||||||
]
|
|
||||||
BROW_STRAIGHT = [(1, 0), (1, 1), (1, 2)]
|
|
||||||
BROW_DOWN = [
|
|
||||||
(0, 1), (0, 2),
|
|
||||||
(1, 3)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Mouths (centered, not mirrored)
|
|
||||||
MOUTH_SMILE = [
|
|
||||||
(6, 6), (6, 9),
|
|
||||||
(7, 7), (7, 8),
|
|
||||||
]
|
|
||||||
MOUTH_NORMAL = [(7, 7), (7, 8)]
|
|
||||||
MOUTH_SAD = [
|
|
||||||
(6, 7), (6, 8),
|
|
||||||
(7, 6), (7, 9)
|
|
||||||
]
|
|
||||||
|
|
||||||
# --- Animations ---
|
|
||||||
|
|
||||||
NORMAL = Animation(
|
|
||||||
frames=[
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(EYE_HALF, _mirror(EYE_HALF), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), BROW_LOWERED, _mirror(BROW_LOWERED), MOUTH_SMILE),
|
|
||||||
],
|
|
||||||
left_turn_remove=[
|
|
||||||
(3, 3), (3, 4),
|
|
||||||
(4, 3), (4, 4),
|
|
||||||
] + _mirror_no_flip([
|
|
||||||
(3, 1), (3, 2),
|
|
||||||
(4, 1), (4, 2),
|
|
||||||
]),
|
|
||||||
right_turn_remove=[
|
|
||||||
(3, 1), (3, 2),
|
|
||||||
(4, 1), (4, 2),
|
|
||||||
] + _mirror_no_flip([
|
|
||||||
(3, 3), (3, 4),
|
|
||||||
(4, 3), (4, 4),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
ASLEEP = Animation(
|
|
||||||
frames=[
|
|
||||||
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), [], [], MOUTH_NORMAL),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
SLEEPY = Animation(
|
|
||||||
frames=[
|
|
||||||
_make_frame(EYE_CLOSED, _mirror(EYE_CLOSED), _shift(BROW_STRAIGHT, (1, 0)), [], MOUTH_NORMAL),
|
|
||||||
_make_frame(EYE_HALF, _mirror(EYE_CLOSED), BROW_LOWERED, [], MOUTH_NORMAL),
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, [], MOUTH_NORMAL)
|
|
||||||
],
|
|
||||||
frame_duration=0.25,
|
|
||||||
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
|
|
||||||
repeat_interval=10,
|
|
||||||
hold_end=1.5,
|
|
||||||
)
|
|
||||||
|
|
||||||
INQUISITIVE = Animation(
|
|
||||||
frames=[
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
|
|
||||||
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_LEFT_LOOK, (0, -1)), _shift(_mirror(EYE_RIGHT_LOOK), (0, -1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(EYE_LEFT_LOOK, _mirror(EYE_RIGHT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
|
|
||||||
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(_shift(EYE_RIGHT_LOOK, (0, 1)), _shift(_mirror(EYE_LEFT_LOOK), (0, 1)), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(EYE_RIGHT_LOOK, _mirror(EYE_LEFT_LOOK), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
],
|
|
||||||
mode=AnimationMode.REPEAT_FORWARD,
|
|
||||||
frame_duration=0.15,
|
|
||||||
repeat_interval=10
|
|
||||||
)
|
|
||||||
|
|
||||||
WINK = Animation(
|
|
||||||
frames=[
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_OPEN), BROW_HIGH, _mirror(BROW_HIGH), MOUTH_SMILE),
|
|
||||||
_make_frame(EYE_OPEN, _mirror(EYE_CLOSED), BROW_HIGH, _mirror(_shift(BROW_DOWN, (0, 2))), MOUTH_SMILE),
|
|
||||||
],
|
|
||||||
mode=AnimationMode.ONCE_FORWARD_BACKWARD,
|
|
||||||
frame_duration=0.75,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Face Animator Class ---
|
|
||||||
|
|
||||||
class FaceAnimator:
|
|
||||||
def __init__(self, animation: Animation):
|
|
||||||
self._animation = animation
|
|
||||||
self._next: Animation | None = None
|
|
||||||
self._start_time = time.monotonic()
|
|
||||||
self._rewinding = False
|
|
||||||
self._rewind_start: float = 0.0
|
|
||||||
self._rewind_from: int = 0
|
|
||||||
self._seen_nonzero = False
|
|
||||||
|
|
||||||
def set_animation(self, animation: Animation):
|
|
||||||
if animation is not self._animation:
|
|
||||||
self._next = animation
|
|
||||||
|
|
||||||
def get_dots(self) -> list[tuple[int, int]]:
|
|
||||||
now = time.monotonic()
|
|
||||||
elapsed = now - self._start_time
|
|
||||||
|
|
||||||
# Handle rewind for forward-only animations
|
|
||||||
if self._rewinding:
|
|
||||||
rewind_elapsed = now - self._rewind_start
|
|
||||||
frames_back = round(rewind_elapsed / self._animation.frame_duration)
|
|
||||||
frame_index = self._rewind_from - frames_back
|
|
||||||
if frame_index <= 0:
|
|
||||||
return self._switch_to_next(now)
|
|
||||||
return self._animation.frames[frame_index]
|
|
||||||
|
|
||||||
# Play starting frames first (once)
|
|
||||||
starting = self._animation.starting_frames or []
|
|
||||||
starting_duration = len(starting) * self._animation.frame_duration
|
|
||||||
if starting and elapsed < starting_duration:
|
|
||||||
frame_index = min(int(elapsed / self._animation.frame_duration), len(starting) - 1)
|
|
||||||
return starting[frame_index]
|
|
||||||
|
|
||||||
# Main loop
|
|
||||||
loop_elapsed = elapsed - starting_duration if starting else elapsed
|
|
||||||
frame_index = _get_frame_index(self._animation, loop_elapsed, gap_first=bool(starting))
|
|
||||||
|
|
||||||
if frame_index != 0:
|
|
||||||
self._seen_nonzero = True
|
|
||||||
|
|
||||||
if self._next is not None:
|
|
||||||
if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero):
|
|
||||||
return self._switch_to_next(now)
|
|
||||||
# No natural return to frame 0 — start rewinding
|
|
||||||
if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD):
|
|
||||||
self._rewinding = True
|
|
||||||
self._rewind_start = now
|
|
||||||
self._rewind_from = frame_index
|
|
||||||
|
|
||||||
return self._animation.frames[frame_index]
|
|
||||||
|
|
||||||
def _switch_to_next(self, now: float) -> list[tuple[int, int]]:
|
|
||||||
self._animation = self._next
|
|
||||||
self._next = None
|
|
||||||
self._rewinding = False
|
|
||||||
self._seen_nonzero = False
|
|
||||||
self._start_time = now
|
|
||||||
return self._animation.frames[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_frame_index(animation: Animation, elapsed: float, gap_first: bool = False) -> int:
|
|
||||||
"""Get the current frame index given elapsed time and animation mode."""
|
|
||||||
num_frames = len(animation.frames)
|
|
||||||
if num_frames == 1:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
fd = animation.frame_duration
|
|
||||||
has_backward = animation.mode in (AnimationMode.ONCE_FORWARD_BACKWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
|
|
||||||
repeats = animation.mode in (AnimationMode.REPEAT_FORWARD, AnimationMode.REPEAT_FORWARD_BACKWARD)
|
|
||||||
|
|
||||||
forward_duration = num_frames * fd
|
|
||||||
backward_frames = max(num_frames - 2, 0) if has_backward else 0
|
|
||||||
hold = animation.hold_end if has_backward else 0.0
|
|
||||||
cycle_duration = forward_duration + hold + backward_frames * fd
|
|
||||||
|
|
||||||
if not repeats:
|
|
||||||
t = min(elapsed, cycle_duration)
|
|
||||||
else:
|
|
||||||
t = (elapsed + cycle_duration if gap_first else elapsed) % animation.repeat_interval
|
|
||||||
|
|
||||||
# Forward phase
|
|
||||||
if t < forward_duration:
|
|
||||||
return min(int(t / fd), num_frames - 1)
|
|
||||||
t -= forward_duration
|
|
||||||
|
|
||||||
# Hold at last frame
|
|
||||||
if t < hold:
|
|
||||||
return num_frames - 1
|
|
||||||
t -= hold
|
|
||||||
|
|
||||||
# Backward phase
|
|
||||||
if backward_frames and t < backward_frames * fd:
|
|
||||||
return num_frames - 2 - min(int(t / fd), backward_frames - 1)
|
|
||||||
|
|
||||||
return 0 if has_backward else num_frames - 1
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import time
|
|
||||||
import pyray as rl
|
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
|
||||||
from openpilot.system.ui.widgets import Widget
|
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
|
||||||
from openpilot.selfdrive.ui.body.animations import FaceAnimator, ASLEEP, INQUISITIVE, NORMAL, SLEEPY
|
|
||||||
|
|
||||||
GRID_COLS = 16
|
|
||||||
GRID_ROWS = 8
|
|
||||||
DOT_RADIUS = 50 if gui_app.big_ui() else 10
|
|
||||||
|
|
||||||
IDLE_TIMEOUT = 30.0 # seconds of no joystick input before playing INQUISITIVE
|
|
||||||
IDLE_STEER_THRESH = 0.5 # degrees — below this counts as no input
|
|
||||||
IDLE_SPEED_THRESH = 0.01 # m/s — below this counts as no input
|
|
||||||
|
|
||||||
|
|
||||||
# This class is used both in BIG (tizi) and small (mici) UIs
|
|
||||||
class BodyLayout(Widget):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self._animator = FaceAnimator(ASLEEP)
|
|
||||||
self._turning_left = False
|
|
||||||
self._turning_right = False
|
|
||||||
self._last_input_time = time.monotonic()
|
|
||||||
self._was_active = False
|
|
||||||
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
grid_w = (GRID_COLS - 1) * spacing
|
|
||||||
grid_h = (GRID_ROWS - 1) * spacing
|
|
||||||
|
|
||||||
offset_x = rect.x + (rect.width - grid_w) / 2
|
|
||||||
offset_y = rect.y + (rect.height - grid_h) / 2
|
|
||||||
|
|
||||||
for row, col in dots:
|
|
||||||
x = int(offset_x + col * spacing)
|
|
||||||
y = int(offset_y + row * spacing)
|
|
||||||
rl.draw_circle(x, y, DOT_RADIUS, color)
|
|
||||||
|
|
||||||
def _update_state(self):
|
|
||||||
super()._update_state()
|
|
||||||
|
|
||||||
sm = ui_state.sm
|
|
||||||
|
|
||||||
if ui_state.is_onroad():
|
|
||||||
if not self._was_active:
|
|
||||||
self._last_input_time = time.monotonic()
|
|
||||||
self._was_active = True
|
|
||||||
|
|
||||||
cs = sm['carState']
|
|
||||||
has_input = abs(cs.steeringAngleDeg) > IDLE_STEER_THRESH or abs(cs.vEgo) > IDLE_SPEED_THRESH
|
|
||||||
if has_input:
|
|
||||||
self._last_input_time = time.monotonic()
|
|
||||||
|
|
||||||
if time.monotonic() - self._last_input_time > IDLE_TIMEOUT:
|
|
||||||
self._animator.set_animation(INQUISITIVE)
|
|
||||||
else:
|
|
||||||
self._animator.set_animation(NORMAL)
|
|
||||||
else:
|
|
||||||
self._was_active = False
|
|
||||||
self._animator.set_animation(ASLEEP)
|
|
||||||
|
|
||||||
steer = sm['testJoystick'].axes[1] if len(sm['testJoystick'].axes) > 1 else 0
|
|
||||||
self._turning_left = steer <= -0.05
|
|
||||||
self._turning_right = steer >= 0.05
|
|
||||||
|
|
||||||
# play animation on screen tap
|
|
||||||
def _handle_mouse_release(self, mouse_pos):
|
|
||||||
super()._handle_mouse_release(mouse_pos)
|
|
||||||
if not self._was_active:
|
|
||||||
self._animator.set_animation(SLEEPY)
|
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
|
||||||
dots = self._animator.get_dots()
|
|
||||||
animation = self._animator._animation
|
|
||||||
if self._turning_left and animation.left_turn_remove:
|
|
||||||
remove_set = set(animation.left_turn_remove)
|
|
||||||
dots = [d for d in dots if d not in remove_set]
|
|
||||||
elif self._turning_right and animation.right_turn_remove:
|
|
||||||
remove_set = set(animation.right_turn_remove)
|
|
||||||
dots = [d for d in dots if d not in remove_set]
|
|
||||||
self.draw_dot_grid(rect, dots, rl.WHITE)
|
|
||||||
|
|
||||||
if ui_state.is_offroad():
|
|
||||||
rl.draw_rectangle(int(self.rect.x), int(self.rect.y), int(self.rect.width), int(self.rect.height), rl.Color(0, 0, 0, 175))
|
|
||||||
upper_half = rl.Rectangle(rect.x, rect.y, rect.width, rect.height / 2)
|
|
||||||
self._offroad_label.render(upper_half)
|
|
||||||
@@ -2,14 +2,13 @@ import pyray as rl
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
|
||||||
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout
|
||||||
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType
|
||||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
|
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow
|
||||||
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
|
||||||
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
|
||||||
@@ -32,9 +31,7 @@ class MainLayout(Widget):
|
|||||||
self._prev_onroad = False
|
self._prev_onroad = False
|
||||||
|
|
||||||
# Initialize layouts
|
# Initialize layouts
|
||||||
self._home_layout = HomeLayout()
|
self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
||||||
self._home_body_layout = BodyLayout()
|
|
||||||
self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
|
|
||||||
|
|
||||||
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
|
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
|
||||||
self._content_rect = rl.Rectangle(0, 0, 0, 0)
|
self._content_rect = rl.Rectangle(0, 0, 0, 0)
|
||||||
@@ -60,18 +57,14 @@ class MainLayout(Widget):
|
|||||||
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE))
|
||||||
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
|
self._layouts[MainState.HOME].set_settings_callback(lambda: self.open_settings(PanelType.TOGGLES))
|
||||||
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state)
|
||||||
|
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)
|
||||||
for layout in (self._layouts[MainState.ONROAD], self._home_body_layout):
|
|
||||||
layout.set_click_callback(self._on_onroad_clicked)
|
|
||||||
|
|
||||||
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
device.add_interactive_timeout_callback(self._set_mode_for_state)
|
||||||
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
|
|
||||||
|
|
||||||
def _update_layout_rects(self):
|
def _update_layout_rects(self):
|
||||||
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
self._sidebar_rect = rl.Rectangle(self._rect.x, self._rect.y, SIDEBAR_WIDTH, self._rect.height)
|
||||||
|
|
||||||
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
x_offset = SIDEBAR_WIDTH if self._sidebar.is_visible else 0
|
||||||
self._content_rect = rl.Rectangle(self._rect.x + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
|
self._content_rect = rl.Rectangle(self._rect.y + x_offset, self._rect.y, self._rect.width - x_offset, self._rect.height)
|
||||||
|
|
||||||
def _handle_onroad_transition(self):
|
def _handle_onroad_transition(self):
|
||||||
if ui_state.started != self._prev_onroad:
|
if ui_state.started != self._prev_onroad:
|
||||||
@@ -80,12 +73,6 @@ class MainLayout(Widget):
|
|||||||
self._set_mode_for_state()
|
self._set_mode_for_state()
|
||||||
|
|
||||||
def _set_mode_for_state(self):
|
def _set_mode_for_state(self):
|
||||||
# Don't go onroad if body, home is onroad
|
|
||||||
if ui_state.is_body:
|
|
||||||
self._set_current_layout(MainState.HOME)
|
|
||||||
self._sidebar.set_visible(not ui_state.ignition)
|
|
||||||
return
|
|
||||||
|
|
||||||
if ui_state.started:
|
if ui_state.started:
|
||||||
# Don't hide sidebar from interactive timeout
|
# Don't hide sidebar from interactive timeout
|
||||||
if self._current_mode != MainState.ONROAD:
|
if self._current_mode != MainState.ONROAD:
|
||||||
@@ -117,10 +104,6 @@ class MainLayout(Widget):
|
|||||||
def _on_onroad_clicked(self):
|
def _on_onroad_clicked(self):
|
||||||
self._sidebar.set_visible(not self._sidebar.is_visible)
|
self._sidebar.set_visible(not self._sidebar.is_visible)
|
||||||
|
|
||||||
def _on_body_changed(self):
|
|
||||||
self._layouts[MainState.HOME] = self._home_body_layout if ui_state.is_body else self._home_layout
|
|
||||||
self._set_mode_for_state()
|
|
||||||
|
|
||||||
def _render_main_content(self):
|
def _render_main_content(self):
|
||||||
# Render sidebar
|
# Render sidebar
|
||||||
if self._sidebar.is_visible:
|
if self._sidebar.is_visible:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DeveloperLayout(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||||
|
|
||||||
# Build items and keep references for callbacks/state updates
|
# Build items and keep references for callbacks/state updates
|
||||||
self._adb_toggle = toggle_item(
|
self._adb_toggle = toggle_item(
|
||||||
@@ -135,6 +135,12 @@ class DeveloperLayout(Widget):
|
|||||||
|
|
||||||
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||||
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
|
self._long_maneuver_toggle.action_item.set_enabled(long_man_enabled)
|
||||||
|
if not long_man_enabled:
|
||||||
|
self._long_maneuver_toggle.action_item.set_state(False)
|
||||||
|
self._params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
|
||||||
|
lat_man_enabled = ui_state.is_offroad()
|
||||||
|
self._lat_maneuver_toggle.action_item.set_enabled(lat_man_enabled)
|
||||||
else:
|
else:
|
||||||
self._long_maneuver_toggle.action_item.set_enabled(False)
|
self._long_maneuver_toggle.action_item.set_enabled(False)
|
||||||
self._lat_maneuver_toggle.action_item.set_enabled(False)
|
self._lat_maneuver_toggle.action_item.set_enabled(False)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
from openpilot.system.ui.lib.multilang import tr, trn, tr_noop
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
@@ -65,13 +65,12 @@ class FirehoseLayout(FirehoseLayoutBase):
|
|||||||
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
y = self._draw_wrapped_text(x, y, w, status_text, gui_app.font(FontWeight.BOLD), 60, status_color)
|
||||||
y += 20 + 20
|
y += 20 + 20
|
||||||
|
|
||||||
# TODO: add back once reliable
|
|
||||||
# Contribution count (if available)
|
# Contribution count (if available)
|
||||||
#if self._segment_count > 0:
|
if self._segment_count > 0:
|
||||||
# contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
contrib_text = trn("{} segment of your driving is in the training dataset so far.",
|
||||||
# "{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
|
"{} segments of your driving is in the training dataset so far.", self._segment_count).format(self._segment_count)
|
||||||
# y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
y = self._draw_wrapped_text(x, y, w, contrib_text, gui_app.font(FontWeight.BOLD), 52, rl.WHITE)
|
||||||
# y += 20 + 20
|
y += 20 + 20
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
rl.draw_rectangle(x, y, w, 2, self.GRAY)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TogglesLayout(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||||
|
|
||||||
# param, title, desc, icon, needs_restart
|
# param, title, desc, icon, needs_restart
|
||||||
self._toggle_defs = {
|
self._toggle_defs = {
|
||||||
|
|||||||
@@ -125,8 +125,10 @@ class Sidebar(Widget, SidebarSP):
|
|||||||
def _update_temperature_status(self, device_state):
|
def _update_temperature_status(self, device_state):
|
||||||
thermal_status = device_state.thermalStatus
|
thermal_status = device_state.thermalStatus
|
||||||
|
|
||||||
if thermal_status == ThermalStatus.ok:
|
if thermal_status == ThermalStatus.green:
|
||||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
|
||||||
|
elif thermal_status == ThermalStatus.yellow:
|
||||||
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("OK"), Colors.WARNING)
|
||||||
else:
|
else:
|
||||||
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
|
self._temp_status.update(tr_noop("TEMP"), tr_noop("HIGH"), Colors.DANGER)
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ class MiciHomeLayout(Widget):
|
|||||||
|
|
||||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||||
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._alerts_pill = AlertsPill()
|
self._alerts_pill = AlertsPill()
|
||||||
|
|
||||||
@@ -138,7 +137,6 @@ 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._body_icon,
|
|
||||||
self._mic_icon,
|
self._mic_icon,
|
||||||
], spacing=18)
|
], spacing=18)
|
||||||
|
|
||||||
@@ -249,7 +247,6 @@ class MiciHomeLayout(Widget):
|
|||||||
# ***** Center-aligned bottom section icons *****
|
# ***** Center-aligned bottom section icons *****
|
||||||
self._experimental_icon.set_visible(self._experimental_mode)
|
self._experimental_icon.set_visible(self._experimental_mode)
|
||||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||||
self._body_icon.set_visible(ui_state.is_body)
|
|
||||||
|
|
||||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||||
self._status_bar_layout.render(footer_rect)
|
self._status_bar_layout.render(footer_rect)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
|||||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
|
from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow
|
||||||
from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout
|
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
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
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
@@ -32,25 +31,22 @@ class MiciMainLayout(Scroller):
|
|||||||
self._home_layout = MiciHomeLayout()
|
self._home_layout = MiciHomeLayout()
|
||||||
self._alerts_layout = MiciOffroadAlerts()
|
self._alerts_layout = MiciOffroadAlerts()
|
||||||
self._settings_layout = SettingsLayout()
|
self._settings_layout = SettingsLayout()
|
||||||
self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
|
self._onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked)
|
||||||
self._body_onroad_layout = BodyLayout()
|
|
||||||
|
|
||||||
# Initialize widget rects
|
# Initialize widget rects
|
||||||
for widget in (self._home_layout, self._alerts_layout, self._settings_layout,
|
for widget in (self._home_layout, self._settings_layout, self._alerts_layout, self._onroad_layout):
|
||||||
self._car_onroad_layout, self._body_onroad_layout):
|
|
||||||
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
|
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
|
||||||
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||||
|
|
||||||
self._scroller.add_widgets([
|
self._scroller.add_widgets([
|
||||||
self._alerts_layout,
|
self._alerts_layout,
|
||||||
self._home_layout,
|
self._home_layout,
|
||||||
self._car_onroad_layout,
|
self._onroad_layout,
|
||||||
self._body_onroad_layout,
|
|
||||||
])
|
])
|
||||||
self._scroller.set_reset_scroll_at_show(False)
|
self._scroller.set_reset_scroll_at_show(False)
|
||||||
|
|
||||||
# Disable scrolling when onroad is interacting with bookmark
|
# Disable scrolling when onroad is interacting with bookmark
|
||||||
self._scroller.set_scrolling_enabled(lambda: not self._car_onroad_layout.is_swiping_left())
|
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
|
||||||
|
|
||||||
# Set callbacks
|
# Set callbacks
|
||||||
self._setup_callbacks()
|
self._setup_callbacks()
|
||||||
@@ -63,22 +59,14 @@ class MiciMainLayout(Scroller):
|
|||||||
if not self._onboarding_window.completed:
|
if not self._onboarding_window.completed:
|
||||||
gui_app.push_widget(self._onboarding_window)
|
gui_app.push_widget(self._onboarding_window)
|
||||||
|
|
||||||
@property
|
|
||||||
def _onroad_layout(self) -> Widget:
|
|
||||||
# For scroll_to
|
|
||||||
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
|
|
||||||
|
|
||||||
def _setup_callbacks(self):
|
def _setup_callbacks(self):
|
||||||
self._home_layout.set_callbacks(
|
self._home_layout.set_callbacks(
|
||||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||||
on_alerts=lambda: self._scroll_to(self._alerts_layout),
|
on_alerts=lambda: self._scroll_to(self._alerts_layout),
|
||||||
alert_count_callback=self._alerts_layout.active_alerts,
|
alert_count_callback=self._alerts_layout.active_alerts,
|
||||||
)
|
)
|
||||||
for layout in (self._car_onroad_layout, self._body_onroad_layout):
|
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||||
layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
|
||||||
|
|
||||||
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
||||||
ui_state.add_on_body_changed_callbacks(self._on_body_changed)
|
|
||||||
|
|
||||||
def _scroll_to(self, layout: Widget):
|
def _scroll_to(self, layout: Widget):
|
||||||
layout_x = int(layout.rect.x)
|
layout_x = int(layout.rect.x)
|
||||||
@@ -144,7 +132,3 @@ class MiciMainLayout(Scroller):
|
|||||||
user_bookmark = messaging.new_message('bookmarkButton')
|
user_bookmark = messaging.new_message('bookmarkButton')
|
||||||
user_bookmark.valid = True
|
user_bookmark.valid = True
|
||||||
self._pm.send('bookmarkButton', user_bookmark)
|
self._pm.send('bookmarkButton', user_bookmark)
|
||||||
|
|
||||||
def _on_body_changed(self):
|
|
||||||
self._car_onroad_layout.set_visible(not ui_state.is_body)
|
|
||||||
self._body_onroad_layout.set_visible(ui_state.is_body)
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class TrainingGuideDMTutorial(NavWidget):
|
|||||||
|
|
||||||
# stay at 100% once reached
|
# stay at 100% once reached
|
||||||
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
|
in_bad_face = gui_app.get_active_widget() == self._bad_face_page
|
||||||
if ((dm_state.visionPolicyState.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
|
if ((dm_state.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face:
|
||||||
slow = self._progress.x < 0.25
|
slow = self._progress.x < 0.25
|
||||||
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
|
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
|
||||||
self._progress.x += 1.0 / (duration * gui_app.target_fps)
|
self._progress.x += 1.0 / (duration * gui_app.target_fps)
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ class DeveloperLayoutMici(NavScroller):
|
|||||||
|
|
||||||
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad()
|
||||||
self._long_maneuver_toggle.set_enabled(long_man_enabled)
|
self._long_maneuver_toggle.set_enabled(long_man_enabled)
|
||||||
|
if not long_man_enabled:
|
||||||
|
self._long_maneuver_toggle.set_checked(False)
|
||||||
|
ui_state.params.put_bool("LongitudinalManeuverMode", False)
|
||||||
|
|
||||||
|
lat_man_enabled = ui_state.is_offroad()
|
||||||
|
self._lat_maneuver_toggle.set_enabled(lat_man_enabled)
|
||||||
else:
|
else:
|
||||||
self._long_maneuver_toggle.set_enabled(False)
|
self._long_maneuver_toggle.set_enabled(False)
|
||||||
self._lat_maneuver_toggle.set_enabled(False)
|
self._lat_maneuver_toggle.set_enabled(False)
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from cereal import car, log, messaging
|
from cereal import log, messaging
|
||||||
from msgq.visionipc import VisionStreamType
|
from msgq.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.selfdrive.selfdrived.events import EVENTS, ET
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.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
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
|
|
||||||
|
EventName = log.OnroadEvent.EventName
|
||||||
|
|
||||||
|
EVENT_TO_INT = EventName.schema.enumerants
|
||||||
|
|
||||||
|
|
||||||
class DriverCameraView(CameraView):
|
class DriverCameraView(CameraView):
|
||||||
def _calc_frame_matrix(self, rect: rl.Rectangle):
|
def _calc_frame_matrix(self, rect: rl.Rectangle):
|
||||||
@@ -34,6 +39,8 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
self._eye_fill_texture = None
|
self._eye_fill_texture = None
|
||||||
self._eye_orange_texture = None
|
self._eye_orange_texture = None
|
||||||
self._eye_size = 74
|
self._eye_size = 74
|
||||||
|
self._glasses_texture = None
|
||||||
|
self._glasses_size = 171
|
||||||
|
|
||||||
self._load_eye_textures()
|
self._load_eye_textures()
|
||||||
|
|
||||||
@@ -102,14 +109,11 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
if self._pm is None:
|
if self._pm is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
AudibleAlert = car.CarControl.HUDControl.AudibleAlert
|
|
||||||
ALERT_SOUNDS = {
|
|
||||||
'two': AudibleAlert.promptDistracted,
|
|
||||||
'three': AudibleAlert.warningImmediate,
|
|
||||||
}
|
|
||||||
msg = messaging.new_message('selfdriveState')
|
msg = messaging.new_message('selfdriveState')
|
||||||
if dm_state is not None:
|
if dm_state is not None and len(dm_state.events):
|
||||||
msg.selfdriveState.alertSound = ALERT_SOUNDS.get(str(dm_state.alertLevel), AudibleAlert.none)
|
event_name = EVENT_TO_INT[dm_state.events[0].name]
|
||||||
|
if event_name is not None and event_name in EVENTS and ET.PERMANENT in EVENTS[event_name]:
|
||||||
|
msg.selfdriveState.alertSound = EVENTS[event_name][ET.PERMANENT].audible_alert
|
||||||
self._pm.send('selfdriveState', msg)
|
self._pm.send('selfdriveState', msg)
|
||||||
|
|
||||||
def _render_dm_alerts(self, rect: rl.Rectangle):
|
def _render_dm_alerts(self, rect: rl.Rectangle):
|
||||||
@@ -117,31 +121,29 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
dm_state = ui_state.sm["driverMonitoringState"]
|
dm_state = ui_state.sm["driverMonitoringState"]
|
||||||
self._publish_alert_sound(dm_state)
|
self._publish_alert_sound(dm_state)
|
||||||
|
|
||||||
is_vision = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
|
|
||||||
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: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_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: {dm_state.awarenessStatus * 100:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_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 not dm_state.events:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show alert level
|
# Show first event (only one should be active at a time)
|
||||||
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
event_name_str = str(dm_state.events[0].name).split('.')[-1]
|
||||||
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_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, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_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, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_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)))
|
||||||
@@ -152,11 +154,13 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
||||||
if self._eye_orange_texture is None:
|
if self._eye_orange_texture is None:
|
||||||
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
||||||
|
if self._glasses_texture is None:
|
||||||
|
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
||||||
|
|
||||||
def _draw_face_detection(self, rect: rl.Rectangle):
|
def _draw_face_detection(self, rect: rl.Rectangle):
|
||||||
dm_state = ui_state.sm["driverMonitoringState"]
|
dm_state = ui_state.sm["driverMonitoringState"]
|
||||||
driver_data = self.driver_state_renderer.get_driver_data()
|
driver_data = self.driver_state_renderer.get_driver_data()
|
||||||
if not dm_state.visionPolicyState.faceDetected:
|
if not dm_state.faceDetected:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get face position and orientation
|
# Get face position and orientation
|
||||||
@@ -198,21 +202,31 @@ class BaseDriverCameraDialog(Widget):
|
|||||||
eye_offset_x = 10
|
eye_offset_x = 10
|
||||||
eye_offset_y = 10
|
eye_offset_y = 10
|
||||||
eye_spacing = self._eye_size + 15
|
eye_spacing = self._eye_size + 15
|
||||||
eyes_prob = driver_data.eyesVisibleProb
|
|
||||||
|
|
||||||
left_eye_x = rect.x + eye_offset_x
|
left_eye_x = rect.x + eye_offset_x
|
||||||
left_eye_y = rect.y + eye_offset_y
|
left_eye_y = rect.y + eye_offset_y
|
||||||
|
left_eye_prob = driver_data.leftEyeProb
|
||||||
|
|
||||||
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
||||||
right_eye_y = rect.y + eye_offset_y
|
right_eye_y = rect.y + eye_offset_y
|
||||||
|
right_eye_prob = driver_data.rightEyeProb
|
||||||
|
|
||||||
# Draw eyes with opacity based on probability
|
# Draw eyes with opacity based on probability
|
||||||
fill_opacity = eyes_prob
|
for eye_x, eye_y, eye_prob in [(left_eye_x, left_eye_y, left_eye_prob), (right_eye_x, right_eye_y, right_eye_prob)]:
|
||||||
orange_opacity = 1.0 - eyes_prob
|
fill_opacity = eye_prob
|
||||||
for eye_x, eye_y in [(left_eye_x, left_eye_y), (right_eye_x, right_eye_y)]:
|
orange_opacity = 1.0 - eye_prob
|
||||||
|
|
||||||
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
||||||
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
||||||
|
|
||||||
|
# Draw sunglasses indicator based on sunglasses probability
|
||||||
|
# Position glasses centered between the two eyes at top left
|
||||||
|
glasses_x = rect.x + eye_offset_x - 4
|
||||||
|
glasses_y = rect.y
|
||||||
|
glasses_pos = rl.Vector2(glasses_x, glasses_y)
|
||||||
|
glasses_prob = driver_data.sunglassesProb
|
||||||
|
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
||||||
|
|
||||||
|
|
||||||
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
|||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
|
||||||
|
|
||||||
AlertSize = log.SelfdriveState.AlertSize
|
AlertSize = log.SelfdriveState.AlertSize
|
||||||
|
|
||||||
@@ -35,8 +35,6 @@ class DriverStateRenderer(Widget):
|
|||||||
self._is_active = False
|
self._is_active = False
|
||||||
self._is_rhd = False
|
self._is_rhd = False
|
||||||
self._face_detected = False
|
self._face_detected = False
|
||||||
self._face_pitch = 0.
|
|
||||||
self._face_yaw = 0.
|
|
||||||
self._should_draw = False
|
self._should_draw = False
|
||||||
self._force_active = False
|
self._force_active = False
|
||||||
self._looking_center = False
|
self._looking_center = False
|
||||||
@@ -155,11 +153,9 @@ class DriverStateRenderer(Widget):
|
|||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
|
|
||||||
dm_state = sm["driverMonitoringState"]
|
dm_state = sm["driverMonitoringState"]
|
||||||
self._is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
|
self._is_active = dm_state.isActiveMode
|
||||||
self._is_rhd = dm_state.isRHD
|
self._is_rhd = dm_state.isRHD
|
||||||
self._face_detected = dm_state.visionPolicyState.faceDetected
|
self._face_detected = dm_state.faceDetected
|
||||||
self._face_pitch = dm_state.visionPolicyState.pose.pitch + math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
|
|
||||||
self._face_yaw = -dm_state.visionPolicyState.pose.yaw # undo sign flip in face_orientation_from_model to match UI convention
|
|
||||||
|
|
||||||
driverstate = sm["driverStateV2"]
|
driverstate = sm["driverStateV2"]
|
||||||
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
|
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
|
||||||
@@ -167,9 +163,26 @@ class DriverStateRenderer(Widget):
|
|||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
# Get monitoring state
|
# Get monitoring state
|
||||||
_ = self.get_driver_data()
|
driver_data = self.get_driver_data()
|
||||||
pitch = self._pitch_filter.update(self._face_pitch)
|
driver_orient = driver_data.faceOrientation
|
||||||
yaw = self._yaw_filter.update(self._face_yaw)
|
driver_position = driver_data.facePosition
|
||||||
|
|
||||||
|
if len(driver_orient) != 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calibrate orientation so looking straight ahead at road (instead of at device) is (0, 0, 0)
|
||||||
|
sm = ui_state.sm
|
||||||
|
if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
|
||||||
|
cal_rpy = sm['liveCalibration'].rpyCalib
|
||||||
|
else:
|
||||||
|
cal_rpy = [0.0, 0.0, 0.0]
|
||||||
|
|
||||||
|
_, pitch, yaw = face_orientation_from_net(driver_orient, driver_position, cal_rpy)
|
||||||
|
pitch += math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
|
||||||
|
yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
|
||||||
|
|
||||||
|
pitch = self._pitch_filter.update(pitch)
|
||||||
|
yaw = self._yaw_filter.update(yaw)
|
||||||
|
|
||||||
# hysteresis on looking center
|
# hysteresis on looking center
|
||||||
if abs(pitch) < LOOKING_CENTER_THRESHOLD_LOWER and abs(yaw) < LOOKING_CENTER_THRESHOLD_LOWER:
|
if abs(pitch) < LOOKING_CENTER_THRESHOLD_LOWER and abs(yaw) < LOOKING_CENTER_THRESHOLD_LOWER:
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class DriverStateRenderer(Widget):
|
|||||||
|
|
||||||
# Get monitoring state
|
# Get monitoring state
|
||||||
dm_state = sm["driverMonitoringState"]
|
dm_state = sm["driverMonitoringState"]
|
||||||
self.is_active = dm_state.activePolicy == log.DriverMonitoringState.MonitoringPolicy.vision
|
self.is_active = dm_state.isActiveMode
|
||||||
self.is_rhd = dm_state.isRHD
|
self.is_rhd = dm_state.isRHD
|
||||||
|
|
||||||
# Update fade state (smoother transition between active/inactive)
|
# Update fade state (smoother transition between active/inactive)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import time
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from cereal import custom
|
from cereal import custom
|
||||||
|
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
from openpilot.selfdrive.ui.ui_state import device, ui_state
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
@@ -207,7 +208,7 @@ class ModelsLayout(Widget):
|
|||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
|
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
|
||||||
|
|
||||||
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': tr("Default Model"), 'short_name': "Default"})])]
|
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{DEFAULT_MODEL} (Default)", 'short_name': "Default"})])]
|
||||||
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
|
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
|
||||||
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
|
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
|
||||||
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
|
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
|
||||||
@@ -243,7 +244,7 @@ class ModelsLayout(Widget):
|
|||||||
self._update_lagd_description(live_delay)
|
self._update_lagd_description(live_delay)
|
||||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||||
self._handle_bundle_download_progress()
|
self._handle_bundle_download_progress()
|
||||||
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else tr("Default Model")
|
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
|
||||||
self.current_model_item.action_item.set_value(active_name)
|
self.current_model_item.action_item.set_value(active_name)
|
||||||
|
|
||||||
if not ui_state.is_offroad():
|
if not ui_state.is_offroad():
|
||||||
|
|||||||
@@ -120,20 +120,12 @@ class SteeringLayout(Widget):
|
|||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
super()._update_state()
|
super()._update_state()
|
||||||
|
|
||||||
torque_allowed = True
|
torque_allowed = ui_state.CP is not None and ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle
|
||||||
if ui_state.CP is not None:
|
if ui_state.CP is not None:
|
||||||
mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc
|
mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc
|
||||||
self._mads_toggle.set_description(f"<b>{mads_main_desc}</b><br><br>{self._mads_base_desc}")
|
self._mads_toggle.set_description(f"<b>{mads_main_desc}</b><br><br>{self._mads_base_desc}")
|
||||||
|
|
||||||
if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle:
|
|
||||||
ui_state.params.remove("EnforceTorqueControl")
|
|
||||||
ui_state.params.remove("NeuralNetworkLateralControl")
|
|
||||||
torque_allowed = False
|
|
||||||
else:
|
else:
|
||||||
self._mads_toggle.set_description(f"<b>{self._mads_check_compat_desc}</b><br><br>{self._mads_base_desc}")
|
self._mads_toggle.set_description(f"<b>{self._mads_check_compat_desc}</b><br><br>{self._mads_base_desc}")
|
||||||
ui_state.params.remove("EnforceTorqueControl")
|
|
||||||
ui_state.params.remove("NeuralNetworkLateralControl")
|
|
||||||
torque_allowed = False
|
|
||||||
|
|
||||||
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
|
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||||
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
|
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from collections.abc import Callable
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from cereal import custom
|
from cereal import custom
|
||||||
|
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
@@ -27,7 +28,8 @@ class CurrentModelInfo(Widget):
|
|||||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||||
max_width = int(self._rect.width - 20)
|
max_width = int(self._rect.width - 20)
|
||||||
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||||
self.current_model_text = UnifiedLabel(tr("default model"), 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
default_text = f"{DEFAULT_MODEL} (Default)".lower()
|
||||||
|
self.current_model_text = UnifiedLabel(default_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
||||||
|
|
||||||
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||||
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||||
@@ -98,7 +100,7 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
|
|
||||||
folders = self._get_grouped_bundles(favorites)
|
folders = self._get_grouped_bundles(favorites)
|
||||||
folder_buttons = []
|
folder_buttons = []
|
||||||
default_btn = BigButton(tr("default model"))
|
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
|
||||||
default_btn.set_click_callback(self._select_default)
|
default_btn.set_click_callback(self._select_default)
|
||||||
folder_buttons.append(default_btn)
|
folder_buttons.append(default_btn)
|
||||||
|
|
||||||
@@ -168,7 +170,8 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self._was_downloading = is_downloading
|
self._was_downloading = is_downloading
|
||||||
|
|
||||||
self.current_model_info.current_model_header.set_text(tr("active model"))
|
self.current_model_info.current_model_header.set_text(tr("active model"))
|
||||||
self.current_model_info.current_model_text.set_text(manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else tr("default model"))
|
model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else f"{DEFAULT_MODEL} (Default)".lower()
|
||||||
|
self.current_model_info.current_model_text.set_text(model_text)
|
||||||
self.current_model_info.info_header.set_text(tr("cache size"))
|
self.current_model_info.info_header.set_text(tr("cache size"))
|
||||||
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
|||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||||
|
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsBigButton
|
||||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
||||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
|
||||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
||||||
@@ -32,11 +33,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
|||||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||||
|
|
||||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||||
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
|
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
|
||||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||||
|
|
||||||
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
||||||
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||||
|
|
||||||
# onroad: enable button sits at the front (left of toggles)
|
# onroad: enable button sits at the front (left of toggles)
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ class DeveloperUiRenderer(Widget):
|
|||||||
|
|
||||||
# Add torque-specific elements if using torque control
|
# Add torque-specific elements if using torque control
|
||||||
if sm['controlsState'].lateralControlState.which() == 'torqueState':
|
if sm['controlsState'].lateralControlState.which() == 'torqueState':
|
||||||
if sm.valid['liveTorqueParameters']:
|
override_active = ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled
|
||||||
|
if sm.valid['liveTorqueParameters'] or override_active:
|
||||||
elements.extend([
|
elements.extend([
|
||||||
self.friction_elem.update(sm, ui_state.is_metric),
|
self.friction_elem.update(sm, ui_state.is_metric),
|
||||||
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),
|
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import pyray as rl
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
|
||||||
|
|
||||||
@@ -248,12 +247,12 @@ class FrictionCoefficientElement:
|
|||||||
self.unit = ""
|
self.unit = ""
|
||||||
|
|
||||||
def update(self, sm, is_metric: bool) -> UiElement:
|
def update(self, sm, is_metric: bool) -> UiElement:
|
||||||
ltp = sm['liveTorqueParameters']
|
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
|
||||||
friction_coef = ltp.frictionCoefficientFiltered
|
return UiElement(f"{ui_state.torque_override_friction:.3f}", "FRIC.", self.unit, rl.WHITE)
|
||||||
live_valid = ltp.liveValid
|
|
||||||
|
|
||||||
value = f"{friction_coef:.3f}"
|
ltp = sm['liveTorqueParameters']
|
||||||
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
|
value = f"{ltp.frictionCoefficientFiltered:.3f}"
|
||||||
|
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
|
||||||
return UiElement(value, "FRIC.", self.unit, color)
|
return UiElement(value, "FRIC.", self.unit, color)
|
||||||
|
|
||||||
|
|
||||||
@@ -262,12 +261,12 @@ class LatAccelFactorElement:
|
|||||||
self.unit = ""
|
self.unit = ""
|
||||||
|
|
||||||
def update(self, sm, is_metric: bool) -> UiElement:
|
def update(self, sm, is_metric: bool) -> UiElement:
|
||||||
ltp = sm['liveTorqueParameters']
|
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
|
||||||
lat_accel_factor = ltp.latAccelFactorFiltered
|
return UiElement(f"{ui_state.torque_override_lat_accel_factor:.3f}", "L.A.F.", self.unit, rl.WHITE)
|
||||||
live_valid = ltp.liveValid
|
|
||||||
|
|
||||||
value = f"{lat_accel_factor:.3f}"
|
ltp = sm['liveTorqueParameters']
|
||||||
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
|
value = f"{ltp.latAccelFactorFiltered:.3f}"
|
||||||
|
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
|
||||||
return UiElement(value, "L.A.F.", self.unit, color)
|
return UiElement(value, "L.A.F.", self.unit, color)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from cereal import messaging, log, custom
|
from cereal import messaging, log, car, custom
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
||||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||||
@@ -26,22 +26,20 @@ class OnroadTimerStatus(Enum):
|
|||||||
|
|
||||||
class UIStateSP:
|
class UIStateSP:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.CP_SP: custom.CarParamsSP | None = None
|
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
|
self.CP_SP: custom.CarParamsSP | None = None
|
||||||
|
self.has_icbm: bool = False
|
||||||
|
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", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
|
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
|
||||||
]
|
]
|
||||||
|
|
||||||
self.sunnylink_state = SunnylinkState()
|
self.sunnylink_state = SunnylinkState()
|
||||||
self.update_params_()
|
|
||||||
|
|
||||||
self.onroad_brightness_timer: int = 0
|
self.onroad_brightness_timer: int = 0
|
||||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
self.custom_interactive_timeout: int = 0
|
||||||
self.reset_onroad_sleep_timer()
|
self._sp_initialized: bool = False
|
||||||
self.CP_SP: custom.CarParamsSP | None = None
|
|
||||||
self.has_icbm: bool = False
|
|
||||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
if self.sunnylink_enabled:
|
if self.sunnylink_enabled:
|
||||||
@@ -123,11 +121,13 @@ class UIStateSP:
|
|||||||
|
|
||||||
return "disengaged"
|
return "disengaged"
|
||||||
|
|
||||||
def update_params_(self) -> None:
|
def update_params(self) -> None:
|
||||||
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
||||||
if CP_SP_bytes is not None:
|
if CP_SP_bytes is not None:
|
||||||
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||||
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
|
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
|
||||||
|
|
||||||
|
self._enforce_constraints()
|
||||||
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
||||||
self.blindspot = self.params.get_bool("BlindSpot")
|
self.blindspot = self.params.get_bool("BlindSpot")
|
||||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||||
@@ -143,11 +143,63 @@ class UIStateSP:
|
|||||||
self.standstill_timer = self.params.get_bool("StandstillTimer")
|
self.standstill_timer = self.params.get_bool("StandstillTimer")
|
||||||
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
|
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
|
||||||
self.torque_bar = self.params.get_bool("TorqueBar")
|
self.torque_bar = self.params.get_bool("TorqueBar")
|
||||||
|
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||||
|
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
|
||||||
|
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
|
||||||
|
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
|
||||||
|
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
|
||||||
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||||
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
||||||
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
||||||
self.always_offroad = self.params.get_bool("OffroadMode")
|
self.always_offroad = self.params.get_bool("OffroadMode")
|
||||||
|
|
||||||
|
if not self._sp_initialized:
|
||||||
|
self._sp_initialized = True
|
||||||
|
self.reset_onroad_sleep_timer()
|
||||||
|
|
||||||
|
def _enforce_constraints(self) -> None:
|
||||||
|
has_long = self.has_longitudinal_control
|
||||||
|
CP = self.CP
|
||||||
|
|
||||||
|
if CP is not None:
|
||||||
|
# Angle steering: no torque-based lateral controls
|
||||||
|
if CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||||
|
self.params.remove("EnforceTorqueControl")
|
||||||
|
self.params.remove("NeuralNetworkLateralControl")
|
||||||
|
|
||||||
|
# Alpha longitudinal: clear if not available
|
||||||
|
if not CP.alphaLongitudinalAvailable:
|
||||||
|
self.params.remove("AlphaLongitudinalEnabled")
|
||||||
|
|
||||||
|
# BSM not available: clear BSM-dependent settings
|
||||||
|
if not CP.enableBsm:
|
||||||
|
self.params.remove("AutoLaneChangeBsmDelay")
|
||||||
|
else:
|
||||||
|
# No CarParams: clear all car-dependent params as safety default
|
||||||
|
self.params.remove("EnforceTorqueControl")
|
||||||
|
self.params.remove("NeuralNetworkLateralControl")
|
||||||
|
self.params.remove("AlphaLongitudinalEnabled")
|
||||||
|
|
||||||
|
# No longitudinal control: no experimental mode or DEC
|
||||||
|
if not has_long:
|
||||||
|
self.params.remove("ExperimentalMode")
|
||||||
|
self.params.remove("DynamicExperimentalControl")
|
||||||
|
|
||||||
|
# ICBM: clear if not available or if full longitudinal control is active
|
||||||
|
if self.CP_SP is not None:
|
||||||
|
if not self.CP_SP.intelligentCruiseButtonManagementAvailable or has_long:
|
||||||
|
self.params.remove("IntelligentCruiseButtonManagement")
|
||||||
|
self.has_icbm = False
|
||||||
|
else:
|
||||||
|
self.params.remove("IntelligentCruiseButtonManagement")
|
||||||
|
self.has_icbm = False
|
||||||
|
|
||||||
|
# Cruise features requiring longitudinal or ICBM
|
||||||
|
if not (has_long or self.has_icbm):
|
||||||
|
self.params.remove("CustomAccIncrementsEnabled")
|
||||||
|
self.params.remove("SmartCruiseControlVision")
|
||||||
|
self.params.remove("SmartCruiseControlMap")
|
||||||
|
|
||||||
|
|
||||||
class DeviceSP:
|
class DeviceSP:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -163,7 +215,6 @@ class DeviceSP:
|
|||||||
if _ui_state.onroad_brightness_timer != 0:
|
if _ui_state.onroad_brightness_timer != 0:
|
||||||
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
|
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
|
||||||
return max(30.0, cur_brightness)
|
return max(30.0, cur_brightness)
|
||||||
# For AUTO (Default) and Manual modes (while timer running), use standard brightness
|
|
||||||
return cur_brightness
|
return cur_brightness
|
||||||
|
|
||||||
# 0: Auto (Default), 1: Auto (Dark), 2: Screen Off
|
# 0: Auto (Default), 1: Auto (Dark), 2: Screen Off
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from openpilot.system.hardware import HARDWARE, PC
|
|||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||||
|
|
||||||
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
||||||
PARAM_UPDATE_TIME = 5.0
|
|
||||||
|
|
||||||
|
|
||||||
class UIStatus(Enum):
|
class UIStatus(Enum):
|
||||||
@@ -60,7 +59,6 @@ class UIState(UIStateSP):
|
|||||||
"carOutput",
|
"carOutput",
|
||||||
"carControl",
|
"carControl",
|
||||||
"liveParameters",
|
"liveParameters",
|
||||||
"testJoystick",
|
|
||||||
"rawAudioData",
|
"rawAudioData",
|
||||||
] + self.sm_services_ext
|
] + self.sm_services_ext
|
||||||
)
|
)
|
||||||
@@ -76,7 +74,7 @@ class UIState(UIStateSP):
|
|||||||
|
|
||||||
# Core state variables
|
# Core state variables
|
||||||
self.is_metric: bool = self.params.get_bool("IsMetric")
|
self.is_metric: bool = self.params.get_bool("IsMetric")
|
||||||
self.is_release = self.params.get_bool("IsReleaseBranch")
|
self.is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||||
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
||||||
self.started: bool = False
|
self.started: bool = False
|
||||||
self.ignition: bool = False
|
self.ignition: bool = False
|
||||||
@@ -84,15 +82,15 @@ class UIState(UIStateSP):
|
|||||||
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
|
self.panda_type: log.PandaState.PandaType = log.PandaState.PandaType.unknown
|
||||||
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
|
self.personality: log.LongitudinalPersonality = log.LongitudinalPersonality.standard
|
||||||
self.has_longitudinal_control: bool = False
|
self.has_longitudinal_control: bool = False
|
||||||
self.is_body: bool | None = None
|
|
||||||
self.CP: car.CarParams | None = None
|
self.CP: car.CarParams | None = None
|
||||||
self.light_sensor: float = -1.0
|
self.light_sensor: float = -1.0
|
||||||
self._param_update_time: float = -PARAM_UPDATE_TIME
|
self._param_update_time: float = 0.0
|
||||||
|
|
||||||
# Callbacks
|
# Callbacks
|
||||||
self._offroad_transition_callbacks: list[Callable[[], None]] = []
|
self._offroad_transition_callbacks: list[Callable[[], None]] = []
|
||||||
self._engaged_transition_callbacks: list[Callable[[], None]] = []
|
self._engaged_transition_callbacks: list[Callable[[], None]] = []
|
||||||
self._on_body_changed_callbacks: list[Callable[[], None]] = []
|
|
||||||
|
self.update_params()
|
||||||
|
|
||||||
def add_offroad_transition_callback(self, callback: Callable[[], None]):
|
def add_offroad_transition_callback(self, callback: Callable[[], None]):
|
||||||
self._offroad_transition_callbacks.append(callback)
|
self._offroad_transition_callbacks.append(callback)
|
||||||
@@ -100,9 +98,6 @@ class UIState(UIStateSP):
|
|||||||
def add_engaged_transition_callback(self, callback: Callable[[], None]):
|
def add_engaged_transition_callback(self, callback: Callable[[], None]):
|
||||||
self._engaged_transition_callbacks.append(callback)
|
self._engaged_transition_callbacks.append(callback)
|
||||||
|
|
||||||
def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
|
|
||||||
self._on_body_changed_callbacks.append(callback)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def engaged(self) -> bool:
|
def engaged(self) -> bool:
|
||||||
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
|
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
|
||||||
@@ -118,7 +113,7 @@ class UIState(UIStateSP):
|
|||||||
self.sm.update(0)
|
self.sm.update(0)
|
||||||
self._update_state()
|
self._update_state()
|
||||||
self._update_status()
|
self._update_status()
|
||||||
if time.monotonic() - self._param_update_time >= PARAM_UPDATE_TIME:
|
if time.monotonic() - self._param_update_time > 5.0:
|
||||||
self.update_params()
|
self.update_params()
|
||||||
device.update()
|
device.update()
|
||||||
UIStateSP.update(self)
|
UIStateSP.update(self)
|
||||||
@@ -193,13 +188,7 @@ class UIState(UIStateSP):
|
|||||||
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
|
self.has_longitudinal_control = self.params.get_bool("AlphaLongitudinalEnabled")
|
||||||
else:
|
else:
|
||||||
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
|
self.has_longitudinal_control = self.CP.openpilotLongitudinalControl
|
||||||
|
UIStateSP.update_params(self)
|
||||||
if self.is_body != self.CP.notCar:
|
|
||||||
self.is_body = self.CP.notCar
|
|
||||||
for callback in self._on_body_changed_callbacks:
|
|
||||||
callback()
|
|
||||||
|
|
||||||
UIStateSP.update_params_(self)
|
|
||||||
self._param_update_time = time.monotonic()
|
self._param_update_time = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define SUNNYPILOT_VERSION "2026.001.000"
|
#define SUNNYPILOT_VERSION "2026.002.000"
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ class ModularAssistiveDrivingSystem:
|
|||||||
self.events.remove(EventName.speedTooLow)
|
self.events.remove(EventName.speedTooLow)
|
||||||
self.events.remove(EventName.cruiseDisabled)
|
self.events.remove(EventName.cruiseDisabled)
|
||||||
self.events.remove(EventName.manualRestart)
|
self.events.remove(EventName.manualRestart)
|
||||||
|
self.events.remove(EventName.espActive)
|
||||||
|
|
||||||
selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable)
|
selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable)
|
||||||
set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents)
|
set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
from tinygrad import Device
|
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -22,19 +21,10 @@ if PC:
|
|||||||
if outputs:
|
if outputs:
|
||||||
lenv.Command(outputs, inputs, cmd)
|
lenv.Command(outputs, inputs, cmd)
|
||||||
|
|
||||||
available = set(Device.get_available_devices())
|
tg_flags = {
|
||||||
if 'CUDA' in available:
|
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
|
||||||
tg_backend = 'CUDA'
|
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}',
|
||||||
tg_flags = f'DEV={tg_backend}'
|
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
|
||||||
elif 'QCOM' in available:
|
|
||||||
tg_backend = 'QCOM'
|
|
||||||
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
|
||||||
else:
|
|
||||||
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
|
|
||||||
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
|
||||||
tg_flags = f'DEV={tg_backend} THREADS=0'
|
|
||||||
|
|
||||||
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
|
|
||||||
|
|
||||||
image_flag = {
|
image_flag = {
|
||||||
'larch64': 'IMAGE=2',
|
'larch64': 'IMAGE=2',
|
||||||
@@ -48,7 +38,7 @@ def tg_compile(flags, model_name):
|
|||||||
return lenv.Command(
|
return lenv.Command(
|
||||||
out,
|
out,
|
||||||
[fn + ".onnx"] + tinygrad_files,
|
[fn + ".onnx"] + tinygrad_files,
|
||||||
f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
|
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compile models
|
# Compile models
|
||||||
@@ -56,9 +46,9 @@ for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'drivin
|
|||||||
if File(f"models/{model_name}.onnx").exists():
|
if File(f"models/{model_name}.onnx").exists():
|
||||||
tg_compile(tg_flags, model_name)
|
tg_compile(tg_flags, model_name)
|
||||||
|
|
||||||
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_modeld.py").abspath)]
|
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
|
||||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
|
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
|
||||||
compile_warp_cmd = f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 -m sunnypilot.modeld_v2.warp'
|
compile_warp_cmd = f'{pythonpath_string} {tg_flags} python3 -m sunnypilot.modeld_v2.warp'
|
||||||
|
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||||
warp_targets = []
|
warp_targets = []
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import os
|
|||||||
os.environ['DEV'] = 'CPU'
|
os.environ['DEV'] = 'CPU'
|
||||||
import pytest
|
import pytest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
|
from openpilot.selfdrive.modeld.compile_warp import get_nv12_info, CAMERA_CONFIGS
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
|
||||||
from openpilot.sunnypilot.modeld_v2.warp import Warp
|
|
||||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
|
||||||
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
|
|
||||||
|
|
||||||
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
|
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
|
||||||
('img', 'big_img'),
|
('img', 'big_img'),
|
||||||
|
|||||||
@@ -6,61 +6,29 @@ from tinygrad.tensor import Tensor
|
|||||||
from tinygrad.engine.jit import TinyJit
|
from tinygrad.engine.jit import TinyJit
|
||||||
from tinygrad.device import Device
|
from tinygrad.device import Device
|
||||||
|
|
||||||
# https://github.com/tinygrad/tinygrad/issues/15682
|
|
||||||
from tinygrad.uop.ops import UOp, Ops
|
|
||||||
_orig = UOp.__reduce__
|
|
||||||
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
|
|
||||||
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import (
|
from openpilot.selfdrive.modeld.compile_warp import (
|
||||||
NV12Frame, make_frame_prepare,
|
CAMERA_CONFIGS, MEDMODEL_INPUT_SIZE, make_frame_prepare, make_update_both_imgs,
|
||||||
|
warp_pkl_path,
|
||||||
)
|
)
|
||||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
|
||||||
|
|
||||||
CAMERA_CONFIGS = [
|
|
||||||
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
|
||||||
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
|
||||||
]
|
|
||||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
|
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).parent / 'models'
|
MODELS_DIR = Path(__file__).parent / 'models'
|
||||||
|
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
|
||||||
UPSTREAM_BUFFER_LENGTH = 5
|
UPSTREAM_BUFFER_LENGTH = 5
|
||||||
|
|
||||||
def warp_pkl_path(cam_w, cam_h):
|
|
||||||
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_tinygrad.pkl'
|
|
||||||
|
|
||||||
def make_update_img_input(frame_prepare, model_w, model_h):
|
|
||||||
def update_img_input_tinygrad(tensor, frame, M_inv):
|
|
||||||
M_inv = M_inv.to(Device.DEFAULT)
|
|
||||||
new_img = frame_prepare(frame, M_inv)
|
|
||||||
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
|
||||||
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
|
||||||
return update_img_input_tinygrad
|
|
||||||
|
|
||||||
def make_update_both_imgs(frame_prepare, model_w, model_h):
|
|
||||||
update_img = make_update_img_input(frame_prepare, model_w, model_h)
|
|
||||||
|
|
||||||
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
|
||||||
calib_big_img_buffer, new_big_img, M_inv_big):
|
|
||||||
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
|
||||||
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
|
||||||
return calib_img_pair, calib_big_img_pair
|
|
||||||
return update_both_imgs_tinygrad
|
|
||||||
|
|
||||||
|
|
||||||
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
|
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
|
||||||
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
|
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
|
||||||
|
|
||||||
|
|
||||||
def compile_v2_warp(cam_w, cam_h, buffer_length, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
|
def compile_v2_warp(cam_w, cam_h, buffer_length):
|
||||||
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
|
||||||
img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
|
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
|
|
||||||
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
|
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
|
||||||
|
|
||||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
||||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
||||||
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
|
|
||||||
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
update_img_jit = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||||
@@ -94,11 +62,9 @@ def compile_v2_warp(cam_w, cam_h, buffer_length, model_w=MEDMODEL_INPUT_SIZE[0],
|
|||||||
|
|
||||||
|
|
||||||
class Warp:
|
class Warp:
|
||||||
def __init__(self, buffer_length=2, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
|
def __init__(self, buffer_length=2):
|
||||||
self.buffer_length = buffer_length
|
self.buffer_length = buffer_length
|
||||||
self.model_w = model_w
|
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
|
||||||
self.model_h = model_h
|
|
||||||
self.img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
|
|
||||||
|
|
||||||
self.jit_cache = {}
|
self.jit_cache = {}
|
||||||
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
|
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
|
||||||
@@ -126,9 +92,8 @@ class Warp:
|
|||||||
with open(upstream_pkl, 'rb') as f:
|
with open(upstream_pkl, 'rb') as f:
|
||||||
self.jit_cache[key] = pickle.load(f)
|
self.jit_cache[key] = pickle.load(f)
|
||||||
if key not in self.jit_cache:
|
if key not in self.jit_cache:
|
||||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
|
||||||
frame_prepare = make_frame_prepare(nv12, self.model_w, self.model_h)
|
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
|
||||||
update_both_imgs = make_update_both_imgs(frame_prepare, self.model_w, self.model_h)
|
|
||||||
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
|
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
|
||||||
|
|
||||||
if key not in self._nv12_cache:
|
if key not in self._nv12_cache:
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import hashlib
|
|||||||
|
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.sunnypilot import get_file_hash
|
from openpilot.sunnypilot import get_file_hash
|
||||||
|
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL
|
||||||
|
|
||||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
|
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "model_name.py")
|
||||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
|
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
|
||||||
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
|
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
|
||||||
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
|
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
|
||||||
@@ -25,8 +26,7 @@ def update_model_hash():
|
|||||||
|
|
||||||
def get_current_default_model_name():
|
def get_current_default_model_name():
|
||||||
print("[GET DEFAULT MODEL NAME]")
|
print("[GET DEFAULT MODEL NAME]")
|
||||||
with open(DEFAULT_MODEL_NAME_PATH) as f:
|
name = DEFAULT_MODEL
|
||||||
name = f.read().split('"')[1]
|
|
||||||
print(f'Current default model name: "{name}"')
|
print(f'Current default model name: "{name}"')
|
||||||
|
|
||||||
return name
|
return name
|
||||||
@@ -35,7 +35,7 @@ def get_current_default_model_name():
|
|||||||
def update_default_model_name(name: str):
|
def update_default_model_name(name: str):
|
||||||
print("[CHANGE DEFAULT MODEL NAME]")
|
print("[CHANGE DEFAULT MODEL NAME]")
|
||||||
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
|
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
|
||||||
f.write(f'#define DEFAULT_MODEL "{name}"\n')
|
f.write(f'DEFAULT_MODEL = "{name}"\n')
|
||||||
print(f'New default model name: "{name}"')
|
print(f'New default model name: "{name}"')
|
||||||
print("[DONE]")
|
print("[DONE]")
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ if __name__ == "__main__":
|
|||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
current_name = get_current_default_model_name()
|
current_name = get_current_default_model_name()
|
||||||
new_name = f"{args.new_name} (Default)"
|
new_name = args.new_name
|
||||||
if current_name == new_name:
|
if current_name == new_name:
|
||||||
print(f'Proposed default model name: "{new_name}"')
|
print(f'Proposed default model name: "{new_name}"')
|
||||||
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()
|
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class ModelCache:
|
|||||||
|
|
||||||
class ModelFetcher:
|
class ModelFetcher:
|
||||||
"""Handles fetching and caching of model data from remote source"""
|
"""Handles fetching and caching of model data from remote source"""
|
||||||
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v17.json"
|
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v16.json"
|
||||||
|
|
||||||
def __init__(self, params: Params):
|
def __init__(self, params: Params):
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DEFAULT_MODEL = "POP model"
|
||||||
@@ -1 +1 @@
|
|||||||
32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf
|
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cereal import custom
|
||||||
|
import numpy as np
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
|
|
||||||
|
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||||
|
|
||||||
|
|
||||||
|
A_MAX_BP = [0.0, 4.0, 8.0, 16.0, 40.0]
|
||||||
|
A_MAX_V = {
|
||||||
|
AccelPersonality.eco: [1.20, 1.40, 1.20, 0.40, 0.08],
|
||||||
|
AccelPersonality.normal: [1.80, 1.80, 1.35, 0.50, 0.15],
|
||||||
|
AccelPersonality.sport: [2.20, 2.20, 1.60, 0.70, 0.25],
|
||||||
|
}
|
||||||
|
|
||||||
|
COAST_DRAG_BP = [0.0, 10.0, 25.0, 40.0]
|
||||||
|
COAST_DRAG_V = {
|
||||||
|
AccelPersonality.eco: [-0.03, -0.05, -0.08, -0.12],
|
||||||
|
AccelPersonality.normal: [-0.04, -0.07, -0.12, -0.18],
|
||||||
|
AccelPersonality.sport: [-0.06, -0.10, -0.18, -0.28],
|
||||||
|
}
|
||||||
|
|
||||||
|
A_MIN_FLOOR_BP = [0.0, 5.0, 15.0, 40.0]
|
||||||
|
A_MIN_FLOOR_V = {
|
||||||
|
AccelPersonality.eco: [-0.20, -0.35, -0.55, -0.50],
|
||||||
|
AccelPersonality.normal: [-0.25, -0.45, -0.75, -0.65],
|
||||||
|
AccelPersonality.sport: [-0.35, -0.65, -1.00, -0.95],
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFICIT_TO_FLOOR = 8.5
|
||||||
|
COAST_DEADBAND = 1.0
|
||||||
|
RAMP_OFF_RANGE = 5.0
|
||||||
|
|
||||||
|
A_MIN_TIGHTEN_RATE = 0.6
|
||||||
|
A_MIN_RELAX_RATE = 0.9
|
||||||
|
A_MAX_RATE_UP = 1.5
|
||||||
|
A_MAX_RATE_DOWN = 0.6
|
||||||
|
|
||||||
|
MIN_MAX_GAP = 0.05
|
||||||
|
|
||||||
|
PARAM_REFRESH_FRAMES = max(1, int(1.0 / DT_MDL))
|
||||||
|
|
||||||
|
|
||||||
|
class AccelPersonalityController:
|
||||||
|
def __init__(self):
|
||||||
|
self.params = Params()
|
||||||
|
self.frame = 0
|
||||||
|
self._first = True
|
||||||
|
|
||||||
|
val = self.params.get('AccelPersonality')
|
||||||
|
self._personality = val if val is not None else AccelPersonality.normal
|
||||||
|
self._enabled = self.params.get_bool('AccelPersonalityEnabled')
|
||||||
|
|
||||||
|
self._v_cruise = 0.0
|
||||||
|
self._a_min = -0.05
|
||||||
|
self._a_max = 1.50
|
||||||
|
|
||||||
|
self._cache_v: float | None = None
|
||||||
|
self._cache_v_cruise: float | None = None
|
||||||
|
self._cache_a_min = self._a_min
|
||||||
|
self._cache_a_max = self._a_max
|
||||||
|
|
||||||
|
def update(self, sm=None):
|
||||||
|
self.frame += 1
|
||||||
|
self._cache_v = None
|
||||||
|
self._cache_v_cruise = None
|
||||||
|
|
||||||
|
if sm is not None:
|
||||||
|
vc = sm['carState'].vCruise
|
||||||
|
self._v_cruise = float(vc) * (1000.0 / 3600.0) if vc != V_CRUISE_UNSET else 0.0
|
||||||
|
|
||||||
|
if self.frame % PARAM_REFRESH_FRAMES == 0:
|
||||||
|
val = self.params.get('AccelPersonality')
|
||||||
|
self._personality = val if val is not None else AccelPersonality.normal
|
||||||
|
new_enabled = self.params.get_bool('AccelPersonalityEnabled')
|
||||||
|
if new_enabled and not self._enabled:
|
||||||
|
self._first = True
|
||||||
|
self._enabled = new_enabled
|
||||||
|
|
||||||
|
def get_accel_personality(self) -> int:
|
||||||
|
return int(self._personality)
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
def get_accel_limits(self, v_ego: float) -> tuple[float, float]:
|
||||||
|
v_ego = max(0.0, v_ego)
|
||||||
|
if (self._cache_v is not None
|
||||||
|
and abs(self._cache_v - v_ego) < 0.01
|
||||||
|
and self._cache_v_cruise == self._v_cruise):
|
||||||
|
return self._cache_a_min, self._cache_a_max
|
||||||
|
self._cache_a_min, self._cache_a_max = self._step(v_ego)
|
||||||
|
self._cache_v = v_ego
|
||||||
|
self._cache_v_cruise = self._v_cruise
|
||||||
|
return self._cache_a_min, self._cache_a_max
|
||||||
|
|
||||||
|
def get_min_accel(self, v_ego: float) -> float:
|
||||||
|
return self.get_accel_limits(v_ego)[0]
|
||||||
|
|
||||||
|
def get_max_accel(self, v_ego: float) -> float:
|
||||||
|
return self.get_accel_limits(v_ego)[1]
|
||||||
|
|
||||||
|
def _ramp_off(self, v_ego: float) -> float:
|
||||||
|
if self._v_cruise <= 0.0:
|
||||||
|
return 1.0
|
||||||
|
return float(np.clip((self._v_cruise - v_ego) / RAMP_OFF_RANGE, 0.0, 1.0))
|
||||||
|
|
||||||
|
def _target_max(self, v_ego: float) -> float:
|
||||||
|
base = float(np.interp(v_ego, A_MAX_BP, A_MAX_V[self._personality]))
|
||||||
|
return base * self._ramp_off(v_ego)
|
||||||
|
|
||||||
|
def _target_min(self, v_ego: float) -> float:
|
||||||
|
coast = float(np.interp(v_ego, COAST_DRAG_BP, COAST_DRAG_V[self._personality]))
|
||||||
|
if self._v_cruise <= 0.0 or v_ego >= self._v_cruise:
|
||||||
|
return coast
|
||||||
|
floor = float(np.interp(v_ego, A_MIN_FLOOR_BP, A_MIN_FLOOR_V[self._personality]))
|
||||||
|
deficit = self._v_cruise - v_ego
|
||||||
|
t = float(np.clip(deficit / DEFICIT_TO_FLOOR, 0.0, 1.0)) ** 1.5
|
||||||
|
return coast + t * (floor - coast)
|
||||||
|
|
||||||
|
def _apply_coast_deadband(self, v_ego: float, t_min: float, t_max: float) -> tuple[float, float]:
|
||||||
|
if self._v_cruise <= 0.0 or abs(v_ego - self._v_cruise) >= COAST_DEADBAND:
|
||||||
|
return t_min, t_max
|
||||||
|
coast = float(np.interp(v_ego, COAST_DRAG_BP, COAST_DRAG_V[self._personality]))
|
||||||
|
return coast, max(0.05, t_max * 0.25)
|
||||||
|
|
||||||
|
def _rate_limit(self, last: float, target: float, rate_down: float, rate_up: float) -> float:
|
||||||
|
rate = rate_up if target > last else rate_down
|
||||||
|
step = rate * DT_MDL
|
||||||
|
return float(np.clip(target, last - step, last + step))
|
||||||
|
|
||||||
|
def _step(self, v_ego: float) -> tuple[float, float]:
|
||||||
|
t_max = self._target_max(v_ego)
|
||||||
|
t_min = self._target_min(v_ego)
|
||||||
|
t_min, t_max = self._apply_coast_deadband(v_ego, t_min, t_max)
|
||||||
|
|
||||||
|
if self._first:
|
||||||
|
self._a_min, self._a_max = t_min, t_max
|
||||||
|
self._first = False
|
||||||
|
return self._a_min, self._a_max
|
||||||
|
|
||||||
|
new_min = self._rate_limit(self._a_min, t_min, rate_down=A_MIN_TIGHTEN_RATE, rate_up=A_MIN_RELAX_RATE)
|
||||||
|
new_max = self._rate_limit(self._a_max, t_max, rate_down=A_MAX_RATE_DOWN, rate_up=A_MAX_RATE_UP)
|
||||||
|
|
||||||
|
new_min = min(new_min, new_max - MIN_MAX_GAP)
|
||||||
|
|
||||||
|
self._a_min, self._a_max = new_min, new_max
|
||||||
|
return self._a_min, self._a_max
|
||||||
@@ -17,6 +17,9 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_resolve
|
|||||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||||
|
from opendbc.car.interfaces import ACCEL_MIN
|
||||||
|
|
||||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ class LongitudinalPlannerSP:
|
|||||||
self.events_sp = EventsSP()
|
self.events_sp = EventsSP()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.dec = DynamicExperimentalController(CP, mpc)
|
self.dec = DynamicExperimentalController(CP, mpc)
|
||||||
|
self.accel_controller = AccelPersonalityController()
|
||||||
self.scc = SmartCruiseControl()
|
self.scc = SmartCruiseControl()
|
||||||
self.resolver = SpeedLimitResolver()
|
self.resolver = SpeedLimitResolver()
|
||||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||||
@@ -43,6 +47,17 @@ class LongitudinalPlannerSP:
|
|||||||
|
|
||||||
return experimental_mode and self.dec.mode() == "blended"
|
return experimental_mode and self.dec.mode() == "blended"
|
||||||
|
|
||||||
|
def get_accel_clip(self, v_ego: float) -> list[float] | None:
|
||||||
|
if not self.accel_controller.is_enabled():
|
||||||
|
return None
|
||||||
|
a_max = self.accel_controller.get_max_accel(v_ego)
|
||||||
|
return [ACCEL_MIN, max(ACCEL_MIN, a_max)]
|
||||||
|
|
||||||
|
def get_cruise_min_accel(self, v_ego: float) -> float | None:
|
||||||
|
if self.accel_controller.is_enabled():
|
||||||
|
return self.accel_controller.get_min_accel(v_ego)
|
||||||
|
return None
|
||||||
|
|
||||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||||
CS = sm['carState']
|
CS = sm['carState']
|
||||||
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
v_cruise_cluster_kph = min(CS.vCruiseCluster, V_CRUISE_MAX)
|
||||||
@@ -77,6 +92,7 @@ class LongitudinalPlannerSP:
|
|||||||
self.events_sp.clear()
|
self.events_sp.clear()
|
||||||
self.dec.update(sm)
|
self.dec.update(sm)
|
||||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||||
|
self.accel_controller.update(sm)
|
||||||
|
|
||||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||||
@@ -95,6 +111,8 @@ class LongitudinalPlannerSP:
|
|||||||
dec.enabled = self.dec.enabled()
|
dec.enabled = self.dec.enabled()
|
||||||
dec.active = self.dec.active()
|
dec.active = self.dec.active()
|
||||||
|
|
||||||
|
longitudinalPlanSP.accelPersonality = int(self.accel_controller.get_accel_personality())
|
||||||
|
|
||||||
# Smart Cruise Control
|
# Smart Cruise Control
|
||||||
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
smartCruiseControl = longitudinalPlanSP.smartCruiseControl
|
||||||
# Vision Control
|
# Vision Control
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
|
||||||
|
Coverage for AccelPersonalityController:
|
||||||
|
- live param flip via auto-refresh (no Python set_enabled() call needed)
|
||||||
|
- V_CRUISE_UNSET guard
|
||||||
|
- enable-transition snap to fresh target
|
||||||
|
- per-personality accel limit deltas vs stock get_max_accel
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from cereal import custom
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from opendbc.car.interfaces import ACCEL_MIN
|
||||||
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
|
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_max_accel as stock_get_max_accel
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import (
|
||||||
|
AccelPersonalityController,
|
||||||
|
PARAM_REFRESH_FRAMES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AccelPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCarState:
|
||||||
|
def __init__(self, v_cruise=30.0):
|
||||||
|
self.vCruise = v_cruise
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSM:
|
||||||
|
def __init__(self, v_cruise=30.0):
|
||||||
|
self._data = {'carState': FakeCarState(v_cruise)}
|
||||||
|
|
||||||
|
def __getitem__(self, k):
|
||||||
|
return self._data[k]
|
||||||
|
|
||||||
|
|
||||||
|
def _print_table(title, header, rows):
|
||||||
|
print(f"\n--- {title} ---")
|
||||||
|
print(" | ".join(f"{h:>12}" for h in header))
|
||||||
|
print("-" * (15 * len(header)))
|
||||||
|
for row in rows:
|
||||||
|
print(" | ".join(f"{v:>12.3f}" if isinstance(v, float) else f"{v:>12}" for v in row))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccelLiveFlip:
|
||||||
|
def test_enable_via_param(self):
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', False)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
assert not c.is_enabled()
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM())
|
||||||
|
assert c.is_enabled()
|
||||||
|
|
||||||
|
def test_disable_via_param(self):
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
assert c.is_enabled()
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', False)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM())
|
||||||
|
assert not c.is_enabled()
|
||||||
|
|
||||||
|
def test_personality_change_via_param(self):
|
||||||
|
Params().put('AccelPersonality', AccelPersonality.normal)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
assert c.get_accel_personality() == AccelPersonality.normal
|
||||||
|
Params().put('AccelPersonality', AccelPersonality.sport)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM())
|
||||||
|
assert c.get_accel_personality() == AccelPersonality.sport
|
||||||
|
|
||||||
|
def test_refresh_boundary_below_threshold(self):
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', False)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES - 1):
|
||||||
|
c.update(FakeSM())
|
||||||
|
assert not c.is_enabled()
|
||||||
|
|
||||||
|
def test_enable_transition_snaps_to_target(self):
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
Params().put('AccelPersonality', AccelPersonality.sport)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM(v_cruise=35.0))
|
||||||
|
c.get_accel_limits(25.0)
|
||||||
|
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', False)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM(v_cruise=35.0))
|
||||||
|
assert not c.is_enabled()
|
||||||
|
|
||||||
|
Params().put('AccelPersonality', AccelPersonality.eco)
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
for _ in range(PARAM_REFRESH_FRAMES + 1):
|
||||||
|
c.update(FakeSM(v_cruise=35.0))
|
||||||
|
assert c._first
|
||||||
|
|
||||||
|
def test_vcruise_unset_treated_as_zero(self):
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
c.update(FakeSM(v_cruise=V_CRUISE_UNSET))
|
||||||
|
assert c._v_cruise == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccelUsageDiff:
|
||||||
|
def test_accel_clip_per_personality(self, capsys):
|
||||||
|
rows = []
|
||||||
|
speeds = [3.0, 10.0, 20.0, 30.0]
|
||||||
|
personalities = [
|
||||||
|
('eco', AccelPersonality.eco),
|
||||||
|
('normal', AccelPersonality.normal),
|
||||||
|
('sport', AccelPersonality.sport),
|
||||||
|
]
|
||||||
|
|
||||||
|
Params().put_bool('AccelPersonalityEnabled', True)
|
||||||
|
sm = FakeSM(v_cruise=35.0)
|
||||||
|
|
||||||
|
any_delta = False
|
||||||
|
for label, p in personalities:
|
||||||
|
Params().put('AccelPersonality', p)
|
||||||
|
c = AccelPersonalityController()
|
||||||
|
c.update(sm)
|
||||||
|
for v_ego in speeds:
|
||||||
|
stock_hi = float(stock_get_max_accel(v_ego))
|
||||||
|
c_lo, c_hi = c.get_accel_limits(v_ego)
|
||||||
|
delta_hi = c_hi - stock_hi
|
||||||
|
delta_lo = c_lo - ACCEL_MIN
|
||||||
|
if abs(delta_hi) > 0.01 or abs(delta_lo) > 0.01:
|
||||||
|
any_delta = True
|
||||||
|
rows.append((label, v_ego, stock_hi, c_hi, delta_hi, c_lo, delta_lo))
|
||||||
|
|
||||||
|
with capsys.disabled():
|
||||||
|
_print_table(
|
||||||
|
"AccelPersonalityController: a_max stock vs controller",
|
||||||
|
["personality", "v_ego", "stock_hi", "ctrl_hi", "delta_hi", "ctrl_lo", "delta_lo"],
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
assert any_delta
|
||||||
@@ -255,7 +255,7 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gyro Uncalibrated
|
// Gyro Uncalibrated
|
||||||
if (log.which() == cereal::SensorEventData::GYRO_UNCALIBRATED) {
|
if (log.getSensor() == SENSOR_GYRO_UNCALIBRATED && log.getType() == SENSOR_TYPE_GYROSCOPE_UNCALIBRATED) {
|
||||||
auto v = log.getGyroUncalibrated().getV();
|
auto v = log.getGyroUncalibrated().getV();
|
||||||
auto meas = Vector3d(-v[2], -v[1], -v[0]);
|
auto meas = Vector3d(-v[2], -v[1], -v[0]);
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ void Localizer::handle_sensor(double current_time, const cereal::SensorEventData
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Accelerometer
|
// Accelerometer
|
||||||
if (log.which() == cereal::SensorEventData::ACCELERATION) {
|
if (log.getSensor() == SENSOR_ACCELEROMETER && log.getType() == SENSOR_TYPE_ACCELEROMETER) {
|
||||||
auto v = log.getAcceleration().getV();
|
auto v = log.getAcceleration().getV();
|
||||||
|
|
||||||
// TODO: reduce false positives and re-enable this check
|
// TODO: reduce false positives and re-enable this check
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ if platform.system() == 'Darwin':
|
|||||||
|
|
||||||
class TestLocationdProc:
|
class TestLocationdProc:
|
||||||
LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration',
|
LLD_MSGS = ['gpsLocationExternal', 'cameraOdometry', 'carState', 'liveCalibration',
|
||||||
'accelerometer', 'gyroscope']
|
'accelerometer', 'gyroscope', 'magnetometer']
|
||||||
|
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.pm = messaging.PubMaster(self.LLD_MSGS)
|
self.pm = messaging.PubMaster(self.LLD_MSGS)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|||||||
This file is part of sunnypilot and is licensed under the MIT License.
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
@@ -18,7 +17,6 @@ RELAXED_MIN_BUCKET_POINTS = np.array([1, 200, 300, 500, 500, 300, 200, 1])
|
|||||||
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
|
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TorqueEstimatorExt:
|
class TorqueEstimatorExt:
|
||||||
def __init__(self, CP: car.CarParams):
|
def __init__(self, CP: car.CarParams):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
@@ -28,6 +26,7 @@ class TorqueEstimatorExt:
|
|||||||
self.enforce_torque_control_toggle = self._params.get_bool("EnforceTorqueControl") # only during init
|
self.enforce_torque_control_toggle = self._params.get_bool("EnforceTorqueControl") # only during init
|
||||||
self.use_params = self.CP.brand in ALLOWED_CARS and self.CP.lateralTuning.which() == 'torque'
|
self.use_params = self.CP.brand in ALLOWED_CARS and self.CP.lateralTuning.which() == 'torque'
|
||||||
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
||||||
|
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
|
||||||
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
||||||
self.min_bucket_points = RELAXED_MIN_BUCKET_POINTS
|
self.min_bucket_points = RELAXED_MIN_BUCKET_POINTS
|
||||||
self.factor_sanity = 0.0
|
self.factor_sanity = 0.0
|
||||||
@@ -51,13 +50,14 @@ class TorqueEstimatorExt:
|
|||||||
def _update_params(self):
|
def _update_params(self):
|
||||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||||
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
||||||
|
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
|
||||||
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
||||||
|
|
||||||
def update_use_params(self):
|
def update_use_params(self):
|
||||||
self._update_params()
|
self._update_params()
|
||||||
|
|
||||||
if self.enforce_torque_control_toggle:
|
if self.enforce_torque_control_toggle:
|
||||||
if self.torque_override_enabled:
|
if self.custom_torque_params and self.torque_override_enabled:
|
||||||
self.use_params = False
|
self.use_params = False
|
||||||
else:
|
else:
|
||||||
self.use_params = self.use_live_torque_params
|
self.use_params = self.use_live_torque_params
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import time
|
|||||||
|
|
||||||
from jsonrpc import dispatcher
|
from jsonrpc import dispatcher
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params, ParamKeyType
|
||||||
from openpilot.common.realtime import set_core_affinity
|
from openpilot.common.realtime import set_core_affinity
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.hardware.hw import Paths
|
from openpilot.system.hardware.hw import Paths
|
||||||
@@ -28,11 +28,14 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
|
|||||||
create_connection, WebSocketConnectionClosedException)
|
create_connection, WebSocketConnectionClosedException)
|
||||||
|
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param
|
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
|
||||||
|
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
|
||||||
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
||||||
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
||||||
|
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
|
||||||
|
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
|
||||||
|
|
||||||
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai')
|
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
|
||||||
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
|
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
|
||||||
LOCAL_PORT_WHITELIST = {8022}
|
LOCAL_PORT_WHITELIST = {8022}
|
||||||
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
||||||
@@ -44,12 +47,15 @@ params = Params()
|
|||||||
|
|
||||||
# Parameters that should never be remotely modified
|
# Parameters that should never be remotely modified
|
||||||
BLOCKED_PARAMS = {
|
BLOCKED_PARAMS = {
|
||||||
|
"AdbEnabled",
|
||||||
"CompletedSunnylinkConsentVersion",
|
"CompletedSunnylinkConsentVersion",
|
||||||
"CompletedTrainingVersion",
|
"CompletedTrainingVersion",
|
||||||
"GithubUsername", # Could grant SSH access
|
"GithubUsername", # Could grant SSH access
|
||||||
"GithubSshKeys", # Direct SSH key injection
|
"GithubSshKeys", # Direct SSH key injection
|
||||||
"HasAcceptedTerms",
|
"HasAcceptedTerms",
|
||||||
"HasAcceptedTermsSP",
|
"HasAcceptedTermsSP",
|
||||||
|
"OnroadCycleRequested", # Prevent remote cycle trigger
|
||||||
|
"ParamsVersion", # Device-managed version counter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -199,34 +205,19 @@ def getParamsAllKeysV1() -> dict[str, str]:
|
|||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def getParamsMetadata() -> str:
|
def getParamsMetadata() -> str:
|
||||||
"""Compressed equivalent of getParamsAllKeysV1 — same struct, gzipped + base64."""
|
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
|
||||||
|
|
||||||
|
Reads settings_ui.json, injects live capabilities from CarParams, compresses,
|
||||||
|
and returns. Single RPC for the frontend to get the complete settings UI and
|
||||||
|
runtime capabilities.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with open(METADATA_PATH) as f:
|
schema = generate_schema()
|
||||||
metadata = json.load(f)
|
schema["capabilities"] = generate_capabilities()
|
||||||
except Exception:
|
schema["capability_labels"] = CAPABILITY_LABELS
|
||||||
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
|
schema["default_model"] = DEFAULT_MODEL
|
||||||
metadata = {}
|
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
|
||||||
|
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
|
||||||
try:
|
|
||||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
|
||||||
|
|
||||||
params_list: list[dict] = []
|
|
||||||
for key in available_keys:
|
|
||||||
value = get_param_as_byte(key, get_default=True)
|
|
||||||
|
|
||||||
param_entry: dict = {
|
|
||||||
"key": key,
|
|
||||||
"type": int(params.get_type(key).value),
|
|
||||||
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
if key in metadata:
|
|
||||||
param_entry["_extra"] = metadata[key]
|
|
||||||
|
|
||||||
params_list.append(param_entry)
|
|
||||||
|
|
||||||
raw = json.dumps(params_list, separators=(',', ':')).encode('utf-8')
|
|
||||||
return base64.b64encode(gzip.compress(raw)).decode('utf-8')
|
|
||||||
except Exception:
|
except Exception:
|
||||||
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
|
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
|
||||||
raise
|
raise
|
||||||
@@ -238,12 +229,25 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s
|
|||||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
zero_values: dict[int, bytes] = {
|
||||||
|
ParamKeyType.STRING.value: b"",
|
||||||
|
ParamKeyType.BOOL.value: b"0",
|
||||||
|
ParamKeyType.INT.value: b"0",
|
||||||
|
ParamKeyType.FLOAT.value: b"0.0",
|
||||||
|
ParamKeyType.TIME.value: b"",
|
||||||
|
ParamKeyType.JSON.value: b"{}",
|
||||||
|
ParamKeyType.BYTES.value: b"",
|
||||||
|
}
|
||||||
|
|
||||||
param_keys_validated = [key for key in params_keys if key in available_keys]
|
param_keys_validated = [key for key in params_keys if key in available_keys]
|
||||||
params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []}
|
params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []}
|
||||||
for key in param_keys_validated:
|
for key in param_keys_validated:
|
||||||
value = get_param_as_byte(key)
|
value = get_param_as_byte(key)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
value = get_param_as_byte(key, get_default=True)
|
||||||
|
if value is None:
|
||||||
|
param_type = params.get_type(key)
|
||||||
|
value = zero_values.get(param_type.value, b"")
|
||||||
|
|
||||||
params_dict["params"].append({
|
params_dict["params"].append({
|
||||||
"key": key,
|
"key": key,
|
||||||
@@ -274,6 +278,13 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
cloudlog.error(f"sunnylinkd.saveParams.exception {e}")
|
cloudlog.error(f"sunnylinkd.saveParams.exception {e}")
|
||||||
|
|
||||||
|
# Increment version counter for frontend change detection
|
||||||
|
try:
|
||||||
|
current = int(params.get("ParamsVersion") or "0")
|
||||||
|
params.put("ParamsVersion", str(current + 1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
||||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||||
|
|
||||||
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
|
See the LICENSE.md file in the root directory for more details.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from cereal import car, custom, messaging
|
||||||
|
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
||||||
|
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
|
||||||
|
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
|
|
||||||
|
|
||||||
|
# Wire-protocol version for the capabilities payload. Bump on breaking changes
|
||||||
|
# only; additive fields are backward-compatible and do not require a bump.
|
||||||
|
PROTOCOL_VERSION = 1
|
||||||
|
|
||||||
|
# All capability fields that rules may reference.
|
||||||
|
# Non-boolean fields must have defaults in CAPABILITY_DEFAULTS.
|
||||||
|
CAPABILITY_FIELDS = (
|
||||||
|
"protocol_version",
|
||||||
|
"has_longitudinal_control",
|
||||||
|
"has_icbm",
|
||||||
|
"icbm_available",
|
||||||
|
"torque_allowed",
|
||||||
|
"brand",
|
||||||
|
"pcm_cruise",
|
||||||
|
"alpha_long_available",
|
||||||
|
"steer_control_type",
|
||||||
|
"enable_bsm",
|
||||||
|
"is_release",
|
||||||
|
"is_sp_release",
|
||||||
|
"is_development",
|
||||||
|
"tesla_has_vehicle_bus",
|
||||||
|
"has_stop_and_go",
|
||||||
|
"stock_longitudinal",
|
||||||
|
"device_type",
|
||||||
|
"subaru_has_sng",
|
||||||
|
"hyundai_alpha_long_available",
|
||||||
|
)
|
||||||
|
|
||||||
|
CAPABILITY_LABELS: dict[str, str] = {
|
||||||
|
"protocol_version": "Capabilities protocol version",
|
||||||
|
"has_longitudinal_control": "sunnypilot longitudinal control",
|
||||||
|
"has_icbm": "ICBM enabled",
|
||||||
|
"icbm_available": "ICBM available",
|
||||||
|
"torque_allowed": "torque steering (not available for angle steering vehicles)",
|
||||||
|
"brand": "Vehicle brand",
|
||||||
|
"pcm_cruise": "PCM cruise",
|
||||||
|
"alpha_long_available": "Alpha Longitudinal available",
|
||||||
|
"steer_control_type": "Steer control type",
|
||||||
|
"enable_bsm": "BSM available",
|
||||||
|
"is_release": "Release branch",
|
||||||
|
"is_sp_release": "SP release branch",
|
||||||
|
"is_development": "Development branch",
|
||||||
|
"tesla_has_vehicle_bus": "Tesla vehicle bus",
|
||||||
|
"has_stop_and_go": "Stop and Go",
|
||||||
|
"stock_longitudinal": "stock longitudinal",
|
||||||
|
"device_type": "Device type",
|
||||||
|
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||||
|
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Explicit defaults for non-boolean capability fields
|
||||||
|
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
|
||||||
|
"brand": "",
|
||||||
|
"steer_control_type": "",
|
||||||
|
"device_type": "",
|
||||||
|
"protocol_version": PROTOCOL_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_field(bundle: dict | None, key: str) -> str:
|
||||||
|
return bundle.get(key, "") if isinstance(bundle, dict) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||||
|
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
|
||||||
|
|
||||||
|
Bundle (manual car selection) is a pre-fingerprint approximation.
|
||||||
|
CarParams (auto-fingerprint) is the authoritative post-fingerprint source.
|
||||||
|
Mirrors the per-brand update_settings() logic in device UI layouts.
|
||||||
|
"""
|
||||||
|
brand = caps["brand"]
|
||||||
|
|
||||||
|
if brand == "hyundai":
|
||||||
|
if bundle_platform:
|
||||||
|
try:
|
||||||
|
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
|
||||||
|
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
|
||||||
|
except KeyError:
|
||||||
|
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
|
||||||
|
elif CP is not None:
|
||||||
|
caps["hyundai_alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
|
||||||
|
|
||||||
|
elif brand == "subaru":
|
||||||
|
if bundle_platform:
|
||||||
|
try:
|
||||||
|
flags = SUBARU_CAR[bundle_platform].config.flags
|
||||||
|
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||||
|
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||||
|
except KeyError:
|
||||||
|
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
|
||||||
|
elif CP is not None:
|
||||||
|
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||||
|
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_capabilities(params: Params | None = None) -> dict:
|
||||||
|
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
||||||
|
|
||||||
|
When CarPlatformBundle is present, brand and platform come from the bundle
|
||||||
|
(mirrors Raylib). CarParams* deserialization is the fallback before the bundle
|
||||||
|
is written (early after first pairing).
|
||||||
|
"""
|
||||||
|
params = params or Params()
|
||||||
|
|
||||||
|
caps: dict = {field: CAPABILITY_DEFAULTS.get(field, False) for field in CAPABILITY_FIELDS}
|
||||||
|
|
||||||
|
# Wire-protocol version is always set explicitly.
|
||||||
|
caps["protocol_version"] = PROTOCOL_VERSION
|
||||||
|
|
||||||
|
# Hardware + boolean params (no CarParams dependency)
|
||||||
|
caps["device_type"] = HARDWARE.get_device_type()
|
||||||
|
caps["is_release"] = False # params.get_bool("IsReleaseBranch")
|
||||||
|
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
|
||||||
|
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
||||||
|
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
||||||
|
|
||||||
|
bundle = params.get("CarPlatformBundle")
|
||||||
|
bundle_brand = _bundle_field(bundle, "brand")
|
||||||
|
bundle_platform = _bundle_field(bundle, "platform")
|
||||||
|
|
||||||
|
# Bundle-first brand resolution; CP is fallback only.
|
||||||
|
if bundle_brand:
|
||||||
|
caps["brand"] = bundle_brand
|
||||||
|
|
||||||
|
# CarParams-derived capabilities
|
||||||
|
CP = None
|
||||||
|
CP_bytes = params.get("CarParamsPersistent")
|
||||||
|
if CP_bytes is not None:
|
||||||
|
try:
|
||||||
|
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
|
||||||
|
caps["alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
|
||||||
|
if CP.alphaLongitudinalAvailable:
|
||||||
|
caps["has_longitudinal_control"] = params.get_bool("AlphaLongitudinalEnabled")
|
||||||
|
else:
|
||||||
|
caps["has_longitudinal_control"] = bool(CP.openpilotLongitudinalControl)
|
||||||
|
# CP.steerControlType is the physical control mode (angle / torque).
|
||||||
|
# CP.lateralTuning.which() returns the tuning class (pid / torque / indi)
|
||||||
|
# which is a separate concept and is not interchangeable.
|
||||||
|
caps["steer_control_type"] = str(CP.steerControlType)
|
||||||
|
caps["torque_allowed"] = CP.steerControlType != car.CarParams.SteerControlType.angle
|
||||||
|
if not caps["brand"] and CP.brand:
|
||||||
|
caps["brand"] = str(CP.brand)
|
||||||
|
caps["pcm_cruise"] = bool(CP.pcmCruise)
|
||||||
|
caps["enable_bsm"] = bool(CP.enableBsm)
|
||||||
|
# Generic SnG fallback. Brand-specific opaque flags below override.
|
||||||
|
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
|
||||||
|
except Exception:
|
||||||
|
CP = None
|
||||||
|
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
|
||||||
|
|
||||||
|
# CarParamsSP-derived capabilities
|
||||||
|
CP_SP_bytes = params.get("CarParamsSPPersistent")
|
||||||
|
if CP_SP_bytes is not None:
|
||||||
|
try:
|
||||||
|
CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||||
|
caps["icbm_available"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable)
|
||||||
|
caps["has_icbm"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable) and params.get_bool("IntelligentCruiseButtonManagement")
|
||||||
|
caps["tesla_has_vehicle_bus"] = bool(CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||||
|
except Exception:
|
||||||
|
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
|
||||||
|
|
||||||
|
_resolve_brand_capabilities(caps, bundle_platform, CP)
|
||||||
|
|
||||||
|
return caps
|
||||||
|
|
||||||
|
|
||||||
|
def generate_capabilities_json(params: Params | None = None) -> str:
|
||||||
|
"""Generate SettingsCapabilities as a JSON string."""
|
||||||
|
return json.dumps(generate_capabilities(params), separators=(",", ":"))
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
# sunnylink Settings UI Guide
|
||||||
|
|
||||||
|
> One YAML file per page. Edit, run the compiler, commit. The sunnylink frontend updates automatically.
|
||||||
|
|
||||||
|
## What you edit (and what's generated)
|
||||||
|
|
||||||
|
| File | What | When to edit |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `settings_ui_src/pages/<page>.yaml` | One YAML per page (panel). Contains panel metadata + sections + items + sub_panels inline. | Adding/changing/removing a setting. |
|
||||||
|
| `settings_ui_src/pages/vehicle.yaml` | Per-brand settings page (`kind: vehicle`). Each brand is a section. | Adding/changing a vehicle-specific setting. |
|
||||||
|
| `settings_ui_src/_macros.yaml` | Named rule fragments referenced via `{$ref: "#/macros/<name>"}`. | Adding a reusable rule (e.g. a new platform gate). |
|
||||||
|
| **`settings_ui.json`** | **Generated from src tree by `compile_settings_ui.py`. Do not edit by hand.** | Never. Compiler emits it; frontend reads it. |
|
||||||
|
|
||||||
|
Pages today: `steering, cruise, display, visuals, toggles, device, software, developer, models, vehicle` (10).
|
||||||
|
|
||||||
|
Run `python sunnypilot/sunnylink/tools/compile_settings_ui.py` after edits. Add `--check` in CI to fail on out-of-sync `settings_ui.json`.
|
||||||
|
|
||||||
|
Display metadata (titles, descriptions, options, min/max/step/unit) is inline on each item. There is no separate metadata file.
|
||||||
|
|
||||||
|
## Page file shape
|
||||||
|
|
||||||
|
A page YAML contains the whole panel: metadata at the top, then `sections`. Each section has its own `items` and (optionally) `sub_panels`. Sub-panels are nested inside the section they belong to. Items appear in the order written in the file.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=../_schemas/page.schema.json
|
||||||
|
id: steering
|
||||||
|
label: Steering
|
||||||
|
icon: steering_wheel
|
||||||
|
order: 1
|
||||||
|
remote_configurable: true
|
||||||
|
description: Lateral control, lane changes, and steering behavior
|
||||||
|
|
||||||
|
sections:
|
||||||
|
- id: mads
|
||||||
|
title: Modular Assistive Driving System (MADS)
|
||||||
|
items:
|
||||||
|
- key: Mads
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Modular Assistive Driving System (MADS)
|
||||||
|
description: |
|
||||||
|
Enable the beloved MADS feature. Disable toggle to revert back
|
||||||
|
to stock sunnypilot engagement/disengagement.
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
|
||||||
|
sub_panels:
|
||||||
|
- id: mads_settings
|
||||||
|
label: MADS Settings
|
||||||
|
trigger_key: Mads
|
||||||
|
trigger_condition: {type: param, key: Mads, equals: true}
|
||||||
|
items:
|
||||||
|
- key: MadsMainCruiseAllowed
|
||||||
|
widget: toggle
|
||||||
|
title: Toggle with Main Cruise
|
||||||
|
description: |
|
||||||
|
Note: For vehicles without LFA/LKAS button, disabling this will
|
||||||
|
prevent lateral control engagement.
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {$ref: "#/macros/mads_full_platforms"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The vehicle page has the same shape but declares `kind: vehicle`; each section's `id` becomes a brand key under `vehicle_settings` in the compiled JSON.
|
||||||
|
|
||||||
|
## Macros (named rule fragments)
|
||||||
|
|
||||||
|
`_macros.yaml` declares reusable rule lists. Reference them from any rules array via `{$ref: "#/macros/<name>"}`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
macros:
|
||||||
|
offroad: [{type: offroad_only}]
|
||||||
|
longitudinal: [{type: capability, field: has_longitudinal_control, equals: true}]
|
||||||
|
mads_full_platforms:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: any
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: rivian}
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: tesla}
|
||||||
|
- type: not
|
||||||
|
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
|
||||||
|
```
|
||||||
|
|
||||||
|
In an item:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {$ref: "#/macros/mads_full_platforms"}
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiler splices a list-context `$ref` into its parent list. Macros may reference other macros up to depth 3; cycles are an error.
|
||||||
|
|
||||||
|
## Compiler workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. common/params_keys.h — add/remove the C++ param key
|
||||||
|
2. params_metadata.json — automated via update_params_metadata.py
|
||||||
|
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||||
|
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||||
|
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||||
|
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||||
|
7. commit
|
||||||
|
```
|
||||||
|
|
||||||
|
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
|
||||||
|
|
||||||
|
## Compiled output reference (schema contract)
|
||||||
|
|
||||||
|
The tables below describe the **compiled** `settings_ui.json` schema — what the frontend consumes at runtime. JSON snippets show the wire shape; in the src tree you author YAML that compiles to the same shape. Use these as a contract reference for valid fields, their meanings, and rule types.
|
||||||
|
|
||||||
|
## Quick reference: widget types
|
||||||
|
|
||||||
|
| Widget | Use for | Fields needed |
|
||||||
|
|--------|---------|---------------|
|
||||||
|
| `toggle` | On/off boolean | `title` |
|
||||||
|
| `multiple_button` | 2-4 discrete options | `title` + `options` array |
|
||||||
|
| `option` | Numeric range or dropdown | `title` + `min/max/step` or `options` |
|
||||||
|
| `info` | Read-only display | `title` |
|
||||||
|
|
||||||
|
## Quick reference: item fields
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `key` | Yes | Param key name (must exist in `params_keys.h`) |
|
||||||
|
| `widget` | Yes | `toggle`, `option`, `multiple_button`, `button`, `info` |
|
||||||
|
| `title` | Yes | Display name shown to the user |
|
||||||
|
| `description` | No | Inline explanatory text below the title. May be empty when only `details` is used. |
|
||||||
|
| `details` | No | Extended help text shown in a modal when the user taps an "i" button on the row. Independent of `description`: either, both, or neither may be present. |
|
||||||
|
| `options` | For selectors | Array of `{"value": 0, "label": "Off"}` objects (see per-option enablement below) |
|
||||||
|
| `min`, `max`, `step` | For sliders | Numeric range constraints |
|
||||||
|
| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (resolved by IsMetric) |
|
||||||
|
| `visibility` | No | Rules for show/hide. Settings are never hidden, always dimmed with UNAVAILABLE badge when rules fail |
|
||||||
|
| `enablement` | No | Rules for enabled/disabled (all must pass). Dimmed with badge when rules fail |
|
||||||
|
| `blocked` | No | `true` for device-only settings that cannot be modified remotely. Frontend shows as read-only |
|
||||||
|
| `title_param_suffix` | No | Dynamic title suffix. Example: `{"param": "IsMetric", "values": {"0": "mph", "1": "km/h"}}` |
|
||||||
|
| `sub_items` | No | Nested child items |
|
||||||
|
| `needs_onroad_cycle` | No | `true` if changing this param triggers a system restart. Frontend shows a "Restart" badge. See [REFERENCE.md - Remote Onroad Cycle](REFERENCE.md#remote-onroad-cycle) |
|
||||||
|
|
||||||
|
## Quick reference: rule types
|
||||||
|
|
||||||
|
| Rule | Example | Use for |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `offroad_only` | `{"type": "offroad_only"}` | Grey out while driving |
|
||||||
|
| `not_engaged` | `{"type": "not_engaged"}` | Grey out only while engaged (started + selfdrive/MADS active) |
|
||||||
|
| `capability` | `{"type": "capability", "field": "has_longitudinal_control", "equals": true}` | Car-dependent visibility |
|
||||||
|
| `param` | `{"type": "param", "key": "Mads", "equals": true}` | Show/enable based on another setting |
|
||||||
|
| `param_compare` | `{"type": "param_compare", "key": "SpeedLimitMode", "op": ">", "value": 0}` | Numeric comparison |
|
||||||
|
| `not` | `{"type": "not", "condition": {...}}` | Negate a rule |
|
||||||
|
| `any` | `{"type": "any", "conditions": [...]}` | OR logic |
|
||||||
|
| `all` | `{"type": "all", "conditions": [...]}` | AND logic (for nesting inside `any`/`not`) |
|
||||||
|
| `$ref` | `{"$ref": "#/macros/offroad"}` | Reference a named rule fragment in `_macros.yaml` |
|
||||||
|
|
||||||
|
**Visibility design**: Settings are always visible. When visibility rules fail, the setting is dimmed with an UNAVAILABLE badge, so users know it exists but is not applicable.
|
||||||
|
|
||||||
|
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
|
||||||
|
|
||||||
|
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `enable_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to
|
||||||
|
|
||||||
|
### Pick a writability rule (offroad / not_engaged / param-based)
|
||||||
|
|
||||||
|
| Use this | When | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| `offroad_only` | Param can only be safely changed when the car is parked. Most user-facing toggles. | Strictest. Frontend shows "device is driving" badge and disables the row. |
|
||||||
|
| `not_engaged` | Param can be changed while the car is started but only when sunnypilot/MADS is **not** actively driving. | Less strict than offroad. Matches Raylib `engaged = started AND (selfdriveState.enabled OR mads.enabled)`. Use for items the device must apply mid-drive (e.g. test maneuvers, longitudinal stock-vs-OP toggle). |
|
||||||
|
| `param`-based | Behavior depends on another setting's value (parent toggle, mode selector, etc.). | Composes with `not`/`any`/`all` for arbitrary logic. |
|
||||||
|
| `capability`-based | Behavior depends on the connected car or device (brand, longitudinal, hardware). | Resolved on the device from `CarParams` / hardware. See [`capabilities.py`](../capabilities.py) for the full field list. |
|
||||||
|
| (no rule) | Param is always writable, no gating. | Rare. Prefer at least `offroad_only` unless the param is genuinely safe to flip mid-drive. |
|
||||||
|
|
||||||
|
Default for new toggles: `enablement: [{$ref: "#/macros/offroad"}]`. Drop down to `not_engaged` only if you've confirmed mid-drive write is safe in the controls/UI code path.
|
||||||
|
|
||||||
|
### Use `details` for safety notes / extended help
|
||||||
|
|
||||||
|
Inline `description` shows under the title. For longer caveats, safety notes, or "learn more" content, use `details` — the frontend renders an info button that opens a modal. Either field may be present alone or both together.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: AutoLaneChangeTimer
|
||||||
|
widget: option
|
||||||
|
title: Auto Lane Change by Blinker
|
||||||
|
description: |-
|
||||||
|
Set a timer to delay the auto lane change operation when the blinker is used.
|
||||||
|
No nudge on the steering wheel is required to auto lane change if a timer is set.
|
||||||
|
Default is Nudge.
|
||||||
|
details: |-
|
||||||
|
Please use caution when using this feature. Only use the blinker when traffic
|
||||||
|
and road conditions permit.
|
||||||
|
options: [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
For an item that is intentionally minimal inline (no inline body, only the modal):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: SomeAdvancedToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Some Advanced Feature
|
||||||
|
details: |-
|
||||||
|
Long-form rationale, caveats, links, etc. — kept entirely behind the info button.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a toggle
|
||||||
|
|
||||||
|
1. Register in `common/params_keys.h`:
|
||||||
|
```cpp
|
||||||
|
{"MyToggle", {PERSISTENT | BACKUP, BOOL}},
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open `settings_ui_src/pages/<page>.yaml`. Add the item to the right section:
|
||||||
|
```yaml
|
||||||
|
- key: MyToggle
|
||||||
|
widget: toggle
|
||||||
|
title: My Feature
|
||||||
|
description: What this feature does.
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If changing the param requires an onroad cycle to take effect, add `needs_onroad_cycle: true`.
|
||||||
|
|
||||||
|
3. Compile + validate + test:
|
||||||
|
```
|
||||||
|
python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||||
|
python sunnypilot/sunnylink/tools/validate_settings_ui.py
|
||||||
|
uv run python -m pytest sunnypilot/sunnylink/tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a multi-button option
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: MySelector
|
||||||
|
widget: multiple_button
|
||||||
|
title: Mode
|
||||||
|
options:
|
||||||
|
- {value: 0, label: Off}
|
||||||
|
- {value: 1, label: On}
|
||||||
|
- {value: 2, label: Auto}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a slider or range
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: MyRange
|
||||||
|
widget: option
|
||||||
|
title: Follow Distance
|
||||||
|
description: Time gap to lead vehicle.
|
||||||
|
min: 0.5
|
||||||
|
max: 3.0
|
||||||
|
step: 0.1
|
||||||
|
unit: seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a slider with metric/imperial units
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: MinSpeed
|
||||||
|
widget: option
|
||||||
|
title: Minimum Speed
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 5
|
||||||
|
unit: {metric: km/h, imperial: mph}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend resolves the unit string based on the device's `IsMetric` param. Static units (e.g. `seconds`, `m/s²`) stay plain strings.
|
||||||
|
|
||||||
|
### Add a dynamic title suffix
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: FollowDistance
|
||||||
|
widget: option
|
||||||
|
title: Follow Distance
|
||||||
|
title_param_suffix:
|
||||||
|
param: IsMetric
|
||||||
|
values: {'0': mph, '1': km/h}
|
||||||
|
min: 0.5
|
||||||
|
max: 3.0
|
||||||
|
step: 0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders as "Follow Distance: mph" / "Follow Distance: km/h".
|
||||||
|
|
||||||
|
### Add a device-only read-only setting
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: OnroadCyclePendingRemote
|
||||||
|
widget: info
|
||||||
|
title: Pending Remote Cycle
|
||||||
|
blocked: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend treats `blocked: true` items as read-only.
|
||||||
|
|
||||||
|
### Add a dropdown option
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: MyDropdown
|
||||||
|
widget: option
|
||||||
|
title: Recording Quality
|
||||||
|
options:
|
||||||
|
- {value: 0, label: Low (720p)}
|
||||||
|
- {value: 1, label: Medium (1080p)}
|
||||||
|
- {value: 2, label: High (4K)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-option enablement rules
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: MadsSteeringMode
|
||||||
|
widget: multiple_button
|
||||||
|
title: Steering Mode on Brake Pedal
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Remain Active
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/mads_full_platforms"}
|
||||||
|
- value: 1
|
||||||
|
label: Pause
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/mads_full_platforms"}
|
||||||
|
- value: 2
|
||||||
|
label: Disengage
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
```
|
||||||
|
|
||||||
|
When an option's enablement fails, that option is grayed out but still visible.
|
||||||
|
|
||||||
|
### Show only when another setting is on
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: ChildSetting
|
||||||
|
widget: toggle
|
||||||
|
title: Child Feature
|
||||||
|
visibility:
|
||||||
|
- {type: param, key: ParentToggle, equals: true}
|
||||||
|
```
|
||||||
|
|
||||||
|
(With the "dim instead of hide" design, this setting is dimmed, not hidden, when the rule fails.)
|
||||||
|
|
||||||
|
### Show only for specific brands
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: LongFeature
|
||||||
|
widget: toggle
|
||||||
|
title: Longitudinal Feature
|
||||||
|
visibility:
|
||||||
|
- {$ref: "#/macros/longitudinal"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combine multiple conditions
|
||||||
|
|
||||||
|
The `enablement` array is implicit-AND: every entry must pass. Use `any` for OR, `all` for nested AND, `not` for negation. Wrap repeated combinations in a macro so future you doesn't re-derive the logic.
|
||||||
|
|
||||||
|
**AND across two params** (writable only when both Mads is on AND ICBM is enabled):
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- {type: param, key: Mads, equals: true}
|
||||||
|
- {type: param, key: IntelligentCruiseButtonManagement, equals: true}
|
||||||
|
```
|
||||||
|
|
||||||
|
**OR across two params** (writable when either is on):
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- {type: param, key: ExperimentalMode, equals: true}
|
||||||
|
- {type: param, key: DynamicExperimentalControl, equals: true}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mixed: capability AND param** (only on longitudinal cars when ShowAdvancedControls is on):
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/longitudinal"}
|
||||||
|
- {$ref: "#/macros/advanced_only"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Three-way: offroad AND torque-allowed AND not-NNLC** (real example: `EnforceTorqueControl`):
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {type: capability, field: torque_allowed, equals: true}
|
||||||
|
- {type: param, key: NeuralNetworkLateralControl, equals: false}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Negation across multiple platforms** (everything except Rivian + Tesla-no-bus):
|
||||||
|
```yaml
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {$ref: "#/macros/mads_full_platforms"} # macro encapsulates the not(any(rivian, all(tesla, not(bus)))) logic
|
||||||
|
```
|
||||||
|
|
||||||
|
If the same multi-condition block appears in 2+ items, **promote it to a macro** in `_macros.yaml`. Re-run `python sunnypilot/sunnylink/tools/apply_macros.py` to substitute existing inlined matches automatically.
|
||||||
|
|
||||||
|
### Mutual exclusion
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- key: FeatureAlpha
|
||||||
|
widget: toggle
|
||||||
|
title: Feature Alpha
|
||||||
|
enablement:
|
||||||
|
- {type: param, key: FeatureBeta, equals: false}
|
||||||
|
|
||||||
|
- key: FeatureBeta
|
||||||
|
widget: toggle
|
||||||
|
title: Feature Beta
|
||||||
|
enablement:
|
||||||
|
- {type: param, key: FeatureAlpha, equals: false}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a section
|
||||||
|
|
||||||
|
In the page YAML, add an entry to the `sections` list:
|
||||||
|
```yaml
|
||||||
|
sections:
|
||||||
|
- id: my_section
|
||||||
|
title: My Section
|
||||||
|
description: Optional subtitle
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/longitudinal"}
|
||||||
|
items:
|
||||||
|
- {key: ..., widget: toggle, title: ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sections support `visibility`, `enablement`, and `attestation_required`. When section-level rules fail, all items within are dimmed.
|
||||||
|
|
||||||
|
### Add a sub-panel
|
||||||
|
|
||||||
|
Sub-panels nest inside the section they belong to:
|
||||||
|
```yaml
|
||||||
|
sections:
|
||||||
|
- id: parent_section
|
||||||
|
title: Parent
|
||||||
|
items: [...]
|
||||||
|
sub_panels:
|
||||||
|
- id: my_sub
|
||||||
|
label: Advanced Settings
|
||||||
|
trigger_key: ParentParam
|
||||||
|
trigger_condition: {type: param, key: ParentParam, equals: true}
|
||||||
|
items:
|
||||||
|
- {key: ..., widget: toggle, title: ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add vehicle-brand settings
|
||||||
|
|
||||||
|
Edit `pages/vehicle.yaml`. Each section is a brand:
|
||||||
|
```yaml
|
||||||
|
id: vehicle
|
||||||
|
kind: vehicle
|
||||||
|
sections:
|
||||||
|
- id: rivian
|
||||||
|
title: Rivian Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: RivianFeature
|
||||||
|
widget: toggle
|
||||||
|
title: Rivian One Pedal
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`kind: vehicle` tells the compiler to emit this page as `vehicle_settings.<brand>` in the wire JSON.
|
||||||
|
|
||||||
|
### Add a feature with toggles, sub-panel, and macro
|
||||||
|
|
||||||
|
Example: "Smart Wipers" with a master toggle, intensity selector, and sub-panel for advanced tuning, gated to torque-steering Hyundais on offroad.
|
||||||
|
|
||||||
|
1. **Param keys** — register all 4 in `common/params_keys.h`.
|
||||||
|
|
||||||
|
2. **Decide on a macro** — if "torque Hyundai" gating is reused, add to `_macros.yaml`:
|
||||||
|
```yaml
|
||||||
|
torque_hyundai:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {type: capability, field: brand, equals: hyundai}
|
||||||
|
- {type: capability, field: torque_allowed, equals: true}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Edit the relevant page** — `pages/visuals.yaml` (or wherever the feature lives). Add a new section + sub_panel:
|
||||||
|
```yaml
|
||||||
|
sections:
|
||||||
|
- id: smart_wipers
|
||||||
|
title: Smart Wipers
|
||||||
|
description: Camera-driven wiper control (Hyundai/Kia, torque only)
|
||||||
|
items:
|
||||||
|
- key: SmartWipersEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Smart Wipers
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/torque_hyundai"}
|
||||||
|
- key: SmartWipersIntensity
|
||||||
|
widget: multiple_button
|
||||||
|
title: Sensitivity
|
||||||
|
options:
|
||||||
|
- {value: 0, label: Low}
|
||||||
|
- {value: 1, label: Medium}
|
||||||
|
- {value: 2, label: High}
|
||||||
|
visibility:
|
||||||
|
- {type: param, key: SmartWipersEnabled, equals: true}
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/torque_hyundai"}
|
||||||
|
sub_panels:
|
||||||
|
- id: smart_wipers_tuning
|
||||||
|
label: Smart Wipers Tuning
|
||||||
|
trigger_key: SmartWipersEnabled
|
||||||
|
trigger_condition: {type: param, key: SmartWipersEnabled, equals: true}
|
||||||
|
items:
|
||||||
|
- key: SmartWipersHysteresis
|
||||||
|
widget: option
|
||||||
|
title: Hysteresis (frames)
|
||||||
|
min: 1
|
||||||
|
max: 30
|
||||||
|
step: 1
|
||||||
|
enablement:
|
||||||
|
- {$ref: "#/macros/offroad"}
|
||||||
|
- {$ref: "#/macros/advanced_only"}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Compile / validate / test**:
|
||||||
|
```
|
||||||
|
python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||||
|
python sunnypilot/sunnylink/tools/validate_settings_ui.py
|
||||||
|
uv run python -m pytest sunnypilot/sunnylink/tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
`apply_macros.py` is automatic for newly-added items only if you wrote the rule list inline; for greenfield items, you'd write `$ref` directly.
|
||||||
|
|
||||||
|
### Change a toggle's behavior
|
||||||
|
|
||||||
|
1. Find the item in `pages/<page>.yaml`.
|
||||||
|
2. Edit `visibility`/`enablement`/`options[].enablement` directly. Use macros where possible.
|
||||||
|
3. **Add a regression test** in `sunnypilot/sunnylink/tests/test_settings_changes.py` that asserts the new gate exists. Use existing tests (e.g. `TestMadsBrandGates`, `TestNotEngagedReplacement`) as templates: lookup item by key, assert `_references_capability_field(rules, "...")` or `_flatten_rule_types(rules)` contains/excludes a type. This freezes the new behavior so a future edit won't silently revert it.
|
||||||
|
4. Compile + run the full suite. Per-bug test should pass; structural tests should remain green.
|
||||||
|
|
||||||
|
### Change a widget type or options
|
||||||
|
|
||||||
|
Editing `widget:` from `toggle` to `multiple_button` is a frontend behavior change. Whenever you change widget shape:
|
||||||
|
- The param's underlying type (bool / int / string) must match what the new widget writes. `toggle` writes bool; `multiple_button`/`option` write int/string. Update `params_keys.h` if the type changes.
|
||||||
|
- Add an `options:` list when switching to `multiple_button` or `option`.
|
||||||
|
- Old values stored on devices may not be valid for the new widget. Consider a migration in `sunnypilot/system/updated/` if users have stale values.
|
||||||
|
|
||||||
|
### Deprecate or remove a setting
|
||||||
|
|
||||||
|
1. Remove the item from `pages/<page>.yaml`.
|
||||||
|
2. Remove the param key from `common/params_keys.h` **only after** confirming nothing in `selfdrive/`, `sunnypilot/`, or any controls code reads it.
|
||||||
|
3. If the param has been on user devices, drop it via a migration (see `sunnypilot/system/updated/`) so stale values don't linger.
|
||||||
|
4. Compile + validate + test. The validator's "no duplicate keys" + structural checks will fail if anything still references the removed key.
|
||||||
|
|
||||||
|
### Move a setting to another page
|
||||||
|
|
||||||
|
Cut the item block from one page YAML, paste into the target page's section. Compile + validate. The "no duplicate keys" check catches forgotten copies.
|
||||||
|
|
||||||
|
### Change display text
|
||||||
|
|
||||||
|
Edit `title:` or `description:` in the page YAML and recompile to regenerate `settings_ui.json`.
|
||||||
|
|
||||||
|
### Reorder sections, sub-panels, and items
|
||||||
|
|
||||||
|
Reorder them within their parent list in the YAML. The compiler preserves authored order — no `order:` field required at the section/sub_panel/item level (panel-level `order:` controls which page comes first in the side nav).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Capability labels and tooltips
|
||||||
|
|
||||||
|
The schema response includes `capability_labels`, which map capability field names to descriptions. The frontend uses these to show contextual tooltips when a capability rule prevents a setting from being used.
|
||||||
|
|
||||||
|
The device defines these labels in `capabilities.py:CAPABILITY_LABELS`. Examples:
|
||||||
|
|
||||||
|
- `has_longitudinal_control` → "sunnypilot longitudinal control"
|
||||||
|
- `torque_allowed` → "torque steering (not available for angle steering vehicles)"
|
||||||
|
- `brand` → "Vehicle brand"
|
||||||
|
|
||||||
|
### Centralized param enforcement
|
||||||
|
|
||||||
|
The device-side UI enforces capability constraints in `selfdrive/ui/sunnypilot/ui_state.py:_enforce_constraints()`, which removes incompatible params based on car capabilities. This is the single source of truth for such constraints.
|
||||||
|
|
||||||
|
Settings layouts should not duplicate these params.remove() calls. Instead, rely on schema rules and centralized enforcement to prevent duplicate logic and ensure consistency.
|
||||||
|
|
||||||
|
Example constraints in `_enforce_constraints()`:
|
||||||
|
- Angle steering cars: remove `EnforceTorqueControl` and `NeuralNetworkLateralControl`
|
||||||
|
- No CarParams: remove all car-dependent params
|
||||||
|
- No longitudinal: remove `ExperimentalMode`
|
||||||
|
- No ICBM: remove `IntelligentCruiseButtonManagement`
|
||||||
@@ -1,4 +1,26 @@
|
|||||||
{
|
{
|
||||||
|
"AccelPersonality": {
|
||||||
|
"title": "Acceleration Personality",
|
||||||
|
"description": "Select the acceleration personality profile. Sport provides more aggressive acceleration, Eco provides gentler acceleration.",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": 0,
|
||||||
|
"label": "Sport"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 1,
|
||||||
|
"label": "Normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": 2,
|
||||||
|
"label": "Eco"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AccelPersonalityEnabled": {
|
||||||
|
"title": "Custom Acceleration Personality",
|
||||||
|
"description": "Enable custom acceleration and braking profiles that adjust max acceleration and min deceleration based on speed and selected personality."
|
||||||
|
},
|
||||||
"AccessToken": {
|
"AccessToken": {
|
||||||
"title": "AccessTokenIsNice",
|
"title": "AccessTokenIsNice",
|
||||||
"description": ""
|
"description": ""
|
||||||
@@ -1071,6 +1093,10 @@
|
|||||||
"title": "Panda Som Reset Triggered",
|
"title": "Panda Som Reset Triggered",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"ParamsVersion": {
|
||||||
|
"title": "Params Version",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"PlanplusControl": {
|
"PlanplusControl": {
|
||||||
"title": "Plan Plus Controls",
|
"title": "Plan Plus Controls",
|
||||||
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",
|
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,516 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sunnypilot.com/schemas/settings_ui.schema.json",
|
||||||
|
"title": "sunnypilot Settings UI Schema",
|
||||||
|
"description": "Defines the structure of the sunnypilot settings UI panels, items, rules, and vehicle-specific settings.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "panels", "vehicle_settings"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JSON Schema reference for editor support."
|
||||||
|
},
|
||||||
|
"schema_version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Version of the settings UI schema format.",
|
||||||
|
"examples": ["1.0"]
|
||||||
|
},
|
||||||
|
"panels": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Top-level settings panels displayed in the UI.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Panel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vehicle_settings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Brand-keyed vehicle-specific settings. Each key is a car brand (e.g. 'hyundai', 'toyota').",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/VehicleBrandSettings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"Panel": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A top-level settings panel (tab) in the UI.",
|
||||||
|
"required": ["id", "label", "icon", "order"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for this panel."
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display label shown in the UI."
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Icon identifier for this panel."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Sort order for panel display.",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional description shown below the panel label."
|
||||||
|
},
|
||||||
|
"remote_configurable": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether this panel's settings can be changed remotely via sunnylink.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Grouped sections within this panel.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/PanelSection"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Settings items directly in this panel (no section grouping).",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sub_panels": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Nested sub-panels triggered by a setting.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SubPanel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PanelSection": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A grouped section within a panel.",
|
||||||
|
"required": ["id", "title"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for this section."
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display title for this section."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional description shown below the section title."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Sort order within the parent panel.",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Rules that determine whether this section is visible. All rules must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enablement": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Rules that determine whether items in this section are enabled. All rules must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attestation_required": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true, the UI must show an attestation modal before any write to items in this section.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Settings items within this section.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sub_panels": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Nested sub-panels within this section.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SubPanel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"VehicleBrandSettings": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Brand-specific settings group inside vehicle_settings.",
|
||||||
|
"required": ["items"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display title for this brand's settings group."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional description shown below the brand title."
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Settings items for this brand.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SchemaItem": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A single settings item (toggle, option selector, button group, etc.).",
|
||||||
|
"required": ["key", "widget"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The param key this item reads/writes."
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The UI widget type to render.",
|
||||||
|
"enum": ["toggle", "option", "multiple_button", "button", "info"]
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override display title (defaults to metadata lookup by key)."
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Override description text. Rendered inline below the title. May be empty when only `details` is used."
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Extended help text shown in a popover/modal when the user taps an info ('i') button on the row. Independent of `description`: either, both, or neither may be present."
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Available options for 'option' or 'multiple_button' widgets.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaOption"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Minimum value for numeric option widgets."
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum value for numeric option widgets."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Step increment for numeric option widgets."
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Static unit label (e.g. 'seconds', 'm/s²')."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Dynamic unit that changes based on IsMetric param.",
|
||||||
|
"required": ["metric", "imperial"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"metric": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unit label when IsMetric is true (e.g. 'km/h')."
|
||||||
|
},
|
||||||
|
"imperial": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unit label when IsMetric is false (e.g. 'mph')."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Unit label for numeric values. Use a string for static units or an object with metric/imperial variants for units that depend on the IsMetric param."
|
||||||
|
},
|
||||||
|
"value_map": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Maps stored values to display labels.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Rules that determine whether this item is visible. All rules must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enablement": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Rules that determine whether this item is enabled/interactive. All rules must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sub_items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Child items nested under this item (e.g. options revealed by a toggle).",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Action identifier for button widgets."
|
||||||
|
},
|
||||||
|
"title_param_suffix": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Renders an extra suffix in the item title chosen by the value of another param.",
|
||||||
|
"required": ["param", "values"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"param": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Param key whose value selects the suffix label."
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Map from stringified param value to suffix label.",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"needs_onroad_cycle": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true, the device must cycle onroad/offroad for the new value to take effect.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true, this item is treated as DEVICE_ONLY and the dashboard must not write it remotely.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"requires_attestation": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true, writes to this item require an explicit per-write confirmation modal.",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SubPanel": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A nested panel that opens when triggered by a parent item.",
|
||||||
|
"required": ["id", "label", "trigger_key"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique identifier for this sub-panel."
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display label for the sub-panel header."
|
||||||
|
},
|
||||||
|
"trigger_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The param key that triggers opening this sub-panel."
|
||||||
|
},
|
||||||
|
"trigger_condition": {
|
||||||
|
"$ref": "#/$defs/Rule",
|
||||||
|
"description": "Optional rule that must evaluate to true for the sub-panel trigger to be active."
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Settings items within this sub-panel.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SchemaItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SchemaOption": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "A selectable option for option/multiple_button widgets.",
|
||||||
|
"required": ["value", "label"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "number" },
|
||||||
|
{ "type": "string" }
|
||||||
|
],
|
||||||
|
"description": "The stored value when this option is selected."
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The display label for this option."
|
||||||
|
},
|
||||||
|
"enablement": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Rules that determine whether this option is selectable. All rules must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Rule": {
|
||||||
|
"description": "A visibility or enablement rule. Discriminated union on the 'type' field.",
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/RuleOffroadOnly" },
|
||||||
|
{ "$ref": "#/$defs/RuleNotEngaged" },
|
||||||
|
{ "$ref": "#/$defs/RuleCapability" },
|
||||||
|
{ "$ref": "#/$defs/RuleParam" },
|
||||||
|
{ "$ref": "#/$defs/RuleParamCompare" },
|
||||||
|
{ "$ref": "#/$defs/RuleNot" },
|
||||||
|
{ "$ref": "#/$defs/RuleAny" },
|
||||||
|
{ "$ref": "#/$defs/RuleAll" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RuleOffroadOnly": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that passes only when the device is offroad.",
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "offroad_only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleNotEngaged": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that passes when the vehicle is not engaged (matches Raylib `engaged = started AND (selfdriveState.enabled OR selfdriveStateSP.mads.enabled)`).",
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "not_engaged"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleCapability": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that checks a vehicle capability field against an expected value.",
|
||||||
|
"required": ["type", "field", "equals"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "capability"
|
||||||
|
},
|
||||||
|
"field": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The capability field name to check."
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"description": "The expected value to match against."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleParam": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that checks a param value against an expected value.",
|
||||||
|
"required": ["type", "key", "equals"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "param"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The param key to read."
|
||||||
|
},
|
||||||
|
"equals": {
|
||||||
|
"description": "The expected value to match against."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleParamCompare": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that compares a numeric param value using a comparison operator.",
|
||||||
|
"required": ["type", "key", "op", "value"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "param_compare"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The param key to read."
|
||||||
|
},
|
||||||
|
"op": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comparison operator.",
|
||||||
|
"enum": [">", "<", ">=", "<="]
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The numeric value to compare against."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleNot": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that negates a single child condition.",
|
||||||
|
"required": ["type", "condition"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "not"
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"$ref": "#/$defs/Rule",
|
||||||
|
"description": "The rule to negate."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleAny": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that passes if ANY of the child conditions pass (logical OR).",
|
||||||
|
"required": ["type", "conditions"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "any"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Child rules; at least one must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleAll": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Rule that passes only if ALL child conditions pass (logical AND).",
|
||||||
|
"required": ["type", "conditions"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "all"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Child rules; all must pass.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Rule"
|
||||||
|
},
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Named rule fragments. Reference from items/sections via {$ref: "#/macros/<name>"}.
|
||||||
|
# Macros may $ref other macros (max depth 3 — see compile_settings_ui.py). No template logic.
|
||||||
|
#
|
||||||
|
# Adding a macro: define here once, then reference everywhere. The compiler
|
||||||
|
# resolves $refs into the canonical settings_ui.json output the frontend reads.
|
||||||
|
|
||||||
|
macros:
|
||||||
|
# Most-used: only writable when the device is offroad.
|
||||||
|
offroad:
|
||||||
|
- {type: offroad_only}
|
||||||
|
|
||||||
|
# Writable while not engaged (started, but selfdrive/MADS not active).
|
||||||
|
not_engaged:
|
||||||
|
- {type: not_engaged}
|
||||||
|
|
||||||
|
# sunnypilot longitudinal control is active.
|
||||||
|
longitudinal:
|
||||||
|
- {type: capability, field: has_longitudinal_control, equals: true}
|
||||||
|
|
||||||
|
# Longitudinal + ICBM both available.
|
||||||
|
longitudinal_and_icbm:
|
||||||
|
- {type: capability, field: has_longitudinal_control, equals: true}
|
||||||
|
- {type: capability, field: has_icbm, equals: true}
|
||||||
|
|
||||||
|
# Item only meaningful when "Show Advanced Controls" is enabled by the user.
|
||||||
|
advanced_only:
|
||||||
|
- {type: param, key: ShowAdvancedControls, equals: true}
|
||||||
|
|
||||||
|
# Hide on MICI hardware (no analog HUD support yet).
|
||||||
|
hide_on_mici:
|
||||||
|
- type: not
|
||||||
|
condition: {type: capability, field: device_type, equals: mici}
|
||||||
|
|
||||||
|
# Mirrors selfdrive/ui/sunnypilot/layouts/.../mads_settings.py:_mads_limited_settings()
|
||||||
|
# Rivian + Tesla-without-vehicle-bus get the limited MADS UI (3-toggle subset).
|
||||||
|
# On those platforms these toggles are disabled — full MADS settings are
|
||||||
|
# only writable on platforms NOT in the limited-set.
|
||||||
|
mads_full_platforms:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: any
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: rivian}
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: tesla}
|
||||||
|
- type: not
|
||||||
|
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
|
||||||
|
|
||||||
|
# Inverse of mads_full_platforms: present only on the limited platforms.
|
||||||
|
# Useful for "show this only on rivian/tesla-no-bus" toggles.
|
||||||
|
mads_limited_platforms:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: rivian}
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- {type: capability, field: brand, equals: tesla}
|
||||||
|
- type: not
|
||||||
|
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
|
||||||
|
|
||||||
|
# Hide on sunnypilot release branches (is_release is hardcoded False everywhere; is_sp_release is the active gate).
|
||||||
|
release_branches_hide:
|
||||||
|
- type: not
|
||||||
|
condition: {type: capability, field: is_sp_release, equals: true}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sunnypilot.com/schemas/sdui/macros.schema.json",
|
||||||
|
"title": "Settings UI macros (named rule fragments)",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["macros"],
|
||||||
|
"properties": {
|
||||||
|
"macros": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Named rule fragments. Each value is either a list of rules (typical) or a single rule object. Reference from items/layout via {$ref: '#/macros/<name>'}.",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[A-Za-z_][A-Za-z0-9_]*$": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "array", "items": {"$ref": "rule.schema.json"}},
|
||||||
|
{"$ref": "rule.schema.json"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sunnypilot.com/schemas/sdui/page.schema.json",
|
||||||
|
"title": "Settings UI page (panel) YAML",
|
||||||
|
"description": "Validates pages/<id>.yaml. Each page describes one settings panel (or the vehicle namespace via kind: vehicle).",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"label": {"type": "string"},
|
||||||
|
"icon": {"type": "string"},
|
||||||
|
"order": {"type": "integer"},
|
||||||
|
"remote_configurable": {"type": "boolean"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["panel", "vehicle"],
|
||||||
|
"description": "panel (default) or vehicle (compiles to settings_ui.json#vehicle_settings)."
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/$defs/Section"}
|
||||||
|
},
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/$defs/Item"}
|
||||||
|
},
|
||||||
|
"sub_panels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/$defs/SubPanel"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"Section": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "title"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"order": {"type": "integer"},
|
||||||
|
"attestation_required": {"type": "boolean"},
|
||||||
|
"visibility": {"type": "array", "items": {"$ref": "rule.schema.json"}},
|
||||||
|
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}},
|
||||||
|
"items": {"type": "array", "items": {"$ref": "#/$defs/Item"}},
|
||||||
|
"sub_panels": {"type": "array", "items": {"$ref": "#/$defs/SubPanel"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SubPanel": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "label"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"label": {"type": "string"},
|
||||||
|
"trigger_key": {"type": ["string", "null"]},
|
||||||
|
"trigger_condition": {"$ref": "rule.schema.json"},
|
||||||
|
"items": {"type": "array", "items": {"$ref": "#/$defs/Item"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Item": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "widget"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"key": {"type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$"},
|
||||||
|
"widget": {"type": "string", "enum": ["toggle", "option", "multiple_button", "button", "info"]},
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"description": {"type": "string", "description": "Inline body text under the title. May be omitted when only details is used."},
|
||||||
|
"details": {"type": "string", "description": "Extended help shown in a modal when the user taps the info (i) button. Independent of description; either, both, or neither may be present."},
|
||||||
|
"title_param_suffix": {"type": "object"},
|
||||||
|
"min": {"type": "number"},
|
||||||
|
"max": {"type": "number"},
|
||||||
|
"step": {"type": "number"},
|
||||||
|
"unit": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["metric", "imperial"],
|
||||||
|
"properties": {
|
||||||
|
"metric": {"type": "string"},
|
||||||
|
"imperial": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"needs_onroad_cycle": {"type": "boolean"},
|
||||||
|
"requires_attestation": {"type": "boolean"},
|
||||||
|
"blocked": {"type": "boolean"},
|
||||||
|
"options": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["value", "label"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"value": {},
|
||||||
|
"label": {"type": "string"},
|
||||||
|
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visibility": {"type": "array", "items": {"$ref": "rule.schema.json"}},
|
||||||
|
"enablement": {"type": "array", "items": {"$ref": "rule.schema.json"}},
|
||||||
|
"sub_items": {"type": "array", "items": {"$ref": "#/$defs/Item"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://sunnypilot.com/schemas/sdui/rule.schema.json",
|
||||||
|
"title": "Rule",
|
||||||
|
"description": "Visibility/enablement rule. Discriminated union on 'type'. Macro reference is also accepted via {$ref: '#/macros/<name>'}.",
|
||||||
|
"oneOf": [
|
||||||
|
{"$ref": "#/$defs/MacroRef"},
|
||||||
|
{"$ref": "#/$defs/RuleOffroadOnly"},
|
||||||
|
{"$ref": "#/$defs/RuleNotEngaged"},
|
||||||
|
{"$ref": "#/$defs/RuleCapability"},
|
||||||
|
{"$ref": "#/$defs/RuleParam"},
|
||||||
|
{"$ref": "#/$defs/RuleParamCompare"},
|
||||||
|
{"$ref": "#/$defs/RuleNot"},
|
||||||
|
{"$ref": "#/$defs/RuleAny"},
|
||||||
|
{"$ref": "#/$defs/RuleAll"}
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"MacroRef": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["$ref"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"$ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^#/macros/[A-Za-z_][A-Za-z0-9_]*$",
|
||||||
|
"description": "Reference to a macro defined in _macros.yaml under #/macros/<name>."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleOffroadOnly": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {"type": {"const": "offroad_only"}}
|
||||||
|
},
|
||||||
|
"RuleNotEngaged": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {"type": {"const": "not_engaged"}}
|
||||||
|
},
|
||||||
|
"RuleCapability": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "field", "equals"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "capability"},
|
||||||
|
"field": {"type": "string"},
|
||||||
|
"equals": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleParam": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "key", "equals"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "param"},
|
||||||
|
"key": {"type": "string"},
|
||||||
|
"equals": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleParamCompare": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "key", "op", "value"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "param_compare"},
|
||||||
|
"key": {"type": "string"},
|
||||||
|
"op": {"type": "string", "enum": [">", "<", ">=", "<="]},
|
||||||
|
"value": {"type": "number"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleNot": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "condition"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "not"},
|
||||||
|
"condition": {"$ref": "#"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleAny": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "conditions"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "any"},
|
||||||
|
"conditions": {"type": "array", "items": {"$ref": "#"}, "minItems": 1}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RuleAll": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "conditions"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "all"},
|
||||||
|
"conditions": {"type": "array", "items": {"$ref": "#"}, "minItems": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
# Page: cruise
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: cruise
|
||||||
|
label: Cruise
|
||||||
|
icon: cruise_control
|
||||||
|
order: 2
|
||||||
|
remote_configurable: true
|
||||||
|
description: Longitudinal control, speed limits, and cruise behavior
|
||||||
|
sections:
|
||||||
|
- id: core_cruise_features
|
||||||
|
title: ''
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: ExperimentalMode
|
||||||
|
widget: toggle
|
||||||
|
title: Experimental Mode
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: DynamicExperimentalControl
|
||||||
|
widget: toggle
|
||||||
|
title: Dynamic Experimental Control
|
||||||
|
description: Let the model decide when to use sunnypilot ACC or sunnypilot End to End Longitudinal.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: DisengageOnAccelerator
|
||||||
|
widget: toggle
|
||||||
|
title: Disengage Cruise on Accelerator Pedal
|
||||||
|
description: When enabled, pressing the accelerator pedal will disengage longitudinal control.
|
||||||
|
- key: LongitudinalPersonality
|
||||||
|
widget: multiple_button
|
||||||
|
title: Driving Personality
|
||||||
|
description: Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive
|
||||||
|
with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can
|
||||||
|
cycle through these personalities with your steering wheel distance button.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Aggressive
|
||||||
|
- value: 1
|
||||||
|
label: Standard
|
||||||
|
- value: 2
|
||||||
|
label: Relaxed
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: AccelPersonalityEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Acceleration Personality
|
||||||
|
description: Enable per-personality acceleration profiles. Sport allows stronger acceleration; Eco is gentler.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: AccelPersonality
|
||||||
|
widget: multiple_button
|
||||||
|
title: Acceleration Profile
|
||||||
|
description: Sport allows the most aggressive acceleration; Eco the gentlest. Normal sits between.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Sport
|
||||||
|
- value: 1
|
||||||
|
label: Normal
|
||||||
|
- value: 2
|
||||||
|
label: Eco
|
||||||
|
visibility:
|
||||||
|
- type: param
|
||||||
|
key: AccelPersonalityEnabled
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- type: param
|
||||||
|
key: AccelPersonalityEnabled
|
||||||
|
equals: true
|
||||||
|
- key: IntelligentCruiseButtonManagement
|
||||||
|
widget: toggle
|
||||||
|
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||||
|
visibility:
|
||||||
|
- type: capability
|
||||||
|
field: icbm_available
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- id: custom_acc_increments
|
||||||
|
title: Custom ACC Speed Intervals
|
||||||
|
description: ''
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: pcm_cruise
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: CustomAccIncrementsEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Custom ACC Speed Intervals
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: pcm_cruise
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
sub_panels:
|
||||||
|
- id: custom_acc_intervals
|
||||||
|
label: Custom ACC Speed Intervals Settings
|
||||||
|
trigger_key: CustomAccIncrementsEnabled
|
||||||
|
trigger_condition:
|
||||||
|
type: param
|
||||||
|
key: CustomAccIncrementsEnabled
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: CustomAccShortPressIncrement
|
||||||
|
widget: option
|
||||||
|
title: Short Press Increment
|
||||||
|
min: 1
|
||||||
|
max: 10
|
||||||
|
step: 1
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: CustomAccIncrementsEnabled
|
||||||
|
equals: true
|
||||||
|
- key: CustomAccLongPressIncrement
|
||||||
|
widget: option
|
||||||
|
title: Long Press Increment
|
||||||
|
min: 1
|
||||||
|
max: 10
|
||||||
|
step: 1
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: CustomAccIncrementsEnabled
|
||||||
|
equals: true
|
||||||
|
- id: speed_limits
|
||||||
|
title: Speed Limits
|
||||||
|
description: Speed limit detection and offset behavior
|
||||||
|
items: []
|
||||||
|
sub_panels:
|
||||||
|
- id: speed_limit_settings
|
||||||
|
label: Speed Limit Settings
|
||||||
|
trigger_key: SpeedLimitMode
|
||||||
|
items:
|
||||||
|
- key: SpeedLimitMode
|
||||||
|
widget: multiple_button
|
||||||
|
title: Speed Limit Assist Mode
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: 'Off'
|
||||||
|
- value: 1
|
||||||
|
label: Information
|
||||||
|
- value: 2
|
||||||
|
label: Warning
|
||||||
|
- value: 3
|
||||||
|
label: Assist
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: brand
|
||||||
|
equals: rivian
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: all
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: brand
|
||||||
|
equals: tesla
|
||||||
|
- type: capability
|
||||||
|
field: is_sp_release
|
||||||
|
equals: true
|
||||||
|
- key: SpeedLimitPolicy
|
||||||
|
widget: multiple_button
|
||||||
|
title: Speed Limit Source
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Car State Only
|
||||||
|
- value: 1
|
||||||
|
label: Map Data Only
|
||||||
|
- value: 2
|
||||||
|
label: Car State Priority
|
||||||
|
- value: 3
|
||||||
|
label: Map Data Priority
|
||||||
|
- value: 4
|
||||||
|
label: Combined
|
||||||
|
- key: SpeedLimitOffsetType
|
||||||
|
widget: multiple_button
|
||||||
|
title: Speed Limit Offset Type
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: 'Off'
|
||||||
|
- value: 1
|
||||||
|
label: Fixed
|
||||||
|
- value: 2
|
||||||
|
label: Percentage
|
||||||
|
- key: SpeedLimitValueOffset
|
||||||
|
widget: option
|
||||||
|
title: Speed Limit Offset Value
|
||||||
|
min: -30
|
||||||
|
max: 30
|
||||||
|
step: 1
|
||||||
|
unit:
|
||||||
|
metric: km/h
|
||||||
|
imperial: mph
|
||||||
|
visibility:
|
||||||
|
- type: param_compare
|
||||||
|
key: SpeedLimitOffsetType
|
||||||
|
op: '>'
|
||||||
|
value: 0
|
||||||
|
- id: smart_cruise
|
||||||
|
title: Smart Cruise Control
|
||||||
|
description: ''
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: SmartCruiseControlVision
|
||||||
|
widget: toggle
|
||||||
|
title: Vision
|
||||||
|
description: Use vision path predictions to estimate the appropriate speed to drive through turns ahead.
|
||||||
|
visibility:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
- key: SmartCruiseControlMap
|
||||||
|
widget: toggle
|
||||||
|
title: Map
|
||||||
|
description: Use map data to estimate the appropriate speed to drive through turns ahead.
|
||||||
|
visibility:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: has_longitudinal_control
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Page: developer
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: developer
|
||||||
|
label: Developer
|
||||||
|
icon: developer
|
||||||
|
order: 9
|
||||||
|
remote_configurable: true
|
||||||
|
description: Debug tools, remote access, and advanced services
|
||||||
|
sections:
|
||||||
|
- id: connectivity
|
||||||
|
title: Connectivity
|
||||||
|
description: Remote access and debugging interfaces
|
||||||
|
items:
|
||||||
|
- key: AdbEnabled
|
||||||
|
widget: toggle
|
||||||
|
blocked: true
|
||||||
|
title: Enable ADB
|
||||||
|
description: ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma
|
||||||
|
for more info.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- key: SshEnabled
|
||||||
|
widget: toggle
|
||||||
|
blocked: true
|
||||||
|
title: Enable SSH
|
||||||
|
- key: JoystickDebugMode
|
||||||
|
widget: toggle
|
||||||
|
title: Joystick Debug Mode
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- key: AlphaLongitudinalEnabled
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: sunnypilot Longitudinal Control (Alpha)
|
||||||
|
description: 'WARNING: sunnypilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking
|
||||||
|
(AEB). On this car, sunnypilot defaults to the car''s built-in ACC instead of sunnypilot''s longitudinal control. Enable
|
||||||
|
this to switch to sunnypilot longitudinal control. Enabling Experimental mode is recommended when enabling sunnypilot
|
||||||
|
longitudinal control alpha. Changing this setting will restart sunnypilot if the car is powered on.'
|
||||||
|
visibility:
|
||||||
|
- type: all
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: alpha_long_available
|
||||||
|
equals: true
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: has_icbm
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- key: ShowDebugInfo
|
||||||
|
widget: toggle
|
||||||
|
title: UI Debug Mode
|
||||||
|
- id: test_maneuvers
|
||||||
|
title: Test Maneuvers
|
||||||
|
description: 'DANGER: enabling these maneuvers replaces normal driving behavior with deterministic test sequences. Each
|
||||||
|
toggle requires explicit confirmation per write. Use only in a closed environment.'
|
||||||
|
visibility:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: is_development
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: is_sp_release
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: is_development
|
||||||
|
equals: true
|
||||||
|
- type: param
|
||||||
|
key: ShowAdvancedControls
|
||||||
|
equals: true
|
||||||
|
attestation_required: true
|
||||||
|
items:
|
||||||
|
- key: LateralManeuverMode
|
||||||
|
widget: toggle
|
||||||
|
title: '[TEST] Lateral Maneuver Mode'
|
||||||
|
description: Replaces normal lateral control with a deterministic test sequence. NOT for road use.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- type: capability
|
||||||
|
field: torque_allowed
|
||||||
|
equals: true
|
||||||
|
- key: LongitudinalManeuverMode
|
||||||
|
widget: toggle
|
||||||
|
title: '[TEST] Longitudinal Maneuver Mode'
|
||||||
|
description: Replaces normal longitudinal control with a deterministic test sequence. NOT for road use.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- id: advanced_services
|
||||||
|
title: Advanced Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: ShowAdvancedControls
|
||||||
|
widget: toggle
|
||||||
|
title: Show Advanced Controls
|
||||||
|
description: Toggle visibility of advanced sunnypilot controls. This only changes the visibility of the toggles; it does
|
||||||
|
not change the actual enabled/disabled state.
|
||||||
|
- key: EnableGithubRunner
|
||||||
|
widget: toggle
|
||||||
|
title: GitHub Runner Service
|
||||||
|
description: Enables or disables the GitHub runner service.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/release_branches_hide'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
|
- key: EnableCopyparty
|
||||||
|
widget: toggle
|
||||||
|
title: copyparty Service
|
||||||
|
description: copyparty is a very capable file server, you can use it to download your routes, view your logs and even
|
||||||
|
make some edits on some files from your browser. Requires you to connect to your comma locally via its IP address.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
|
- key: QuickBootToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Quickboot Mode
|
||||||
|
visibility:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: any
|
||||||
|
conditions:
|
||||||
|
- type: capability
|
||||||
|
field: is_sp_release
|
||||||
|
equals: true
|
||||||
|
- type: capability
|
||||||
|
field: is_development
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: DisableUpdates
|
||||||
|
equals: true
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Page: device
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: device
|
||||||
|
label: Device
|
||||||
|
icon: device
|
||||||
|
order: 6
|
||||||
|
remote_configurable: true
|
||||||
|
description: Device behavior, units, and recording settings
|
||||||
|
sections:
|
||||||
|
- id: general
|
||||||
|
title: General
|
||||||
|
description: Power, boot, and unit preferences
|
||||||
|
items:
|
||||||
|
- key: OffroadMode
|
||||||
|
widget: toggle
|
||||||
|
title: Force Offroad Mode
|
||||||
|
- key: DeviceBootMode
|
||||||
|
widget: option
|
||||||
|
title: Wake Up Behavior
|
||||||
|
description: 'Controls state of the device after boot/sleep. Default: Device will boot/wake-up normally and will be ready
|
||||||
|
to engage. Offroad: Device will be in Always Offroad mode after boot/wake-up.'
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Standard
|
||||||
|
- value: 1
|
||||||
|
label: Always Offroad
|
||||||
|
- key: QuietMode
|
||||||
|
widget: toggle
|
||||||
|
title: Quiet Mode
|
||||||
|
- key: OnroadUploads
|
||||||
|
widget: toggle
|
||||||
|
title: Onroad Uploads
|
||||||
|
- key: MaxTimeOffroad
|
||||||
|
widget: option
|
||||||
|
title: Max Time Offroad
|
||||||
|
description: Device will automatically shutdown after set time once the engine is turned off. 30h is the default.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Always On
|
||||||
|
- value: 5
|
||||||
|
label: 5m
|
||||||
|
- value: 10
|
||||||
|
label: 10m
|
||||||
|
- value: 15
|
||||||
|
label: 15m
|
||||||
|
- value: 30
|
||||||
|
label: 30m
|
||||||
|
- value: 60
|
||||||
|
label: 1h
|
||||||
|
- value: 120
|
||||||
|
label: 2h
|
||||||
|
- value: 180
|
||||||
|
label: 3h
|
||||||
|
- value: 300
|
||||||
|
label: 5h
|
||||||
|
- value: 600
|
||||||
|
label: 10h
|
||||||
|
- value: 1440
|
||||||
|
label: 24h
|
||||||
|
- value: 1800
|
||||||
|
label: 30h (Default)
|
||||||
|
- id: language
|
||||||
|
title: Language
|
||||||
|
items:
|
||||||
|
- key: LanguageSetting
|
||||||
|
widget: info
|
||||||
|
title: Language
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Page: display
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: display
|
||||||
|
label: Display
|
||||||
|
icon: display
|
||||||
|
order: 3
|
||||||
|
remote_configurable: true
|
||||||
|
description: Screen brightness, timeout, and interactivity settings
|
||||||
|
sections:
|
||||||
|
- id: brightness_timeout
|
||||||
|
title: Brightness & Timeout
|
||||||
|
description: Screen dimming and sleep behavior while driving
|
||||||
|
items:
|
||||||
|
- key: OnroadScreenOffBrightness
|
||||||
|
widget: multiple_button
|
||||||
|
title: Onroad Brightness
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Auto (Default)
|
||||||
|
- value: 1
|
||||||
|
label: Auto (Dark)
|
||||||
|
- value: 2
|
||||||
|
label: Screen Off
|
||||||
|
- value: 3
|
||||||
|
label: 5 %
|
||||||
|
- value: 4
|
||||||
|
label: 10 %
|
||||||
|
- value: 5
|
||||||
|
label: 15 %
|
||||||
|
- value: 6
|
||||||
|
label: 20 %
|
||||||
|
- value: 7
|
||||||
|
label: 25 %
|
||||||
|
- value: 8
|
||||||
|
label: 30 %
|
||||||
|
- value: 9
|
||||||
|
label: 35 %
|
||||||
|
- value: 10
|
||||||
|
label: 40 %
|
||||||
|
- value: 11
|
||||||
|
label: 45 %
|
||||||
|
- value: 12
|
||||||
|
label: 50 %
|
||||||
|
- value: 13
|
||||||
|
label: 55 %
|
||||||
|
- value: 14
|
||||||
|
label: 60 %
|
||||||
|
- value: 15
|
||||||
|
label: 65 %
|
||||||
|
- value: 16
|
||||||
|
label: 70 %
|
||||||
|
- value: 17
|
||||||
|
label: 75 %
|
||||||
|
- value: 18
|
||||||
|
label: 80 %
|
||||||
|
- value: 19
|
||||||
|
label: 85 %
|
||||||
|
- value: 20
|
||||||
|
label: 90 %
|
||||||
|
- value: 21
|
||||||
|
label: 95 %
|
||||||
|
- value: 22
|
||||||
|
label: 100 %
|
||||||
|
- key: OnroadScreenOffTimer
|
||||||
|
widget: multiple_button
|
||||||
|
title: Onroad Brightness Delay
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Always On
|
||||||
|
- value: 3
|
||||||
|
label: 3s
|
||||||
|
- value: 5
|
||||||
|
label: 5s
|
||||||
|
- value: 10
|
||||||
|
label: 10s
|
||||||
|
- value: 15
|
||||||
|
label: 15s
|
||||||
|
- value: 30
|
||||||
|
label: 30s
|
||||||
|
- value: 60
|
||||||
|
label: 1m
|
||||||
|
- value: 180
|
||||||
|
label: 3m
|
||||||
|
- value: 300
|
||||||
|
label: 5m
|
||||||
|
- value: 600
|
||||||
|
label: 10m
|
||||||
|
enablement:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: any
|
||||||
|
conditions:
|
||||||
|
- type: param
|
||||||
|
key: OnroadScreenOffBrightness
|
||||||
|
equals: 0
|
||||||
|
- type: param
|
||||||
|
key: OnroadScreenOffBrightness
|
||||||
|
equals: 1
|
||||||
|
- key: InteractivityTimeout
|
||||||
|
widget: multiple_button
|
||||||
|
title: Interactivity Timeout
|
||||||
|
description: Apply a custom timeout for settings UI. This is the time after which settings UI closes automatically if
|
||||||
|
user is not interacting with the screen.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Default
|
||||||
|
- value: 10
|
||||||
|
label: 10 s
|
||||||
|
- value: 20
|
||||||
|
label: 20 s
|
||||||
|
- value: 30
|
||||||
|
label: 30 s
|
||||||
|
- value: 40
|
||||||
|
label: 40 s
|
||||||
|
- value: 50
|
||||||
|
label: 50 s
|
||||||
|
- value: 60
|
||||||
|
label: 1 m
|
||||||
|
- value: 70
|
||||||
|
label: 1 m
|
||||||
|
- value: 80
|
||||||
|
label: 1 m
|
||||||
|
- value: 90
|
||||||
|
label: 1 m
|
||||||
|
- value: 100
|
||||||
|
label: 1 m
|
||||||
|
- value: 110
|
||||||
|
label: 1 m
|
||||||
|
- value: 120
|
||||||
|
label: 2 m
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Page: models
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: models
|
||||||
|
label: Models
|
||||||
|
icon: models
|
||||||
|
order: 10
|
||||||
|
remote_configurable: false
|
||||||
|
description: Driving model behavior and camera calibration
|
||||||
|
sections:
|
||||||
|
- id: model_behavior
|
||||||
|
title: Model Behavior
|
||||||
|
description: Lane desire and lead-vehicle awareness tuning
|
||||||
|
items:
|
||||||
|
- key: LaneTurnDesire
|
||||||
|
widget: toggle
|
||||||
|
title: Use Lane Turn Desires
|
||||||
|
description: If you are driving at 20 mph (32 km/h) or below and have your blinker on, the car will plan a turn in that
|
||||||
|
direction at the nearest drivable path. This prevents situations (like at red lights) where the car might plan the wrong
|
||||||
|
turn direction.
|
||||||
|
- key: LaneTurnValue
|
||||||
|
widget: option
|
||||||
|
title: Adjust Lane Turn Speed
|
||||||
|
description: Set the maximum speed for lane turn desires.
|
||||||
|
min: 0
|
||||||
|
max: 20
|
||||||
|
step: 1
|
||||||
|
unit:
|
||||||
|
metric: km/h
|
||||||
|
imperial: mph
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: LaneTurnDesire
|
||||||
|
equals: true
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
|
- key: LagdToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Live Learning Steer Delay
|
||||||
|
description: Allow device to learn and adapt car's steering response time
|
||||||
|
- key: LagdToggleDelay
|
||||||
|
widget: option
|
||||||
|
title: Adjust Software Delay
|
||||||
|
description: Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value
|
||||||
|
is 0.2
|
||||||
|
min: 0.05
|
||||||
|
max: 0.5
|
||||||
|
step: 0.01
|
||||||
|
enablement:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: param
|
||||||
|
key: LagdToggle
|
||||||
|
equals: true
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
|
- id: lateral_control
|
||||||
|
title: Lateral Control
|
||||||
|
description: Neural network lateral control for supported models
|
||||||
|
items:
|
||||||
|
- key: NeuralNetworkLateralControl
|
||||||
|
widget: toggle
|
||||||
|
title: Neural Network Lateral Control (NNLC)
|
||||||
|
description: Use a neural network for lateral control instead of the default torque controller.
|
||||||
|
visibility:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: steer_control_type
|
||||||
|
equals: angle
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: capability
|
||||||
|
field: torque_allowed
|
||||||
|
equals: true
|
||||||
|
- type: param
|
||||||
|
key: EnforceTorqueControl
|
||||||
|
equals: false
|
||||||
|
- id: camera
|
||||||
|
title: Camera
|
||||||
|
description: Camera position and calibration
|
||||||
|
items:
|
||||||
|
- key: CameraOffset
|
||||||
|
widget: option
|
||||||
|
title: Adjust Camera Offset
|
||||||
|
description: Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)
|
||||||
|
min: -0.35
|
||||||
|
max: 0.35
|
||||||
|
step: 0.01
|
||||||
|
unit: meters
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Page: software
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: software
|
||||||
|
label: Software
|
||||||
|
icon: software
|
||||||
|
order: 7
|
||||||
|
remote_configurable: true
|
||||||
|
description: Software update preferences
|
||||||
|
sections:
|
||||||
|
- id: updates
|
||||||
|
title: Updates
|
||||||
|
description: Control software updates
|
||||||
|
items:
|
||||||
|
- key: DisableUpdates
|
||||||
|
widget: toggle
|
||||||
|
title: Disable Updates
|
||||||
|
description: When enabled, software updates will be off. This requires a reboot to take effect.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- $ref: '#/macros/advanced_only'
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# Page: steering
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: steering
|
||||||
|
label: Steering
|
||||||
|
icon: steering_wheel
|
||||||
|
order: 1
|
||||||
|
remote_configurable: true
|
||||||
|
description: Lateral control, lane changes, and steering behavior
|
||||||
|
sections:
|
||||||
|
- id: mads
|
||||||
|
title: Modular Assistive Driving System (MADS)
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: Mads
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Modular Assistive Driving System (MADS)
|
||||||
|
description: Enable MADS. Disable toggle to revert back to stock sunnypilot engagement/disengagement.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
sub_panels:
|
||||||
|
- id: mads_settings
|
||||||
|
label: MADS Settings
|
||||||
|
trigger_key: Mads
|
||||||
|
trigger_condition:
|
||||||
|
type: param
|
||||||
|
key: Mads
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: MadsMainCruiseAllowed
|
||||||
|
widget: toggle
|
||||||
|
title: Toggle with Main Cruise
|
||||||
|
description: 'Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement.'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- $ref: '#/macros/mads_full_platforms'
|
||||||
|
- key: MadsUnifiedEngagementMode
|
||||||
|
widget: toggle
|
||||||
|
title: Unified Engagement Mode (UEM)
|
||||||
|
description: 'Engage lateral and longitudinal control with cruise control engagement. Note: Once lateral control is
|
||||||
|
engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off.'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- $ref: '#/macros/mads_full_platforms'
|
||||||
|
- key: MadsSteeringMode
|
||||||
|
widget: multiple_button
|
||||||
|
title: Steering Mode on Brake Pedal
|
||||||
|
description: Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: Remain Active
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/mads_full_platforms'
|
||||||
|
- value: 1
|
||||||
|
label: Pause
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/mads_full_platforms'
|
||||||
|
- value: 2
|
||||||
|
label: Disengage
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- id: blinker
|
||||||
|
title: Blinker Control
|
||||||
|
description: Lateral pause behavior during turn signals
|
||||||
|
items:
|
||||||
|
- key: BlinkerPauseLateralControl
|
||||||
|
widget: toggle
|
||||||
|
title: Pause Lateral Control with Blinker
|
||||||
|
description: Pause lateral control with blinker when traveling below the desired speed selected.
|
||||||
|
sub_items:
|
||||||
|
- key: BlinkerMinLateralControlSpeed
|
||||||
|
widget: option
|
||||||
|
title: Minimum Speed to Pause Lateral Control
|
||||||
|
min: 0
|
||||||
|
max: 255
|
||||||
|
step: 5
|
||||||
|
unit:
|
||||||
|
metric: km/h
|
||||||
|
imperial: mph
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: BlinkerPauseLateralControl
|
||||||
|
equals: true
|
||||||
|
- key: BlinkerLateralReengageDelay
|
||||||
|
widget: option
|
||||||
|
title: Post-Blinker Delay
|
||||||
|
description: Delay before lateral control resumes after the turn signal ends.
|
||||||
|
min: 0
|
||||||
|
max: 10
|
||||||
|
step: 1
|
||||||
|
unit: second
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: BlinkerPauseLateralControl
|
||||||
|
equals: true
|
||||||
|
- id: torque
|
||||||
|
title: Torque Control
|
||||||
|
description: Steering torque tuning and lateral control method
|
||||||
|
enablement:
|
||||||
|
- type: capability
|
||||||
|
field: torque_allowed
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: EnforceTorqueControl
|
||||||
|
widget: toggle
|
||||||
|
title: Enforce Torque Lateral Control
|
||||||
|
description: Enable this to enforce sunnypilot to steer with Torque lateral control.
|
||||||
|
visibility:
|
||||||
|
- type: not
|
||||||
|
condition:
|
||||||
|
type: capability
|
||||||
|
field: steer_control_type
|
||||||
|
equals: angle
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: capability
|
||||||
|
field: torque_allowed
|
||||||
|
equals: true
|
||||||
|
- type: param
|
||||||
|
key: NeuralNetworkLateralControl
|
||||||
|
equals: false
|
||||||
|
sub_panels:
|
||||||
|
- id: torque_settings
|
||||||
|
label: Torque Settings
|
||||||
|
trigger_key: EnforceTorqueControl
|
||||||
|
trigger_condition:
|
||||||
|
type: param
|
||||||
|
key: EnforceTorqueControl
|
||||||
|
equals: true
|
||||||
|
items:
|
||||||
|
- key: LiveTorqueParamsToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Self-Tune
|
||||||
|
description: Enables self-tune for Torque lateral control for platforms that do not use Torque lateral control by default.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- key: LiveTorqueParamsRelaxedToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Less Restrict Settings for Self-Tune (Beta)
|
||||||
|
description: Less strict settings when using Self-Tune. This allows torqued to be more forgiving when learning values.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: param
|
||||||
|
key: LiveTorqueParamsToggle
|
||||||
|
equals: true
|
||||||
|
- key: CustomTorqueParams
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Custom Tuning
|
||||||
|
description: Enables custom tuning for Torque lateral control. Modifying Lateral Acceleration Factor and Friction below
|
||||||
|
will override the offline values indicated in the YAML files within "opendbc/car/torque_data". The values will also
|
||||||
|
be used live when "Manual Real-Time Tuning" toggle is enabled.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- key: TorqueParamsOverrideEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Manual Real-Time Tuning
|
||||||
|
description: Enforces the torque lateral controller to use the fixed values instead of the learned values from Self-Tune.
|
||||||
|
Enabling this toggle overrides Self-Tune values.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: param
|
||||||
|
key: CustomTorqueParams
|
||||||
|
equals: true
|
||||||
|
- key: TorqueParamsOverrideLatAccelFactor
|
||||||
|
widget: option
|
||||||
|
title: Lateral Acceleration Factor
|
||||||
|
title_param_suffix:
|
||||||
|
param: TorqueParamsOverrideEnabled
|
||||||
|
values:
|
||||||
|
'true': (Real-Time & Offline)
|
||||||
|
'false': (Offline Only)
|
||||||
|
min: 0.1
|
||||||
|
max: 5.0
|
||||||
|
step: 0.1
|
||||||
|
unit: m/s²
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: CustomTorqueParams
|
||||||
|
equals: true
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: param
|
||||||
|
key: TorqueParamsOverrideEnabled
|
||||||
|
equals: true
|
||||||
|
- type: offroad_only
|
||||||
|
- key: TorqueParamsOverrideFriction
|
||||||
|
widget: option
|
||||||
|
title: Friction
|
||||||
|
title_param_suffix:
|
||||||
|
param: TorqueParamsOverrideEnabled
|
||||||
|
values:
|
||||||
|
'true': (Real-Time & Offline)
|
||||||
|
'false': (Offline Only)
|
||||||
|
min: 0.0
|
||||||
|
max: 1.0
|
||||||
|
step: 0.01
|
||||||
|
enablement:
|
||||||
|
- type: param
|
||||||
|
key: CustomTorqueParams
|
||||||
|
equals: true
|
||||||
|
- type: any
|
||||||
|
conditions:
|
||||||
|
- type: param
|
||||||
|
key: TorqueParamsOverrideEnabled
|
||||||
|
equals: true
|
||||||
|
- type: offroad_only
|
||||||
|
- key: TorqueControlTune
|
||||||
|
widget: multiple_button
|
||||||
|
title: Torque Control Tune Version
|
||||||
|
description: Select the version of Torque Control Tune to use.
|
||||||
|
options:
|
||||||
|
- value: ''
|
||||||
|
label: Default
|
||||||
|
- value: 1.0
|
||||||
|
label: v1.0
|
||||||
|
- value: 0.0
|
||||||
|
label: v0.0
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- id: lane_change
|
||||||
|
title: Lane Change
|
||||||
|
description: Automatic lane change timing and behavior
|
||||||
|
items:
|
||||||
|
- key: AutoLaneChangeTimer
|
||||||
|
widget: option
|
||||||
|
title: Auto Lane Change by Blinker
|
||||||
|
description: |-
|
||||||
|
Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge.
|
||||||
|
details: |-
|
||||||
|
Please use caution when using this feature. Only use the blinker when traffic and road conditions permit.
|
||||||
|
options:
|
||||||
|
- value: -1
|
||||||
|
label: 'Off'
|
||||||
|
- value: 0
|
||||||
|
label: Nudge
|
||||||
|
- value: 1
|
||||||
|
label: Nudgeless
|
||||||
|
- value: 2
|
||||||
|
label: 0.5 second
|
||||||
|
- value: 3
|
||||||
|
label: 1 second
|
||||||
|
- value: 4
|
||||||
|
label: 2 seconds
|
||||||
|
- value: 5
|
||||||
|
label: 3 seconds
|
||||||
|
- key: AutoLaneChangeBsmDelay
|
||||||
|
widget: toggle
|
||||||
|
title: 'Auto Lane Change: Delay with Blind Spot'
|
||||||
|
description: Toggle to enable a delay timer for lane changes when blind spot monitoring (BSM) detects a vehicle in your blind
|
||||||
|
spot.
|
||||||
|
enablement:
|
||||||
|
- type: capability
|
||||||
|
field: enable_bsm
|
||||||
|
equals: true
|
||||||
|
- type: param_compare
|
||||||
|
key: AutoLaneChangeTimer
|
||||||
|
op: '>'
|
||||||
|
value: 0
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Page: toggles
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: toggles
|
||||||
|
label: Toggles
|
||||||
|
icon: toggles
|
||||||
|
order: 5
|
||||||
|
remote_configurable: true
|
||||||
|
description: Core openpilot feature toggles
|
||||||
|
sections:
|
||||||
|
- id: core_toggles
|
||||||
|
title: ''
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: OpenpilotEnabledToggle
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: Enable sunnypilot
|
||||||
|
description: Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is
|
||||||
|
required at all times to use this feature.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- key: IsLdwEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Enable Lane Departure Warnings
|
||||||
|
description: Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn
|
||||||
|
signal activated while driving over 31 mph (50 km/h).
|
||||||
|
- key: AlwaysOnDM
|
||||||
|
widget: toggle
|
||||||
|
title: Always-On Driver Monitoring
|
||||||
|
description: Enable driver monitoring even when sunnypilot is not engaged.
|
||||||
|
- key: IsMetric
|
||||||
|
widget: toggle
|
||||||
|
title: Use Metric System
|
||||||
|
description: Display speed in km/h instead of mph.
|
||||||
|
- id: recording
|
||||||
|
title: Recording
|
||||||
|
description: Camera and audio recording during drives
|
||||||
|
items:
|
||||||
|
- key: RecordFront
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: Record and Upload Driver Camera
|
||||||
|
description: Upload data from the driver facing camera and help improve the driver monitoring algorithm.
|
||||||
|
- key: RecordAudio
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: Record and Upload Microphone Audio
|
||||||
|
description: Record and store microphone audio while driving. The audio will be included in the dashcam video in comma
|
||||||
|
connect.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Page: vehicle (per-brand settings)
|
||||||
|
# Compiles to settings_ui.json#vehicle_settings (brand = section id).
|
||||||
|
id: vehicle
|
||||||
|
label: Vehicle
|
||||||
|
icon: vehicle
|
||||||
|
order: 99
|
||||||
|
kind: vehicle
|
||||||
|
sections:
|
||||||
|
- id: hyundai
|
||||||
|
title: Hyundai / Kia / Genesis Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: HyundaiLongitudinalTuning
|
||||||
|
widget: multiple_button
|
||||||
|
title: Custom Longitudinal Tuning
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: 'Off'
|
||||||
|
- value: 1
|
||||||
|
label: Dynamic
|
||||||
|
- value: 2
|
||||||
|
label: Predictive
|
||||||
|
visibility:
|
||||||
|
- type: capability
|
||||||
|
field: hyundai_alpha_long_available
|
||||||
|
equals: true
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- id: subaru
|
||||||
|
title: Subaru Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: SubaruStopAndGo
|
||||||
|
widget: toggle
|
||||||
|
title: Stop and Go (Beta)
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: capability
|
||||||
|
field: has_stop_and_go
|
||||||
|
equals: true
|
||||||
|
- key: SubaruStopAndGoManualParkingBrake
|
||||||
|
widget: toggle
|
||||||
|
title: Stop and Go for Manual Parking Brake (Beta)
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- type: capability
|
||||||
|
field: has_stop_and_go
|
||||||
|
equals: true
|
||||||
|
- id: tesla
|
||||||
|
title: Tesla Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: TeslaCoopSteering
|
||||||
|
widget: toggle
|
||||||
|
title: Cooperative Steering (Beta)
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
|
- id: toyota
|
||||||
|
title: Toyota / Lexus Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: ToyotaEnforceStockLongitudinal
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: Enforce Factory Longitudinal Control
|
||||||
|
description: sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- key: ToyotaStopAndGoHack
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: Stop and Go Hack (Alpha)
|
||||||
|
description: sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. This feature is only
|
||||||
|
applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- type: param
|
||||||
|
key: ToyotaEnforceStockLongitudinal
|
||||||
|
equals: false
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Page: visuals
|
||||||
|
# Edit this file. Run compile_settings_ui.py to emit settings_ui.json.
|
||||||
|
id: visuals
|
||||||
|
label: Visuals
|
||||||
|
icon: visuals
|
||||||
|
order: 4
|
||||||
|
remote_configurable: true
|
||||||
|
description: HUD overlays, alerts, and on-screen display elements
|
||||||
|
sections:
|
||||||
|
- id: hud_elements
|
||||||
|
title: HUD Elements
|
||||||
|
description: Overlays shown on the driving screen
|
||||||
|
items:
|
||||||
|
- key: BlindSpot
|
||||||
|
widget: toggle
|
||||||
|
title: Show Blind Spot Warnings
|
||||||
|
description: Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has
|
||||||
|
BSM supported.
|
||||||
|
- key: TorqueBar
|
||||||
|
widget: toggle
|
||||||
|
title: Steering Arc
|
||||||
|
description: Display steering arc on the driving screen when lateral control is enabled.
|
||||||
|
- key: ShowTurnSignals
|
||||||
|
widget: toggle
|
||||||
|
title: Display Turn Signals
|
||||||
|
description: When enabled, visual turn indicators are drawn on the HUD.
|
||||||
|
- key: RoadNameToggle
|
||||||
|
widget: toggle
|
||||||
|
title: Display Road Name
|
||||||
|
description: Displays the name of the road the car is traveling on. The OpenStreetMap database of the location must be
|
||||||
|
downloaded to fetch the road name.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/hide_on_mici'
|
||||||
|
- key: StandstillTimer
|
||||||
|
widget: toggle
|
||||||
|
title: Standstill Timer
|
||||||
|
description: Show a timer on the HUD when the car is at a standstill.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/hide_on_mici'
|
||||||
|
- key: RocketFuel
|
||||||
|
widget: toggle
|
||||||
|
title: Real-time Acceleration Bar
|
||||||
|
description: Show an indicator on the left side of the screen to display real-time vehicle acceleration and deceleration.
|
||||||
|
This displays what the car is currently doing, not what the planner is requesting.
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/hide_on_mici'
|
||||||
|
- key: ChevronInfo
|
||||||
|
widget: option
|
||||||
|
title: Display Metrics Below Chevron
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: 'Off'
|
||||||
|
- value: 1
|
||||||
|
label: Distance
|
||||||
|
- value: 2
|
||||||
|
label: Speed
|
||||||
|
- value: 3
|
||||||
|
label: Time
|
||||||
|
- value: 4
|
||||||
|
label: All
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/hide_on_mici'
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- id: developer_ui
|
||||||
|
title: Developer UI Info
|
||||||
|
description: Speedometer and debug display options
|
||||||
|
visibility:
|
||||||
|
- $ref: '#/macros/hide_on_mici'
|
||||||
|
items:
|
||||||
|
- key: DevUIInfo
|
||||||
|
widget: option
|
||||||
|
title: Developer UI
|
||||||
|
description: Display real-time parameters and metrics from various sources.
|
||||||
|
options:
|
||||||
|
- value: 0
|
||||||
|
label: 'Off'
|
||||||
|
- value: 1
|
||||||
|
label: Bottom
|
||||||
|
- value: 2
|
||||||
|
label: Right
|
||||||
|
- value: 3
|
||||||
|
label: Right & Bottom
|
||||||
|
- key: TrueVEgoUI
|
||||||
|
widget: toggle
|
||||||
|
title: 'Speedometer: Always Display True Speed'
|
||||||
|
description: For applicable vehicles, always display the true vehicle current speed from wheel speed sensors.
|
||||||
|
- key: HideVEgoUI
|
||||||
|
widget: toggle
|
||||||
|
title: 'Speedometer: Hide from Onroad Screen'
|
||||||
|
description: When enabled, the speedometer on the onroad screen is not displayed.
|
||||||
|
- id: alerts_extras
|
||||||
|
title: Alerts & Extras
|
||||||
|
description: Traffic light alerts and visual flair
|
||||||
|
items:
|
||||||
|
- key: GreenLightAlert
|
||||||
|
widget: toggle
|
||||||
|
title: Green Traffic Light Alert (Beta)
|
||||||
|
description: 'A chime and on-screen alert will play when the traffic light you are waiting for turns green and you have
|
||||||
|
no vehicle in front of you. On-screen visual alert is only available on comma 3X. Note: This chime is only designed
|
||||||
|
as a notification. It is the driver''s responsibility to observe their environment and make decisions accordingly.'
|
||||||
|
- key: LeadDepartAlert
|
||||||
|
widget: toggle
|
||||||
|
title: Lead Departure Alert (Beta)
|
||||||
|
description: 'A chime and on-screen alert will play when you are stopped, and the vehicle in front of you start moving.
|
||||||
|
On-screen visual alert is only available on comma 3X. Note: This chime is only designed as a notification. It is the
|
||||||
|
driver''s responsibility to observe their environment and make decisions accordingly.'
|
||||||
|
- key: RainbowMode
|
||||||
|
widget: toggle
|
||||||
|
title: Tesla Rainbow Mode
|
||||||
|
description: Display a rainbow effect on the path the model wants to take. It does not affect driving in any way.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user