mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-03 16:53:44 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 458a3015cd | |||
| 336ce75f3d | |||
| 517c15f9c2 | |||
| aa73207ab8 | |||
| 184b73d8de | |||
| 6a1b697ed3 | |||
| 7eb7e93deb | |||
| 693daf9866 | |||
| afcc2b9455 | |||
| 3fdab7e8f0 | |||
| f488bfc806 | |||
| fd62fed669 | |||
| 3a665737c2 | |||
| b6a87b8958 | |||
| a1dcec490f | |||
| 0729ce7c08 | |||
| 916fb1d522 | |||
| af5e7f5327 | |||
| bf2e9ca318 | |||
| 41b433c619 | |||
| ed56f3ff7c | |||
| 5865ad108c | |||
| 8ed82eae6f | |||
| 88f6f66032 | |||
| 33e70080ad | |||
| b7f0e3fbdc | |||
| 7f371b8acd | |||
| 24c858e618 | |||
| 405407c252 | |||
| d49b56bff5 | |||
| f8d8b8ee56 | |||
| 8774a462ac | |||
| 1b41e9637f | |||
| 27a220677a | |||
| 26e4889fcb | |||
| 70fa5d0fca | |||
| 2d700cc0d0 | |||
| cc9ae66b22 | |||
| 505270420f | |||
| bb1a17d2a0 | |||
| 6db807b5a0 | |||
| 42e1414bc4 | |||
| 3e020e321f | |||
| 7e2000e909 | |||
| e96055846c | |||
| d47646b28f | |||
| e75bc83424 | |||
| 08e48958b6 | |||
| 25d0d0f1ff |
@@ -9,6 +9,7 @@
|
|||||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.otf filter=lfs diff=lfs merge=lfs -text
|
*.otf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
|
openpilot/selfdrive/assets/sounds/milestone.wav -filter -diff -merge -text
|
||||||
|
|
||||||
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
openpilot/selfdrive/car/tests/test_models_segs.txt filter=lfs diff=lfs merge=lfs -text
|
||||||
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
openpilot/common/hardware/comma/updater filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
model_name: ${{ steps.resolve.outputs.model_name }}
|
model_name: ${{ steps.resolve.outputs.model_name }}
|
||||||
safe_model_name: ${{ steps.resolve.outputs.safe_model_name }}
|
|
||||||
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
onnx_ref: ${{ steps.resolve.outputs.onnx_ref }}
|
||||||
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
onnx_path: ${{ steps.resolve.outputs.onnx_path }}
|
||||||
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }}
|
||||||
@@ -65,9 +64,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SAFE_NAME="${NAME// /-}"
|
|
||||||
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
echo "model_name=${NAME}" >> $GITHUB_OUTPUT
|
||||||
echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT
|
|
||||||
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT
|
||||||
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT
|
||||||
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT
|
||||||
@@ -138,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare output
|
- name: Prepare output
|
||||||
env:
|
env:
|
||||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||||
run: |
|
run: |
|
||||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||||
export PYTHONPATH=${{ github.workspace }}
|
export PYTHONPATH=${{ github.workspace }}
|
||||||
@@ -161,13 +158,13 @@ jobs:
|
|||||||
- name: Upload small model artifact
|
- name: Upload small model artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||||
path: ${{ github.workspace }}/small_output/
|
path: ${{ github.workspace }}/small_output/
|
||||||
|
|
||||||
- name: Upload artifact name file
|
- name: Upload artifact name file
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
path: ${{ github.workspace }}/small_output/artifact_name.txt
|
||||||
|
|
||||||
- name: Re-enable powersave
|
- name: Re-enable powersave
|
||||||
@@ -257,7 +254,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Prepare output
|
- name: Prepare output
|
||||||
env:
|
env:
|
||||||
MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }}
|
MODEL_NAME: ${{ needs.resolve.outputs.model_name }}
|
||||||
run: |
|
run: |
|
||||||
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
|
||||||
export PYTHONPATH=${{ github.workspace }}
|
export PYTHONPATH=${{ github.workspace }}
|
||||||
@@ -280,13 +277,13 @@ jobs:
|
|||||||
- name: Upload big model artifact
|
- name: Upload big model artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }}
|
name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }}
|
||||||
path: ${{ github.workspace }}/big_output/
|
path: ${{ github.workspace }}/big_output/
|
||||||
|
|
||||||
- name: Upload artifact name file
|
- name: Upload artifact name file
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
path: ${{ github.workspace }}/big_output/artifact_name.txt
|
||||||
|
|
||||||
- name: Re-enable powersave
|
- name: Re-enable powersave
|
||||||
@@ -321,7 +318,7 @@ jobs:
|
|||||||
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
if: ${{ inputs.target == 'small' || inputs.target == 'big' }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: artifact-name-${{ needs.resolve.outputs.safe_model_name }}
|
name: artifact-name-${{ needs.resolve.outputs.model_name }}
|
||||||
path: artifact_name
|
path: artifact_name
|
||||||
|
|
||||||
- name: Read artifact name
|
- name: Read artifact name
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
|||||||
export QCOM_PRIORITY=12
|
export QCOM_PRIORITY=12
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="19.7"
|
export AGNOS_VERSION="19.6"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: f95f996f59...f6b7f24174
@@ -383,6 +383,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
leadOne @2 :LeadData;
|
leadOne @2 :LeadData;
|
||||||
leadTwo @3 :LeadData;
|
leadTwo @3 :LeadData;
|
||||||
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
|
||||||
|
fordLateralPath @5 :FordLateralPath;
|
||||||
|
|
||||||
struct Param {
|
struct Param {
|
||||||
key @0 :Text;
|
key @0 :Text;
|
||||||
@@ -403,6 +404,14 @@ struct CarControlSP @0xa5cd762cd951a455 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FordLateralPath {
|
||||||
|
pathOffset @0 :Float32; # c0 [m]
|
||||||
|
pathAngle @1 :Float32; # c1 [rad]
|
||||||
|
curvature @2 :Float32; # c2 [1/m]
|
||||||
|
curvatureRate @3 :Float32; # c3 [1/m^2]
|
||||||
|
valid @4 :Bool;
|
||||||
|
}
|
||||||
|
|
||||||
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
struct BackupManagerSP @0xf98d843bfd7004a3 {
|
||||||
backupStatus @0 :Status;
|
backupStatus @0 :Status;
|
||||||
restoreStatus @1 :Status;
|
restoreStatus @1 :Status;
|
||||||
@@ -470,7 +479,30 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved10 @0xcb9fd56c7057593a {
|
struct AssistedDrivingMilestoneState @0xcb9fd56c7057593a {
|
||||||
|
enabled @0 :Bool;
|
||||||
|
madsDistanceMeters @1 :Float64;
|
||||||
|
fullAssistDistanceMeters @2 :Float64;
|
||||||
|
event @3 :Event;
|
||||||
|
|
||||||
|
struct Event {
|
||||||
|
id @0 :UInt64;
|
||||||
|
category @1 :Category;
|
||||||
|
distanceMeters @2 :Float64;
|
||||||
|
previousDistanceMeters @3 :Float64;
|
||||||
|
unit @4 :Unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Category {
|
||||||
|
none @0;
|
||||||
|
mads @1;
|
||||||
|
fullAssist @2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Unit {
|
||||||
|
imperial @0;
|
||||||
|
metric @1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CustomReserved11 @0xc2243c65e0340384 {
|
struct CustomReserved11 @0xc2243c65e0340384 {
|
||||||
|
|||||||
@@ -725,7 +725,6 @@ struct ChestnutState {
|
|||||||
pcieLtssm @7 :UInt8;
|
pcieLtssm @7 :UInt8;
|
||||||
supplyVoltage @8 :UInt16; # mV
|
supplyVoltage @8 :UInt16; # mV
|
||||||
supplyCurrent @9 :Int16; # mA
|
supplyCurrent @9 :Int16; # mA
|
||||||
supplyFault @10 :Bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RadarState @0x9a185389d6fdd05f {
|
struct RadarState @0x9a185389d6fdd05f {
|
||||||
@@ -1005,7 +1004,6 @@ struct DrivingModelData {
|
|||||||
frameIdExtra @1 :UInt32;
|
frameIdExtra @1 :UInt32;
|
||||||
frameDropPerc @6 :Float32;
|
frameDropPerc @6 :Float32;
|
||||||
modelExecutionTime @7 :Float32;
|
modelExecutionTime @7 :Float32;
|
||||||
big @8 :Bool;
|
|
||||||
|
|
||||||
action @2 :ModelDataV2.Action;
|
action @2 :ModelDataV2.Action;
|
||||||
|
|
||||||
@@ -2642,7 +2640,7 @@ struct Event {
|
|||||||
carStateSP @114 :Custom.CarStateSP;
|
carStateSP @114 :Custom.CarStateSP;
|
||||||
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
liveMapDataSP @115 :Custom.LiveMapDataSP;
|
||||||
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
modelDataV2SP @116 :Custom.ModelDataV2SP;
|
||||||
customReserved10 @136 :Custom.CustomReserved10;
|
assistedDrivingMilestoneState @136 :Custom.AssistedDrivingMilestoneState;
|
||||||
customReserved11 @137 :Custom.CustomReserved11;
|
customReserved11 @137 :Custom.CustomReserved11;
|
||||||
customReserved12 @138 :Custom.CustomReserved12;
|
customReserved12 @138 :Custom.CustomReserved12;
|
||||||
customReserved13 @139 :Custom.CustomReserved13;
|
customReserved13 @139 :Custom.CustomReserved13;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carParamsSP": (True, 0.02, 1),
|
"carParamsSP": (True, 0.02, 1),
|
||||||
"carControlSP": (True, 100., 10),
|
"carControlSP": (True, 100., 10),
|
||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
|
"assistedDrivingMilestoneState": (True, 10., 1),
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
||||||
"liveLocationKalman": (True, 20.),
|
"liveLocationKalman": (True, 20.),
|
||||||
|
|||||||
@@ -56,29 +56,29 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "boot",
|
"name": "boot",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz",
|
||||||
"hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
"hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||||
"hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d",
|
"hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd",
|
||||||
"size": 46897152,
|
"size": 46897152,
|
||||||
"sparse": false,
|
"sparse": false,
|
||||||
"full_check": true,
|
"full_check": true,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808"
|
"ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "system",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz",
|
||||||
"hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744",
|
"hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b",
|
||||||
"hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
"hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||||
"size": 4718592000,
|
"size": 4718592000,
|
||||||
"sparse": true,
|
"sparse": true,
|
||||||
"full_check": false,
|
"full_check": false,
|
||||||
"has_ab": true,
|
"has_ab": true,
|
||||||
"ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448",
|
"ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468",
|
||||||
"alt": {
|
"alt": {
|
||||||
"hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f",
|
"hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3",
|
||||||
"url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img",
|
"url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img",
|
||||||
"size": 4718592000
|
"size": 4718592000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
import string
|
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -355,7 +354,7 @@ class Modem:
|
|||||||
imei = ""
|
imei = ""
|
||||||
|
|
||||||
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F")
|
||||||
if not all(c in string.hexdigits for c in iccid):
|
if not iccid.isdigit():
|
||||||
iccid = ""
|
iccid = ""
|
||||||
|
|
||||||
imsi = first_line("AT+CIMI")
|
imsi = first_line("AT+CIMI")
|
||||||
|
|||||||
@@ -4,17 +4,11 @@ from pathlib import Path
|
|||||||
CHESTNUT_FW_VERSION = "ed4e39b7"
|
CHESTNUT_FW_VERSION = "ed4e39b7"
|
||||||
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001))
|
||||||
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463))
|
||||||
CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN"
|
|
||||||
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
USB_DEVICES_PATH = Path("/sys/bus/usb/devices")
|
||||||
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation")
|
||||||
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
PRIMARY_USB_CONTROLLER = "a600000.ssusb"
|
||||||
|
|
||||||
|
|
||||||
def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool:
|
|
||||||
ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS
|
|
||||||
return (vendor_id, product_id) in ids
|
|
||||||
|
|
||||||
|
|
||||||
def get_usb_topology() -> set[str]:
|
def get_usb_topology() -> set[str]:
|
||||||
try:
|
try:
|
||||||
return set(os.listdir(USB_DEVICES_PATH))
|
return set(os.listdir(USB_DEVICES_PATH))
|
||||||
@@ -87,7 +81,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None:
|
|||||||
entry.linkErrorCount = device["linkErrorCount"]
|
entry.linkErrorCount = device["linkErrorCount"]
|
||||||
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
entry.usb3Lane = device.get("usb3Lane", "unknown")
|
||||||
|
|
||||||
if is_chestnut_usb_id(entry.vendorId, entry.productId):
|
if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS:
|
||||||
chestnut_present = True
|
chestnut_present = True
|
||||||
|
|
||||||
device_state.chestnutPresent = chestnut_present
|
device_state.chestnutPresent = chestnut_present
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ Params::Params(const std::string &path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Params::~Params() {
|
Params::~Params() {
|
||||||
|
flushNonBlockingWrites();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Params::flushNonBlockingWrites() {
|
||||||
if (future.valid()) {
|
if (future.valid()) {
|
||||||
future.wait();
|
future.wait();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ public:
|
|||||||
return put(key.c_str(), val ? "1" : "0", 1);
|
return put(key.c_str(), val ? "1" : "0", 1);
|
||||||
}
|
}
|
||||||
void putNonBlocking(const std::string &key, const std::string &val);
|
void putNonBlocking(const std::string &key, const std::string &val);
|
||||||
|
void flushNonBlockingWrites();
|
||||||
inline void putBoolNonBlocking(const std::string &key, bool val) {
|
inline void putBoolNonBlocking(const std::string &key, bool val) {
|
||||||
putNonBlocking(key, val ? "1" : "0");
|
putNonBlocking(key, val ? "1" : "0");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ params_get = _bind("params_get", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool],
|
|||||||
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
params_get_bool = _bind("params_get_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool], ctypes.c_bool)
|
||||||
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
params_put = _bind("params_put", [ParamsHandle, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_int)
|
||||||
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
params_put_bool = _bind("params_put_bool", [ParamsHandle, ctypes.c_char_p, ctypes.c_bool, ctypes.c_bool], ctypes.c_int)
|
||||||
|
params_flush = _bind("params_flush", [ParamsHandle])
|
||||||
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
params_remove = _bind("params_remove", [ParamsHandle, ctypes.c_char_p], ctypes.c_int)
|
||||||
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
params_get_path = _bind("params_get_path", [ParamsHandle, ctypes.c_char_p, ctypes.c_size_t], ParamsBuffer)
|
||||||
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
params_keys_size = _bind("params_keys_size", [ParamsHandle], ctypes.c_size_t)
|
||||||
@@ -178,6 +179,10 @@ class Params:
|
|||||||
def put_bool(self, key, val, block=False):
|
def put_bool(self, key, val, block=False):
|
||||||
params_put_bool(self.p, self.check_key(key), val, block)
|
params_put_bool(self.p, self.check_key(key), val, block)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""Wait for all prior nonblocking writes from this Params instance."""
|
||||||
|
params_flush(self.p)
|
||||||
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
params_remove(self.p, self.check_key(key))
|
params_remove(self.p, self.check_key(key))
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ int params_put_bool(ParamsHandle *handle, const char *key, bool value, bool bloc
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void params_flush(ParamsHandle *handle) noexcept {
|
||||||
|
translate_exceptions([&]() {
|
||||||
|
handle->params.flushNonBlockingWrites();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
int params_remove(ParamsHandle *handle, const char *key) noexcept {
|
||||||
return translate_exceptions(-1, [&]() {
|
return translate_exceptions(-1, [&]() {
|
||||||
return handle->params.remove(key);
|
return handle->params.remove(key);
|
||||||
|
|||||||
@@ -92,12 +92,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
{"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}},
|
||||||
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
{"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
||||||
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}},
|
|
||||||
{"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}},
|
|
||||||
{"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}},
|
|
||||||
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
{"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}},
|
||||||
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
{"Offroad_ExcessiveActuation", {PERSISTENT, JSON}},
|
||||||
@@ -138,11 +132,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||||
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
{"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||||
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
{"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||||
{"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
|
||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
|
{"AssistedDrivingMilestonesEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||||
|
{"AssistedDrivingMilestoneState", {PERSISTENT, JSON, "{}"}},
|
||||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
{"BlinkerLateralReengageDelay", {PERSISTENT | BACKUP, INT, "0"}}, // seconds
|
||||||
@@ -163,6 +158,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||||
|
{"FullAssistDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
||||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||||
@@ -172,7 +168,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"IsReleaseSpBranch", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
{"LastGPSPositionLLK", {PERSISTENT, STRING}},
|
||||||
|
{"LastDriveAssistedDrivingSummary", {PERSISTENT, JSON, "{}"}},
|
||||||
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"LeadDepartAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"MadsDrivenDistanceMeters", {PERSISTENT, FLOAT, "0.0"}},
|
||||||
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
{"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}},
|
||||||
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
{"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}},
|
||||||
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
{"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||||
@@ -232,6 +230,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
|
{"BackupManager_RestoreVersion", {PERSISTENT, STRING}},
|
||||||
|
|
||||||
// sunnypilot car specific params
|
// sunnypilot car specific params
|
||||||
|
{"FordPscmObserver", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ public:
|
|||||||
auto param_path = Params().getParamPath();
|
auto param_path = Params().getParamPath();
|
||||||
if (util::file_exists(param_path)) {
|
if (util::file_exists(param_path)) {
|
||||||
std::string real_path = util::readlink(param_path);
|
std::string real_path = util::readlink(param_path);
|
||||||
util::check_system(util::string_format("rm -rf %s", real_path.c_str()));
|
util::check_system(util::string_format("rm %s -rf", real_path.c_str()));
|
||||||
unlink(param_path.c_str());
|
unlink(param_path.c_str());
|
||||||
}
|
}
|
||||||
if (getenv("COMMA_CACHE") == nullptr) {
|
if (getenv("COMMA_CACHE") == nullptr) {
|
||||||
util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str()));
|
util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str()));
|
||||||
}
|
}
|
||||||
util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str()));
|
util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str()));
|
||||||
util::check_system(util::string_format("rm -rf %s", msgq_path.c_str()));
|
util::check_system(util::string_format("rm %s -rf", msgq_path.c_str()));
|
||||||
unsetenv("OPENPILOT_PREFIX");
|
unsetenv("OPENPILOT_PREFIX");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ class TestParams(OpenpilotTestCase):
|
|||||||
assert q.get("CarParams") is None
|
assert q.get("CarParams") is None
|
||||||
assert q.get("CarParams", True) == b"1"
|
assert q.get("CarParams", True) == b"1"
|
||||||
|
|
||||||
|
def test_flush_non_blocking_writes(self):
|
||||||
|
self.params.put("DongleId", "first")
|
||||||
|
self.params.put("DongleId", "last")
|
||||||
|
self.params.flush()
|
||||||
|
|
||||||
|
assert self.params.get("DongleId") == "last"
|
||||||
|
|
||||||
def test_params_all_keys(self):
|
def test_params_all_keys(self):
|
||||||
keys = Params().all_keys()
|
keys = Params().all_keys()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500
|
|
||||||
size 1544
|
|
||||||
Binary file not shown.
@@ -63,5 +63,6 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
|
|||||||
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
|
||||||
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
|
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
|
||||||
)
|
)
|
||||||
|
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
|
||||||
|
|
||||||
return struct_dataclass
|
return struct_dataclass
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ from openpilot.common.realtime import config_realtime_process, DT_CTRL, Priority
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
from opendbc.car.car_helpers import interfaces
|
from opendbc.car.car_helpers import interfaces
|
||||||
|
from opendbc.car.ford.values import FordFlags
|
||||||
from opendbc.car.vehicle_model import VehicleModel
|
from opendbc.car.vehicle_model import VehicleModel
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
|
||||||
|
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController, FordPscmObserverPathController
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
|
||||||
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
|
||||||
@@ -52,6 +54,10 @@ class Controls(ControlsExt):
|
|||||||
self.steer_limited_by_safety = False
|
self.steer_limited_by_safety = False
|
||||||
self.curvature = 0.0
|
self.curvature = 0.0
|
||||||
self.desired_curvature = 0.0
|
self.desired_curvature = 0.0
|
||||||
|
self.ford_pscm_observer = (self.CP.brand == "ford" and self.CP.flags & FordFlags.CANFD and
|
||||||
|
self.params.get_bool("FordPscmObserver"))
|
||||||
|
self.ford_path_controller = FordPscmObserverPathController() if self.ford_pscm_observer else FordPathController()
|
||||||
|
self.ford_path = FordPath()
|
||||||
|
|
||||||
self.pose_calibrator = PoseCalibrator()
|
self.pose_calibrator = PoseCalibrator()
|
||||||
self.calibrated_pose: Pose | None = None
|
self.calibrated_pose: Pose | None = None
|
||||||
@@ -155,6 +161,17 @@ class Controls(ControlsExt):
|
|||||||
actuators.curvature = float(lateral_output)
|
actuators.curvature = float(lateral_output)
|
||||||
else:
|
else:
|
||||||
actuators.steeringAngleDeg = float(lateral_output)
|
actuators.steeringAngleDeg = float(lateral_output)
|
||||||
|
if self.CP.brand == "ford":
|
||||||
|
ford_model = model_v2 if self.sm.valid['modelV2'] else None
|
||||||
|
if self.ford_pscm_observer:
|
||||||
|
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
|
||||||
|
current_curvature=self.curvature, v_ego=CS.vEgo,
|
||||||
|
v_ego_raw=CS.vEgoRaw, active=CC.latActive)
|
||||||
|
else:
|
||||||
|
self.ford_path = self.ford_path_controller.update(ford_model, self.desired_curvature,
|
||||||
|
current_curvature=self.curvature, v_ego=CS.vEgo,
|
||||||
|
active=CC.latActive)
|
||||||
|
actuators.curvature = float(self.ford_path.curvature)
|
||||||
# Ensure no NaNs/Infs
|
# Ensure no NaNs/Infs
|
||||||
for p in ACTUATOR_FIELDS:
|
for p in ACTUATOR_FIELDS:
|
||||||
attr = getattr(actuators, p)
|
attr = getattr(actuators, p)
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import math
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from opendbc.car.ford.values import CarControllerParams
|
||||||
|
|
||||||
|
|
||||||
|
DBC_OFFSET = (-5.12, 5.11)
|
||||||
|
DBC_ANGLE = (-0.5, 0.5235)
|
||||||
|
DBC_CURVATURE = (-0.02, 0.02)
|
||||||
|
DBC_CURVATURE_RATE = (-0.001024, 0.001023)
|
||||||
|
|
||||||
|
DBC_OFFSET_RESOLUTION = 0.01
|
||||||
|
DBC_ANGLE_RESOLUTION = 0.0005
|
||||||
|
DBC_CURVATURE_RESOLUTION = 0.00002
|
||||||
|
DBC_CURVATURE_RATE_RESOLUTION = 0.000001
|
||||||
|
_PATH_MIN_LOOKAHEAD = 7.0
|
||||||
|
_POSE_PREDICTION_TIME = 0.1
|
||||||
|
_POSE_BLEND_CURVATURE = (0.006, 0.012)
|
||||||
|
_PATH_OFFSET_RATE = 4.0
|
||||||
|
_PATH_ANGLE_RATE = 1.0
|
||||||
|
|
||||||
|
_PSCM_DT = 0.004
|
||||||
|
_PSCM_C0_RATE = 1.5
|
||||||
|
_PSCM_C1_RATE = 0.100006103515625
|
||||||
|
_PSCM_C2_RATE = 0.0030059814453125
|
||||||
|
_PSCM_SPEED_KPH = (0.0, 15.0, 40.0, 70.0, 100.0, 150.0, 200.0, 250.0)
|
||||||
|
_PSCM_SPEED_GAIN = (32.0, 32.0, 32.0, 30.0, 30.0, 24.0, 12.0, 0.0)
|
||||||
|
_PSCM_C0_EFFECTIVE_LIMIT = 1.0
|
||||||
|
_PSCM_C1_EFFECTIVE_LIMIT = 0.349609375 / 10.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FordPath:
|
||||||
|
valid: bool = False
|
||||||
|
path_offset: float = 0.0
|
||||||
|
path_angle: float = 0.0
|
||||||
|
curvature: float = 0.0
|
||||||
|
curvature_rate: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FordPscmState:
|
||||||
|
path_offset: float = 0.0
|
||||||
|
path_angle: float = 0.0
|
||||||
|
curvature: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _finite(value: float) -> float:
|
||||||
|
return float(value) if math.isfinite(value) else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _sample(distance: float, distances: list[float], values: list[float]) -> float:
|
||||||
|
return float(np.interp(distance, distances, values))
|
||||||
|
|
||||||
|
|
||||||
|
def _blend_share(demand: float) -> float:
|
||||||
|
lower, upper = _POSE_BLEND_CURVATURE
|
||||||
|
return float(np.clip((demand - lower) / (upper - lower), 0.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
def _model_path(model) -> tuple[list[float], list[float], list[float], list[float]] | None:
|
||||||
|
try:
|
||||||
|
x = [float(value) for value in model.position.x]
|
||||||
|
y = [float(value) for value in model.position.y]
|
||||||
|
heading = [float(value) for value in model.orientation.z]
|
||||||
|
except (AttributeError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if len(x) < 2 or len(x) != len(y) or len(x) != len(heading):
|
||||||
|
return None
|
||||||
|
if not all(math.isfinite(value) for values in (x, y, heading) for value in values):
|
||||||
|
return None
|
||||||
|
|
||||||
|
distance = [0.0]
|
||||||
|
for i in range(1, len(x)):
|
||||||
|
distance.append(distance[-1] + math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]))
|
||||||
|
if distance[-1] <= 0.0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
unwrapped_heading = [heading[0]]
|
||||||
|
for value in heading[1:]:
|
||||||
|
delta = (value - unwrapped_heading[-1] + math.pi) % (2.0 * math.pi) - math.pi
|
||||||
|
unwrapped_heading.append(unwrapped_heading[-1] + delta)
|
||||||
|
return distance, x, y, unwrapped_heading
|
||||||
|
|
||||||
|
|
||||||
|
def _predicted_pose(distance: float, current_curvature: float,
|
||||||
|
curvature_delta: float) -> tuple[float, float, float]:
|
||||||
|
curvature = current_curvature + 0.5 * curvature_delta
|
||||||
|
heading = curvature * distance
|
||||||
|
if abs(curvature) < 1e-9:
|
||||||
|
return distance, 0.0, 0.0
|
||||||
|
return math.sin(heading) / curvature, (1.0 - math.cos(heading)) / curvature, heading
|
||||||
|
|
||||||
|
|
||||||
|
def _relative_pose(target_distance: float, path: tuple[list[float], list[float], list[float], list[float]],
|
||||||
|
vehicle_pose: tuple[float, float, float]) -> tuple[float, float]:
|
||||||
|
distance, x, y, heading = path
|
||||||
|
vehicle_x, vehicle_y, vehicle_heading = vehicle_pose
|
||||||
|
dx = _sample(target_distance, distance, x) - vehicle_x
|
||||||
|
dy = _sample(target_distance, distance, y) - vehicle_y
|
||||||
|
cosine = math.cos(vehicle_heading)
|
||||||
|
sine = math.sin(vehicle_heading)
|
||||||
|
offset = -sine * dx + cosine * dy
|
||||||
|
angle = math.atan2(math.sin(_sample(target_distance, distance, heading) - vehicle_heading),
|
||||||
|
math.cos(_sample(target_distance, distance, heading) - vehicle_heading))
|
||||||
|
return offset, angle
|
||||||
|
|
||||||
|
|
||||||
|
def _path_pose(target_distance: float,
|
||||||
|
path: tuple[list[float], list[float], list[float], list[float]]) -> tuple[float, float, float]:
|
||||||
|
distance, x, y, heading = path
|
||||||
|
return (_sample(target_distance, distance, x), _sample(target_distance, distance, y),
|
||||||
|
_sample(target_distance, distance, heading))
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_feedback(feedforward: float, feedback: float, resolution: float, zero_path_limit: float) -> float:
|
||||||
|
quantization_threshold = 0.5 * resolution
|
||||||
|
limit = max(abs(feedforward) - resolution, 0.0) if abs(feedforward) >= quantization_threshold else zero_path_limit
|
||||||
|
return float(np.clip(feedback, -limit, limit))
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_path(path: tuple[list[float], list[float], list[float], list[float]], desired_curvature: float,
|
||||||
|
current_curvature: float, curvature_delta: float, v_ego: float) -> FordPath:
|
||||||
|
distance, _, _, _ = path
|
||||||
|
advance = min(v_ego * _POSE_PREDICTION_TIME, distance[-1])
|
||||||
|
offset_horizon = min(_PATH_MIN_LOOKAHEAD, distance[-1] - advance)
|
||||||
|
angle_horizon = min(max(v_ego, _PATH_MIN_LOOKAHEAD), distance[-1] - advance)
|
||||||
|
|
||||||
|
# Keep the model's remaining path as feedforward. Measured vehicle motion is
|
||||||
|
# a separate, short delay-aligned correction, so catching the requested
|
||||||
|
# curvature cannot erase a turn that is still present in the model path.
|
||||||
|
model_pose = _path_pose(advance, path)
|
||||||
|
model_offset, _ = _relative_pose(advance + offset_horizon, path, model_pose)
|
||||||
|
_, model_angle = _relative_pose(advance + angle_horizon, path, model_pose)
|
||||||
|
vehicle_pose = _predicted_pose(advance, current_curvature, curvature_delta)
|
||||||
|
feedback_offset, feedback_angle = _relative_pose(advance, path, vehicle_pose)
|
||||||
|
gentle_curvature = _POSE_BLEND_CURVATURE[0]
|
||||||
|
feedback_offset = _bounded_feedback(model_offset, feedback_offset, DBC_OFFSET_RESOLUTION,
|
||||||
|
0.5 * gentle_curvature * advance ** 2)
|
||||||
|
feedback_angle = _bounded_feedback(model_angle, feedback_angle, DBC_ANGLE_RESOLUTION,
|
||||||
|
gentle_curvature * advance)
|
||||||
|
|
||||||
|
offset_curvature = 2.0 * model_offset / max(offset_horizon, 1e-3) ** 2
|
||||||
|
angle_curvature = model_angle / max(angle_horizon, 1e-3)
|
||||||
|
pose_share = _blend_share(max(abs(offset_curvature), abs(angle_curvature), abs(desired_curvature)))
|
||||||
|
|
||||||
|
# Match upstream's C2-only normal driving, then continuously transfer the
|
||||||
|
# command to the model pose for larger maneuvers. An opposing/finished model
|
||||||
|
# path must unload sticky C2 and retain the fast pose needed to unwind it.
|
||||||
|
c2_opposes_path = desired_curvature != 0.0 and desired_curvature * model_angle <= 0.0
|
||||||
|
if c2_opposes_path:
|
||||||
|
pose_share = 1.0
|
||||||
|
curvature = 0.0
|
||||||
|
else:
|
||||||
|
curvature = desired_curvature * (1.0 - pose_share)
|
||||||
|
|
||||||
|
path_offset = pose_share * (model_offset + feedback_offset)
|
||||||
|
path_angle = pose_share * (model_angle + feedback_angle)
|
||||||
|
if abs(path_offset) < 0.5 * DBC_OFFSET_RESOLUTION:
|
||||||
|
path_offset = 0.0
|
||||||
|
if abs(path_angle) < 0.5 * DBC_ANGLE_RESOLUTION:
|
||||||
|
path_angle = 0.0
|
||||||
|
limited_path_angle = float(np.clip(path_angle, *DBC_ANGLE))
|
||||||
|
path_offset += (path_angle - limited_path_angle) * offset_horizon
|
||||||
|
return FordPath(
|
||||||
|
valid=True,
|
||||||
|
path_offset=float(np.clip(path_offset, *DBC_OFFSET)),
|
||||||
|
path_angle=limited_path_angle,
|
||||||
|
curvature=float(np.clip(curvature, *DBC_CURVATURE)),
|
||||||
|
curvature_rate=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FordPathController:
|
||||||
|
"""Blend normal C2 following into the model's forward C0/C1 pose."""
|
||||||
|
|
||||||
|
def __init__(self, dt: float = 0.01):
|
||||||
|
self.dt = dt
|
||||||
|
self._last_path = FordPath(valid=True)
|
||||||
|
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
|
||||||
|
|
||||||
|
def _limit(self, target: FordPath) -> FordPath:
|
||||||
|
offset_delta = target.path_offset - self._last_path.path_offset
|
||||||
|
angle_delta = target.path_angle - self._last_path.path_angle
|
||||||
|
scale = min(
|
||||||
|
1.0,
|
||||||
|
_PATH_OFFSET_RATE * self.dt / abs(offset_delta) if offset_delta else 1.0,
|
||||||
|
_PATH_ANGLE_RATE * self.dt / abs(angle_delta) if angle_delta else 1.0,
|
||||||
|
)
|
||||||
|
self._last_path = FordPath(
|
||||||
|
True,
|
||||||
|
self._last_path.path_offset + scale * offset_delta,
|
||||||
|
self._last_path.path_angle + scale * angle_delta,
|
||||||
|
self._last_path.curvature + scale * (target.curvature - self._last_path.curvature),
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
return self._last_path
|
||||||
|
|
||||||
|
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
|
||||||
|
v_ego: float = 0.0, active: bool = True) -> FordPath:
|
||||||
|
if not active:
|
||||||
|
self._last_path = FordPath(valid=True)
|
||||||
|
self._curvature_history.clear()
|
||||||
|
return FordPath()
|
||||||
|
current_curvature = _finite(current_curvature)
|
||||||
|
self._curvature_history.append(current_curvature)
|
||||||
|
curvature_delta = (current_curvature - self._curvature_history[0]
|
||||||
|
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
|
||||||
|
path = _model_path(model) if model is not None else None
|
||||||
|
if path is None:
|
||||||
|
return self._limit(FordPath(valid=True))
|
||||||
|
return self._limit(_encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
|
||||||
|
max(_finite(v_ego), 0.0)))
|
||||||
|
|
||||||
|
|
||||||
|
def _pscm_slew(value: float, target: float, rate: float, ticks: int) -> float:
|
||||||
|
step = rate * _PSCM_DT * ticks
|
||||||
|
return float(np.clip(target, value - step, value + step))
|
||||||
|
|
||||||
|
|
||||||
|
def _pscm_speed_gain(v_ego: float) -> float:
|
||||||
|
return float(np.interp(max(v_ego, 0.0) * 3.6, _PSCM_SPEED_KPH, _PSCM_SPEED_GAIN))
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_path(path: FordPath) -> FordPath:
|
||||||
|
return FordPath(
|
||||||
|
valid=path.valid,
|
||||||
|
path_offset=round(path.path_offset / DBC_OFFSET_RESOLUTION) * DBC_OFFSET_RESOLUTION,
|
||||||
|
path_angle=round(path.path_angle / DBC_ANGLE_RESOLUTION) * DBC_ANGLE_RESOLUTION,
|
||||||
|
curvature=round(path.curvature / DBC_CURVATURE_RESOLUTION) * DBC_CURVATURE_RESOLUTION,
|
||||||
|
curvature_rate=round(path.curvature_rate / DBC_CURVATURE_RATE_RESOLUTION) * DBC_CURVATURE_RATE_RESOLUTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pscm_contributions(state: FordPscmState, v_ego: float) -> tuple[float, float, float]:
|
||||||
|
gain = _pscm_speed_gain(v_ego)
|
||||||
|
return (
|
||||||
|
float(np.clip(0.5 * gain * state.path_offset, -0.5 * gain, 0.5 * gain)),
|
||||||
|
float(np.clip(10.0 * gain * state.path_angle, -0.349609375 * gain, 0.349609375 * gain)),
|
||||||
|
float(np.clip(0.30078125 * gain * state.curvature * v_ego ** 2, -0.5 * gain, 0.5 * gain)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FordPscmObserver:
|
||||||
|
"""Mirror the firmware's held-command coefficient states at its 250 Hz step."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.state = FordPscmState()
|
||||||
|
self.command = FordPath(valid=True)
|
||||||
|
self._phase = 0.0
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.state = FordPscmState()
|
||||||
|
self.command = FordPath(valid=True)
|
||||||
|
self._phase = 0.0
|
||||||
|
|
||||||
|
def advance(self, elapsed: float) -> None:
|
||||||
|
self._phase += max(elapsed, 0.0)
|
||||||
|
ticks = int((self._phase + 1e-12) / _PSCM_DT)
|
||||||
|
self._phase -= ticks * _PSCM_DT
|
||||||
|
if ticks == 0:
|
||||||
|
return
|
||||||
|
self.state = FordPscmState(
|
||||||
|
_pscm_slew(self.state.path_offset, self.command.path_offset, _PSCM_C0_RATE, ticks),
|
||||||
|
_pscm_slew(self.state.path_angle, self.command.path_angle, _PSCM_C1_RATE, ticks),
|
||||||
|
_pscm_slew(self.state.curvature, self.command.curvature + 10.0 * self.command.curvature_rate,
|
||||||
|
_PSCM_C2_RATE, ticks),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_command(self, command: FordPath) -> None:
|
||||||
|
self.command = _wire_path(command)
|
||||||
|
|
||||||
|
|
||||||
|
class FordPscmObserverPathController:
|
||||||
|
"""Compensate model-path commands for the PSCM coefficient state it still carries."""
|
||||||
|
|
||||||
|
def __init__(self, dt: float = 0.01):
|
||||||
|
self.dt = dt
|
||||||
|
self._last_path = FordPath(valid=True)
|
||||||
|
self._curvature_history = deque(maxlen=max(round(_POSE_PREDICTION_TIME / dt) + 1, 2))
|
||||||
|
self.observer = FordPscmObserver()
|
||||||
|
self._sent_c2 = 0.0
|
||||||
|
|
||||||
|
def _reset(self) -> None:
|
||||||
|
self._last_path = FordPath(valid=True)
|
||||||
|
self._curvature_history.clear()
|
||||||
|
self.observer.reset()
|
||||||
|
self._sent_c2 = 0.0
|
||||||
|
|
||||||
|
def _command_for_state(self, target: FordPath, v_ego: float) -> FordPath:
|
||||||
|
# The target describes the desired fully-settled PSCM contribution. C0 keeps
|
||||||
|
# the remaining C1-saturated residual. C1 supplies the primary contribution
|
||||||
|
# that the known slow C2 state does not yet provide, without a guessed gain.
|
||||||
|
target_state = FordPscmState(target.path_offset, target.path_angle, target.curvature)
|
||||||
|
target_contribution = sum(_pscm_contributions(target_state, v_ego))
|
||||||
|
_, _, observed_c2 = _pscm_contributions(self.observer.state, v_ego)
|
||||||
|
gain = _pscm_speed_gain(v_ego)
|
||||||
|
required_fast = target_contribution - observed_c2
|
||||||
|
c1_contribution = float(np.clip(required_fast, -0.349609375 * gain, 0.349609375 * gain))
|
||||||
|
c0_contribution = required_fast - c1_contribution
|
||||||
|
path_offset = c0_contribution / (0.5 * gain) if gain > 0.0 else 0.0
|
||||||
|
path_angle = c1_contribution / (10.0 * gain) if gain > 0.0 else 0.0
|
||||||
|
return FordPath(
|
||||||
|
valid=True,
|
||||||
|
path_offset=float(np.clip(path_offset, -_PSCM_C0_EFFECTIVE_LIMIT, _PSCM_C0_EFFECTIVE_LIMIT)),
|
||||||
|
path_angle=float(np.clip(path_angle, -_PSCM_C1_EFFECTIVE_LIMIT, _PSCM_C1_EFFECTIVE_LIMIT)),
|
||||||
|
curvature=target.curvature,
|
||||||
|
curvature_rate=target.curvature_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _limit(self, target: FordPath, v_ego_raw: float) -> FordPath:
|
||||||
|
path_offset = float(np.clip(target.path_offset,
|
||||||
|
self._last_path.path_offset - _PATH_OFFSET_RATE * self.dt,
|
||||||
|
self._last_path.path_offset + _PATH_OFFSET_RATE * self.dt))
|
||||||
|
path_angle = float(np.clip(target.path_angle,
|
||||||
|
self._last_path.path_angle - _PATH_ANGLE_RATE * self.dt,
|
||||||
|
self._last_path.path_angle + _PATH_ANGLE_RATE * self.dt))
|
||||||
|
curvature = CarControllerParams.CURVATURE_LIMITS.apply_limits(
|
||||||
|
target.curvature, self._sent_c2, v_ego_raw, 0.0, True, CarControllerParams.LMC2_STEP,
|
||||||
|
)
|
||||||
|
self._sent_c2 = curvature
|
||||||
|
self._last_path = FordPath(True, path_offset, path_angle, curvature, target.curvature_rate)
|
||||||
|
self.observer.set_command(self._last_path)
|
||||||
|
return self._last_path
|
||||||
|
|
||||||
|
def update(self, model, desired_curvature: float, *, current_curvature: float = 0.0,
|
||||||
|
v_ego: float = 0.0, v_ego_raw: float = 0.0, active: bool = True) -> FordPath:
|
||||||
|
if not active:
|
||||||
|
self._reset()
|
||||||
|
return FordPath()
|
||||||
|
|
||||||
|
self.observer.advance(self.dt)
|
||||||
|
current_curvature = _finite(current_curvature)
|
||||||
|
self._curvature_history.append(current_curvature)
|
||||||
|
curvature_delta = (current_curvature - self._curvature_history[0]
|
||||||
|
if len(self._curvature_history) == self._curvature_history.maxlen else 0.0)
|
||||||
|
path = _model_path(model) if model is not None else None
|
||||||
|
if path is None:
|
||||||
|
target = FordPath(valid=True)
|
||||||
|
else:
|
||||||
|
target = _encode_path(path, _finite(desired_curvature), current_curvature, curvature_delta,
|
||||||
|
max(_finite(v_ego), 0.0))
|
||||||
|
v_ego_raw = max(_finite(v_ego_raw), 0.0)
|
||||||
|
command = self._command_for_state(target, v_ego_raw)
|
||||||
|
return self._limit(command, v_ego_raw)
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
import math
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from openpilot.cereal import custom
|
||||||
|
from openpilot.selfdrive.car.helpers import convert_carControlSP
|
||||||
|
from openpilot.selfdrive.controls.lib.ford_path import (DBC_ANGLE, DBC_CURVATURE, DBC_OFFSET, FordPath, FordPathController,
|
||||||
|
FordPscmObserver, FordPscmObserverPathController, FordPscmState,
|
||||||
|
_bounded_feedback, _encode_path, _model_path, _predicted_pose,
|
||||||
|
_pscm_contributions, _relative_pose)
|
||||||
|
|
||||||
|
|
||||||
|
def _path(curvature: float, speed: float = 8.0):
|
||||||
|
t = np.linspace(0.0, 3.0, 61)
|
||||||
|
distance = speed * t
|
||||||
|
heading = curvature * distance
|
||||||
|
x = np.zeros_like(distance)
|
||||||
|
y = np.zeros_like(distance)
|
||||||
|
for i in range(1, len(distance)):
|
||||||
|
ds = distance[i] - distance[i - 1]
|
||||||
|
average_heading = 0.5 * (heading[i] + heading[i - 1])
|
||||||
|
x[i] = x[i - 1] + ds * math.cos(average_heading)
|
||||||
|
y[i] = y[i - 1] + ds * math.sin(average_heading)
|
||||||
|
return SimpleNamespace(
|
||||||
|
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
|
||||||
|
orientation=SimpleNamespace(z=heading.tolist()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _changing_path(start_curvature: float, end_curvature: float, speed: float = 8.0):
|
||||||
|
t = np.linspace(0.0, 3.0, 61)
|
||||||
|
distance = speed * t
|
||||||
|
curvature = np.interp(distance, [distance[0], min(distance[-1], 7.0)], [start_curvature, end_curvature])
|
||||||
|
heading = np.zeros_like(distance)
|
||||||
|
x = np.zeros_like(distance)
|
||||||
|
y = np.zeros_like(distance)
|
||||||
|
for i in range(1, len(distance)):
|
||||||
|
ds = distance[i] - distance[i - 1]
|
||||||
|
heading[i] = heading[i - 1] + 0.5 * (curvature[i] + curvature[i - 1]) * ds
|
||||||
|
average_heading = 0.5 * (heading[i] + heading[i - 1])
|
||||||
|
x[i] = x[i - 1] + ds * math.cos(average_heading)
|
||||||
|
y[i] = y[i - 1] + ds * math.sin(average_heading)
|
||||||
|
return SimpleNamespace(
|
||||||
|
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
|
||||||
|
orientation=SimpleNamespace(z=heading.tolist()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _command(model, desired_curvature: float, *, current_curvature: float = 0.0, v_ego: float = 8.0):
|
||||||
|
return FordPathController(dt=1.0).update(model, desired_curvature, current_curvature=current_curvature, v_ego=v_ego)
|
||||||
|
|
||||||
|
|
||||||
|
def _equivalent_curvature(command) -> float:
|
||||||
|
return 2.0 * command.path_offset / 7.0 ** 2 + 2.0 * command.path_angle / 7.0 + command.curvature
|
||||||
|
|
||||||
|
|
||||||
|
def test_gentle_path_uses_only_c2():
|
||||||
|
command = _command(_path(0.004, speed=20.0), 0.004, current_curvature=0.004, v_ego=20.0)
|
||||||
|
assert command.valid
|
||||||
|
assert command.path_offset == 0.0
|
||||||
|
assert command.path_angle == 0.0
|
||||||
|
assert np.isclose(command.curvature, 0.004, atol=1e-6)
|
||||||
|
assert command.curvature_rate == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_gentle_path_uses_only_c2_when_model_and_action_disagree():
|
||||||
|
command = _command(_path(0.005), 0.002, current_curvature=0.005)
|
||||||
|
assert command.path_offset == 0.0
|
||||||
|
assert command.path_angle == 0.0
|
||||||
|
assert np.isclose(command.curvature, 0.002, atol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_spatially_growing_path_adds_fast_pose_before_action_becomes_large():
|
||||||
|
controller = FordPathController(dt=1.0)
|
||||||
|
command = controller.update(_changing_path(0.0, 0.04), 0.012, current_curvature=0.0, v_ego=8.0)
|
||||||
|
assert command.path_offset > 0.0
|
||||||
|
assert command.path_angle > 0.0
|
||||||
|
assert command.curvature < 0.012
|
||||||
|
assert command.curvature_rate == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_growing_model_pose_adds_authority_but_c3_is_never_transmitted():
|
||||||
|
constant = _command(_path(0.012), 0.012)
|
||||||
|
growing = _command(_changing_path(0.0, 0.04), 0.012)
|
||||||
|
assert _equivalent_curvature(growing) > _equivalent_curvature(constant)
|
||||||
|
assert constant.curvature_rate == 0.0
|
||||||
|
assert growing.curvature_rate == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_tracking_error_corrects_without_replacing_forward_pose():
|
||||||
|
model = _changing_path(0.0, 0.04)
|
||||||
|
local_curvature = 0.5 * 0.04 * 2.0 / 7.0
|
||||||
|
aligned = _command(model, 0.012, current_curvature=local_curvature)
|
||||||
|
under = _command(model, 0.012, current_curvature=0.0)
|
||||||
|
assert aligned.path_offset > 0.0
|
||||||
|
assert aligned.path_angle > 0.0
|
||||||
|
assert under.path_offset > aligned.path_offset
|
||||||
|
assert under.path_angle > aligned.path_angle
|
||||||
|
|
||||||
|
|
||||||
|
def test_large_maneuver_uses_fast_pose_and_zeros_c2():
|
||||||
|
command = _command(_path(0.04), 0.04)
|
||||||
|
assert command.path_offset > 0.5
|
||||||
|
assert command.path_angle > 0.2
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
assert command.curvature_rate == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_pose_can_trigger_maneuver_when_action_is_late():
|
||||||
|
command = _command(_path(0.04), 0.002)
|
||||||
|
assert command.path_offset > 0.5
|
||||||
|
assert command.path_angle > 0.2
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_gentle_model_pose_does_not_replace_a_collapsed_action():
|
||||||
|
command = _command(_path(0.005), 0.0, current_curvature=0.005)
|
||||||
|
assert command.path_offset == 0.0
|
||||||
|
assert command.path_angle == 0.0
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_changing_gentle_curve_keeps_upstream_strength_c2():
|
||||||
|
command = _command(_changing_path(0.0, 0.008), 0.004, current_curvature=0.0)
|
||||||
|
assert np.isclose(command.curvature, 0.004)
|
||||||
|
assert command.path_offset == 0.0
|
||||||
|
assert command.path_angle == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_action_only_maneuver_cannot_invent_large_model_pose():
|
||||||
|
command = _command(_path(0.002), 0.04)
|
||||||
|
assert 0.0 < command.path_offset < 0.1
|
||||||
|
assert 0.0 < command.path_angle < 0.03
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearby_demands_blend_continuously_without_a_mode_threshold():
|
||||||
|
low = _command(_path(0.0119), 0.0119)
|
||||||
|
high = _command(_path(0.0121), 0.0121)
|
||||||
|
assert abs(high.path_offset - low.path_offset) < 0.05
|
||||||
|
assert abs(high.path_angle - low.path_angle) < 0.03
|
||||||
|
assert abs(high.curvature - low.curvature) < 0.001
|
||||||
|
|
||||||
|
|
||||||
|
def test_leaving_c2_normal_band_does_not_drop_total_authority():
|
||||||
|
normal = _command(_path(0.006), 0.006)
|
||||||
|
transition = _command(_path(0.0061), 0.0061)
|
||||||
|
assert transition.curvature <= normal.curvature
|
||||||
|
assert _equivalent_curvature(transition) >= _equivalent_curvature(normal)
|
||||||
|
|
||||||
|
|
||||||
|
def test_low_speed_still_uses_available_model_pose():
|
||||||
|
command = _command(_path(0.04, speed=2.0), 0.04, v_ego=2.0)
|
||||||
|
assert command.path_offset > 0.0
|
||||||
|
assert command.path_angle > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_higher_speed_advances_predicted_pose_and_extends_heading_horizon():
|
||||||
|
model = _changing_path(0.0, 0.015, speed=20.0)
|
||||||
|
slow = _command(model, 0.012, v_ego=7.0)
|
||||||
|
fast = _command(model, 0.012, v_ego=20.0)
|
||||||
|
assert fast.path_offset > slow.path_offset
|
||||||
|
assert fast.path_angle > slow.path_angle
|
||||||
|
|
||||||
|
|
||||||
|
def test_short_model_uses_available_endpoint():
|
||||||
|
model = _path(0.04, speed=1.0)
|
||||||
|
command = _command(model, 0.04, v_ego=1.0)
|
||||||
|
assert command.valid
|
||||||
|
assert command.path_offset > 0.0
|
||||||
|
assert command.path_angle > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_entry_coordinates_c2_release_with_fast_pose_attack():
|
||||||
|
controller = FordPathController(dt=0.01)
|
||||||
|
for _ in range(20):
|
||||||
|
assert controller.update(_path(0.004), 0.004, v_ego=8.0).curvature > 0.0
|
||||||
|
outputs = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
|
||||||
|
assert 0.0 < outputs[0].curvature < 0.004
|
||||||
|
assert outputs[0].path_offset > 0.0
|
||||||
|
assert outputs[0].path_angle > 0.0
|
||||||
|
assert outputs[-1].curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_exit_allows_c2_to_take_over_while_fast_pose_drains():
|
||||||
|
controller = FordPathController(dt=0.01)
|
||||||
|
for _ in range(20):
|
||||||
|
controller.update(_path(0.04), 0.04, current_curvature=0.02, v_ego=8.0)
|
||||||
|
outputs = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
|
||||||
|
assert 0.0 < outputs[0].curvature < 0.004
|
||||||
|
assert outputs[0].path_offset != 0.0 or outputs[0].path_angle != 0.0
|
||||||
|
assert outputs[-1].path_offset == 0.0
|
||||||
|
assert outputs[-1].path_angle == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_100hz_handoff_preserves_total_authority_without_entry_drop_or_exit_overshoot():
|
||||||
|
controller = FordPathController(dt=0.01)
|
||||||
|
normal = controller.update(_path(0.006), 0.006, current_curvature=0.006, v_ego=8.0)
|
||||||
|
entries = [controller.update(_path(0.04), 0.04, current_curvature=0.01, v_ego=8.0) for _ in range(100)]
|
||||||
|
entry_authority = np.asarray([_equivalent_curvature(command) for command in entries])
|
||||||
|
assert np.all(np.diff(entry_authority) >= -1e-9)
|
||||||
|
assert entry_authority[0] >= _equivalent_curvature(normal)
|
||||||
|
|
||||||
|
exits = [controller.update(_path(0.004), 0.004, current_curvature=0.004, v_ego=8.0) for _ in range(100)]
|
||||||
|
exit_authority = np.asarray([_equivalent_curvature(command) for command in exits])
|
||||||
|
assert np.all(np.diff(exit_authority) <= 1e-9)
|
||||||
|
assert np.all(exit_authority >= 0.004 - 1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_measured_tracking_error_closes_bidirectionally_without_abandoning_the_turn():
|
||||||
|
model = _path(0.04)
|
||||||
|
under = _command(model, 0.04, current_curvature=0.005)
|
||||||
|
on_target = _command(model, 0.04, current_curvature=0.04)
|
||||||
|
over = _command(model, 0.04, current_curvature=0.05)
|
||||||
|
assert under.path_offset > on_target.path_offset
|
||||||
|
assert under.path_angle > on_target.path_angle
|
||||||
|
assert 0.0 < over.path_offset < on_target.path_offset
|
||||||
|
assert 0.0 < over.path_angle < on_target.path_angle
|
||||||
|
|
||||||
|
|
||||||
|
def test_gentle_curve_does_not_add_fast_tracking_trim():
|
||||||
|
model = _path(0.004)
|
||||||
|
under = _command(model, 0.004, current_curvature=0.002)
|
||||||
|
on_target = _command(model, 0.004, current_curvature=0.004)
|
||||||
|
over = _command(model, 0.004, current_curvature=0.006)
|
||||||
|
assert under.path_offset == on_target.path_offset == over.path_offset == 0.0
|
||||||
|
assert under.path_angle == on_target.path_angle == over.path_angle == 0.0
|
||||||
|
assert np.allclose([under.curvature, on_target.curvature, over.curvature], 0.004, atol=2e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_overshoot_trim_cannot_erase_a_modeled_turn():
|
||||||
|
model = _path(0.04)
|
||||||
|
on_target = _command(model, 0.04, current_curvature=0.04)
|
||||||
|
over = _command(model, 0.04, current_curvature=0.06)
|
||||||
|
assert over.path_offset > 0.95 * on_target.path_offset
|
||||||
|
assert over.path_angle > 0.9 * on_target.path_angle
|
||||||
|
|
||||||
|
|
||||||
|
def test_corrupt_measured_curvature_cannot_reverse_a_modeled_turn():
|
||||||
|
command = _command(_path(0.04), 0.04, current_curvature=0.5)
|
||||||
|
assert command.path_offset > 0.0
|
||||||
|
assert command.path_angle > 0.0
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_feedback_preserves_half_lsb_feedforward_direction():
|
||||||
|
for feedforward, resolution in ((0.006, 0.01), (0.0004, 0.0005)):
|
||||||
|
result = feedforward + _bounded_feedback(feedforward, -1.0, resolution, 1.0)
|
||||||
|
assert result >= 0.5 * resolution
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_curvature_trend_advances_vehicle_pose_without_a_response_gain():
|
||||||
|
model = _model_path(_path(0.04))
|
||||||
|
assert model is not None
|
||||||
|
constant = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.0, v_ego=8.0)
|
||||||
|
rising = _encode_path(model, 0.04, current_curvature=0.02, curvature_delta=0.01, v_ego=8.0)
|
||||||
|
assert 0.0 < rising.path_offset < constant.path_offset
|
||||||
|
assert 0.0 < rising.path_angle < constant.path_angle
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_path_exit_zeros_lingering_c2_and_countersteers():
|
||||||
|
command = _command(_path(0.0), 0.004, current_curvature=0.006)
|
||||||
|
assert command.path_offset <= 0.0
|
||||||
|
assert command.path_angle < 0.0
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_path_reversal_zeros_opposing_lingering_c2():
|
||||||
|
command = _command(_path(-0.004), 0.004, current_curvature=0.002)
|
||||||
|
assert command.path_offset < 0.0
|
||||||
|
assert command.path_angle < 0.0
|
||||||
|
assert command.curvature == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_s_turn_reverses_model_pose_without_slow_c2():
|
||||||
|
controller = FordPathController(dt=0.05)
|
||||||
|
for _ in range(10):
|
||||||
|
controller.update(_path(0.04), 0.04, v_ego=8.0)
|
||||||
|
outputs = [controller.update(_path(-0.04), -0.04, v_ego=8.0) for _ in range(10)]
|
||||||
|
assert all(command.curvature == 0.0 for command in outputs)
|
||||||
|
assert np.all(np.diff([command.path_offset for command in outputs]) < 0.0)
|
||||||
|
assert np.all(np.diff([command.path_angle for command in outputs]) < 0.0)
|
||||||
|
assert outputs[-1].path_offset < 0.0
|
||||||
|
assert outputs[-1].path_angle < 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_limits_and_rates_are_bounded():
|
||||||
|
controller = FordPathController()
|
||||||
|
outputs = [controller.update(_path(0.2), 0.2, v_ego=8.0) for _ in range(100)]
|
||||||
|
assert all(DBC_OFFSET[0] <= command.path_offset <= DBC_OFFSET[1] for command in outputs)
|
||||||
|
assert all(DBC_ANGLE[0] <= command.path_angle <= DBC_ANGLE[1] for command in outputs)
|
||||||
|
assert all(DBC_CURVATURE[0] <= command.curvature <= DBC_CURVATURE[1] for command in outputs)
|
||||||
|
assert np.max(np.abs(np.diff([command.path_offset for command in outputs]))) <= 0.04 + 1e-9
|
||||||
|
assert np.max(np.abs(np.diff([command.path_angle for command in outputs]))) <= 0.01 + 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_clipped_path_angle_uses_available_offset_to_preserve_endpoint():
|
||||||
|
horizon = 7.0
|
||||||
|
for curvature, angle_limit in ((-0.1, DBC_ANGLE[0]), (0.1, DBC_ANGLE[1])):
|
||||||
|
model = _path(curvature)
|
||||||
|
command = _command(model, curvature, current_curvature=curvature, v_ego=horizon)
|
||||||
|
path = _model_path(model)
|
||||||
|
assert path is not None
|
||||||
|
advance = 0.1 * horizon
|
||||||
|
model_offset, model_angle = _relative_pose(advance + horizon, path,
|
||||||
|
_predicted_pose(advance, curvature, 0.0))
|
||||||
|
|
||||||
|
assert command.path_angle == angle_limit
|
||||||
|
assert np.isclose(command.path_offset + horizon * command.path_angle,
|
||||||
|
model_offset + horizon * model_angle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_model_ramps_pose_to_zero_and_inactive_resets():
|
||||||
|
controller = FordPathController(dt=0.01)
|
||||||
|
for _ in range(20):
|
||||||
|
active = controller.update(_path(0.04), 0.04, v_ego=8.0)
|
||||||
|
invalid = controller.update(None, 0.0, v_ego=8.0)
|
||||||
|
assert invalid.valid
|
||||||
|
assert abs(invalid.path_offset) < abs(active.path_offset)
|
||||||
|
assert abs(invalid.path_angle) < abs(active.path_angle)
|
||||||
|
assert not controller.update(_path(0.0), 0.0, v_ego=8.0, active=False).valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_sunnypilot_path_message_round_trip():
|
||||||
|
message = custom.CarControlSP.new_message()
|
||||||
|
message.fordLateralPath.pathOffset = 0.3
|
||||||
|
message.fordLateralPath.pathAngle = -0.2
|
||||||
|
message.fordLateralPath.curvature = 0.008
|
||||||
|
message.fordLateralPath.curvatureRate = -0.0004
|
||||||
|
message.fordLateralPath.valid = True
|
||||||
|
path = convert_carControlSP(message.as_reader()).fordLateralPath
|
||||||
|
assert np.isclose(path.pathOffset, 0.3)
|
||||||
|
assert np.isclose(path.pathAngle, -0.2)
|
||||||
|
assert np.isclose(path.curvature, 0.008)
|
||||||
|
assert np.isclose(path.curvatureRate, -0.0004)
|
||||||
|
assert path.valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_mirrors_exact_250hz_slew_and_c3_target():
|
||||||
|
observer = FordPscmObserver()
|
||||||
|
observer.set_command(FordPath(True, 1.0, 0.5, 0.0, 0.001))
|
||||||
|
observer.advance(1.0)
|
||||||
|
assert np.isclose(observer.state.path_offset, 1.0)
|
||||||
|
assert np.isclose(observer.state.path_angle, 0.100006103515625)
|
||||||
|
assert np.isclose(observer.state.curvature, 0.0030059814453125)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_tracks_wire_quantized_commands():
|
||||||
|
observer = FordPscmObserver()
|
||||||
|
observer.set_command(FordPath(True, 0.006, 0.0004, 0.000011, 0.0))
|
||||||
|
assert observer.command.path_offset == 0.01
|
||||||
|
assert observer.command.path_angle == 0.0005
|
||||||
|
assert observer.command.curvature == 0.00002
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_c2_contribution_is_speed_scheduled():
|
||||||
|
state = FordPscmObserver().state
|
||||||
|
state = type(state)(curvature=0.004)
|
||||||
|
low = _pscm_contributions(state, 5.0)[2]
|
||||||
|
high = _pscm_contributions(state, 20.0)[2]
|
||||||
|
assert high > low * 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_fills_missing_gentle_c2_with_fast_fields():
|
||||||
|
controller = FordPscmObserverPathController(dt=0.01)
|
||||||
|
command = controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
||||||
|
v_ego=20.0, v_ego_raw=20.0)
|
||||||
|
assert command.path_offset > 0.0
|
||||||
|
assert command.path_angle > 0.0
|
||||||
|
assert command.curvature > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_uses_c0_only_after_c1_reaches_its_effective_limit():
|
||||||
|
controller = FordPscmObserverPathController(dt=0.01)
|
||||||
|
small = controller._command_for_state(FordPath(True, 0.2, 0.0, 0.0, 0.0), 8.0)
|
||||||
|
large = controller._command_for_state(FordPath(True, 1.0, 0.5, 0.0, 0.0), 8.0)
|
||||||
|
assert small.path_offset == 0.0
|
||||||
|
assert small.path_angle > 0.0
|
||||||
|
assert large.path_offset > 0.0
|
||||||
|
assert large.path_angle == 0.349609375 / 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_preserves_c2_residual_across_c0_c1_headroom():
|
||||||
|
controller = FordPscmObserverPathController(dt=0.01)
|
||||||
|
target = FordPath(True, 0.0, 0.0, 0.004, 0.0)
|
||||||
|
command = controller._command_for_state(target, 20.0)
|
||||||
|
target_contribution = sum(_pscm_contributions(FordPscmState(curvature=target.curvature), 20.0))
|
||||||
|
command_contributions = _pscm_contributions(FordPscmState(command.path_offset, command.path_angle), 20.0)
|
||||||
|
assert np.isclose(sum(command_contributions), target_contribution)
|
||||||
|
|
||||||
|
controller.observer.state = FordPscmState(curvature=0.004)
|
||||||
|
unwind = controller._command_for_state(FordPath(valid=True), 20.0)
|
||||||
|
unwind_contributions = _pscm_contributions(FordPscmState(unwind.path_offset, unwind.path_angle), 20.0)
|
||||||
|
lingering_c2 = _pscm_contributions(controller.observer.state, 20.0)[2]
|
||||||
|
assert np.isclose(sum(unwind_contributions) + lingering_c2, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_unloads_fast_residual_as_c2_loads():
|
||||||
|
controller = FordPscmObserverPathController(dt=0.01)
|
||||||
|
outputs = [controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
||||||
|
v_ego=20.0, v_ego_raw=20.0) for _ in range(200)]
|
||||||
|
assert outputs[0].path_angle > outputs[-1].path_angle >= 0.0
|
||||||
|
assert controller.observer.state.curvature > 0.003
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_counters_lingering_c2_during_model_exit():
|
||||||
|
controller = FordPscmObserverPathController(dt=0.01)
|
||||||
|
for _ in range(200):
|
||||||
|
controller.update(_path(0.004, speed=20.0), 0.004, current_curvature=0.004,
|
||||||
|
v_ego=20.0, v_ego_raw=20.0)
|
||||||
|
command = controller.update(_path(0.0, speed=20.0), 0.0, current_curvature=0.004,
|
||||||
|
v_ego=20.0, v_ego_raw=20.0)
|
||||||
|
assert command.path_angle < 0.0
|
||||||
|
assert command.curvature < controller.observer.state.curvature
|
||||||
|
|
||||||
|
|
||||||
|
def test_pscm_observer_avoids_ineffective_c0_c1_windup():
|
||||||
|
controller = FordPscmObserverPathController(dt=1.0)
|
||||||
|
command = controller.update(_path(0.2), 0.2, v_ego=8.0, v_ego_raw=8.0)
|
||||||
|
assert abs(command.path_offset) <= 1.0
|
||||||
|
assert abs(command.path_angle) <= 0.349609375 / 10.0
|
||||||
@@ -10,6 +10,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
|||||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||||
|
|
||||||
|
|
||||||
|
CAMERA_CONFIGS = [
|
||||||
|
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
|
||||||
|
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
|
||||||
|
]
|
||||||
|
|
||||||
Import('env', 'arch')
|
Import('env', 'arch')
|
||||||
chunker_file = File("#openpilot/common/file_chunker.py")
|
chunker_file = File("#openpilot/common/file_chunker.py")
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
@@ -19,22 +24,20 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
|||||||
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))]
|
||||||
|
|
||||||
def estimate_pickle_max_size(onnx_size):
|
def estimate_pickle_max_size(onnx_size):
|
||||||
# QCOM programs for models with spatial recurrent features can approach 2x
|
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||||
# the ONNX size. Overestimating only adds an empty trailing chunk.
|
|
||||||
return 2.0 * onnx_size + 10 * 1024 * 1024
|
|
||||||
|
|
||||||
if arch == 'comma_arm64':
|
if arch == 'comma_arm64':
|
||||||
from openpilot.common.hardware import HARDWARE
|
|
||||||
camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye
|
|
||||||
camera_configs = [(camera.width, camera.height)]
|
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||||
else:
|
else:
|
||||||
camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)]
|
|
||||||
tg_backend = 'CPU'
|
tg_backend = 'CPU'
|
||||||
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM'
|
||||||
|
|
||||||
tg_devices = { # which device to put jit inputs to at runtime
|
tg_devices = { # which device to put jit inputs to at runtime
|
||||||
|
'openpilot.selfdrive.modeld.modeld': {
|
||||||
|
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
|
||||||
|
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||||
|
},
|
||||||
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
||||||
'default': {'DEV': tg_backend}
|
'default': {'DEV': tg_backend}
|
||||||
},
|
},
|
||||||
@@ -42,7 +45,7 @@ tg_devices = { # which device to put jit inputs to at runtime
|
|||||||
|
|
||||||
CHESTNUT = chestnut_present()
|
CHESTNUT = chestnut_present()
|
||||||
if CHESTNUT:
|
if CHESTNUT:
|
||||||
chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1'
|
chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
|
||||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||||
chestnut_lock = File("models/.chestnut.lock").abspath
|
chestnut_lock = File("models/.chestnut.lock").abspath
|
||||||
|
|
||||||
@@ -73,9 +76,10 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
|||||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||||
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
||||||
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('', tg_flags)
|
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut
|
||||||
|
file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs)
|
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||||
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
# CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it.
|
||||||
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else ''
|
||||||
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py '
|
||||||
@@ -103,7 +107,7 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
|||||||
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
|
||||||
node = lenv.Command(
|
node = lenv.Command(
|
||||||
chunk_targets,
|
chunk_targets,
|
||||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file],
|
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||||
actions,
|
actions,
|
||||||
)
|
)
|
||||||
if chestnut:
|
if chestnut:
|
||||||
@@ -117,7 +121,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file
|
|||||||
|
|
||||||
dm_w, dm_h = DM_INPUT_SIZE
|
dm_w, dm_h = DM_INPUT_SIZE
|
||||||
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
|
||||||
for cam_w, cam_h in camera_configs:
|
for cam_w, cam_h in CAMERA_CONFIGS:
|
||||||
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath
|
||||||
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py '
|
||||||
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} '
|
||||||
@@ -139,4 +143,5 @@ def tg_compile(flags, model_name):
|
|||||||
Action(do_chunk, " [CHUNK] $TARGET")],
|
Action(do_chunk, " [CHUNK] $TARGET")],
|
||||||
)
|
)
|
||||||
|
|
||||||
tg_compile(tg_flags, 'dmonitoring_model')
|
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||||
|
tg_compile(tg_flags, 'dmonitoring_model')
|
||||||
|
|||||||
@@ -37,12 +37,17 @@ from tinygrad.engine.jit import TinyJit
|
|||||||
|
|
||||||
|
|
||||||
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
|
||||||
MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
WARP_INPUTS = ['tfm', 'big_tfm']
|
||||||
|
POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs']
|
||||||
|
|
||||||
|
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
|
||||||
|
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
|
||||||
|
|
||||||
|
WARP_DEV = os.getenv('WARP_DEV')
|
||||||
|
|
||||||
|
|
||||||
def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int:
|
def make_random_images(keys, shape, device=None):
|
||||||
# Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation.
|
return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys}
|
||||||
return stride * (y_height + uv_height)
|
|
||||||
|
|
||||||
|
|
||||||
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None):
|
||||||
@@ -94,7 +99,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
|||||||
|
|
||||||
def frame_prepare_tinygrad(input_frame, M_inv):
|
def frame_prepare_tinygrad(input_frame, M_inv):
|
||||||
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
|
||||||
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT)
|
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV)
|
||||||
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
|
||||||
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
|
||||||
with Context(SPLIT_REDUCEOP=0):
|
with Context(SPLIT_REDUCEOP=0):
|
||||||
@@ -113,43 +118,49 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
|
|||||||
return frame_prepare_tinygrad
|
return frame_prepare_tinygrad
|
||||||
|
|
||||||
|
|
||||||
|
def make_warp_input_queues(vision_input_shapes, frame_skip, device):
|
||||||
|
img = vision_input_shapes['img'] # (1, 12, 128, 256)
|
||||||
|
n_frames = img[1] // 6
|
||||||
|
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
||||||
|
|
||||||
|
npy = {
|
||||||
|
'tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
|
'big_tfm': np.zeros((3, 3), dtype=np.float32),
|
||||||
|
}
|
||||||
|
input_queues = {
|
||||||
|
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||||
|
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
|
||||||
|
}
|
||||||
|
return input_queues, npy
|
||||||
|
|
||||||
|
|
||||||
def get_policy_npy_shapes(input_shapes):
|
def get_policy_npy_shapes(input_shapes):
|
||||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||||
tc = input_shapes['traffic_convention'] # (1, 2)
|
tc = input_shapes['traffic_convention'] # (1, 2)
|
||||||
at = input_shapes['action_t'] # (1, 2)
|
at = input_shapes['action_t'] # (1, 2)
|
||||||
fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features
|
fb = input_shapes['features_buffer'] # (1, 24, 512)
|
||||||
feat_dim = math.prod(fb[2:])
|
|
||||||
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
# TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now
|
||||||
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)}
|
shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])}
|
||||||
return shapes, [math.prod(s) for s in shapes.values()]
|
return shapes, [math.prod(s) for s in shapes.values()]
|
||||||
|
|
||||||
|
|
||||||
def make_input_queues(input_shapes, frame_skip, device, frame_copy_size):
|
def make_input_queues(input_shapes, frame_skip, device):
|
||||||
img = input_shapes['img'] # (1, 12, 128, 256)
|
input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device)
|
||||||
fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature
|
|
||||||
feat_dim = math.prod(fb[2:])
|
|
||||||
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
|
||||||
n_frames = img[1] // 6
|
|
||||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
|
|
||||||
|
|
||||||
policy_shapes, _ = get_policy_npy_shapes(input_shapes)
|
fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature
|
||||||
shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes
|
dp = input_shapes['desire_pulse'] # (1, 25, 8)
|
||||||
sizes = [math.prod(s) for s in shapes.values()]
|
|
||||||
packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize
|
shapes, sizes = get_policy_npy_shapes(input_shapes)
|
||||||
packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8)
|
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
|
||||||
packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32)
|
|
||||||
frames = packed_input[packed_npy_size:]
|
|
||||||
frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]}
|
|
||||||
# views into the packed inputs, to be refilled at runtime
|
# views into the packed inputs, to be refilled at runtime
|
||||||
npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}
|
npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)})
|
||||||
input_queues = {
|
input_queues.update({
|
||||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
|
||||||
'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(),
|
|
||||||
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(),
|
||||||
'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(),
|
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
|
||||||
}
|
})
|
||||||
return input_queues, npy, frame_views
|
return input_queues, npy
|
||||||
|
|
||||||
|
|
||||||
def shift_and_sample(buf, new_val, sample_fn):
|
def shift_and_sample(buf, new_val, sample_fn):
|
||||||
@@ -165,15 +176,13 @@ def sample_desire(buf, frame_skip):
|
|||||||
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
|
||||||
|
|
||||||
|
|
||||||
def make_warp(nv12, model_w, model_h):
|
def make_warp(nv12, model_w, model_h, frame_skip):
|
||||||
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
|
||||||
|
|
||||||
def warp(tfm, big_tfm, frame, big_frame):
|
def warp(tfm, big_tfm, frame, big_frame):
|
||||||
tfm = tfm.to(Device.DEFAULT)
|
tfm = tfm.to(WARP_DEV)
|
||||||
big_tfm = big_tfm.to(Device.DEFAULT)
|
big_tfm = big_tfm.to(WARP_DEV)
|
||||||
frame = frame.to(Device.DEFAULT)
|
Tensor.realize(tfm, big_tfm)
|
||||||
big_frame = big_frame.to(Device.DEFAULT)
|
|
||||||
Tensor.realize(tfm, big_tfm, frame, big_frame)
|
|
||||||
|
|
||||||
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
warped_frame = frame_prepare(frame, tfm).unsqueeze(0)
|
||||||
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0)
|
||||||
@@ -186,10 +195,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
|||||||
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
|
||||||
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
|
||||||
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
||||||
model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()}
|
|
||||||
|
|
||||||
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
||||||
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT)
|
||||||
|
warped = warped.to(Device.DEFAULT)
|
||||||
Tensor.realize(packed_npy_inputs, warped)
|
Tensor.realize(packed_npy_inputs, warped)
|
||||||
|
|
||||||
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
img = shift_and_sample(img_q, warped[0:1], sample_skip_fn)
|
||||||
@@ -202,50 +211,33 @@ def make_run_policy(model_runner, model_metadata, frame_skip):
|
|||||||
inputs = {
|
inputs = {
|
||||||
'img': img,
|
'img': img,
|
||||||
'big_img': big_img,
|
'big_img': big_img,
|
||||||
'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']),
|
'features_buffer': feat_buf,
|
||||||
'desire_pulse': desire_buf,
|
'desire_pulse': desire_buf,
|
||||||
'traffic_convention': traffic_convention,
|
'traffic_convention': traffic_convention,
|
||||||
'action_t': action_t,
|
'action_t': action_t,
|
||||||
}
|
}
|
||||||
inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()}
|
|
||||||
out = next(iter(model_runner(inputs).values())).cast('float32')
|
out = next(iter(model_runner(inputs).values())).cast('float32')
|
||||||
return out,
|
return out,
|
||||||
return run_policy
|
return run_policy
|
||||||
|
|
||||||
|
|
||||||
def make_run_model(warp, run_policy, model_metadata, frame_copy_size):
|
def compile_jit(jit, make_random_inputs, input_keys, make_queues):
|
||||||
_, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes'])
|
|
||||||
packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize
|
|
||||||
|
|
||||||
def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs):
|
|
||||||
packed_input = packed_npy_inputs.to(Device.DEFAULT)
|
|
||||||
Tensor.realize(packed_input)
|
|
||||||
packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32')
|
|
||||||
frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size]
|
|
||||||
big_frame = packed_input[packed_npy_size + frame_copy_size:]
|
|
||||||
tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)])
|
|
||||||
warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame)
|
|
||||||
return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs)
|
|
||||||
return run_model
|
|
||||||
|
|
||||||
|
|
||||||
def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
|
||||||
if benchmark_runs < 1:
|
|
||||||
raise ValueError("benchmark_runs must be at least 1")
|
|
||||||
|
|
||||||
SEED = 42
|
SEED = 42
|
||||||
def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True):
|
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
|
||||||
input_queues, npy, frame_views = make_queues(Device.DEFAULT)
|
input_queues, npy = make_queues(Device.DEFAULT)
|
||||||
rng = np.random.default_rng(seed)
|
rng = np.random.default_rng(seed)
|
||||||
|
Tensor.manual_seed(seed)
|
||||||
|
|
||||||
|
testing = test_val is not None or test_buffers is not None
|
||||||
|
n_runs = 1 if testing else 3
|
||||||
|
|
||||||
for i in range(n_runs):
|
for i in range(n_runs):
|
||||||
for v in npy.values():
|
for v in npy.values():
|
||||||
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
|
||||||
for v in frame_views.values():
|
|
||||||
v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8)
|
|
||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
|
random_inputs = make_random_inputs()
|
||||||
st = time.perf_counter()
|
st = time.perf_counter()
|
||||||
outs = fn(**{k: input_queues[k] for k in input_keys})
|
outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs)
|
||||||
mt = time.perf_counter()
|
mt = time.perf_counter()
|
||||||
Device.default.synchronize()
|
Device.default.synchronize()
|
||||||
et = time.perf_counter()
|
et = time.perf_counter()
|
||||||
@@ -264,15 +256,14 @@ def compile_jit(jit, input_keys, make_queues, benchmark_runs):
|
|||||||
return val, buffers
|
return val, buffers
|
||||||
|
|
||||||
print('capture + replay')
|
print('capture + replay')
|
||||||
test_val, test_buffers = random_inputs_run(jit, SEED, 3)
|
test_val, test_buffers = random_inputs_run(jit, SEED)
|
||||||
print(f'pickle round trip ({benchmark_runs} runs per seed)')
|
print('pickle round trip')
|
||||||
with tempfile.TemporaryFile(dir=".") as f:
|
with tempfile.TemporaryFile(dir=".") as f:
|
||||||
dump_oob(jit, f)
|
dump_oob(jit, f)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
loaded_jit = load_oob(f)
|
jit = load_oob(f)
|
||||||
random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True)
|
random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True)
|
||||||
random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False)
|
random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False)
|
||||||
# Keep the original so per-resolution JITs share model weight buffers in the final pickle.
|
|
||||||
return jit
|
return jit
|
||||||
|
|
||||||
|
|
||||||
@@ -301,31 +292,27 @@ if __name__ == "__main__":
|
|||||||
p.add_argument('--onnx', required=True)
|
p.add_argument('--onnx', required=True)
|
||||||
p.add_argument('--output', required=True)
|
p.add_argument('--output', required=True)
|
||||||
p.add_argument('--frame-skip', type=int, required=True)
|
p.add_argument('--frame-skip', type=int, required=True)
|
||||||
p.add_argument('--benchmark-runs', type=int, default=1,
|
|
||||||
help='timed loaded-JIT runs for each correctness seed')
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
model_path = read_file_chunked_to_disk(args.onnx)
|
model_path = read_file_chunked_to_disk(args.onnx)
|
||||||
model_w, model_h = args.model_size
|
model_w, model_h = args.model_size
|
||||||
|
|
||||||
model_runner = OnnxRunner(model_path)
|
model_runner = OnnxRunner(model_path)
|
||||||
out = {
|
out = {'metadata': make_metadata_dict(model_path)}
|
||||||
'metadata': make_metadata_dict(model_path),
|
|
||||||
'input_devices': {'model': Device.DEFAULT},
|
|
||||||
'run_model': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip)
|
run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True)
|
||||||
|
|
||||||
|
make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||||
|
make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV)
|
||||||
|
out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS,
|
||||||
|
make_policy_queues)
|
||||||
|
|
||||||
for cam_w, cam_h in args.camera_resolutions:
|
for cam_w, cam_h in args.camera_resolutions:
|
||||||
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h))
|
||||||
frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height)
|
make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV)
|
||||||
make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip,
|
warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True)
|
||||||
frame_copy_size=frame_copy_size)
|
make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip)
|
||||||
warp = make_warp(nv12, model_w, model_h)
|
out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues)
|
||||||
run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True)
|
|
||||||
out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues,
|
|
||||||
args.benchmark_runs)
|
|
||||||
|
|
||||||
with open(args.output, "wb") as f:
|
with open(args.output, "wb") as f:
|
||||||
dump_oob(out, f)
|
dump_oob(out, f)
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn
|
|||||||
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
driving_model_data.frameIdExtra = modelV2.frameIdExtra
|
||||||
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
driving_model_data.frameDropPerc = modelV2.frameDropPerc
|
||||||
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
driving_model_data.modelExecutionTime = modelV2.modelExecutionTime
|
||||||
driving_model_data.big = modelV2.big
|
|
||||||
driving_model_data.action = modelV2.action
|
driving_model_data.action = modelV2.action
|
||||||
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState
|
||||||
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openpilot.common.file_chunker import get_manifest_path
|
from openpilot.common.file_chunker import get_manifest_path
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id
|
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||||
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
||||||
CHESTNUT_POWERED_VOLTAGE = 5000
|
|
||||||
CHESTNUT_PCIE_READY = 0x78
|
|
||||||
|
|
||||||
|
|
||||||
def get_tg_input_devices(process_name: str, chestnut: bool):
|
def get_tg_input_devices(process_name: str, chestnut: bool):
|
||||||
@@ -52,7 +50,7 @@ def chestnut_present() -> bool:
|
|||||||
try:
|
try:
|
||||||
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
||||||
product = (d / "product").read_text().strip()
|
product = (d / "product").read_text().strip()
|
||||||
if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT:
|
if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN":
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -60,7 +58,3 @@ def chestnut_present() -> bool:
|
|||||||
|
|
||||||
def chestnut_compiled() -> bool:
|
def chestnut_compiled() -> bool:
|
||||||
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
||||||
|
|
||||||
|
|
||||||
def chestnut_ready(state) -> bool:
|
|
||||||
return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import ctypes
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
import os
|
import os
|
||||||
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
|
||||||
|
from tinygrad.tensor import Tensor
|
||||||
from tinygrad.device import Device
|
from tinygrad.device import Device
|
||||||
import usb1
|
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -28,17 +28,17 @@ from openpilot.common.transformations.model import get_warp_matrix
|
|||||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
|
||||||
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS
|
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
|
||||||
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
|
||||||
from openpilot.common.file_chunker import open_file_chunked
|
from openpilot.common.file_chunker import open_file_chunked
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_USB_IDS
|
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob
|
from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||||
|
|
||||||
|
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
|
||||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||||
|
|
||||||
LAT_SMOOTH_SECONDS = 0.0
|
LAT_SMOOTH_SECONDS = 0.0
|
||||||
@@ -83,37 +83,6 @@ class ChestnutState:
|
|||||||
self.valid = True
|
self.valid = True
|
||||||
self.sends = 0
|
self.sends = 0
|
||||||
self.metrics = {}
|
self.metrics = {}
|
||||||
self._asm_usb = None
|
|
||||||
|
|
||||||
def _close_asm_usb(self) -> None:
|
|
||||||
if self._asm_usb is not None:
|
|
||||||
self._asm_usb.close()
|
|
||||||
self._asm_usb = None
|
|
||||||
|
|
||||||
def _open_asm_usb(self):
|
|
||||||
context = usb1.USBContext()
|
|
||||||
for vendor_id, product_id in CHESTNUT_USB_IDS:
|
|
||||||
if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None:
|
|
||||||
return handle
|
|
||||||
context.close()
|
|
||||||
|
|
||||||
def _read_ina(self) -> tuple[int, int, bool]:
|
|
||||||
if "AMD" in Device._opened_devices and self._asm_usb is None:
|
|
||||||
try:
|
|
||||||
raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5)
|
|
||||||
return struct.unpack('<Hh?', bytes(raw))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self._asm_usb is None:
|
|
||||||
self._asm_usb = self._open_asm_usb()
|
|
||||||
if self._asm_usb is None:
|
|
||||||
raise usb1.USBErrorNoDevice
|
|
||||||
try:
|
|
||||||
raw = self._asm_usb.controlRead(0xC0, 0xC0, 0, 0, 5, timeout=100)
|
|
||||||
except usb1.USBError:
|
|
||||||
self._close_asm_usb()
|
|
||||||
raise
|
|
||||||
return struct.unpack('<Hh?', bytes(raw))
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def power_limit(self) -> int:
|
def power_limit(self) -> int:
|
||||||
@@ -149,15 +118,13 @@ class ChestnutState:
|
|||||||
setattr(state, k, v)
|
setattr(state, k, v)
|
||||||
|
|
||||||
asm_valid = False
|
asm_valid = False
|
||||||
try:
|
|
||||||
# ASM runs on USB-C power, these still read without a gpu
|
|
||||||
state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina()
|
|
||||||
asm_valid = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if "AMD" in Device._opened_devices:
|
if "AMD" in Device._opened_devices:
|
||||||
try:
|
try:
|
||||||
state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0]
|
# ASM runs on USB-C power, these still read without a gpu
|
||||||
|
asm = Device["AMD"].iface.pci_dev.usb
|
||||||
|
state.pcieLtssm = asm.read(0xB450, 1)[0]
|
||||||
|
state.supplyVoltage, state.supplyCurrent = struct.unpack('<Hh', bytes(asm.usb.control_read(0xC0, 5))[:4])
|
||||||
|
asm_valid = True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -180,9 +147,9 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
|
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
|
||||||
|
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||||
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
||||||
input_devices = jits['input_devices']
|
|
||||||
self.model_device = input_devices['model']
|
|
||||||
metadata = jits['metadata']
|
metadata = jits['metadata']
|
||||||
self.input_shapes = metadata['input_shapes']
|
self.input_shapes = metadata['input_shapes']
|
||||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||||
@@ -192,11 +159,13 @@ class ModelState(ModelStateBase):
|
|||||||
self.chestnut = chestnut
|
self.chestnut = chestnut
|
||||||
|
|
||||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
self.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3])
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
self.full_frames: dict[str, Tensor] = {}
|
||||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.run_model = jits['run_model'][(cam_w,cam_h)]
|
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||||
|
self.run_policy = jits['run_policy']
|
||||||
|
self.warp = jits[(cam_w,cam_h)]
|
||||||
|
|
||||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||||
@@ -204,8 +173,14 @@ class ModelState(ModelStateBase):
|
|||||||
|
|
||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
||||||
for key, buf in bufs.items():
|
for key in bufs.keys():
|
||||||
np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size))
|
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||||
|
yuv_size = self.frame_buf_params[key][3]
|
||||||
|
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||||
|
cache_key = (key, ptr)
|
||||||
|
if cache_key not in self._blob_cache:
|
||||||
|
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||||
|
self.full_frames[key] = self._blob_cache[cache_key]
|
||||||
|
|
||||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire_pulse'][0] = 0
|
inputs['desire_pulse'][0] = 0
|
||||||
@@ -216,7 +191,11 @@ class ModelState(ModelStateBase):
|
|||||||
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
self.npy['tfm'][:,:] = transforms['img'][:,:]
|
||||||
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
|
||||||
|
|
||||||
outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS})
|
warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
|
||||||
|
|
||||||
|
outs, = self.run_policy(
|
||||||
|
**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped
|
||||||
|
)
|
||||||
if after_enqueue is not None:
|
if after_enqueue is not None:
|
||||||
after_enqueue()
|
after_enqueue()
|
||||||
model_output = outs.numpy()[0]
|
model_output = outs.numpy()[0]
|
||||||
@@ -230,37 +209,25 @@ class ModelState(ModelStateBase):
|
|||||||
return outputs_dict
|
return outputs_dict
|
||||||
|
|
||||||
def warmup(self) -> None:
|
def warmup(self) -> None:
|
||||||
dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names}
|
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
|
||||||
eye = np.eye(3, dtype=np.float32)
|
eye = np.eye(3, dtype=np.float32)
|
||||||
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
|
||||||
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
|
||||||
self.input_queues, self.npy, self.frame_views = make_input_queues(
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size)
|
|
||||||
self.prev_desire[:] = 0
|
self.prev_desire[:] = 0
|
||||||
|
self.full_frames.clear()
|
||||||
|
self._blob_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
def main(demo=False):
|
def main(demo=False):
|
||||||
cloudlog.warning("modeld init")
|
cloudlog.warning("modeld init")
|
||||||
|
|
||||||
chestnut_available = chestnut_present() and chestnut_compiled()
|
CHESTNUT = chestnut_present() and chestnut_compiled()
|
||||||
CHESTNUT = False
|
|
||||||
if chestnut_available:
|
|
||||||
poller = messaging.Poller()
|
|
||||||
sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True)
|
|
||||||
deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency
|
|
||||||
while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.:
|
|
||||||
if not poller.poll(round(remaining * 1000)):
|
|
||||||
break
|
|
||||||
msg = messaging.recv_one_or_none(sock)
|
|
||||||
CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState)
|
|
||||||
if CHESTNUT:
|
if CHESTNUT:
|
||||||
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
||||||
params = Params()
|
params = Params()
|
||||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||||
if chestnut_available and not CHESTNUT:
|
params.remove("ChestnutActive")
|
||||||
params.put_bool("ChestnutActive", False)
|
|
||||||
else:
|
|
||||||
params.remove("ChestnutActive")
|
|
||||||
|
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
|
|
||||||
@@ -304,11 +271,7 @@ def main(demo=False):
|
|||||||
loader.start()
|
loader.start()
|
||||||
loader.join(BIG_MODEL_TIMEOUT)
|
loader.join(BIG_MODEL_TIMEOUT)
|
||||||
model = big_model
|
model = big_model
|
||||||
if model is None:
|
|
||||||
params.put_bool("ChestnutModelError", True)
|
|
||||||
params.put_bool("ChestnutActive", model is not None)
|
params.put_bool("ChestnutActive", model is not None)
|
||||||
if model is not None:
|
|
||||||
params.remove("ChestnutModelError")
|
|
||||||
|
|
||||||
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
|
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
|
||||||
if model is None:
|
if model is None:
|
||||||
@@ -442,7 +405,6 @@ def main(demo=False):
|
|||||||
raise
|
raise
|
||||||
# fallback to small model
|
# fallback to small model
|
||||||
cloudlog.exception("big model failed, fall back to small")
|
cloudlog.exception("big model failed, fall back to small")
|
||||||
params.put_bool("ChestnutModelError", True)
|
|
||||||
params.put_bool("ChestnutActive", False)
|
params.put_bool("ChestnutActive", False)
|
||||||
assert small_model is not None
|
assert small_model is not None
|
||||||
model = small_model
|
model = small_model
|
||||||
@@ -469,11 +431,12 @@ def main(demo=False):
|
|||||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||||
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||||
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
|
||||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, left_edge, right_edge)
|
|
||||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
|
mdv2sp_send = messaging.new_message('modelDataV2SP')
|
||||||
|
left_edge, right_edge = RELC.update_and_fill(modelv2_send.modelV2, mdv2sp_send.modelDataV2SP, v_ego)
|
||||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
|
|
||||||
fill_driving_model_data(drivingdata_send, modelv2_send)
|
fill_driving_model_data(drivingdata_send, modelv2_send)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123
|
oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff
|
||||||
size 765950064
|
size 1757355221
|
||||||
|
|||||||
@@ -123,22 +123,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
|||||||
ps.setUptime(health.uptime_pkt);
|
ps.setUptime(health.uptime_pkt);
|
||||||
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt);
|
||||||
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt);
|
||||||
ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U);
|
ps.setIgnitionLine(health.ignition_line_pkt);
|
||||||
ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U);
|
ps.setIgnitionCan(health.ignition_can_pkt);
|
||||||
ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U);
|
ps.setControlsAllowed(health.controls_allowed_pkt);
|
||||||
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt);
|
||||||
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt);
|
||||||
ps.setPandaType(hw_type);
|
ps.setPandaType(hw_type);
|
||||||
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt));
|
||||||
ps.setSafetyParam(health.safety_param_pkt);
|
ps.setSafetyParam(health.safety_param_pkt);
|
||||||
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt));
|
||||||
ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U);
|
ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt));
|
||||||
ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U);
|
ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt));
|
||||||
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
ps.setAlternativeExperience(health.alternative_experience_pkt);
|
||||||
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt));
|
||||||
ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f);
|
ps.setInterruptLoad(health.interrupt_load_pkt);
|
||||||
ps.setFanPower(health.fan_power);
|
ps.setFanPower(health.fan_power);
|
||||||
ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U);
|
ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt));
|
||||||
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
ps.setSpiErrorCount(health.spi_error_count_pkt);
|
||||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||||
@@ -198,10 +198,10 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (spoofing_started) {
|
if (spoofing_started) {
|
||||||
health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE;
|
health.ignition_line_pkt = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ignition_local = ((health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U) && !always_offroad;
|
bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)) && !always_offroad;
|
||||||
|
|
||||||
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
// Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node
|
||||||
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) {
|
||||||
@@ -209,7 +209,7 @@ std::optional<bool> send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool power_save_desired = !ignition_local;
|
bool power_save_desired = !ignition_local;
|
||||||
if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) {
|
if (health.power_save_enabled_pkt != power_save_desired) {
|
||||||
panda->set_power_saving(power_save_desired);
|
panda->set_power_saving(power_save_desired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,30 +19,6 @@
|
|||||||
},
|
},
|
||||||
"Offroad_ChestnutBranch": {
|
"Offroad_ChestnutBranch": {
|
||||||
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
"text": "Chestnut detected! Switch to the %1 branch to use chestnut-class models.",
|
||||||
"severity": -1
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutNotDetected": {
|
|
||||||
"text": "Chestnut not detected. Check USB and 12V connections.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutOverheated": {
|
|
||||||
"text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutPcieUnavailable": {
|
|
||||||
"text": "%1",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUncompiled": {
|
|
||||||
"text": "Chestnut model not compiled. Keep ignition on and reboot the comma.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUpdateFailed": {
|
|
||||||
"text": "Chestnut update failed. Check the USB cable.",
|
|
||||||
"severity": 0
|
|
||||||
},
|
|
||||||
"Offroad_ChestnutUsbSlow": {
|
|
||||||
"text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.",
|
|
||||||
"severity": 0
|
"severity": 0
|
||||||
},
|
},
|
||||||
"Offroad_UnregisteredHardware": {
|
"Offroad_UnregisteredHardware": {
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
|||||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
||||||
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
||||||
|
from openpilot.sunnypilot.selfdrive.selfdrived.assisted_driving_milestones import (
|
||||||
|
AssistCategory,
|
||||||
|
AssistedDrivingMilestones,
|
||||||
|
MilestoneEvent,
|
||||||
|
MilestoneStore,
|
||||||
|
)
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
|
from openpilot.sunnypilot.system.statsd import statlog
|
||||||
|
|
||||||
REPLAY = "REPLAY" in os.environ
|
REPLAY = "REPLAY" in os.environ
|
||||||
SIMULATION = "SIMULATION" in os.environ
|
SIMULATION = "SIMULATION" in os.environ
|
||||||
@@ -88,7 +95,8 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.big_model_ready_t = 0.
|
self.big_model_ready_t = 0.
|
||||||
|
|
||||||
# Setup sockets
|
# Setup sockets
|
||||||
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP'])
|
self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] +
|
||||||
|
['selfdriveStateSP', 'onroadEventsSP', 'assistedDrivingMilestoneState'])
|
||||||
|
|
||||||
self.gps_location_service = get_gps_location_service(self.params)
|
self.gps_location_service = get_gps_location_service(self.params)
|
||||||
self.gps_packets = [self.gps_location_service]
|
self.gps_packets = [self.gps_location_service]
|
||||||
@@ -127,6 +135,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.params.remove("ExperimentalMode")
|
self.params.remove("ExperimentalMode")
|
||||||
|
|
||||||
self.CS_prev = car.CarState.new_message()
|
self.CS_prev = car.CarState.new_message()
|
||||||
|
self.car_state_log_mono_time = 0
|
||||||
self.AM = AlertManager()
|
self.AM = AlertManager()
|
||||||
self.events = Events()
|
self.events = Events()
|
||||||
|
|
||||||
@@ -137,6 +146,11 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.cruise_mismatch_counter = 0
|
self.cruise_mismatch_counter = 0
|
||||||
self.last_steering_pressed_frame = 0
|
self.last_steering_pressed_frame = 0
|
||||||
self.distance_traveled = 0
|
self.distance_traveled = 0
|
||||||
|
self.assisted_driving_milestones = AssistedDrivingMilestones(MilestoneStore(self.params))
|
||||||
|
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
|
||||||
|
self.assisted_driving_milestone_drive_id = ""
|
||||||
|
self._milestone_event: MilestoneEvent | None = None
|
||||||
|
self._milestone_event_expires_ns = 0
|
||||||
self.last_functional_fan_frame = 0
|
self.last_functional_fan_frame = 0
|
||||||
self.events_prev = []
|
self.events_prev = []
|
||||||
self.logged_comm_issue = None
|
self.logged_comm_issue = None
|
||||||
@@ -528,6 +542,8 @@ class SelfdriveD(CruiseHelper):
|
|||||||
def data_sample(self):
|
def data_sample(self):
|
||||||
_car_state = messaging.recv_one(self.car_state_sock)
|
_car_state = messaging.recv_one(self.car_state_sock)
|
||||||
CS = _car_state.carState if _car_state else self.CS_prev
|
CS = _car_state.carState if _car_state else self.CS_prev
|
||||||
|
if _car_state is not None:
|
||||||
|
self.car_state_log_mono_time = _car_state.logMonoTime
|
||||||
|
|
||||||
self.sm.update(0)
|
self.sm.update(0)
|
||||||
|
|
||||||
@@ -646,6 +662,31 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.pm.send('onroadEventsSP', ce_send_sp)
|
self.pm.send('onroadEventsSP', ce_send_sp)
|
||||||
self.events_sp_prev = self.events_sp.names.copy()
|
self.events_sp_prev = self.events_sp.names.copy()
|
||||||
|
|
||||||
|
def publish_assisted_driving_milestones(self, now_ns: int, event: MilestoneEvent | None) -> None:
|
||||||
|
if event is not None:
|
||||||
|
self._milestone_event = event
|
||||||
|
self._milestone_event_expires_ns = now_ns + 1_000_000_000
|
||||||
|
elif now_ns >= self._milestone_event_expires_ns:
|
||||||
|
self._milestone_event = None
|
||||||
|
|
||||||
|
if event is None and self.sm.frame % 10 != 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
snapshot = self.assisted_driving_milestones.snapshot()
|
||||||
|
msg = messaging.new_message("assistedDrivingMilestoneState")
|
||||||
|
msg.valid = True
|
||||||
|
state = msg.assistedDrivingMilestoneState
|
||||||
|
state.enabled = self.assisted_driving_milestones_enabled
|
||||||
|
state.madsDistanceMeters = snapshot.distances_meters[AssistCategory.MADS]
|
||||||
|
state.fullAssistDistanceMeters = snapshot.distances_meters[AssistCategory.FULL_ASSIST]
|
||||||
|
if self._milestone_event is not None:
|
||||||
|
state.event.id = self._milestone_event.event_id
|
||||||
|
state.event.category = self._milestone_event.category.value
|
||||||
|
state.event.distanceMeters = self._milestone_event.distance_meters
|
||||||
|
state.event.previousDistanceMeters = self._milestone_event.previous_distance_meters
|
||||||
|
state.event.unit = self._milestone_event.unit.value
|
||||||
|
self.pm.send("assistedDrivingMilestoneState", msg)
|
||||||
|
|
||||||
def step(self):
|
def step(self):
|
||||||
CS = self.data_sample()
|
CS = self.data_sample()
|
||||||
self.update_events(CS)
|
self.update_events(CS)
|
||||||
@@ -655,6 +696,28 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.mads.update(CS)
|
self.mads.update(CS)
|
||||||
self.update_alerts(CS)
|
self.update_alerts(CS)
|
||||||
|
|
||||||
|
now_ns = time.monotonic_ns()
|
||||||
|
if not self.assisted_driving_milestone_drive_id:
|
||||||
|
self.assisted_driving_milestone_drive_id = self.params.get("CurrentRoute") or ""
|
||||||
|
self.assisted_driving_milestones.set_drive_id(self.assisted_driving_milestone_drive_id)
|
||||||
|
car_control = self.sm['carControl']
|
||||||
|
milestone_event = self.assisted_driving_milestones.update(
|
||||||
|
self.car_state_log_mono_time,
|
||||||
|
CS.vEgo,
|
||||||
|
lat_active=car_control.latActive,
|
||||||
|
long_active=car_control.longActive,
|
||||||
|
is_metric=self.is_metric,
|
||||||
|
enabled=self.assisted_driving_milestones_enabled,
|
||||||
|
)
|
||||||
|
if milestone_event is not None:
|
||||||
|
cloudlog.event("assisted_driving_milestone_reached",
|
||||||
|
event_id=milestone_event.event_id,
|
||||||
|
category=milestone_event.category.value,
|
||||||
|
distance_meters=milestone_event.distance_meters)
|
||||||
|
statlog.gauge(f"assisted_driving_milestone.{milestone_event.category.value}.meters",
|
||||||
|
milestone_event.distance_meters)
|
||||||
|
self.publish_assisted_driving_milestones(now_ns, milestone_event)
|
||||||
|
|
||||||
self.button_state_tracker.update(CS)
|
self.button_state_tracker.update(CS)
|
||||||
self.publish_selfdriveState(CS)
|
self.publish_selfdriveState(CS)
|
||||||
|
|
||||||
@@ -667,6 +730,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||||
|
self.assisted_driving_milestones_enabled = bool(self.params.get("AssistedDrivingMilestonesEnabled", return_default=True))
|
||||||
|
|
||||||
self.mads.read_params()
|
self.mads.read_params()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -680,6 +744,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.step()
|
self.step()
|
||||||
self.rk.monitor_time()
|
self.rk.monitor_time()
|
||||||
finally:
|
finally:
|
||||||
|
self.assisted_driving_milestones.close()
|
||||||
e.set()
|
e.set()
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ def migrate_drivingModelData(msgs):
|
|||||||
add_ops = []
|
add_ops = []
|
||||||
for _, msg in msgs:
|
for _, msg in msgs:
|
||||||
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime)
|
||||||
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]:
|
for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]:
|
||||||
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field))
|
||||||
for meta_field in ["laneChangeState", "laneChangeState"]:
|
for meta_field in ["laneChangeState", "laneChangeState"]:
|
||||||
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field))
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master"
|
|||||||
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN)
|
||||||
|
|
||||||
EXEC_TIMINGS = [
|
EXEC_TIMINGS = [
|
||||||
# model, instant max, average max, chestnut average max
|
# model, instant max, average max
|
||||||
("modelV2", 0.05, 0.03, 0.05),
|
("modelV2", 0.05, 0.028),
|
||||||
("driverStateV2", 0.05, 0.018, 0.018),
|
("driverStateV2", 0.05, 0.018),
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_log_fn(test_route, ref="master"):
|
def get_log_fn(test_route, ref="master"):
|
||||||
@@ -169,13 +169,11 @@ def model_replay(lr, frs):
|
|||||||
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs)
|
||||||
|
|
||||||
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
msgs = modeld_msgs + dmonitoringmodeld_msgs
|
||||||
chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2")
|
|
||||||
|
|
||||||
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result']
|
||||||
rows = []
|
rows = []
|
||||||
timings_ok = True
|
timings_ok = True
|
||||||
for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS:
|
for (s, instant_max, avg_max) in EXEC_TIMINGS:
|
||||||
avg_max = chestnut_avg_max if chestnut else avg_max
|
|
||||||
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s]
|
||||||
# TODO some init can happen in first iteration
|
# TODO some init can happen in first iteration
|
||||||
ts = ts[1:]
|
ts = ts[1:]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
@@ -26,8 +26,8 @@ class BodyLayout(Widget):
|
|||||||
self._last_input_time = time.monotonic()
|
self._last_input_time = time.monotonic()
|
||||||
self._was_active = False
|
self._was_active = False
|
||||||
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color):
|
||||||
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto
|
|||||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.system.ui.lib.multilang import tr, trn
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -178,7 +178,7 @@ class HomeLayout(Widget):
|
|||||||
|
|
||||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||||
version_text_width, self.header_rect.height)
|
version_text_width, self.header_rect.height)
|
||||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT)
|
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
|
|
||||||
def _render_home_content(self):
|
def _render_home_content(self):
|
||||||
self._render_left_column()
|
self._render_left_column()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from enum import IntEnum
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment, gui_app
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
@@ -115,9 +115,9 @@ class TermsPage(Widget):
|
|||||||
self._on_accept = on_accept
|
self._on_accept = on_accept
|
||||||
self._on_decline = on_decline
|
self._on_decline = on_decline
|
||||||
|
|
||||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT)
|
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
|
|
||||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||||
@@ -150,7 +150,7 @@ class DeclinePage(Widget):
|
|||||||
def __init__(self, back_callback=None):
|
def __init__(self, back_callback=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT)
|
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||||
click_callback=self._on_uninstall_clicked)
|
click_callback=self._on_uninstall_clicked)
|
||||||
|
|||||||
@@ -199,9 +199,6 @@ class SoftwareLayout(Widget):
|
|||||||
selection = self._branch_dialog.selection
|
selection = self._branch_dialog.selection
|
||||||
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
ui_state.params.put("UpdaterTargetBranch", selection, block=True)
|
||||||
self._branch_btn.action_item.set_value(selection)
|
self._branch_btn.action_item.set_value(selection)
|
||||||
self._download_btn.action_item.set_enabled(False)
|
|
||||||
self._waiting_for_updater = True
|
|
||||||
self._waiting_start_ts = time.monotonic()
|
|
||||||
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
self._branch_dialog = None
|
self._branch_dialog = None
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
@@ -9,7 +8,7 @@ from openpilot.system.ui.widgets import Widget
|
|||||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||||
from openpilot.common.version import RELEASE_BRANCHES
|
from openpilot.common.version import RELEASE_BRANCHES
|
||||||
|
|
||||||
@@ -70,8 +69,8 @@ class AlertsPill(Widget):
|
|||||||
|
|
||||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||||
gui_label(count_rect, str(alert_count), font_size=36,
|
gui_label(count_rect, str(alert_count), font_size=36,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
|
|
||||||
class NetworkIcon(Widget):
|
class NetworkIcon(Widget):
|
||||||
@@ -140,9 +139,7 @@ class MiciHomeLayout(Widget):
|
|||||||
self._version_text = self._get_version_text()
|
self._version_text = self._get_version_text()
|
||||||
|
|
||||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||||
self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40))
|
|
||||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
|
||||||
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
|
||||||
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
|
||||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||||
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
self._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
||||||
@@ -153,15 +150,13 @@ class MiciHomeLayout(Widget):
|
|||||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||||
NetworkIcon(),
|
NetworkIcon(),
|
||||||
self._experimental_icon,
|
self._experimental_icon,
|
||||||
self._usb_icon,
|
|
||||||
self._chestnut_icon,
|
self._chestnut_icon,
|
||||||
self._chestnut_loading_icon,
|
|
||||||
self._chestnut_failed_icon,
|
self._chestnut_failed_icon,
|
||||||
self._body_icon,
|
self._body_icon,
|
||||||
self._mic_icon,
|
self._mic_icon,
|
||||||
], spacing=18)
|
], spacing=18)
|
||||||
|
|
||||||
self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False)
|
||||||
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False)
|
||||||
@@ -252,20 +247,12 @@ class MiciHomeLayout(Widget):
|
|||||||
self._version_commit_label.render()
|
self._version_commit_label.render()
|
||||||
|
|
||||||
# ***** Center-aligned bottom section icons *****
|
# ***** Center-aligned bottom section icons *****
|
||||||
usb_connected = ui_state.usb_connected
|
|
||||||
usb_unknown = ui_state.usb_unknown
|
|
||||||
chestnut_state = ui_state.chestnut_state
|
|
||||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
self._set_chestnut_visibility()
|
self._set_chestnut_visibility()
|
||||||
else:
|
else:
|
||||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
|
||||||
self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in
|
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||||
(ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and
|
|
||||||
(usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)))
|
|
||||||
self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING)
|
|
||||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
|
||||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
|
||||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
import openpilot.cereal.messaging as messaging
|
import openpilot.cereal.messaging as messaging
|
||||||
|
from openpilot.common.hardware import PC
|
||||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||||
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout
|
||||||
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts
|
||||||
@@ -61,7 +64,8 @@ class MiciMainLayout(Scroller):
|
|||||||
|
|
||||||
# Start onboarding if terms or training not completed, make sure to push after self
|
# Start onboarding if terms or training not completed, make sure to push after self
|
||||||
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self))
|
||||||
if not self._onboarding_window.completed:
|
skip_onboarding_for_milestone_preview = PC and os.getenv("SP_MILESTONE_PREVIEW") == "1"
|
||||||
|
if not self._onboarding_window.completed and not skip_onboarding_for_milestone_preview:
|
||||||
gui_app.push_widget(self._onboarding_window)
|
gui_app.push_widget(self._onboarding_window)
|
||||||
|
|
||||||
# initialize correct onroad layout
|
# initialize correct onroad layout
|
||||||
@@ -119,6 +123,8 @@ class MiciMainLayout(Scroller):
|
|||||||
self._onroad_time_delay = rl.get_time()
|
self._onroad_time_delay = rl.get_time()
|
||||||
else:
|
else:
|
||||||
self._scroll_to(self._home_layout)
|
self._scroll_to(self._home_layout)
|
||||||
|
if hasattr(self._home_layout, "request_drive_summary"):
|
||||||
|
self._home_layout.request_drive_summary()
|
||||||
|
|
||||||
# FIXME: these two pops can interrupt user interacting in the settings
|
# FIXME: these two pops can interrupt user interacting in the settings
|
||||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE
|
|||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
|
||||||
REFRESH_INTERVAL = 5.0 # seconds
|
REFRESH_INTERVAL = 5.0 # seconds
|
||||||
@@ -62,12 +62,12 @@ class AlertItem(Widget):
|
|||||||
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE)
|
||||||
|
|
||||||
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95)
|
||||||
|
|
||||||
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95)
|
||||||
|
|
||||||
self._title_text = ""
|
self._title_text = ""
|
||||||
self._body_text = ""
|
self._body_text = ""
|
||||||
@@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller):
|
|||||||
|
|
||||||
# Create empty state label
|
# Create empty state label
|
||||||
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
# Build initial alert list
|
# Build initial alert list
|
||||||
self._build_alerts()
|
self._build_alerts()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pyray as rl
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.common.qrcode import make_texture
|
from openpilot.common.qrcode import make_texture
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
from openpilot.system.ui.widgets.button import SmallCircleIconButton
|
||||||
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
from openpilot.system.ui.widgets.scroller import NavScroller, Scroller
|
||||||
@@ -35,7 +35,7 @@ class DriverCameraSetupDialog(BaseCabinCameraDialog):
|
|||||||
|
|
||||||
if not self._camera_view.frame:
|
if not self._camera_view.frame:
|
||||||
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER)
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ class SoftwareInfoLayoutMici(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class CheckUpdateButton(BigButton):
|
class CheckUpdateButton(BigButton):
|
||||||
UPDATER_PROC = "openpilot.system.updated.updated"
|
|
||||||
CHECK_FOR_UPDATE = "SIGUSR1"
|
|
||||||
DOWNLOAD_UPDATE = "SIGHUP"
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75)
|
||||||
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64)
|
||||||
@@ -101,20 +97,15 @@ class CheckUpdateButton(BigButton):
|
|||||||
gui_app.push_widget(dlg)
|
gui_app.push_widget(dlg)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE)
|
|
||||||
|
|
||||||
def check_for_update(self):
|
|
||||||
self._signal_updater(self.CHECK_FOR_UPDATE)
|
|
||||||
|
|
||||||
def _signal_updater(self, sig: str):
|
|
||||||
self.set_enabled(False)
|
self.set_enabled(False)
|
||||||
self._state = UpdaterState.WAITING_FOR_UPDATER
|
self._state = UpdaterState.WAITING_FOR_UPDATER
|
||||||
self._hide_value_t = None
|
|
||||||
self.set_value("")
|
|
||||||
self.set_icon(self._txt_update_icon)
|
self.set_icon(self._txt_update_icon)
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True)
|
if self.get_value() == "download update":
|
||||||
|
subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True)
|
||||||
|
else:
|
||||||
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
@@ -193,7 +184,7 @@ class CheckUpdateButton(BigButton):
|
|||||||
|
|
||||||
class InstallUpdateButton(BigButton):
|
class InstallUpdateButton(BigButton):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70))
|
||||||
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable"))
|
||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
@@ -241,9 +232,8 @@ class BranchSelectPage(NavScroller):
|
|||||||
|
|
||||||
|
|
||||||
class TargetBranchButton(BigButton):
|
class TargetBranchButton(BigButton):
|
||||||
def __init__(self, check_update_btn: CheckUpdateButton):
|
def __init__(self):
|
||||||
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "")
|
||||||
self._check_update_btn = check_update_btn
|
|
||||||
self.set_click_callback(self._on_click)
|
self.set_click_callback(self._on_click)
|
||||||
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
self.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||||
self.set_enabled(lambda: ui_state.is_offroad())
|
self.set_enabled(lambda: ui_state.is_offroad())
|
||||||
@@ -256,15 +246,12 @@ class TargetBranchButton(BigButton):
|
|||||||
self.set_value(target)
|
self.set_value(target)
|
||||||
|
|
||||||
def _on_click(self):
|
def _on_click(self):
|
||||||
if not ui_state.params.get("UpdaterAvailableBranches"):
|
|
||||||
gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again.")))
|
|
||||||
return
|
|
||||||
gui_app.push_widget(BranchSelectPage(self._on_select))
|
gui_app.push_widget(BranchSelectPage(self._on_select))
|
||||||
|
|
||||||
def _on_select(self, branch: str):
|
def _on_select(self, branch: str):
|
||||||
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
ui_state.params.put("UpdaterTargetBranch", branch, block=True)
|
||||||
self.set_value(branch)
|
self.set_value(branch)
|
||||||
self._check_update_btn.check_for_update()
|
subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True)
|
||||||
|
|
||||||
|
|
||||||
class SoftwareLayoutMici(NavScroller):
|
class SoftwareLayoutMici(NavScroller):
|
||||||
@@ -278,11 +265,10 @@ class SoftwareLayoutMici(NavScroller):
|
|||||||
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64),
|
||||||
uninstall_openpilot_callback, exit_on_confirm=False)
|
uninstall_openpilot_callback, exit_on_confirm=False)
|
||||||
|
|
||||||
check_update_btn = CheckUpdateButton()
|
|
||||||
self._scroller.add_widgets([
|
self._scroller.add_widgets([
|
||||||
SoftwareInfoLayoutMici(),
|
SoftwareInfoLayoutMici(),
|
||||||
check_update_btn,
|
CheckUpdateButton(),
|
||||||
InstallUpdateButton(),
|
InstallUpdateButton(),
|
||||||
TargetBranchButton(check_update_btn),
|
TargetBranchButton(),
|
||||||
uninstall_openpilot_btn,
|
uninstall_openpilot_btn,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
is_metric_toggle = BigParamControl("use metric units", "IsMetric")
|
||||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled")
|
||||||
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM")
|
||||||
|
milestone_celebrations_toggle = BigParamControl("assisted driving milestones", "AssistedDrivingMilestonesEnabled")
|
||||||
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
record_front = BigParamControl("record & upload cabin camera", "RecordFront", toggle_callback=restart_needed_callback)
|
||||||
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
|
||||||
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
|
||||||
@@ -57,6 +58,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
is_metric_toggle,
|
is_metric_toggle,
|
||||||
ldw_toggle,
|
ldw_toggle,
|
||||||
always_on_dm_toggle,
|
always_on_dm_toggle,
|
||||||
|
milestone_celebrations_toggle,
|
||||||
record_front,
|
record_front,
|
||||||
record_mic,
|
record_mic,
|
||||||
enable_openpilot,
|
enable_openpilot,
|
||||||
@@ -68,6 +70,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
("IsMetric", is_metric_toggle),
|
("IsMetric", is_metric_toggle),
|
||||||
("IsLdwEnabled", ldw_toggle),
|
("IsLdwEnabled", ldw_toggle),
|
||||||
("AlwaysOnDM", always_on_dm_toggle),
|
("AlwaysOnDM", always_on_dm_toggle),
|
||||||
|
("AssistedDrivingMilestonesEnabled", milestone_celebrations_toggle),
|
||||||
("RecordFront", record_front),
|
("RecordFront", record_front),
|
||||||
("RecordAudio", record_mic),
|
("RecordAudio", record_mic),
|
||||||
("OpenpilotEnabledToggle", enable_openpilot),
|
("OpenpilotEnabledToggle", enable_openpilot),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from opendbc.car.structs import car
|
|||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter
|
||||||
from openpilot.common.hardware import COMMA_HARDWARE
|
from openpilot.common.hardware import COMMA_HARDWARE
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ AlertSize = log.SelfdriveState.AlertSize
|
|||||||
AlertStatus = log.SelfdriveState.AlertStatus
|
AlertStatus = log.SelfdriveState.AlertStatus
|
||||||
|
|
||||||
ALERT_MARGIN = 18
|
ALERT_MARGIN = 18
|
||||||
|
ALERT_BACKGROUND_OPACITY = 0.90
|
||||||
|
|
||||||
ALERT_FONT_SMALL = 66 - 50
|
ALERT_FONT_SMALL = 66 - 50
|
||||||
ALERT_FONT_BIG = 88 - 40
|
ALERT_FONT_BIG = 88 - 40
|
||||||
@@ -279,7 +280,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
def _draw_background(self, alert: Alert) -> None:
|
def _draw_background(self, alert: Alert) -> None:
|
||||||
# draw top gradient for alert text at top
|
# draw top gradient for alert text at top
|
||||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
||||||
color = rl.Color(color.r, color.g, color.b, int(255 * 0.90 * self._alpha_filter.x))
|
color = rl.Color(color.r, color.g, color.b, int(255 * ALERT_BACKGROUND_OPACITY * self._alpha_filter.x))
|
||||||
translucent_color = rl.Color(color.r, color.g, color.b, int(0 * self._alpha_filter.x))
|
translucent_color = rl.Color(color.r, color.g, color.b, int(0 * self._alpha_filter.x))
|
||||||
|
|
||||||
small_alert_height = round(self._rect.height * 0.583) # 140px at mici height
|
small_alert_height = round(self._rect.height * 0.583) # 140px at mici height
|
||||||
@@ -333,7 +334,7 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
self._alert_text1_label.set_text(alert_text1)
|
self._alert_text1_label.set_text(alert_text1)
|
||||||
self._alert_text1_label.set_text_color(color)
|
self._alert_text1_label.set_text_color(color)
|
||||||
self._alert_text1_label.set_font_size(font_size)
|
self._alert_text1_label.set_font_size(font_size)
|
||||||
self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
self._alert_text1_label.render(text_rect1)
|
self._alert_text1_label.render(text_rect1)
|
||||||
|
|
||||||
alert_text2 = alert.text2.lower()
|
alert_text2 = alert.text2.lower()
|
||||||
@@ -365,5 +366,5 @@ class AlertRenderer(Widget, SpeedLimitAlertRenderer):
|
|||||||
self._alert_text2_label.set_text(alert_text2)
|
self._alert_text2_label.set_text(alert_text2)
|
||||||
self._alert_text2_label.set_text_color(color)
|
self._alert_text2_label.set_text_color(color)
|
||||||
self._alert_text2_label.set_font_size(small_font_size)
|
self._alert_text2_label.set_font_size(small_font_size)
|
||||||
self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT)
|
self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
self._alert_text2_label.render(text_rect2)
|
self._alert_text2_label.render(text_rect2)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
|||||||
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer
|
||||||
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall
|
||||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.common.filter_simple import BounceFilter
|
from openpilot.common.filter_simple import BounceFilter
|
||||||
@@ -19,10 +19,15 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCamera
|
|||||||
from openpilot.common.transformations.orientation import rot_from_euler
|
from openpilot.common.transformations.orientation import rot_from_euler
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
|
MILESTONE_CELEBRATION_ENABLED = gui_app.sunnypilot_ui()
|
||||||
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
||||||
|
|
||||||
|
if MILESTONE_CELEBRATION_ENABLED:
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.onroad.milestone_celebration import MilestoneCelebration
|
||||||
|
|
||||||
OpState = log.SelfdriveState.OpenpilotState
|
OpState = log.SelfdriveState.OpenpilotState
|
||||||
CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated
|
CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated
|
||||||
NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD
|
NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD
|
||||||
@@ -156,10 +161,11 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._alert_renderer = AlertRenderer()
|
self._alert_renderer = AlertRenderer()
|
||||||
self._driver_state_renderer = DriverStateRenderer()
|
self._driver_state_renderer = DriverStateRenderer()
|
||||||
self._confidence_ball = ConfidenceBall()
|
self._confidence_ball = ConfidenceBall()
|
||||||
|
self._milestone_celebration = self._child(MilestoneCelebration()) if MILESTONE_CELEBRATION_ENABLED else None
|
||||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||||
|
|
||||||
@@ -223,6 +229,12 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
alert_to_render, not_animating_out = self._alert_renderer.will_render()
|
||||||
|
|
||||||
|
if self._milestone_celebration is not None:
|
||||||
|
if alert_to_render is not None:
|
||||||
|
self._milestone_celebration.cancel_for_alert()
|
||||||
|
else:
|
||||||
|
self._milestone_celebration.render(self._content_rect)
|
||||||
|
|
||||||
# Hide DMoji when disengaged unless AlwaysOnDM is enabled
|
# Hide DMoji when disengaged unless AlwaysOnDM is enabled
|
||||||
should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and
|
should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and
|
||||||
(ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm))
|
(ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm))
|
||||||
@@ -247,7 +259,6 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._confidence_ball.render(self.rect)
|
self._confidence_ball.render(self.rect)
|
||||||
|
|
||||||
self._bookmark_icon.render(self.rect)
|
self._bookmark_icon.render(self.rect)
|
||||||
|
|
||||||
def _switch_stream_if_needed(self, sm):
|
def _switch_stream_if_needed(self, sm):
|
||||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
v_ego = sm['carState'].vEgo
|
v_ego = sm['carState'].vEgo
|
||||||
@@ -355,10 +366,12 @@ class AugmentedRoadView(CameraView):
|
|||||||
return self._cached_matrix
|
return self._cached_matrix
|
||||||
|
|
||||||
def show_event(self):
|
def show_event(self):
|
||||||
|
super().show_event()
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
|
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME)
|
||||||
|
|
||||||
def hide_event(self):
|
def hide_event(self):
|
||||||
|
super().hide_event()
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
|
ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
|||||||
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||||
@@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
|
|
||||||
if not self._camera_view.frame:
|
if not self._camera_view.frame:
|
||||||
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER)
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
self._publish_alert_sound(None)
|
self._publish_alert_sound(None)
|
||||||
return
|
return
|
||||||
@@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent
|
||||||
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height),
|
||||||
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=TextAlignment.RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
color=rl.Color(0, 0, 0, 180))
|
color=rl.Color(0, 0, 0, 180))
|
||||||
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM,
|
||||||
alignment=TextAlignment.RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
|
|
||||||
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none:
|
||||||
@@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget):
|
|||||||
|
|
||||||
# Show alert level
|
# Show alert level
|
||||||
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}"
|
||||||
alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT
|
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
||||||
|
|
||||||
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
||||||
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||||
color=rl.Color(0, 0, 0, 180))
|
color=rl.Color(0, 0, 0, 180))
|
||||||
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
alignment=alignment,
|
alignment=alignment,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM,
|
||||||
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
color=rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
|
|
||||||
def _load_eye_textures(self):
|
def _load_eye_textures(self):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
from openpilot.system.ui.widgets.scroller import DO_ZOOM
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.common.filter_simple import BounceFilter
|
from openpilot.common.filter_simple import BounceFilter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -125,10 +125,10 @@ class BigButton(Widget):
|
|||||||
self._rotate_icon_t: float | None = None
|
self._rotate_icon_t: float | None = None
|
||||||
|
|
||||||
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD,
|
||||||
text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll,
|
text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll,
|
||||||
line_height=0.9)
|
line_height=0.9)
|
||||||
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN,
|
||||||
text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
self._update_label_layout()
|
self._update_label_layout()
|
||||||
|
|
||||||
self._load_images()
|
self._load_images()
|
||||||
@@ -167,9 +167,9 @@ class BigButton(Widget):
|
|||||||
def _update_label_layout(self):
|
def _update_label_layout(self):
|
||||||
self._label.set_font_size(self._get_label_font_size())
|
self._label.set_font_size(self._get_label_font_size())
|
||||||
if self.value:
|
if self.value:
|
||||||
self._label.set_alignment_vertical(TextAlignmentVertical.TOP)
|
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
else:
|
else:
|
||||||
self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM)
|
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
|
|
||||||
def set_text(self, text: str):
|
def set_text(self, text: str):
|
||||||
self.text = text
|
self.text = text
|
||||||
@@ -356,8 +356,8 @@ class GreyBigButton(BigButton):
|
|||||||
self._sub_label.set_font_size(36)
|
self._sub_label.set_font_size(36)
|
||||||
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||||
self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else
|
self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else
|
||||||
TextAlignmentVertical.BOTTOM)
|
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
self._sub_label.set_line_height(0.95)
|
self._sub_label.set_line_height(0.95)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|||||||
from openpilot.cereal import messaging, log
|
from openpilot.cereal import messaging, log
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.common.hardware import COMMA_HARDWARE
|
from openpilot.common.hardware import COMMA_HARDWARE
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -76,10 +76,10 @@ class AlertRenderer(Widget):
|
|||||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||||
|
|
||||||
# font size is set dynamically
|
# font size is set dynamically
|
||||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER,
|
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER,
|
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_alignment_vertical=TextAlignmentVertical.TOP)
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||||
|
|
||||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||||
"""Generate the current alert based on selfdrive state."""
|
"""Generate the current alert based on selfdrive state."""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType
|
|||||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView):
|
|||||||
tr("camera starting"),
|
tr("camera starting"),
|
||||||
font_size=100,
|
font_size=100,
|
||||||
font_weight=FontWeight.BOLD,
|
font_weight=FontWeight.BOLD,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
)
|
)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,8 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate
|
|||||||
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
|
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
|
||||||
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
|
||||||
|
|
||||||
AMBIENT_DB = 26 # DB where MIN_VOLUME is applied
|
|
||||||
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
|
||||||
|
|
||||||
VOLUME_BASE = 20
|
|
||||||
if HARDWARE.get_device_type() == "tizi":
|
|
||||||
AMBIENT_DB = 30
|
|
||||||
VOLUME_BASE = 10
|
|
||||||
|
|
||||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||||
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
AudibleAlertSP = custom.SelfdriveStateSP.AudibleAlert
|
||||||
|
|
||||||
@@ -53,6 +47,7 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
|||||||
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
|
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
|
||||||
|
|
||||||
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
|
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
|
||||||
|
AudibleAlert.complete: ("milestone.wav", 1, MAX_VOLUME),
|
||||||
|
|
||||||
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
|
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
|
||||||
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
|
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
|
||||||
@@ -60,6 +55,14 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
|
|||||||
**sound_list_sp,
|
**sound_list_sp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_volume_for_device(weighted_db: float, device_type: str) -> float:
|
||||||
|
ambient_db = 30 if device_type in ("mici", "tizi") else 26
|
||||||
|
volume_base = 10 if device_type in ("mici", "tizi") else 20
|
||||||
|
volume_boost = 1.5 if device_type == "mici" else 1.0
|
||||||
|
volume = ((weighted_db - ambient_db) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME
|
||||||
|
return min(MAX_VOLUME, volume_boost * math.pow(volume_base, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1)))
|
||||||
|
|
||||||
def check_selfdrive_timeout_alert(sm):
|
def check_selfdrive_timeout_alert(sm):
|
||||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ class Soundd(QuietMode):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self.device_type = HARDWARE.get_device_type()
|
||||||
self.load_sounds()
|
self.load_sounds()
|
||||||
|
|
||||||
self.current_alert = AudibleAlert.none
|
self.current_alert = AudibleAlert.none
|
||||||
@@ -85,6 +89,7 @@ class Soundd(QuietMode):
|
|||||||
|
|
||||||
self.selfdrive_timeout_alert = False
|
self.selfdrive_timeout_alert = False
|
||||||
self.pending_stop = False
|
self.pending_stop = False
|
||||||
|
self.last_milestone_event_id = 0
|
||||||
|
|
||||||
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
|
||||||
|
|
||||||
@@ -164,9 +169,19 @@ class Soundd(QuietMode):
|
|||||||
self.update_alert(AudibleAlert.none)
|
self.update_alert(AudibleAlert.none)
|
||||||
self.selfdrive_timeout_alert = False
|
self.selfdrive_timeout_alert = False
|
||||||
|
|
||||||
|
def update_milestone_alert(self, sm):
|
||||||
|
if not sm.updated['assistedDrivingMilestoneState']:
|
||||||
|
return
|
||||||
|
milestone_state = sm['assistedDrivingMilestoneState']
|
||||||
|
event_id = milestone_state.event.id
|
||||||
|
if not milestone_state.enabled or event_id == 0 or event_id == self.last_milestone_event_id:
|
||||||
|
return
|
||||||
|
self.last_milestone_event_id = event_id
|
||||||
|
if self.current_alert == AudibleAlert.none and not self.enabled:
|
||||||
|
self.update_alert(AudibleAlert.complete)
|
||||||
|
|
||||||
def calculate_volume(self, weighted_db):
|
def calculate_volume(self, weighted_db):
|
||||||
volume = ((weighted_db - AMBIENT_DB) / DB_SCALE) * (MAX_VOLUME - MIN_VOLUME) + MIN_VOLUME
|
return calculate_volume_for_device(weighted_db, self.device_type)
|
||||||
return math.pow(VOLUME_BASE, (np.clip(volume, MIN_VOLUME, MAX_VOLUME) - 1))
|
|
||||||
|
|
||||||
@retry(attempts=10, delay=3)
|
@retry(attempts=10, delay=3)
|
||||||
def get_stream(self, sd):
|
def get_stream(self, sd):
|
||||||
@@ -180,7 +195,7 @@ class Soundd(QuietMode):
|
|||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
micd.patch_sounddevice(sd)
|
micd.patch_sounddevice(sd)
|
||||||
|
|
||||||
sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure'])
|
sm = messaging.SubMaster(['selfdriveState', 'selfdriveStateSP', 'soundPressure', 'assistedDrivingMilestoneState'])
|
||||||
|
|
||||||
with self.get_stream(sd) as stream:
|
with self.get_stream(sd) as stream:
|
||||||
rk = Ratekeeper(20)
|
rk = Ratekeeper(20)
|
||||||
@@ -198,6 +213,7 @@ class Soundd(QuietMode):
|
|||||||
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
||||||
|
|
||||||
self.get_audible_alert(sm)
|
self.get_audible_alert(sm)
|
||||||
|
self.update_milestone_alert(sm)
|
||||||
|
|
||||||
# Ramp up immediate warning sound over 4s
|
# Ramp up immediate warning sound over 4s
|
||||||
if self.current_alert == AudibleAlert.warningImmediate:
|
if self.current_alert == AudibleAlert.warningImmediate:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
from openpilot.selfdrive.ui.layouts.home import HomeLayout, HomeLayoutState, HEAD_BUTTON_FONT_SIZE, SPACING
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.multilang import tr, trn
|
from openpilot.system.ui.lib.multilang import tr, trn
|
||||||
from openpilot.system.ui.widgets.label import gui_label
|
from openpilot.system.ui.widgets.label import gui_label
|
||||||
@@ -59,7 +59,7 @@ class HomeLayoutSP(HomeLayout):
|
|||||||
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
desc_size = measure_text_cached(gui_app.font(FontWeight.NORMAL), description, BRAND_FONT_SIZE)
|
||||||
desc_width = desc_size.x
|
desc_width = desc_size.x
|
||||||
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
desc_rect = rl.Rectangle(version_right - desc_width, self.header_rect.y, desc_width, self.header_rect.height)
|
||||||
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=TextAlignment.RIGHT)
|
gui_label(desc_rect, description, BRAND_FONT_SIZE, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||||
|
|
||||||
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
brand_size = measure_text_cached(gui_app.font(FontWeight.AUDIOWIDE), brand, BRAND_FONT_SIZE)
|
||||||
spacing = BRAND_DESC_SPACING if description else 0
|
spacing = BRAND_DESC_SPACING if description else 0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.application import FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
@@ -20,7 +20,7 @@ class SunnylinkConsentPage(Widget):
|
|||||||
self._done_callback = done_callback
|
self._done_callback = done_callback
|
||||||
self._step = 0
|
self._step = 0
|
||||||
|
|
||||||
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=TextAlignment.LEFT))
|
self._title = self._child(Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.AUDIOWIDE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||||
|
|
||||||
self._content = [
|
self._content = [
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ class SunnylinkConsentPage(Widget):
|
|||||||
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
|
self._primary_btn = self._child(Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable")))
|
||||||
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
|
self._secondary_btn = self._child(Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary")))
|
||||||
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
|
self._danger_btn = self._child(Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable")))
|
||||||
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT))
|
self._desc = self._child(Label("", font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT))
|
||||||
|
|
||||||
def _handle_choice(self, choice):
|
def _handle_choice(self, choice):
|
||||||
if choice == "enable":
|
if choice == "enable":
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from openpilot.cereal import custom
|
|||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||||
@@ -32,8 +32,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=90,
|
font_size=90,
|
||||||
font_weight=FontWeight.AUDIOWIDE,
|
font_weight=FontWeight.AUDIOWIDE,
|
||||||
text_color=rl.WHITE,
|
text_color=rl.WHITE,
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=False,
|
wrap_text=False,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -43,8 +43,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=40,
|
font_size=40,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.Color(0, 255, 0, 255), # Green
|
text_color=rl.Color(0, 255, 0, 255), # Green
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -55,8 +55,8 @@ class SunnylinkHeader(Widget):
|
|||||||
font_size=35,
|
font_size=35,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.Color(255, 165, 0, 255), # Orange
|
text_color=rl.Color(255, 165, 0, 255), # Orange
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False
|
elide=False
|
||||||
)
|
)
|
||||||
@@ -109,8 +109,8 @@ class SunnylinkDescriptionItem(Widget):
|
|||||||
font_size=40,
|
font_size=40,
|
||||||
font_weight=FontWeight.NORMAL,
|
font_weight=FontWeight.NORMAL,
|
||||||
text_color=rl.WHITE,
|
text_color=rl.WHITE,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.TOP,
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
wrap_text=True,
|
wrap_text=True,
|
||||||
elide=False,
|
elide=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,29 +5,90 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
|||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||||
from openpilot.system.ui.lib.application import FontWeight
|
from openpilot.system.ui.lib.application import FontWeight
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||||
|
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||||
|
|
||||||
|
METERS_PER_MILE = 1609.344
|
||||||
|
METERS_PER_KILOMETER = 1000.0
|
||||||
|
SUMMARY_DURATION_SECONDS = 10.0
|
||||||
|
SUMMARY_WAIT_SECONDS = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def _nonnegative_float(value) -> float:
|
||||||
|
try:
|
||||||
|
return max(0.0, float(value))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
class MiciHomeLayoutSP(MiciHomeLayout):
|
class MiciHomeLayoutSP(MiciHomeLayout):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||||
|
self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40))
|
||||||
|
self._chestnut_loading_icon.set_visible(False)
|
||||||
|
failed_idx = self._status_bar_layout.widgets.index(self._chestnut_failed_icon)
|
||||||
|
self._status_bar_layout.widgets.insert(failed_idx + 1, self._chestnut_loading_icon)
|
||||||
|
initial_summary = ui_state.params.get("LastDriveAssistedDrivingSummary", return_default=True) or {}
|
||||||
|
self._last_summary_id = initial_summary.get("id", 0)
|
||||||
|
self._summary_wait_until = 0.0
|
||||||
|
self._summary_visible_until = 0.0
|
||||||
|
self._drive_summary = {}
|
||||||
|
|
||||||
|
def request_drive_summary(self) -> None:
|
||||||
|
self._summary_wait_until = time.monotonic() + SUMMARY_WAIT_SECONDS
|
||||||
|
|
||||||
|
def _render(self, _: rl.Rectangle) -> None:
|
||||||
|
super()._render(_)
|
||||||
|
now = time.monotonic()
|
||||||
|
if now < self._summary_wait_until:
|
||||||
|
summary = ui_state.params.get("LastDriveAssistedDrivingSummary", return_default=True) or {}
|
||||||
|
summary_id = summary.get("id", 0)
|
||||||
|
if summary_id and summary_id != self._last_summary_id:
|
||||||
|
self._last_summary_id = summary_id
|
||||||
|
distances = summary.get("distancesMeters", {})
|
||||||
|
enabled = ui_state.params.get_bool("AssistedDrivingMilestonesEnabled")
|
||||||
|
if enabled and any(_nonnegative_float(distances.get(category, 0.0)) > 0.0 for category in ("mads", "fullAssist")):
|
||||||
|
self._drive_summary = summary
|
||||||
|
self._summary_visible_until = now + SUMMARY_DURATION_SECONDS
|
||||||
|
self._summary_wait_until = 0.0
|
||||||
|
|
||||||
|
if now < self._summary_visible_until:
|
||||||
|
self._draw_drive_summary(_)
|
||||||
|
|
||||||
|
def _draw_drive_summary(self, rect: rl.Rectangle) -> None:
|
||||||
|
distances = self._drive_summary.get("distancesMeters", {})
|
||||||
|
metric = self._drive_summary.get("unit") == "metric"
|
||||||
|
meters_per_unit = METERS_PER_KILOMETER if metric else METERS_PER_MILE
|
||||||
|
unit = "KM" if metric else "MI"
|
||||||
|
mads = _nonnegative_float(distances.get("mads", 0.0)) / meters_per_unit
|
||||||
|
full_assist = _nonnegative_float(distances.get("fullAssist", 0.0)) / meters_per_unit
|
||||||
|
|
||||||
|
rl.draw_rectangle_rec(rect, rl.Color(0, 0, 0, 235))
|
||||||
|
gui_label(rl.Rectangle(rect.x, rect.y + 14, rect.width, 52), tr("DRIVE COMPLETE"), 42,
|
||||||
|
font_weight=FontWeight.SEMI_BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
gui_label(rl.Rectangle(rect.x + 20, rect.y + 78, rect.width / 2 - 30, 42), tr("MADS"), 28,
|
||||||
|
color=rl.Color(255, 255, 255, 184), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
gui_label(rl.Rectangle(rect.x + rect.width / 2 + 10, rect.y + 78, rect.width / 2 - 30, 42), tr("FULL ASSIST"), 28,
|
||||||
|
color=rl.Color(255, 255, 255, 184), alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
gui_label(rl.Rectangle(rect.x + 20, rect.y + 116, rect.width / 2 - 30, 72), f"{mads:.1f} {unit}", 48,
|
||||||
|
font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
gui_label(rl.Rectangle(rect.x + rect.width / 2 + 10, rect.y + 116, rect.width / 2 - 30, 72), f"{full_assist:.1f} {unit}", 48,
|
||||||
|
font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
def _set_chestnut_visibility(self):
|
def _set_chestnut_visibility(self):
|
||||||
usb_connected = ui_state.usb_connected
|
# stock has no loading tier: it shows green from the moment a big model is available. keep the
|
||||||
usb_unknown = ui_state.usb_unknown
|
# pulse so the status bar and the onroad HUD agree on what loading looks like.
|
||||||
chestnut_state = ui_state.chestnut_state
|
loading = ui_state.chestnut_state == ChestnutState.LOADING
|
||||||
loading = chestnut_state == ChestnutState.LOADING
|
self._chestnut_loading_icon._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||||
|
self._chestnut_loading_icon.set_visible(loading)
|
||||||
self._usb_icon.set_visible(usb_connected and usb_unknown)
|
self._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
||||||
self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)))
|
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||||
self._chestnut_loading_icon.set_visible(not usb_unknown and loading)
|
|
||||||
self._chestnut_icon.set_visible(not usb_unknown and not loading and
|
|
||||||
chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
|
||||||
self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
"""Render assisted-driving milestone celebrations over the on-road view."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pyray as rl
|
||||||
|
|
||||||
|
from openpilot.cereal import custom
|
||||||
|
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import ALERT_BACKGROUND_OPACITY
|
||||||
|
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import FONT_SIZES
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
|
||||||
|
|
||||||
|
CELEBRATION_DURATION = 4.5
|
||||||
|
PARTICLE_COUNT = 150
|
||||||
|
METERS_PER_MILE = 1609.344
|
||||||
|
METERS_PER_KILOMETER = 1000.0
|
||||||
|
|
||||||
|
CONFETTI_COLORS = (
|
||||||
|
rl.Color(255, 55, 95, 255),
|
||||||
|
rl.Color(255, 183, 3, 255),
|
||||||
|
rl.Color(48, 209, 88, 255),
|
||||||
|
rl.Color(36, 179, 255, 255),
|
||||||
|
rl.Color(112, 72, 232, 255),
|
||||||
|
rl.Color(255, 45, 196, 255),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ConfettiParticle:
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
width: float
|
||||||
|
height: float
|
||||||
|
speed: float
|
||||||
|
drift: float
|
||||||
|
angle: float
|
||||||
|
spin: float
|
||||||
|
phase: float
|
||||||
|
color: rl.Color
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CelebrationMilestone:
|
||||||
|
event_id: int
|
||||||
|
full_assist: bool
|
||||||
|
distance_meters: float
|
||||||
|
previous_distance_meters: float
|
||||||
|
metric: bool
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneCelebration(Widget):
|
||||||
|
"""Pure renderer for typed assisted-driving milestone events."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._drive_started_time = -1.0
|
||||||
|
self._celebration_started_time: float | None = None
|
||||||
|
self._current_milestone: CelebrationMilestone | None = None
|
||||||
|
self._pending_milestones: deque[CelebrationMilestone] = deque()
|
||||||
|
self._last_event_id = 0
|
||||||
|
self._particles = self._make_particles()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_particles() -> list[ConfettiParticle]:
|
||||||
|
rng = random.Random(20260828)
|
||||||
|
return [
|
||||||
|
ConfettiParticle(
|
||||||
|
x=rng.random(),
|
||||||
|
y=rng.uniform(-0.25, 0.95),
|
||||||
|
width=rng.uniform(10, 24),
|
||||||
|
height=rng.uniform(24, 58),
|
||||||
|
speed=rng.uniform(0.12, 0.34),
|
||||||
|
drift=rng.uniform(-0.035, 0.035),
|
||||||
|
angle=rng.uniform(0, 360),
|
||||||
|
spin=rng.uniform(-150, 150),
|
||||||
|
phase=rng.uniform(0, math.tau),
|
||||||
|
color=CONFETTI_COLORS[rng.randrange(len(CONFETTI_COLORS))],
|
||||||
|
)
|
||||||
|
for _ in range(PARTICLE_COUNT)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _render(self, rect: rl.Rectangle, /) -> None:
|
||||||
|
now = time.monotonic()
|
||||||
|
if ui_state.started_time != self._drive_started_time:
|
||||||
|
self._drive_started_time = ui_state.started_time
|
||||||
|
self._celebration_started_time = None
|
||||||
|
self._current_milestone = None
|
||||||
|
self._pending_milestones.clear()
|
||||||
|
|
||||||
|
self._consume_event(suppress=False)
|
||||||
|
|
||||||
|
if self._current_milestone is None and self._pending_milestones:
|
||||||
|
self._current_milestone = self._pending_milestones.popleft()
|
||||||
|
self._celebration_started_time = now
|
||||||
|
|
||||||
|
if self._celebration_started_time is None or self._current_milestone is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
elapsed = now - self._celebration_started_time
|
||||||
|
if elapsed >= CELEBRATION_DURATION:
|
||||||
|
self._celebration_started_time = None
|
||||||
|
self._current_milestone = None
|
||||||
|
return
|
||||||
|
|
||||||
|
alpha = min(1.0, elapsed / 0.2, (CELEBRATION_DURATION - elapsed) / 0.8)
|
||||||
|
self._draw_background_scrim(rect, alpha)
|
||||||
|
self._draw_confetti(rect, elapsed, alpha)
|
||||||
|
self._draw_milestone(rect, elapsed, alpha, self._current_milestone)
|
||||||
|
|
||||||
|
def cancel_for_alert(self) -> None:
|
||||||
|
self._consume_event(suppress=True)
|
||||||
|
self._celebration_started_time = None
|
||||||
|
self._current_milestone = None
|
||||||
|
self._pending_milestones.clear()
|
||||||
|
|
||||||
|
def _consume_event(self, suppress: bool) -> None:
|
||||||
|
if not ui_state.sm.updated["assistedDrivingMilestoneState"]:
|
||||||
|
return
|
||||||
|
state = ui_state.sm["assistedDrivingMilestoneState"]
|
||||||
|
event = state.event
|
||||||
|
if not state.enabled:
|
||||||
|
self._celebration_started_time = None
|
||||||
|
self._current_milestone = None
|
||||||
|
self._pending_milestones.clear()
|
||||||
|
return
|
||||||
|
if event.id == 0 or event.id == self._last_event_id:
|
||||||
|
return
|
||||||
|
self._last_event_id = event.id
|
||||||
|
if suppress:
|
||||||
|
return
|
||||||
|
self._pending_milestones.append(CelebrationMilestone(
|
||||||
|
event_id=event.id,
|
||||||
|
full_assist=event.category == custom.AssistedDrivingMilestoneState.Category.fullAssist,
|
||||||
|
distance_meters=event.distanceMeters,
|
||||||
|
previous_distance_meters=event.previousDistanceMeters,
|
||||||
|
metric=event.unit == custom.AssistedDrivingMilestoneState.Unit.metric,
|
||||||
|
))
|
||||||
|
|
||||||
|
def _draw_confetti(self, rect: rl.Rectangle, elapsed: float, alpha: float) -> None:
|
||||||
|
travel_height = rect.height * 1.45
|
||||||
|
compact = rect.height <= 300
|
||||||
|
particle_scale = rect.height / 1080.0
|
||||||
|
particles = self._particles[:100] if compact else self._particles
|
||||||
|
for particle in particles:
|
||||||
|
x = rect.x + rect.width * (particle.x + particle.drift * elapsed + 0.012 * math.sin(elapsed * 3 + particle.phase))
|
||||||
|
y = rect.y - rect.height * 0.2 + (particle.y * travel_height + particle.speed * rect.height * elapsed) % travel_height
|
||||||
|
flip = 0.2 + 0.8 * abs(math.sin(elapsed * 5 + particle.phase))
|
||||||
|
particle_rect = rl.Rectangle(x, y, particle.width * particle_scale * flip, particle.height * particle_scale)
|
||||||
|
origin = rl.Vector2(particle_rect.width / 2, particle_rect.height / 2)
|
||||||
|
color = rl.Color(particle.color.r, particle.color.g, particle.color.b, int(255 * alpha))
|
||||||
|
rl.draw_rectangle_pro(particle_rect, origin, particle.angle + particle.spin * elapsed, color)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _draw_milestone(rect: rl.Rectangle, elapsed: float, alpha: float, milestone: CelebrationMilestone) -> None:
|
||||||
|
# Match the comma four set-speed hierarchy: DISPLAY number with a MAX-sized label.
|
||||||
|
scale = rect.height / 240.0
|
||||||
|
pulse = 1.0 + 0.025 * math.sin(min(elapsed, 0.6) / 0.6 * math.pi)
|
||||||
|
number_size = int(FONT_SIZES.set_speed * scale * pulse)
|
||||||
|
milestone_size = int(FONT_SIZES.max_speed * scale * pulse)
|
||||||
|
category_size = int(22 * scale * pulse)
|
||||||
|
unit_size = category_size
|
||||||
|
|
||||||
|
display_font = gui_app.font(FontWeight.DISPLAY)
|
||||||
|
semibold_font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||||
|
tween_progress = min(elapsed / 0.85, 1.0)
|
||||||
|
tween_progress = 1.0 - (1.0 - tween_progress) ** 3
|
||||||
|
meters_per_unit = METERS_PER_KILOMETER if milestone.metric else METERS_PER_MILE
|
||||||
|
previous_distance = milestone.previous_distance_meters / meters_per_unit
|
||||||
|
milestone_distance = milestone.distance_meters / meters_per_unit
|
||||||
|
displayed_distance = previous_distance + (milestone_distance - previous_distance) * tween_progress
|
||||||
|
if tween_progress >= 1.0:
|
||||||
|
number = f"{round(milestone_distance):,}"
|
||||||
|
else:
|
||||||
|
number = f"{displayed_distance:,.1f}"
|
||||||
|
unit = tr("KM") if milestone.metric else tr("MI")
|
||||||
|
category = tr("FULL ASSIST") if milestone.full_assist else tr("MADS")
|
||||||
|
milestone_label = tr("MILESTONE")
|
||||||
|
|
||||||
|
unit_bounds = measure_text_cached(semibold_font, unit, unit_size)
|
||||||
|
number_bounds = measure_text_cached(display_font, number, number_size)
|
||||||
|
max_number_width = rect.width * 0.72 - unit_bounds.x - 8 * scale
|
||||||
|
if number_bounds.x > max_number_width:
|
||||||
|
number_size = max(1, int(number_size * max_number_width / number_bounds.x))
|
||||||
|
number_bounds = measure_text_cached(display_font, number, number_size)
|
||||||
|
category_bounds = measure_text_cached(semibold_font, category, category_size)
|
||||||
|
milestone_bounds = measure_text_cached(semibold_font, milestone_label, milestone_size)
|
||||||
|
|
||||||
|
center_x = rect.x + rect.width / 2
|
||||||
|
center_y = rect.y + rect.height / 2
|
||||||
|
text_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha))
|
||||||
|
secondary_color = rl.Color(255, 255, 255, int(255 * 0.72 * alpha))
|
||||||
|
number_line_width = number_bounds.x + 8 * scale + unit_bounds.x
|
||||||
|
number_x = center_x - number_line_width / 2
|
||||||
|
number_y = center_y - 76 * scale
|
||||||
|
unit_y = center_y + 14 * scale
|
||||||
|
category_y = center_y - 91 * scale
|
||||||
|
milestone_y = center_y + 50 * scale
|
||||||
|
|
||||||
|
rl.draw_text_ex(semibold_font, category, rl.Vector2(center_x - category_bounds.x / 2, category_y),
|
||||||
|
category_size, 0, secondary_color)
|
||||||
|
rl.draw_text_ex(display_font, number, rl.Vector2(number_x, number_y), number_size, 0, text_color)
|
||||||
|
rl.draw_text_ex(semibold_font, unit, rl.Vector2(number_x + number_bounds.x + 8 * scale, unit_y),
|
||||||
|
unit_size, 0, secondary_color)
|
||||||
|
rl.draw_text_ex(semibold_font, milestone_label, rl.Vector2(center_x - milestone_bounds.x / 2, milestone_y),
|
||||||
|
milestone_size, 0, text_color)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _draw_background_scrim(rect: rl.Rectangle, alpha: float) -> None:
|
||||||
|
# Match the alert background: a mostly opaque black core fading to transparent.
|
||||||
|
fade_height = round(rect.height * 0.25)
|
||||||
|
solid_height = round(rect.height * 0.50)
|
||||||
|
solid_color = rl.Color(0, 0, 0, int(255 * ALERT_BACKGROUND_OPACITY * alpha))
|
||||||
|
transparent = rl.Color(0, 0, 0, 0)
|
||||||
|
x = int(rect.x)
|
||||||
|
y = int(rect.y)
|
||||||
|
width = int(rect.width)
|
||||||
|
|
||||||
|
rl.draw_rectangle_gradient_v(x, y, width, fade_height, transparent, solid_color)
|
||||||
|
rl.draw_rectangle(x, y + fade_height, width, solid_height, solid_color)
|
||||||
|
rl.draw_rectangle_gradient_v(x, y + fade_height + solid_height, width, fade_height, solid_color, transparent)
|
||||||
@@ -35,7 +35,8 @@ class UIStateSP:
|
|||||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||||
self.sm_services_ext = [
|
self.sm_services_ext = [
|
||||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||||
"gpsLocation", "lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay"
|
"gpsLocation", "lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay",
|
||||||
|
"assistedDrivingMilestoneState",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.sunnylink_state = SunnylinkState()
|
self.sunnylink_state = SunnylinkState()
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the assisted-driving milestone celebration chime."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import wave
|
||||||
|
from array import array
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_RATE = 48_000
|
||||||
|
DURATION_SECONDS = 0.82
|
||||||
|
NOTES = (
|
||||||
|
(0.00, 523.25),
|
||||||
|
(0.11, 659.25),
|
||||||
|
(0.22, 783.99),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def note_sample(age: float, frequency: float) -> float:
|
||||||
|
if not 0 <= age <= 0.58:
|
||||||
|
return 0.0
|
||||||
|
attack = min(age / 0.008, 1.0)
|
||||||
|
release = min((0.58 - age) / 0.15, 1.0)
|
||||||
|
envelope = attack * release * math.exp(-3.8 * age)
|
||||||
|
tone = math.sin(math.tau * frequency * age) + 0.16 * math.sin(math.tau * frequency * 2 * age)
|
||||||
|
return envelope * tone
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
output = Path(__file__).parents[4] / "openpilot/selfdrive/assets/sounds/milestone.wav"
|
||||||
|
samples = array('h')
|
||||||
|
for frame in range(round(SAMPLE_RATE * DURATION_SECONDS)):
|
||||||
|
t = frame / SAMPLE_RATE
|
||||||
|
value = 0.38 * sum(note_sample(t - start, frequency) for start, frequency in NOTES)
|
||||||
|
samples.append(round(max(-1.0, min(1.0, value)) * 32767))
|
||||||
|
|
||||||
|
with wave.open(str(output), "wb") as wav:
|
||||||
|
wav.setnchannels(1)
|
||||||
|
wav.setsampwidth(2)
|
||||||
|
wav.setframerate(SAMPLE_RATE)
|
||||||
|
wav.writeframes(samples.tobytes())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish deterministic milestone events for the local comma-four UI preview."""
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import time
|
||||||
|
|
||||||
|
from openpilot.cereal import messaging
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pm = messaging.PubMaster(["assistedDrivingMilestoneState"])
|
||||||
|
milestones = itertools.cycle(((1, 0, "mads"), (2, 1, "fullAssist"), (5, 2, "mads"), (10, 5, "fullAssist")))
|
||||||
|
event_id = 0
|
||||||
|
milestone, previous_milestone, category = 0, 0, "mads"
|
||||||
|
next_event_time = time.monotonic() + 1.0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = time.monotonic()
|
||||||
|
if now >= next_event_time:
|
||||||
|
event_id += 1
|
||||||
|
milestone, previous_milestone, category = next(milestones)
|
||||||
|
next_event_time = now + 6.0
|
||||||
|
|
||||||
|
msg = messaging.new_message("assistedDrivingMilestoneState")
|
||||||
|
state = msg.assistedDrivingMilestoneState
|
||||||
|
state.enabled = True
|
||||||
|
if event_id:
|
||||||
|
state.event.id = event_id
|
||||||
|
state.event.category = category
|
||||||
|
state.event.distanceMeters = milestone * 1609.344
|
||||||
|
state.event.previousDistanceMeters = previous_milestone * 1609.344
|
||||||
|
state.event.unit = "imperial"
|
||||||
|
pm.send("assistedDrivingMilestoneState", msg)
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
replay_pid=""
|
||||||
|
preview_pid=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
for pid in "$preview_pid" "$replay_pid"; do
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
export PATH="$repo_root/.venv/bin:$PATH"
|
||||||
|
export SP_MILESTONE_PREVIEW=1
|
||||||
|
playback="${SP_MILESTONE_PLAYBACK:-1}"
|
||||||
|
|
||||||
|
"$repo_root/openpilot/tools/replay/replay" --demo --playback "$playback" &
|
||||||
|
replay_pid=$!
|
||||||
|
"$repo_root/.venv/bin/python" "$repo_root/openpilot/selfdrive/ui/tests/milestone_preview.py" &
|
||||||
|
preview_pid=$!
|
||||||
|
|
||||||
|
"$repo_root/.venv/bin/python" "$repo_root/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py"
|
||||||
@@ -4,12 +4,63 @@ import time
|
|||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
from openpilot.cereal import log, messaging
|
from openpilot.cereal import log, messaging
|
||||||
from openpilot.cereal.messaging import SubMaster, PubMaster
|
from openpilot.cereal.messaging import SubMaster, PubMaster
|
||||||
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, check_selfdrive_timeout_alert
|
from openpilot.selfdrive.ui.soundd import SELFDRIVE_STATE_TIMEOUT, Soundd, calculate_volume_for_device, check_selfdrive_timeout_alert
|
||||||
|
|
||||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||||
|
|
||||||
|
|
||||||
class TestSoundd(OpenpilotTestCase):
|
class TestSoundd(OpenpilotTestCase):
|
||||||
|
@staticmethod
|
||||||
|
def milestone_submaster(event_id=42):
|
||||||
|
class SubMasterStub:
|
||||||
|
def __init__(self):
|
||||||
|
self.updated = {'assistedDrivingMilestoneState': True}
|
||||||
|
msg = messaging.new_message('assistedDrivingMilestoneState')
|
||||||
|
msg.assistedDrivingMilestoneState.enabled = True
|
||||||
|
msg.assistedDrivingMilestoneState.event.id = event_id
|
||||||
|
self.data = {'assistedDrivingMilestoneState': msg.assistedDrivingMilestoneState}
|
||||||
|
|
||||||
|
def __getitem__(self, service):
|
||||||
|
return self.data[service]
|
||||||
|
|
||||||
|
return SubMasterStub()
|
||||||
|
|
||||||
|
def test_comma_four_volume_is_50_percent_louder_than_comma_three_x(self):
|
||||||
|
for weighted_db in (20.0, 30.0, 40.0, 50.0):
|
||||||
|
with self.subTest(weighted_db=weighted_db):
|
||||||
|
comma_three_x_volume = calculate_volume_for_device(weighted_db, "tizi")
|
||||||
|
comma_four_volume = calculate_volume_for_device(weighted_db, "mici")
|
||||||
|
assert comma_four_volume == min(1.0, comma_three_x_volume * 1.5)
|
||||||
|
|
||||||
|
def test_milestone_chime_uses_typed_milestone_event_once(self):
|
||||||
|
soundd = Soundd()
|
||||||
|
sm = self.milestone_submaster()
|
||||||
|
soundd.update_milestone_alert(sm)
|
||||||
|
|
||||||
|
assert soundd.current_alert == AudibleAlert.complete
|
||||||
|
soundd.current_alert = AudibleAlert.none
|
||||||
|
soundd.update_milestone_alert(sm)
|
||||||
|
assert soundd.current_alert == AudibleAlert.none
|
||||||
|
|
||||||
|
def test_safety_alert_consumes_milestone_without_replaying_it(self):
|
||||||
|
soundd = Soundd()
|
||||||
|
sm = self.milestone_submaster()
|
||||||
|
soundd.current_alert = AudibleAlert.warningImmediate
|
||||||
|
|
||||||
|
soundd.update_milestone_alert(sm)
|
||||||
|
soundd.current_alert = AudibleAlert.none
|
||||||
|
soundd.update_milestone_alert(sm)
|
||||||
|
|
||||||
|
assert soundd.current_alert == AudibleAlert.none
|
||||||
|
|
||||||
|
def test_quiet_mode_consumes_milestone_without_playing_it(self):
|
||||||
|
soundd = Soundd()
|
||||||
|
soundd.enabled = True
|
||||||
|
|
||||||
|
soundd.update_milestone_alert(self.milestone_submaster())
|
||||||
|
|
||||||
|
assert soundd.current_alert == AudibleAlert.none
|
||||||
|
|
||||||
def test_check_selfdrive_timeout_alert(self, mocker):
|
def test_check_selfdrive_timeout_alert(self, mocker):
|
||||||
sm = SubMaster(['selfdriveState', 'selfdriveStateSP'])
|
sm = SubMaster(['selfdriveState', 'selfdriveStateSP'])
|
||||||
pm = PubMaster(['selfdriveState', 'selfdriveStateSP'])
|
pm = PubMaster(['selfdriveState', 'selfdriveStateSP'])
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from openpilot.common.swaglog import cloudlog
|
|||||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.common.hardware import HARDWARE, PC
|
from openpilot.common.hardware import HARDWARE, PC
|
||||||
from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int
|
|
||||||
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
||||||
|
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||||
@@ -96,10 +95,6 @@ class UIState(UIStateSP):
|
|||||||
self.chestnut_compiled: bool = chestnut_compiled()
|
self.chestnut_compiled: bool = chestnut_compiled()
|
||||||
self.chestnut_active: bool | None = None
|
self.chestnut_active: bool | None = None
|
||||||
self.chestnut_loading: bool = False
|
self.chestnut_loading: bool = False
|
||||||
self.usb_connected: bool = False
|
|
||||||
self.usb_connected_ts: float | None = None
|
|
||||||
self.usb_disconnected_ts: float | None = None
|
|
||||||
self.usb_unknown: bool = False
|
|
||||||
self.chestnut_state = ChestnutState.DISCONNECTED
|
self.chestnut_state = ChestnutState.DISCONNECTED
|
||||||
self.started: bool = False
|
self.started: bool = False
|
||||||
self.ignition: bool = False
|
self.ignition: bool = False
|
||||||
@@ -259,23 +254,6 @@ class UIState(UIStateSP):
|
|||||||
self.chestnut_compiled = chestnut_compiled()
|
self.chestnut_compiled = chestnut_compiled()
|
||||||
self.chestnut_active = self.params.get("ChestnutActive")
|
self.chestnut_active = self.params.get("ChestnutActive")
|
||||||
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
||||||
now = time.monotonic()
|
|
||||||
if read_int(TYPEC_CC_ORIENTATION_PATH) != 0:
|
|
||||||
self.usb_disconnected_ts = None
|
|
||||||
if not self.usb_connected:
|
|
||||||
self.usb_connected = True
|
|
||||||
self.usb_connected_ts = now
|
|
||||||
self.usb_unknown = False
|
|
||||||
elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.:
|
|
||||||
self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state())
|
|
||||||
self.usb_connected_ts = None
|
|
||||||
elif self.usb_connected:
|
|
||||||
if self.usb_disconnected_ts is None:
|
|
||||||
self.usb_disconnected_ts = now
|
|
||||||
elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME:
|
|
||||||
self.usb_connected = False
|
|
||||||
self.usb_connected_ts = None
|
|
||||||
self.usb_unknown = False
|
|
||||||
|
|
||||||
UIStateSP.update_params(self)
|
UIStateSP.update_params(self)
|
||||||
|
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ class TestStockEquivalence(OpenpilotTestCase):
|
|||||||
|
|
||||||
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
|
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
|
||||||
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
|
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
|
||||||
stock_queues, stock_npy, _frame_views = make_input_queues(stock_shapes, frame_skip, device='NPY', frame_copy_size=49152)
|
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
|
||||||
|
|
||||||
# sunnypilot split pipeline has tfm/big_tfm as queues (stock has them in npy only)
|
assert set(state.input_queues.keys()) == set(stock_queues.keys())
|
||||||
assert set(stock_queues.keys()) <= set(state.input_queues.keys())
|
|
||||||
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
|
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
|
||||||
|
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
|
||||||
|
|
||||||
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
|
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
|
||||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -249,27 +248,23 @@ class TestStockCompileModeldEquivalence(OpenpilotTestCase):
|
|||||||
input_shapes = {
|
input_shapes = {
|
||||||
'img': (1, 12, 128, 256),
|
'img': (1, 12, 128, 256),
|
||||||
'desire_pulse': (1, 25, 8),
|
'desire_pulse': (1, 25, 8),
|
||||||
'features_buffer': (1, 24, 32, 512),
|
'features_buffer': (1, 24, 512), # when https://github.com/commaai/openpilot/pull/38681 merges, update to 1,24,32,512
|
||||||
'traffic_convention': (1, 2),
|
'traffic_convention': (1, 2),
|
||||||
'action_t': (1, 2),
|
'action_t': (1, 2),
|
||||||
}
|
}
|
||||||
frame_skip = 4
|
frame_skip = 4
|
||||||
|
|
||||||
stock_queues, stock_npy, _frame_views = stock_make_input_queues(input_shapes, frame_skip, device='NPY', frame_copy_size=49152)
|
stock_queues, stock_npy = stock_make_input_queues(input_shapes, frame_skip, device='NPY')
|
||||||
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
|
sunny_queues, sunny_npy = sunny_make_supercombo_input_queues(input_shapes, frame_skip, device='NPY')
|
||||||
# sunnypilot split pipeline has tfm/big_tfm as queues; packed_npy_inputs size differs (different frame packing)
|
assert set(sunny_queues.keys()) == set(stock_queues.keys())
|
||||||
assert set(stock_queues.keys()) <= set(sunny_queues.keys())
|
|
||||||
for key in stock_queues:
|
for key in stock_queues:
|
||||||
if key == 'packed_npy_inputs':
|
|
||||||
continue
|
|
||||||
assert sunny_queues[key].shape == stock_queues[key].shape, \
|
assert sunny_queues[key].shape == stock_queues[key].shape, \
|
||||||
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
|
f"Queue shape mismatch for {key}: sunny {sunny_queues[key].shape} != stock {stock_queues[key].shape}"
|
||||||
assert set(stock_npy.keys()) <= set(sunny_npy.keys())
|
assert set(sunny_npy.keys()) == set(stock_npy.keys())
|
||||||
for key in stock_npy:
|
for key in stock_npy:
|
||||||
assert sunny_npy[key].shape == stock_npy[key].shape, \
|
assert sunny_npy[key].shape == stock_npy[key].shape, \
|
||||||
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
|
f"Numpy array shape mismatch for {key}: sunny {sunny_npy[key].shape} != stock {stock_npy[key].shape}"
|
||||||
|
|
||||||
@unittest.skip("upstream removed make_warp_input_queues — warp merged into run_model")
|
|
||||||
def test_make_warp_queues_stock_equivalence(self):
|
def test_make_warp_queues_stock_equivalence(self):
|
||||||
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
|
from openpilot.selfdrive.modeld.compile_modeld import make_warp_input_queues as stock_make_warp_queues
|
||||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
|
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_warp_queues as sunny_make_warp_queues
|
||||||
|
|||||||
@@ -20,46 +20,18 @@ def get_default_model() -> str:
|
|||||||
|
|
||||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
|
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
|
||||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
|
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
|
||||||
BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash")
|
|
||||||
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
|
SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx")
|
||||||
BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx")
|
|
||||||
|
|
||||||
|
|
||||||
def _read_model_name_fields():
|
|
||||||
with open(DEFAULT_MODEL_NAME_PATH) as f:
|
|
||||||
content = f.read()
|
|
||||||
fields = {}
|
|
||||||
for line in content.splitlines():
|
|
||||||
if "=" in line:
|
|
||||||
key, val = line.split("=", 1)
|
|
||||||
fields[key.strip()] = val.strip().strip('"')
|
|
||||||
return fields
|
|
||||||
|
|
||||||
|
|
||||||
def update_model_hash():
|
def update_model_hash():
|
||||||
fields = _read_model_name_fields()
|
|
||||||
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
||||||
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
|
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
|
||||||
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
|
|
||||||
|
|
||||||
with open(MODEL_HASH_PATH, "w") as f:
|
with open(MODEL_HASH_PATH, "w") as f:
|
||||||
f.write(combined_hash)
|
f.write(combined_hash)
|
||||||
|
|
||||||
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
|
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
|
||||||
|
|
||||||
if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
|
||||||
import subprocess
|
|
||||||
rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())
|
|
||||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True)
|
|
||||||
oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:"))
|
|
||||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
|
||||||
big_combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
|
||||||
|
|
||||||
with open(BIG_MODEL_HASH_PATH, "w") as f:
|
|
||||||
f.write(big_combined_hash)
|
|
||||||
|
|
||||||
print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_ref_for_name(url: str, name: str) -> str:
|
def get_ref_for_name(url: str, name: str) -> str:
|
||||||
response = requests.get(url, timeout=10)
|
response = requests.get(url, timeout=10)
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ 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_v22.json"
|
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json"
|
||||||
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json"
|
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json"
|
||||||
|
|
||||||
MODEL_SOURCES = {
|
MODEL_SOURCES = {
|
||||||
"qcom": (MODEL_URL, ""),
|
"qcom": (MODEL_URL, ""),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from openpilot.common.hardware.hw import Paths
|
|||||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||||
|
|
||||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||||
REQUIRED_JSON_VERSION = 19
|
REQUIRED_JSON_VERSION = 18
|
||||||
|
|
||||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||||
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
DEFAULT_MODEL = "CD210"
|
DEFAULT_MODEL = "CD210"
|
||||||
DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
|
DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391"
|
||||||
DEFAULT_BIG_MODEL = "BMRLNAP Model v4"
|
DEFAULT_BIG_MODEL = "Lebowski"
|
||||||
DEFAULT_BIG_MODEL_REF = "f877d7a0ccc3cce943c76e285214c020cd65c899"
|
DEFAULT_BIG_MODEL_REF = "fa0c6876d3cf070e91e25e5353ceadc68a5b3285"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
2c814f08a2c51323b87839fbf8d2c2a9853a2b5536271b3d67f7b7a2de7f9374
|
|
||||||
@@ -1 +1 @@
|
|||||||
c5be11d2fb1115be953c541f30c50f7c71a00bc4a0e128e19aa11b60689317fc
|
49133798d9cd9cacf47085c7ef8122bfee88cd9c6192a8314c81bfb1b37f5809
|
||||||
@@ -5,47 +5,19 @@ 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 os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from openpilot.sunnypilot import get_file_hash
|
from openpilot.sunnypilot import get_file_hash
|
||||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \
|
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH
|
||||||
BIG_SUPERCOMBO_ONNX_PATH, _read_model_name_fields
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
|
||||||
|
|
||||||
def _get_lfs_oid(path: str) -> str:
|
|
||||||
"""Extract the LFS OID (SHA256 of actual content) from git, works whether the file is smudged or not."""
|
|
||||||
pointer = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
|
|
||||||
for line in pointer.splitlines():
|
|
||||||
if line.startswith("oid sha256:"):
|
|
||||||
return line.split(":", 1)[1]
|
|
||||||
raise ValueError(f"No LFS OID found for {path}")
|
|
||||||
|
|
||||||
|
|
||||||
class TestDefaultModel(OpenpilotTestCase):
|
class TestDefaultModel(OpenpilotTestCase):
|
||||||
def test_compare_onnx_hashes(self):
|
def test_compare_onnx_hashes(self):
|
||||||
fields = _read_model_name_fields()
|
|
||||||
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
|
||||||
fingerprint = f"{supercombo_hash}:{fields.get('DEFAULT_MODEL', '')}:{fields.get('DEFAULT_MODEL_REF', '')}"
|
|
||||||
combined_hash = hashlib.sha256(fingerprint.encode()).hexdigest()
|
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
|
||||||
|
|
||||||
with open(MODEL_HASH_PATH) as f:
|
with open(MODEL_HASH_PATH) as f:
|
||||||
current_hash = f.read().strip()
|
current_hash = f.read().strip()
|
||||||
|
|
||||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
||||||
|
|
||||||
def test_compare_big_onnx_hashes(self):
|
|
||||||
if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH):
|
|
||||||
self.skipTest("big_driving_supercombo.onnx not present")
|
|
||||||
|
|
||||||
fields = _read_model_name_fields()
|
|
||||||
oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()))
|
|
||||||
big_fingerprint = f"{oid}:{fields.get('DEFAULT_BIG_MODEL', '')}:{fields.get('DEFAULT_BIG_MODEL_REF', '')}"
|
|
||||||
combined_hash = hashlib.sha256(big_fingerprint.encode()).hexdigest()
|
|
||||||
|
|
||||||
with open(BIG_MODEL_HASH_PATH) as f:
|
|
||||||
current_hash = f.read().strip()
|
|
||||||
|
|
||||||
assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
|
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
|||||||
def body():
|
def body():
|
||||||
artifact = self.make_artifact(chunked=True)
|
artifact = self.make_artifact(chunked=True)
|
||||||
self._bundle.ref = "test-ref"
|
self._bundle.ref = "test-ref"
|
||||||
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
self._bundle.minimumSelectorVersion = 18
|
||||||
params, store = self._make_params_with_store()
|
params, store = self._make_params_with_store()
|
||||||
self.manager.params = params
|
self.manager.params = params
|
||||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
|
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
|
||||||
@@ -406,7 +406,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
|||||||
def body():
|
def body():
|
||||||
self.make_artifact(chunked=True)
|
self.make_artifact(chunked=True)
|
||||||
self._bundle.ref = "big-ref"
|
self._bundle.ref = "big-ref"
|
||||||
self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
self._bundle.minimumSelectorVersion = 18
|
||||||
params, store = self._make_params_with_store()
|
params, store = self._make_params_with_store()
|
||||||
self.manager.params = params
|
self.manager.params = params
|
||||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
|
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
|
||||||
@@ -469,7 +469,7 @@ def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = Fa
|
|||||||
"environment": "release",
|
"environment": "release",
|
||||||
"runner": "tinygrad",
|
"runner": "tinygrad",
|
||||||
"is_big": is_big,
|
"is_big": is_big,
|
||||||
"minimum_selector_version": str(helpers.REQUIRED_JSON_VERSION),
|
"minimum_selector_version": "18",
|
||||||
"ref": ref,
|
"ref": ref,
|
||||||
"models": [{
|
"models": [{
|
||||||
"type": "supercombo",
|
"type": "supercombo",
|
||||||
@@ -655,7 +655,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
|
|||||||
def _raw_bundle(ref: str, runner: int | None = None) -> dict:
|
def _raw_bundle(ref: str, runner: int | None = None) -> dict:
|
||||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||||
bundle.ref = ref
|
bundle.ref = ref
|
||||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
bundle.minimumSelectorVersion = 18
|
||||||
if runner is not None:
|
if runner is not None:
|
||||||
bundle.runner = runner
|
bundle.runner = runner
|
||||||
return bundle.to_dict()
|
return bundle.to_dict()
|
||||||
@@ -697,7 +697,7 @@ class TestActiveBundleSelection(OpenpilotTestCase):
|
|||||||
def _raw_bundle(ref: str) -> dict:
|
def _raw_bundle(ref: str) -> dict:
|
||||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||||
bundle.ref = ref
|
bundle.ref = ref
|
||||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
bundle.minimumSelectorVersion = 18
|
||||||
return bundle.to_dict()
|
return bundle.to_dict()
|
||||||
|
|
||||||
def _params(self, qcom=None, chestnut=None):
|
def _params(self, qcom=None, chestnut=None):
|
||||||
@@ -744,7 +744,7 @@ class TestEffectiveSource(OpenpilotTestCase):
|
|||||||
def _raw_bundle(ref: str) -> dict:
|
def _raw_bundle(ref: str) -> dict:
|
||||||
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
bundle = custom.ModelManagerSP.ModelBundle.new_message()
|
||||||
bundle.ref = ref
|
bundle.ref = ref
|
||||||
bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION
|
bundle.minimumSelectorVersion = 18
|
||||||
return bundle.to_dict()
|
return bundle.to_dict()
|
||||||
|
|
||||||
def test_runtime_no_gpu(self):
|
def test_runtime_no_gpu(self):
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ class ControlsExt(ModelStateBase):
|
|||||||
CC_SP.intelligentCruiseButtonManagement.sendButton = icbm_src.sendButton
|
CC_SP.intelligentCruiseButtonManagement.sendButton = icbm_src.sendButton
|
||||||
CC_SP.intelligentCruiseButtonManagement.vTarget = icbm_src.vTarget
|
CC_SP.intelligentCruiseButtonManagement.vTarget = icbm_src.vTarget
|
||||||
|
|
||||||
|
ford_path = getattr(self, 'ford_path', None)
|
||||||
|
if ford_path is not None:
|
||||||
|
CC_SP.fordLateralPath.valid = ford_path.valid
|
||||||
|
CC_SP.fordLateralPath.pathOffset = ford_path.path_offset
|
||||||
|
CC_SP.fordLateralPath.pathAngle = ford_path.path_angle
|
||||||
|
CC_SP.fordLateralPath.curvature = ford_path.curvature
|
||||||
|
CC_SP.fordLateralPath.curvatureRate = ford_path.curvature_rate
|
||||||
|
|
||||||
return CC_SP
|
return CC_SP
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
"""Authoritative assisted-driving distance and milestone tracking."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
|
||||||
|
|
||||||
|
METERS_PER_MILE = 1609.344
|
||||||
|
METERS_PER_KILOMETER = 1000.0
|
||||||
|
MAX_SAMPLE_INTERVAL_SECONDS = 0.5
|
||||||
|
PERSIST_INTERVAL_NS = 10_000_000_000
|
||||||
|
STATE_VERSION = 1
|
||||||
|
STATE_PARAM = "AssistedDrivingMilestoneState"
|
||||||
|
LAST_DRIVE_SUMMARY_PARAM = "LastDriveAssistedDrivingSummary"
|
||||||
|
|
||||||
|
|
||||||
|
class AssistCategory(StrEnum):
|
||||||
|
MADS = "mads"
|
||||||
|
FULL_ASSIST = "fullAssist"
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneUnit(StrEnum):
|
||||||
|
IMPERIAL = "imperial"
|
||||||
|
METRIC = "metric"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MilestoneEvent:
|
||||||
|
event_id: int
|
||||||
|
category: AssistCategory
|
||||||
|
distance_meters: float
|
||||||
|
previous_distance_meters: float
|
||||||
|
unit: MilestoneUnit
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MilestoneSnapshot:
|
||||||
|
distances_meters: dict[AssistCategory, float]
|
||||||
|
drive_start_distances_meters: dict[AssistCategory, float]
|
||||||
|
next_event_id: int
|
||||||
|
next_summary_id: int
|
||||||
|
unit: MilestoneUnit
|
||||||
|
active_drive_id: str
|
||||||
|
|
||||||
|
|
||||||
|
def assist_category(lat_active: bool, long_active: bool) -> AssistCategory | None:
|
||||||
|
if not lat_active:
|
||||||
|
return None
|
||||||
|
return AssistCategory.FULL_ASSIST if long_active else AssistCategory.MADS
|
||||||
|
|
||||||
|
|
||||||
|
def _meters_per_unit(unit: MilestoneUnit) -> float:
|
||||||
|
return METERS_PER_KILOMETER if unit == MilestoneUnit.METRIC else METERS_PER_MILE
|
||||||
|
|
||||||
|
|
||||||
|
def _next_ladder_value(value: float) -> float:
|
||||||
|
value = max(0.0, value)
|
||||||
|
magnitude = 10.0 ** math.floor(math.log10(max(1.0, value)))
|
||||||
|
for multiplier in (1.0, 2.0, 5.0):
|
||||||
|
candidate = multiplier * magnitude
|
||||||
|
if candidate > value + 1e-9:
|
||||||
|
return candidate
|
||||||
|
return 10.0 * magnitude
|
||||||
|
|
||||||
|
|
||||||
|
def _previous_ladder_value(value: float) -> float:
|
||||||
|
if value <= 1.0:
|
||||||
|
return 0.0
|
||||||
|
magnitude = 10.0 ** math.floor(math.log10(value))
|
||||||
|
normalized = value / magnitude
|
||||||
|
if normalized <= 1.0 + 1e-9:
|
||||||
|
return 5.0 * magnitude / 10.0
|
||||||
|
if normalized <= 2.0 + 1e-9:
|
||||||
|
return magnitude
|
||||||
|
return 2.0 * magnitude
|
||||||
|
|
||||||
|
|
||||||
|
def next_milestone_meters(distance_meters: float, unit: MilestoneUnit) -> float:
|
||||||
|
meters_per_unit = _meters_per_unit(unit)
|
||||||
|
return _next_ladder_value(distance_meters / meters_per_unit) * meters_per_unit
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneStore:
|
||||||
|
def __init__(self, params: Params | None = None):
|
||||||
|
self._params = params or Params()
|
||||||
|
|
||||||
|
def load(self) -> MilestoneSnapshot:
|
||||||
|
raw = self._params.get(STATE_PARAM, return_default=True)
|
||||||
|
raw = raw if isinstance(raw, dict) else {}
|
||||||
|
raw_distances = raw.get("distancesMeters", {})
|
||||||
|
raw_distances = raw_distances if isinstance(raw_distances, dict) else {}
|
||||||
|
try:
|
||||||
|
unit = MilestoneUnit(raw.get("unit", MilestoneUnit.IMPERIAL))
|
||||||
|
except ValueError:
|
||||||
|
unit = MilestoneUnit.IMPERIAL
|
||||||
|
|
||||||
|
def distance(category: AssistCategory) -> float:
|
||||||
|
try:
|
||||||
|
return max(0.0, float(raw_distances.get(category.value, 0.0)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
distances = {category: distance(category) for category in AssistCategory}
|
||||||
|
raw_drive_start = raw.get("driveStartDistancesMeters", {})
|
||||||
|
raw_drive_start = raw_drive_start if isinstance(raw_drive_start, dict) else {}
|
||||||
|
|
||||||
|
def drive_start_distance(category: AssistCategory) -> float:
|
||||||
|
try:
|
||||||
|
return max(0.0, min(float(raw_drive_start.get(category.value, distances[category])), distances[category]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return distances[category]
|
||||||
|
|
||||||
|
try:
|
||||||
|
next_event_id = max(1, int(raw.get("nextEventId", 1)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
next_event_id = 1
|
||||||
|
try:
|
||||||
|
next_summary_id = max(1, int(raw.get("nextSummaryId", 1)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
next_summary_id = 1
|
||||||
|
|
||||||
|
return MilestoneSnapshot(
|
||||||
|
distances_meters=distances,
|
||||||
|
drive_start_distances_meters={category: drive_start_distance(category) for category in AssistCategory},
|
||||||
|
next_event_id=next_event_id,
|
||||||
|
next_summary_id=next_summary_id,
|
||||||
|
unit=unit,
|
||||||
|
active_drive_id=str(raw.get("activeDriveId", "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, snapshot: MilestoneSnapshot, block: bool = False) -> None:
|
||||||
|
if block:
|
||||||
|
self._params.flush()
|
||||||
|
self._params.put(STATE_PARAM, {
|
||||||
|
"version": STATE_VERSION,
|
||||||
|
"distancesMeters": {category.value: max(0.0, snapshot.distances_meters.get(category, 0.0)) for category in AssistCategory},
|
||||||
|
"driveStartDistancesMeters": {
|
||||||
|
category.value: max(0.0, snapshot.drive_start_distances_meters.get(category, 0.0)) for category in AssistCategory
|
||||||
|
},
|
||||||
|
"nextEventId": max(1, snapshot.next_event_id),
|
||||||
|
"nextSummaryId": max(1, snapshot.next_summary_id),
|
||||||
|
"unit": snapshot.unit.value,
|
||||||
|
"activeDriveId": snapshot.active_drive_id,
|
||||||
|
}, block=block)
|
||||||
|
|
||||||
|
def save_drive_summary(self, summary_id: int, distances_meters: Mapping[AssistCategory, float], unit: MilestoneUnit) -> None:
|
||||||
|
self._params.put(LAST_DRIVE_SUMMARY_PARAM, {
|
||||||
|
"version": STATE_VERSION,
|
||||||
|
"id": summary_id,
|
||||||
|
"distancesMeters": {category.value: max(0.0, distances_meters.get(category, 0.0)) for category in AssistCategory},
|
||||||
|
"unit": unit.value,
|
||||||
|
}, block=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AssistedDrivingMilestones:
|
||||||
|
"""Tracks, persists, and emits milestones through one small interface."""
|
||||||
|
|
||||||
|
def __init__(self, store: MilestoneStore | None = None):
|
||||||
|
self._store = store or MilestoneStore()
|
||||||
|
snapshot = self._store.load()
|
||||||
|
self._distances_meters = snapshot.distances_meters
|
||||||
|
self._drive_start_distances_meters = snapshot.drive_start_distances_meters
|
||||||
|
self._next_event_id = snapshot.next_event_id
|
||||||
|
self._next_summary_id = snapshot.next_summary_id
|
||||||
|
self._unit = snapshot.unit
|
||||||
|
self._active_drive_id = snapshot.active_drive_id
|
||||||
|
self._next_milestone_meters = {
|
||||||
|
category: next_milestone_meters(distance, self._unit)
|
||||||
|
for category, distance in self._distances_meters.items()
|
||||||
|
}
|
||||||
|
self._last_timestamp_ns: int | None = None
|
||||||
|
self._last_persist_timestamp_ns: int | None = None
|
||||||
|
self._last_speed_mps = 0.0
|
||||||
|
self._last_category: AssistCategory | None = None
|
||||||
|
self._enabled = False
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def snapshot(self) -> MilestoneSnapshot:
|
||||||
|
return MilestoneSnapshot(
|
||||||
|
self._distances_meters.copy(),
|
||||||
|
self._drive_start_distances_meters.copy(),
|
||||||
|
self._next_event_id,
|
||||||
|
self._next_summary_id,
|
||||||
|
self._unit,
|
||||||
|
self._active_drive_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_drive_id(self, drive_id: str) -> None:
|
||||||
|
if not drive_id or drive_id == self._active_drive_id:
|
||||||
|
return
|
||||||
|
self._active_drive_id = drive_id
|
||||||
|
self._drive_start_distances_meters = self._distances_meters.copy()
|
||||||
|
self._persist()
|
||||||
|
|
||||||
|
def update(self, timestamp_ns: int, speed_mps: float, *, lat_active: bool, long_active: bool,
|
||||||
|
is_metric: bool, enabled: bool) -> MilestoneEvent | None:
|
||||||
|
self._enabled = enabled
|
||||||
|
unit = MilestoneUnit.METRIC if is_metric else MilestoneUnit.IMPERIAL
|
||||||
|
if unit != self._unit:
|
||||||
|
self._unit = unit
|
||||||
|
self._next_milestone_meters = {
|
||||||
|
category: next_milestone_meters(distance, unit)
|
||||||
|
for category, distance in self._distances_meters.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
speed_mps = max(0.0, speed_mps)
|
||||||
|
category = assist_category(lat_active, long_active) if enabled else None
|
||||||
|
event = None
|
||||||
|
|
||||||
|
if self._last_timestamp_ns is not None and timestamp_ns != self._last_timestamp_ns:
|
||||||
|
dt = (timestamp_ns - self._last_timestamp_ns) / 1e9
|
||||||
|
if 0 < dt <= MAX_SAMPLE_INTERVAL_SECONDS and self._last_category is not None:
|
||||||
|
active_category = self._last_category
|
||||||
|
self._distances_meters[active_category] += (self._last_speed_mps + speed_mps) / 2.0 * dt
|
||||||
|
threshold_meters = self._next_milestone_meters[active_category]
|
||||||
|
if self._distances_meters[active_category] >= threshold_meters:
|
||||||
|
meters_per_unit = _meters_per_unit(self._unit)
|
||||||
|
threshold_units = threshold_meters / meters_per_unit
|
||||||
|
event = MilestoneEvent(
|
||||||
|
event_id=self._next_event_id,
|
||||||
|
category=active_category,
|
||||||
|
distance_meters=threshold_meters,
|
||||||
|
previous_distance_meters=_previous_ladder_value(threshold_units) * meters_per_unit,
|
||||||
|
unit=self._unit,
|
||||||
|
)
|
||||||
|
self._next_event_id += 1
|
||||||
|
self._next_milestone_meters[active_category] = next_milestone_meters(threshold_meters, self._unit)
|
||||||
|
self._persist(timestamp_ns=timestamp_ns)
|
||||||
|
|
||||||
|
self._last_timestamp_ns = timestamp_ns
|
||||||
|
self._last_speed_mps = speed_mps
|
||||||
|
self._last_category = category
|
||||||
|
|
||||||
|
if self._last_persist_timestamp_ns is None:
|
||||||
|
self._last_persist_timestamp_ns = timestamp_ns
|
||||||
|
elif timestamp_ns - self._last_persist_timestamp_ns >= PERSIST_INTERVAL_NS:
|
||||||
|
self._persist(timestamp_ns=timestamp_ns)
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._closed = True
|
||||||
|
drive_distances = {
|
||||||
|
category: self._distances_meters[category] - self._drive_start_distances_meters[category]
|
||||||
|
for category in AssistCategory
|
||||||
|
}
|
||||||
|
summary_id = self._next_summary_id
|
||||||
|
self._next_summary_id += 1
|
||||||
|
self._persist(block=True)
|
||||||
|
if self._enabled:
|
||||||
|
self._store.save_drive_summary(summary_id, drive_distances, self._unit)
|
||||||
|
|
||||||
|
def _persist(self, block: bool = False, timestamp_ns: int | None = None) -> None:
|
||||||
|
self._store.save(self.snapshot(), block=block)
|
||||||
|
self._last_persist_timestamp_ns = self._last_timestamp_ns if timestamp_ns is None else timestamp_ns
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from openpilot.sunnypilot.selfdrive.selfdrived.assisted_driving_milestones import (
|
||||||
|
METERS_PER_MILE,
|
||||||
|
AssistCategory,
|
||||||
|
AssistedDrivingMilestones,
|
||||||
|
MilestoneStore,
|
||||||
|
MilestoneUnit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParamsStub:
|
||||||
|
def __init__(self, state=None):
|
||||||
|
self.values = {"AssistedDrivingMilestoneState": state or {}}
|
||||||
|
self.writes = []
|
||||||
|
|
||||||
|
def get(self, key, return_default=False):
|
||||||
|
return self.values.get(key, {} if return_default else None)
|
||||||
|
|
||||||
|
def put(self, key, value, block=False):
|
||||||
|
self.values[key] = value
|
||||||
|
self.writes.append((key, value, block))
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssistedDrivingMilestones(unittest.TestCase):
|
||||||
|
def test_emits_and_asynchronously_persists_first_imperial_milestone(self):
|
||||||
|
params = ParamsStub({
|
||||||
|
"version": 1,
|
||||||
|
"distancesMeters": {"mads": METERS_PER_MILE - 5.0, "fullAssist": 0.0},
|
||||||
|
"nextEventId": 7,
|
||||||
|
"unit": "imperial",
|
||||||
|
})
|
||||||
|
milestones = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.assertIsNone(milestones.update(0, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True))
|
||||||
|
event = milestones.update(500_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
assert event is not None
|
||||||
|
self.assertEqual(event.event_id, 7)
|
||||||
|
self.assertEqual(event.category, AssistCategory.MADS)
|
||||||
|
self.assertEqual(event.unit, MilestoneUnit.IMPERIAL)
|
||||||
|
self.assertAlmostEqual(event.distance_meters, METERS_PER_MILE)
|
||||||
|
self.assertFalse(params.writes[-1][2])
|
||||||
|
|
||||||
|
def test_switching_units_schedules_only_a_future_milestone(self):
|
||||||
|
params = ParamsStub({
|
||||||
|
"version": 1,
|
||||||
|
"distancesMeters": {"mads": 9_500.0, "fullAssist": 0.0},
|
||||||
|
"nextEventId": 2,
|
||||||
|
"unit": "imperial",
|
||||||
|
})
|
||||||
|
milestones = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.assertIsNone(milestones.update(0, 1_000.0, lat_active=True, long_active=False, is_metric=True, enabled=True))
|
||||||
|
event = milestones.update(500_000_000, 1_000.0, lat_active=True, long_active=False, is_metric=True, enabled=True)
|
||||||
|
|
||||||
|
self.assertIsNotNone(event)
|
||||||
|
assert event is not None
|
||||||
|
self.assertEqual(event.unit, MilestoneUnit.METRIC)
|
||||||
|
self.assertAlmostEqual(event.distance_meters, 10_000.0)
|
||||||
|
|
||||||
|
def test_ignores_disabled_reverse_and_timestamp_gaps(self):
|
||||||
|
params = ParamsStub()
|
||||||
|
milestones = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
milestones.update(0, 20.0, lat_active=True, long_active=False, is_metric=False, enabled=False)
|
||||||
|
milestones.update(500_000_000, 20.0, lat_active=True, long_active=False, is_metric=False, enabled=False)
|
||||||
|
milestones.update(1_000_000_000, -20.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
milestones.update(2_000_000_000, 20.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
|
||||||
|
self.assertEqual(milestones.snapshot().distances_meters[AssistCategory.MADS], 0.0)
|
||||||
|
|
||||||
|
def test_close_persists_totals_and_last_drive_summary(self):
|
||||||
|
params = ParamsStub()
|
||||||
|
milestones = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
milestones.update(0, 10.0, lat_active=True, long_active=True, is_metric=False, enabled=True)
|
||||||
|
milestones.update(500_000_000, 10.0, lat_active=True, long_active=True, is_metric=False, enabled=True)
|
||||||
|
|
||||||
|
milestones.close()
|
||||||
|
|
||||||
|
summary = params.values["LastDriveAssistedDrivingSummary"]
|
||||||
|
self.assertAlmostEqual(summary["distancesMeters"]["fullAssist"], 5.0)
|
||||||
|
self.assertTrue(params.writes[-1][2])
|
||||||
|
|
||||||
|
write_count = len(params.writes)
|
||||||
|
milestones.close()
|
||||||
|
self.assertEqual(len(params.writes), write_count)
|
||||||
|
|
||||||
|
def test_process_restart_preserves_the_current_drive_start(self):
|
||||||
|
params = ParamsStub()
|
||||||
|
first_process = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
first_process.set_drive_id("route-1")
|
||||||
|
first_process.update(0, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
first_process.update(500_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
first_process.close()
|
||||||
|
|
||||||
|
second_process = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
second_process.set_drive_id("route-1")
|
||||||
|
second_process.update(1_000_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
second_process.update(1_500_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
second_process.close()
|
||||||
|
|
||||||
|
summary = params.values["LastDriveAssistedDrivingSummary"]
|
||||||
|
self.assertAlmostEqual(summary["distancesMeters"]["mads"], 10.0)
|
||||||
|
|
||||||
|
def test_disabled_feature_does_not_publish_drive_summary(self):
|
||||||
|
params = ParamsStub()
|
||||||
|
milestones = AssistedDrivingMilestones(MilestoneStore(params)) # type: ignore[arg-type]
|
||||||
|
milestones.update(0, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
milestones.update(500_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=True)
|
||||||
|
milestones.update(1_000_000_000, 10.0, lat_active=True, long_active=False, is_metric=False, enabled=False)
|
||||||
|
|
||||||
|
milestones.close()
|
||||||
|
|
||||||
|
self.assertNotIn("LastDriveAssistedDrivingSummary", params.values)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1383,6 +1383,12 @@
|
|||||||
"title": "Steering Arc",
|
"title": "Steering Arc",
|
||||||
"description": "Display steering arc on the driving screen when lateral control is enabled."
|
"description": "Display steering arc on the driving screen when lateral control is enabled."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "AssistedDrivingMilestonesEnabled",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Assisted Driving Milestones",
|
||||||
|
"description": "Celebrate cumulative MADS and full-assist distance milestones while driving."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "ShowTurnSignals",
|
"key": "ShowTurnSignals",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -2168,6 +2174,25 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"vehicle_settings": {
|
"vehicle_settings": {
|
||||||
|
"ford": {
|
||||||
|
"title": "Ford Settings",
|
||||||
|
"description": "",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "FordPscmObserver",
|
||||||
|
"widget": "toggle",
|
||||||
|
"needs_onroad_cycle": true,
|
||||||
|
"title": "PSCM Coefficient Observer (Experimental)",
|
||||||
|
"description": "Track the Ford steering controller's internal polynomial states and use fast path terms only for the response that slow curvature cannot provide.",
|
||||||
|
"details": "This changes live steering behavior on Ford CAN FD vehicles. Use only for supervised testing and be ready to take over immediately.",
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "offroad_only"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"hyundai": {
|
"hyundai": {
|
||||||
"title": "Hyundai / Kia / Genesis Settings",
|
"title": "Hyundai / Kia / Genesis Settings",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ icon: vehicle
|
|||||||
order: 99
|
order: 99
|
||||||
kind: vehicle
|
kind: vehicle
|
||||||
sections:
|
sections:
|
||||||
|
- id: ford
|
||||||
|
title: Ford Settings
|
||||||
|
description: ''
|
||||||
|
items:
|
||||||
|
- key: FordPscmObserver
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: PSCM Coefficient Observer (Experimental)
|
||||||
|
description: Track the Ford steering controller's internal polynomial states and use fast path terms only for the response that slow curvature cannot provide.
|
||||||
|
details: This changes live steering behavior on Ford CAN FD vehicles. Use only for supervised testing and be ready to take over immediately.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/offroad'
|
||||||
- id: hyundai
|
- id: hyundai
|
||||||
title: Hyundai / Kia / Genesis Settings
|
title: Hyundai / Kia / Genesis Settings
|
||||||
description: ''
|
description: ''
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ sections:
|
|||||||
widget: toggle
|
widget: toggle
|
||||||
title: Steering Arc
|
title: Steering Arc
|
||||||
description: Display steering arc on the driving screen when lateral control is enabled.
|
description: Display steering arc on the driving screen when lateral control is enabled.
|
||||||
|
- key: AssistedDrivingMilestonesEnabled
|
||||||
|
widget: toggle
|
||||||
|
title: Assisted Driving Milestones
|
||||||
|
description: Celebrate cumulative MADS and full-assist distance milestones while driving.
|
||||||
- key: ShowTurnSignals
|
- key: ShowTurnSignals
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Display Turn Signals
|
title: Display Turn Signals
|
||||||
|
|||||||
@@ -278,6 +278,12 @@ class TestKnownPanels(OpenpilotTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestKnownVehicleSettings(OpenpilotTestCase):
|
class TestKnownVehicleSettings(OpenpilotTestCase):
|
||||||
|
def test_ford_has_pscm_observer(self, schema):
|
||||||
|
items = _brand_items(schema["vehicle_settings"].get("ford"))
|
||||||
|
observer = next(item for item in items if item["key"] == "FordPscmObserver")
|
||||||
|
assert observer["needs_onroad_cycle"] is True
|
||||||
|
assert observer["enablement"] == [{"type": "offroad_only"}]
|
||||||
|
|
||||||
def test_hyundai_has_longitudinal_tuning(self, schema):
|
def test_hyundai_has_longitudinal_tuning(self, schema):
|
||||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))}
|
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("hyundai"))}
|
||||||
assert "HyundaiLongitudinalTuning" in keys
|
assert "HyundaiLongitudinalTuning" in keys
|
||||||
|
|||||||
@@ -103,6 +103,32 @@ def _migrate_model_bundle_slots(_params):
|
|||||||
cloudlog.exception(f"Error migrating model bundle slots: {e}")
|
cloudlog.exception(f"Error migrating model bundle slots: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_assisted_driving_milestones(_params):
|
||||||
|
try:
|
||||||
|
state = _params.get("AssistedDrivingMilestoneState", return_default=True)
|
||||||
|
if isinstance(state, dict) and state.get("version") == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
_params.put("AssistedDrivingMilestoneState", {
|
||||||
|
"version": 1,
|
||||||
|
"distancesMeters": {
|
||||||
|
"mads": max(0.0, _params.get("MadsDrivenDistanceMeters", return_default=True) or 0.0),
|
||||||
|
"fullAssist": max(0.0, _params.get("FullAssistDrivenDistanceMeters", return_default=True) or 0.0),
|
||||||
|
},
|
||||||
|
"driveStartDistancesMeters": {
|
||||||
|
"mads": max(0.0, _params.get("MadsDrivenDistanceMeters", return_default=True) or 0.0),
|
||||||
|
"fullAssist": max(0.0, _params.get("FullAssistDrivenDistanceMeters", return_default=True) or 0.0),
|
||||||
|
},
|
||||||
|
"nextEventId": 1,
|
||||||
|
"nextSummaryId": 1,
|
||||||
|
"unit": "metric" if _params.get_bool("IsMetric") else "imperial",
|
||||||
|
"activeDriveId": "",
|
||||||
|
}, block=True)
|
||||||
|
cloudlog.info("params_migration: migrated assisted-driving milestone state")
|
||||||
|
except Exception as e:
|
||||||
|
cloudlog.exception(f"Error migrating assisted-driving milestone state: {e}")
|
||||||
|
|
||||||
|
|
||||||
def run_migration(_params):
|
def run_migration(_params):
|
||||||
# migrate OnroadScreenOffBrightness
|
# migrate OnroadScreenOffBrightness
|
||||||
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
|
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
|
||||||
@@ -142,3 +168,5 @@ def run_migration(_params):
|
|||||||
|
|
||||||
# seed the chestnut model slot from the pre-split single slot
|
# seed the chestnut model slot from the pre-split single slot
|
||||||
_migrate_model_bundle_slots(_params)
|
_migrate_model_bundle_slots(_params)
|
||||||
|
|
||||||
|
_migrate_assisted_driving_milestones(_params)
|
||||||
|
|||||||
@@ -7,7 +7,44 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
|
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
from openpilot.sunnypilot.system.params_migration import _migrate_model_bundle_slots
|
from openpilot.sunnypilot.system.params_migration import _migrate_model_bundle_slots, run_migration
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssistedDrivingMilestoneMigration(OpenpilotTestCase):
|
||||||
|
def test_preserves_prototype_distances_once(self):
|
||||||
|
class ParamsStub:
|
||||||
|
def __init__(self):
|
||||||
|
self.values = {
|
||||||
|
"MadsDrivenDistanceMeters": 123.0,
|
||||||
|
"FullAssistDrivenDistanceMeters": 456.0,
|
||||||
|
"OnroadScreenOffBrightness": 0,
|
||||||
|
"OnroadScreenOffTimer": 15,
|
||||||
|
"AssistedDrivingMilestoneState": {},
|
||||||
|
"IsMetric": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, key, return_default=False):
|
||||||
|
return self.values.get(key)
|
||||||
|
|
||||||
|
def put(self, key, value, block=False):
|
||||||
|
self.values[key] = value
|
||||||
|
|
||||||
|
def get_bool(self, key):
|
||||||
|
return bool(self.values.get(key, False))
|
||||||
|
|
||||||
|
params = ParamsStub()
|
||||||
|
|
||||||
|
run_migration(params)
|
||||||
|
|
||||||
|
state = params.get("AssistedDrivingMilestoneState")
|
||||||
|
assert state["distancesMeters"] == {"mads": 123.0, "fullAssist": 456.0}
|
||||||
|
|
||||||
|
params.put("MadsDrivenDistanceMeters", 12.0, block=True)
|
||||||
|
params.put("FullAssistDrivenDistanceMeters", 34.0, block=True)
|
||||||
|
run_migration(params)
|
||||||
|
|
||||||
|
state = params.get("AssistedDrivingMilestoneState")
|
||||||
|
assert state["distancesMeters"] == {"mads": 123.0, "fullAssist": 456.0}
|
||||||
|
|
||||||
|
|
||||||
class TestModelBundleSlotMigration(OpenpilotTestCase):
|
class TestModelBundleSlotMigration(OpenpilotTestCase):
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id
|
|
||||||
from openpilot.common.version import get_build_metadata, CHESTNUT_BRANCHES
|
|
||||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
|
|
||||||
|
|
||||||
|
|
||||||
CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging")
|
|
||||||
CHESTNUT_POWERED_VOLTAGE = 5000
|
|
||||||
GPU_TEMP_LIMIT = 100.
|
|
||||||
MEMORY_TEMP_LIMIT = 95.
|
|
||||||
TEMP_HYSTERESIS = 5.
|
|
||||||
|
|
||||||
|
|
||||||
class ChestnutStatus:
|
|
||||||
def __init__(self):
|
|
||||||
self.started = time.monotonic()
|
|
||||||
self.offroad = True
|
|
||||||
self.pcie_failed = False
|
|
||||||
self.power_seen = False
|
|
||||||
self.power_unavailable = False
|
|
||||||
self.power_lost = False
|
|
||||||
self.power_restored = False
|
|
||||||
self.link_failures = 0
|
|
||||||
self.model_loading_seen = False
|
|
||||||
self.model_attempted = False
|
|
||||||
self.overheated = False
|
|
||||||
self.usb_seen = False
|
|
||||||
self.usb_failed = False
|
|
||||||
|
|
||||||
def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool,
|
|
||||||
model_loading: bool, model_active: bool | None, state, set_alert) -> None:
|
|
||||||
detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)]
|
|
||||||
devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])]
|
|
||||||
firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT
|
|
||||||
|
|
||||||
if self.offroad and not offroad:
|
|
||||||
self.pcie_failed = False
|
|
||||||
self.power_seen = False
|
|
||||||
self.power_unavailable = False
|
|
||||||
self.power_lost = False
|
|
||||||
self.power_restored = False
|
|
||||||
self.link_failures = 0
|
|
||||||
self.model_loading_seen = False
|
|
||||||
self.model_attempted = False
|
|
||||||
self.usb_seen = firmware_ok
|
|
||||||
self.usb_failed = False
|
|
||||||
|
|
||||||
self.model_loading_seen |= model_loading
|
|
||||||
self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None
|
|
||||||
|
|
||||||
if not offroad and self.usb_seen and not firmware_ok:
|
|
||||||
self.usb_failed = True
|
|
||||||
|
|
||||||
if not offroad and state is not None:
|
|
||||||
powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
|
|
||||||
power_lost = state.supplyFault or not powered
|
|
||||||
if self.model_attempted and power_lost and not self.power_lost:
|
|
||||||
self.power_unavailable = not self.power_seen
|
|
||||||
self.power_seen |= powered
|
|
||||||
|
|
||||||
if not offroad and self.model_attempted and state is not None:
|
|
||||||
self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0
|
|
||||||
self.pcie_failed |= self.link_failures >= 2 or power_lost
|
|
||||||
self.power_lost |= power_lost
|
|
||||||
|
|
||||||
if self.pcie_failed and self.power_lost and state is not None:
|
|
||||||
self.power_restored |= not state.supplyFault and state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE
|
|
||||||
if self.usb_failed:
|
|
||||||
self.pcie_failed = False
|
|
||||||
self.power_seen = False
|
|
||||||
self.power_unavailable = False
|
|
||||||
self.power_lost = False
|
|
||||||
self.power_restored = False
|
|
||||||
|
|
||||||
if state is not None:
|
|
||||||
gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
|
|
||||||
memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.)
|
|
||||||
self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit
|
|
||||||
|
|
||||||
release = branch in CHESTNUT_RELEASE_BRANCHES
|
|
||||||
missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1)
|
|
||||||
slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000
|
|
||||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
|
|
||||||
current_channel = get_build_metadata().channel
|
|
||||||
chestnut_target = CHESTNUT_BRANCHES.get(current_channel)
|
|
||||||
chestnut_needs_switch = len(devices) == 1 and not big_model_available and chestnut_target is not None
|
|
||||||
set_alert("Offroad_ChestnutBranch", chestnut_needs_switch, chestnut_target if chestnut_needs_switch else None)
|
|
||||||
set_alert("Offroad_ChestnutNotDetected", missing)
|
|
||||||
set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None)
|
|
||||||
set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None)
|
|
||||||
if self.power_lost:
|
|
||||||
pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else
|
|
||||||
"Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else
|
|
||||||
"Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.")
|
|
||||||
else:
|
|
||||||
pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated."
|
|
||||||
set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert)
|
|
||||||
set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled())
|
|
||||||
set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed)
|
|
||||||
self.offroad = offroad
|
|
||||||
@@ -16,19 +16,19 @@ from openpilot.common.utils import strip_deprecated_keys
|
|||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import DT_HW
|
from openpilot.common.realtime import DT_HW
|
||||||
|
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
|
||||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||||
from openpilot.common.basedir import BASEDIR
|
from openpilot.common.basedir import BASEDIR
|
||||||
from openpilot.common.git import get_short_branch
|
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state
|
||||||
from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state
|
|
||||||
from openpilot.common.linux import LinuxSystemStats
|
from openpilot.common.linux import LinuxSystemStats
|
||||||
from openpilot.system.loggerd.config import get_available_percent
|
from openpilot.system.loggerd.config import get_available_percent
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.sunnypilot.system.statsd import statlog
|
from openpilot.sunnypilot.system.statsd import statlog
|
||||||
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
from openpilot.system.hardware.power_monitoring import PowerMonitoring
|
||||||
from openpilot.system.hardware.fan_controller import FanController
|
from openpilot.system.hardware.fan_controller import FanController
|
||||||
from openpilot.system.hardware.chestnut.status import ChestnutStatus
|
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp, CHESTNUT_BRANCHES
|
||||||
from openpilot.common.version import terms_version, training_version, get_build_metadata, terms_version_sp
|
|
||||||
|
|
||||||
ThermalStatus = log.DeviceState.ThermalStatus
|
ThermalStatus = log.DeviceState.ThermalStatus
|
||||||
NetworkType = log.DeviceState.NetworkType
|
NetworkType = log.DeviceState.NetworkType
|
||||||
@@ -49,11 +49,6 @@ class Chestnut:
|
|||||||
self.attempts = 0
|
self.attempts = 0
|
||||||
self.last_attempt = 0.
|
self.last_attempt = 0.
|
||||||
self.flashed = False
|
self.flashed = False
|
||||||
self.mismatch = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def failed(self) -> bool:
|
|
||||||
return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed
|
|
||||||
|
|
||||||
def flash(self) -> None:
|
def flash(self) -> None:
|
||||||
ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION],
|
ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION],
|
||||||
@@ -62,9 +57,9 @@ class Chestnut:
|
|||||||
self.flashed = ret.returncode == 0
|
self.flashed = ret.returncode == 0
|
||||||
|
|
||||||
def update(self, offroad: bool, usb_state: list[dict]) -> None:
|
def update(self, offroad: bool, usb_state: list[dict]) -> None:
|
||||||
self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and
|
mismatch = any((d["vendorId"], d["productId"]) in CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS and
|
||||||
d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state)
|
d["product"] != f"custom {CHESTNUT_FW_VERSION}-CLEAN" for d in usb_state)
|
||||||
if not self.mismatch:
|
if not mismatch:
|
||||||
self.flashed = False
|
self.flashed = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -196,7 +191,7 @@ def hw_state_thread(end_event, hw_queue):
|
|||||||
def hardware_thread(end_event, hw_queue) -> None:
|
def hardware_thread(end_event, hw_queue) -> None:
|
||||||
system_stats = LinuxSystemStats()
|
system_stats = LinuxSystemStats()
|
||||||
pm = messaging.PubMaster(['deviceState'])
|
pm = messaging.PubMaster(['deviceState'])
|
||||||
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates")
|
sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates")
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
@@ -244,8 +239,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
|||||||
|
|
||||||
fan_controller = FanController(int(1./DT_HW))
|
fan_controller = FanController(int(1./DT_HW))
|
||||||
chestnut = Chestnut()
|
chestnut = Chestnut()
|
||||||
chestnut_status = ChestnutStatus()
|
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
|
||||||
branch = get_short_branch()
|
|
||||||
|
|
||||||
while not end_event.is_set():
|
while not end_event.is_set():
|
||||||
sm.update(PANDA_STATES_TIMEOUT)
|
sm.update(PANDA_STATES_TIMEOUT)
|
||||||
@@ -307,11 +301,12 @@ def hardware_thread(end_event, hw_queue) -> None:
|
|||||||
|
|
||||||
set_usb_state(msg.deviceState, last_hw_state.usb_state)
|
set_usb_state(msg.deviceState, last_hw_state.usb_state)
|
||||||
chestnut.update(started_ts is None, last_hw_state.usb_state)
|
chestnut.update(started_ts is None, last_hw_state.usb_state)
|
||||||
chestnut_state = sm["chestnutState"]
|
current_channel = get_build_metadata().channel
|
||||||
chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"]
|
chestnut_target = CHESTNUT_BRANCHES.get(current_channel)
|
||||||
chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed,
|
chestnut_needs_switch = msg.deviceState.chestnutPresent and not big_model_available and chestnut_target is not None
|
||||||
params.get_bool("ChestnutLoading"), params.get("ChestnutActive"),
|
set_offroad_alert_if_changed("Offroad_ChestnutBranch", chestnut_needs_switch,
|
||||||
chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed)
|
extra_text=chestnut_target if chestnut_needs_switch else None)
|
||||||
|
|
||||||
# this subset is only used for offroad
|
# this subset is only used for offroad
|
||||||
temp_sources = [
|
temp_sources = [
|
||||||
msg.deviceState.memoryTempC,
|
msg.deviceState.memoryTempC,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import subprocess
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from enum import IntEnum, StrEnum
|
from enum import StrEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from importlib.resources import as_file, files
|
from importlib.resources import as_file, files
|
||||||
@@ -118,18 +118,6 @@ class FontWeight(StrEnum):
|
|||||||
DISPLAY = "Inter-Bold.ttf"
|
DISPLAY = "Inter-Bold.ttf"
|
||||||
|
|
||||||
|
|
||||||
class TextAlignment(IntEnum):
|
|
||||||
LEFT = 0
|
|
||||||
CENTER = 1
|
|
||||||
RIGHT = 2
|
|
||||||
|
|
||||||
|
|
||||||
class TextAlignmentVertical(IntEnum):
|
|
||||||
TOP = 0
|
|
||||||
MIDDLE = 1
|
|
||||||
BOTTOM = 2
|
|
||||||
|
|
||||||
|
|
||||||
def font_fallback(font: rl.Font) -> rl.Font:
|
def font_fallback(font: rl.Font) -> rl.Font:
|
||||||
"""Use a Noto fallback for languages not covered by Inter."""
|
"""Use a Noto fallback for languages not covered by Inter."""
|
||||||
if multilang.requires_font_fallback():
|
if multilang.requires_font_fallback():
|
||||||
@@ -347,6 +335,7 @@ class GuiApplication(GuiApplicationExt):
|
|||||||
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
|
rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps)
|
||||||
|
|
||||||
self._target_fps = fps
|
self._target_fps = fps
|
||||||
|
self._set_styles()
|
||||||
self._load_fonts()
|
self._load_fonts()
|
||||||
self._patch_text_functions()
|
self._patch_text_functions()
|
||||||
self._patch_scissor_mode()
|
self._patch_scissor_mode()
|
||||||
@@ -750,6 +739,14 @@ class GuiApplication(GuiApplicationExt):
|
|||||||
self._fonts[font_weight_file] = font
|
self._fonts[font_weight_file] = font
|
||||||
if multilang.requires_font_fallback():
|
if multilang.requires_font_fallback():
|
||||||
self.fallback_font()
|
self.fallback_font()
|
||||||
|
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
|
||||||
|
|
||||||
|
def _set_styles(self):
|
||||||
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)
|
||||||
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE)
|
||||||
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK))
|
||||||
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
|
||||||
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
||||||
|
|
||||||
def _patch_text_functions(self):
|
def _patch_text_functions(self):
|
||||||
# Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt
|
# Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import pyray as rl
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
class GuiStyleContext:
|
||||||
|
def __init__(self, styles: Sequence[tuple[int, int, int]]):
|
||||||
|
"""styles is a list of tuples (control, prop, new_value)"""
|
||||||
|
self.styles = styles
|
||||||
|
self.prev_styles: list[tuple[int, int, int]] = []
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
for control, prop, new_value in self.styles:
|
||||||
|
prev_value = rl.gui_get_style(control, prop)
|
||||||
|
self.prev_styles.append((control, prop, prev_value))
|
||||||
|
rl.gui_set_style(control, prop, new_value)
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
for control, prop, prev_value in self.prev_styles:
|
||||||
|
rl.gui_set_style(control, prop, prev_value)
|
||||||
@@ -18,7 +18,7 @@ from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
|||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.common.time_helpers import system_time_valid
|
from openpilot.common.time_helpers import system_time_valid
|
||||||
from openpilot.common.utils import run_cmd
|
from openpilot.common.utils import run_cmd
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus
|
from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus
|
||||||
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
|
||||||
@@ -105,8 +105,8 @@ class StartPage(Widget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER,
|
font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False)
|
self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False)
|
||||||
self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False)
|
self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False)
|
||||||
@@ -197,7 +197,7 @@ class DownloadingPage(NavWidget):
|
|||||||
self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
font_weight=FontWeight.DISPLAY)
|
font_weight=FontWeight.DISPLAY)
|
||||||
self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||||
font_weight=FontWeight.ROMAN, alignment_vertical=TextAlignmentVertical.BOTTOM)
|
font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
self._progress = 0
|
self._progress = 0
|
||||||
|
|
||||||
def _back_enabled(self) -> bool:
|
def _back_enabled(self) -> bool:
|
||||||
@@ -261,8 +261,8 @@ class BigPillButton(BigButton):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self._label.set_font_size(48)
|
self._label.set_font_size(48)
|
||||||
self._label.set_alignment(TextAlignment.CENTER)
|
self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
self._label.set_alignment_vertical(TextAlignmentVertical.MIDDLE)
|
self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
def _load_images(self):
|
def _load_images(self):
|
||||||
if self._green:
|
if self._green:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pyray as rl
|
|||||||
from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
from openpilot.common.realtime import config_realtime_process, set_core_affinity
|
||||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
from openpilot.system.ui.widgets.nav_widget import NavWidget
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
@@ -30,7 +30,7 @@ class ProgressPage(NavWidget):
|
|||||||
font_weight=FontWeight.DISPLAY, line_height=0.8)
|
font_weight=FontWeight.DISPLAY, line_height=0.8)
|
||||||
self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)),
|
||||||
font_weight=FontWeight.ROMAN,
|
font_weight=FontWeight.ROMAN,
|
||||||
alignment_vertical=TextAlignmentVertical.BOTTOM)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM)
|
||||||
|
|
||||||
def _back_enabled(self) -> bool:
|
def _back_enabled(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
|
from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP
|
||||||
from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel
|
from openpilot.system.ui.widgets.label import ScrollState, UnifiedLabel
|
||||||
@@ -40,7 +40,7 @@ class ScrollingButtonAction(ButtonActionSP):
|
|||||||
super().__init__(text=text, width=width, enabled=enabled)
|
super().__init__(text=text, width=width, enabled=enabled)
|
||||||
self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
|
self._value_label = UnifiedLabelSP("", font_size=style.ITEM_TEXT_FONT_SIZE, font_weight=FontWeight.NORMAL,
|
||||||
text_color=self._value_color, scroll=True,
|
text_color=self._value_color, scroll=True,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
def set_value(self, value: str | Callable[[], str], color: rl.Color = style.ITEM_TEXT_VALUE_COLOR):
|
||||||
if self.value != _resolve_value(value, ""):
|
if self.value != _resolve_value(value, ""):
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import math
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import TextAlignment, TextAlignmentVertical
|
|
||||||
|
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
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.shader_polygon import draw_polygon, Gradient
|
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||||
@@ -63,11 +61,11 @@ class DownloadStatusAction(ItemAction):
|
|||||||
self._sweep = 0.0
|
self._sweep = 0.0
|
||||||
|
|
||||||
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
self._name_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||||
alignment=TextAlignment.LEFT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
self._percent_label = UnifiedLabel("", font_size=FONT_SIZE, font_weight=FontWeight.NORMAL, text_color=TEXT_COLOR,
|
||||||
alignment=TextAlignment.RIGHT,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None):
|
def update(self, name, downloading=False, progress=0.0, status_text="", text_color=rl.GRAY, icon=None, icon_color=None, segments=None):
|
||||||
self.segments = segments
|
self.segments = segments
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from collections.abc import Callable, Sequence
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
|
from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -90,8 +90,8 @@ class ButtonActionSP(ButtonAction):
|
|||||||
if value_text:
|
if value_text:
|
||||||
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
||||||
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color,
|
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._value_color,
|
||||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
|
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
pressed = self._pressed
|
pressed = self._pressed
|
||||||
self._pressed = False
|
self._pressed = False
|
||||||
@@ -319,7 +319,7 @@ class ListItemSP(ListItem):
|
|||||||
)
|
)
|
||||||
if value_rect.width > 0:
|
if value_rect.width > 0:
|
||||||
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL,
|
gui_label(value_rect, value_text, font_size=style.ITEM_TEXT_FONT_SIZE, color=self._right_value_color, font_weight=FontWeight.NORMAL,
|
||||||
alignment=TextAlignment.RIGHT, alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
# Render toggle and handle callback
|
# Render toggle and handle callback
|
||||||
if self.action_item.render(left_rect) and self.action_item.enabled:
|
if self.action_item.render(left_rect) and self.action_item.enabled:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from dataclasses import dataclass, field
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.system.ui.lib.application import TextAlignment
|
|
||||||
from openpilot.system.ui.lib.application import FontWeight
|
from openpilot.system.ui.lib.application import FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import DialogResult
|
from openpilot.system.ui.widgets import DialogResult
|
||||||
@@ -36,7 +35,7 @@ class TreeFolder:
|
|||||||
|
|
||||||
class TreeItemWidget(Button):
|
class TreeItemWidget(Button):
|
||||||
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False):
|
def __init__(self, text, ref, is_folder=False, indent_level=0, click_callback=None, favorite_callback=None, is_favorite=False, is_expanded=False):
|
||||||
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=TextAlignment.LEFT,
|
super().__init__(text, click_callback, button_style=ButtonStyle.NORMAL, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
text_padding=20 + indent_level * 30, elide_right=True)
|
text_padding=20 + indent_level * 30, elide_right=True)
|
||||||
self.text = text
|
self.text = text
|
||||||
self.ref = ref
|
self.ref = ref
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from openpilot.common.hardware import PC
|
|||||||
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.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import gui_label, gui_text_box
|
||||||
|
|
||||||
USERDATA = "/dev/disk/by-partlabel/userdata"
|
USERDATA = "/dev/disk/by-partlabel/userdata"
|
||||||
TIMEOUT = 3*60
|
TIMEOUT = 3*60
|
||||||
@@ -38,7 +38,6 @@ class Reset(Widget):
|
|||||||
self._cancel_button = Button("Cancel", gui_app.request_close)
|
self._cancel_button = Button("Cancel", gui_app.request_close)
|
||||||
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
|
self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY)
|
||||||
self._reboot_button = Button("Reboot", self._reboot)
|
self._reboot_button = Button("Reboot", self._reboot)
|
||||||
self._body_label = UnifiedLabel(self._get_body_text, 90, line_height=1 / 0.9)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _reboot() -> None:
|
def _reboot() -> None:
|
||||||
@@ -76,7 +75,7 @@ class Reset(Widget):
|
|||||||
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
|
gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD)
|
||||||
|
|
||||||
text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE)
|
text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE)
|
||||||
self._body_label.render(text_rect)
|
gui_text_box(text_rect, self._get_body_text(), 90)
|
||||||
|
|
||||||
button_height = 160
|
button_height = 160
|
||||||
button_spacing = 50
|
button_spacing = 50
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import pyray as rl
|
|||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
from openpilot.common.hardware import HARDWARE
|
from openpilot.common.hardware import HARDWARE
|
||||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio
|
||||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||||
@@ -67,17 +67,17 @@ class Setup(Widget):
|
|||||||
self.warning = gui_app.texture("icons/warning.png", 150, 150)
|
self.warning = gui_app.texture("icons/warning.png", 150, 150)
|
||||||
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100)
|
self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100)
|
||||||
|
|
||||||
self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT,
|
self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
text_color=rl.Color(255, 89, 79, 255), text_padding=20)
|
text_color=rl.Color(255, 89, 79, 255), text_padding=20)
|
||||||
self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE,
|
self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE,
|
||||||
text_alignment=TextAlignment.LEFT, text_padding=20)
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback)
|
self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback)
|
||||||
self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown)
|
self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown)
|
||||||
|
|
||||||
self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0)
|
self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0)
|
||||||
self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.",
|
self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.",
|
||||||
BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
|
BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
|
|
||||||
self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
||||||
self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80)
|
||||||
@@ -85,32 +85,32 @@ class Setup(Widget):
|
|||||||
button_style=ButtonStyle.PRIMARY)
|
button_style=ButtonStyle.PRIMARY)
|
||||||
self._software_selection_continue_button.set_enabled(False)
|
self._software_selection_continue_button.set_enabled(False)
|
||||||
self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback)
|
self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback)
|
||||||
self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT,
|
self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
text_padding=20)
|
text_padding=20)
|
||||||
|
|
||||||
self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot)
|
self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot)
|
||||||
self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY)
|
self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY)
|
||||||
self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
|
self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20)
|
self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
|
|
||||||
self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback)
|
self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback)
|
||||||
self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback,
|
self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback,
|
||||||
button_style=ButtonStyle.PRIMARY)
|
button_style=ButtonStyle.PRIMARY)
|
||||||
self._network_setup_continue_button.set_enabled(False)
|
self._network_setup_continue_button.set_enabled(False)
|
||||||
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
|
|
||||||
self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback,
|
self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback,
|
||||||
button_style=ButtonStyle.PRIMARY)
|
button_style=ButtonStyle.PRIMARY)
|
||||||
self._custom_software_warning_continue_button.set_enabled(False)
|
self._custom_software_warning_continue_button.set_enabled(False)
|
||||||
self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback)
|
self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback)
|
||||||
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, TextAlignment.LEFT,
|
self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
text_color=rl.Color(255, 89, 79, 255),
|
text_color=rl.Color(255, 89, 79, 255),
|
||||||
text_padding=60)
|
text_padding=60)
|
||||||
self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE))
|
self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE))
|
||||||
self._custom_software_warning_body_labels = [
|
self._custom_software_warning_body_labels = [
|
||||||
Label(text, 68, text_alignment=TextAlignment.LEFT,
|
Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
text_alignment_vertical=TextAlignmentVertical.TOP,
|
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
text_padding=60, icon=self._yellow_warning_icon if has_icon else None)
|
text_padding=60, icon=self._yellow_warning_icon if has_icon else None)
|
||||||
for text, has_icon in [
|
for text, has_icon in [
|
||||||
("Use caution when installing third-party software.", False),
|
("Use caution when installing third-party software.", False),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
|||||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import gui_text_box, gui_label
|
||||||
from openpilot.system.ui.widgets.network import WifiManagerUI
|
from openpilot.system.ui.widgets.network import WifiManagerUI
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
@@ -50,8 +50,6 @@ class Updater(Widget):
|
|||||||
self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY)
|
self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY)
|
||||||
self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT))
|
self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT))
|
||||||
self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot())
|
self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot())
|
||||||
self._desc_label = UnifiedLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
|
|
||||||
"The download size is approximately 1GB.", BODY_FONT_SIZE, line_height=1 / 0.9)
|
|
||||||
|
|
||||||
def set_current_screen(self, screen: Screen):
|
def set_current_screen(self, screen: Screen):
|
||||||
self.current_screen = screen
|
self.current_screen = screen
|
||||||
@@ -101,8 +99,11 @@ class Updater(Widget):
|
|||||||
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
|
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " +
|
||||||
|
"The download size is approximately 1GB.")
|
||||||
|
|
||||||
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4)
|
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4)
|
||||||
self._desc_label.render(desc_rect)
|
gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE)
|
||||||
|
|
||||||
# Buttons at the bottom
|
# Buttons at the bottom
|
||||||
button_y = rect.height - MARGIN - BUTTON_HEIGHT
|
button_y = rect.height - MARGIN - BUTTON_HEIGHT
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from enum import IntEnum
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.label import Label
|
from openpilot.system.ui.widgets.label import Label
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
@@ -86,7 +86,7 @@ class Button(Widget):
|
|||||||
font_weight: FontWeight = FontWeight.MEDIUM,
|
font_weight: FontWeight = FontWeight.MEDIUM,
|
||||||
button_style: ButtonStyle = ButtonStyle.NORMAL,
|
button_style: ButtonStyle = ButtonStyle.NORMAL,
|
||||||
border_radius: int = 10,
|
border_radius: int = 10,
|
||||||
text_alignment: int = TextAlignment.CENTER,
|
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_padding: int = 20,
|
text_padding: int = 20,
|
||||||
icon=None,
|
icon=None,
|
||||||
elide_right: bool = False,
|
elide_right: bool = False,
|
||||||
@@ -139,7 +139,7 @@ class ButtonRadio(Button):
|
|||||||
icon,
|
icon,
|
||||||
click_callback: Callable[[], None] | None = None,
|
click_callback: Callable[[], None] | None = None,
|
||||||
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
|
font_size: int = DEFAULT_BUTTON_FONT_SIZE,
|
||||||
text_alignment: int = TextAlignment.LEFT,
|
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
border_radius: int = 10,
|
border_radius: int = 10,
|
||||||
text_padding: int = 20,
|
text_padding: int = 20,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -14,6 +14,3 @@ class IconWidget(Widget):
|
|||||||
def _render(self, _) -> None:
|
def _render(self, _) -> None:
|
||||||
color = rl.Color(255, 255, 255, int(self._opacity * 255))
|
color = rl.Color(255, 255, 255, int(self._opacity * 255))
|
||||||
rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color)
|
rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color)
|
||||||
|
|
||||||
def set_opacity(self, opacity: float) -> None:
|
|
||||||
self._opacity = opacity
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||||
@@ -65,8 +65,8 @@ class Keyboard(Widget):
|
|||||||
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
|
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
|
||||||
self._caps_lock = False
|
self._caps_lock = False
|
||||||
self._last_shift_press_time = 0
|
self._last_shift_press_time = 0
|
||||||
self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20)
|
self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20)
|
self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20)
|
||||||
|
|
||||||
self._max_text_size = max_text_size
|
self._max_text_size = max_text_size
|
||||||
self._min_text_size = min_text_size
|
self._min_text_size = min_text_size
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from collections.abc import Callable
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
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.utils import GuiStyleContext
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
|
|
||||||
ICON_PADDING = 15
|
ICON_PADDING = 15
|
||||||
@@ -31,8 +32,8 @@ def gui_label(
|
|||||||
font_size: int = DEFAULT_TEXT_SIZE,
|
font_size: int = DEFAULT_TEXT_SIZE,
|
||||||
color: rl.Color = DEFAULT_TEXT_COLOR,
|
color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||||
font_weight: FontWeight = FontWeight.NORMAL,
|
font_weight: FontWeight = FontWeight.NORMAL,
|
||||||
alignment: int = TextAlignment.LEFT,
|
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical: int = TextAlignmentVertical.MIDDLE,
|
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
elide_right: bool = True
|
elide_right: bool = True
|
||||||
):
|
):
|
||||||
font = gui_app.font(font_weight)
|
font = gui_app.font(font_weight)
|
||||||
@@ -56,16 +57,16 @@ def gui_label(
|
|||||||
|
|
||||||
# Calculate horizontal position based on alignment
|
# Calculate horizontal position based on alignment
|
||||||
text_x = rect.x + {
|
text_x = rect.x + {
|
||||||
TextAlignment.LEFT: 0,
|
rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0,
|
||||||
TextAlignment.CENTER: (rect.width - text_size.x) / 2,
|
rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2,
|
||||||
TextAlignment.RIGHT: rect.width - text_size.x,
|
rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x,
|
||||||
}.get(alignment, 0)
|
}.get(alignment, 0)
|
||||||
|
|
||||||
# Calculate vertical position based on alignment
|
# Calculate vertical position based on alignment
|
||||||
text_y = rect.y + {
|
text_y = rect.y + {
|
||||||
TextAlignmentVertical.TOP: 0,
|
rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0,
|
||||||
TextAlignmentVertical.MIDDLE: (rect.height - text_size.y) / 2,
|
rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2,
|
||||||
TextAlignmentVertical.BOTTOM: rect.height - text_size.y,
|
rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y,
|
||||||
}.get(alignment_vertical, 0)
|
}.get(alignment_vertical, 0)
|
||||||
|
|
||||||
# Draw the text in the specified rectangle
|
# Draw the text in the specified rectangle
|
||||||
@@ -73,14 +74,42 @@ def gui_label(
|
|||||||
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
|
rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color)
|
||||||
|
|
||||||
|
|
||||||
|
def gui_text_box(
|
||||||
|
rect: rl.Rectangle,
|
||||||
|
text: str,
|
||||||
|
font_size: int = DEFAULT_TEXT_SIZE,
|
||||||
|
color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||||
|
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
|
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
|
font_weight: FontWeight = FontWeight.NORMAL,
|
||||||
|
line_scale: float = 1.0,
|
||||||
|
):
|
||||||
|
styles = [
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)),
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)),
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE * line_scale)),
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment),
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical),
|
||||||
|
(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD)
|
||||||
|
]
|
||||||
|
if font_weight != FontWeight.NORMAL:
|
||||||
|
rl.gui_set_font(gui_app.font(font_weight))
|
||||||
|
|
||||||
|
with GuiStyleContext(styles):
|
||||||
|
rl.gui_label(rect, text)
|
||||||
|
|
||||||
|
if font_weight != FontWeight.NORMAL:
|
||||||
|
rl.gui_set_font(gui_app.font(FontWeight.NORMAL))
|
||||||
|
|
||||||
|
|
||||||
# Non-interactive text area. Can render an optional specified icon.
|
# Non-interactive text area. Can render an optional specified icon.
|
||||||
class Label(Widget):
|
class Label(Widget):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
text: str | Callable[[], str],
|
text: str | Callable[[], str],
|
||||||
font_size: int = DEFAULT_TEXT_SIZE,
|
font_size: int = DEFAULT_TEXT_SIZE,
|
||||||
font_weight: FontWeight = FontWeight.NORMAL,
|
font_weight: FontWeight = FontWeight.NORMAL,
|
||||||
text_alignment: int = TextAlignment.CENTER,
|
text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
text_alignment_vertical: int = TextAlignmentVertical.MIDDLE,
|
text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
text_padding: int = 0,
|
text_padding: int = 0,
|
||||||
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||||
icon: Union[rl.Texture, None] = None,
|
icon: Union[rl.Texture, None] = None,
|
||||||
@@ -152,10 +181,10 @@ class Label(Widget):
|
|||||||
self._update_text(self._text)
|
self._update_text(self._text)
|
||||||
|
|
||||||
text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
|
text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0)
|
||||||
if self._text_alignment_vertical == TextAlignmentVertical.MIDDLE:
|
if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE:
|
||||||
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
||||||
text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2))
|
text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2))
|
||||||
elif self._text_alignment_vertical == TextAlignmentVertical.BOTTOM:
|
elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM:
|
||||||
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE
|
||||||
text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height)
|
text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height)
|
||||||
else:
|
else:
|
||||||
@@ -164,10 +193,10 @@ class Label(Widget):
|
|||||||
if self._icon:
|
if self._icon:
|
||||||
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2
|
||||||
if len(self._text_wrapped) > 0:
|
if len(self._text_wrapped) > 0:
|
||||||
if self._text_alignment == TextAlignment.LEFT:
|
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||||
icon_x = self._rect.x + self._text_padding
|
icon_x = self._rect.x + self._text_padding
|
||||||
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
||||||
elif self._text_alignment == TextAlignment.CENTER:
|
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||||
total_width = self._icon.width + ICON_PADDING + text_size.x
|
total_width = self._icon.width + ICON_PADDING + text_size.x
|
||||||
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
icon_x = self._rect.x + (self._rect.width - total_width) / 2
|
||||||
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
text_pos.x = self._rect.x + self._icon.width + ICON_PADDING
|
||||||
@@ -179,11 +208,11 @@ class Label(Widget):
|
|||||||
|
|
||||||
for text, text_size in zip(self._text_wrapped, self._text_size, strict=True):
|
for text, text_size in zip(self._text_wrapped, self._text_size, strict=True):
|
||||||
line_pos = rl.Vector2(text_pos.x, text_pos.y)
|
line_pos = rl.Vector2(text_pos.x, text_pos.y)
|
||||||
if self._text_alignment == TextAlignment.LEFT:
|
if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||||
line_pos.x += self._text_padding
|
line_pos.x += self._text_padding
|
||||||
elif self._text_alignment == TextAlignment.CENTER:
|
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||||
line_pos.x += (self._rect.width - text_size.x) // 2
|
line_pos.x += (self._rect.width - text_size.x) // 2
|
||||||
elif self._text_alignment == TextAlignment.RIGHT:
|
elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||||
line_pos.x += self._rect.width - text_size.x - self._text_padding
|
line_pos.x += self._rect.width - text_size.x - self._text_padding
|
||||||
|
|
||||||
rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color)
|
rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color)
|
||||||
@@ -192,7 +221,7 @@ class Label(Widget):
|
|||||||
|
|
||||||
class UnifiedLabel(Widget):
|
class UnifiedLabel(Widget):
|
||||||
"""
|
"""
|
||||||
Unified label widget that combines functionality from gui_label and Label.
|
Unified label widget that combines functionality from gui_label, gui_text_box, and Label.
|
||||||
|
|
||||||
Supports:
|
Supports:
|
||||||
- Text wrapping
|
- Text wrapping
|
||||||
@@ -212,8 +241,8 @@ class UnifiedLabel(Widget):
|
|||||||
font_size: int = DEFAULT_TEXT_SIZE,
|
font_size: int = DEFAULT_TEXT_SIZE,
|
||||||
font_weight: FontWeight = FontWeight.NORMAL,
|
font_weight: FontWeight = FontWeight.NORMAL,
|
||||||
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
text_color: rl.Color = DEFAULT_TEXT_COLOR,
|
||||||
alignment: int = TextAlignment.LEFT,
|
alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical: int = TextAlignmentVertical.TOP,
|
alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||||
text_padding: int = 0,
|
text_padding: int = 0,
|
||||||
max_width: int | None = None,
|
max_width: int | None = None,
|
||||||
elide: bool = True,
|
elide: bool = True,
|
||||||
@@ -532,9 +561,9 @@ class UnifiedLabel(Widget):
|
|||||||
total_visible_height += size.y * self._line_height
|
total_visible_height += size.y * self._line_height
|
||||||
|
|
||||||
# Calculate vertical alignment offset
|
# Calculate vertical alignment offset
|
||||||
if self._alignment_vertical == TextAlignmentVertical.TOP:
|
if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP:
|
||||||
start_y = self._rect.y
|
start_y = self._rect.y
|
||||||
elif self._alignment_vertical == TextAlignmentVertical.BOTTOM:
|
elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM:
|
||||||
start_y = self._rect.y + self._rect.height - total_visible_height
|
start_y = self._rect.y + self._rect.height - total_visible_height
|
||||||
else: # TEXT_ALIGN_MIDDLE
|
else: # TEXT_ALIGN_MIDDLE
|
||||||
start_y = self._rect.y + (self._rect.height - total_visible_height) / 2
|
start_y = self._rect.y + (self._rect.height - total_visible_height) / 2
|
||||||
@@ -611,11 +640,11 @@ class UnifiedLabel(Widget):
|
|||||||
|
|
||||||
def _render_line(self, line, size, current_y, x_offset=0.0):
|
def _render_line(self, line, size, current_y, x_offset=0.0):
|
||||||
# Calculate horizontal position
|
# Calculate horizontal position
|
||||||
if self._alignment == TextAlignment.LEFT:
|
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT:
|
||||||
line_x = self._rect.x + self._text_padding
|
line_x = self._rect.x + self._text_padding
|
||||||
elif self._alignment == TextAlignment.CENTER:
|
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||||
line_x = self._rect.x + (self._rect.width - size.x) / 2
|
line_x = self._rect.x + (self._rect.width - size.x) / 2
|
||||||
elif self._alignment == TextAlignment.RIGHT:
|
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||||
line_x = self._rect.x + self._rect.width - size.x - self._text_padding
|
line_x = self._rect.x + self._rect.width - size.x - self._text_padding
|
||||||
else:
|
else:
|
||||||
line_x = self._rect.x + self._text_padding
|
line_x = self._rect.x + self._text_padding
|
||||||
@@ -633,9 +662,9 @@ class UnifiedLabel(Widget):
|
|||||||
def _render_line_shimmer(self, line, line_x, current_y):
|
def _render_line_shimmer(self, line, line_x, current_y):
|
||||||
# Shimmer range based on widest line so sweep is even across all lines
|
# Shimmer range based on widest line so sweep is even across all lines
|
||||||
max_width = self.text_width
|
max_width = self.text_width
|
||||||
if self._alignment == TextAlignment.RIGHT:
|
if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT:
|
||||||
shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width
|
shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width
|
||||||
elif self._alignment == TextAlignment.CENTER:
|
elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER:
|
||||||
shimmer_left = self._rect.x + (self._rect.width - max_width) / 2
|
shimmer_left = self._rect.x + (self._rect.width - max_width) / 2
|
||||||
else:
|
else:
|
||||||
shimmer_left = self._rect.x + self._text_padding
|
shimmer_left = self._rect.x + self._text_padding
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -139,8 +139,8 @@ class ButtonAction(ItemAction):
|
|||||||
if value_text:
|
if value_text:
|
||||||
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height)
|
||||||
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
|
gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR,
|
||||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT,
|
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
|
||||||
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
|
# TODO: just use the generic Widget click callbacks everywhere, no returning from render
|
||||||
pressed = self._pressed
|
pressed = self._pressed
|
||||||
@@ -168,8 +168,8 @@ class TextAction(ItemAction):
|
|||||||
|
|
||||||
def _render(self, rect: rl.Rectangle) -> bool:
|
def _render(self, rect: rl.Rectangle) -> bool:
|
||||||
gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color,
|
gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color,
|
||||||
font_weight=FontWeight.NORMAL, alignment=TextAlignment.RIGHT,
|
font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
alignment_vertical=TextAlignmentVertical.MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_text(self, text: str | Callable[[], str]):
|
def set_text(self, text: str | Callable[[], str]):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from functools import partial
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.system.ui.lib.application import gui_app, TextAlignment
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel
|
||||||
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid
|
from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid
|
||||||
@@ -67,7 +67,7 @@ class NavButton(Widget):
|
|||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255)
|
color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255)
|
||||||
rl.draw_rectangle_rounded(self._rect, 0.6, 10, color)
|
rl.draw_rectangle_rounded(self._rect, 0.6, 10, color)
|
||||||
gui_label(self.rect, self.text, font_size=60, alignment=TextAlignment.CENTER)
|
gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
|
|
||||||
|
|
||||||
class NetworkUI(Widget):
|
class NetworkUI(Widget):
|
||||||
@@ -315,7 +315,7 @@ class WifiManagerUI(Widget):
|
|||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
if not self._networks:
|
if not self._networks:
|
||||||
gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=TextAlignment.CENTER)
|
gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.state == UIState.NEEDS_AUTH and self._state_network:
|
if self.state == UIState.NEEDS_AUTH and self._state_network:
|
||||||
@@ -389,7 +389,7 @@ class WifiManagerUI(Widget):
|
|||||||
|
|
||||||
if status_text:
|
if status_text:
|
||||||
status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT)
|
status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT)
|
||||||
gui_label(status_text_rect, status_text, font_size=48, alignment=TextAlignment.CENTER)
|
gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||||
else:
|
else:
|
||||||
# If the network is saved, show the "Forget" button
|
# If the network is saved, show the "Forget" button
|
||||||
if self._wifi_manager.is_connection_saved(network.ssid):
|
if self._wifi_manager.is_connection_saved(network.ssid):
|
||||||
@@ -455,7 +455,7 @@ class WifiManagerUI(Widget):
|
|||||||
self._networks = networks
|
self._networks = networks
|
||||||
for n in self._networks:
|
for n in self._networks:
|
||||||
self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55,
|
self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55,
|
||||||
text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
|
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT)
|
||||||
self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
|
self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid())
|
||||||
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
|
self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
|
||||||
font_size=45)
|
font_size=45)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user