Compare commits

..

9 Commits

Author SHA1 Message Date
Isaac Barham 6db807b5a0 ford: narrow lateral path interface
Assisted-by: Codex
2026-08-27 20:06:36 -04:00
Isaac Barham 42e1414bc4 ford: source C2 only from desired curvature
Prevent model-fit curvature jitter from directly modulating the PSCM's slow C2 channel.

Assisted-by: Codex
2026-08-27 19:53:59 -04:00
Isaac Barham 3e020e321f ford: gate curvature rate with maneuver demand
Assisted-by: Codex
2026-08-27 19:44:19 -04:00
Isaac Barham 7e2000e909 ford: make path allocation demand driven
Assisted-by: Codex
2026-08-27 19:08:21 -04:00
Isaac Barham e96055846c ford: distill lateral path controller
Assisted-by: Codex
2026-08-27 16:27:38 -04:00
Isaac Barham d47646b28f Ford: keep LMC2 available through path gaps
Assisted-by: Codex
2026-08-27 15:36:50 -04:00
Isaac Barham e75bc83424 Ford: retain centering through curve exits
Keep a bounded geometric C2 band for lane centering, preserve established rolling arcs during same-direction unwind, and smoothly release old-direction C2 on reversals. Slew-limit the fast C1 command to prevent threshold chatter.

Assisted-by: Codex
2026-08-27 15:13:56 -04:00
Isaac Barham 08e48958b6 Ford: close the loop on path curvature
Use the rolling path for pose and slow geometry while allocating jerk-limited requested curvature and bounded tracking error to the fast heading field. Prevent filtered C2 from reinforcing an unwind or reversal.

Assisted-by: Codex
2026-08-27 14:35:59 -04:00
Isaac Barham 25d0d0f1ff Ford: embed model path in rolling reference
Assisted-by: Codex
2026-08-27 13:08:15 -04:00
47 changed files with 816 additions and 350 deletions
@@ -8,13 +8,13 @@ on:
required: true
type: string
target_hardware:
description: 'Hardware target to compile for (qcom or chestnut)'
description: 'Hardware target to compile for (qcom or usbgpu)'
required: true
type: choice
default: 'qcom'
options:
- qcom
- chestnut
- usbgpu
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 == 'chestnut' && 'chestnut_' || '' }}v"
PREFIX="driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_' || '' }}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"
+1 -1
View File
@@ -176,7 +176,7 @@ jobs:
build_big_model:
needs: resolve
if: ${{ inputs.target == 'big' }}
runs-on: [self-hosted, chestnut]
runs-on: [self-hosted, usbgpu]
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 chestnut)'
description: 'Hardware target to compile for (qcom or usbgpu)'
required: false
type: string
default: 'qcom'
@@ -101,7 +101,7 @@ on:
default: 'qcom'
options:
- qcom
- chestnut
- usbgpu
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 == 'chestnut' && 'chestnut_v' || 'v' }}${{ inputs.json_version }}.json
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_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 chestnut)'
description: 'Hardware target to compile for (qcom or usbgpu)'
required: false
type: string
default: 'qcom'
@@ -57,7 +57,7 @@ on:
type: choice
options:
- qcom
- chestnut
- usbgpu
default: 'qcom'
@@ -102,7 +102,7 @@ jobs:
cat $GITHUB_OUTPUT
- run: |
cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "chestnut" ]; then
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; 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, chestnut]
runs-on: [self-hosted, usbgpu]
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 }}" == "chestnut" ]; then
echo "CHESTNUT build"
export CHESTNUT=1
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build"
export USBGPU=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
+9
View File
@@ -382,6 +382,7 @@ struct CarControlSP @0xa5cd762cd951a455 {
leadOne @2 :LeadData;
leadTwo @3 :LeadData;
intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement;
fordLateralPath @5 :FordLateralPath;
struct Param {
key @0 :Text;
@@ -402,6 +403,14 @@ struct CarControlSP @0xa5cd762cd951a455 {
}
}
struct FordLateralPath {
pathOffset @0 :Float32; # c0 [m]
pathAngle @1 :Float32; # c1 [rad]
curvature @2 :Float32; # c2 [1/m]
curvatureRate @3 :Float32; # c3 [1/m^2]
valid @4 :Bool;
}
struct BackupManagerSP @0xf98d843bfd7004a3 {
backupStatus @0 :Status;
restoreStatus @1 :Status;
+5 -6
View File
@@ -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"}},
{"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}},
{"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}},
{"Version", {PERSISTENT, STRING}},
// --- sunnypilot params --- //
@@ -195,16 +195,15 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// Model Manager params
{"ModelManager_ActiveBundle", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleUSBGPU", {PERSISTENT, JSON}}, //TODO-SP: kept for migration, remove on next sync?
{"ModelManager_ActiveBundleChestnut", {PERSISTENT, JSON}},
{"ModelManager_ActiveBundleUSBGPU", {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_Chestnut", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_Chestnut", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820
size 13079
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842
size 2034
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472
size 7279
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1
size 10876
+1
View File
@@ -63,5 +63,6 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct
struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement(
**remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {}))
)
struct_dataclass.fordLateralPath = structs.FordLateralPath(**remove_deprecated(struct_dict.get('fordLateralPath', {})))
return struct_dataclass
@@ -13,6 +13,7 @@ from openpilot.common.swaglog import cloudlog
from opendbc.car.car_helpers import interfaces
from opendbc.car.vehicle_model import VehicleModel
from openpilot.selfdrive.controls.lib.drive_helpers import clip_curvature
from openpilot.selfdrive.controls.lib.ford_path import FordPath, FordPathController
from openpilot.selfdrive.controls.lib.latcontrol import LatControl
from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID
from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle, STEER_ANGLE_SATURATION_THRESHOLD
@@ -52,6 +53,8 @@ class Controls(ControlsExt):
self.steer_limited_by_safety = False
self.curvature = 0.0
self.desired_curvature = 0.0
self.ford_path_controller = FordPathController()
self.ford_path = FordPath()
self.pose_calibrator = PoseCalibrator()
self.calibrated_pose: Pose | None = None
@@ -155,6 +158,11 @@ class Controls(ControlsExt):
actuators.curvature = float(lateral_output)
else:
actuators.steeringAngleDeg = float(lateral_output)
if self.CP.brand == "ford":
self.ford_path = self.ford_path_controller.update(model_v2 if self.sm.valid['modelV2'] else None,
self.desired_curvature, v_ego=CS.vEgo, active=CC.latActive,
current_curvature=self.curvature)
actuators.curvature = float(self.ford_path.curvature)
# Ensure no NaNs/Infs
for p in ACTUATOR_FIELDS:
attr = getattr(actuators, p)
@@ -0,0 +1,166 @@
from dataclasses import dataclass, fields
import math
import numpy as np
DBC_OFFSET = (-5.12, 5.11)
DBC_ANGLE = (-0.5, 0.5235)
DBC_CURVATURE = (-0.02, 0.02)
DBC_CURVATURE_RATE = (-0.001024, 0.001023)
_PATH_OFFSET_DISTANCE = 7.0
_PATH_MIN_LOOKAHEAD = 7.0
_CURVATURE_RATE_HORIZONS = (3.5, 5.0, 7.0)
_CENTERING_CURVATURE_BASEBAND = (0.003, 0.006)
_TRACKING_ERROR_DEADZONE = 0.0005
_TRACKING_ERROR_LIMIT = 0.012
_PATH_RATES = (4.0, 1.0, math.inf, 0.002)
@dataclass(frozen=True)
class FordPath:
valid: bool = False
path_offset: float = 0.0
path_angle: float = 0.0
curvature: float = 0.0
curvature_rate: float = 0.0
def _finite(value: float) -> float:
return float(value) if math.isfinite(value) else 0.0
def _sample(distance: float, distances: list[float], values: list[float]) -> float:
return float(np.interp(distance, distances, values))
def _model_path(model) -> tuple[list[float], list[float], list[float]] | None:
try:
x = [float(value) for value in model.position.x]
y = [float(value) for value in model.position.y]
heading = [float(value) for value in model.orientation.z]
except (AttributeError, TypeError, ValueError):
return None
if len(x) < 2 or len(x) != len(y) or len(x) != len(heading):
return None
if not all(math.isfinite(value) for values in (x, y, heading) for value in values):
return None
distance = [0.0]
for i in range(1, len(x)):
distance.append(distance[-1] + math.hypot(x[i] - x[i - 1], y[i] - y[i - 1]))
if distance[-1] <= 0.0:
return None
unwrapped_heading = [heading[0]]
for value in heading[1:]:
delta = (value - unwrapped_heading[-1] + math.pi) % (2.0 * math.pi) - math.pi
unwrapped_heading.append(unwrapped_heading[-1] + delta)
return distance, y, unwrapped_heading
def _curvature_rate(path: tuple[list[float], list[float], list[float]]) -> float:
distance, _, heading = path
rates = []
for requested_horizon in _CURVATURE_RATE_HORIZONS:
horizon = min(requested_horizon, distance[-1])
start = _sample(0.0, distance, heading)
midpoint = _sample(0.5 * horizon, distance, heading)
end = _sample(horizon, distance, heading)
rates.append(4.0 * (start - 2.0 * midpoint + end) / horizon ** 2)
magnitude = sum(abs(rate) for rate in rates)
if magnitude == 0.0:
return 0.0
return sorted(rates)[1] * abs(sum(rates)) / magnitude
def _curvature(path: tuple[list[float], list[float], list[float]]) -> float:
distance, _, heading = path
horizon = min(_PATH_MIN_LOOKAHEAD, distance[-1])
return (_sample(horizon, distance, heading) - _sample(0.0, distance, heading)) / horizon
def _encode_path(model, desired_curvature: float, v_ego: float, current_curvature: float | None) -> FordPath:
path = _model_path(model)
if path is None:
return FordPath()
distance, offset, heading = path
lookahead = max(_finite(v_ego), _PATH_MIN_LOOKAHEAD)
path_offset = _sample(_PATH_OFFSET_DISTANCE, distance, offset)
path_angle = _sample(lookahead, distance, heading)
model_curvature = _curvature(path)
model_curvature_rate = _curvature_rate(path)
action_curvature = _finite(desired_curvature)
requested_curvature = max((model_curvature, action_curvature), key=abs)
maneuver_residual = requested_curvature - model_curvature
path_offset += 0.5 * maneuver_residual * _PATH_OFFSET_DISTANCE ** 2
path_angle += maneuver_residual * lookahead
correction = 0.0
tracking_demand = 0.0
wheel_beyond_target = False
if current_curvature is not None:
target_curvature = action_curvature
measured_curvature = _finite(current_curvature)
tracking_error = target_curvature - measured_curvature
correction = math.copysign(max(abs(tracking_error) - _TRACKING_ERROR_DEADZONE, 0.0), tracking_error)
tracking_demand = abs(correction)
wheel_beyond_target = target_curvature * measured_curvature > 0.0 and \
abs(target_curvature) + _TRACKING_ERROR_DEADZONE < abs(measured_curvature)
correction_limit = _TRACKING_ERROR_LIMIT
if correction * target_curvature < 0.0:
correction_limit = 0.5 * abs(target_curvature)
correction = float(np.clip(correction, -correction_limit, correction_limit))
correction_offset = 0.5 * correction * _PATH_OFFSET_DISTANCE ** 2
correction_angle = correction * lookahead
if wheel_beyond_target:
path_offset = correction_offset
path_angle = correction_angle
else:
path_offset += correction_offset
path_angle += correction_angle
spatial_demand = abs(model_curvature_rate) * lookahead / 3.0
overflow_demand = max(abs(requested_curvature) - DBC_CURVATURE[1], 0.0)
maneuver_demand = max(spatial_demand, overflow_demand, tracking_demand)
maneuver_share = 1.0 if wheel_beyond_target else \
float(np.interp(maneuver_demand, _CENTERING_CURVATURE_BASEBAND, (0.0, 1.0)))
path_offset *= maneuver_share
path_angle *= maneuver_share
centering_curvature = action_curvature
return FordPath(
valid=True,
path_offset=float(np.clip(path_offset, *DBC_OFFSET)),
path_angle=float(np.clip(path_angle, *DBC_ANGLE)),
curvature=float(np.clip(centering_curvature * (1.0 - maneuver_share), *DBC_CURVATURE)),
curvature_rate=float(np.clip(model_curvature_rate * maneuver_share, *DBC_CURVATURE_RATE)),
)
class FordPathController:
"""Convert the model path directly into one vehicle-independent Ford path command."""
def __init__(self, dt: float = 0.01):
self.dt = dt
self._last_path = FordPath(valid=True)
def _limit(self, target: FordPath) -> FordPath:
values = []
for field, rate in zip(fields(FordPath)[1:], _PATH_RATES, strict=True):
previous = getattr(self._last_path, field.name)
value = getattr(target, field.name)
values.append(float(np.clip(value, previous - rate * self.dt, previous + rate * self.dt)))
self._last_path = FordPath(True, *values)
return self._last_path
def update(self, model, desired_curvature: float, *, v_ego: float = 0.0, active: bool = True,
current_curvature: float | None = None) -> FordPath:
if not active:
self._last_path = FordPath(valid=True)
return FordPath()
if model is None:
return self._limit(FordPath(valid=True))
return self._limit(_encode_path(model, desired_curvature, v_ego, current_curvature))
@@ -0,0 +1,290 @@
import math
from types import SimpleNamespace
import numpy as np
from openpilot.cereal import custom
from openpilot.selfdrive.car.helpers import convert_carControlSP
from openpilot.selfdrive.controls.lib.ford_path import DBC_CURVATURE, FordPathController
def _path(curvature: float, curvature_rate: float = 0.0, speed: float = 8.0):
t = np.linspace(0.0, 3.0, 61)
distance = speed * t
heading = curvature * distance + 0.5 * curvature_rate * distance ** 2
x = np.zeros_like(distance)
y = np.zeros_like(distance)
for i in range(1, len(distance)):
ds = distance[i] - distance[i - 1]
average_heading = 0.5 * (heading[i] + heading[i - 1])
x[i] = x[i - 1] + ds * math.cos(average_heading)
y[i] = y[i - 1] + ds * math.sin(average_heading)
return SimpleNamespace(
position=SimpleNamespace(t=t.tolist(), x=x.tolist(), y=y.tolist()),
orientation=SimpleNamespace(z=heading.tolist()),
)
def _equivalent_curvature(path, distance: float = 7.0) -> float:
offset = path.path_offset + path.path_angle * distance + 0.5 * path.curvature * distance ** 2 + \
path.curvature_rate * distance ** 3 / 6.0
return 2.0 * offset / distance ** 2
def _command(model, desired_curvature: float, *, v_ego: float = 0.0, current_curvature: float | None = None):
return FordPathController(dt=1.0).update(model, desired_curvature, v_ego=v_ego, current_curvature=current_curvature)
def test_steady_arc_uses_c2_without_fast_pose_fields():
path = _command(_path(0.008), 0.008, v_ego=8.0)
assert path.valid
assert abs(path.path_offset) < 1e-9
assert abs(path.path_angle) < 1e-9
assert np.isclose(path.curvature, 0.008, atol=5e-5)
assert abs(path.curvature_rate) < 1e-5
def test_sunnypilot_path_message_round_trip():
message = custom.CarControlSP.new_message()
message.fordLateralPath.pathOffset = 0.3
message.fordLateralPath.pathAngle = -0.2
message.fordLateralPath.curvature = 0.008
message.fordLateralPath.curvatureRate = -0.0004
message.fordLateralPath.valid = True
path = convert_carControlSP(message.as_reader()).fordLateralPath
assert np.isclose(path.pathOffset, 0.3)
assert np.isclose(path.pathAngle, -0.2)
assert np.isclose(path.curvature, 0.008)
assert np.isclose(path.curvatureRate, -0.0004)
assert path.valid
def test_tight_arc_uses_signed_forward_pose_without_slow_c2():
left = _command(_path(0.04), 0.04, v_ego=8.0)
right = _command(_path(-0.04), -0.04, v_ego=8.0)
assert left.curvature == 0.0
assert right.curvature == 0.0
assert abs(left.curvature_rate) < 1e-4
assert abs(right.curvature_rate) < 1e-4
assert left.path_angle > 0.06
assert right.path_angle < -0.06
assert left.path_offset > 0.5
assert right.path_offset < -0.5
def test_c2_does_not_increase_while_tight_curve_unwinds():
curvatures = (0.04, 0.018, 0.016, 0.014, 0.012, 0.010, 0.008, 0.006, 0.0)
measured = (0.04,) + curvatures[:-1]
commands = [_command(_path(curvature), curvature, v_ego=8.0, current_curvature=actual).curvature
for curvature, actual in zip(curvatures, measured, strict=True)]
assert np.all(np.diff(commands) <= 1e-9)
def test_fresh_model_replaces_previous_path_without_hidden_state():
controller = FordPathController(dt=1.0)
initial = controller.update(_path(0.04), 0.04, v_ego=8.0)
replanned = controller.update(_path(0.0), 0.0, v_ego=8.0)
assert initial.path_offset > 0.5
assert replanned == FordPathController().update(_path(0.0), 0.0, v_ego=8.0)
def test_s_turn_reverses_fast_fields_while_c2_is_bounded():
controller = FordPathController(dt=0.05)
controller.update(_path(0.04), 0.04, v_ego=8.0)
controller.update(_path(0.04), 0.04, v_ego=8.0)
outputs = []
for frame_id in range(5):
model = _path(-0.02)
model.frameId = frame_id + 1
model.timestampEof = frame_id + 1
outputs.append(controller.update(model, -0.02, v_ego=8.0, current_curvature=0.02))
assert all(path.valid for path in outputs)
assert all(DBC_CURVATURE[0] <= path.curvature <= DBC_CURVATURE[1] for path in outputs)
assert all(path.curvature <= 0.0 for path in outputs)
assert outputs[-1].path_angle < -0.03
assert outputs[-1].path_offset < 0.0
def test_reversal_does_not_add_software_persistence_to_centering_c2():
controller = FordPathController()
assert controller.update(_path(0.002), 0.002, v_ego=8.0).curvature > 0.0
reversing = controller.update(_path(-0.02), -0.02, v_ego=8.0)
assert reversing.curvature <= 0.0
def test_requested_turn_is_not_cancelled_by_previous_path():
controller = FordPathController(dt=1.0)
previous = _path(-0.02, speed=3.0)
previous.frameId = 1
previous.timestampEof = 1
controller.update(previous, -0.02, v_ego=3.0, current_curvature=-0.01)
requested = _path(0.02, speed=3.0)
requested.frameId = 2
requested.timestampEof = 2
command = controller.update(requested, 0.02, v_ego=3.0, current_curvature=0.007)
assert command.path_offset >= 0.0
assert command.path_angle >= 0.0
assert command.curvature >= 0.0
assert _equivalent_curvature(command) >= 0.02
def test_short_low_speed_model_uses_available_path_endpoint():
command = FordPathController().update(_path(0.02, speed=1.0), 0.02, v_ego=1.0, current_curvature=0.0)
assert command.valid
assert command.path_offset > 0.0
assert command.path_angle > 0.0
def test_action_demand_exposes_forward_path_authority():
command = FordPathController().update(_path(0.002), 0.0055, v_ego=8.0, current_curvature=0.0005)
assert _equivalent_curvature(command) >= 0.004
def test_minor_curve_uses_c2_when_tracking_is_close():
command = FordPathController(dt=1.0).update(_path(0.005), 0.005, v_ego=8.0, current_curvature=0.0048)
assert abs(command.path_offset) < 1e-9
assert abs(command.path_angle) < 1e-9
assert command.curvature > 0.0049
def test_minor_changing_curve_does_not_emit_ungated_c3():
command = FordPathController(dt=1.0).update(_path(0.005, 0.0003), 0.005, v_ego=8.0, current_curvature=0.0048)
assert abs(command.path_offset) < 1e-9
assert abs(command.path_angle) < 1e-9
assert command.curvature > 0.0049
assert command.curvature_rate == 0.0
def test_c2_uses_stable_action_curvature_not_independent_model_fit():
controller = FordPathController(dt=1.0)
first = controller.update(_path(0.004), 0.002, v_ego=8.0, current_curvature=0.002)
second = controller.update(_path(0.006), 0.002, v_ego=8.0, current_curvature=0.002)
assert np.isclose(first.curvature, 0.002)
assert np.isclose(second.curvature, 0.002)
def test_action_curvature_corrects_stale_opposing_model_at_low_speed():
command = FordPathController().update(_path(-0.001, speed=1.0), 0.005, v_ego=1.0, current_curvature=0.001)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
assert command.curvature >= 0.0
assert _equivalent_curvature(command) >= 0.004
def test_small_action_sign_noise_does_not_reverse_a_strong_model_path():
command = FordPathController(dt=1.0).update(_path(0.04), -0.0005, v_ego=6.0, current_curvature=0.02)
assert command.path_offset > 0.0
assert command.path_angle > 0.0
assert _equivalent_curvature(command) > 0.02
def test_measured_curvature_after_path_exit_commands_countersteer():
command = FordPathController().update(_path(0.0), 0.0, v_ego=8.0, current_curvature=0.006)
assert command.curvature == 0.0
assert command.path_offset < 0.0
assert command.path_angle < 0.0
def test_measured_curvature_countersteers_when_beyond_modeled_arc():
controller = FordPathController(dt=1.0)
command = controller.update(_path(0.004), 0.003, v_ego=8.0, current_curvature=0.012)
tracking = FordPathController(dt=1.0).update(_path(0.004), 0.003, v_ego=8.0, current_curvature=0.004)
assert command.path_offset < 0.0
assert command.path_angle < 0.0
assert command.path_angle < tracking.path_angle
assert command.curvature == 0.0
def test_model_reversal_suppresses_old_c2_and_countersteers():
reversing = FordPathController().update(_path(-0.02), -0.0005, v_ego=8.0, current_curvature=0.01)
assert reversing.curvature <= 0.0
assert reversing.path_angle < 0.0
def test_reversal_noise_band_is_continuous():
inside = FordPathController(dt=1.0).update(_path(0.02), -0.000099, v_ego=8.0, current_curvature=0.01)
outside = FordPathController(dt=1.0).update(_path(0.02), -0.000101, v_ego=8.0, current_curvature=0.01)
assert abs(outside.path_angle - inside.path_angle) < 0.005
def test_curvature_error_increases_forward_pose_command_while_behind():
behind = FordPathController(dt=1.0).update(_path(0.008), 0.008, v_ego=15.0, current_curvature=0.0)
tracking = FordPathController(dt=1.0).update(_path(0.008), 0.008, v_ego=15.0, current_curvature=0.008)
assert behind.path_offset > tracking.path_offset + 0.01
assert behind.path_angle > tracking.path_angle + 0.015
assert behind.curvature == 0.0
assert tracking.curvature > 0.007
assert np.isclose(behind.curvature_rate, tracking.curvature_rate)
def test_measured_wheel_beyond_action_countersteers_model_arc():
controller = FordPathController()
controller.update(_path(0.02), 0.02, v_ego=15.0, current_curvature=0.02)
outputs = [controller.update(_path(0.02), 0.003, v_ego=15.0, current_curvature=0.01) for _ in range(4)]
unwinding = outputs[-1]
assert unwinding.curvature == 0.0
assert unwinding.path_angle < 0.0
def test_action_c2_remains_active_for_centering():
centering = FordPathController(dt=1.0).update(_path(0.002), 0.002, v_ego=15.0, current_curvature=0.002)
assert centering.curvature > 0.001
assert abs(centering.path_offset) < 1e-9
assert abs(centering.path_angle) < 1e-9
def test_tight_turn_from_stop_builds_bounded_forward_pose_authority():
controller = FordPathController()
outputs = [controller.update(_path(0.04), 0.04, v_ego=0.0, current_curvature=0.0) for _ in range(20)]
path = outputs[-1]
assert path.curvature == 0.0
assert path.path_offset > 0.7
assert path.path_angle > 0.15
assert np.max(np.abs(np.diff([output.path_offset for output in outputs]))) <= 0.04 + 1e-9
assert np.max(np.abs(np.diff([output.path_angle for output in outputs]))) <= 0.01 + 1e-9
def test_curvature_feedback_is_bounded_for_bad_measurement():
bounded = FordPathController().update(_path(0.008), 0.008, v_ego=15.0, current_curvature=-0.02)
corrupted = FordPathController().update(_path(0.008), 0.008, v_ego=15.0, current_curvature=-1.0)
assert np.isclose(corrupted.path_angle, bounded.path_angle)
def test_invalid_model_ramps_pose_to_zero_while_remaining_in_extended_mode():
controller = FordPathController()
for _ in range(10):
active = controller.update(_path(0.04), 0.04, v_ego=12.0)
missing = controller.update(None, 0.0, v_ego=12.0)
assert active.path_offset > 0.0
assert missing.valid
assert np.isclose(active.path_offset - missing.path_offset, 0.04)
assert missing.curvature == 0.0
assert not controller.update(_path(0.0), 0.0, v_ego=12.0, active=False).valid
+13 -13
View File
@@ -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, chestnut_present, modeld_pkl_path
from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_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},
'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'}
},
'openpilot.selfdrive.modeld.dmonitoringmodeld': {
'default': {'DEV': tg_backend}
},
}
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'
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'
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
chestnut_lock = File("models/.chestnut.lock").abspath
usbgpu_lock = File("models/.usb_gpu.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 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)
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)
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, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")]
actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu 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 chestnut:
lenv.SideEffect(chestnut_lock, node)
if usbgpu:
lenv.SideEffect(usbgpu_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, chestnut=False)['DEV']
self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV']
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes']
+7 -7
View File
@@ -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, chestnut: bool):
def get_tg_input_devices(process_name: str, usbgpu: bool):
with open(TG_INPUT_DEVICES_PATH) as f:
return json.load(f)[process_name]['default' if not chestnut else 'chestnut']
return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu']
def modeld_pkl_path(chestnut: bool):
prefix = 'big_' if chestnut else ''
def modeld_pkl_path(usbgpu: bool):
prefix = 'big_' if usbgpu 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 chestnut_present() -> bool:
def usbgpu_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 chestnut_present() -> bool:
pass
return False
def chestnut_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file()
def usbgpu_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file()
+30 -32
View File
@@ -1,9 +1,7 @@
#!/usr/bin/env python3
from collections.abc import Callable
import ctypes
from functools import cached_property
import os
os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom
os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom
from tinygrad.tensor import Tensor
from tinygrad.device import Device
import struct
@@ -32,7 +30,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 chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_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
@@ -96,10 +94,8 @@ 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_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:])
metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics
metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics
self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT],
'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM],
'powerDrawW': metrics.AverageSocketPower,
@@ -145,18 +141,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, chestnut: bool):
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
ModelStateBase.__init__(self)
input_devices = get_tg_input_devices(PROCESS_NAME, chestnut)
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut)))
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
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.chestnut = chestnut
self.usbgpu = usbgpu
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)
@@ -172,7 +168,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], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]:
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3]
@@ -196,11 +192,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.chestnut and not np.all(np.isfinite(model_output)):
raise RuntimeError("model output not finite")
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
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']]
@@ -222,12 +218,12 @@ class ModelState(ModelStateBase):
def main(demo=False):
cloudlog.warning("modeld init")
CHESTNUT = chestnut_present() and chestnut_compiled()
if CHESTNUT:
USBGPU = usbgpu_present() and usbgpu_compiled()
if USBGPU:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("ChestnutLoading", CHESTNUT)
params.remove("ChestnutActive")
params.put_bool("UsbGpuLoading", USBGPU)
params.remove("UsbGpuActive")
config_realtime_process(7, 54)
@@ -257,7 +253,7 @@ def main(demo=False):
st = time.monotonic()
cloudlog.warning("loading model")
model = None
if CHESTNUT:
if USBGPU:
big_model = None
def load_big():
nonlocal big_model
@@ -271,23 +267,23 @@ def main(demo=False):
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
params.put_bool("ChestnutActive", model is not None)
params.put_bool("UsbGpuActive", model is not None)
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None
if model is None:
model = small_model
params.put_bool("ChestnutLoading", False)
params.put_bool("UsbGpuLoading", 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 CHESTNUT else [])
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState()
params = Params()
chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None
chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None
# setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ)
@@ -397,15 +393,13 @@ def main(demo=False):
mt1 = time.perf_counter()
try:
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)
model_output = model.run(bufs, transforms, inputs)
except Exception:
if not params.get_bool("ChestnutActive"):
if not params.get_bool("UsbGpuActive"):
raise
# fallback to small model
cloudlog.exception("big model failed, fall back to small")
params.put_bool("ChestnutActive", False)
params.put_bool("UsbGpuActive", False)
assert small_model is not None
model = small_model
if chestnut_state is not None:
@@ -425,7 +419,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.chestnut
modelv2_send.modelV2.big = model.usbgpu
desire_state = modelv2_send.modelV2.meta.desireState
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
@@ -447,6 +441,10 @@ 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
+4 -4
View File
@@ -195,17 +195,17 @@ class SelfdriveD(CruiseHelper):
self.events.add(EventName.joystickDebug)
self.startup_event = None
loading = self.params.get_bool("ChestnutLoading")
loading = self.params.get_bool("UsbGpuLoading")
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("ChestnutActive")
chestnut_present = self.sm['deviceState'].chestnutPresent
big_active = self.params.get("UsbGpuActive")
usbgpu_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 chestnut_present)
big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present)
if big_failed and not self.big_model_failed:
self.events.add(EventName.bigModelFailed)
self.big_model_failed = big_failed
+8 -8
View File
@@ -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, ChestnutState
from openpilot.selfdrive.ui.ui_state import ui_state
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._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40))
self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40))
self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37))
self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37))
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._chestnut_icon,
self._chestnut_failed_icon,
self._egpu_icon,
self._egpu_icon_gray,
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_chestnut_visibility()
self._set_egpu_visibility()
else:
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._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._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(bool(ui_state.is_body))
@@ -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, ChestnutState
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
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,7 +107,8 @@ class HudRenderer(Widget):
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
self._engaged: bool = False
self._chestnut_fade_time: float = 0
self._small_model_engaged: bool = False
self._egpu_fade_time: float = 0
self._can_draw_top_icons = True
self._show_wheel_critical = False
@@ -123,15 +124,17 @@ 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_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._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._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._chestnut_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)
def set_wheel_critical_icon(self, critical: bool):
"""Set the wheel icon to critical or normal state."""
@@ -162,10 +165,13 @@ 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
@@ -185,7 +191,8 @@ class HudRenderer(Widget):
if self.is_cruise_set:
self._draw_set_speed(rect)
self._draw_model_source(rect)
if ui_state.usbgpu and ui_state.usbgpu_compiled:
self._draw_model_source(rect)
self._draw_steering_wheel(rect)
@@ -193,24 +200,30 @@ class HudRenderer(Widget):
if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame:
return
loading = ui_state.chestnut_state == ChestnutState.LOADING
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)
if loading:
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
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
opacity = 1.0
else:
return
icon = self._txt_egpu_green
opacity = 1.0
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 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 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("chestnut")
callback=lambda: self._open_source_dialog("usbgpu")
)
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_Chestnut", 0),
ui_state.params.put("ModelManager_LastSyncTime_USBGPU", 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")), ("chestnut", tr("big"))):
for source, label in (("qcom", tr("small")), ("usbgpu", 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 == "chestnut":
if source == "usbgpu":
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.chestnut_present:
if not ui_state.usbgpu:
return ""
big_bundle = get_selected_bundle(ui_state.params, "chestnut")
big_name = big_bundle.internalName if big_bundle else default_model_name("chestnut")
big_bundle = get_selected_bundle(ui_state.params, "usbgpu")
big_name = big_bundle.internalName if big_bundle else default_model_name("usbgpu")
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 chestnut is ready.").format(big_name)
return tr("{} will drive when the eGPU 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", "chestnut")}
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
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, "chestnut")):
for item, item_source in ((self.small_model_item, "qcom"), (self.big_model_item, "usbgpu")):
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
@@ -21,8 +21,8 @@ METRIC_MARGIN = 30
METRIC_START_Y = 300
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
CHESTNUT_ICON_WIDTH = 180
CHESTNUT_ICON_HEIGHT = 133
EGPU_ICON_WIDTH = 180
EGPU_ICON_HEIGHT = 133
# Color scheme
@@ -59,11 +59,10 @@ class MetricData:
class SidebarSP:
def __init__(self):
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
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)
# gray state uses the default art (chestnut_gray.png removed upstream)
self._chestnut_gray_img = gui_app.texture("icons_mici/chestnut.png", CHESTNUT_ICON_WIDTH, CHESTNUT_ICON_HEIGHT)
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)
def _update_sunnylink_status(self):
if not ui_state.params.get_bool("SunnylinkEnabled"):
@@ -94,19 +93,19 @@ class SidebarSP:
if not ui_state.sm["deviceState"].chestnutPresent:
return default_img, default_pos, 1.0
big_model_selected = ui_state.chestnut_compiled or ui_state.model_runner_tinygrad
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.chestnut_loading or (big_model_selected and ui_state.started and ui_state.chestnut_active is None)
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
if loading:
icon = self._chestnut_default_img
icon = self._egpu_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._chestnut_orange_img, 1.0
icon, opacity = self._egpu_orange_img, 1.0
elif big_model_selected:
icon, opacity = self._chestnut_green_img, 1.0
icon, opacity = self._egpu_green_img, 1.0
else:
icon, opacity = self._chestnut_gray_img, 1.0
icon, opacity = self._egpu_gray_img, 1.0
x = HOME_BTN.x + (HOME_BTN.width - icon.width) / 2
y = HOME_BTN.y + (HOME_BTN.height - icon.height) / 2
@@ -19,30 +19,35 @@ 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._chestnut_icon_gray = IconWidget("icons_mici/chestnut.png", (68, 40))
self._chestnut_icon_gray.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_icon_gray)
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)
def _set_chestnut_visibility(self):
def _set_egpu_visibility(self):
chestnut = ui_state.sm["deviceState"].chestnutPresent
if not chestnut:
self._chestnut_icon.set_visible(False)
self._chestnut_failed_icon.set_visible(False)
self._chestnut_icon_gray.set_visible(False)
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.chestnut_compiled or ui_state.model_runner_tinygrad
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.chestnut_loading or (big_model_selected and ui_state.started and ui_state.chestnut_active is None)
loading = ui_state.usbgpu_loading or (big_model_selected and ui_state.started and ui_state.usbgpu_active is None)
if loading:
self._chestnut_icon_gray._opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))
self._chestnut_icon_gray.set_visible(True)
self._chestnut_icon.set_visible(False)
self._chestnut_failed_icon.set_visible(False)
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._chestnut_icon_gray._opacity = 1.0
self._chestnut_icon_gray.set_visible(not big_model_selected)
self._chestnut_icon.set_visible(big_model_selected and not big_model_failed)
self._chestnut_failed_icon.set_visible(big_model_selected and big_model_failed)
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)
@@ -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, "chestnut")
carry_display = big.displayName if big else default_model_name("chestnut")
big = get_selected_bundle(ui_state.params, "usbgpu")
carry_display = big.displayName if big else default_model_name("usbgpu")
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 == "chestnut" else tr("big model")
header = tr("small model") if source == "usbgpu" 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")), ("chestnut", tr("big models"))):
for source, label in (("qcom", tr("small models")), ("usbgpu", 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:
@@ -22,7 +22,7 @@ class HudRendererSP(HudRenderer):
def _render(self, rect: rl.Rectangle) -> None:
super()._render(rect)
if ui_state.chestnut_present and not ui_state.chestnut_compiled and ui_state.model_runner_tinygrad:
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)
+15 -15
View File
@@ -11,8 +11,8 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_BIG_MODEL, DEFAULT_MO
def active_source() -> str:
return get_active_source(chestnut=ui_state.chestnut_present,
chestnut_active=ui_state.chestnut_active, chestnut_loading=ui_state.chestnut_loading,
return get_active_source(usbgpu=ui_state.usbgpu,
usbgpu_active=ui_state.usbgpu_active, usbgpu_loading=ui_state.usbgpu_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 == 'chestnut' else DEFAULT_MODEL
return DEFAULT_BIG_MODEL if source == 'usbgpu' else DEFAULT_MODEL
def default_model_name(source: str) -> str:
@@ -32,10 +32,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.chestnut_present and ui_state.big_model_failed:
if ui_state.started and ui_state.usbgpu and ui_state.big_model_failed:
return 'failed'
big_selected = ui_state.chestnut_compiled or ui_state.model_runner_tinygrad
if ui_state.chestnut_loading or (big_selected and ui_state.started and ui_state.chestnut_active is None):
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
@@ -45,14 +45,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 == "chestnut":
bundle = get_selected_bundle(ui_state.params, "chestnut")
if source == "usbgpu":
bundle = get_selected_bundle(ui_state.params, "usbgpu")
if bundle:
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:
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:
name = default_model_name("qcom")
return "qcom", name, name
return None, None, None
@@ -66,7 +66,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", "chestnut")}
source_bundles = {source: bundles_for_source(source) for source in ("qcom", "usbgpu")}
if resolved := resolve_bundle_by_ref(ref, source_bundles):
return resolved[0].internalName
return None
@@ -79,7 +79,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 == "chestnut" else "chestnut"
other = "qcom" if source == "usbgpu" else "usbgpu"
active_bundle = get_selected_bundle(ui_state.params, source)
other_bundle = get_selected_bundle(ui_state.params, other)
@@ -152,8 +152,8 @@ class UIStateSP:
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
self._enforce_constraints()
source = get_active_source(chestnut=self.chestnut_present, chestnut_active=self.chestnut_active,
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
source = get_active_source(usbgpu=self.usbgpu, usbgpu_active=self.usbgpu_active,
usbgpu_loading=self.usbgpu_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"
self.blindspot = self.params.get_bool("BlindSpot")
+14 -46
View File
@@ -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 chestnut_compiled
from openpilot.selfdrive.modeld.helpers import usbgpu_compiled
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
@@ -28,15 +28,6 @@ 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
@@ -91,11 +82,10 @@ 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.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.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.started: bool = False
self.ignition: bool = False
self.recording_audio: bool = False
@@ -125,11 +115,11 @@ class UIState(UIStateSP):
@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.chestnut_active is False or
return (self.usbgpu_active is False or
not self.sm['deviceState'].chestnutPresent or
(self.chestnut_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and
(self.usbgpu_active is True and self.sm.recv_frame['modelV2'] > self.started_frame and
not self.sm.alive['modelV2']) or
(self.chestnut_active is None and self.sm.recv_frame['modelV2'] > self.started_frame))
(self.usbgpu_active is None and self.sm.recv_frame['modelV2'] > self.started_frame))
@property
def engaged(self) -> bool:
@@ -150,7 +140,6 @@ class UIState(UIStateSP):
self.sm.update(0)
self._update_state()
self._update_status()
self._update_chestnut_state()
device.update()
UIStateSP.update(self)
@@ -214,35 +203,12 @@ 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
@@ -259,10 +225,12 @@ 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")
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")
# 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")
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('CHESTNUT'):
if 'USB' in os.getenv('DEV', '') or os.getenv('USBGPU'):
from openpilot.system.hardware.chestnut.flash import link_up
for _ in range(10):
if link_up():
+21 -21
View File
@@ -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 chestnut_present, load_oob
from openpilot.selfdrive.modeld.helpers import usbgpu_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, chestnut: bool = False):
def __init__(self, cam_w: int, cam_h: int, usbgpu: 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(chestnut=chestnut)
model_bundle = get_active_bundle(usbgpu=usbgpu)
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.chestnut = chestnut
self.usbgpu = usbgpu
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.chestnut else self.WARP_DEV
self.DEV = 'AMD' if self.usbgpu 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.chestnut:
if self.usbgpu:
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.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))):
if self.usbgpu 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)
CHESTNUT = chestnut_present()
if CHESTNUT:
USBGPU = usbgpu_present()
if USBGPU:
os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000'
params = Params()
params.put_bool("ChestnutLoading", CHESTNUT)
params.remove("ChestnutActive")
params.put_bool("UsbGpuLoading", USBGPU)
params.remove("UsbGpuActive")
# visionipc clients
while True:
@@ -362,31 +362,31 @@ def main(demo=False):
st = time.monotonic()
model = None
if CHESTNUT:
if USBGPU:
import threading
def load():
nonlocal model
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True)
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=True)
t = threading.Thread(target=load, daemon=True)
t.start()
t.join(60)
if model is None:
params.put_bool("ChestnutActive", False)
raise RuntimeError("chestnut model load failed or timed out (60s)")
params.put_bool("ChestnutActive", True)
params.put_bool("UsbGpuActive", False)
raise RuntimeError("eGPU model load failed or timed out (60s)")
params.put_bool("UsbGpuActive", True)
else:
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False)
model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, usbgpu=False)
params.put_bool("ChestnutLoading", False)
params.put_bool("UsbGpuLoading", False)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if CHESTNUT else [])
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutState"] if USBGPU else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"])
publish_state = PublishState()
chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None
chestnut_state = ChestnutState(pm, USBGPU) if USBGPU 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.chestnut
modelv2_send.modelV2.big = model.usbgpu
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, *, chestnut=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: 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)
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, *, chestnut=None: bundle)
monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: 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)
with self.assertRaisesRegex(AssertionError, "No driving pkl found"):
ModelState(cam_w=CAM_W, cam_h=CAM_H)
+2 -2
View File
@@ -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.chestnut_present
and (ui_state.chestnut_active or ui_state.chestnut_loading or ui_state.is_offroad()))
show_big_model = (ui_state.usbgpu
and (ui_state.usbgpu_active or ui_state.usbgpu_loading or ui_state.is_offroad()))
return DEFAULT_BIG_MODEL if show_big_model else DEFAULT_MODEL
+7 -7
View File
@@ -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_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json"
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v22.json"
MODEL_SOURCES = {
"qcom": (MODEL_URL, ""),
"chestnut": (MODEL_URL_CHESTNUT, "_Chestnut"),
"usbgpu": (MODEL_URL_USBGPU, "_USBGPU"),
}
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,
"chestnut": self.MODEL_URL_CHESTNUT,
"usbgpu": self.MODEL_URL_USBGPU,
}, block=True)
@staticmethod
def active_source(chestnut_present: bool) -> str:
return "chestnut" if chestnut_present else "qcom"
return "usbgpu" 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 == "chestnut":
if source == "usbgpu":
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 chestnut_present
from openpilot.selfdrive.modeld.helpers import usbgpu_present
params = Params()
model_fetcher = ModelFetcher(params)
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(chestnut_present()))
bundles = model_fetcher.get_bundles_for_source(ModelFetcher.active_source(usbgpu_present()))
for bundle in bundles:
for model in bundle.models:
model_overrides = {override.key: override.value for override in bundle.overrides}
+11 -11
View File
@@ -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 chestnut_present
from openpilot.selfdrive.modeld.helpers import usbgpu_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",
"chestnut": "ModelManager_ActiveBundleChestnut",
"usbgpu": "ModelManager_ActiveBundleUSBGPU",
}
_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(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_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_bundle(params: Params | None = None, *, chestnut: bool | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
def get_active_bundle(params: Params | None = None, *, usbgpu: 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(chestnut=chestnut))
return get_selected_bundle(params, get_active_source(usbgpu=usbgpu))
def resolve_bundle_by_ref(
+3 -3
View File
@@ -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, chestnut=self.chestnut_present)
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, usbgpu=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, chestnut=self.chestnut_present)
self.active_bundle = get_active_bundle(self.params, usbgpu=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, chestnut=self.chestnut_present)
self.active_bundle = get_active_bundle(self.params, usbgpu=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_ActiveBundleChestnut" not in store, "qcom download must not touch the chestnut slot"
assert "ModelManager_ActiveBundleUSBGPU" not in store, "qcom download must not touch the usbgpu 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_chestnut_slot(self):
"""A download resolved to the chestnut source writes the chestnut active bundle slot only."""
def test_download_writes_usbgpu_slot(self):
"""A download resolved to the usbgpu source writes the usbgpu 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, "chestnut"))
asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "usbgpu"))
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 "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 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], "chestnut": []}) == (small, "qcom")
assert resolve_bundle_by_ref("small", {"qcom": [small], "usbgpu": []}) == (small, "qcom")
def test_chestnut_ref_resolves_to_chestnut_slot(self):
def test_usbgpu_ref_resolves_to_usbgpu_slot(self):
big = self._bundle("big")
assert resolve_bundle_by_ref("big", {"qcom": [], "chestnut": [big]}) == (big, "chestnut")
assert resolve_bundle_by_ref("big", {"qcom": [], "usbgpu": [big]}) == (big, "usbgpu")
def test_unknown_ref_returns_none(self):
source_bundles = {"qcom": [self._bundle("small")], "chestnut": []}
source_bundles = {"qcom": [self._bundle("small")], "usbgpu": []}
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 (chestnut) bundles carry `is_big: true` in the manifest JSON."""
Big (usbgpu) 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, chestnut_manifest):
def _make_params(self, qcom_manifest, usbgpu_manifest):
params = mock.MagicMock()
def get(key):
if key == "ModelManager_ModelsCache":
return qcom_manifest
if key == "ModelManager_ModelsCache_Chestnut":
return chestnut_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
if key == "ModelManager_ModelsCache_USBGPU":
return usbgpu_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
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) == "chestnut"
assert ModelFetcher.active_source(True) == "usbgpu"
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("chestnut")] == ["bbb"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["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")
chestnut_bundles = get_cached_bundles(params, "chestnut")
usbgpu_bundles = get_cached_bundles(params, "usbgpu")
assert [b.ref for b in qcom_bundles] == ["aaa"]
assert [b.ref for b in chestnut_bundles] == ["bbb"]
assert [b.ref for b in usbgpu_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, "chestnut") == []
assert get_cached_bundles(params, "usbgpu") == []
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,
"chestnut": ModelFetcher.MODEL_URL_CHESTNUT,
"usbgpu": ModelFetcher.MODEL_URL_USBGPU,
}
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 (chestnut) models. A mismatched cache is
`is_big` flag in the JSON marks the big (usbgpu) 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, chestnut_manifest):
def _make_params(self, qcom_manifest, usbgpu_manifest):
params = mock.MagicMock()
def get(key):
if key == "ModelManager_ModelsCache":
return qcom_manifest
if key == "ModelManager_ModelsCache_Chestnut":
return chestnut_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_Chestnut"):
if key == "ModelManager_ModelsCache_USBGPU":
return usbgpu_manifest
if key in ("ModelManager_LastSyncTime", "ModelManager_LastSyncTime_USBGPU"):
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_chestnut_cache_without_big_models_is_refetched(self):
def test_usbgpu_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("chestnut")
bundles = fetcher.get_bundles_for_source("usbgpu")
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("chestnut")] == ["bbb"]
assert [bundle.ref for bundle in fetcher.get_bundles_for_source("usbgpu")] == ["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, chestnut=None):
def _params(self, qcom=None, usbgpu=None):
params = mock.MagicMock()
def get(key, *args, **kwargs):
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleChestnut": chestnut}.get(key)
return {"ModelManager_ActiveBundle": qcom, "ModelManager_ActiveBundleUSBGPU": usbgpu}.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.chestnut_present", return_value=False):
validate_active_bundles(params, {"qcom": [], "chestnut": []})
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
validate_active_bundles(params, {"qcom": [], "usbgpu": []})
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"), chestnut=big_raw)
params = self._params(qcom=self._raw_bundle("gone"), usbgpu=big_raw)
catalog = {"qcom": [custom.ModelManagerSP.ModelBundle(**self._raw_bundle("other"))],
"chestnut": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
"usbgpu": [custom.ModelManagerSP.ModelBundle(**big_raw)]}
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_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: chestnut when a GPU is
"""The effective active bundle is the active source's slot: usbgpu 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, chestnut=None):
def _params(self, qcom=None, usbgpu=None):
params = mock.MagicMock()
def get(key, *args, **kwargs):
if key == "ModelManager_ActiveBundle":
return qcom
if key == "ModelManager_ActiveBundleChestnut":
return chestnut
if key == "ModelManager_ActiveBundleUSBGPU":
return usbgpu
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"), chestnut=self._raw_bundle("big"))
params = self._params(qcom=self._raw_bundle("small"), usbgpu=self._raw_bundle("big"))
assert get_selected_bundle(params, "qcom").ref == "small"
assert get_selected_bundle(params, "chestnut").ref == "big"
assert get_selected_bundle(params, "usbgpu").ref == "big"
def test_no_gpu_uses_qcom_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=False):
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):
assert get_active_bundle(params).ref == "small"
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):
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):
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"), chestnut=None)
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
params = self._params(qcom=self._raw_bundle("small"), usbgpu=None)
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_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.chestnut_present", return_value=False):
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
assert get_active_source() == "qcom"
def test_runtime_gpu_present(self):
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=True):
assert get_active_source() == "chestnut"
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=True):
assert get_active_source() == "usbgpu"
def test_display_offroad_gpu_present_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=True) == "chestnut"
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=True) == "usbgpu"
def test_display_onroad_gpu_loading_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=True, offroad=False) == "chestnut"
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=True, offroad=False) == "usbgpu"
def test_display_onroad_gpu_active_shows_big(self):
assert get_active_source(chestnut=True, chestnut_active=True, chestnut_loading=False, offroad=False) == "chestnut"
assert get_active_source(usbgpu=True, usbgpu_active=True, usbgpu_loading=False, offroad=False) == "usbgpu"
def test_display_onroad_gpu_idle_shows_small(self):
assert get_active_source(chestnut=True, chestnut_active=False, chestnut_loading=False, offroad=False) == "qcom"
assert get_active_source(usbgpu=True, usbgpu_active=False, usbgpu_loading=False, offroad=False) == "qcom"
def test_display_active_none_is_idle(self):
assert get_active_source(chestnut=True, chestnut_active=None, chestnut_loading=False, offroad=False) == "qcom"
assert get_active_source(usbgpu=True, usbgpu_active=None, usbgpu_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_ActiveBundleChestnut": self._raw_bundle("big")}.get(key)
with mock.patch("openpilot.sunnypilot.models.helpers.chestnut_present", return_value=False):
"ModelManager_ActiveBundleUSBGPU": self._raw_bundle("big")}.get(key)
with mock.patch("openpilot.sunnypilot.models.helpers.usbgpu_present", return_value=False):
assert get_active_bundle(params).ref == "small"
assert get_selected_bundle(params, get_active_source(chestnut=True, chestnut_active=False,
chestnut_loading=False, offroad=True)).ref == "big"
assert get_selected_bundle(params, get_active_source(usbgpu=True, usbgpu_active=False,
usbgpu_loading=False, offroad=True)).ref == "big"
@unittest.skipUnless(os.environ.get('RUN_INTEGRATION_TESTS'), 'requires external network')
@@ -104,6 +104,14 @@ class ControlsExt(ModelStateBase):
CC_SP.intelligentCruiseButtonManagement.sendButton = icbm_src.sendButton
CC_SP.intelligentCruiseButtonManagement.vTarget = icbm_src.vTarget
ford_path = getattr(self, 'ford_path', None)
if ford_path is not None:
CC_SP.fordLateralPath.valid = ford_path.valid
CC_SP.fordLateralPath.pathOffset = ford_path.path_offset
CC_SP.fordLateralPath.pathAngle = ford_path.path_angle
CC_SP.fordLateralPath.curvature = ford_path.curvature
CC_SP.fordLateralPath.curvatureRate = ford_path.curvature_rate
return CC_SP
@staticmethod
@@ -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["chestnut_active"] = params.get_bool("ChestnutActive")
schema["usbgpu_active"] = params.get_bool("UsbGpuActive")
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception:
+1 -1
View File
@@ -65,7 +65,7 @@ def sp_stats(end_event):
'MadsSteeringMode',
'MadsUnifiedEngagementMode',
'ModelManager_ActiveBundle',
'ModelManager_ActiveBundleChestnut',
'ModelManager_ActiveBundleUSBGPU',
'ModelManager_Favs',
'EnableSunnylinkUploader',
'SunnylinkEnabled',
@@ -89,16 +89,12 @@ def _migrate_model_bundle_slots(_params):
# ActiveBundle. Seed both slots; validation drops whichever does not match
# its own manifest.
try:
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")
if _params.get("ModelManager_ActiveBundleUSBGPU") is not None:
return
if (bundle := _params.get("ModelManager_ActiveBundle")) is None:
return
_params.put("ModelManager_ActiveBundleChestnut", bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleChestnut from ModelManager_ActiveBundle")
_params.put("ModelManager_ActiveBundleUSBGPU", bundle, block=True)
cloudlog.info("params_migration: seeded ModelManager_ActiveBundleUSBGPU from ModelManager_ActiveBundle")
except Exception as e:
cloudlog.exception(f"Error migrating model bundle slots: {e}")
@@ -140,5 +136,5 @@ def run_migration(_params):
# seed TeslaMadsScreenButton for existing Tesla installs
_migrate_tesla_mads_screen_button(_params)
# seed the chestnut model slot from the pre-split single slot
# seed the usbgpu 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_chestnut_slot_from_active_bundle(self):
def test_seeds_usbgpu_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_ActiveBundleChestnut") == bundle
assert params.get("ModelManager_ActiveBundleUSBGPU") == bundle
assert params.get("ModelManager_ActiveBundle") == bundle
def test_noop_when_chestnut_slot_already_set(self):
def test_noop_when_usbgpu_slot_already_set(self):
params = Params()
params.put("ModelManager_ActiveBundle", {"ref": "small"}, block=True)
params.put("ModelManager_ActiveBundleChestnut", {"ref": "big"}, block=True)
params.put("ModelManager_ActiveBundleUSBGPU", {"ref": "big"}, block=True)
_migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleChestnut") == {"ref": "big"}
assert params.get("ModelManager_ActiveBundleUSBGPU") == {"ref": "big"}
def test_noop_when_no_selection(self):
params = Params()
_migrate_model_bundle_slots(params)
assert params.get("ModelManager_ActiveBundleChestnut") is None
assert params.get("ModelManager_ActiveBundleUSBGPU") is None
+2 -2
View File
@@ -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, chestnut_compiled
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_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 chestnut_compiled()
big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled()
while not end_event.is_set():
sm.update(PANDA_STATES_TIMEOUT)