mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-05 16:26:06 +08:00
My Mower Died
This commit is contained in:
Binary file not shown.
@@ -388,6 +388,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"LaneLinesColor", {PERSISTENT, STRING, "", "", 2, SETTINGS_SIMPLE}},
|
||||
{"LaneLinesWidth", {PERSISTENT, FLOAT, "4.0", "2.0", 2, SETTINGS_SIMPLE}},
|
||||
{"LastMapsUpdate", {PERSISTENT, STRING, "", ""}},
|
||||
{"MapsDownloadProgress", {CLEAR_ON_MANAGER_START, STRING, "", ""}},
|
||||
{"MapsDownloadSizeCache", {PERSISTENT, STRING, "{}", "{}"}},
|
||||
{"LateralTune", {PERSISTENT, BOOL, "1", "0", 1}},
|
||||
{"LeadDepartingAlert", {PERSISTENT, BOOL, "0", "0", 0, SETTINGS_SIMPLE}},
|
||||
{"LeadDetectionThreshold", {PERSISTENT, INT, "35", "50", 3}},
|
||||
|
||||
Binary file not shown.
@@ -12,6 +12,14 @@ from opendbc.car.tesla.preap.pedal_feedback import PedalFeedback
|
||||
|
||||
ButtonType = structs.CarState.ButtonEvent.Type
|
||||
|
||||
TESLA_GAS_PRESS_ON = 0.8
|
||||
TESLA_GAS_PRESS_OFF = 0.4
|
||||
|
||||
|
||||
def update_tesla_gas_pressed(previous: bool, pedal_position: float) -> bool:
|
||||
threshold = TESLA_GAS_PRESS_OFF if previous else TESLA_GAS_PRESS_ON
|
||||
return float(pedal_position) > threshold
|
||||
|
||||
|
||||
class CarState(CarStateBase):
|
||||
def __init__(self, CP, FPCP):
|
||||
@@ -28,6 +36,7 @@ class CarState(CarStateBase):
|
||||
self.das_control = None
|
||||
self.cruise_buttons = 0
|
||||
self.prev_cruise_buttons = 0
|
||||
self.gas_pressed = False
|
||||
self.msg_stw_actn_req = None
|
||||
self.speed_units = "MPH"
|
||||
self.cooperative_steering = any(
|
||||
@@ -76,7 +85,11 @@ class CarState(CarStateBase):
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
|
||||
# Gas pedal
|
||||
ret.gasPressed = cp_party.vl["DI_systemStatus"]["DI_accelPedalPos"] > 0
|
||||
self.gas_pressed = update_tesla_gas_pressed(
|
||||
self.gas_pressed,
|
||||
cp_party.vl["DI_systemStatus"]["DI_accelPedalPos"],
|
||||
)
|
||||
ret.gasPressed = self.gas_pressed
|
||||
|
||||
# Brake pedal
|
||||
ret.brake = 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from opendbc.car.common.conversions import Conversions as CV
|
||||
from opendbc.car.tesla.carstate import update_tesla_gas_pressed
|
||||
from opendbc.car.tesla.teslacan import TeslaCAN
|
||||
|
||||
|
||||
@@ -23,3 +24,11 @@ def test_longitudinal_set_speed_tracks_accel_continuously(active, v_ego, accel,
|
||||
_, _, values = TeslaCAN(RecordingPacker()).create_longitudinal_command(4, accel, 0, v_ego, active)
|
||||
|
||||
assert values["DAS_setSpeed"] == pytest.approx(expected_set_speed)
|
||||
|
||||
|
||||
def test_tesla_gas_pressed_hysteresis_prevents_release_chatter():
|
||||
assert update_tesla_gas_pressed(False, 0.4) is False
|
||||
assert update_tesla_gas_pressed(False, 0.8) is False
|
||||
assert update_tesla_gas_pressed(False, 1.2) is True
|
||||
assert update_tesla_gas_pressed(True, 0.4) is False
|
||||
assert update_tesla_gas_pressed(True, 0.8) is True
|
||||
|
||||
@@ -62,6 +62,12 @@ def is_ths_hybrid(CP) -> bool:
|
||||
return CP.carFingerprint == CAR.TOYOTA_PRIUS or is_camry_hybrid(CP)
|
||||
|
||||
|
||||
def should_bypass_toyota_long_pid(CP) -> bool:
|
||||
return bool(CP.enableGasInterceptorDEPRECATED or (
|
||||
CP.carFingerprint == CAR.TOYOTA_CAMRY and not is_camry_hybrid(CP)
|
||||
))
|
||||
|
||||
|
||||
def get_long_tune(CP, params):
|
||||
kiBP = [2., 5.]
|
||||
kiV = [0.5, 0.25]
|
||||
@@ -448,7 +454,7 @@ class CarController(CarControllerBase):
|
||||
a_ego_future = a_ego_blended + j_ego * future_t
|
||||
|
||||
if CC.longActive:
|
||||
if self.CP.enableGasInterceptorDEPRECATED:
|
||||
if should_bypass_toyota_long_pid(self.CP):
|
||||
# Pedal/SDSU Toyotas have shown better behavior when we trust the planner
|
||||
# target directly instead of letting the Toyota longitudinal PID swing it
|
||||
# around. Keep the shared rate limits above, but bypass the extra
|
||||
|
||||
@@ -12,7 +12,7 @@ from opendbc.car.toyota.carcontroller import CarController, get_camry_hybrid_fee
|
||||
get_prius_positive_feedforward_scale, \
|
||||
limit_interceptor_pcm_accel, \
|
||||
limit_interceptor_stopping_accel, limit_no_lead_cruise_sign_flip, \
|
||||
limit_prius_stopping_accel, update_permit_braking
|
||||
limit_prius_stopping_accel, should_bypass_toyota_long_pid, update_permit_braking
|
||||
from opendbc.car.toyota.carstate import CarState, LKAS_BUTTON_CAR, calculate_interceptor_gas_pressed, create_lkas_button_events
|
||||
from opendbc.car.toyota.fingerprints import FW_VERSIONS
|
||||
from opendbc.car.toyota.interface import CarInterface
|
||||
@@ -320,6 +320,22 @@ class TestToyotaInterfaces:
|
||||
controller.speed = 0.0
|
||||
assert controller.k_i == pytest.approx(3.6)
|
||||
assert controller.k_f == pytest.approx(1.0)
|
||||
assert should_bypass_toyota_long_pid(car_params)
|
||||
|
||||
def test_camry_hybrid_keeps_toyota_longitudinal_pid(self):
|
||||
fingerprint = {bus: ({0x2FF: 8} if bus == 0 else {}) for bus in range(8)}
|
||||
hybrid_fw = [CarParams.CarFw(ecu=Ecu.hybrid, address=0x7D2, fwVersion=b"test")]
|
||||
car_params = CarInterface.get_params(
|
||||
CAR.TOYOTA_CAMRY,
|
||||
fingerprint,
|
||||
hybrid_fw,
|
||||
alpha_long=True,
|
||||
is_release=False,
|
||||
docs=False,
|
||||
starpilot_toggles=SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert not should_bypass_toyota_long_pid(car_params)
|
||||
|
||||
def test_camry_continental_radar_converts_absolute_target_speed(self):
|
||||
radar_interface = RadarInterface.__new__(RadarInterface)
|
||||
@@ -450,7 +466,6 @@ class TestToyotaFingerprint:
|
||||
codes |= result
|
||||
|
||||
# Toyota places the ECU part number in their FW versions, assert all parsable
|
||||
# Note that there is only one unique part number per ECU across the fleet, so this
|
||||
# is not important for identification, just a sanity check.
|
||||
assert all(code.count(b"-") > 1 for code in codes), f"FW does not have part number: {fw} {codes}"
|
||||
|
||||
|
||||
@@ -422,7 +422,8 @@ class Controls:
|
||||
if not CC.latActive:
|
||||
self.LaC.reset()
|
||||
self.lane_centering.reset()
|
||||
if not CC.longActive:
|
||||
tesla_pedal_override = self.CP.brand == "tesla" and bool(CS.gasPressed)
|
||||
if not CC.longActive and not tesla_pedal_override:
|
||||
self.LoC.reset()
|
||||
|
||||
# accel PID loop
|
||||
@@ -431,7 +432,8 @@ class Controls:
|
||||
actuators.accel = float(min(self.LoC.update(CC.longActive, CS, long_plan.aTarget, long_plan.shouldStop, pid_accel_limits,
|
||||
self.starpilot_toggles, has_lead=long_plan.hasLead,
|
||||
traffic_mode_enabled=self.sm['starpilotCarState'].trafficModeEnabled,
|
||||
profile_max_accel=self.sm['starpilotPlan'].maxAcceleration),
|
||||
profile_max_accel=self.sm['starpilotPlan'].maxAcceleration,
|
||||
pedal_override=tesla_pedal_override),
|
||||
self.starpilot_toggles.max_desired_acceleration))
|
||||
|
||||
# Steering PID loop and lateral MPC
|
||||
|
||||
@@ -238,6 +238,11 @@ BOLT_2022_2023_LOW_SPEED_CENTER_TAPER_SPEED = 4.0
|
||||
BOLT_2022_2023_LOW_SPEED_CENTER_TAPER_SPEED_WIDTH = 1.5
|
||||
BOLT_2022_2023_LOW_SPEED_CENTER_TAPER_SPEED_MAX = 14.0
|
||||
BOLT_2022_2023_LOW_SPEED_CENTER_TAPER_SPEED_MAX_WIDTH = 2.0
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_BUMP = 0.035
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_LAT = 0.18
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_LAT_WIDTH = 0.06
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_SPEED = 6.7
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_SPEED_WIDTH = 1.5
|
||||
BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_LEFT = 0.16
|
||||
BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_RIGHT = 0.12
|
||||
BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_LEFT = 0.26
|
||||
@@ -858,13 +863,13 @@ RAM_1500_TRANSITION_LAT_FADE_END = 1.85
|
||||
# reverses the requested lateral acceleration roughly once per second at
|
||||
# 29-30 m/s. Fade only rapid, high-speed turn-building torque so the EPS has
|
||||
# less stored torque to unwind while leaving steady curves and counter-torque.
|
||||
KONA_NON_SCC_TRANSITION_TAPER_MAX = 0.28
|
||||
KONA_NON_SCC_TRANSITION_SPEED_ONSET = 23.0
|
||||
KONA_NON_SCC_TRANSITION_TAPER_MAX = 0.34
|
||||
KONA_NON_SCC_TRANSITION_SPEED_ONSET = 22.0
|
||||
KONA_NON_SCC_TRANSITION_SPEED_FULL = 29.0
|
||||
KONA_NON_SCC_TRANSITION_JERK_ONSET = 0.45
|
||||
KONA_NON_SCC_TRANSITION_JERK_FULL = 1.25
|
||||
KONA_NON_SCC_TRANSITION_LAT_FADE_START = 0.55
|
||||
KONA_NON_SCC_TRANSITION_LAT_FADE_END = 1.65
|
||||
KONA_NON_SCC_TRANSITION_JERK_ONSET = 0.35
|
||||
KONA_NON_SCC_TRANSITION_JERK_FULL = 1.20
|
||||
KONA_NON_SCC_TRANSITION_LAT_FADE_START = 0.45
|
||||
KONA_NON_SCC_TRANSITION_LAT_FADE_END = 1.60
|
||||
KONA_NON_SCC_CENTER_TAPER_MAX = 0.14
|
||||
KONA_NON_SCC_CENTER_TAPER_LAT = 0.28
|
||||
KONA_NON_SCC_CENTER_TAPER_SPEED_ONSET = 12.0
|
||||
@@ -1644,6 +1649,15 @@ def get_bolt_2022_2023_center_output_scale(desired_lateral_accel: float, v_ego:
|
||||
|
||||
def get_bolt_2022_2023_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.0, desired_lateral_jerk: float = 0.0) -> float:
|
||||
base_threshold = get_gm_base_friction_threshold(v_ego)
|
||||
center_weight = _bolt_2022_2023_sigmoid(
|
||||
(BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_LAT - abs(desired_lateral_accel)) /
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_LAT_WIDTH
|
||||
)
|
||||
low_speed_weight = _bolt_2022_2023_sigmoid(
|
||||
(BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_SPEED - v_ego) /
|
||||
BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_SPEED_WIDTH
|
||||
)
|
||||
base_threshold += (BOLT_2022_2023_CENTER_FRICTION_THRESHOLD_BUMP * center_weight * low_speed_weight)
|
||||
transition_envelope = _bolt_2022_2023_transition_envelope(v_ego, desired_lateral_accel, desired_lateral_jerk)
|
||||
phase = _bolt_2022_2023_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
|
||||
@@ -18,6 +18,8 @@ MOVING_STOP_FOLLOW_MIN_GAP = 0.25
|
||||
NEGATIVE_TARGET_CREEP_GUARD_SPEED = 0.35
|
||||
NEGATIVE_TARGET_CREEP_GUARD_DECEL = 0.40
|
||||
MODE_TRANSITION_MAX_DECEL = 4.0
|
||||
TESLA_PEDAL_RELEASE_GUARD_TIME = 0.15
|
||||
TESLA_PEDAL_RELEASE_GUARD_MAX_DECEL = 0.35
|
||||
|
||||
LongCtrlState = car.CarControl.Actuators.LongControlState
|
||||
|
||||
@@ -125,6 +127,8 @@ class LongControl:
|
||||
self._mode_setup()
|
||||
self.last_output_accel = 0.0
|
||||
self.stop_release_counter = 0
|
||||
self.pedal_override_active = False
|
||||
self.pedal_override_release_frames = 0
|
||||
self.vehicle_tuning = LongControlVehicleTuning(CP)
|
||||
|
||||
def update_mpc_mode(self, experimental_mode):
|
||||
@@ -229,11 +233,22 @@ class LongControl:
|
||||
return min(output_accel, float(positive_cap))
|
||||
|
||||
def update(self, active, CS, a_target, should_stop, accel_limits, starpilot_toggles, has_lead=False,
|
||||
traffic_mode_enabled=False, profile_max_accel=0.0):
|
||||
traffic_mode_enabled=False, profile_max_accel=0.0, pedal_override=False):
|
||||
"""Update longitudinal control. This updates the state machine and runs a PID loop"""
|
||||
self.pid.neg_limit = accel_limits[0]
|
||||
self.pid.pos_limit = accel_limits[1]
|
||||
|
||||
if pedal_override:
|
||||
self.pedal_override_active = True
|
||||
self.pedal_override_release_frames = 0
|
||||
return 0.0
|
||||
|
||||
if self.pedal_override_active:
|
||||
self.pedal_override_active = False
|
||||
self.pedal_override_release_frames = max(
|
||||
1, int(round(TESLA_PEDAL_RELEASE_GUARD_TIME / DT_CTRL)),
|
||||
)
|
||||
|
||||
previous_long_control_state = self.long_control_state
|
||||
allow_stopping_release = self._stop_release_ready(CS, a_target, should_stop, has_lead, starpilot_toggles)
|
||||
self.long_control_state = long_control_state_trans(self.CP, active, self.long_control_state, CS.vEgo,
|
||||
@@ -316,6 +331,11 @@ class LongControl:
|
||||
else:
|
||||
output_accel = raw_output_accel
|
||||
|
||||
if self.pedal_override_release_frames > 0:
|
||||
self.pedal_override_release_frames -= 1
|
||||
if not should_stop and -TESLA_PEDAL_RELEASE_GUARD_MAX_DECEL < output_accel < 0.0:
|
||||
output_accel = 0.0
|
||||
|
||||
self.last_output_accel = clip(output_accel, accel_limits[0], accel_limits[1])
|
||||
return self.last_output_accel
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ FAR_RADAR_LEAD_ACCEL_TAPER_MIN_GAP_EXCESS = 8.0
|
||||
FAR_RADAR_LEAD_ACCEL_TAPER_MIN_GAP_GAIN = 0.25
|
||||
FAR_RADAR_LEAD_ACCEL_TAPER_FULL_GAP_EXCESS = 25.0
|
||||
FAR_RADAR_LEAD_ACCEL_TAPER_FULL_GAP_GAIN = 0.9
|
||||
STABLE_FOLLOW_CRUISE_MIN_SPEED = 12.0
|
||||
STABLE_FOLLOW_CRUISE_MIN_SPEED = 8.0
|
||||
STABLE_FOLLOW_CRUISE_HYSTERESIS_MIN = 4.0
|
||||
STABLE_FOLLOW_CRUISE_HYSTERESIS_GAIN = 0.14
|
||||
STABLE_FOLLOW_CRUISE_MAX_REL_SPEED = 2.5
|
||||
@@ -92,7 +92,7 @@ STABLE_FOLLOW_CRUISE_PULLAWAY_MIN_HEADWAY_MARGIN = -0.10
|
||||
STABLE_FOLLOW_CRUISE_PULLAWAY_HYSTERESIS_MAX = 1.75
|
||||
VISION_FOLLOW_CRUISE_HOLD_MIN_MODEL_PROB = 0.95
|
||||
VISION_FOLLOW_CRUISE_HOLD_MAX_CRUISE_ADVANTAGE = 2.0
|
||||
NEAR_DUPLICATE_LEAD_SOURCE_MIN_SPEED = 20.0
|
||||
NEAR_DUPLICATE_LEAD_SOURCE_MIN_SPEED = 8.0
|
||||
NEAR_DUPLICATE_IDENTICAL_RADAR_SOURCE_MIN_SPEED = 10.0
|
||||
NEAR_DUPLICATE_LEAD_SOURCE_MIN_MODEL_PROB = 0.9
|
||||
NEAR_DUPLICATE_LEAD_SOURCE_MAX_LEAD_BRAKE = 0.35
|
||||
|
||||
@@ -251,6 +251,18 @@ class TestLatControl:
|
||||
right_unwind = get_bolt_2022_2023_friction_threshold(6.0, -0.7, 0.8)
|
||||
assert left_turn_in <= right_turn_in < base < right_unwind <= left_unwind
|
||||
|
||||
def test_bolt_2022_2023_center_friction_threshold_targets_low_speed_chatter(self):
|
||||
base = get_gm_base_friction_threshold(5.0)
|
||||
low_speed_center = get_bolt_2022_2023_friction_threshold(5.0, 0.0, 0.0)
|
||||
low_speed_turn = get_bolt_2022_2023_friction_threshold(5.0, 0.7, 0.8)
|
||||
medium_speed_center = get_bolt_2022_2023_friction_threshold(8.5, 0.0, 0.0)
|
||||
high_speed_center = get_bolt_2022_2023_friction_threshold(14.0, 0.0, 0.0)
|
||||
|
||||
assert low_speed_center > base
|
||||
assert low_speed_center > low_speed_turn
|
||||
assert low_speed_center - base > medium_speed_center - get_gm_base_friction_threshold(8.5)
|
||||
assert medium_speed_center - get_gm_base_friction_threshold(8.5) > high_speed_center - get_gm_base_friction_threshold(14.0)
|
||||
|
||||
def test_bolt_2022_2023_friction_scale_curve(self):
|
||||
base = get_bolt_2022_2023_friction_scale(25.0, 0.7, 0.8)
|
||||
left_turn_in = get_bolt_2022_2023_friction_scale(6.0, 0.7, 0.8)
|
||||
@@ -809,7 +821,7 @@ class TestLatControl:
|
||||
|
||||
center_transition = get_kona_non_scc_highway_transition_output_scale(0.4, 1.25, 30.0)
|
||||
medium_transition = get_kona_non_scc_highway_transition_output_scale(1.1, -1.25, 30.0)
|
||||
assert center_transition == pytest.approx(0.72)
|
||||
assert center_transition == pytest.approx(0.66)
|
||||
assert center_transition < medium_transition < 1.0
|
||||
assert get_kona_non_scc_highway_transition_output_scale(1.65, 2.5, 30.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
@@ -338,6 +338,65 @@ def test_bolt_acc_pedal_starting_handoff_keeps_small_positive_command():
|
||||
assert output_accel == pytest.approx(0.188, abs=0.01)
|
||||
|
||||
|
||||
def test_tesla_pedal_override_keeps_longitudinal_state_warm_for_release():
|
||||
CP = make_longcontrol_cp(
|
||||
brand="tesla",
|
||||
carFingerprint="TESLA_MODEL_3",
|
||||
startingState=True,
|
||||
vEgoStarting=0.35,
|
||||
)
|
||||
lc = LongControl(CP)
|
||||
lc.long_control_state = LongCtrlState.pid
|
||||
lc.last_output_accel = 0.8
|
||||
|
||||
CS = car.CarState.new_message(vEgo=12.0, aEgo=0.8, brakePressed=False, gasPressed=True)
|
||||
CS.cruiseState.standstill = False
|
||||
override_output = lc.update(
|
||||
active=False,
|
||||
CS=CS,
|
||||
a_target=0.6,
|
||||
should_stop=False,
|
||||
accel_limits=(-3.0, 2.0),
|
||||
starpilot_toggles=make_toggles(),
|
||||
pedal_override=True,
|
||||
)
|
||||
|
||||
assert override_output == 0.0
|
||||
assert lc.long_control_state == LongCtrlState.pid
|
||||
assert lc.last_output_accel == pytest.approx(0.8)
|
||||
|
||||
CS.gasPressed = False
|
||||
release_output = lc.update(
|
||||
active=True,
|
||||
CS=CS,
|
||||
a_target=0.6,
|
||||
should_stop=False,
|
||||
accel_limits=(-3.0, 2.0),
|
||||
starpilot_toggles=make_toggles(),
|
||||
)
|
||||
|
||||
assert release_output >= 0.6
|
||||
|
||||
|
||||
def test_tesla_pedal_release_guard_blocks_mild_regen_pulse():
|
||||
CP = make_longcontrol_cp(
|
||||
brand="tesla",
|
||||
carFingerprint="TESLA_MODEL_3",
|
||||
startingState=True,
|
||||
vEgoStarting=0.35,
|
||||
)
|
||||
lc = LongControl(CP)
|
||||
lc.long_control_state = LongCtrlState.pid
|
||||
CS = car.CarState.new_message(vEgo=12.0, aEgo=0.8, brakePressed=False, gasPressed=True)
|
||||
CS.cruiseState.standstill = False
|
||||
lc.update(False, CS, -0.2, False, (-3.0, 2.0), make_toggles(), pedal_override=True)
|
||||
|
||||
CS.gasPressed = False
|
||||
release_output = lc.update(True, CS, -0.2, False, (-3.0, 2.0), make_toggles())
|
||||
|
||||
assert release_output == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("a_target", "should_stop"), ((-0.2, False), (0.55, True)))
|
||||
def test_bolt_acc_pedal_starting_handoff_never_overrides_stop_request(a_target, should_stop):
|
||||
CP = make_longcontrol_cp(
|
||||
|
||||
@@ -4165,6 +4165,19 @@ def test_near_duplicate_lead_source_hysteresis_prefers_previous_source():
|
||||
assert lead_1_bias > 0.0
|
||||
|
||||
|
||||
def test_near_duplicate_vision_source_hysteresis_applies_at_tesla_city_speed():
|
||||
v_ego = 11.5
|
||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
planner = LongitudinalPlanner(CP, init_v=v_ego)
|
||||
lead_one = make_lead(status=True, d_rel=22.0, v_lead=10.8, a_lead=-0.04, radar=False, model_prob=1.0)
|
||||
lead_two = make_lead(status=True, d_rel=22.1, v_lead=10.82, a_lead=-0.03, radar=False, model_prob=1.0)
|
||||
|
||||
lead_0_bias, lead_1_bias = planner.mpc.get_near_duplicate_lead_source_hysteresis("lead0", lead_one, lead_two, v_ego)
|
||||
|
||||
assert lead_0_bias == 0.0
|
||||
assert lead_1_bias > 0.0
|
||||
|
||||
|
||||
def test_stable_follow_cruise_hysteresis_applies_for_radar_lead():
|
||||
v_ego = 27.0
|
||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
@@ -4176,6 +4189,17 @@ def test_stable_follow_cruise_hysteresis_applies_for_radar_lead():
|
||||
assert hysteresis > 0.0
|
||||
|
||||
|
||||
def test_stable_follow_cruise_hysteresis_applies_to_radarless_lead_below_highway_speed():
|
||||
v_ego = 10.0
|
||||
CP = CarInterface.get_non_essential_params(CAR.HONDA_CIVIC)
|
||||
planner = LongitudinalPlanner(CP, init_v=v_ego)
|
||||
lead = make_lead(status=True, d_rel=18.0, v_lead=9.9, a_lead=-0.02, radar=False, model_prob=1.0)
|
||||
|
||||
hysteresis = planner.mpc.get_stable_follow_cruise_hysteresis(lead, v_ego, 1.45)
|
||||
|
||||
assert hysteresis > 0.0
|
||||
|
||||
|
||||
def test_stable_follow_cruise_hysteresis_holds_pullaway_lead_longer_near_target_gap():
|
||||
v_ego = 15.0
|
||||
t_follow = 1.45
|
||||
|
||||
@@ -269,8 +269,7 @@ class HudRenderer(Widget):
|
||||
|
||||
def render_foreground(self) -> None:
|
||||
"""Draw HUD elements that should sit above alerts."""
|
||||
if ui_state.sm['controlsState'].lateralControlState.which() != 'angleState' and \
|
||||
ui_state.params.get_bool("EnableTorqueBarWidget", default=True):
|
||||
if ui_state.params.get_bool("EnableTorqueBarWidget", default=True):
|
||||
self._torque_bar.render(self._rect)
|
||||
|
||||
if self.is_cruise_set:
|
||||
|
||||
@@ -130,8 +130,6 @@ class StarPilotOnroadView(AugmentedRoadView):
|
||||
"""Draw the curved torque-utilization indicator at the bottom of the screen."""
|
||||
if not self._params.get_bool("EnableTorqueBarWidget", default=True):
|
||||
return
|
||||
if ui_state.sm['controlsState'].lateralControlState.which() == 'angleState':
|
||||
return
|
||||
rl.begin_scissor_mode(
|
||||
int(self._content_rect.x), int(self._content_rect.y),
|
||||
int(self._content_rect.width), int(self._content_rect.height),
|
||||
|
||||
+1
-3
@@ -508,9 +508,7 @@ void Device::updateWakefulness(const UIState &s, const StarPilotUIState &fs) {
|
||||
emit interactiveTimeout();
|
||||
}
|
||||
|
||||
// Power the display from filtered onroad state rather than raw ignition so
|
||||
// brief ignition-line glitches do not blank the screen immediately.
|
||||
setAwake(s.scene.started || interactive_timeout > 0);
|
||||
setAwake(s.scene.ignition || interactive_timeout > 0);
|
||||
}
|
||||
|
||||
UIState *uiState() {
|
||||
|
||||
@@ -404,7 +404,7 @@ class Device:
|
||||
callback()
|
||||
self._prev_timed_out = interaction_timeout
|
||||
|
||||
self._set_awake(ui_state.started or not interaction_timeout or PC)
|
||||
self._set_awake(ui_state.ignition or not interaction_timeout or PC)
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def nonnegative_int(value):
|
||||
try:
|
||||
return max(int(value or 0), 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def storage_bytes(path):
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
return 0
|
||||
|
||||
total = 0
|
||||
try:
|
||||
for item in path.rglob("*"):
|
||||
try:
|
||||
if item.is_file():
|
||||
total += item.stat().st_size
|
||||
except OSError:
|
||||
continue
|
||||
except OSError:
|
||||
return total
|
||||
return total
|
||||
|
||||
|
||||
def selection_key(selected_locations):
|
||||
if isinstance(selected_locations, str):
|
||||
selected_locations = selected_locations.split(",")
|
||||
return ",".join(sorted({str(location).strip() for location in selected_locations if str(location).strip()}))
|
||||
|
||||
|
||||
def estimate_download_bytes(storage_delta_bytes, total_files, downloaded_files):
|
||||
storage_delta_bytes = nonnegative_int(storage_delta_bytes)
|
||||
total_files = nonnegative_int(total_files)
|
||||
downloaded_files = nonnegative_int(downloaded_files)
|
||||
if storage_delta_bytes <= 0 or downloaded_files <= 0 or total_files <= 0:
|
||||
return 0
|
||||
return max(storage_delta_bytes, math.ceil(storage_delta_bytes * total_files / downloaded_files))
|
||||
|
||||
|
||||
def estimate_eta_seconds(estimated_download_bytes, storage_delta_bytes, bytes_per_second):
|
||||
remaining_bytes = max(nonnegative_int(estimated_download_bytes) - nonnegative_int(storage_delta_bytes), 0)
|
||||
bytes_per_second = float(bytes_per_second or 0.0)
|
||||
if remaining_bytes <= 0 or bytes_per_second <= 0:
|
||||
return 0
|
||||
return max(1, math.ceil(remaining_bytes / bytes_per_second))
|
||||
|
||||
|
||||
def load_size_cache(raw_value):
|
||||
if isinstance(raw_value, bytes):
|
||||
raw_value = raw_value.decode("utf-8", errors="ignore")
|
||||
if not raw_value:
|
||||
return {}
|
||||
try:
|
||||
value = json.loads(raw_value)
|
||||
except (TypeError, ValueError):
|
||||
return {}
|
||||
return value if isinstance(value, dict) else {}
|
||||
@@ -19,6 +19,14 @@ from openpilot.starpilot.assets.theme_manager import ThemeManager
|
||||
from openpilot.starpilot.common.starpilot_backups import backup_starpilot
|
||||
from openpilot.starpilot.common.connect_server import sync_konik_dongle_id
|
||||
from openpilot.starpilot.common.maps_catalog import normalize_schedule_value, sanitize_selected_locations_csv
|
||||
from openpilot.starpilot.common.maps_download_progress import (
|
||||
estimate_download_bytes,
|
||||
estimate_eta_seconds,
|
||||
load_size_cache,
|
||||
nonnegative_int,
|
||||
selection_key,
|
||||
storage_bytes,
|
||||
)
|
||||
from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file
|
||||
from openpilot.starpilot.common.starpilot_utilities import get_starpilot_api_info, is_FrogsGoMoo, is_url_pingable, run_cmd
|
||||
from openpilot.starpilot.common.starpilot_variables import (
|
||||
@@ -202,6 +210,92 @@ def update_boot_logo(starpilot=False, stock=False, selected_logo=None):
|
||||
run_cmd(["sudo", "mount", "-o", f"remount,{mount_options}", "/"], "Successfully restored / mount options", "Failed to restore / mount options")
|
||||
|
||||
|
||||
MAPS_DOWNLOAD_PROGRESS_PARAM = "MapsDownloadProgress"
|
||||
MAPS_DOWNLOAD_SIZE_CACHE_PARAM = "MapsDownloadSizeCache"
|
||||
|
||||
|
||||
def _decode_map_param(value):
|
||||
return value.decode("utf-8", errors="ignore") if isinstance(value, bytes) else value
|
||||
|
||||
|
||||
def _get_map_size_cache(params):
|
||||
return load_size_cache(_decode_map_param(params.get(MAPS_DOWNLOAD_SIZE_CACHE_PARAM)))
|
||||
|
||||
|
||||
def _publish_maps_progress(
|
||||
params_memory,
|
||||
maps_selected,
|
||||
baseline_storage_bytes,
|
||||
started_at,
|
||||
progress=None,
|
||||
*,
|
||||
active=None,
|
||||
cancelled=False,
|
||||
completed=False,
|
||||
phase="starting",
|
||||
cached_estimate_bytes=0,
|
||||
):
|
||||
total_files = 0
|
||||
downloaded_files = 0
|
||||
progress_cancelled = False
|
||||
primary_location = ""
|
||||
if progress is not None:
|
||||
total_files = int(progress.totalFiles)
|
||||
downloaded_files = int(progress.downloadedFiles)
|
||||
progress_cancelled = bool(progress.cancelled)
|
||||
try:
|
||||
if len(progress.locationDetails) > 0:
|
||||
primary_location = str(progress.locationDetails[0].location)
|
||||
elif len(progress.locations) > 0:
|
||||
primary_location = str(progress.locations[0])
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
pass
|
||||
|
||||
if active is None:
|
||||
active = bool(progress.active) if progress is not None else False
|
||||
cancelled = bool(cancelled or progress_cancelled)
|
||||
elapsed_seconds = max(time.monotonic() - started_at, 0.0)
|
||||
current_storage_bytes = storage_bytes(MAPS_PATH)
|
||||
storage_delta_bytes = max(current_storage_bytes - baseline_storage_bytes, 0)
|
||||
bytes_per_second = storage_delta_bytes / elapsed_seconds if elapsed_seconds > 0 else 0.0
|
||||
estimated_bytes = estimate_download_bytes(storage_delta_bytes, total_files, downloaded_files)
|
||||
estimate_source = "live_file_rate" if estimated_bytes else ""
|
||||
if not estimated_bytes and cached_estimate_bytes:
|
||||
estimated_bytes = int(cached_estimate_bytes)
|
||||
estimate_source = "previous_download"
|
||||
|
||||
if completed:
|
||||
percent = 100
|
||||
elif estimated_bytes > 0:
|
||||
percent = min(99, int(storage_delta_bytes * 100 / estimated_bytes))
|
||||
elif total_files > 0:
|
||||
percent = min(99, int(downloaded_files * 100 / total_files))
|
||||
else:
|
||||
percent = 0
|
||||
|
||||
payload = {
|
||||
"active": bool(active),
|
||||
"cancelled": cancelled,
|
||||
"completed": bool(completed),
|
||||
"downloadedBytes": storage_delta_bytes,
|
||||
"downloadedFiles": downloaded_files,
|
||||
"estimatedDownloadBytes": estimated_bytes,
|
||||
"estimateSource": estimate_source,
|
||||
"etaSeconds": estimate_eta_seconds(estimated_bytes, storage_delta_bytes, bytes_per_second) if not completed else 0,
|
||||
"percent": percent,
|
||||
"phase": phase,
|
||||
"primaryLocation": primary_location,
|
||||
"selectedKey": selection_key(maps_selected),
|
||||
"selectedLocations": [location for location in maps_selected.split(",") if location],
|
||||
"storageBytes": current_storage_bytes,
|
||||
"totalFiles": total_files,
|
||||
"updatedAt": time.time(),
|
||||
"bytesPerSecond": round(bytes_per_second, 2),
|
||||
}
|
||||
params_memory.put(MAPS_DOWNLOAD_PROGRESS_PARAM, json.dumps(payload, separators=(",", ":")))
|
||||
return payload
|
||||
|
||||
|
||||
def update_maps(now, params, params_memory, manual_update=False):
|
||||
maps_selected_raw = params.get("MapsSelected")
|
||||
maps_selected = sanitize_selected_locations_csv(maps_selected_raw)
|
||||
@@ -230,6 +324,21 @@ def update_maps(now, params, params_memory, manual_update=False):
|
||||
pm = messaging.PubMaster(["mapdIn"])
|
||||
sm = messaging.SubMaster(["mapdExtendedOut"])
|
||||
|
||||
size_cache = _get_map_size_cache(params)
|
||||
cached_entry = size_cache.get(selection_key(maps_selected), {})
|
||||
cached_estimate_bytes = nonnegative_int(cached_entry.get("downloadBytes", 0)) if isinstance(cached_entry, dict) else 0
|
||||
baseline_storage_bytes = storage_bytes(MAPS_PATH)
|
||||
started_at = time.monotonic()
|
||||
_publish_maps_progress(
|
||||
params_memory,
|
||||
maps_selected,
|
||||
baseline_storage_bytes,
|
||||
started_at,
|
||||
active=True,
|
||||
phase="starting",
|
||||
cached_estimate_bytes=cached_estimate_bytes,
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
msg = messaging.new_message("mapdIn")
|
||||
@@ -238,6 +347,7 @@ def update_maps(now, params, params_memory, manual_update=False):
|
||||
pm.send("mapdIn", msg)
|
||||
|
||||
started = False
|
||||
last_progress = None
|
||||
while True:
|
||||
sm.update(1000)
|
||||
|
||||
@@ -246,19 +356,60 @@ def update_maps(now, params, params_memory, manual_update=False):
|
||||
msg.mapdIn.type = 27
|
||||
pm.send("mapdIn", msg)
|
||||
|
||||
_publish_maps_progress(
|
||||
params_memory,
|
||||
maps_selected,
|
||||
baseline_storage_bytes,
|
||||
started_at,
|
||||
progress=last_progress,
|
||||
active=False,
|
||||
cancelled=True,
|
||||
phase="cancelled",
|
||||
cached_estimate_bytes=cached_estimate_bytes,
|
||||
)
|
||||
params_memory.remove("CancelDownloadMaps")
|
||||
params_memory.remove("DownloadMaps")
|
||||
return
|
||||
|
||||
if sm.updated["mapdExtendedOut"]:
|
||||
progress = sm["mapdExtendedOut"].downloadProgress
|
||||
last_progress = progress
|
||||
|
||||
if progress.active:
|
||||
started = True
|
||||
|
||||
_publish_maps_progress(
|
||||
params_memory,
|
||||
maps_selected,
|
||||
baseline_storage_bytes,
|
||||
started_at,
|
||||
progress=progress,
|
||||
phase="downloading" if progress.active else "finishing",
|
||||
cached_estimate_bytes=cached_estimate_bytes,
|
||||
)
|
||||
|
||||
if not progress.active and started:
|
||||
break
|
||||
|
||||
final_progress = _publish_maps_progress(
|
||||
params_memory,
|
||||
maps_selected,
|
||||
baseline_storage_bytes,
|
||||
started_at,
|
||||
progress=last_progress,
|
||||
active=False,
|
||||
completed=True,
|
||||
phase="complete",
|
||||
cached_estimate_bytes=cached_estimate_bytes,
|
||||
)
|
||||
if final_progress["downloadedBytes"] > 0:
|
||||
size_cache[selection_key(maps_selected)] = {
|
||||
"downloadBytes": final_progress["downloadedBytes"],
|
||||
"totalFiles": final_progress["totalFiles"],
|
||||
"updatedAt": now.isoformat(),
|
||||
}
|
||||
params.put(MAPS_DOWNLOAD_SIZE_CACHE_PARAM, json.dumps(size_cache, separators=(",", ":")))
|
||||
|
||||
params.put("LastMapsUpdate", todays_date)
|
||||
params_memory.remove("DownloadMaps")
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from openpilot.starpilot.common.maps_download_progress import (
|
||||
estimate_download_bytes,
|
||||
estimate_eta_seconds,
|
||||
load_size_cache,
|
||||
selection_key,
|
||||
storage_bytes,
|
||||
)
|
||||
|
||||
|
||||
def test_storage_bytes_and_selection_key(tmp_path):
|
||||
maps_path = tmp_path / "maps"
|
||||
maps_path.mkdir()
|
||||
(maps_path / "first.bin").write_bytes(b"1234")
|
||||
(maps_path / "nested").mkdir()
|
||||
(maps_path / "nested" / "second.bin").write_bytes(b"123456")
|
||||
|
||||
assert storage_bytes(maps_path) == 10
|
||||
assert selection_key("us-ca,us-tx,us-ca") == "us-ca,us-tx"
|
||||
|
||||
|
||||
def test_download_size_and_eta_estimates():
|
||||
assert estimate_download_bytes(100, total_files=10, downloaded_files=2) == 500
|
||||
assert estimate_download_bytes(0, total_files=10, downloaded_files=2) == 0
|
||||
assert estimate_eta_seconds(500, 100, 100) == 4
|
||||
assert estimate_eta_seconds(500, 500, 100) == 0
|
||||
|
||||
|
||||
def test_load_size_cache_rejects_invalid_values():
|
||||
assert load_size_cache(b'{"us-ca":{"downloadBytes":123}}')["us-ca"]["downloadBytes"] == 123
|
||||
assert load_size_cache("not json") == {}
|
||||
assert load_size_cache("[]") == {}
|
||||
@@ -65,6 +65,47 @@
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.maps-progress-card {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.maps-progress-header,
|
||||
.maps-progress-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.maps-progress-meta,
|
||||
.maps-progress-note,
|
||||
.maps-progress-location {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.84rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.maps-progress-track {
|
||||
background: var(--input-bg);
|
||||
border-radius: 999px;
|
||||
height: 0.65rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.maps-progress-fill {
|
||||
background: var(--success-bg);
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
.maps-error {
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
@@ -283,4 +324,10 @@
|
||||
.maps-group-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.maps-progress-meta {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,20 @@ const state = reactive({
|
||||
scheduleLabel: "Monthly",
|
||||
selectedCount: 0,
|
||||
storageBytes: 0,
|
||||
downloadProgress: {
|
||||
active: false,
|
||||
cancelled: false,
|
||||
completed: false,
|
||||
downloadedBytes: 0,
|
||||
downloadedFiles: 0,
|
||||
estimatedDownloadBytes: 0,
|
||||
estimateSource: "",
|
||||
etaSeconds: 0,
|
||||
percent: 0,
|
||||
phase: "idle",
|
||||
primaryLocation: "",
|
||||
totalFiles: 0,
|
||||
},
|
||||
},
|
||||
tokenLabels: {},
|
||||
});
|
||||
@@ -82,6 +96,34 @@ function formatBytes(bytes) {
|
||||
return `${scaled >= 10 || index === 0 ? scaled.toFixed(0) : scaled.toFixed(2)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const value = Math.max(0, Math.round(Number(seconds || 0)));
|
||||
if (value < 60) return `${value}s`;
|
||||
const minutes = Math.floor(value / 60);
|
||||
const remainingSeconds = value % 60;
|
||||
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
|
||||
function normalizeDownloadProgress(progress) {
|
||||
const value = progress && typeof progress === "object" ? progress : {};
|
||||
return {
|
||||
active: Boolean(value.active),
|
||||
cancelled: Boolean(value.cancelled),
|
||||
completed: Boolean(value.completed),
|
||||
downloadedBytes: Number(value.downloadedBytes || 0),
|
||||
downloadedFiles: Number(value.downloadedFiles || 0),
|
||||
estimatedDownloadBytes: Number(value.estimatedDownloadBytes || 0),
|
||||
estimateSource: String(value.estimateSource || ""),
|
||||
etaSeconds: Number(value.etaSeconds || 0),
|
||||
percent: Math.max(0, Math.min(100, Number(value.percent || 0))),
|
||||
phase: String(value.phase || "idle"),
|
||||
primaryLocation: String(value.primaryLocation || ""),
|
||||
totalFiles: Number(value.totalFiles || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return [...new Set(values)].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
||||
}
|
||||
@@ -143,6 +185,7 @@ function applyStatus(payload) {
|
||||
scheduleLabel: payload.scheduleLabel || "Monthly",
|
||||
selectedCount: Number(payload.selectedCount || 0),
|
||||
storageBytes: Number(payload.storageBytes || 0),
|
||||
downloadProgress: normalizeDownloadProgress(payload.downloadProgress),
|
||||
};
|
||||
state.selectedSaved = selectedLocations;
|
||||
if (!hadSelectionChanges) {
|
||||
@@ -375,6 +418,53 @@ function renderSelectedSummary() {
|
||||
`;
|
||||
}
|
||||
|
||||
function downloadSizeLabel() {
|
||||
const progress = state.status.downloadProgress;
|
||||
if (!selectionDirty() && progress.estimatedDownloadBytes > 0) {
|
||||
return `~${formatBytes(progress.estimatedDownloadBytes)}`;
|
||||
}
|
||||
if (state.selectedDraft.length > 0) {
|
||||
return "Calculated during download";
|
||||
}
|
||||
return "Select regions";
|
||||
}
|
||||
|
||||
function renderDownloadProgress() {
|
||||
const progress = state.status.downloadProgress;
|
||||
const visible = state.status.downloading || (!selectionDirty() && (progress.completed || progress.cancelled || progress.estimatedDownloadBytes > 0));
|
||||
if (!visible) return "";
|
||||
|
||||
const isActive = state.status.downloading;
|
||||
const title = isActive ? "Download Progress" : progress.completed ? "Last Download" : progress.cancelled ? "Download Cancelled" : "Download Estimate";
|
||||
const sizeLabel = progress.estimatedDownloadBytes > 0 ? `~${formatBytes(progress.estimatedDownloadBytes)} total` : "Calculating total size...";
|
||||
const storedLabel = progress.downloadedBytes > 0 ? `${formatBytes(progress.downloadedBytes)} stored` : "No files stored yet";
|
||||
const filesLabel = progress.totalFiles > 0 ? `${progress.downloadedFiles} / ${progress.totalFiles} files` : "Waiting for map service...";
|
||||
const etaLabel = isActive && progress.etaSeconds > 0 ? `About ${formatDuration(progress.etaSeconds)} remaining` : "ETA unavailable until files start arriving";
|
||||
const sourceLabel = progress.estimateSource === "previous_download"
|
||||
? "Estimate based on the last download of this exact selection."
|
||||
: "Size is estimated from the map files as they arrive.";
|
||||
|
||||
return html`
|
||||
<div class="maps-progress-card">
|
||||
<div class="maps-progress-header">
|
||||
<strong>${title}</strong>
|
||||
<span>${Math.round(progress.percent)}%</span>
|
||||
</div>
|
||||
<div class="maps-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${Math.round(progress.percent)}">
|
||||
<div class="maps-progress-fill" style="width: ${progress.percent}%;"></div>
|
||||
</div>
|
||||
<div class="maps-progress-meta">
|
||||
<span>${sizeLabel}</span>
|
||||
<span>${storedLabel}</span>
|
||||
<span>${filesLabel}</span>
|
||||
<span>${etaLabel}</span>
|
||||
</div>
|
||||
${progress.primaryLocation ? html`<p class="maps-progress-location">Current region: ${progress.primaryLocation}</p>` : ""}
|
||||
<p class="maps-progress-note">${sourceLabel}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroup(group) {
|
||||
const selectedCount = selectedCountForGroup(group);
|
||||
|
||||
@@ -432,6 +522,10 @@ export function MapsManager() {
|
||||
<span class="maps-stat-label">Saved Regions</span>
|
||||
<span class="maps-stat-value">${() => state.status.selectedCount}</span>
|
||||
</div>
|
||||
<div class="maps-stat">
|
||||
<span class="maps-stat-label">Download Size</span>
|
||||
<span class="maps-stat-value">${() => downloadSizeLabel()}</span>
|
||||
</div>
|
||||
<div class="maps-stat">
|
||||
<span class="maps-stat-label">Last Updated</span>
|
||||
<span class="maps-stat-value">${() => state.status.lastUpdate}</span>
|
||||
@@ -445,6 +539,7 @@ export function MapsManager() {
|
||||
${() => state.status.isOnroad ? html`<p class="maps-warning">Map downloads and removal are blocked while driving.</p>` : ""}
|
||||
${() => selectionDirty() ? html`<p class="maps-warning">You have unsaved region changes. Downloading now will use the current Galaxy selection.</p>` : ""}
|
||||
${() => scheduleDirty() ? html`<p class="maps-warning">You have an unsaved schedule change. Downloading now will also apply it.</p>` : ""}
|
||||
${() => renderDownloadProgress()}
|
||||
<div class="maps-action-row">
|
||||
<button
|
||||
class="maps-btn maps-btn-primary"
|
||||
|
||||
@@ -64,6 +64,7 @@ from openpilot.starpilot.common.maps_catalog import (
|
||||
schedule_label,
|
||||
schedule_param_value,
|
||||
)
|
||||
from openpilot.starpilot.common.maps_download_progress import load_size_cache, nonnegative_int, selection_key
|
||||
from openpilot.starpilot.common.experimental_state import sync_persist_chill_state, sync_persist_experimental_state
|
||||
from openpilot.starpilot.common.favorite_slots import (
|
||||
FAVORITE_ACTION_OPTIONS,
|
||||
@@ -638,6 +639,8 @@ MODEL_SORT_MODE_PARAM = "ModelSortMode"
|
||||
MODEL_USER_FAVORITES_PARAM = "UserFavorites"
|
||||
MAPS_DOWNLOAD_PARAM = "DownloadMaps"
|
||||
MAPS_CANCEL_DOWNLOAD_PARAM = "CancelDownloadMaps"
|
||||
MAPS_DOWNLOAD_PROGRESS_PARAM = "MapsDownloadProgress"
|
||||
MAPS_DOWNLOAD_SIZE_CACHE_PARAM = "MapsDownloadSizeCache"
|
||||
|
||||
|
||||
def _get_galaxy_dir():
|
||||
@@ -5141,6 +5144,60 @@ def setup(app):
|
||||
except Exception:
|
||||
storage_bytes = 0
|
||||
|
||||
selected_key = selection_key(selected_locations)
|
||||
raw_progress = params_memory.get(MAPS_DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or ""
|
||||
try:
|
||||
download_progress = json.loads(raw_progress) if raw_progress else {}
|
||||
except (TypeError, ValueError):
|
||||
download_progress = {}
|
||||
if not isinstance(download_progress, dict):
|
||||
download_progress = {}
|
||||
|
||||
if not params_memory.get_bool(MAPS_DOWNLOAD_PARAM) and selected_key and download_progress.get("selectedKey") != selected_key:
|
||||
size_cache = load_size_cache(params.get(MAPS_DOWNLOAD_SIZE_CACHE_PARAM, encoding="utf-8") or "")
|
||||
cached_entry = size_cache.get(selected_key, {})
|
||||
cached_bytes = nonnegative_int(cached_entry.get("downloadBytes", 0)) if isinstance(cached_entry, dict) else 0
|
||||
if cached_bytes > 0:
|
||||
download_progress = {
|
||||
"active": False,
|
||||
"cancelled": False,
|
||||
"completed": False,
|
||||
"downloadedBytes": 0,
|
||||
"downloadedFiles": 0,
|
||||
"estimatedDownloadBytes": cached_bytes,
|
||||
"estimateSource": "previous_download",
|
||||
"etaSeconds": 0,
|
||||
"percent": 0,
|
||||
"phase": "idle",
|
||||
"primaryLocation": "",
|
||||
"selectedKey": selected_key,
|
||||
"selectedLocations": selected_locations,
|
||||
"storageBytes": storage_bytes,
|
||||
"totalFiles": nonnegative_int(cached_entry.get("totalFiles", 0)),
|
||||
"updatedAt": cached_entry.get("updatedAt", ""),
|
||||
"bytesPerSecond": 0,
|
||||
}
|
||||
else:
|
||||
download_progress = {
|
||||
"active": False,
|
||||
"cancelled": False,
|
||||
"completed": False,
|
||||
"downloadedBytes": 0,
|
||||
"downloadedFiles": 0,
|
||||
"estimatedDownloadBytes": 0,
|
||||
"estimateSource": "",
|
||||
"etaSeconds": 0,
|
||||
"percent": 0,
|
||||
"phase": "idle",
|
||||
"primaryLocation": "",
|
||||
"selectedKey": selected_key,
|
||||
"selectedLocations": selected_locations,
|
||||
"storageBytes": storage_bytes,
|
||||
"totalFiles": 0,
|
||||
"updatedAt": "",
|
||||
"bytesPerSecond": 0,
|
||||
}
|
||||
|
||||
return {
|
||||
"selectedLocations": selected_locations,
|
||||
"selectedEntries": selected_entries,
|
||||
@@ -5155,6 +5212,7 @@ def setup(app):
|
||||
"scheduleOptions": MAP_SCHEDULE_OPTIONS,
|
||||
"scheduleValue": schedule_param_value(params.get("PreferredSchedule")),
|
||||
"storageBytes": storage_bytes,
|
||||
"downloadProgress": download_progress,
|
||||
}
|
||||
|
||||
@app.route("/api/maps/catalog", methods=["GET"])
|
||||
|
||||
Reference in New Issue
Block a user