mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-30 02:23:43 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a04d756649 | |||
| 1dd5a7c91d | |||
| acb784d207 | |||
| 4a13639cfd | |||
| fa75fdd852 | |||
| 5cfdb2f4da | |||
| 63548ce10d | |||
| 980fb79c1a | |||
| d40df6f829 | |||
| 6f0b4f2338 | |||
| 93138ade9d | |||
| d26dc2dd38 | |||
| 1cb30b8142 | |||
| 8cf62e9733 | |||
| d936c7aede | |||
| e7c3736fc7 | |||
| 0268dfe79a | |||
| bfe5c079a6 | |||
| f7543e47a0 | |||
| b94f8eaecb | |||
| c41d953be5 | |||
| c603d07012 | |||
| d2ab08eaf2 | |||
| 1086abcf6b | |||
| 617a98d769 | |||
| 7e9de97596 | |||
| 21ab8c9daf | |||
| d671c7e03a | |||
| 7cd9a76a25 | |||
| 7292d409a7 | |||
| 78009bdf39 | |||
| 5e097b2682 | |||
| 31a61a24f8 | |||
| 000c6ce261 | |||
| f6998b3d98 | |||
| c197a1a45a | |||
| 33e3809330 | |||
| df5c0a2690 | |||
| cce352cccb |
@@ -8,13 +8,13 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
target_hardware:
|
||||
description: 'Hardware target to compile for (qcom or usbgpu)'
|
||||
description: 'Hardware target to compile for (qcom or chestnut)'
|
||||
required: true
|
||||
type: choice
|
||||
default: 'qcom'
|
||||
options:
|
||||
- qcom
|
||||
- usbgpu
|
||||
- chestnut
|
||||
hf_repo:
|
||||
description: 'Hugging Face dataset repository'
|
||||
required: false
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
id: get-json
|
||||
run: |
|
||||
cd docs/docs
|
||||
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}v"
|
||||
PREFIX="driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_' || '' }}v"
|
||||
latest=$(ls ${PREFIX}*.json | sed -E "s/${PREFIX}([0-9]+)\.json/\1/" | sort -n | tail -1)
|
||||
next=$((latest+1))
|
||||
json_file="${PREFIX}${next}.json"
|
||||
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
build_big_model:
|
||||
needs: resolve
|
||||
if: ${{ inputs.target == 'big' }}
|
||||
runs-on: [self-hosted, usbgpu]
|
||||
runs-on: [self-hosted, chestnut]
|
||||
env:
|
||||
BIG_ONNX: openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx
|
||||
BIG_PKL: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||
|
||||
@@ -30,7 +30,7 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
target_hardware:
|
||||
description: 'Hardware target to compile for (qcom or usbgpu)'
|
||||
description: 'Hardware target to compile for (qcom or chestnut)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'qcom'
|
||||
@@ -101,7 +101,7 @@ on:
|
||||
default: 'qcom'
|
||||
options:
|
||||
- qcom
|
||||
- usbgpu
|
||||
- chestnut
|
||||
hf_repo:
|
||||
description: 'Hugging Face dataset repository'
|
||||
required: false
|
||||
@@ -109,7 +109,7 @@ on:
|
||||
default: 'sunnypilot/sunnypilot_models_v1'
|
||||
env:
|
||||
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
|
||||
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
|
||||
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
|
||||
|
||||
jobs:
|
||||
build_model:
|
||||
|
||||
@@ -31,7 +31,7 @@ on:
|
||||
type: string
|
||||
default: ''
|
||||
target_hardware:
|
||||
description: 'Hardware target to compile for (qcom or usbgpu)'
|
||||
description: 'Hardware target to compile for (qcom or chestnut)'
|
||||
required: false
|
||||
type: string
|
||||
default: 'qcom'
|
||||
@@ -57,7 +57,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- qcom
|
||||
- usbgpu
|
||||
- chestnut
|
||||
default: 'qcom'
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
cat $GITHUB_OUTPUT
|
||||
- run: |
|
||||
cd ${{ github.workspace }}/openpilot/openpilot
|
||||
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
|
||||
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
|
||||
git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
|
||||
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
|
||||
else
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
build_model:
|
||||
runs-on: [self-hosted, usbgpu]
|
||||
runs-on: [self-hosted, chestnut]
|
||||
needs: get_model
|
||||
env:
|
||||
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
|
||||
@@ -185,9 +185,9 @@ jobs:
|
||||
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
|
||||
|
||||
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
|
||||
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
|
||||
echo "USBGPU build"
|
||||
export USBGPU=1
|
||||
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
|
||||
echo "CHESTNUT build"
|
||||
export CHESTNUT=1
|
||||
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2"
|
||||
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
|
||||
else
|
||||
|
||||
@@ -130,8 +130,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UpdaterLastFetchTime", {PERSISTENT, TIME}},
|
||||
{"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}},
|
||||
{"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}},
|
||||
{"UsbGpuLoading", {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}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
@@ -195,15 +195,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
|
||||
// Model Manager params
|
||||
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
|
||||
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}},
|
||||
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync?
|
||||
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
|
||||
{"ModelManager_ActiveJson", {CLEAR_ON_MANAGER_START, JSON}},
|
||||
{"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}},
|
||||
{"ModelManager_DownloadRef", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}},
|
||||
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
|
||||
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"ModelManager_LastSyncTime_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
|
||||
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
|
||||
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
|
||||
{"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}},
|
||||
|
||||
// Neural Network Lateral Control
|
||||
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
|
||||
size 13079
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
|
||||
size 2034
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
|
||||
size 7279
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
|
||||
size 10876
|
||||
@@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi
|
||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path
|
||||
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path
|
||||
|
||||
|
||||
CAMERA_CONFIGS = [
|
||||
@@ -36,18 +36,18 @@ else:
|
||||
tg_devices = { # which device to put jit inputs to at runtime
|
||||
'openpilot.selfdrive.modeld.modeld': {
|
||||
'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend},
|
||||
'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
|
||||
},
|
||||
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
|
||||
'default': {'DEV': tg_backend}
|
||||
},
|
||||
}
|
||||
|
||||
USBGPU = usbgpu_present()
|
||||
if USBGPU:
|
||||
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2'
|
||||
CHESTNUT = chestnut_present()
|
||||
if CHESTNUT:
|
||||
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
|
||||
usbgpu_lock = File("models/.usb_gpu.lock").abspath
|
||||
chestnut_lock = File("models/.chestnut.lock").abspath
|
||||
|
||||
def write_tg_devices(target, source, env):
|
||||
with open(str(target[0]), "w") as f:
|
||||
@@ -74,10 +74,10 @@ model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
|
||||
if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
for usbgpu in [False, True] if USBGPU else [False]:
|
||||
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
||||
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||
for chestnut in [False, True] if CHESTNUT else [False]:
|
||||
target_pkl_path = File(modeld_pkl_path(chestnut)).abspath
|
||||
# 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)
|
||||
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.
|
||||
@@ -104,14 +104,14 @@ if not os.getenv('SKIP_TINYGRAD_COMPILE'):
|
||||
chunk_file(pkl, chunks)
|
||||
def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets):
|
||||
chunk_file(pkl, chunks)
|
||||
actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu 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(
|
||||
chunk_targets,
|
||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||
actions,
|
||||
)
|
||||
if usbgpu:
|
||||
lenv.SideEffect(usbgpu_lock, node)
|
||||
if chestnut:
|
||||
lenv.SideEffect(chestnut_lock, node)
|
||||
|
||||
# get model metadata
|
||||
fn = File(f"models/dmonitoring_model").abspath
|
||||
|
||||
@@ -29,7 +29,7 @@ class ModelState:
|
||||
output: np.ndarray
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int):
|
||||
self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV']
|
||||
self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV']
|
||||
with open(METADATA_PATH, 'rb') as f:
|
||||
model_metadata = pickle.load(f)
|
||||
self.input_shapes = model_metadata['input_shapes']
|
||||
|
||||
@@ -13,12 +13,12 @@ MODELS_DIR = Path(__file__).resolve().parent / 'models'
|
||||
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
|
||||
|
||||
|
||||
def get_tg_input_devices(process_name: str, usbgpu: bool):
|
||||
def get_tg_input_devices(process_name: str, chestnut: bool):
|
||||
with open(TG_INPUT_DEVICES_PATH) as f:
|
||||
return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu']
|
||||
return json.load(f)[process_name]['default' if not chestnut else 'chestnut']
|
||||
|
||||
def modeld_pkl_path(usbgpu: bool):
|
||||
prefix = 'big_' if usbgpu else ''
|
||||
def modeld_pkl_path(chestnut: bool):
|
||||
prefix = 'big_' if chestnut else ''
|
||||
return MODELS_DIR / f'{prefix}driving_tinygrad.pkl'
|
||||
|
||||
def dump_oob(obj, f):
|
||||
@@ -45,7 +45,7 @@ def load_oob(f):
|
||||
yield pb
|
||||
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
|
||||
|
||||
def usbgpu_present() -> bool:
|
||||
def chestnut_present() -> bool:
|
||||
for d in USB_DEVICES_PATH.glob("*"):
|
||||
try:
|
||||
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
|
||||
@@ -56,5 +56,5 @@ def usbgpu_present() -> bool:
|
||||
pass
|
||||
return False
|
||||
|
||||
def usbgpu_compiled() -> bool:
|
||||
return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file()
|
||||
def chestnut_compiled() -> bool:
|
||||
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections.abc import Callable
|
||||
import ctypes
|
||||
from functools import cached_property
|
||||
import os
|
||||
os.environ['GMMU'] = '0' # for usbgpu 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
|
||||
import struct
|
||||
@@ -30,7 +32,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN
|
||||
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.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, 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.modeld_v2.modeld_base import ModelStateBase
|
||||
@@ -94,8 +96,10 @@ class ChestnutState:
|
||||
if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1:
|
||||
try:
|
||||
smu = Device["AMD"].iface.dev_impl.smu
|
||||
metrics_t = smu.smu_mod.SmuMetricsExternal_t
|
||||
smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100)
|
||||
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
|
||||
metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
|
||||
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
|
||||
self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
|
||||
'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
|
||||
'powerDrawW': metrics.AverageSocketPower,
|
||||
@@ -141,18 +145,18 @@ class FrameMeta:
|
||||
class ModelState(ModelStateBase):
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
|
||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool):
|
||||
ModelStateBase.__init__(self)
|
||||
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
||||
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(usbgpu)))
|
||||
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
|
||||
metadata = jits['metadata']
|
||||
self.input_shapes = metadata['input_shapes']
|
||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||
self.output_slices = metadata['output_slices']
|
||||
|
||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||
self.usbgpu = usbgpu
|
||||
self.chestnut = chestnut
|
||||
|
||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
@@ -168,7 +172,7 @@ class ModelState(ModelStateBase):
|
||||
return parsed_model_outputs
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
||||
inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
|
||||
for key in bufs.keys():
|
||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||
yuv_size = self.frame_buf_params[key][3]
|
||||
@@ -192,11 +196,11 @@ class ModelState(ModelStateBase):
|
||||
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:
|
||||
after_enqueue()
|
||||
model_output = outs.numpy()[0]
|
||||
if self.usbgpu and not np.all(np.isfinite(model_output)):
|
||||
# TODO remove with prev_feat
|
||||
cloudlog.error("model output not finite, dropping frame")
|
||||
return None
|
||||
if self.chestnut and not np.all(np.isfinite(model_output)):
|
||||
raise RuntimeError("model output not finite")
|
||||
outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices))
|
||||
self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']]
|
||||
|
||||
@@ -218,12 +222,12 @@ class ModelState(ModelStateBase):
|
||||
def main(demo=False):
|
||||
cloudlog.warning("modeld init")
|
||||
|
||||
USBGPU = usbgpu_present() and usbgpu_compiled()
|
||||
if USBGPU:
|
||||
CHESTNUT = chestnut_present() and chestnut_compiled()
|
||||
if CHESTNUT:
|
||||
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
||||
params = Params()
|
||||
params.put_bool("UsbGpuLoading", USBGPU)
|
||||
params.remove("UsbGpuActive")
|
||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||
params.remove("ChestnutActive")
|
||||
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
@@ -253,7 +257,7 @@ def main(demo=False):
|
||||
st = time.monotonic()
|
||||
cloudlog.warning("loading model")
|
||||
model = None
|
||||
if USBGPU:
|
||||
if CHESTNUT:
|
||||
big_model = None
|
||||
def load_big():
|
||||
nonlocal big_model
|
||||
@@ -267,23 +271,23 @@ def main(demo=False):
|
||||
loader.start()
|
||||
loader.join(BIG_MODEL_TIMEOUT)
|
||||
model = big_model
|
||||
params.put_bool("UsbGpuActive", model is not None)
|
||||
params.put_bool("ChestnutActive", model is not None)
|
||||
|
||||
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU 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:
|
||||
model = small_model
|
||||
params.put_bool("UsbGpuLoading", False)
|
||||
params.put_bool("ChestnutLoading", False)
|
||||
assert model is not None
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
|
||||
pm = PubMaster(pub_socks)
|
||||
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
|
||||
|
||||
publish_state = PublishState()
|
||||
params = Params()
|
||||
chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None
|
||||
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
|
||||
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
|
||||
@@ -393,13 +397,15 @@ def main(demo=False):
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
try:
|
||||
model_output = model.run(bufs, transforms, inputs)
|
||||
send_chestnut = (chestnut_state is not None and
|
||||
run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0)
|
||||
model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None)
|
||||
except Exception:
|
||||
if not params.get_bool("UsbGpuActive"):
|
||||
if not params.get_bool("ChestnutActive"):
|
||||
raise
|
||||
# fallback to small model
|
||||
cloudlog.exception("big model failed, fall back to small")
|
||||
params.put_bool("UsbGpuActive", False)
|
||||
params.put_bool("ChestnutActive", False)
|
||||
assert small_model is not None
|
||||
model = small_model
|
||||
if chestnut_state is not None:
|
||||
@@ -419,7 +425,7 @@ def main(demo=False):
|
||||
fill_model_msg(modelv2_send, model_output, action,
|
||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen)
|
||||
modelv2_send.modelV2.big = model.usbgpu
|
||||
modelv2_send.modelV2.big = model.chestnut
|
||||
|
||||
desire_state = modelv2_send.modelV2.meta.desireState
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
@@ -441,10 +447,6 @@ def main(demo=False):
|
||||
pm.send('modelDataV2SP', mdv2sp_send)
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
|
||||
if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0:
|
||||
chestnut_state.send()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import argparse
|
||||
|
||||
@@ -195,17 +195,17 @@ class SelfdriveD(CruiseHelper):
|
||||
self.events.add(EventName.joystickDebug)
|
||||
self.startup_event = None
|
||||
|
||||
loading = self.params.get_bool("UsbGpuLoading")
|
||||
loading = self.params.get_bool("ChestnutLoading")
|
||||
if self.big_model_loading and not loading:
|
||||
self.big_model_ready_t = time.monotonic()
|
||||
self.big_model_loading = loading
|
||||
if self.big_model_loading:
|
||||
self.events.add(EventName.bigModelLoading)
|
||||
|
||||
big_active = self.params.get("UsbGpuActive")
|
||||
usbgpu_present = self.sm['deviceState'].chestnutPresent
|
||||
big_active = self.params.get("ChestnutActive")
|
||||
chestnut_present = self.sm['deviceState'].chestnutPresent
|
||||
model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2']
|
||||
big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present)
|
||||
big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present)
|
||||
if big_failed and not self.big_model_failed:
|
||||
self.events.add(EventName.bigModelFailed)
|
||||
self.big_model_failed = big_failed
|
||||
|
||||
@@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.common.version import RELEASE_BRANCHES
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
@@ -139,8 +139,8 @@ class MiciHomeLayout(Widget):
|
||||
self._version_text = self._get_version_text()
|
||||
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37))
|
||||
self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37))
|
||||
self._chestnut_icon = IconWidget("icons_mici/chestnut_green.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._body_icon = IconWidget("icons_mici/body.png", (54, 37))
|
||||
|
||||
@@ -150,8 +150,8 @@ class MiciHomeLayout(Widget):
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
self._experimental_icon,
|
||||
self._egpu_icon,
|
||||
self._egpu_icon_gray,
|
||||
self._chestnut_icon,
|
||||
self._chestnut_failed_icon,
|
||||
self._body_icon,
|
||||
self._mic_icon,
|
||||
], spacing=18)
|
||||
@@ -249,10 +249,10 @@ class MiciHomeLayout(Widget):
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||
if gui_app.sunnypilot_ui():
|
||||
self._set_egpu_visibility()
|
||||
self._set_chestnut_visibility()
|
||||
else:
|
||||
self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled)
|
||||
self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled)
|
||||
self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
@@ -72,6 +73,9 @@ class MiciMainLayout(Scroller):
|
||||
# For scroll_to
|
||||
return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return True
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
@@ -122,13 +126,15 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
# 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:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._onroad_time_delay = None
|
||||
|
||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -107,8 +107,7 @@ class HudRenderer(Widget):
|
||||
self.speed: float = 0.0
|
||||
self.v_ego_cluster_seen: bool = False
|
||||
self._engaged: bool = False
|
||||
self._small_model_engaged: bool = False
|
||||
self._egpu_fade_time: float = 0
|
||||
self._chestnut_fade_time: float = 0
|
||||
|
||||
self._can_draw_top_icons = True
|
||||
self._show_wheel_critical = False
|
||||
@@ -124,17 +123,15 @@ class HudRenderer(Widget):
|
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50)
|
||||
self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50)
|
||||
self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44)
|
||||
self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44)
|
||||
self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44)
|
||||
self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44)
|
||||
self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52)
|
||||
self._egpu_icon: rl.Texture | None = None
|
||||
|
||||
self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44)
|
||||
self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44)
|
||||
self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44)
|
||||
self._chestnut_icon: rl.Texture | None = None
|
||||
self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||
|
||||
def set_wheel_critical_icon(self, critical: bool):
|
||||
"""Set the wheel icon to critical or normal state."""
|
||||
@@ -165,13 +162,10 @@ class HudRenderer(Widget):
|
||||
controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
)
|
||||
engaged = sm['selfdriveState'].enabled
|
||||
if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and
|
||||
ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame):
|
||||
self._small_model_engaged = True
|
||||
if engaged != self._engaged:
|
||||
self._egpu_fade_time = rl.get_time() if engaged else 0
|
||||
if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged):
|
||||
self._set_speed_changed_time = rl.get_time()
|
||||
if engaged != self._engaged:
|
||||
self._chestnut_fade_time = rl.get_time() if engaged else 0
|
||||
self._engaged = engaged
|
||||
self.set_speed = set_speed
|
||||
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
|
||||
@@ -191,8 +185,7 @@ class HudRenderer(Widget):
|
||||
if self.is_cruise_set:
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
if ui_state.usbgpu and ui_state.usbgpu_compiled:
|
||||
self._draw_model_source(rect)
|
||||
self._draw_model_source(rect)
|
||||
|
||||
self._draw_steering_wheel(rect)
|
||||
|
||||
@@ -200,30 +193,24 @@ class HudRenderer(Widget):
|
||||
if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame:
|
||||
return
|
||||
|
||||
big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or
|
||||
(ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and
|
||||
not ui_state.sm.alive['modelV2']) or
|
||||
(ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame))
|
||||
self._small_model_engaged &= big_failed
|
||||
loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed)
|
||||
loading = ui_state.chestnut_state == ChestnutState.LOADING
|
||||
if loading:
|
||||
pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0)
|
||||
icon = self._txt_egpu
|
||||
opacity = 0.35 + 0.65 * pulse
|
||||
elif self._small_model_engaged:
|
||||
icon = self._txt_egpu_crossed
|
||||
opacity = 0.65
|
||||
elif big_failed:
|
||||
icon = self._txt_egpu_orange
|
||||
icon = self._txt_chestnut
|
||||
opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED):
|
||||
icon = self._txt_chestnut_orange
|
||||
opacity = 1.0
|
||||
elif ui_state.chestnut_state == ChestnutState.ACTIVE:
|
||||
icon = self._txt_chestnut_green
|
||||
opacity = 1.0
|
||||
else:
|
||||
icon = self._txt_egpu_green
|
||||
opacity = 1.0
|
||||
return
|
||||
|
||||
if icon is not self._egpu_icon:
|
||||
self._egpu_fade_time = rl.get_time()
|
||||
self._egpu_icon = icon
|
||||
alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE)
|
||||
if icon is not self._chestnut_icon:
|
||||
self._chestnut_fade_time = rl.get_time()
|
||||
self._chestnut_icon = icon
|
||||
visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE
|
||||
alpha = self._chestnut_alpha_filter.update(visible)
|
||||
if alpha < 1e-2:
|
||||
return
|
||||
|
||||
|
||||
@@ -62,14 +62,14 @@ class ModelsLayout(Widget):
|
||||
self.big_model_item = ListItemSP(
|
||||
title=tr("Big Model"),
|
||||
action_item=ScrollingButtonAction(tr("SELECT")),
|
||||
callback=lambda: self._open_source_dialog("usbgpu")
|
||||
callback=lambda: self._open_source_dialog("chestnut")
|
||||
)
|
||||
|
||||
self.download_item = download_status_item(lambda: tr("Download") if self._downloading else tr("Model Status"))
|
||||
|
||||
self.refresh_item = button_item(tr("Refresh Model List"), tr("REFRESH"), "",
|
||||
lambda: (ui_state.params.put("ModelManager_LastSyncTime", 0),
|
||||
ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 0),
|
||||
ui_state.params.put("ModelManager_LastSyncTime_Chestnut", 0),
|
||||
gui_app.push_widget(alert_dialog(tr("Fetching Latest Models")))))
|
||||
|
||||
self.clear_cache_item = ListItemSP(
|
||||
@@ -177,14 +177,14 @@ class ModelsLayout(Widget):
|
||||
big_state = big_model_state()
|
||||
carry_source, carry_internal, _ = carrying_model()
|
||||
segments = []
|
||||
for source, label in (("qcom", tr("small")), ("usbgpu", tr("big"))):
|
||||
for source, label in (("qcom", tr("small")), ("chestnut", tr("big"))):
|
||||
if segments:
|
||||
segments.append(("|", rl.GRAY, None, None))
|
||||
bundle = get_selected_bundle(ui_state.params, source)
|
||||
name = bundle.internalName if bundle else default_model_name(source)
|
||||
color = ON_COLOR if (source == carry_source and name == carry_internal) else rl.LIGHTGRAY
|
||||
name = "● " + name
|
||||
if source == "usbgpu":
|
||||
if source == "chestnut":
|
||||
if big_state == 'failed':
|
||||
color = rl.RED
|
||||
elif big_state == 'loading':
|
||||
@@ -208,10 +208,10 @@ class ModelsLayout(Widget):
|
||||
"""The failover story for the Model Status row. One-way big -> small, and the
|
||||
fallback is runner-matched: a Default big can only fall back to the Default
|
||||
small (stock modeld), a custom big has no automatic fallback yet."""
|
||||
if not ui_state.usbgpu:
|
||||
if not ui_state.chestnut_present:
|
||||
return ""
|
||||
big_bundle = get_selected_bundle(ui_state.params, "usbgpu")
|
||||
big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu")
|
||||
big_bundle = get_selected_bundle(ui_state.params, "chestnut")
|
||||
big_name = big_bundle.internalName if big_bundle else default_model_name("chestnut")
|
||||
big_is_default = big_bundle is None
|
||||
fallback_name = default_model_name("qcom")
|
||||
state = big_model_state()
|
||||
@@ -225,7 +225,7 @@ class ModelsLayout(Widget):
|
||||
return tr("Getting the big model ready.")
|
||||
if big_is_default:
|
||||
return tr("{} will drive. If it fails during a drive, {} takes over until the next drive.").format(big_name, fallback_name)
|
||||
return tr("{} will drive when the eGPU is ready.").format(big_name)
|
||||
return tr("{} will drive when the chestnut is ready.").format(big_name)
|
||||
|
||||
@staticmethod
|
||||
def _download_row_state(progresses, name: str) -> dict:
|
||||
@@ -261,7 +261,7 @@ class ModelsLayout(Widget):
|
||||
ui_state.params.put("ModelManager_DownloadRef", selected_bundle.ref)
|
||||
|
||||
def _resolve_selected_bundle(self, ref):
|
||||
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
|
||||
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")}
|
||||
resolved = resolve_bundle_by_ref(ref, source_bundles)
|
||||
return resolved[0] if resolved else None
|
||||
|
||||
@@ -329,7 +329,7 @@ class ModelsLayout(Widget):
|
||||
self._handle_bundle_download_progress()
|
||||
|
||||
carry_source, _, carry_display = carrying_model()
|
||||
for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")):
|
||||
for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "chestnut")):
|
||||
bundle = get_selected_bundle(ui_state.params, item_source)
|
||||
name = bundle.displayName if bundle else default_model_name(item_source)
|
||||
color = ON_COLOR if (item_source == carry_source and name == carry_display) else style.ITEM_TEXT_VALUE_COLOR
|
||||
|
||||
@@ -9,7 +9,7 @@ import math
|
||||
import pyray as rl
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
@@ -21,8 +21,8 @@ METRIC_MARGIN = 30
|
||||
METRIC_START_Y = 300
|
||||
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
|
||||
EGPU_ICON_WIDTH = 180
|
||||
EGPU_ICON_HEIGHT = 133
|
||||
CHESTNUT_ICON_WIDTH = 180
|
||||
CHESTNUT_ICON_HEIGHT = 133
|
||||
|
||||
|
||||
# Color scheme
|
||||
@@ -59,10 +59,9 @@ class MetricData:
|
||||
class SidebarSP:
|
||||
def __init__(self):
|
||||
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
self._egpu_green_img = gui_app.texture("icons_mici/egpu_green.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
|
||||
self._egpu_default_img = gui_app.texture("icons_mici/egpu.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
|
||||
self._egpu_orange_img = gui_app.texture("icons_mici/egpu_orange.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
|
||||
self._egpu_gray_img = gui_app.texture("icons_mici/egpu_gray.png", EGPU_ICON_WIDTH, EGPU_ICON_HEIGHT)
|
||||
self._chestnut_green_img = gui_app.texture("icons_mici/chestnut_green.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
|
||||
self._chestnut_default_img = gui_app.texture("icons_mici/chestnut.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
|
||||
self._chestnut_orange_img = gui_app.texture("icons_mici/chestnut_orange.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
|
||||
|
||||
def _update_sunnylink_status(self):
|
||||
if not ui_state.params.get_bool("SunnylinkEnabled"):
|
||||
@@ -90,22 +89,17 @@ class SidebarSP:
|
||||
|
||||
def _get_home_icon(self, default_img: rl.Texture) -> tuple[rl.Texture, rl.Vector2, float]:
|
||||
default_pos = rl.Vector2(HOME_BTN.x, HOME_BTN.y)
|
||||
if not ui_state.sm["deviceState"].chestnutPresent:
|
||||
state = ui_state.chestnut_state
|
||||
if state == ChestnutState.DISCONNECTED:
|
||||
return default_img, default_pos, 1.0
|
||||
|
||||
big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
|
||||
big_model_failed = ui_state.started and ui_state.big_model_failed
|
||||
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
|
||||
|
||||
if loading:
|
||||
icon = self._egpu_default_img
|
||||
if state == ChestnutState.LOADING:
|
||||
icon = self._chestnut_default_img
|
||||
opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
elif big_model_selected and big_model_failed:
|
||||
icon, opacity = self._egpu_orange_img, 1.0
|
||||
elif big_model_selected:
|
||||
icon, opacity = self._egpu_green_img, 1.0
|
||||
elif state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED):
|
||||
icon, opacity = self._chestnut_orange_img, 1.0
|
||||
else:
|
||||
icon, opacity = self._egpu_gray_img, 1.0
|
||||
icon, opacity = self._chestnut_green_img, 1.0
|
||||
|
||||
x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2
|
||||
y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2
|
||||
|
||||
@@ -9,7 +9,7 @@ import math
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
@@ -19,35 +19,16 @@ class MiciHomeLayoutSP(MiciHomeLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._openpilot_label = UnifiedLabel("sunnypilot", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False)
|
||||
self._egpu_icon_default = IconWidget("icons_mici/egpu.png", (50, 37))
|
||||
self._egpu_icon_default.set_visible(False)
|
||||
self._egpu_icon_orange = IconWidget("icons_mici/egpu_orange.png", (50, 37))
|
||||
self._egpu_icon_orange.set_visible(False)
|
||||
gray_idx = self._status_bar_layout.widgets.index(self._egpu_icon_gray)
|
||||
self._status_bar_layout.widgets.insert(gray_idx + 1, self._egpu_icon_default)
|
||||
self._status_bar_layout.widgets.insert(gray_idx + 2, self._egpu_icon_orange)
|
||||
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)
|
||||
|
||||
def _set_egpu_visibility(self):
|
||||
chestnut = ui_state.sm["deviceState"].chestnutPresent
|
||||
if not chestnut:
|
||||
self._egpu_icon.set_visible(False)
|
||||
self._egpu_icon_default.set_visible(False)
|
||||
self._egpu_icon_orange.set_visible(False)
|
||||
self._egpu_icon_gray.set_visible(False)
|
||||
return
|
||||
|
||||
big_model_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
|
||||
big_model_failed = ui_state.started and ui_state.big_model_failed
|
||||
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
|
||||
|
||||
if loading:
|
||||
self._egpu_icon_default._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
|
||||
self._egpu_icon_default.set_visible(True)
|
||||
self._egpu_icon.set_visible(False)
|
||||
self._egpu_icon_orange.set_visible(False)
|
||||
self._egpu_icon_gray.set_visible(False)
|
||||
else:
|
||||
self._egpu_icon_default.set_visible(False)
|
||||
self._egpu_icon.set_visible(big_model_selected and not big_model_failed)
|
||||
self._egpu_icon_orange.set_visible(big_model_selected and big_model_failed)
|
||||
self._egpu_icon_gray.set_visible(not big_model_selected)
|
||||
def _set_chestnut_visibility(self):
|
||||
# stock has no loading tier: it shows green from the moment a big model is available. keep the
|
||||
# pulse so the status bar and the onroad HUD agree on what loading looks like.
|
||||
loading = ui_state.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._chestnut_icon.set_visible(not loading and ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))
|
||||
self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class MiciMainLayoutSP(MiciMainLayout):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
scroller = self._scroller
|
||||
scroller.scroll_panel = GuiScrollPanel2SP(scroller._horizontal, handle_out_of_bounds=not scroller._snap_items)
|
||||
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return not self._onroad_layout.is_on_info_panel()
|
||||
@@ -27,14 +27,14 @@ def _model_info() -> tuple[str, str, str]:
|
||||
state = big_model_state()
|
||||
_, _, carry_display = carrying_model()
|
||||
if carry_display is None:
|
||||
big = get_selected_bundle(ui_state.params, "usbgpu")
|
||||
carry_display = big.displayName if big else default_model_name("usbgpu")
|
||||
big = get_selected_bundle(ui_state.params, "chestnut")
|
||||
carry_display = big.displayName if big else default_model_name("chestnut")
|
||||
active_text = (carry_display or active_name).lower()
|
||||
if state == 'failed':
|
||||
return active_text, tr("big model"), tr("unavailable")
|
||||
if state == 'loading':
|
||||
return active_text, tr("big model"), tr("getting ready")
|
||||
header = tr("small model") if source == "usbgpu" else tr("big model")
|
||||
header = tr("small model") if source == "chestnut" else tr("big model")
|
||||
return active_text, header, other_name.lower()
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
|
||||
hardware_btns = []
|
||||
active = active_source()
|
||||
for source, label in (("qcom", tr("small models")), ("usbgpu", tr("big models"))):
|
||||
for source, label in (("qcom", tr("small models")), ("chestnut", tr("big models"))):
|
||||
bundle = get_selected_bundle(ui_state.params, source)
|
||||
value = (bundle.internalName if bundle else default_model_name(source)).lower()
|
||||
if source == active:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroller_sp import ScrollerSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.augmented_road_view import AugmentedRoadViewSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad_info_panel import OnroadInfoPanel
|
||||
|
||||
CONFIDENCE_BALL_VISIBLE_RATIO = 0.4
|
||||
HORIZONTAL_SETTLE_PX = 5
|
||||
HORIZONTAL_RESET_RATIO = 0.5
|
||||
|
||||
|
||||
class OnroadViewContainerSP(ScrollerSP):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__(horizontal=False, snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
|
||||
self.road_view = AugmentedRoadViewSP(bookmark_callback=bookmark_callback)
|
||||
self.onroad_info_panel = OnroadInfoPanel(bookmark_callback=bookmark_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self.road_view,
|
||||
self.onroad_info_panel,
|
||||
])
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
self._scroller.set_scrolling_enabled(lambda: abs(self.rect.x) < HORIZONTAL_SETTLE_PX)
|
||||
|
||||
for child in (self.road_view, self.onroad_info_panel):
|
||||
inner_touch_valid = child._touch_valid_callback
|
||||
child.set_touch_valid_callback(
|
||||
lambda inner=inner_touch_valid: self._touch_valid() and (inner() if inner else True)
|
||||
)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle):
|
||||
super().set_rect(rect)
|
||||
self.road_view.set_rect(rect)
|
||||
self.onroad_info_panel.set_rect(rect)
|
||||
return self
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self.road_view.is_swiping_left() or self.onroad_info_panel.is_swiping_left()
|
||||
|
||||
def set_click_callback(self, click_callback: Callable[[], None] | None) -> None:
|
||||
self.road_view.set_click_callback(click_callback)
|
||||
self.onroad_info_panel.set_click_callback(click_callback)
|
||||
|
||||
def is_on_info_panel(self) -> bool:
|
||||
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
|
||||
to skip auto-pop-back-to-camera while user is reading the info panel)."""
|
||||
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
vertical_offset = self._scroller.scroll_panel.get_offset()
|
||||
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
|
||||
self.road_view.set_show_confidence_ball(show_ball)
|
||||
|
||||
super()._render(rect)
|
||||
@@ -0,0 +1,403 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
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
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import BookmarkIcon
|
||||
|
||||
METER_TO_KM = 0.001
|
||||
METER_TO_MILE = 0.000621371
|
||||
|
||||
CONTENT_MARGIN = 16
|
||||
SPEED_LIMIT_SIGN_WIDTH = 146
|
||||
VIENNA_SIGN_SIZE = 146
|
||||
MUTCD_SIGN_HEIGHT = 178
|
||||
OFFSET_BADGE_SIZE = 50
|
||||
OFFSET_BADGE_PANEL_PADDING = 4
|
||||
MUTCD_OFFSET_SIGN_Y_SHIFT = 6
|
||||
VIENNA_BADGE_X_RATIO = 0.80
|
||||
VIENNA_BADGE_UPCOMING_X_RATIO = 0.70
|
||||
VIENNA_BADGE_Y_RATIO = -0.82
|
||||
UPCOMING_SIGN_SIZE_RATIO = 0.76
|
||||
UPCOMING_SIGN_OVERLAP_RATIO = 0.05
|
||||
UNIT_FONT_SIZE = 40
|
||||
SPEED_FONT_SIZE = 114
|
||||
ROAD_FONT_SIZE = 32
|
||||
SCC_TAG_WIDTH = 78
|
||||
SCC_TAG_HEIGHT = 30
|
||||
SCC_TAG_GAP = 5
|
||||
COLUMN_GAP = 12
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnroadInfoPanelColors:
|
||||
white: rl.Color = rl.WHITE
|
||||
black: rl.Color = rl.BLACK
|
||||
red: rl.Color = field(default_factory=lambda: rl.Color(255, 0, 0, 255))
|
||||
green: rl.Color = field(default_factory=lambda: rl.Color(0, 255, 0, 255))
|
||||
grey: rl.Color = field(default_factory=lambda: rl.Color(190, 195, 190, 255))
|
||||
light_grey: rl.Color = field(default_factory=lambda: rl.Color(200, 200, 200, 255))
|
||||
dark_grey: rl.Color = field(default_factory=lambda: rl.Color(100, 100, 100, 255))
|
||||
bg_dark: rl.Color = field(default_factory=lambda: rl.Color(0, 0, 0, 255))
|
||||
card_bg: rl.Color = field(default_factory=lambda: rl.Color(50, 50, 50, 200))
|
||||
badge_bg: rl.Color = field(default_factory=lambda: rl.Color(60, 60, 60, 255))
|
||||
|
||||
|
||||
COLORS = OnroadInfoPanelColors()
|
||||
|
||||
|
||||
class OnroadInfoPanel(Widget):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__()
|
||||
self.speed_limit: float = 0.0
|
||||
self.speed_limit_valid: bool = False
|
||||
self.speed_limit_offset: float = 0.0
|
||||
self.next_speed_limit: float = 0.0
|
||||
self.next_speed_limit_distance: float = 0.0
|
||||
self.road_name: str = ""
|
||||
self.current_speed: float = 0.0
|
||||
self.set_speed: float = 0.0
|
||||
self.cruise_enabled: bool = False
|
||||
|
||||
self._sign_slide: float = 0.0
|
||||
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._marquee_offset: float = 0.0
|
||||
self._marquee_direction: int = 1
|
||||
self._marquee_pause_timer: float = 0.0
|
||||
self._marquee_speed: float = 40.0
|
||||
self._marquee_pause_duration: float = 1.5
|
||||
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._alert_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._bookmark_icon = BookmarkIcon(bookmark_callback)
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Mirror stock AugmentedRoadView: suppress click while bookmark gesture active
|
||||
if not self._bookmark_icon.interacting():
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
sm = ui_state.sm
|
||||
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
if sm.valid["longitudinalPlanSP"]:
|
||||
lp_sp = sm["longitudinalPlanSP"]
|
||||
resolver = lp_sp.speedLimit.resolver
|
||||
self.speed_limit = resolver.speedLimit * speed_conv
|
||||
self.speed_limit_valid = resolver.speedLimitValid
|
||||
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
|
||||
|
||||
if sm.valid["liveMapDataSP"]:
|
||||
lmd = sm["liveMapDataSP"]
|
||||
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
|
||||
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
|
||||
self.road_name = lmd.roadName
|
||||
|
||||
if sm.updated["carState"]:
|
||||
self.current_speed = sm["carState"].vEgo * speed_conv
|
||||
|
||||
if sm.valid["carState"] and sm.valid["controlsState"]:
|
||||
self.cruise_enabled = sm["carState"].cruiseState.enabled
|
||||
v_cruise_cluster = sm["carState"].vCruiseCluster
|
||||
set_speed_kph = sm["controlsState"].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
self.set_speed = set_speed_kph * (METER_TO_MILE / METER_TO_KM) if not ui_state.is_metric else set_speed_kph
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
self._update_state()
|
||||
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
|
||||
|
||||
left_x = rect.x + CONTENT_MARGIN
|
||||
|
||||
if self.cruise_enabled:
|
||||
unit = tr("MAX")
|
||||
display_speed = self.set_speed
|
||||
else:
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
|
||||
display_speed = self.current_speed
|
||||
|
||||
display_speed_text = str(round(display_speed))
|
||||
if self.speed_limit_valid and display_speed > self.speed_limit:
|
||||
speed_color = COLORS.red
|
||||
else:
|
||||
speed_color = COLORS.white
|
||||
|
||||
sign_width = min(SPEED_LIMIT_SIGN_WIDTH, rect.width * 0.30)
|
||||
sign_height = VIENNA_SIGN_SIZE if ui_state.is_metric else MUTCD_SIGN_HEIGHT
|
||||
|
||||
has_upcoming_limit = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
|
||||
target_sign_slide = 1.0 if has_upcoming_limit else 0.0
|
||||
slide_speed = 3.0 * rl.get_frame_time()
|
||||
if self._sign_slide < target_sign_slide:
|
||||
self._sign_slide = min(self._sign_slide + slide_speed, target_sign_slide)
|
||||
elif self._sign_slide > target_sign_slide:
|
||||
self._sign_slide = max(self._sign_slide - slide_speed, target_sign_slide)
|
||||
|
||||
upcoming_width = int(sign_width * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_height = int(sign_height * UPCOMING_SIGN_SIZE_RATIO)
|
||||
upcoming_reserved_width = int(upcoming_width * 0.85) + 5
|
||||
sign_x_without_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN
|
||||
sign_x_with_upcoming = rect.x + rect.width - sign_width - CONTENT_MARGIN - upcoming_reserved_width
|
||||
sign_x = sign_x_without_upcoming + (sign_x_with_upcoming - sign_x_without_upcoming) * self._sign_slide
|
||||
sign_y = rect.y + (rect.height - sign_height) / 2
|
||||
if not ui_state.is_metric and self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
sign_y += MUTCD_OFFSET_SIGN_Y_SHIFT
|
||||
|
||||
readout_right = sign_x - COLUMN_GAP
|
||||
readout_width = max(1, readout_right - left_x)
|
||||
road_y = rect.y + rect.height - 44
|
||||
|
||||
unit_font_size = self._fit_font_size(self._font_semi_bold, unit, readout_width, 46, UNIT_FONT_SIZE, 28)
|
||||
speed_font_size = self._fit_font_size(self._font_bold, display_speed_text, readout_width, road_y - (rect.y + 54) - 8,
|
||||
SPEED_FONT_SIZE, 76)
|
||||
speed_size = measure_text_cached(self._font_bold, display_speed_text, speed_font_size)
|
||||
speed_y = min(rect.y + 54, road_y - speed_size.y - 8)
|
||||
unit_y = max(rect.y + 14, speed_y - unit_font_size - 6)
|
||||
|
||||
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, unit_y), unit_font_size, 0, COLORS.grey)
|
||||
rl.draw_text_ex(self._font_bold, display_speed_text, rl.Vector2(left_x, speed_y), speed_font_size, 0, speed_color)
|
||||
self._draw_road_name(left_x, road_y, readout_width)
|
||||
|
||||
if has_upcoming_limit and self._sign_slide > 0.01:
|
||||
upcoming_speed_text = str(round(self.next_speed_limit))
|
||||
distance_text = self._format_distance(self.next_speed_limit_distance)
|
||||
upcoming_x = sign_x + sign_width - int(upcoming_width * UPCOMING_SIGN_OVERLAP_RATIO)
|
||||
upcoming_y = sign_y + (sign_height - upcoming_height) / 2
|
||||
|
||||
upcoming_speed_color = COLORS.black
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
else:
|
||||
self._draw_mutcd_sign(upcoming_x, upcoming_y, upcoming_width, upcoming_height, upcoming_speed_text, upcoming_speed_color, is_upcoming=True)
|
||||
|
||||
distance_font_size = self._fit_font_size(self._font_medium, distance_text, upcoming_width, 30, 24, 16)
|
||||
distance_size = measure_text_cached(self._font_medium, distance_text, distance_font_size)
|
||||
rl.draw_text_ex(self._font_medium, distance_text, rl.Vector2(upcoming_x + upcoming_width / 2 - distance_size.x / 2, upcoming_y + upcoming_height),
|
||||
distance_font_size, 0, COLORS.grey)
|
||||
|
||||
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
|
||||
|
||||
if self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
offset_text = str(abs(round(self.speed_limit_offset)))
|
||||
badge_size = OFFSET_BADGE_SIZE
|
||||
badge_rect = self._offset_badge_rect(rect, sign_x, sign_y, sign_width, sign_height, badge_size, has_upcoming_limit)
|
||||
|
||||
if ui_state.is_metric:
|
||||
badge_radius = badge_size / 2
|
||||
badge_center_x = badge_rect.x + badge_radius
|
||||
badge_center_y = badge_rect.y + badge_radius
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius + 2, COLORS.dark_grey)
|
||||
rl.draw_circle(int(badge_center_x), int(badge_center_y), badge_radius, COLORS.badge_bg)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_center_x, badge_center_y), COLORS.white,
|
||||
badge_size - 10, badge_size - 8, min_size=24)
|
||||
else:
|
||||
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, COLORS.badge_bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
|
||||
self._draw_text_centered_fit(self._font_bold, offset_text, 32, rl.Vector2(badge_rect.x + badge_size / 2, badge_rect.y + badge_size / 2),
|
||||
COLORS.white, badge_size - 10, badge_size - 8, min_size=24)
|
||||
|
||||
scc_tag_x = min(left_x + speed_size.x + COLUMN_GAP, readout_right - SCC_TAG_WIDTH)
|
||||
scc_tag_y = speed_y + (speed_size.y - (SCC_TAG_HEIGHT * 2 + SCC_TAG_GAP)) / 2
|
||||
if scc_tag_x >= left_x + speed_size.x + 8:
|
||||
self._draw_scc_icons(scc_tag_x, scc_tag_y, readout_right)
|
||||
|
||||
self._bookmark_icon.render(rect)
|
||||
|
||||
if ui_state.started:
|
||||
alert_obj, no_alert = self._alert_renderer.will_render()
|
||||
self._alert_alpha_filter.update(0 if no_alert else 1)
|
||||
alpha = self._alert_alpha_filter.x
|
||||
if alpha > 0.01:
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, int(150 * alpha)))
|
||||
self._alert_renderer.render(rect)
|
||||
|
||||
def _draw_scc_icons(self, x: float, y: float, right_limit: float) -> None:
|
||||
sm = ui_state.sm
|
||||
if not sm.valid["longitudinalPlanSP"]:
|
||||
return
|
||||
scc = sm["longitudinalPlanSP"].smartCruiseControl
|
||||
|
||||
drawn = 0
|
||||
|
||||
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
|
||||
if not active:
|
||||
continue
|
||||
tag_x = x
|
||||
if tag_x + SCC_TAG_WIDTH > right_limit:
|
||||
return
|
||||
tag_y = y + drawn * (SCC_TAG_HEIGHT + SCC_TAG_GAP)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(tag_x, tag_y, SCC_TAG_WIDTH, SCC_TAG_HEIGHT), 0.3, 10, COLORS.green)
|
||||
self._draw_text_centered_fit(self._font_bold, label, 18, rl.Vector2(tag_x + SCC_TAG_WIDTH / 2, tag_y + SCC_TAG_HEIGHT / 2), COLORS.black,
|
||||
SCC_TAG_WIDTH - 10, SCC_TAG_HEIGHT - 4, min_size=14)
|
||||
drawn += 1
|
||||
|
||||
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
|
||||
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid and self.speed_limit > 0 else "--"
|
||||
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
|
||||
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
else:
|
||||
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
|
||||
def _draw_road_name(self, x: float, y: float, width: float) -> None:
|
||||
if width <= 0:
|
||||
return
|
||||
|
||||
road_display = self.road_name if self.road_name else "--"
|
||||
font_size = self._fit_font_size(self._font_semi_bold, road_display, width, 38, ROAD_FONT_SIZE, 28)
|
||||
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
|
||||
text_width = road_size.x
|
||||
|
||||
if text_width <= width:
|
||||
self._marquee_offset = 0.0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = 0.0
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
|
||||
else:
|
||||
overflow = text_width - width
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
if self._marquee_pause_timer > 0:
|
||||
self._marquee_pause_timer -= dt
|
||||
else:
|
||||
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
|
||||
|
||||
if self._marquee_offset >= overflow:
|
||||
self._marquee_offset = overflow
|
||||
self._marquee_direction = -1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
elif self._marquee_offset <= 0:
|
||||
self._marquee_offset = 0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
|
||||
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
|
||||
text_pos = rl.Vector2(x - self._marquee_offset, y)
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
center = rl.Vector2(x + width / 2, y + height / 2)
|
||||
outer_radius = min(width, height) / 2
|
||||
|
||||
rl.draw_circle_v(center, outer_radius, COLORS.white)
|
||||
ring_width = outer_radius * 0.18
|
||||
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
|
||||
|
||||
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, int(font_size), center, speed_color, width * 0.72, height * 0.50, min_size=24)
|
||||
|
||||
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
sign_rect = rl.Rectangle(x, y, width, height)
|
||||
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
|
||||
|
||||
inset = max(4, width * 0.05)
|
||||
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
|
||||
outer_radius = 0.35 * width / 2.0
|
||||
inner_radius = outer_radius - inset
|
||||
inner_roundness = inner_radius / (inner_rect.width / 2.0)
|
||||
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
|
||||
|
||||
mid_x = x + width / 2
|
||||
label_size = max(18, int(width * 0.26))
|
||||
if is_upcoming:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("AHEAD"), int(width * 0.34), rl.Vector2(mid_x, y + height * 0.28), COLORS.black,
|
||||
width * 0.94, height * 0.32, min_size=20)
|
||||
else:
|
||||
self._draw_text_centered_fit(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
self._draw_text_centered_fit(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black,
|
||||
width * 0.84, height * 0.24, min_size=16)
|
||||
|
||||
speed_font_size = int(width * 0.60) if len(speed_str) >= 3 else int(width * 0.72)
|
||||
self._draw_text_centered_fit(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color,
|
||||
width * 0.90, height * 0.52, min_size=32)
|
||||
|
||||
def _draw_text_centered(self, font, text, size, pos_center, color):
|
||||
sz = measure_text_cached(font, text, size)
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
|
||||
|
||||
def _draw_text_centered_fit(self, font, text, size, pos_center, color, max_width: float, max_height: float, min_size: int = 10):
|
||||
size = self._fit_font_size(font, text, max_width, max_height, size, min_size)
|
||||
self._draw_text_centered(font, text, size, pos_center, color)
|
||||
|
||||
def _fit_font_size(self, font, text: str, max_width: float, max_height: float, max_size: int | float, min_size: int) -> int:
|
||||
size = int(max_size)
|
||||
while size > min_size:
|
||||
text_size = measure_text_cached(font, text, size)
|
||||
if text_size.x <= max_width and text_size.y <= max_height:
|
||||
return size
|
||||
size -= 2
|
||||
return min_size
|
||||
|
||||
def _offset_badge_rect(self, panel_rect: rl.Rectangle, sign_x: float, sign_y: float, sign_width: float, sign_height: float,
|
||||
badge_size: float, has_upcoming_limit: bool) -> rl.Rectangle:
|
||||
if ui_state.is_metric:
|
||||
radius = min(sign_width, sign_height) / 2
|
||||
center_x = sign_x + sign_width / 2
|
||||
center_y = sign_y + sign_height / 2
|
||||
badge_x_ratio = VIENNA_BADGE_UPCOMING_X_RATIO if has_upcoming_limit else VIENNA_BADGE_X_RATIO
|
||||
badge_center_x = center_x + radius * badge_x_ratio
|
||||
badge_center_y = center_y + radius * VIENNA_BADGE_Y_RATIO
|
||||
badge_x = badge_center_x - badge_size / 2
|
||||
badge_y = badge_center_y - badge_size / 2
|
||||
else:
|
||||
badge_x = sign_x + sign_width - badge_size * 0.45
|
||||
badge_y = sign_y - badge_size * 0.75
|
||||
|
||||
return rl.Rectangle(
|
||||
self._clamp(
|
||||
badge_x,
|
||||
panel_rect.x + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.x + panel_rect.width - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
self._clamp(
|
||||
badge_y,
|
||||
panel_rect.y + OFFSET_BADGE_PANEL_PADDING,
|
||||
panel_rect.y + panel_rect.height - badge_size - OFFSET_BADGE_PANEL_PADDING,
|
||||
),
|
||||
badge_size,
|
||||
badge_size,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _clamp(value: float, min_value: float, max_value: float) -> float:
|
||||
return max(min_value, min(max_value, value))
|
||||
|
||||
def _format_distance(self, distance: float) -> str:
|
||||
if ui_state.is_metric:
|
||||
if distance < 50:
|
||||
return tr("Near")
|
||||
if distance >= 1000:
|
||||
return f"{distance * METER_TO_KM:.1f}" + tr("km")
|
||||
if distance < 200:
|
||||
rounded = max(10, int(distance / 10) * 10)
|
||||
else:
|
||||
rounded = int(distance / 100) * 100
|
||||
return str(rounded) + tr("m")
|
||||
else:
|
||||
distance_mi = distance * METER_TO_MILE
|
||||
if distance_mi < 0.1:
|
||||
return tr("Near")
|
||||
return f"{distance_mi:.1f}" + tr("mi")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||
|
||||
|
||||
class _SuppressedConfidenceBall:
|
||||
def render(self, *_):
|
||||
pass
|
||||
|
||||
|
||||
class AugmentedRoadViewSP(AugmentedRoadView):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._show_confidence_ball: bool = True
|
||||
self._real_confidence_ball = self._confidence_ball
|
||||
self._confidence_ball = _SuppressedConfidenceBall()
|
||||
|
||||
def set_show_confidence_ball(self, show: bool) -> None:
|
||||
self._show_confidence_ball = show
|
||||
|
||||
def _render(self, _) -> None:
|
||||
super()._render(_)
|
||||
if self._show_confidence_ball:
|
||||
self._real_confidence_ball.render(self.rect)
|
||||
@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators
|
||||
|
||||
|
||||
@@ -22,8 +21,6 @@ class HudRendererSP(HudRenderer):
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
super()._render(rect)
|
||||
if ui_state.usbgpu and not ui_state.usbgpu_compiled and ui_state.model_runner_tinygrad:
|
||||
self._draw_model_source(rect)
|
||||
self.blind_spot_indicators.render(rect)
|
||||
|
||||
def _has_blind_spot_detected(self) -> bool:
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import MouseEvent, MousePos, gui_app
|
||||
from openpilot.system.ui.lib.scroll_panel2 import ScrollState
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets import scroller as scroller_mod
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class DummyScrollIndicator:
|
||||
def update(self, *_) -> None:
|
||||
pass
|
||||
|
||||
def render(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class DummyWidget(Widget):
|
||||
def __init__(self, rect: rl.Rectangle):
|
||||
super().__init__()
|
||||
self.set_rect(rect)
|
||||
|
||||
def _render(self, _) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _mouse_event(x: float, y: float, *, pressed: bool = False, released: bool = False,
|
||||
down: bool = True, t: float = 0.0) -> MouseEvent:
|
||||
return MouseEvent(MousePos(x, y), 0, pressed, released, down, t)
|
||||
|
||||
|
||||
def test_vertical_snap_items_are_supported(monkeypatch):
|
||||
monkeypatch.setattr(scroller_mod, "ScrollIndicator", DummyScrollIndicator)
|
||||
|
||||
scroller = scroller_mod._Scroller([], horizontal=False, snap_items=True, scroll_indicator=False)
|
||||
scroller.set_rect(rl.Rectangle(0, 0, 100, 100))
|
||||
scroller.scroll_panel.set_offset(-60)
|
||||
|
||||
captured_snap_target = None
|
||||
|
||||
def update(_, __, snap_target=None):
|
||||
nonlocal captured_snap_target
|
||||
captured_snap_target = snap_target
|
||||
return scroller.scroll_panel.get_offset()
|
||||
|
||||
monkeypatch.setattr(scroller.scroll_panel, "update", update)
|
||||
|
||||
visible_items: list[Widget] = [
|
||||
DummyWidget(rl.Rectangle(0, -60, 100, 100)),
|
||||
DummyWidget(rl.Rectangle(0, 40, 100, 100)),
|
||||
]
|
||||
scroller._get_scroll(visible_items, 200)
|
||||
|
||||
assert captured_snap_target == -100
|
||||
|
||||
|
||||
def test_scroll_panel_sp_rejects_orthogonal_drags(monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=True)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(10, 10, pressed=True, t=1.0)])
|
||||
panel.update(bounds, 200)
|
||||
assert panel.state == ScrollState.PRESSED
|
||||
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [_mouse_event(23, 60, t=1.1)])
|
||||
panel.update(bounds, 200)
|
||||
|
||||
assert panel.state == ScrollState.STEADY
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
|
||||
def test_scroll_panel_sp_can_disable_out_of_bounds_handling(monkeypatch):
|
||||
panel = GuiScrollPanel2SP(horizontal=False, handle_out_of_bounds=False)
|
||||
bounds = rl.Rectangle(0, 0, 100, 100)
|
||||
monkeypatch.setattr(gui_app, "_mouse_events", [])
|
||||
|
||||
panel.set_offset(20)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == 0
|
||||
|
||||
panel.set_offset(-150)
|
||||
panel.update(bounds, 200)
|
||||
assert panel.get_offset() == -100
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import MouseEvent
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
|
||||
|
||||
|
||||
class GuiScrollPanel2SP(GuiScrollPanel2):
|
||||
"""Scroll panel behavior for nested Mici pagers."""
|
||||
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
super().__init__(horizontal, handle_out_of_bounds=handle_out_of_bounds)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
state_before_update = self._state
|
||||
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
|
||||
if self._state == ScrollState.MANUAL_SCROLL and state_before_update == ScrollState.PRESSED and \
|
||||
self._initial_click_event is not None:
|
||||
drag_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
|
||||
drag_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
|
||||
primary_drag = drag_x if self._horizontal else drag_y
|
||||
cross_drag = drag_y if self._horizontal else drag_x
|
||||
if cross_drag > primary_drag:
|
||||
self._state = ScrollState.STEADY
|
||||
self._velocity = 0.0
|
||||
self._velocity_buffer.clear()
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class ScrollerSP(Scroller):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
inner = self._scroller
|
||||
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
|
||||
@@ -4,15 +4,15 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState
|
||||
from openpilot.sunnypilot.models.fetcher import get_cached_bundles
|
||||
from openpilot.sunnypilot.models.helpers import get_active_source, get_selected_bundle, resolve_bundle_by_ref
|
||||
from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MODEL
|
||||
|
||||
|
||||
def active_source() -> str:
|
||||
return get_active_source(usbgpu=ui_state.usbgpu,
|
||||
usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_loading,
|
||||
return get_active_source(chestnut=ui_state.chestnut_present,
|
||||
chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading,
|
||||
offroad=ui_state.is_offroad())
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def bundles_for_source(source: str):
|
||||
|
||||
|
||||
def default_model(source: str) -> str:
|
||||
return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL
|
||||
return DEFAULT_BIG_MODEL if source == 'chestnut' else DEFAULT_MODEL
|
||||
|
||||
|
||||
def default_model_name(source: str) -> str:
|
||||
@@ -31,13 +31,10 @@ def default_model_name(source: str) -> str:
|
||||
|
||||
|
||||
def big_model_state() -> str | None:
|
||||
"""'failed' | 'loading' | None, mirroring the sidebar's detection (#1969)."""
|
||||
if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed:
|
||||
return 'failed'
|
||||
big_selected = ui_state.usbgpu_compiled or ui_state.model_runner_tinygrad
|
||||
if ui_state.usbgpu_loading or (big_selected and ui_state.started and ui_state.usbgpu_active is None):
|
||||
return 'loading'
|
||||
return None
|
||||
"""'failed' | 'loading' | None, from the same state the icons render."""
|
||||
return {ChestnutState.UNCOMPILED: 'failed',
|
||||
ChestnutState.FAILED: 'failed',
|
||||
ChestnutState.LOADING: 'loading'}.get(ui_state.chestnut_state)
|
||||
|
||||
|
||||
def carrying_model() -> tuple[str | None, str | None, str | None]:
|
||||
@@ -45,14 +42,14 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
|
||||
when a Default big cannot carry, stock modeld runs the Default small, never the
|
||||
small slot's pick; a custom big has no automatic fallback yet -> (None, None, None)."""
|
||||
source = active_source()
|
||||
if source == "usbgpu":
|
||||
bundle = get_selected_bundle(ui_state.params, "usbgpu")
|
||||
if source == "chestnut":
|
||||
bundle = get_selected_bundle(ui_state.params, "chestnut")
|
||||
if bundle:
|
||||
return "usbgpu", bundle.internalName, bundle.displayName
|
||||
name = default_model_name("usbgpu")
|
||||
return "usbgpu", name, name
|
||||
if ui_state.usbgpu:
|
||||
if get_selected_bundle(ui_state.params, "usbgpu") is None:
|
||||
return "chestnut", bundle.internalName, bundle.displayName
|
||||
name = default_model_name("chestnut")
|
||||
return "chestnut", name, name
|
||||
if ui_state.chestnut_present:
|
||||
if get_selected_bundle(ui_state.params, "chestnut") is None:
|
||||
name = default_model_name("qcom")
|
||||
return "qcom", name, name
|
||||
return None, None, None
|
||||
@@ -66,7 +63,7 @@ def carrying_model() -> tuple[str | None, str | None, str | None]:
|
||||
def queued_name(current_ref) -> str | None:
|
||||
ref = ui_state.params.get("ModelManager_DownloadRef")
|
||||
if ref and ref != current_ref:
|
||||
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
|
||||
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "chestnut")}
|
||||
if resolved := resolve_bundle_by_ref(ref, source_bundles):
|
||||
return resolved[0].internalName
|
||||
return None
|
||||
@@ -79,7 +76,7 @@ def model_info() -> tuple[str, str, str]:
|
||||
manager republishes a tick after a chestnut change, so the stale bundle
|
||||
would flash the wrong model."""
|
||||
source = active_source()
|
||||
other = "qcom" if source == "usbgpu" else "usbgpu"
|
||||
other = "qcom" if source == "chestnut" else "chestnut"
|
||||
active_bundle = get_selected_bundle(ui_state.params, source)
|
||||
other_bundle = get_selected_bundle(ui_state.params, other)
|
||||
|
||||
|
||||
@@ -152,10 +152,13 @@ class UIStateSP:
|
||||
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
|
||||
|
||||
self._enforce_constraints()
|
||||
source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active,
|
||||
usbgpu_loading=self.usbgpu_loading, offroad=self.is_offroad())
|
||||
source = get_active_source(chestnut=self.chestnut_present, chestnut_active=self.chestnut_active,
|
||||
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
|
||||
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
|
||||
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
|
||||
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
|
||||
# chestnut just the same, so ChestnutState has to see it as available too.
|
||||
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
|
||||
self.blindspot = self.params.get_bool("BlindSpot")
|
||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
|
||||
|
||||
@@ -10,6 +10,9 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.main import MiciMainLayoutSP as MiciMainLayout
|
||||
|
||||
BIG_UI = gui_app.big_ui()
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.hardware import HARDWARE, PC
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_compiled
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_compiled
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||
|
||||
@@ -28,6 +28,15 @@ class UIStatus(Enum):
|
||||
LONG_ONLY = "long_only"
|
||||
|
||||
|
||||
class ChestnutState(Enum):
|
||||
DISCONNECTED = "disconnected"
|
||||
UNCOMPILED = "uncompiled"
|
||||
READY = "ready"
|
||||
LOADING = "loading"
|
||||
ACTIVE = "active"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class UIState(UIStateSP):
|
||||
_instance: 'UIState | None' = None
|
||||
|
||||
@@ -82,10 +91,11 @@ class UIState(UIStateSP):
|
||||
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
||||
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
||||
self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed")
|
||||
self.usbgpu: bool = False
|
||||
self.usbgpu_compiled: bool = usbgpu_compiled()
|
||||
self.usbgpu_active: bool | None = self.params.get("UsbGpuActive")
|
||||
self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading")
|
||||
self.chestnut_present: bool = False
|
||||
self.chestnut_compiled: bool = chestnut_compiled()
|
||||
self.chestnut_active: bool | None = None
|
||||
self.chestnut_loading: bool = False
|
||||
self.chestnut_state = ChestnutState.DISCONNECTED
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
self.recording_audio: bool = False
|
||||
@@ -112,15 +122,6 @@ class UIState(UIStateSP):
|
||||
def add_on_body_changed_callbacks(self, callback: Callable[[], None]):
|
||||
self._on_body_changed_callbacks.append(callback)
|
||||
|
||||
@property
|
||||
def big_model_failed(self) -> bool:
|
||||
# Mirrors the onroad HUD's four-condition check so sidebar and home icons reflect the same failure states
|
||||
return (self.usbgpu_active is False or
|
||||
not self.sm['deviceState'].chestnutPresent or
|
||||
(self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and
|
||||
not self.sm.alive['modelV2']) or
|
||||
(self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame))
|
||||
|
||||
@property
|
||||
def engaged(self) -> bool:
|
||||
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
|
||||
@@ -140,6 +141,7 @@ class UIState(UIStateSP):
|
||||
self.sm.update(0)
|
||||
self._update_state()
|
||||
self._update_status()
|
||||
self._update_chestnut_state()
|
||||
device.update()
|
||||
UIStateSP.update(self)
|
||||
|
||||
@@ -203,12 +205,35 @@ class UIState(UIStateSP):
|
||||
self.status = UIStatus.DISENGAGED
|
||||
self.started_frame = self.sm.frame
|
||||
self.started_time = time.monotonic()
|
||||
self.chestnut_present = self.sm["deviceState"].chestnutPresent
|
||||
|
||||
for callback in self._offroad_transition_callbacks:
|
||||
callback()
|
||||
|
||||
self._started_prev = self.started
|
||||
|
||||
def _update_chestnut_state(self) -> None:
|
||||
detected = self.sm["deviceState"].chestnutPresent
|
||||
if not self.started:
|
||||
self.chestnut_present = detected
|
||||
self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else
|
||||
ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED)
|
||||
return
|
||||
|
||||
model_seen = self.sm.recv_frame["modelV2"] > self.started_frame
|
||||
if not self.chestnut_present:
|
||||
self.chestnut_state = ChestnutState.DISCONNECTED
|
||||
elif not self.chestnut_compiled:
|
||||
self.chestnut_state = ChestnutState.UNCOMPILED
|
||||
elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)):
|
||||
self.chestnut_state = ChestnutState.FAILED
|
||||
elif self.chestnut_loading or not model_seen:
|
||||
self.chestnut_state = ChestnutState.LOADING
|
||||
elif self.chestnut_active is False:
|
||||
self.chestnut_state = ChestnutState.FAILED
|
||||
else:
|
||||
self.chestnut_state = ChestnutState.ACTIVE
|
||||
|
||||
def update_params(self) -> None:
|
||||
# For slower operations
|
||||
# Update longitudinal control state
|
||||
@@ -225,12 +250,10 @@ class UIState(UIStateSP):
|
||||
self.always_on_dm = self.params.get_bool("AlwaysOnDM")
|
||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
||||
self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed")
|
||||
# keep usbgpu UI active until offroad transition when gpu disappears
|
||||
self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started)
|
||||
if not self.usbgpu_compiled:
|
||||
self.usbgpu_compiled = usbgpu_compiled()
|
||||
self.usbgpu_active = self.params.get("UsbGpuActive")
|
||||
self.usbgpu_loading = self.params.get_bool("UsbGpuLoading")
|
||||
if not self.chestnut_compiled:
|
||||
self.chestnut_compiled = chestnut_compiled()
|
||||
self.chestnut_active = self.params.get("ChestnutActive")
|
||||
self.chestnut_loading = self.params.get_bool("ChestnutLoading")
|
||||
|
||||
UIStateSP.update_params(self)
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'):
|
||||
if 'USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT'):
|
||||
from openpilot.system.hardware.chestnut.flash import link_up
|
||||
for _ in range(10):
|
||||
if link_up():
|
||||
|
||||
@@ -9,7 +9,7 @@ See the LICENSE.md file in the root directory for more details.
|
||||
import os
|
||||
os.environ['GMMU'] = '0'
|
||||
from openpilot.common.hardware import COMMA_HARDWARE
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob
|
||||
import time
|
||||
import numpy as np
|
||||
import openpilot.cereal.messaging as messaging
|
||||
@@ -84,14 +84,14 @@ class ModelState(ModelStateBase):
|
||||
inputs: dict[str, np.ndarray]
|
||||
prev_desire: np.ndarray
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool = False):
|
||||
def __init__(self, cam_w: int, cam_h: int, chestnut: bool = False):
|
||||
ModelStateBase.__init__(self)
|
||||
|
||||
env_pkl = os.environ.get('COMBINED_MODEL_PKL')
|
||||
if env_pkl and os.path.exists(env_pkl):
|
||||
model_bundle = None
|
||||
else:
|
||||
model_bundle = get_active_bundle(usbgpu=usbgpu)
|
||||
model_bundle = get_active_bundle(chestnut=chestnut)
|
||||
self.generation = model_bundle.generation if model_bundle is not None else None
|
||||
overrides = {override.key: override.value for override in model_bundle.overrides} if model_bundle else {}
|
||||
|
||||
@@ -99,7 +99,7 @@ class ModelState(ModelStateBase):
|
||||
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
self.PLANPLUS_CONTROL: float = 1.0
|
||||
self.usbgpu = usbgpu
|
||||
self.chestnut = chestnut
|
||||
|
||||
pkl_path = _find_driving_pkl(model_bundle)
|
||||
assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py"
|
||||
@@ -110,7 +110,7 @@ class ModelState(ModelStateBase):
|
||||
jits = load_oob(open_file_chunked(pkl_path))
|
||||
|
||||
self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU'
|
||||
self.DEV = 'AMD' if self.usbgpu else self.WARP_DEV
|
||||
self.DEV = 'AMD' if self.chestnut else self.WARP_DEV
|
||||
self.QUEUE_DEV = self.DEV
|
||||
metadata = jits['metadata']
|
||||
|
||||
@@ -185,7 +185,7 @@ class ModelState(ModelStateBase):
|
||||
else:
|
||||
self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor)
|
||||
|
||||
if self.usbgpu:
|
||||
if self.chestnut:
|
||||
self.warmup()
|
||||
|
||||
def warmup(self) -> None:
|
||||
@@ -287,7 +287,7 @@ class ModelState(ModelStateBase):
|
||||
buf[0, :-1] = buf[0, 1:]
|
||||
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
|
||||
|
||||
if self.usbgpu and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
|
||||
if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
|
||||
cloudlog.error("model output not finite, dropping frame")
|
||||
return None
|
||||
|
||||
@@ -327,13 +327,13 @@ def main(demo=False):
|
||||
setproctitle(PROCESS_NAME)
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
USBGPU = usbgpu_present()
|
||||
if USBGPU:
|
||||
CHESTNUT = chestnut_present()
|
||||
if CHESTNUT:
|
||||
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
|
||||
|
||||
params = Params()
|
||||
params.put_bool("UsbGpuLoading", USBGPU)
|
||||
params.remove("UsbGpuActive")
|
||||
params.put_bool("ChestnutLoading", CHESTNUT)
|
||||
params.remove("ChestnutActive")
|
||||
|
||||
# visionipc clients
|
||||
while True:
|
||||
@@ -362,31 +362,31 @@ def main(demo=False):
|
||||
st = time.monotonic()
|
||||
|
||||
model = None
|
||||
if USBGPU:
|
||||
if CHESTNUT:
|
||||
import threading
|
||||
def load():
|
||||
nonlocal model
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True)
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True)
|
||||
t = threading.Thread(target=load, daemon=True)
|
||||
t.start()
|
||||
t.join(60)
|
||||
if model is None:
|
||||
params.put_bool("UsbGpuActive", False)
|
||||
raise RuntimeError("eGPU model load failed or timed out (60s)")
|
||||
params.put_bool("UsbGpuActive", True)
|
||||
params.put_bool("ChestnutActive", False)
|
||||
raise RuntimeError("chestnut model load failed or timed out (60s)")
|
||||
params.put_bool("ChestnutActive", True)
|
||||
else:
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False)
|
||||
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False)
|
||||
|
||||
params.put_bool("UsbGpuLoading", False)
|
||||
params.put_bool("ChestnutLoading", False)
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
|
||||
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
|
||||
pm = PubMaster(pub_socks)
|
||||
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
|
||||
|
||||
publish_state = PublishState()
|
||||
chestnut_state = ChestnutState(pm, USBGPU) if USBGPU else None
|
||||
chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None
|
||||
|
||||
# setup filter to track dropped frames
|
||||
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
|
||||
@@ -524,7 +524,7 @@ def main(demo=False):
|
||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||
frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, meta_constants)
|
||||
modelv2_send.modelV2.big = model.usbgpu
|
||||
modelv2_send.modelV2.big = model.chestnut
|
||||
|
||||
desire_state = modelv2_send.modelV2.meta.desireState
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
|
||||
@@ -190,8 +190,8 @@ def tmp_path():
|
||||
|
||||
def patch_modeld(monkeypatch):
|
||||
def _patch(bundle):
|
||||
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
|
||||
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
|
||||
|
||||
return _patch
|
||||
|
||||
|
||||
@@ -59,8 +59,8 @@ class TestFindDrivingPkl(OpenpilotTestCase):
|
||||
class TestModelStateCombinedInit(OpenpilotTestCase):
|
||||
def test_asserts_when_no_pkl(self, monkeypatch):
|
||||
bundle = DummyBundle(models=[], is_20hz=True)
|
||||
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, usbgpu=None: bundle)
|
||||
monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
|
||||
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: bundle)
|
||||
with self.assertRaisesRegex(AssertionError, "No driving pkl found"):
|
||||
ModelState(cam_w=CAM_W, cam_h=CAM_H)
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MO
|
||||
|
||||
|
||||
def get_default_model() -> str:
|
||||
show_big_model = (ui_state.usbgpu
|
||||
and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad()))
|
||||
show_big_model = (ui_state.chestnut_present
|
||||
and (ui_state.chestnut_active or ui_state.chestnut_loading or ui_state.is_offroad()))
|
||||
|
||||
return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL
|
||||
|
||||
|
||||
@@ -139,11 +139,11 @@ class ModelCache:
|
||||
class ModelFetcher:
|
||||
"""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_v21.json"
|
||||
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json"
|
||||
MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json"
|
||||
|
||||
MODEL_SOURCES = {
|
||||
"qcom": (MODEL_URL, ""),
|
||||
"usbgpu": (MODEL_URL_USBGPU, "_USBGPU"),
|
||||
"chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"),
|
||||
}
|
||||
|
||||
def __init__(self, params: Params):
|
||||
@@ -156,12 +156,12 @@ class ModelFetcher:
|
||||
self._refetched: set[str] = set()
|
||||
self.params.put("ModelManager_ActiveJson", {
|
||||
"qcom": self.MODEL_URL,
|
||||
"usbgpu": self.MODEL_URL_USBGPU,
|
||||
"chestnut": self.MODEL_URL_CHESTNUT,
|
||||
}, block=True)
|
||||
|
||||
@staticmethod
|
||||
def active_source(chestnut_present: bool) -> str:
|
||||
return "usbgpu" if chestnut_present else "qcom"
|
||||
return "chestnut" if chestnut_present else "qcom"
|
||||
|
||||
def _fetch_and_cache_models(self, source: str) -> list[custom.ModelManagerSP.ModelBundle] | None:
|
||||
"""Fetches fresh model data from remote and updates cache.
|
||||
@@ -200,7 +200,7 @@ class ModelFetcher:
|
||||
@staticmethod
|
||||
def _cache_matches_source(source: str, cached_data: dict) -> bool:
|
||||
bundles = cached_data.get("bundles", [])
|
||||
if source == "usbgpu":
|
||||
if source == "chestnut":
|
||||
return any(bundle.get("is_big") is True for bundle in bundles)
|
||||
return not any(bundle.get("is_big") is True for bundle in bundles)
|
||||
|
||||
@@ -261,10 +261,10 @@ def get_cached_bundles(params: Params, source: str) -> list[custom.ModelManagerS
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||
params = Params()
|
||||
model_fetcher = ModelFetcher(params)
|
||||
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present()))
|
||||
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(chestnut_present()))
|
||||
for bundle in bundles:
|
||||
for model in bundle.models:
|
||||
model_overrides = {override.key: override.value for override in bundle.overrides}
|
||||
|
||||
@@ -16,7 +16,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
|
||||
from openpilot.common.hardware.hw import Paths
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present
|
||||
from openpilot.selfdrive.modeld.helpers import chestnut_present
|
||||
|
||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||
REQUIRED_JSON_VERSION = 18
|
||||
@@ -27,7 +27,7 @@ ModelManager = custom.ModelManagerSP
|
||||
|
||||
ACTIVE_BUNDLE_KEYS = {
|
||||
"qcom": "ModelManager_ActiveBundle",
|
||||
"usbgpu": "ModelManager_ActiveBundleUSBGPU",
|
||||
"chestnut": "ModelManager_ActiveBundleChestnut",
|
||||
}
|
||||
_LAST_VALIDATED_RAW: dict[str, dict | None] = {}
|
||||
|
||||
@@ -126,20 +126,20 @@ def get_selected_bundle(params: Params | None = None, source: str = "qcom") -> "
|
||||
return _parse_active_bundle(params.get(ACTIVE_BUNDLE_KEYS[source]))
|
||||
|
||||
|
||||
def get_active_source(usbgpu: bool | None = None, usbgpu_active: bool | None = None,
|
||||
usbgpu_loading: bool | None = None, offroad: bool | None = None) -> str:
|
||||
if usbgpu is None:
|
||||
usbgpu = usbgpu_present()
|
||||
state_valid = usbgpu_active is not None or usbgpu_loading is not None or offroad is not None
|
||||
big_active = usbgpu and (not state_valid or usbgpu_active or usbgpu_loading or offroad)
|
||||
return "usbgpu" if big_active else "qcom"
|
||||
def get_active_source(chestnut: bool | None = None, chestnut_active: bool | None = None,
|
||||
chestnut_loading: bool | None = None, offroad: bool | None = None) -> str:
|
||||
if chestnut is None:
|
||||
chestnut = chestnut_present()
|
||||
state_valid = chestnut_active is not None or chestnut_loading is not None or offroad is not None
|
||||
big_active = chestnut and (not state_valid or chestnut_active or chestnut_loading or offroad)
|
||||
return "chestnut" if big_active else "qcom"
|
||||
|
||||
|
||||
def get_active_bundle(params: Params | None = None, *, usbgpu: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
|
||||
def get_active_bundle(params: Params | None = None, *, chestnut: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
|
||||
# no cross-slot fallback: an empty active slot means the hardware default, which
|
||||
# only stock modeld can run - modeld_v2 requires a real bundle
|
||||
params = params or Params()
|
||||
return get_selected_bundle(params, get_active_source(usbgpu=usbgpu))
|
||||
return get_selected_bundle(params, get_active_source(chestnut=chestnut))
|
||||
|
||||
|
||||
def resolve_bundle_by_ref(
|
||||
|
||||
@@ -40,7 +40,7 @@ class ModelManagerSP:
|
||||
self.available_models: list[custom.ModelManagerSP.ModelBundle] = []
|
||||
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
|
||||
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
|
||||
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
|
||||
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||
self._chunk_size = 128 * 1000 # 128 KB chunks
|
||||
self._download_start_times: dict[str, float] = {} # Track start time per model
|
||||
self._download_ref: bytes | str | None = None
|
||||
@@ -280,7 +280,7 @@ class ModelManagerSP:
|
||||
raise DownloadCancelled("Download cancelled")
|
||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
self.params.put(ACTIVE_BUNDLE_KEYS[source], model_bundle.to_dict(), block=True)
|
||||
self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
|
||||
self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||
|
||||
except Exception:
|
||||
if self.selected_bundle is not None:
|
||||
@@ -326,7 +326,7 @@ class ModelManagerSP:
|
||||
self.source_models = {source: self.model_fetcher.get_bundles_for_source(source) for source in ModelFetcher.MODEL_SOURCES}
|
||||
self.available_models = self.source_models[ModelFetcher.active_source(self.chestnut_present)]
|
||||
validate_active_bundles(self.params, self.source_models)
|
||||
self.active_bundle = get_active_bundle(self.params, usbgpu=self.chestnut_present)
|
||||
self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||
|
||||
self._process_download_requests()
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom"))
|
||||
|
||||
assert "ModelManager_ActiveBundle" in store, "qcom download must write the qcom slot"
|
||||
assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu slot"
|
||||
assert "ModelManager_ActiveBundleChestnut" not in store, "qcom download must not touch the chestnut slot"
|
||||
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
assert self.manager.active_bundle is not None and self.manager.active_bundle.ref == "test-ref"
|
||||
assert self.manager.active_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
@@ -401,18 +401,18 @@ class TestManagerDownload(ManagerDownloadTestBase):
|
||||
assert missing == [], f"chunks missing from the cache: {missing}"
|
||||
self.run_with_server(body)
|
||||
|
||||
def test_download_writes_usbgpu_slot(self):
|
||||
"""A download resolved to the usbgpu source writes the usbgpu active bundle slot only."""
|
||||
def test_download_writes_chestnut_slot(self):
|
||||
"""A download resolved to the chestnut source writes the chestnut active bundle slot only."""
|
||||
def body():
|
||||
self.make_artifact(chunked=True)
|
||||
self._bundle.ref = "big-ref"
|
||||
self._bundle.minimumSelectorVersion = 18
|
||||
params, store = self._make_params_with_store()
|
||||
self.manager.params = params
|
||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu"))
|
||||
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut"))
|
||||
|
||||
assert "ModelManager_ActiveBundleUSBGPU" in store, "usbgpu download must write the usbgpu slot"
|
||||
assert "ModelManager_ActiveBundle" not in store, "usbgpu download must not touch the qcom slot"
|
||||
assert "ModelManager_ActiveBundleChestnut" in store, "chestnut download must write the chestnut slot"
|
||||
assert "ModelManager_ActiveBundle" not in store, "chestnut download must not touch the qcom slot"
|
||||
assert self.manager.selected_bundle.status == custom.ModelManagerSP.DownloadStatus.downloaded
|
||||
self.run_with_server(body)
|
||||
|
||||
@@ -447,20 +447,20 @@ class TestResolveBundleByRef(OpenpilotTestCase):
|
||||
|
||||
def test_qcom_ref_resolves_to_qcom_slot(self):
|
||||
small = self._bundle("small")
|
||||
assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom")
|
||||
assert resolve_bundle_by_ref("small", {"qcom": [small], "chestnut": []}) == (small, "qcom")
|
||||
|
||||
def test_usbgpu_ref_resolves_to_usbgpu_slot(self):
|
||||
def test_chestnut_ref_resolves_to_chestnut_slot(self):
|
||||
big = self._bundle("big")
|
||||
assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu")
|
||||
assert resolve_bundle_by_ref("big", {"qcom": [], "chestnut": [big]}) == (big, "chestnut")
|
||||
|
||||
def test_unknown_ref_returns_none(self):
|
||||
source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []}
|
||||
source_bundles = {"qcom": [self._bundle("small")], "chestnut": []}
|
||||
assert resolve_bundle_by_ref("nope", source_bundles) is None
|
||||
|
||||
|
||||
def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = False) -> dict:
|
||||
"""Minimal manifest bundle dict, version-compatible (no chunks to avoid disk side effects).
|
||||
Big (usbgpu) bundles carry `is_big: true` in the manifest JSON."""
|
||||
Big (chestnut) bundles carry `is_big: true` in the manifest JSON."""
|
||||
return {
|
||||
"index": index,
|
||||
"short_name": short_name,
|
||||
@@ -489,15 +489,15 @@ class TestModelFetcherSources(OpenpilotTestCase):
|
||||
"""Both manifests are always maintained: get_bundles_for_source exposes either
|
||||
source by name, and active_source picks which one matches the attached hardware."""
|
||||
|
||||
def _make_params(self, qcom_manifest, usbgpu_manifest):
|
||||
def _make_params(self, qcom_manifest, chestnut_manifest):
|
||||
params = mock.MagicMock()
|
||||
|
||||
def get(key):
|
||||
if key == "ModelManager_ModelsCache":
|
||||
return qcom_manifest
|
||||
if key == "ModelManager_ModelsCache_USBGPU":
|
||||
return usbgpu_manifest
|
||||
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
|
||||
if key == "ModelManager_ModelsCache_Chestnut":
|
||||
return chestnut_manifest
|
||||
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
|
||||
return fresh_sync_time()
|
||||
return None
|
||||
|
||||
@@ -506,14 +506,14 @@ class TestModelFetcherSources(OpenpilotTestCase):
|
||||
|
||||
def test_active_source_follows_chestnut_presence(self):
|
||||
assert ModelFetcher.active_source(False) == "qcom"
|
||||
assert ModelFetcher.active_source(True) == "usbgpu"
|
||||
assert ModelFetcher.active_source(True) == "chestnut"
|
||||
|
||||
def test_get_bundles_for_source_returns_each_source(self):
|
||||
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
|
||||
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
|
||||
fetcher = ModelFetcher(params)
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"]
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"]
|
||||
|
||||
def test_get_bundles_for_source_unknown(self):
|
||||
assert ModelFetcher(mock.MagicMock()).get_bundles_for_source("bogus") == []
|
||||
@@ -522,16 +522,16 @@ class TestModelFetcherSources(OpenpilotTestCase):
|
||||
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
|
||||
{"bundles": [manifest_bundle("big", "bbb", is_big=True)]})
|
||||
qcom_bundles = get_cached_bundles(params, "qcom")
|
||||
usbgpu_bundles = get_cached_bundles(params, "usbgpu")
|
||||
chestnut_bundles = get_cached_bundles(params, "chestnut")
|
||||
assert [b.ref for b in qcom_bundles] == ["aaa"]
|
||||
assert [b.ref for b in usbgpu_bundles] == ["bbb"]
|
||||
assert [b.ref for b in chestnut_bundles] == ["bbb"]
|
||||
assert qcom_bundles[0].displayName == "SMALL"
|
||||
|
||||
def test_get_cached_bundles_empty_when_missing(self):
|
||||
params = mock.MagicMock()
|
||||
params.get.return_value = None
|
||||
assert get_cached_bundles(params, "qcom") == []
|
||||
assert get_cached_bundles(params, "usbgpu") == []
|
||||
assert get_cached_bundles(params, "chestnut") == []
|
||||
|
||||
def test_get_cached_bundles_unknown_source(self):
|
||||
assert get_cached_bundles(mock.MagicMock(), "bogus") == []
|
||||
@@ -543,27 +543,27 @@ class TestModelFetcherSources(OpenpilotTestCase):
|
||||
assert active_json_calls, "expected ModelManager_ActiveJson to be written"
|
||||
assert active_json_calls[-1].args[1] == {
|
||||
"qcom": ModelFetcher.MODEL_URL,
|
||||
"usbgpu": ModelFetcher.MODEL_URL_USBGPU,
|
||||
"chestnut": ModelFetcher.MODEL_URL_CHESTNUT,
|
||||
}
|
||||
|
||||
|
||||
|
||||
class TestSourceCacheIntegrity(OpenpilotTestCase):
|
||||
"""Each source's cached manifest must contain only that source's models; the
|
||||
`is_big` flag in the JSON marks the big (usbgpu) models. A mismatched cache is
|
||||
`is_big` flag in the JSON marks the big (chestnut) models. A mismatched cache is
|
||||
legacy data from before the per-source split (the active manifest was cached
|
||||
under the unsuffixed key regardless of hardware) and is refetched. This
|
||||
replaces the old one-time bundle migration."""
|
||||
|
||||
def _make_params(self, qcom_manifest, usbgpu_manifest):
|
||||
def _make_params(self, qcom_manifest, chestnut_manifest):
|
||||
params = mock.MagicMock()
|
||||
|
||||
def get(key):
|
||||
if key == "ModelManager_ModelsCache":
|
||||
return qcom_manifest
|
||||
if key == "ModelManager_ModelsCache_USBGPU":
|
||||
return usbgpu_manifest
|
||||
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
|
||||
if key == "ModelManager_ModelsCache_Chestnut":
|
||||
return chestnut_manifest
|
||||
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
|
||||
return fresh_sync_time()
|
||||
return None
|
||||
|
||||
@@ -584,13 +584,13 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
|
||||
bundles = fetcher.get_bundles_for_source("qcom")
|
||||
assert [bundle.ref for bundle in bundles] == ["aaa"]
|
||||
|
||||
def test_usbgpu_cache_without_big_models_is_refetched(self):
|
||||
def test_chestnut_cache_without_big_models_is_refetched(self):
|
||||
params = self._make_params({"bundles": [manifest_bundle("small", "aaa")]},
|
||||
{"bundles": [manifest_bundle("big2", "ccc")]})
|
||||
fetcher = ModelFetcher(params)
|
||||
fetched = self._fetched(manifest_bundle("big", "bbb", is_big=True))
|
||||
with mock.patch.object(fetcher, "_fetch_and_cache_models", return_value=fetched):
|
||||
bundles = fetcher.get_bundles_for_source("usbgpu")
|
||||
bundles = fetcher.get_bundles_for_source("chestnut")
|
||||
assert [bundle.ref for bundle in bundles] == ["bbb"]
|
||||
|
||||
def test_matching_caches_are_used_without_fetch(self):
|
||||
@@ -599,7 +599,7 @@ class TestSourceCacheIntegrity(OpenpilotTestCase):
|
||||
fetcher = ModelFetcher(params)
|
||||
with mock.patch.object(fetcher, "_fetch_and_cache_models", side_effect=AssertionError("cache should be used")):
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("qcom")] == ["aaa"]
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["bbb"]
|
||||
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("chestnut")] == ["bbb"]
|
||||
|
||||
def test_stale_version_cache_is_refetched(self):
|
||||
"""A source-matching cache whose bundles are all filtered by the selector version
|
||||
@@ -660,28 +660,28 @@ class TestActiveBundleValidation(OpenpilotTestCase):
|
||||
bundle.runner = runner
|
||||
return bundle.to_dict()
|
||||
|
||||
def _params(self, qcom=None, usbgpu=None):
|
||||
def _params(self, qcom=None, chestnut=None):
|
||||
params = mock.MagicMock()
|
||||
|
||||
def get(key, *args, **kwargs):
|
||||
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.get(key)
|
||||
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleChestnut": chestnut}.get(key)
|
||||
|
||||
params.get.side_effect = get
|
||||
return params
|
||||
|
||||
def test_empty_catalog_does_not_reset_slot(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"))
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
|
||||
validate_active_bundles(params, {"qcom": [], "usbgpu": []})
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
|
||||
validate_active_bundles(params, {"qcom": [], "chestnut": []})
|
||||
params.remove.assert_not_called()
|
||||
|
||||
def test_reset_recomputes_runner_from_surviving_slot(self):
|
||||
tinygrad = int(custom.ModelManagerSP.Runner.tinygrad)
|
||||
big_raw = self._raw_bundle("big", runner=tinygrad)
|
||||
params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw)
|
||||
params = self._params(qcom=self._raw_bundle("gone"), chestnut=big_raw)
|
||||
catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))],
|
||||
"usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
|
||||
"chestnut": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
|
||||
validate_active_bundles(params, catalog)
|
||||
params.remove.assert_called_once_with("ModelManager_ActiveBundle")
|
||||
runner_puts = [call for call in params.put.call_args_list if call.args[0] == "ModelRunnerTypeCache"]
|
||||
@@ -689,7 +689,7 @@ class TestActiveBundleValidation(OpenpilotTestCase):
|
||||
|
||||
|
||||
class TestActiveBundleSelection(OpenpilotTestCase):
|
||||
"""The effective active bundle is the active source's slot: usbgpu when a GPU is
|
||||
"""The effective active bundle is the active source's slot: chestnut when a GPU is
|
||||
present, qcom otherwise. An empty active slot means the hardware default (stock
|
||||
runner), never the other slot's pick - modeld_v2 requires a real bundle."""
|
||||
|
||||
@@ -700,37 +700,37 @@ class TestActiveBundleSelection(OpenpilotTestCase):
|
||||
bundle.minimumSelectorVersion = 18
|
||||
return bundle.to_dict()
|
||||
|
||||
def _params(self, qcom=None, usbgpu=None):
|
||||
def _params(self, qcom=None, chestnut=None):
|
||||
params = mock.MagicMock()
|
||||
|
||||
def get(key, *args, **kwargs):
|
||||
if key == "ModelManager_ActiveBundle":
|
||||
return qcom
|
||||
if key == "ModelManager_ActiveBundleUSBGPU":
|
||||
return usbgpu
|
||||
if key == "ModelManager_ActiveBundleChestnut":
|
||||
return chestnut
|
||||
return None
|
||||
|
||||
params.get.side_effect = get
|
||||
return params
|
||||
|
||||
def test_selected_bundle_is_per_slot(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
|
||||
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
|
||||
assert get_selected_bundle(params, "qcom").ref == "small"
|
||||
assert get_selected_bundle(params, "usbgpu").ref == "big"
|
||||
assert get_selected_bundle(params, "chestnut").ref == "big"
|
||||
|
||||
def test_no_gpu_uses_qcom_slot(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
|
||||
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
|
||||
assert get_active_bundle(params).ref == "small"
|
||||
|
||||
def test_gpu_uses_usbgpu_slot(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
|
||||
def test_gpu_uses_chestnut_slot(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"), chestnut=self._raw_bundle("big"))
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
|
||||
assert get_active_bundle(params).ref == "big"
|
||||
|
||||
def test_gpu_without_big_selection_is_hardware_default(self):
|
||||
params = self._params(qcom=self._raw_bundle("small"), usbgpu=None)
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
|
||||
params = self._params(qcom=self._raw_bundle("small"), chestnut=None)
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
|
||||
assert get_active_bundle(params) is None
|
||||
|
||||
|
||||
@@ -748,36 +748,36 @@ class TestEffectiveSource(OpenpilotTestCase):
|
||||
return bundle.to_dict()
|
||||
|
||||
def test_runtime_no_gpu(self):
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
|
||||
assert get_active_source() == "qcom"
|
||||
|
||||
def test_runtime_gpu_present(self):
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
|
||||
assert get_active_source() == "usbgpu"
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
|
||||
assert get_active_source() == "chestnut"
|
||||
|
||||
def test_display_offroad_gpu_present_shows_big(self):
|
||||
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu"
|
||||
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=True) == "chestnut"
|
||||
|
||||
def test_display_onroad_gpu_loading_shows_big(self):
|
||||
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu"
|
||||
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=True, offroad=False) == "chestnut"
|
||||
|
||||
def test_display_onroad_gpu_active_shows_big(self):
|
||||
assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu"
|
||||
assert get_active_source(chestnut=True, chestnut_active=True, chestnut_loading=False, offroad=False) == "chestnut"
|
||||
|
||||
def test_display_onroad_gpu_idle_shows_small(self):
|
||||
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom"
|
||||
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=False) == "qcom"
|
||||
|
||||
def test_display_active_none_is_idle(self):
|
||||
assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_loading=False, offroad=False) == "qcom"
|
||||
assert get_active_source(chestnut=True, chestnut_active=None, chestnut_loading=False, offroad=False) == "qcom"
|
||||
|
||||
def test_active_bundle_follows_source(self):
|
||||
params = mock.MagicMock()
|
||||
params.get.side_effect = lambda key: {"ModelManager_ActiveBundle": self._raw_bundle("small"),
|
||||
"ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key)
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
|
||||
"ModelManager_ActiveBundleChestnut": self._raw_bundle("big")}.get(key)
|
||||
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
|
||||
assert get_active_bundle(params).ref == "small"
|
||||
assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False,
|
||||
usbgpu_loading=False, offroad=True)).ref == "big"
|
||||
assert get_selected_bundle(params, get_active_source(chestnut=True, chestnut_active=False,
|
||||
chestnut_loading=False, offroad=True)).ref == "big"
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network')
|
||||
|
||||
@@ -183,7 +183,7 @@ def getParamsMetadata() -> str:
|
||||
schema["capability_labels"] = CAPABILITY_LABELS
|
||||
schema["default_model"] = DEFAULT_MODEL
|
||||
schema["default_big_model"] = DEFAULT_BIG_MODEL
|
||||
schema["usbgpu_active"] = params.get_bool("UsbGpuActive")
|
||||
schema["chestnut_active"] = params.get_bool("ChestnutActive")
|
||||
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
|
||||
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
|
||||
except Exception:
|
||||
|
||||
@@ -65,7 +65,7 @@ def sp_stats(end_event):
|
||||
'MadsSteeringMode',
|
||||
'MadsUnifiedEngagementMode',
|
||||
'ModelManager_ActiveBundle',
|
||||
'ModelManager_ActiveBundleUSBGPU',
|
||||
'ModelManager_ActiveBundleChestnut',
|
||||
'ModelManager_Favs',
|
||||
'EnableSunnylinkUploader',
|
||||
'SunnylinkEnabled',
|
||||
|
||||
@@ -89,12 +89,16 @@ def _migrate_model_bundle_slots(_params):
|
||||
# ActiveBundle. Seed both slots; validation drops whichever does not match
|
||||
# its own manifest.
|
||||
try:
|
||||
if _params.get("ModelManager_ActiveBundleUSBGPU") is not None:
|
||||
if _params.get("ModelManager_ActiveBundleChestnut") is not None:
|
||||
return
|
||||
if (chestnut_bundle := _params.get("ModelManager_ActiveBundleUSBGPU")) is not None:
|
||||
_params.put("ModelManager_ActiveBundleChestnut", chestnut_bundle, block=True)
|
||||
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundleUSBGPU")
|
||||
return
|
||||
if (bundle := _params.get("ModelManager_ActiveBundle")) is None:
|
||||
return
|
||||
_params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True)
|
||||
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle")
|
||||
_params.put("ModelManager_ActiveBundleChestnut", bundle, block=True)
|
||||
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundle")
|
||||
except Exception as e:
|
||||
cloudlog.exception(f"Error migrating model bundle slots: {e}")
|
||||
|
||||
@@ -136,5 +140,5 @@ def run_migration(_params):
|
||||
# seed TeslaMadsScreenButton for existing Tesla installs
|
||||
_migrate_tesla_mads_screen_button(_params)
|
||||
|
||||
# seed the usbgpu model slot from the pre-split single slot
|
||||
# seed the chestnut model slot from the pre-split single slot
|
||||
_migrate_model_bundle_slots(_params)
|
||||
|
||||
@@ -15,22 +15,22 @@ class TestModelBundleSlotMigration(OpenpilotTestCase):
|
||||
The migration seeds both slots; per-source validation later drops whichever does not
|
||||
match its own manifest."""
|
||||
|
||||
def test_seeds_usbgpu_slot_from_active_bundle(self):
|
||||
def test_seeds_chestnut_slot_from_active_bundle(self):
|
||||
params = Params()
|
||||
bundle = {"ref": "big", "minimumSelectorVersion": 18}
|
||||
params.put("ModelManager_ActiveBundle", bundle, block=True)
|
||||
_migrate_model_bundle_slots(params)
|
||||
assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle
|
||||
assert params.get("ModelManager_ActiveBundleChestnut") == bundle
|
||||
assert params.get("ModelManager_ActiveBundle") == bundle
|
||||
|
||||
def test_noop_when_usbgpu_slot_already_set(self):
|
||||
def test_noop_when_chestnut_slot_already_set(self):
|
||||
params = Params()
|
||||
params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True)
|
||||
params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True)
|
||||
params.put("ModelManager_ActiveBundleChestnut", {"ref": "big"}, block=True)
|
||||
_migrate_model_bundle_slots(params)
|
||||
assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"}
|
||||
assert params.get("ModelManager_ActiveBundleChestnut") == {"ref": "big"}
|
||||
|
||||
def test_noop_when_no_selection(self):
|
||||
params = Params()
|
||||
_migrate_model_bundle_slots(params)
|
||||
assert params.get("ModelManager_ActiveBundleUSBGPU") is None
|
||||
assert params.get("ModelManager_ActiveBundleChestnut") is None
|
||||
|
||||
@@ -16,7 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_HW
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled
|
||||
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
@@ -239,7 +239,7 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
|
||||
fan_controller = FanController(int(1./DT_HW))
|
||||
chestnut = Chestnut()
|
||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled()
|
||||
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled()
|
||||
|
||||
while not end_event.is_set():
|
||||
sm.update(PANDA_STATES_TIMEOUT)
|
||||
|
||||
@@ -45,8 +45,9 @@ class ScrollState(Enum):
|
||||
|
||||
|
||||
class GuiScrollPanel2:
|
||||
def __init__(self, horizontal: bool = True) -> None:
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
self._horizontal = horizontal
|
||||
self._handle_out_of_bounds = handle_out_of_bounds
|
||||
self._state = ScrollState.STEADY
|
||||
self._offset: rl.Vector2 = rl.Vector2(0, 0)
|
||||
self._initial_click_event: MouseEvent | None = None
|
||||
@@ -85,6 +86,20 @@ class GuiScrollPanel2:
|
||||
"""Returns (max_offset, min_offset) for the given bounds and content size."""
|
||||
return 0.0, min(0.0, bounds_size - content_size)
|
||||
|
||||
def _clamp_offset(self, bounds_size: float, content_size: float) -> None:
|
||||
if self._handle_out_of_bounds:
|
||||
return
|
||||
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
offset = self.get_offset()
|
||||
clamped_offset = max(min_offset, min(max_offset, offset))
|
||||
if clamped_offset == offset:
|
||||
return
|
||||
|
||||
self.set_offset(clamped_offset)
|
||||
if (clamped_offset == max_offset and self._velocity > 0) or (clamped_offset == min_offset and self._velocity < 0):
|
||||
self._velocity = 0.0
|
||||
|
||||
def _update_state(self, bounds_size: float, content_size: float, snap_target: float | None) -> None:
|
||||
"""Runs per render frame, independent of mouse events. Updates auto-scrolling state and velocity."""
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
@@ -138,6 +153,8 @@ class GuiScrollPanel2:
|
||||
factor = 1.0 - math.exp(-SNAP_RATE * dt)
|
||||
self.set_offset(self.get_offset() + dist * factor)
|
||||
|
||||
self._clamp_offset(bounds_size, content_size)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
max_offset, min_offset = self._get_offset_bounds(bounds_size, content_size)
|
||||
|
||||
@@ -75,7 +75,6 @@ class _Scroller(Widget):
|
||||
self._items: list[Widget] = []
|
||||
self._horizontal = horizontal
|
||||
self._snap_items = snap_items
|
||||
assert not self._snap_items or self._horizontal, "Snapping is only supported for horizontal scrolling"
|
||||
self._spacing = spacing
|
||||
self._pad = pad
|
||||
|
||||
@@ -191,12 +190,20 @@ class _Scroller(Widget):
|
||||
snap_target: float | None = None
|
||||
if self._snap_items and visible_items and self._scrolling_to[0] is None:
|
||||
# TODO: this doesn't handle two small buttons at the edges well
|
||||
center_pos = self._rect.x + self._rect.width / 2
|
||||
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||
center_pos = (self._rect.x + self._rect.width / 2) if self._horizontal else (self._rect.y + self._rect.height / 2)
|
||||
closest_delta_pos = min(
|
||||
(self._item_center_pos(item) - center_pos for item in visible_items),
|
||||
key=abs,
|
||||
)
|
||||
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
|
||||
|
||||
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
|
||||
|
||||
def _item_center_pos(self, item: Widget) -> float:
|
||||
if self._horizontal:
|
||||
return item.rect.x + item.rect.width / 2
|
||||
return item.rect.y + item.rect.height / 2
|
||||
|
||||
@property
|
||||
def moving_items(self) -> bool:
|
||||
return len(self._move_animations) > 0 or len(self._move_lift) > 0
|
||||
|
||||
Reference in New Issue
Block a user