Merge remote-tracking branch 'origin/master' into ui_remove_qt_spinner_text

pull/35003/head
Cameron Clough 2 months ago
commit bf2c8b8dd0
  1. 3
      cereal/log.capnp
  2. 21
      selfdrive/controls/lib/drive_helpers.py
  3. 35
      selfdrive/controls/lib/longitudinal_planner.py
  4. 80
      selfdrive/locationd/lagd.py
  5. 12
      selfdrive/locationd/test/test_lagd.py
  6. 19
      selfdrive/modeld/fill_model_msg.py
  7. 39
      selfdrive/modeld/modeld.py
  8. 4
      selfdrive/modeld/models/driving_policy.onnx
  9. 4
      selfdrive/modeld/models/driving_vision.onnx
  10. 13
      selfdrive/modeld/parse_model_outputs.py
  11. 1
      selfdrive/test/longitudinal_maneuvers/plant.py
  12. 4
      selfdrive/test/process_replay/migration.py
  13. 1
      selfdrive/test/process_replay/model_replay.py
  14. 2
      selfdrive/test/process_replay/ref_commit
  15. 156
      selfdrive/ui/translations/main_de.ts
  16. 4
      system/ui/lib/application.py
  17. 69
      system/ui/spinner.py
  18. 17
      system/ui/text.py
  19. 197
      system/ui/updater.py

@ -1174,6 +1174,8 @@ struct ModelDataV2 {
struct Action { struct Action {
desiredCurvature @0 :Float32; desiredCurvature @0 :Float32;
desiredAcceleration @1 :Float32;
shouldStop @2 :Bool;
} }
} }
@ -2284,6 +2286,7 @@ struct LiveDelayData {
status @2 :Status; status @2 :Status;
lateralDelayEstimate @3 :Float32; lateralDelayEstimate @3 :Float32;
lateralDelayEstimateStd @5 :Float32;
points @4 :List(Float32); points @4 :List(Float32);
enum Status { enum Status {

@ -1,7 +1,7 @@
import numpy as np import numpy as np
from cereal import log from cereal import log
from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
from openpilot.common.realtime import DT_CTRL from openpilot.common.realtime import DT_CTRL, DT_MDL
MIN_SPEED = 1.0 MIN_SPEED = 1.0
CONTROL_N = 17 CONTROL_N = 17
@ -19,6 +19,9 @@ def clamp(val, min_val, max_val):
clamped_val = float(np.clip(val, min_val, max_val)) clamped_val = float(np.clip(val, min_val, max_val))
return clamped_val, clamped_val != val return clamped_val, clamped_val != val
def smooth_value(val, prev_val, tau, dt=DT_MDL):
alpha = 1 - np.exp(-dt/tau) if tau > 0 else 1
return alpha * val + (1 - alpha) * prev_val
def clip_curvature(v_ego, prev_curvature, new_curvature, roll): def clip_curvature(v_ego, prev_curvature, new_curvature, roll):
# This function respects ISO lateral jerk and acceleration limits + a max curvature # This function respects ISO lateral jerk and acceleration limits + a max curvature
@ -43,3 +46,19 @@ def get_speed_error(modelV2: log.ModelDataV2, v_ego: float) -> float:
vel_err = np.clip(modelV2.temporalPose.trans[0] - v_ego, -MAX_VEL_ERR, MAX_VEL_ERR) vel_err = np.clip(modelV2.temporalPose.trans[0] - v_ego, -MAX_VEL_ERR, MAX_VEL_ERR)
return float(vel_err) return float(vel_err)
return 0.0 return 0.0
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05):
if len(speeds) == len(t_idxs):
v_now = speeds[0]
a_now = accels[0]
v_target = np.interp(action_t, t_idxs, speeds)
a_target = 2 * (v_target - v_now) / (action_t) - a_now
v_target_1sec = np.interp(action_t + 1.0, t_idxs, speeds)
else:
v_target = 0.0
v_target_1sec = 0.0
a_target = 0.0
should_stop = (v_target < vEgoStopping and
v_target_1sec < vEgoStopping)
return a_target, should_stop

@ -11,7 +11,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_speed_error from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_speed_error, get_accel_from_plan
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
@ -48,27 +48,11 @@ def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
return [a_target[0], min(a_target[1], a_x_allowed)] return [a_target[0], min(a_target[1], a_x_allowed)]
def get_accel_from_plan(speeds, accels, action_t=DT_MDL, vEgoStopping=0.05):
if len(speeds) == CONTROL_N:
v_now = speeds[0]
a_now = accels[0]
v_target = np.interp(action_t, CONTROL_N_T_IDX, speeds)
a_target = 2 * (v_target - v_now) / (action_t) - a_now
v_target_1sec = np.interp(action_t + 1.0, CONTROL_N_T_IDX, speeds)
else:
v_target = 0.0
v_target_1sec = 0.0
a_target = 0.0
should_stop = (v_target < vEgoStopping and
v_target_1sec < vEgoStopping)
return a_target, should_stop
class LongitudinalPlanner: class LongitudinalPlanner:
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
self.CP = CP self.CP = CP
self.mpc = LongitudinalMpc(dt=dt) self.mpc = LongitudinalMpc(dt=dt)
self.mpc.mode = 'acc'
self.fcw = False self.fcw = False
self.dt = dt self.dt = dt
self.allow_throttle = True self.allow_throttle = True
@ -106,7 +90,7 @@ class LongitudinalPlanner:
return x, v, a, j, throttle_prob return x, v, a, j, throttle_prob
def update(self, sm): def update(self, sm):
self.mpc.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
if len(sm['carControl'].orientationNED) == 3: if len(sm['carControl'].orientationNED) == 3:
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
@ -129,7 +113,7 @@ class LongitudinalPlanner:
# No change cost when user is controlling the speed, or when standstill # No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill) prev_accel_constraint = not (reset_state or sm['carState'].standstill)
if self.mpc.mode == 'acc': if self.mode == 'acc':
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
@ -176,8 +160,17 @@ class LongitudinalPlanner:
self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0 self.v_desired_filter.x = self.v_desired_filter.x + self.dt * (self.a_desired + a_prev) / 2.0
action_t = self.CP.longitudinalActuatorDelay + DT_MDL action_t = self.CP.longitudinalActuatorDelay + DT_MDL
output_a_target, self.output_should_stop = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
action_t=action_t, vEgoStopping=self.CP.vEgoStopping) action_t=action_t, vEgoStopping=self.CP.vEgoStopping)
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
if self.mode == 'acc':
output_a_target = output_a_target_mpc
self.output_should_stop = output_should_stop_mpc
else:
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
for idx in range(2): for idx in range(2):
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05) accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)

@ -21,8 +21,12 @@ MIN_OKAY_WINDOW_SEC = 25.0
MIN_RECOVERY_BUFFER_SEC = 2.0 MIN_RECOVERY_BUFFER_SEC = 2.0
MIN_VEGO = 15.0 MIN_VEGO = 15.0
MIN_ABS_YAW_RATE = np.radians(1.0) MIN_ABS_YAW_RATE = np.radians(1.0)
MAX_YAW_RATE_SANITY_CHECK = 1.0
MIN_NCC = 0.95 MIN_NCC = 0.95
MAX_LAG = 1.0 MAX_LAG = 1.0
MAX_LAG_STD = 0.1
MAX_LAT_ACCEL = 2.0
MAX_LAT_ACCEL_DIFF = 0.6
def masked_normalized_cross_correlation(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, n: int): def masked_normalized_cross_correlation(expected_sig: np.ndarray, actual_sig: np.ndarray, mask: np.ndarray, n: int):
@ -124,13 +128,23 @@ class BlockAverage:
self.block_idx = (self.block_idx + 1) % self.num_blocks self.block_idx = (self.block_idx + 1) % self.num_blocks
self.valid_blocks = min(self.valid_blocks + 1, self.num_blocks) self.valid_blocks = min(self.valid_blocks + 1, self.num_blocks)
def get(self) -> tuple[float, float]: def get(self) -> tuple[float, float, float, float]:
valid_block_idx = [i for i in range(self.valid_blocks) if i != self.block_idx] valid_block_idx = [i for i in range(self.valid_blocks) if i != self.block_idx]
valid_and_current_idx = valid_block_idx + ([self.block_idx] if self.idx > 0 else []) valid_and_current_idx = valid_block_idx + ([self.block_idx] if self.idx > 0 else [])
valid_mean = float(np.mean(self.values[valid_block_idx], axis=0).item()) if len(valid_block_idx) > 0 else float('nan') if len(valid_block_idx) > 0:
current_mean = float(np.mean(self.values[valid_and_current_idx], axis=0).item()) if len(valid_and_current_idx) > 0 else float('nan') valid_mean = float(np.mean(self.values[valid_block_idx], axis=0).item())
return valid_mean, current_mean valid_std = float(np.std(self.values[valid_block_idx], axis=0).item())
else:
valid_mean, valid_std = float('nan'), float('nan')
if len(valid_and_current_idx) > 0:
current_mean = float(np.mean(self.values[valid_and_current_idx], axis=0).item())
current_std = float(np.std(self.values[valid_and_current_idx], axis=0).item())
else:
current_mean, current_std = float('nan'), float('nan')
return valid_mean, valid_std, current_mean, current_std
class LateralLagEstimator: class LateralLagEstimator:
@ -139,7 +153,8 @@ class LateralLagEstimator:
def __init__(self, CP: car.CarParams, dt: float, def __init__(self, CP: car.CarParams, dt: float,
block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE,
window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC, window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC,
min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC): min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC,
max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF):
self.dt = dt self.dt = dt
self.window_sec = window_sec self.window_sec = window_sec
self.okay_window_sec = okay_window_sec self.okay_window_sec = okay_window_sec
@ -151,6 +166,8 @@ class LateralLagEstimator:
self.min_vego = min_vego self.min_vego = min_vego
self.min_yr = min_yr self.min_yr = min_yr
self.min_ncc = min_ncc self.min_ncc = min_ncc
self.max_lat_accel = max_lat_accel
self.max_lat_accel_diff = max_lat_accel_diff
self.t = 0.0 self.t = 0.0
self.lat_active = False self.lat_active = False
@ -159,10 +176,13 @@ class LateralLagEstimator:
self.desired_curvature = 0.0 self.desired_curvature = 0.0
self.v_ego = 0.0 self.v_ego = 0.0
self.yaw_rate = 0.0 self.yaw_rate = 0.0
self.yaw_rate_std = 0.0
self.pose_valid = False
self.last_lat_inactive_t = 0.0 self.last_lat_inactive_t = 0.0
self.last_steering_pressed_t = 0.0 self.last_steering_pressed_t = 0.0
self.last_steering_saturated_t = 0.0 self.last_steering_saturated_t = 0.0
self.last_pose_invalid_t = 0.0
self.last_estimate_t = 0.0 self.last_estimate_t = 0.0
self.calibrator = PoseCalibrator() self.calibrator = PoseCalibrator()
@ -181,17 +201,27 @@ class LateralLagEstimator:
liveDelay = msg.liveDelay liveDelay = msg.liveDelay
valid_mean_lag, current_mean_lag = self.block_avg.get() valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get()
if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag): if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std):
liveDelay.status = log.LiveDelayData.Status.estimated if valid_std > MAX_LAG_STD:
liveDelay.lateralDelay = valid_mean_lag liveDelay.status = log.LiveDelayData.Status.invalid
else:
liveDelay.status = log.LiveDelayData.Status.estimated
else: else:
liveDelay.status = log.LiveDelayData.Status.unestimated liveDelay.status = log.LiveDelayData.Status.unestimated
if liveDelay.status == log.LiveDelayData.Status.estimated:
liveDelay.lateralDelay = valid_mean_lag
else:
liveDelay.lateralDelay = self.initial_lag liveDelay.lateralDelay = self.initial_lag
if not np.isnan(current_mean_lag):
if not np.isnan(current_mean_lag) and not np.isnan(current_std):
liveDelay.lateralDelayEstimate = current_mean_lag liveDelay.lateralDelayEstimate = current_mean_lag
liveDelay.lateralDelayEstimateStd = current_std
else: else:
liveDelay.lateralDelayEstimate = self.initial_lag liveDelay.lateralDelayEstimate = self.initial_lag
liveDelay.lateralDelayEstimateStd = 0.0
liveDelay.validBlocks = self.block_avg.valid_blocks liveDelay.validBlocks = self.block_avg.valid_blocks
if debug: if debug:
liveDelay.points = self.block_avg.values.flatten().tolist() liveDelay.points = self.block_avg.values.flatten().tolist()
@ -212,7 +242,9 @@ class LateralLagEstimator:
elif which == "livePose": elif which == "livePose":
device_pose = Pose.from_live_pose(msg) device_pose = Pose.from_live_pose(msg)
calibrated_pose = self.calibrator.build_calibrated_pose(device_pose) calibrated_pose = self.calibrator.build_calibrated_pose(device_pose)
self.yaw_rate = calibrated_pose.angular_velocity.z self.yaw_rate = calibrated_pose.angular_velocity.yaw
self.yaw_rate_std = calibrated_pose.angular_velocity.yaw_std
self.pose_valid = msg.angularVelocityDevice.valid and msg.posenetOK and msg.inputsOK
self.t = t self.t = t
def points_enough(self): def points_enough(self):
@ -222,23 +254,30 @@ class LateralLagEstimator:
return self.points.num_okay >= int(self.okay_window_sec / self.dt) return self.points.num_okay >= int(self.okay_window_sec / self.dt)
def update_points(self): def update_points(self):
la_desired = self.desired_curvature * self.v_ego * self.v_ego
la_actual_pose = self.yaw_rate * self.v_ego
fast = self.v_ego > self.min_vego
turning = np.abs(self.yaw_rate) >= self.min_yr
sensors_valid = self.pose_valid and np.abs(self.yaw_rate) < MAX_YAW_RATE_SANITY_CHECK and self.yaw_rate_std < MAX_YAW_RATE_SANITY_CHECK
la_valid = np.abs(la_actual_pose) <= self.max_lat_accel and np.abs(la_desired - la_actual_pose) <= self.max_lat_accel_diff
calib_valid = self.calibrator.calib_valid
if not self.lat_active: if not self.lat_active:
self.last_lat_inactive_t = self.t self.last_lat_inactive_t = self.t
if self.steering_pressed: if self.steering_pressed:
self.last_steering_pressed_t = self.t self.last_steering_pressed_t = self.t
if self.steering_saturated: if self.steering_saturated:
self.last_steering_saturated_t = self.t self.last_steering_saturated_t = self.t
if not sensors_valid or not la_valid:
self.last_pose_invalid_t = self.t
la_desired = self.desired_curvature * self.v_ego * self.v_ego has_recovered = all( # wait for recovery after !lat_active, steering_pressed, steering_saturated, !sensors/la_valid
la_actual_pose = self.yaw_rate * self.v_ego
fast = self.v_ego > self.min_vego
turning = np.abs(self.yaw_rate) >= self.min_yr
has_recovered = all( # wait for recovery after !lat_active, steering_pressed, steering_saturated
self.t - last_t >= self.min_recovery_buffer_sec self.t - last_t >= self.min_recovery_buffer_sec
for last_t in [self.last_lat_inactive_t, self.last_steering_pressed_t, self.last_steering_saturated_t] for last_t in [self.last_lat_inactive_t, self.last_steering_pressed_t, self.last_steering_saturated_t, self.last_pose_invalid_t]
) )
okay = self.lat_active and not self.steering_pressed and not self.steering_saturated and fast and turning and has_recovered okay = self.lat_active and not self.steering_pressed and not self.steering_saturated and \
fast and turning and has_recovered and calib_valid and sensors_valid and la_valid
self.points.update(self.t, la_desired, la_actual_pose, okay) self.points.update(self.t, la_desired, la_actual_pose, okay)
@ -288,8 +327,9 @@ def retrieve_initial_lag(params_reader: Params, CP: car.CarParams):
if last_CP.carFingerprint != CP.carFingerprint: if last_CP.carFingerprint != CP.carFingerprint:
raise Exception("Car model mismatch") raise Exception("Car model mismatch")
lag, valid_blocks = ld.lateralDelayEstimate, ld.validBlocks lag, valid_blocks, status = ld.lateralDelayEstimate, ld.validBlocks, ld.status
assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks" assert valid_blocks <= BLOCK_NUM, "Invalid number of valid blocks"
assert status != log.LiveDelayData.Status.invalid, "Lag estimate is invalid"
return lag, valid_blocks return lag, valid_blocks
except Exception as e: except Exception as e:
cloudlog.error(f"Failed to retrieve initial lag: {e}") cloudlog.error(f"Failed to retrieve initial lag: {e}")

@ -3,7 +3,7 @@ import numpy as np
import time import time
import pytest import pytest
from cereal import messaging from cereal import messaging, log
from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \ from openpilot.selfdrive.locationd.lagd import LateralLagEstimator, retrieve_initial_lag, masked_normalized_cross_correlation, \
BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC BLOCK_NUM_NEEDED, BLOCK_SIZE, MIN_OKAY_WINDOW_SEC
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
@ -23,8 +23,8 @@ def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejecti
for i in range(n_frames): for i in range(n_frames):
t = i * estimator.dt t = i * estimator.dt
desired_la = np.cos(t) desired_la = np.cos(t) * 0.1
actual_la = np.cos(t - lag_frames * estimator.dt) actual_la = np.cos(t - lag_frames * estimator.dt) * 0.1
# if sample is masked out, set it to desired value (no lag) # if sample is masked out, set it to desired value (no lag)
rejected = random.uniform(0, 1) < rejection_threshold rejected = random.uniform(0, 1) < rejection_threshold
@ -41,7 +41,9 @@ def process_messages(mocker, estimator, lag_frames, n_frames, vego=20.0, rejecti
(t, "livePose", mocker.Mock(orientationNED=ZeroMock(), (t, "livePose", mocker.Mock(orientationNED=ZeroMock(),
velocityDevice=ZeroMock(), velocityDevice=ZeroMock(),
accelerationDevice=ZeroMock(), accelerationDevice=ZeroMock(),
angularVelocityDevice=ZeroMock(z=actual_yr))), angularVelocityDevice=ZeroMock(z=actual_yr, valid=True),
posenetOK=True, inputsOK=True)),
(t, "liveCalibration", mocker.Mock(rpyCalib=[0, 0, 0], calStatus=log.LiveCalibrationData.Status.calibrated)),
] ]
for t, w, m in msgs: for t, w, m in msgs:
estimator.handle_log(t, w, m) estimator.handle_log(t, w, m)
@ -111,6 +113,7 @@ class TestLagd:
assert msg.liveDelay.status == 'estimated' assert msg.liveDelay.status == 'estimated'
assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelay, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED
def test_estimator_masking(self, mocker): def test_estimator_masking(self, mocker):
@ -119,6 +122,7 @@ class TestLagd:
process_messages(mocker, estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4) process_messages(mocker, estimator, lag_frames, (int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_SIZE) * 2, rejection_threshold=0.4)
msg = estimator.get_msg(True) msg = estimator.get_msg(True)
assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01)
assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01)
@pytest.mark.skipif(PC, reason="only on device") @pytest.mark.skipif(PC, reason="only on device")
@pytest.mark.timeout(60) @pytest.mark.timeout(60)

@ -56,7 +56,7 @@ def fill_lane_line_meta(builder, lane_lines, lane_line_probs):
builder.rightProb = lane_line_probs[2] builder.rightProb = lane_line_probs[2]
def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder, def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._DynamicStructBuilder,
net_output_data: dict[str, np.ndarray], v_ego: float, delay: float, net_output_data: dict[str, np.ndarray], action: log.ModelDataV2.Action,
publish_state: PublishState, vipc_frame_id: int, vipc_frame_id_extra: int, publish_state: PublishState, vipc_frame_id: int, vipc_frame_id_extra: int,
frame_id: int, frame_drop: float, timestamp_eof: int, model_execution_time: float, frame_id: int, frame_drop: float, timestamp_eof: int, model_execution_time: float,
valid: bool) -> None: valid: bool) -> None:
@ -71,7 +71,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
driving_model_data.frameIdExtra = vipc_frame_id_extra driving_model_data.frameIdExtra = vipc_frame_id_extra
driving_model_data.frameDropPerc = frame_drop_perc driving_model_data.frameDropPerc = frame_drop_perc
driving_model_data.modelExecutionTime = model_execution_time driving_model_data.modelExecutionTime = model_execution_time
driving_model_data.action.desiredCurvature = float(net_output_data['desired_curvature'][0,0])
driving_model_data.action = action
modelV2 = extended_msg.modelV2 modelV2 = extended_msg.modelV2
modelV2.frameId = vipc_frame_id modelV2.frameId = vipc_frame_id
@ -89,17 +90,17 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D
fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T)
# temporal pose # temporal pose
temporal_pose = modelV2.temporalPose #temporal_pose = modelV2.temporalPose
temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() #temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() #temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist()
temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() #temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist()
temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() #temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist()
# poly path # poly path
fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T)
# lateral planning # action
modelV2.action.desiredCurvature = float(net_output_data['desired_curvature'][0,0]) modelV2.action = action
# times at X_IDXS of edges and lines aren't used # times at X_IDXS of edges and lines aren't used
LINE_T_IDXS: list[float] = [] LINE_T_IDXS: list[float] = []

@ -21,14 +21,15 @@ from opendbc.car.car_helpers import get_demo_car_params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.realtime import config_realtime_process from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry from openpilot.system import sentry
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext
@ -40,6 +41,26 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
LAT_SMOOTH_SECONDS = 0.1
LONG_SMOOTH_SECONDS = 0.3
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float,) -> log.ModelDataV2.Action:
plan = model_output['plan'][0]
desired_accel, should_stop = get_accel_from_plan(plan[:,Plan.VELOCITY][:,0],
plan[:,Plan.ACCELERATION][:,0],
ModelConstants.T_IDXS,
action_t=long_action_t)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
desired_curvature = model_output['desired_curvature'][0, 0]
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),
desiredAcceleration=float(desired_accel),
shouldStop=bool(should_stop))
class FrameMeta: class FrameMeta:
frame_id: int = 0 frame_id: int = 0
timestamp_sof: int = 0 timestamp_sof: int = 0
@ -147,7 +168,7 @@ class ModelState:
# TODO model only uses last value now # TODO model only uses last value now
self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:]
self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :] self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :]
self.numpy_inputs['prev_desired_curv'][:] = self.full_prev_desired_curv[0, self.temporal_idxs] self.numpy_inputs['prev_desired_curv'][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs]
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED: if SEND_RAW_PRED:
@ -221,7 +242,10 @@ def main(demo=False):
cloudlog.info("modeld got CarParams: %s", CP.brand) cloudlog.info("modeld got CarParams: %s", CP.brand)
# TODO this needs more thought, use .2s extra for now to estimate other delays # TODO this needs more thought, use .2s extra for now to estimate other delays
steer_delay = CP.steerActuatorDelay + .2 # TODO Move smooth seconds to action function
lat_delay = CP.steerActuatorDelay + .2 + LAT_SMOOTH_SECONDS
long_delay = CP.longitudinalActuatorDelay + LONG_SMOOTH_SECONDS
prev_action = log.ModelDataV2.Action()
DH = DesireHelper() DH = DesireHelper()
@ -263,7 +287,7 @@ def main(demo=False):
is_rhd = sm["driverMonitoringState"].isRHD is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.) v_ego = max(sm["carState"].vEgo, 0.)
lateral_control_params = np.array([v_ego, steer_delay], dtype=np.float32) lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32)
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
@ -306,7 +330,10 @@ def main(demo=False):
modelv2_send = messaging.new_message('modelV2') modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData') drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry') posenet_send = messaging.new_message('cameraOdometry')
fill_model_msg(drivingdata_send, modelv2_send, model_output, v_ego, steer_delay,
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL)
prev_action = action
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, 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) frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen)

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:98f0121ccb6f850077b04cc91bd33d370fc6cbdc2bd35f1ab55628a15a813f36 oid sha256:84061882148485dabfdd23ee7e0925284a4370d0ae5be804d6099ae847352891
size 15966721 size 15578328

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:897f80d0388250f99bba69b6a8434560cc0fd83157cbeb0bc134c67fe4e64624 oid sha256:f222d2c528f1763828de01bb55e8979b1e4056e1dbb41350f521d2d2bb09d177
size 34882971 size 46265585

@ -88,6 +88,12 @@ class Parser:
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_binary_crossentropy('meta', outs) self.parse_binary_crossentropy('meta', outs)
return outs return outs
@ -95,17 +101,10 @@ class Parser:
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs: if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs: if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
return outs return outs

@ -107,6 +107,7 @@ class Plant:
position = log.XYZTData.new_message() position = log.XYZTData.new_message()
position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)] position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)]
model.modelV2.position = position model.modelV2.position = position
model.modelV2.action.desiredAcceleration = float(self.acceleration + 0.1)
velocity = log.XYZTData.new_message() velocity = log.XYZTData.new_message()
velocity.x = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)] velocity.x = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)]
velocity.x[0] = float(self.speed) # always start at current speed velocity.x[0] = float(self.speed) # always start at current speed

@ -13,7 +13,7 @@ from opendbc.car.gm.values import GMSafetyFlags
from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.fill_model_msg import fill_xyz_poly, fill_lane_line_meta from openpilot.selfdrive.modeld.fill_model_msg import fill_xyz_poly, fill_lane_line_meta
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_encode_index from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_encode_index
from openpilot.selfdrive.controls.lib.longitudinal_planner import get_accel_from_plan from openpilot.selfdrive.controls.lib.longitudinal_planner import get_accel_from_plan, CONTROL_N_T_IDX
from openpilot.system.manager.process_config import managed_processes from openpilot.system.manager.process_config import managed_processes
from openpilot.tools.lib.logreader import LogIterable from openpilot.tools.lib.logreader import LogIterable
@ -109,7 +109,7 @@ def migrate_longitudinalPlan(msgs):
if msg.which() != 'longitudinalPlan': if msg.which() != 'longitudinalPlan':
continue continue
new_msg = msg.as_builder() new_msg = msg.as_builder()
a_target, should_stop = get_accel_from_plan(msg.longitudinalPlan.speeds, msg.longitudinalPlan.accels) a_target, should_stop = get_accel_from_plan(msg.longitudinalPlan.speeds, msg.longitudinalPlan.accels, CONTROL_N_T_IDX)
new_msg.longitudinalPlan.aTarget, new_msg.longitudinalPlan.shouldStop = float(a_target), bool(should_stop) new_msg.longitudinalPlan.aTarget, new_msg.longitudinalPlan.shouldStop = float(a_target), bool(should_stop)
ops.append((index, new_msg.as_reader())) ops.append((index, new_msg.as_reader()))
return ops, [], [] return ops, [], []

@ -64,6 +64,7 @@ def generate_report(proposed, master, tmp, commit):
ModelV2_Plots = zl([ ModelV2_Plots = zl([
(lambda x: get_idx_if_non_empty(x.velocity.x, 0), "velocity.x"), (lambda x: get_idx_if_non_empty(x.velocity.x, 0), "velocity.x"),
(lambda x: get_idx_if_non_empty(x.action.desiredCurvature), "desiredCurvature"), (lambda x: get_idx_if_non_empty(x.action.desiredCurvature), "desiredCurvature"),
(lambda x: get_idx_if_non_empty(x.action.desiredAcceleration), "desiredAcceleration"),
(lambda x: get_idx_if_non_empty(x.leadsV3[0].x, 0), "leadsV3.x"), (lambda x: get_idx_if_non_empty(x.leadsV3[0].x, 0), "leadsV3.x"),
(lambda x: get_idx_if_non_empty(x.laneLines[1].y, 0), "laneLines.y"), (lambda x: get_idx_if_non_empty(x.laneLines[1].y, 0), "laneLines.y"),
(lambda x: get_idx_if_non_empty(x.meta.desireState, 3), "desireState.laneChangeLeft"), (lambda x: get_idx_if_non_empty(x.meta.desireState, 3), "desireState.laneChangeLeft"),

@ -1 +1 @@
3d3f875dc211ab0ca8a8e564ecf6dd3f52fcf7d9 fcc771b9ceb487a61035885acbd84e592fb316bf

@ -68,23 +68,23 @@
</message> </message>
<message> <message>
<source>Hidden Network</source> <source>Hidden Network</source>
<translation type="unfinished"></translation> <translation>Verborgenes Netzwerk</translation>
</message> </message>
<message> <message>
<source>CONNECT</source> <source>CONNECT</source>
<translation type="unfinished">CONNECT</translation> <translation>VERBINDEN</translation>
</message> </message>
<message> <message>
<source>Enter SSID</source> <source>Enter SSID</source>
<translation type="unfinished">SSID eingeben</translation> <translation>SSID eingeben</translation>
</message> </message>
<message> <message>
<source>Enter password</source> <source>Enter password</source>
<translation type="unfinished">Passwort eingeben</translation> <translation>Passwort eingeben</translation>
</message> </message>
<message> <message>
<source>for &quot;%1&quot;</source> <source>for &quot;%1&quot;</source>
<translation type="unfinished">für &quot;%1&quot;</translation> <translation>für &quot;%1&quot;</translation>
</message> </message>
</context> </context>
<context> <context>
@ -117,31 +117,31 @@
<name>DeveloperPanel</name> <name>DeveloperPanel</name>
<message> <message>
<source>Joystick Debug Mode</source> <source>Joystick Debug Mode</source>
<translation type="unfinished"></translation> <translation>Joystick Debug-Modus</translation>
</message> </message>
<message> <message>
<source>Longitudinal Maneuver Mode</source> <source>Longitudinal Maneuver Mode</source>
<translation type="unfinished"></translation> <translation>Längsmanöver-Modus</translation>
</message> </message>
<message> <message>
<source>openpilot Longitudinal Control (Alpha)</source> <source>openpilot Longitudinal Control (Alpha)</source>
<translation type="unfinished"></translation> <translation>openpilot Längsregelung (Alpha)</translation>
</message> </message>
<message> <message>
<source>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</source> <source>WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB).</source>
<translation type="unfinished"></translation> <translation>WARNUNG: Die openpilot Längsregelung befindet sich für dieses Fahrzeug im Alpha-Stadium und deaktiviert das automatische Notbremsen (AEB).</translation>
</message> </message>
<message> <message>
<source>On this car, openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.</source> <source>On this car, openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha.</source>
<translation type="unfinished"></translation> <translation>Bei diesem Fahrzeug verwendet openpilot standardmäßig den eingebauten Tempomaten anstelle der openpilot Längsregelung. Aktiviere diese Option, um auf die openpilot Längsregelung umzuschalten. Es wird empfohlen, den experimentellen Modus zu aktivieren, wenn die openpilot Längsregelung (Alpha) aktiviert wird.</translation>
</message> </message>
<message> <message>
<source>Enable ADB</source> <source>Enable ADB</source>
<translation type="unfinished"></translation> <translation>ADB aktivieren</translation>
</message> </message>
<message> <message>
<source>ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info.</source> <source>ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info.</source>
<translation type="unfinished"></translation> <translation>ADB (Android Debug Bridge) ermöglicht die Verbindung zu deinem Gerät über USB oder Netzwerk. Siehe https://docs.comma.ai/how-to/connect-to-comma für weitere Informationen.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -280,11 +280,11 @@
</message> </message>
<message> <message>
<source>Pair Device</source> <source>Pair Device</source>
<translation type="unfinished"></translation> <translation>Gerät koppeln</translation>
</message> </message>
<message> <message>
<source>PAIR</source> <source>PAIR</source>
<translation type="unfinished"></translation> <translation>KOPPELN</translation>
</message> </message>
</context> </context>
<context> <context>
@ -309,36 +309,38 @@
<name>FirehosePanel</name> <name>FirehosePanel</name>
<message> <message>
<source>🔥 Firehose Mode 🔥</source> <source>🔥 Firehose Mode 🔥</source>
<translation type="unfinished"></translation> <translation>🔥 Firehose-Modus 🔥</translation>
</message> </message>
<message> <message>
<source>openpilot learns to drive by watching humans, like you, drive. <source>openpilot learns to drive by watching humans, like you, drive.
Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source> Firehose Mode allows you to maximize your training data uploads to improve openpilot&apos;s driving models. More data means bigger models, which means better Experimental Mode.</source>
<translation type="unfinished"></translation> <translation>openpilot lernt das Fahren, indem es Menschen wie dir beim Fahren zuschaut.
Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximieren, um die Fahrmodelle von openpilot zu verbessern. Mehr Daten bedeuten größere Modelle, was zu einem besseren Experimentellen Modus führt.</translation>
</message> </message>
<message> <message>
<source>Firehose Mode: ACTIVE</source> <source>Firehose Mode: ACTIVE</source>
<translation type="unfinished"></translation> <translation>Firehose-Modus: AKTIV</translation>
</message> </message>
<message> <message>
<source>ACTIVE</source> <source>ACTIVE</source>
<translation type="unfinished"></translation> <translation>AKTIV</translation>
</message> </message>
<message> <message>
<source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source> <source>For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.&lt;br&gt;&lt;br&gt;Firehose Mode can also work while you&apos;re driving if connected to a hotspot or unlimited SIM card.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Frequently Asked Questions&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter how or where I drive?&lt;/i&gt; Nope, just drive as you normally would.&lt;br&gt;&lt;br&gt;&lt;i&gt;Do all of my segments get pulled in Firehose Mode?&lt;/i&gt; No, we selectively pull a subset of your segments.&lt;br&gt;&lt;br&gt;&lt;i&gt;What&apos;s a good USB-C adapter?&lt;/i&gt; Any fast phone or laptop charger should be fine.&lt;br&gt;&lt;br&gt;&lt;i&gt;Does it matter which software I run?&lt;/i&gt; Yes, only upstream openpilot (and particular forks) are able to be used for training.</source>
<translation type="unfinished"></translation> <translation>Für maximale Effektivität bring dein Gerät jede Woche nach drinnen und verbinde es mit einem guten USB-C-Adapter und WLAN.&lt;br&gt;&lt;br&gt;Der Firehose-Modus funktioniert auch während der Fahrt, wenn das Gerät mit einem Hotspot oder einer ungedrosselten SIM-Karte verbunden ist.&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;Häufig gestellte Fragen&lt;/b&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;Spielt es eine Rolle, wie oder wo ich fahre?&lt;/i&gt; Nein, fahre einfach wie gewohnt.&lt;br&gt;&lt;br&gt;&lt;i&gt;Werden im Firehose-Modus alle meine Segmente hochgeladen?&lt;/i&gt; Nein, wir wählen selektiv nur einen Teil deiner Segmente aus.&lt;br&gt;&lt;br&gt;&lt;i&gt;Welcher USB-C-Adapter ist gut?&lt;/i&gt; Jedes Schnellladegerät für Handy oder Laptop sollte ausreichen.&lt;br&gt;&lt;br&gt;&lt;i&gt;Spielt es eine Rolle, welche Software ich nutze?&lt;/i&gt; Ja, nur das offizielle Upstreamopenpilot (und bestimmte Forks) kann für das Training verwendet werden.</translation>
</message> </message>
<message numerus="yes"> <message numerus="yes">
<source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source> <source>&lt;b&gt;%n segment(s)&lt;/b&gt; of your driving is in the training dataset so far.</source>
<translation type="unfinished"> <translation>
<numerusform></numerusform> <numerusform>&lt;b&gt;%n Segment&lt;/b&gt; deiner Fahrten ist bisher im Trainingsdatensatz.</numerusform>
<numerusform></numerusform> <numerusform>&lt;b&gt;%n Segmente&lt;/b&gt; deiner Fahrten sind bisher im Trainingsdatensatz.</numerusform>
</translation> </translation>
</message> </message>
<message> <message>
<source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to an unmetered network</source> <source>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INACTIVE&lt;/span&gt;: connect to an unmetered network</source>
<translation type="unfinished"></translation> <translation>&lt;span stylesheet=&apos;font-size: 60px; font-weight: bold; color: #e74c3c;&apos;&gt;INAKTIV&lt;/span&gt;: Verbinde dich mit einem ungedrosselten Netzwerk</translation>
</message> </message>
</context> </context>
<context> <context>
@ -411,48 +413,49 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<name>OffroadAlert</name> <name>OffroadAlert</name>
<message> <message>
<source>Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won&apos;t engage in %1</source> <source>Immediately connect to the internet to check for updates. If you do not connect to the internet, openpilot won&apos;t engage in %1</source>
<translation type="unfinished"></translation> <translation>Stelle sofort eine Internetverbindung her, um nach Updates zu suchen. Wenn du keine Verbindung herstellst, kann openpilot in %1 nicht mehr aktiviert werden.</translation>
</message> </message>
<message> <message>
<source>Connect to internet to check for updates. openpilot won&apos;t automatically start until it connects to internet to check for updates.</source> <source>Connect to internet to check for updates. openpilot won&apos;t automatically start until it connects to internet to check for updates.</source>
<translation type="unfinished"></translation> <translation>Verbinde dich mit dem Internet, um nach Updates zu suchen. openpilot startet nicht automatisch, bis eine Internetverbindung besteht und nach Updates gesucht wurde.</translation>
</message> </message>
<message> <message>
<source>Unable to download updates <source>Unable to download updates
%1</source> %1</source>
<translation type="unfinished"></translation> <translation>Updates konnten nicht heruntergeladen werden
%1</translation>
</message> </message>
<message> <message>
<source>Taking camera snapshots. System won&apos;t start until finished.</source> <source>Taking camera snapshots. System won&apos;t start until finished.</source>
<translation type="unfinished"></translation> <translation>Kamera-Snapshots werden aufgenommen. Das System startet erst, wenn dies abgeschlossen ist.</translation>
</message> </message>
<message> <message>
<source>An update to your device&apos;s operating system is downloading in the background. You will be prompted to update when it&apos;s ready to install.</source> <source>An update to your device&apos;s operating system is downloading in the background. You will be prompted to update when it&apos;s ready to install.</source>
<translation type="unfinished"></translation> <translation>Ein Update für das Betriebssystem deines Geräts wird im Hintergrund heruntergeladen. Du wirst aufgefordert, das Update zu installieren, sobald es bereit ist.</translation>
</message> </message>
<message> <message>
<source>Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.</source> <source>Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.</source>
<translation type="unfinished"></translation> <translation>Gerät konnte nicht registriert werden. Es wird keine Verbindung zu den comma.ai-Servern herstellen oder Daten hochladen und erhält keinen Support von comma.ai. Wenn dies ein offizielles Gerät ist, besuche https://comma.ai/support.</translation>
</message> </message>
<message> <message>
<source>NVMe drive not mounted.</source> <source>NVMe drive not mounted.</source>
<translation type="unfinished"></translation> <translation>NVMe-Laufwerk nicht gemounted.</translation>
</message> </message>
<message> <message>
<source>Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.</source> <source>Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.</source>
<translation type="unfinished"></translation> <translation>Nicht unterstütztes NVMe-Laufwerk erkannt. Das Gerät kann dadurch deutlich mehr Strom verbrauchen und überhitzen.</translation>
</message> </message>
<message> <message>
<source>openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.</source> <source>openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.</source>
<translation type="unfinished"></translation> <translation>openpilot konnte dein Auto nicht identifizieren. Dein Auto wird entweder nicht unterstützt oder die Steuergeräte (ECUs) werden nicht erkannt. Bitte reiche einen Pull Request ein, um die Firmware-Versionen für das richtige Fahrzeug hinzuzufügen. Hilfe findest du auf discord.comma.ai.</translation>
</message> </message>
<message> <message>
<source>openpilot detected a change in the device&apos;s mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.</source> <source>openpilot detected a change in the device&apos;s mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.</source>
<translation type="unfinished"></translation> <translation>openpilot hat eine Änderung der Montageposition des Geräts erkannt. Stelle sicher, dass das Gerät vollständig in der Halterung sitzt und die Halterung fest an der Windschutzscheibe befestigt ist.</translation>
</message> </message>
<message> <message>
<source>Device temperature too high. System cooling down before starting. Current internal component temperature: %1</source> <source>Device temperature too high. System cooling down before starting. Current internal component temperature: %1</source>
<translation type="unfinished"></translation> <translation>Gerätetemperatur zu hoch. Das System kühlt ab, bevor es startet. Aktuelle interne Komponententemperatur: %1</translation>
</message> </message>
</context> </context>
<context> <context>
@ -474,23 +477,23 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
<name>OnroadAlerts</name> <name>OnroadAlerts</name>
<message> <message>
<source>openpilot Unavailable</source> <source>openpilot Unavailable</source>
<translation type="unfinished"></translation> <translation>openpilot nicht verfügbar</translation>
</message> </message>
<message> <message>
<source>TAKE CONTROL IMMEDIATELY</source> <source>TAKE CONTROL IMMEDIATELY</source>
<translation type="unfinished"></translation> <translation>ÜBERNIMM SOFORT DIE KONTROLLE</translation>
</message> </message>
<message> <message>
<source>Reboot Device</source> <source>Reboot Device</source>
<translation type="unfinished"></translation> <translation>Gerät neu starten</translation>
</message> </message>
<message> <message>
<source>Waiting to start</source> <source>Waiting to start</source>
<translation type="unfinished"></translation> <translation>Warten auf Start</translation>
</message> </message>
<message> <message>
<source>System Unresponsive</source> <source>System Unresponsive</source>
<translation type="unfinished"></translation> <translation>System reagiert nicht</translation>
</message> </message>
</context> </context>
<context> <context>
@ -513,7 +516,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>Please connect to Wi-Fi to complete initial pairing</source> <source>Please connect to Wi-Fi to complete initial pairing</source>
<translation type="unfinished"></translation> <translation>Bitte verbinde dich mit WLAN, um die Koppelung abzuschließen.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -547,15 +550,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>24/7 LTE connectivity</source> <source>24/7 LTE connectivity</source>
<translation type="unfinished"></translation> <translation>24/7 LTE-Verbindung</translation>
</message> </message>
<message> <message>
<source>1 year of drive storage</source> <source>1 year of drive storage</source>
<translation type="unfinished"></translation> <translation>Fahrdaten-Speicherung für 1 Jahr</translation>
</message> </message>
<message> <message>
<source>Remote snapshots</source> <source>Remote snapshots</source>
<translation type="unfinished"></translation> <translation>Remote-Snapshots</translation>
</message> </message>
</context> </context>
<context> <context>
@ -606,7 +609,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>now</source> <source>now</source>
<translation type="unfinished"></translation> <translation>jetzt</translation>
</message> </message>
</context> </context>
<context> <context>
@ -637,16 +640,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp
</message> </message>
<message> <message>
<source>Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.</source> <source>Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.</source>
<translation type="unfinished"></translation> <translation>Datenpartition konnte nicht gemounted werden. Die Partition ist möglicherweise beschädigt. Drücke Bestätigen, um das Gerät zu löschen und zurückzusetzen.</translation>
</message> </message>
<message> <message>
<source>Resetting device... <source>Resetting device...
This may take up to a minute.</source> This may take up to a minute.</source>
<translation type="unfinished"></translation> <translation>Gerät wird zurückgesetzt...
Dies kann bis zu einer Minute dauern.</translation>
</message> </message>
<message> <message>
<source>System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.</source> <source>System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.</source>
<translation type="unfinished"></translation> <translation>System-Reset ausgelöst. Drücke Bestätigen, um alle Inhalte und Einstellungen zu löschen. Drücke Abbrechen, um den Startvorgang fortzusetzen.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -673,11 +677,11 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Developer</source> <source>Developer</source>
<translation type="unfinished"></translation> <translation>Entwickler</translation>
</message> </message>
<message> <message>
<source>Firehose</source> <source>Firehose</source>
<translation type="unfinished"></translation> <translation>Firehose</translation>
</message> </message>
</context> </context>
<context> <context>
@ -752,11 +756,11 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>No custom software found at this URL.</source> <source>No custom software found at this URL.</source>
<translation type="unfinished"></translation> <translation>Keine benutzerdefinierte Software unter dieser URL gefunden.</translation>
</message> </message>
<message> <message>
<source>Something went wrong. Reboot the device.</source> <source>Something went wrong. Reboot the device.</source>
<translation type="unfinished"></translation> <translation>Etwas ist schiefgelaufen. Starte das Gerät neu.</translation>
</message> </message>
<message> <message>
<source>Select a language</source> <source>Select a language</source>
@ -764,15 +768,15 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Choose Software to Install</source> <source>Choose Software to Install</source>
<translation type="unfinished"></translation> <translation>Wähle die zu installierende Software</translation>
</message> </message>
<message> <message>
<source>openpilot</source> <source>openpilot</source>
<translation type="unfinished">openpilot</translation> <translation>openpilot</translation>
</message> </message>
<message> <message>
<source>Custom Software</source> <source>Custom Software</source>
<translation type="unfinished"></translation> <translation>Benutzerdefinierte Software</translation>
</message> </message>
</context> </context>
<context> <context>
@ -923,23 +927,23 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>failed to check for update</source> <source>failed to check for update</source>
<translation type="unfinished"></translation> <translation>Update-Prüfung fehlgeschlagen</translation>
</message> </message>
<message> <message>
<source>up to date, last checked %1</source> <source>up to date, last checked %1</source>
<translation type="unfinished"></translation> <translation>Auf dem neuesten Stand, zuletzt geprüft am %1</translation>
</message> </message>
<message> <message>
<source>DOWNLOAD</source> <source>DOWNLOAD</source>
<translation type="unfinished"></translation> <translation>HERUNTERLADEN</translation>
</message> </message>
<message> <message>
<source>update available</source> <source>update available</source>
<translation type="unfinished"></translation> <translation>Update verfügbar</translation>
</message> </message>
<message> <message>
<source>never</source> <source>never</source>
<translation type="unfinished"></translation> <translation>nie</translation>
</message> </message>
</context> </context>
<context> <context>
@ -1000,11 +1004,11 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Welcome to openpilot</source> <source>Welcome to openpilot</source>
<translation type="unfinished"></translation> <translation>Willkommen bei openpilot</translation>
</message> </message>
<message> <message>
<source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source> <source>You must accept the Terms and Conditions to use openpilot. Read the latest terms at &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt; before continuing.</source>
<translation type="unfinished"></translation> <translation>Du musst die Nutzungsbedingungen akzeptieren, um openpilot zu verwenden. Lies die aktuellen Bedingungen unter &lt;span style=&apos;color: #465BEA;&apos;&gt;https://comma.ai/terms&lt;/span&gt;, bevor du fortfährst.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -1071,51 +1075,51 @@ This may take up to a minute.</source>
</message> </message>
<message> <message>
<source>Aggressive</source> <source>Aggressive</source>
<translation type="unfinished"></translation> <translation>Aggressiv</translation>
</message> </message>
<message> <message>
<source>Standard</source> <source>Standard</source>
<translation type="unfinished"></translation> <translation>Standard</translation>
</message> </message>
<message> <message>
<source>Relaxed</source> <source>Relaxed</source>
<translation type="unfinished"></translation> <translation>Entspannt</translation>
</message> </message>
<message> <message>
<source>Driving Personality</source> <source>Driving Personality</source>
<translation type="unfinished"></translation> <translation>Fahrstil</translation>
</message> </message>
<message> <message>
<source>End-to-End Longitudinal Control</source> <source>End-to-End Longitudinal Control</source>
<translation type="unfinished"></translation> <translation>Ende-zu-Ende Längsregelung</translation>
</message> </message>
<message> <message>
<source>openpilot longitudinal control may come in a future update.</source> <source>openpilot longitudinal control may come in a future update.</source>
<translation type="unfinished"></translation> <translation>Die openpilot Längsregelung könnte in einem zukünftigen Update verfügbar sein.</translation>
</message> </message>
<message> <message>
<source>An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches.</source> <source>An alpha version of openpilot longitudinal control can be tested, along with Experimental mode, on non-release branches.</source>
<translation type="unfinished"></translation> <translation>Eine Alpha-Version der openpilot Längsregelung kann zusammen mit dem Experimentellen Modus auf non-stable Branches getestet werden.</translation>
</message> </message>
<message> <message>
<source>Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.</source> <source>Enable the openpilot longitudinal control (alpha) toggle to allow Experimental mode.</source>
<translation type="unfinished"></translation> <translation>Aktiviere den Schalter für openpilot Längsregelung (Alpha), um den Experimentellen Modus zu erlauben.</translation>
</message> </message>
<message> <message>
<source>Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.</source> <source>Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button.</source>
<translation type="unfinished"></translation> <translation>Standard wird empfohlen. Im aggressiven Modus folgt openpilot vorausfahrenden Fahrzeugen enger und ist beim Gasgeben und Bremsen aggressiver. Im entspannten Modus hält openpilot mehr Abstand zu vorausfahrenden Fahrzeugen. Bei unterstützten Fahrzeugen kannst du mit der Abstandstaste am Lenkrad zwischen diesen Fahrstilen wechseln.</translation>
</message> </message>
<message> <message>
<source>The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.</source> <source>The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner.</source>
<translation type="unfinished"></translation> <translation>Die Fahrvisualisierung wechselt bei niedrigen Geschwindigkeiten auf die nach vorne gerichtete Weitwinkelkamera, um Kurven besser darzustellen. Das Logo des Experimentellen Modus wird außerdem oben rechts angezeigt.</translation>
</message> </message>
<message> <message>
<source>Always-On Driver Monitoring</source> <source>Always-On Driver Monitoring</source>
<translation type="unfinished"></translation> <translation>Dauerhaft aktive Fahrerüberwachung</translation>
</message> </message>
<message> <message>
<source>Enable driver monitoring even when openpilot is not engaged.</source> <source>Enable driver monitoring even when openpilot is not engaged.</source>
<translation type="unfinished"></translation> <translation>Fahrerüberwachung auch aktivieren, wenn openpilot nicht aktiv ist.</translation>
</message> </message>
</context> </context>
<context> <context>
@ -1157,15 +1161,15 @@ This may take up to a minute.</source>
<name>WiFiPromptWidget</name> <name>WiFiPromptWidget</name>
<message> <message>
<source>Open</source> <source>Open</source>
<translation type="unfinished"></translation> <translation>Öffnen</translation>
</message> </message>
<message> <message>
<source>Maximize your training data uploads to improve openpilot&apos;s driving models.</source> <source>Maximize your training data uploads to improve openpilot&apos;s driving models.</source>
<translation type="unfinished"></translation> <translation>Maximiere deine Trainingsdaten-Uploads, um die Fahrmodelle von openpilot zu verbessern.</translation>
</message> </message>
<message> <message>
<source>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Firehose Mode &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</source> <source>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Firehose Mode &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</source>
<translation type="unfinished"></translation> <translation>&lt;span style=&apos;font-family: &quot;Noto Color Emoji&quot;;&apos;&gt;🔥&lt;/span&gt; Firehose-Modus &lt;span style=&apos;font-family: Noto Color Emoji;&apos;&gt;🔥&lt;/span&gt;</translation>
</message> </message>
</context> </context>
<context> <context>

@ -50,9 +50,11 @@ class GuiApplication:
self._set_styles() self._set_styles()
self._load_fonts() self._load_fonts()
def load_texture_from_image(self, file_name: str, width: int, height: int): def load_texture_from_image(self, file_name: str, width: int, height: int, alpha_premultiply = False):
"""Load and resize a texture, storing it for later automatic unloading.""" """Load and resize a texture, storing it for later automatic unloading."""
image = rl.load_image(file_name) image = rl.load_image(file_name)
if alpha_premultiply:
rl.image_alpha_premultiply(image)
rl.image_resize(image, width, height) rl.image_resize(image, width, height)
texture = rl.load_texture_from_image(image) texture = rl.load_texture_from_image(image)
# Set texture filtering to smooth the result # Set texture filtering to smooth the result

@ -11,12 +11,11 @@ from openpilot.system.ui.text import wrap_text
# Constants # Constants
PROGRESS_BAR_WIDTH = 1000 PROGRESS_BAR_WIDTH = 1000
PROGRESS_BAR_HEIGHT = 20 PROGRESS_BAR_HEIGHT = 20
ROTATION_TIME_SECONDS = 1.0 # Time for one full circle DEGREES_PER_SECOND = 360.0 # one full rotation per second
MARGIN_H = 50 MARGIN_H = 100
MARGIN_V = 200
TEXTURE_SIZE = 360 TEXTURE_SIZE = 360
FONT_SIZE = 96 FONT_SIZE = 88
LINE_HEIGHT = 104 LINE_HEIGHT = 96
DARKGRAY = (55, 55, 55, 255) DARKGRAY = (55, 55, 55, 255)
@ -27,10 +26,11 @@ def clamp(value, min_value, max_value):
class SpinnerRenderer: class SpinnerRenderer:
def __init__(self): def __init__(self):
self._comma_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_comma.png"), TEXTURE_SIZE, TEXTURE_SIZE) self._comma_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_comma.png"), TEXTURE_SIZE, TEXTURE_SIZE)
self._spinner_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_track.png"), TEXTURE_SIZE, TEXTURE_SIZE) self._spinner_texture = gui_app.load_texture_from_image(os.path.join(BASEDIR, "selfdrive/assets/img_spinner_track.png"), TEXTURE_SIZE, TEXTURE_SIZE,
alpha_premultiply=True)
self._rotation = 0.0 self._rotation = 0.0
self._progress: int | None = 0 self._progress: int | None = None
self._wrapped_lines = [] self._wrapped_lines: list[str] = []
self._lock = threading.Lock() self._lock = threading.Lock()
def set_text(self, text: str) -> None: def set_text(self, text: str) -> None:
@ -40,36 +40,30 @@ class SpinnerRenderer:
self._wrapped_lines = [] self._wrapped_lines = []
else: else:
self._progress = None self._progress = None
self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H * 2) self._wrapped_lines = wrap_text(text, FONT_SIZE, gui_app.width - MARGIN_H)
def render(self): def render(self):
center = rl.Vector2(gui_app.width / 2.0, gui_app.height / 2.0)
# Display progress bar or text based on user input
with self._lock: with self._lock:
progress = self._progress progress = self._progress
wrapped_lines = self._wrapped_lines.copy() wrapped_lines = self._wrapped_lines
if progress is not None:
y_pos = rl.get_screen_height() - MARGIN_V - PROGRESS_BAR_HEIGHT
bar = rl.Rectangle(center.x - PROGRESS_BAR_WIDTH / 2.0, y_pos, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)
rl.draw_rectangle_rounded(bar, 1, 10, DARKGRAY)
bar.width *= progress / 100.0 if wrapped_lines:
rl.draw_rectangle_rounded(bar, 1, 10, rl.WHITE) # Calculate total height required for spinner and text
spacing = 50
total_height = TEXTURE_SIZE + spacing + len(wrapped_lines) * LINE_HEIGHT
center_y = (gui_app.height - total_height) / 2.0 + TEXTURE_SIZE / 2.0
else: else:
center.y -= MARGIN_V # Center spinner vertically
y_pos = center.y + TEXTURE_SIZE / 2 + MARGIN_V / 2 spacing = 150
for i, line in enumerate(wrapped_lines): center_y = gui_app.height / 2.0
text_size = rl.measure_text_ex(gui_app.font(), line, FONT_SIZE, 1.0) y_pos = center_y + TEXTURE_SIZE / 2.0 + spacing
rl.draw_text_ex(gui_app.font(), line, rl.Vector2(center.x - text_size.x / 2, y_pos + i * LINE_HEIGHT),
FONT_SIZE, 1.0, rl.WHITE)
# Draw rotating spinner and static comma logo center = rl.Vector2(gui_app.width / 2.0, center_y)
fps = rl.get_fps() spinner_origin = rl.Vector2(TEXTURE_SIZE / 2.0, TEXTURE_SIZE / 2.0)
if fps > 0: comma_position = rl.Vector2(center.x - TEXTURE_SIZE / 2.0, center.y - TEXTURE_SIZE / 2.0)
degrees_per_frame = 360.0 / (ROTATION_TIME_SECONDS * fps)
self._rotation = (self._rotation + degrees_per_frame) % 360.0 delta_time = rl.get_frame_time()
self._rotation = (self._rotation + DEGREES_PER_SECOND * delta_time) % 360.0
spinner_origin = rl.Vector2(TEXTURE_SIZE / 2.0, TEXTURE_SIZE / 2.0) spinner_origin = rl.Vector2(TEXTURE_SIZE / 2.0, TEXTURE_SIZE / 2.0)
comma_position = rl.Vector2(center.x - TEXTURE_SIZE / 2.0, center.y - TEXTURE_SIZE / 2.0) comma_position = rl.Vector2(center.x - TEXTURE_SIZE / 2.0, center.y - TEXTURE_SIZE / 2.0)
@ -79,6 +73,19 @@ class SpinnerRenderer:
spinner_origin, self._rotation, rl.WHITE) spinner_origin, self._rotation, rl.WHITE)
rl.draw_texture_v(self._comma_texture, comma_position, rl.WHITE) rl.draw_texture_v(self._comma_texture, comma_position, rl.WHITE)
# Display progress bar or text based on user input
if progress is not None:
bar = rl.Rectangle(center.x - PROGRESS_BAR_WIDTH / 2.0, y_pos, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT)
rl.draw_rectangle_rounded(bar, 1, 10, DARKGRAY)
bar.width *= progress / 100.0
rl.draw_rectangle_rounded(bar, 1, 10, rl.WHITE)
elif wrapped_lines:
for i, line in enumerate(wrapped_lines):
text_size = rl.measure_text_ex(gui_app.font(), line, FONT_SIZE, 0.0)
rl.draw_text_ex(gui_app.font(), line, rl.Vector2(center.x - text_size.x / 2, y_pos + i * LINE_HEIGHT),
FONT_SIZE, 0.0, rl.WHITE)
class Spinner(Wrapper): class Spinner(Wrapper):
def __init__(self): def __init__(self):

@ -21,13 +21,12 @@ def wrap_text(text, font_size, max_width):
font = gui_app.font() font = gui_app.font()
for paragraph in text.split("\n"): for paragraph in text.split("\n"):
indent = re.match(r'^\s*', paragraph).group() if not paragraph.strip():
content = paragraph[len(indent):] # Don't add empty lines first, ensuring wrap_text("") returns []
if lines:
if content == "": lines.append("")
lines.append("")
continue continue
indent = re.match(r"^\s*", paragraph).group()
current_line = indent current_line = indent
for word in paragraph.split(): for word in paragraph.split():
test_line = current_line + word + " " test_line = current_line + word + " "
@ -35,8 +34,8 @@ def wrap_text(text, font_size, max_width):
current_line = test_line current_line = test_line
else: else:
lines.append(current_line) lines.append(current_line)
current_line = indent + word + " " current_line = word + " "
current_line = current_line.rstrip()
if current_line: if current_line:
lines.append(current_line) lines.append(current_line)
@ -56,7 +55,7 @@ class TextWindowRenderer:
position = rl.Vector2(self._textarea_rect.x + scroll.x, self._textarea_rect.y + scroll.y + i * LINE_HEIGHT) position = rl.Vector2(self._textarea_rect.x + scroll.x, self._textarea_rect.y + scroll.y + i * LINE_HEIGHT)
if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height: if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height:
continue continue
rl.draw_text_ex(gui_app.font(), line.rstrip(), position, FONT_SIZE, 0, rl.WHITE) rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE)
rl.end_scissor_mode() rl.end_scissor_mode()
button_bounds = rl.Rectangle(gui_app.width - MARGIN - BUTTON_SIZE.x, gui_app.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y) button_bounds = rl.Rectangle(gui_app.width - MARGIN - BUTTON_SIZE.x, gui_app.height - MARGIN - BUTTON_SIZE.y, BUTTON_SIZE.x, BUTTON_SIZE.y)

@ -0,0 +1,197 @@
#!/usr/bin/env python3
import sys
import subprocess
import threading
import pyray as rl
from enum import IntEnum
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.button import gui_button, ButtonStyle
from openpilot.system.ui.lib.label import gui_text_box, gui_label
# Constants
MARGIN = 50
BUTTON_HEIGHT = 160
BUTTON_WIDTH = 400
PROGRESS_BAR_HEIGHT = 72
TITLE_FONT_SIZE = 80
BODY_FONT_SIZE = 65
BACKGROUND_COLOR = rl.BLACK
PROGRESS_BG_COLOR = rl.Color(41, 41, 41, 255)
PROGRESS_COLOR = rl.Color(54, 77, 239, 255)
class Screen(IntEnum):
PROMPT = 0
WIFI = 1
PROGRESS = 2
class Updater:
def __init__(self, updater_path, manifest_path):
self.updater = updater_path
self.manifest = manifest_path
self.current_screen = Screen.PROMPT
self.progress_value = 0
self.progress_text = "Loading..."
self.show_reboot_button = False
self.process = None
self.update_thread = None
def install_update(self):
self.current_screen = Screen.PROGRESS
self.progress_value = 0
self.progress_text = "Downloading..."
self.show_reboot_button = False
# Start the update process in a separate thread
self.update_thread = threading.Thread(target=self._run_update_process)
self.update_thread.daemon = True
self.update_thread.start()
def _run_update_process(self):
# TODO: just import it and run in a thread without a subprocess
cmd = [self.updater, "--swap", self.manifest]
self.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1, universal_newlines=True)
for line in self.process.stdout:
parts = line.strip().split(":")
if len(parts) == 2:
self.progress_text = parts[0]
try:
self.progress_value = int(float(parts[1]))
except ValueError:
pass
exit_code = self.process.wait()
if exit_code == 0:
HARDWARE.reboot()
else:
self.progress_text = "Update failed"
self.show_reboot_button = True
def render_prompt_screen(self):
# Title
title_rect = rl.Rectangle(MARGIN + 50, 250, gui_app.width - MARGIN * 2 - 100, TITLE_FONT_SIZE)
gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD)
# Description
desc_text = "An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. \
The download size is approximately 1GB."
desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE + 75, gui_app.width - MARGIN * 2 - 100, BODY_FONT_SIZE * 3)
gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE)
# Buttons at the bottom
button_y = gui_app.height - MARGIN - BUTTON_HEIGHT
button_width = (gui_app.width - MARGIN * 3) // 2
# WiFi button
wifi_button_rect = rl.Rectangle(MARGIN, button_y, button_width, BUTTON_HEIGHT)
if gui_button(wifi_button_rect, "Connect to Wi-Fi"):
self.current_screen = Screen.WIFI
return # Return to avoid processing other buttons after screen change
# Install button
install_button_rect = rl.Rectangle(MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)
if gui_button(install_button_rect, "Install", button_style=ButtonStyle.PRIMARY):
self.install_update()
return # Return to avoid further processing after action
def render_wifi_screen(self):
# Title and back button
title_rect = rl.Rectangle(MARGIN + 50, MARGIN, gui_app.width - MARGIN * 2 - 100, 60)
gui_label(title_rect, "Wi-Fi Networks", 60, font_weight=FontWeight.BOLD)
back_button_rect = rl.Rectangle(MARGIN, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT)
if gui_button(back_button_rect, "Back"):
self.current_screen = Screen.PROMPT
return # Return to avoid processing other interactions after screen change
# Draw placeholder for WiFi implementation
placeholder_rect = rl.Rectangle(
MARGIN,
title_rect.y + title_rect.height + MARGIN,
gui_app.width - MARGIN * 2,
gui_app.height - title_rect.height - MARGIN * 3 - BUTTON_HEIGHT
)
# Draw rounded rectangle background
rl.draw_rectangle_rounded(
placeholder_rect,
0.1,
10,
rl.Color(41, 41, 41, 255)
)
# Draw placeholder text
placeholder_text = "WiFi Implementation Placeholder"
text_size = rl.measure_text_ex(gui_app.font(), placeholder_text, 80, 1)
text_pos = rl.Vector2(
placeholder_rect.x + (placeholder_rect.width - text_size.x) / 2,
placeholder_rect.y + (placeholder_rect.height - text_size.y) / 2
)
rl.draw_text_ex(gui_app.font(), placeholder_text, text_pos, 80, 1, rl.WHITE)
# Draw instructions
instructions_text = "Real WiFi functionality would be implemented here"
instructions_size = rl.measure_text_ex(gui_app.font(), instructions_text, 40, 1)
instructions_pos = rl.Vector2(
placeholder_rect.x + (placeholder_rect.width - instructions_size.x) / 2,
text_pos.y + text_size.y + 20
)
rl.draw_text_ex(gui_app.font(), instructions_text, instructions_pos, 40, 1, rl.GRAY)
def render_progress_screen(self):
title_rect = rl.Rectangle(MARGIN + 100, 330, gui_app.width - MARGIN * 2 - 200, 100)
gui_label(title_rect, self.progress_text, 90, font_weight=FontWeight.SEMI_BOLD)
# Progress bar
bar_rect = rl.Rectangle(MARGIN + 100, 330 + 100 + 100, gui_app.width - MARGIN * 2 - 200, PROGRESS_BAR_HEIGHT)
rl.draw_rectangle_rounded(bar_rect, 0.5, 10, PROGRESS_BG_COLOR)
# Calculate the width of the progress chunk
progress_width = (bar_rect.width * self.progress_value) / 100
if progress_width > 0:
progress_rect = rl.Rectangle(bar_rect.x, bar_rect.y, progress_width, bar_rect.height)
rl.draw_rectangle_rounded(progress_rect, 0.5, 10, PROGRESS_COLOR)
# Show reboot button if needed
if self.show_reboot_button:
reboot_rect = rl.Rectangle(MARGIN + 100, gui_app.height - MARGIN - BUTTON_HEIGHT, BUTTON_WIDTH, BUTTON_HEIGHT)
if gui_button(reboot_rect, "Reboot"):
# Return True to signal main loop to exit before rebooting
HARDWARE.reboot()
return
def render(self):
if self.current_screen == Screen.PROMPT:
self.render_prompt_screen()
elif self.current_screen == Screen.WIFI:
self.render_wifi_screen()
elif self.current_screen == Screen.PROGRESS:
self.render_progress_screen()
def main():
if len(sys.argv) < 3:
print("Usage: updater.py <updater_path> <manifest_path>")
sys.exit(1)
updater_path = sys.argv[1]
manifest_path = sys.argv[2]
try:
gui_app.init_window("System Update")
updater = Updater(updater_path, manifest_path)
for _ in gui_app.render():
updater.render()
finally:
# Make sure we clean up even if there's an error
gui_app.close()
if __name__ == "__main__":
main()
Loading…
Cancel
Save