mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-12 12:52:13 +08:00
FTM
This commit is contained in:
Binary file not shown.
@@ -318,6 +318,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ForceStopDistanceOffset", {PERSISTENT, INT, "0", "0", 2}},
|
||||
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"FTMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
|
||||
{"FTMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
|
||||
{"FTMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||
{"GalaxyDashboardStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
|
||||
{"StarPilotApiToken", {PERSISTENT | DONT_LOG, STRING, "", "", 0}},
|
||||
|
||||
Binary file not shown.
@@ -90,6 +90,7 @@ class LatControlTorque(LatControl):
|
||||
self.is_silverado = CP.carFingerprint in SILVERADO_CARS
|
||||
self.is_gm = CP.brand == "gm"
|
||||
self.is_hkg_canfd_torque = CP.brand == "hyundai" and bool(CP.flags & HyundaiFlags.CANFD)
|
||||
self.ftm_surface_profile_key = get_ftm_surface_profile_key(CP.carFingerprint, torque_control=True)
|
||||
if self.is_ioniq_6:
|
||||
self.low_speed_reset_threshold = min(self.low_speed_reset_threshold, IONIQ_6_LOW_SPEED_PID_RESET_SPEED)
|
||||
self.use_bolt_ff_scaling = self.is_bolt_2022_2023 or self.is_bolt_2018_2021 or self.is_bolt_2017
|
||||
@@ -157,6 +158,7 @@ class LatControlTorque(LatControl):
|
||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay, calibrated_pose, model_data, starpilot_toggles):
|
||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||
pid_log.version = VERSION
|
||||
set_ftm_runtime_overrides(getattr(starpilot_toggles, "ftm_active_overrides", None))
|
||||
if not active:
|
||||
output_torque = 0.0
|
||||
pid_log.active = False
|
||||
@@ -324,6 +326,15 @@ class LatControlTorque(LatControl):
|
||||
friction_threshold = CIVIC_BOSCH_MODIFIED_B_FIXED_FRICTION_THRESHOLD
|
||||
friction_scale = get_civic_bosch_modified_b_friction_scale(CS.vEgo, setpoint, desired_lateral_jerk)
|
||||
friction_scale = 1.0 + ((friction_scale - 1.0) * civic_bosch_modified_a_center_taper)
|
||||
if self.ftm_surface_profile_key and not ioniq_6_active:
|
||||
universal_ftm_profile = self.ftm_surface_profile_key == FTM_UNIVERSAL_PROFILE_KEY
|
||||
ftm_full_surface_center_taper = get_ftm_full_surface_center_taper_scale(self.ftm_surface_profile_key, setpoint, CS.vEgo,
|
||||
include_base_center=universal_ftm_profile)
|
||||
ff *= get_ftm_full_surface_ff_scale(self.ftm_surface_profile_key, setpoint, desired_lateral_jerk, CS.vEgo,
|
||||
include_base_ff=universal_ftm_profile) * ftm_full_surface_center_taper
|
||||
friction_threshold = get_ftm_full_surface_friction_threshold(self.ftm_surface_profile_key, friction_threshold, CS.vEgo,
|
||||
setpoint, desired_lateral_jerk,
|
||||
include_base_threshold=universal_ftm_profile)
|
||||
if trailer_load_kg > 0.0:
|
||||
ff *= get_trailer_lateral_ff_scale(trailer_load_kg, CS.vEgo, setpoint)
|
||||
friction_scale *= get_trailer_lateral_friction_scale(trailer_load_kg, CS.vEgo, setpoint)
|
||||
@@ -353,6 +364,11 @@ class LatControlTorque(LatControl):
|
||||
actual_angle_no_offset = CS.steeringAngleDeg - params.angleOffsetDeg
|
||||
output_torque = get_ioniq_6_low_speed_angle_assist_torque(desired_angle_no_offset, actual_angle_no_offset,
|
||||
output_torque, CS.vEgo)
|
||||
elif self.ftm_surface_profile_key and not CS.steeringPressed:
|
||||
desired_angle_no_offset = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll))
|
||||
actual_angle_no_offset = CS.steeringAngleDeg - params.angleOffsetDeg
|
||||
output_torque = get_ftm_full_surface_low_speed_angle_assist_torque(self.ftm_surface_profile_key, desired_angle_no_offset,
|
||||
actual_angle_no_offset, output_torque, CS.vEgo)
|
||||
if ioniq_6_active:
|
||||
output_torque *= get_ioniq_6_highway_output_taper_scale(setpoint, CS.vEgo)
|
||||
output_torque *= get_ioniq_6_highway_transition_output_taper_scale(setpoint, desired_lateral_jerk, CS.vEgo)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
@@ -10,6 +11,8 @@ from openpilot.starpilot.common.testing_grounds import testing_ground
|
||||
CIVIC_BOSCH_MODIFIED_B_FIXED_FRICTION_THRESHOLD = 0.30
|
||||
STANDARD_FRICTION_THRESHOLD = 0.30
|
||||
HKG_CANFD_BASE_FRICTION_THRESHOLD = 0.39
|
||||
FTM_SCHEMA_VERSION = 1
|
||||
FTM_FRICTION_SPEED_KNOTS = [0.0, 5.0, 10.0, 15.0, 25.0]
|
||||
CIVIC_BOSCH_MODIFIED_B_LAT_ACCEL_FACTOR_MULT = 1.20
|
||||
CIVIC_BOSCH_MODIFIED_A_VARIANT_LAT_ACCEL_FACTOR_MULT = 1.00
|
||||
CIVIC_BOSCH_MODIFIED_B_VARIANT_LAT_ACCEL_FACTOR_MULT = 1.75
|
||||
@@ -692,6 +695,9 @@ TRAILER_LATERAL_LAT_RISE = 0.30
|
||||
TRAILER_LATERAL_FF_GAIN = 0.05
|
||||
TRAILER_LATERAL_FRICTION_GAIN = 0.03
|
||||
|
||||
_FTM_ACTIVE_OVERRIDES_TEXT = ""
|
||||
_FTM_ACTIVE_OVERRIDES = {}
|
||||
|
||||
|
||||
def _sigmoid(x: float) -> float:
|
||||
if x >= 0.0:
|
||||
@@ -702,17 +708,119 @@ def _sigmoid(x: float) -> float:
|
||||
return z / (1.0 + z)
|
||||
|
||||
|
||||
def get_gm_base_friction_threshold(v_ego: float) -> float:
|
||||
# GM's speed-scaled base friction threshold behavior.
|
||||
def _gm_base_friction_threshold_default(v_ego: float) -> float:
|
||||
return float(np.interp(v_ego, [1 * CV.MPH_TO_MS, 20 * CV.MPH_TO_MS, 75 * CV.MPH_TO_MS], [0.16, 0.19, 0.27]))
|
||||
|
||||
|
||||
def _standard_friction_threshold_default(v_ego: float) -> float:
|
||||
return max(_gm_base_friction_threshold_default(v_ego), STANDARD_FRICTION_THRESHOLD)
|
||||
|
||||
|
||||
def _hkg_canfd_base_friction_threshold_default(v_ego: float) -> float:
|
||||
return max(_gm_base_friction_threshold_default(v_ego), HKG_CANFD_BASE_FRICTION_THRESHOLD)
|
||||
|
||||
|
||||
def _ftm_copy_json(value):
|
||||
return json.loads(json.dumps(value))
|
||||
|
||||
|
||||
def normalize_ftm_overrides(overrides) -> dict:
|
||||
if overrides in (None, "", b""):
|
||||
return {}
|
||||
|
||||
if isinstance(overrides, bytes):
|
||||
overrides = overrides.decode("utf-8", errors="replace")
|
||||
|
||||
if isinstance(overrides, str):
|
||||
stripped = overrides.strip()
|
||||
if not stripped:
|
||||
return {}
|
||||
try:
|
||||
overrides = json.loads(stripped)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
if not isinstance(overrides, dict):
|
||||
return {}
|
||||
|
||||
normalized = {
|
||||
"schemaVersion": FTM_SCHEMA_VERSION,
|
||||
"baseFrictionThresholds": {},
|
||||
"vehicleKnobs": {},
|
||||
}
|
||||
|
||||
for family in ("gm", "standard", "hkg_canfd"):
|
||||
payload = overrides.get("baseFrictionThresholds", {}).get(family, {})
|
||||
values = payload.get("values", payload if isinstance(payload, list) else [])
|
||||
if not isinstance(values, (list, tuple)) or len(values) != len(FTM_FRICTION_SPEED_KNOTS):
|
||||
continue
|
||||
try:
|
||||
normalized["baseFrictionThresholds"][family] = {
|
||||
"speedKnots": list(FTM_FRICTION_SPEED_KNOTS),
|
||||
"values": [float(v) for v in values],
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
vehicle_knobs = overrides.get("vehicleKnobs", {})
|
||||
if isinstance(vehicle_knobs, dict):
|
||||
for key, value in vehicle_knobs.items():
|
||||
try:
|
||||
normalized["vehicleKnobs"][str(key)] = float(value)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not normalized["baseFrictionThresholds"] and not normalized["vehicleKnobs"]:
|
||||
return {}
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def set_ftm_runtime_overrides(overrides) -> None:
|
||||
global _FTM_ACTIVE_OVERRIDES, _FTM_ACTIVE_OVERRIDES_TEXT
|
||||
|
||||
normalized = normalize_ftm_overrides(overrides)
|
||||
text = json.dumps(normalized, sort_keys=True, separators=(",", ":")) if normalized else ""
|
||||
if text == _FTM_ACTIVE_OVERRIDES_TEXT:
|
||||
return
|
||||
|
||||
_FTM_ACTIVE_OVERRIDES_TEXT = text
|
||||
_FTM_ACTIVE_OVERRIDES = normalized
|
||||
|
||||
|
||||
def clear_ftm_runtime_overrides() -> None:
|
||||
set_ftm_runtime_overrides({})
|
||||
|
||||
|
||||
def get_ftm_runtime_overrides() -> dict:
|
||||
return _ftm_copy_json(_FTM_ACTIVE_OVERRIDES) if _FTM_ACTIVE_OVERRIDES else {}
|
||||
|
||||
|
||||
def _ftm_base_friction_threshold(family: str, v_ego: float, default_fn) -> float:
|
||||
payload = _FTM_ACTIVE_OVERRIDES.get("baseFrictionThresholds", {}).get(family, {})
|
||||
values = payload.get("values", [])
|
||||
if isinstance(values, list) and len(values) == len(FTM_FRICTION_SPEED_KNOTS):
|
||||
return float(np.interp(v_ego, FTM_FRICTION_SPEED_KNOTS, values))
|
||||
return float(default_fn(v_ego))
|
||||
|
||||
|
||||
def _ftm_vehicle_knob(name: str, default_value: float) -> float:
|
||||
try:
|
||||
return float(_FTM_ACTIVE_OVERRIDES.get("vehicleKnobs", {}).get(name, default_value))
|
||||
except Exception:
|
||||
return float(default_value)
|
||||
|
||||
|
||||
def get_gm_base_friction_threshold(v_ego: float) -> float:
|
||||
return _ftm_base_friction_threshold("gm", v_ego, _gm_base_friction_threshold_default)
|
||||
|
||||
|
||||
def get_standard_friction_threshold(v_ego: float) -> float:
|
||||
return max(get_gm_base_friction_threshold(v_ego), STANDARD_FRICTION_THRESHOLD)
|
||||
return _ftm_base_friction_threshold("standard", v_ego, _standard_friction_threshold_default)
|
||||
|
||||
|
||||
def get_hkg_canfd_base_friction_threshold(v_ego: float) -> float:
|
||||
return max(get_gm_base_friction_threshold(v_ego), HKG_CANFD_BASE_FRICTION_THRESHOLD)
|
||||
return _ftm_base_friction_threshold("hkg_canfd", v_ego, _hkg_canfd_base_friction_threshold_default)
|
||||
|
||||
|
||||
def get_trailer_lateral_assist_factor(trailer_load_kg: float, v_ego: float, desired_lateral_accel: float) -> float:
|
||||
@@ -756,7 +864,11 @@ def get_prius_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: float
|
||||
if desired_lateral_accel == 0.0:
|
||||
return 1.0
|
||||
|
||||
gain = _prius_side_value(desired_lateral_accel, PRIUS_FF_GAIN_LEFT, PRIUS_FF_GAIN_RIGHT)
|
||||
gain = _prius_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("toyota_prius.ff_gain_left", PRIUS_FF_GAIN_LEFT),
|
||||
_ftm_vehicle_knob("toyota_prius.ff_gain_right", PRIUS_FF_GAIN_RIGHT),
|
||||
)
|
||||
abs_lateral_accel = abs(desired_lateral_accel)
|
||||
onset = _prius_sigmoid((abs_lateral_accel - PRIUS_FF_ONSET) / PRIUS_FF_ONSET_WIDTH)
|
||||
cutoff = _prius_sigmoid((PRIUS_FF_CUTOFF - abs_lateral_accel) / PRIUS_FF_CUTOFF_WIDTH)
|
||||
@@ -765,9 +877,17 @@ def get_prius_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: float
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
low_speed_factor = _prius_low_speed_factor(v_ego)
|
||||
turn_in_boost = 1.0 + (_prius_side_value(desired_lateral_accel, PRIUS_TURN_IN_BOOST_LEFT, PRIUS_TURN_IN_BOOST_RIGHT) *
|
||||
turn_in_boost = 1.0 + (_prius_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("toyota_prius.turn_in_boost_left", PRIUS_TURN_IN_BOOST_LEFT),
|
||||
_ftm_vehicle_knob("toyota_prius.turn_in_boost_right", PRIUS_TURN_IN_BOOST_RIGHT),
|
||||
) *
|
||||
turn_in_weight * (0.35 + 0.65 * low_speed_factor))
|
||||
unwind_taper = 1.0 - (_prius_side_value(desired_lateral_accel, PRIUS_UNWIND_TAPER_LEFT, PRIUS_UNWIND_TAPER_RIGHT) *
|
||||
unwind_taper = 1.0 - (_prius_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("toyota_prius.unwind_taper_left", PRIUS_UNWIND_TAPER_LEFT),
|
||||
_ftm_vehicle_knob("toyota_prius.unwind_taper_right", PRIUS_UNWIND_TAPER_RIGHT),
|
||||
) *
|
||||
unwind_weight * (0.35 + 0.65 * low_speed_factor))
|
||||
return 1.0 + (extra_scale * turn_in_boost * max(unwind_taper, 0.0))
|
||||
|
||||
@@ -778,9 +898,17 @@ def get_prius_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.
|
||||
phase = _prius_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
threshold_scale = 1.0 - (_prius_side_value(desired_lateral_accel, PRIUS_TURN_IN_THRESHOLD_REDUCTION_LEFT, PRIUS_TURN_IN_THRESHOLD_REDUCTION_RIGHT) *
|
||||
threshold_scale = 1.0 - (_prius_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("toyota_prius.turn_in_threshold_reduction_left", PRIUS_TURN_IN_THRESHOLD_REDUCTION_LEFT),
|
||||
_ftm_vehicle_knob("toyota_prius.turn_in_threshold_reduction_right", PRIUS_TURN_IN_THRESHOLD_REDUCTION_RIGHT),
|
||||
) *
|
||||
transition_envelope * turn_in_weight)
|
||||
threshold_scale += (_prius_side_value(desired_lateral_accel, PRIUS_UNWIND_THRESHOLD_INCREASE_LEFT, PRIUS_UNWIND_THRESHOLD_INCREASE_RIGHT) *
|
||||
threshold_scale += (_prius_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("toyota_prius.unwind_threshold_increase_left", PRIUS_UNWIND_THRESHOLD_INCREASE_LEFT),
|
||||
_ftm_vehicle_knob("toyota_prius.unwind_threshold_increase_right", PRIUS_UNWIND_THRESHOLD_INCREASE_RIGHT),
|
||||
) *
|
||||
transition_envelope * unwind_weight)
|
||||
return base_threshold * min(max(threshold_scale, 0.86), 1.16)
|
||||
|
||||
@@ -801,7 +929,7 @@ def get_prius_friction_scale(v_ego: float, desired_lateral_accel: float, desired
|
||||
def get_prius_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float:
|
||||
speed_weight = _prius_sigmoid((v_ego - PRIUS_CENTER_TAPER_SPEED) / PRIUS_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _prius_sigmoid((PRIUS_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / PRIUS_CENTER_TAPER_LAT_WIDTH)
|
||||
reduction = PRIUS_CENTER_TAPER_MAX * speed_weight * center_weight
|
||||
reduction = _ftm_vehicle_knob("toyota_prius.center_taper_max", PRIUS_CENTER_TAPER_MAX) * speed_weight * center_weight
|
||||
return 1.0 - reduction
|
||||
|
||||
|
||||
@@ -1107,23 +1235,35 @@ def get_bolt_2022_2023_ff_scale(desired_lateral_accel: float, desired_lateral_je
|
||||
if desired_lateral_accel == 0.0:
|
||||
return 1.0
|
||||
|
||||
gain = _bolt_2022_2023_side_value(desired_lateral_accel, BOLT_2022_2023_FF_GAIN_LEFT, BOLT_2022_2023_FF_GAIN_RIGHT)
|
||||
gain = _bolt_2022_2023_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.ff_gain_left", BOLT_2022_2023_FF_GAIN_LEFT),
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.ff_gain_right", BOLT_2022_2023_FF_GAIN_RIGHT),
|
||||
)
|
||||
abs_lateral_accel = abs(desired_lateral_accel)
|
||||
onset = _bolt_2022_2023_sigmoid((abs_lateral_accel - BOLT_2022_2023_FF_ONSET) / BOLT_2022_2023_FF_ONSET_WIDTH)
|
||||
cutoff = _bolt_2022_2023_sigmoid((BOLT_2022_2023_FF_CUTOFF - abs_lateral_accel) / BOLT_2022_2023_FF_CUTOFF_WIDTH)
|
||||
extra_scale = gain * onset * cutoff
|
||||
speed_weight = _bolt_2022_2023_sigmoid((v_ego - BOLT_2022_2023_CENTER_TAPER_SPEED) / BOLT_2022_2023_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _bolt_2022_2023_sigmoid((BOLT_2022_2023_CENTER_TAPER_LAT - abs_lateral_accel) / BOLT_2022_2023_CENTER_TAPER_LAT_WIDTH)
|
||||
center_taper = 1.0 - (BOLT_2022_2023_CENTER_TAPER_MAX * speed_weight * center_weight)
|
||||
center_taper = 1.0 - (_ftm_vehicle_knob("gm_bolt_2022_2023.center_taper_max", BOLT_2022_2023_CENTER_TAPER_MAX) * speed_weight * center_weight)
|
||||
low_speed_factor = _bolt_2022_2023_low_speed_factor(v_ego)
|
||||
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)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
turn_in_boost = 1.0 + (_bolt_2022_2023_side_value(desired_lateral_accel, BOLT_2022_2023_TURN_IN_BOOST_LEFT, BOLT_2022_2023_TURN_IN_BOOST_RIGHT) *
|
||||
turn_in_boost = 1.0 + (_bolt_2022_2023_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.turn_in_boost_left", BOLT_2022_2023_TURN_IN_BOOST_LEFT),
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.turn_in_boost_right", BOLT_2022_2023_TURN_IN_BOOST_RIGHT),
|
||||
) *
|
||||
turn_in_weight * low_speed_factor)
|
||||
unwind_envelope = (0.25 + 0.75 * low_speed_factor) * (1.0 + 0.45 * transition_envelope)
|
||||
unwind_taper = 1.0 - (_bolt_2022_2023_side_value(desired_lateral_accel, BOLT_2022_2023_UNWIND_TAPER_LEFT, BOLT_2022_2023_UNWIND_TAPER_RIGHT) *
|
||||
unwind_taper = 1.0 - (_bolt_2022_2023_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.unwind_taper_left", BOLT_2022_2023_UNWIND_TAPER_LEFT),
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.unwind_taper_right", BOLT_2022_2023_UNWIND_TAPER_RIGHT),
|
||||
) *
|
||||
unwind_weight * unwind_envelope)
|
||||
return 1.0 + (extra_scale * center_taper * turn_in_boost * max(unwind_taper, 0.0))
|
||||
|
||||
@@ -1134,9 +1274,17 @@ def get_bolt_2022_2023_friction_threshold(v_ego: float, desired_lateral_accel: f
|
||||
phase = _bolt_2022_2023_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
threshold_scale = 1.0 - (_bolt_2022_2023_side_value(desired_lateral_accel, BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_LEFT, BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_RIGHT) *
|
||||
threshold_scale = 1.0 - (_bolt_2022_2023_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.turn_in_threshold_reduction_left", BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_LEFT),
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.turn_in_threshold_reduction_right", BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_RIGHT),
|
||||
) *
|
||||
transition_envelope * turn_in_weight)
|
||||
threshold_scale += (_bolt_2022_2023_side_value(desired_lateral_accel, BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_LEFT, BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_RIGHT) *
|
||||
threshold_scale += (_bolt_2022_2023_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.unwind_threshold_increase_left", BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_LEFT),
|
||||
_ftm_vehicle_knob("gm_bolt_2022_2023.unwind_threshold_increase_right", BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_RIGHT),
|
||||
) *
|
||||
transition_envelope * unwind_weight)
|
||||
return base_threshold * min(max(threshold_scale, 0.84), 1.14)
|
||||
|
||||
@@ -1802,14 +1950,18 @@ def _ioniq_6_transition_envelope(v_ego: float, desired_lateral_accel: float, des
|
||||
|
||||
|
||||
def _ioniq_6_curvy_speed_weight(v_ego: float) -> float:
|
||||
onset = _ioniq_6_sigmoid((max(v_ego, 0.0) - IONIQ_6_CURVY_SPEED_MIN) / IONIQ_6_CURVY_SPEED_MIN_WIDTH)
|
||||
cutoff = _ioniq_6_sigmoid((IONIQ_6_CURVY_SPEED_MAX - max(v_ego, 0.0)) / IONIQ_6_CURVY_SPEED_MAX_WIDTH)
|
||||
curvy_speed_min = _ftm_vehicle_knob("hyundai_ioniq_6.curvy_speed_min", IONIQ_6_CURVY_SPEED_MIN)
|
||||
curvy_speed_max = _ftm_vehicle_knob("hyundai_ioniq_6.curvy_speed_max", IONIQ_6_CURVY_SPEED_MAX)
|
||||
onset = _ioniq_6_sigmoid((max(v_ego, 0.0) - curvy_speed_min) / IONIQ_6_CURVY_SPEED_MIN_WIDTH)
|
||||
cutoff = _ioniq_6_sigmoid((curvy_speed_max - max(v_ego, 0.0)) / IONIQ_6_CURVY_SPEED_MAX_WIDTH)
|
||||
return onset * cutoff
|
||||
|
||||
|
||||
def _ioniq_6_curvy_turn_in_trim_speed_weight(v_ego: float) -> float:
|
||||
onset = _ioniq_6_sigmoid((max(v_ego, 0.0) - IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
cutoff = _ioniq_6_sigmoid((IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MAX - max(v_ego, 0.0)) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
curvy_turn_in_speed_min = _ftm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_speed_min", IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN)
|
||||
curvy_turn_in_speed_max = _ftm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_speed_max", IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MAX)
|
||||
onset = _ioniq_6_sigmoid((max(v_ego, 0.0) - curvy_turn_in_speed_min) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
cutoff = _ioniq_6_sigmoid((curvy_turn_in_speed_max - max(v_ego, 0.0)) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
return onset * cutoff
|
||||
|
||||
|
||||
@@ -1818,7 +1970,11 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
|
||||
if desired_lateral_accel == 0.0:
|
||||
return 1.0
|
||||
|
||||
gain = _ioniq_6_side_value(desired_lateral_accel, IONIQ_6_FF_GAIN_LEFT, IONIQ_6_FF_GAIN_RIGHT)
|
||||
gain = _ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.ff_gain_left", IONIQ_6_FF_GAIN_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.ff_gain_right", IONIQ_6_FF_GAIN_RIGHT),
|
||||
)
|
||||
abs_lateral_accel = abs(desired_lateral_accel)
|
||||
onset = _ioniq_6_sigmoid((abs_lateral_accel - IONIQ_6_FF_ONSET) / IONIQ_6_FF_ONSET_WIDTH)
|
||||
cutoff = _ioniq_6_sigmoid((IONIQ_6_FF_CUTOFF - abs_lateral_accel) / IONIQ_6_FF_CUTOFF_WIDTH)
|
||||
@@ -1827,9 +1983,17 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
low_speed_factor = _ioniq_6_low_speed_factor(v_ego)
|
||||
turn_in_boost = 1.0 + (_ioniq_6_side_value(desired_lateral_accel, IONIQ_6_TURN_IN_BOOST_LEFT, IONIQ_6_TURN_IN_BOOST_RIGHT) *
|
||||
turn_in_boost = 1.0 + (_ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.turn_in_boost_left", IONIQ_6_TURN_IN_BOOST_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.turn_in_boost_right", IONIQ_6_TURN_IN_BOOST_RIGHT),
|
||||
) *
|
||||
turn_in_weight * low_speed_factor)
|
||||
unwind_taper = 1.0 - (_ioniq_6_side_value(desired_lateral_accel, IONIQ_6_UNWIND_TAPER_LEFT, IONIQ_6_UNWIND_TAPER_RIGHT) *
|
||||
unwind_taper = 1.0 - (_ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.unwind_taper_left", IONIQ_6_UNWIND_TAPER_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.unwind_taper_right", IONIQ_6_UNWIND_TAPER_RIGHT),
|
||||
) *
|
||||
unwind_weight * (0.30 + 0.70 * low_speed_factor))
|
||||
crawl_turn_in_scale = 0.0
|
||||
if desired_lateral_accel * desired_lateral_jerk > 0.0:
|
||||
@@ -1837,8 +2001,11 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
|
||||
IONIQ_6_CRAWL_TURN_IN_FF_SPEED_WIDTH)
|
||||
crawl_lat_weight = _ioniq_6_sigmoid((abs_lateral_accel - IONIQ_6_CRAWL_TURN_IN_FF_LAT) /
|
||||
IONIQ_6_CRAWL_TURN_IN_FF_LAT_WIDTH)
|
||||
crawl_turn_in_scale = _ioniq_6_side_value(desired_lateral_accel, IONIQ_6_CRAWL_TURN_IN_FF_BOOST_LEFT,
|
||||
IONIQ_6_CRAWL_TURN_IN_FF_BOOST_RIGHT) * crawl_speed_weight * crawl_lat_weight
|
||||
crawl_turn_in_scale = _ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.crawl_turn_in_ff_boost_left", IONIQ_6_CRAWL_TURN_IN_FF_BOOST_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.crawl_turn_in_ff_boost_right", IONIQ_6_CRAWL_TURN_IN_FF_BOOST_RIGHT),
|
||||
) * crawl_speed_weight * crawl_lat_weight
|
||||
high_speed_right_turn_in_scale = 0.0
|
||||
if desired_lateral_accel < 0.0 and desired_lateral_accel * desired_lateral_jerk > 0.0:
|
||||
high_speed_weight = _ioniq_6_sigmoid((max(v_ego, 0.0) - IONIQ_6_HIGH_SPEED_RIGHT_TURN_IN_FF_SPEED) /
|
||||
@@ -1861,9 +2028,17 @@ def get_ioniq_6_friction_threshold(v_ego: float, desired_lateral_accel: float =
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
unwind_speed_weight = _ioniq_6_sigmoid((v_ego - IONIQ_6_UNWIND_HIGH_SPEED_SPEED) / IONIQ_6_UNWIND_HIGH_SPEED_SPEED_WIDTH)
|
||||
threshold_scale = 1.0 - (_ioniq_6_side_value(desired_lateral_accel, IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_LEFT, IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_RIGHT) *
|
||||
threshold_scale = 1.0 - (_ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.turn_in_threshold_reduction_left", IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.turn_in_threshold_reduction_right", IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_RIGHT),
|
||||
) *
|
||||
transition_envelope * turn_in_weight)
|
||||
threshold_scale += (_ioniq_6_side_value(desired_lateral_accel, IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT, IONIQ_6_UNWIND_THRESHOLD_INCREASE_RIGHT) *
|
||||
threshold_scale += (_ioniq_6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.unwind_threshold_increase_left", IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.unwind_threshold_increase_right", IONIQ_6_UNWIND_THRESHOLD_INCREASE_RIGHT),
|
||||
) *
|
||||
transition_envelope * unwind_weight * unwind_speed_weight)
|
||||
return base_threshold * min(max(threshold_scale, 0.82), 1.18)
|
||||
|
||||
@@ -1891,12 +2066,12 @@ def get_ioniq_6_friction_center_fade_scale(desired_lateral_accel: float, v_ego:
|
||||
def get_ioniq_6_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float:
|
||||
speed_weight = _ioniq_6_sigmoid((v_ego - IONIQ_6_CENTER_TAPER_SPEED) / IONIQ_6_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _ioniq_6_sigmoid((IONIQ_6_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / IONIQ_6_CENTER_TAPER_LAT_WIDTH)
|
||||
high_speed_reduction = IONIQ_6_CENTER_TAPER_MAX * speed_weight * center_weight
|
||||
high_speed_reduction = _ftm_vehicle_knob("hyundai_ioniq_6.center_taper_max", IONIQ_6_CENTER_TAPER_MAX) * speed_weight * center_weight
|
||||
|
||||
highway_speed_weight = _ioniq_6_sigmoid((v_ego - IONIQ_6_HIGHWAY_CENTER_TAPER_SPEED) / IONIQ_6_HIGHWAY_CENTER_TAPER_SPEED_WIDTH)
|
||||
highway_center_weight = _ioniq_6_sigmoid((IONIQ_6_HIGHWAY_CENTER_TAPER_LAT - abs(desired_lateral_accel)) /
|
||||
IONIQ_6_HIGHWAY_CENTER_TAPER_LAT_WIDTH)
|
||||
highway_center_reduction = IONIQ_6_HIGHWAY_CENTER_TAPER_MAX * highway_speed_weight * highway_center_weight
|
||||
highway_center_reduction = _ftm_vehicle_knob("hyundai_ioniq_6.highway_center_taper_max", IONIQ_6_HIGHWAY_CENTER_TAPER_MAX) * highway_speed_weight * highway_center_weight
|
||||
|
||||
low_mid_onset = _ioniq_6_sigmoid((v_ego - IONIQ_6_LOW_MID_CENTER_TAPER_SPEED_MIN) / IONIQ_6_LOW_MID_CENTER_TAPER_SPEED_WIDTH)
|
||||
low_mid_cutoff = _ioniq_6_sigmoid((IONIQ_6_LOW_MID_CENTER_TAPER_SPEED_MAX - v_ego) / IONIQ_6_LOW_MID_CENTER_TAPER_SPEED_WIDTH)
|
||||
@@ -1944,8 +2119,8 @@ def get_ioniq_6_directional_taper_scale(desired_lateral_accel: float, desired_la
|
||||
reduction = band_weight * (base_reduction + unwind_reduction * unwind_weight)
|
||||
reduction += heavy_band_weight * (heavy_base_reduction + heavy_unwind_reduction * unwind_weight)
|
||||
reduction += (_ioniq_6_side_value(desired_lateral_accel,
|
||||
IONIQ_6_CURVY_TURN_IN_TRIM_LEFT,
|
||||
IONIQ_6_CURVY_TURN_IN_TRIM_RIGHT) *
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_left", IONIQ_6_CURVY_TURN_IN_TRIM_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_right", IONIQ_6_CURVY_TURN_IN_TRIM_RIGHT)) *
|
||||
curvy_turn_in_trim_weight)
|
||||
curvy_unwind_weight = 0.0
|
||||
curvy_unwind_floor_relief = 0.0
|
||||
@@ -1957,12 +2132,12 @@ def get_ioniq_6_directional_taper_scale(desired_lateral_accel: float, desired_la
|
||||
IONIQ_6_CURVY_UNWIND_LAT_CUTOFF_WIDTH)
|
||||
curvy_unwind_weight = curvy_unwind_speed_weight * curvy_unwind_lat_onset * curvy_unwind_lat_cutoff * unwind_weight
|
||||
curvy_unwind_floor_relief = (_ioniq_6_side_value(desired_lateral_accel,
|
||||
IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_LEFT,
|
||||
IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_RIGHT) *
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_floor_relief_left", IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_floor_relief_right", IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_RIGHT)) *
|
||||
curvy_unwind_weight)
|
||||
reduction += (_ioniq_6_side_value(desired_lateral_accel,
|
||||
IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_LEFT,
|
||||
IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_RIGHT) *
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_extra_reduction_left", IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_extra_reduction_right", IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_RIGHT)) *
|
||||
curvy_unwind_weight)
|
||||
floor = _ioniq_6_side_value(desired_lateral_accel, IONIQ_6_DIRECTIONAL_TAPER_FLOOR_LEFT, IONIQ_6_DIRECTIONAL_TAPER_FLOOR_RIGHT)
|
||||
floor -= _ioniq_6_side_value(desired_lateral_accel, IONIQ_6_DIRECTIONAL_TAPER_UNWIND_FLOOR_LEFT, IONIQ_6_DIRECTIONAL_TAPER_UNWIND_FLOOR_RIGHT) * unwind_weight
|
||||
@@ -2011,7 +2186,11 @@ def get_ioniq_6_low_speed_angle_assist_torque(desired_angle_deg: float, actual_a
|
||||
tracking_taper = _ioniq_6_sigmoid((tracking_ratio - IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_START) /
|
||||
IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_WIDTH)
|
||||
tracking_scale = max(1.0 - tracking_taper, IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_FLOOR)
|
||||
assist_torque = math.copysign(IONIQ_6_LOW_SPEED_ANGLE_ASSIST_MAX_TORQUE * speed_weight * error_weight * desired_angle_weight * tracking_scale, -angle_error)
|
||||
assist_torque = math.copysign(
|
||||
_ftm_vehicle_knob("hyundai_ioniq_6.low_speed_angle_assist_max_torque", IONIQ_6_LOW_SPEED_ANGLE_ASSIST_MAX_TORQUE) *
|
||||
speed_weight * error_weight * desired_angle_weight * tracking_scale,
|
||||
-angle_error,
|
||||
)
|
||||
if abs(assist_torque) < 1e-4:
|
||||
return current_output_torque
|
||||
|
||||
@@ -2069,7 +2248,11 @@ def get_kia_ev6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
|
||||
if desired_lateral_accel == 0.0:
|
||||
return 1.0
|
||||
|
||||
gain = _kia_ev6_side_value(desired_lateral_accel, KIA_EV6_FF_GAIN_LEFT, KIA_EV6_FF_GAIN_RIGHT)
|
||||
gain = _kia_ev6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.ff_gain_left", KIA_EV6_FF_GAIN_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.ff_gain_right", KIA_EV6_FF_GAIN_RIGHT),
|
||||
)
|
||||
abs_lateral_accel = abs(desired_lateral_accel)
|
||||
onset = _kia_ev6_sigmoid((abs_lateral_accel - KIA_EV6_FF_ONSET) / KIA_EV6_FF_ONSET_WIDTH)
|
||||
cutoff = _kia_ev6_sigmoid((KIA_EV6_FF_CUTOFF - abs_lateral_accel) / KIA_EV6_FF_CUTOFF_WIDTH)
|
||||
@@ -2078,9 +2261,17 @@ def get_kia_ev6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
low_speed_factor = _kia_ev6_low_speed_factor(v_ego)
|
||||
turn_in_boost = 1.0 + (_kia_ev6_side_value(desired_lateral_accel, KIA_EV6_TURN_IN_BOOST_LEFT, KIA_EV6_TURN_IN_BOOST_RIGHT) *
|
||||
turn_in_boost = 1.0 + (_kia_ev6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.turn_in_boost_left", KIA_EV6_TURN_IN_BOOST_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.turn_in_boost_right", KIA_EV6_TURN_IN_BOOST_RIGHT),
|
||||
) *
|
||||
turn_in_weight * (0.35 + 0.65 * low_speed_factor))
|
||||
unwind_taper = 1.0 - (_kia_ev6_side_value(desired_lateral_accel, KIA_EV6_UNWIND_TAPER_LEFT, KIA_EV6_UNWIND_TAPER_RIGHT) *
|
||||
unwind_taper = 1.0 - (_kia_ev6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.unwind_taper_left", KIA_EV6_UNWIND_TAPER_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.unwind_taper_right", KIA_EV6_UNWIND_TAPER_RIGHT),
|
||||
) *
|
||||
unwind_weight * (0.35 + 0.65 * low_speed_factor))
|
||||
return 1.0 + (extra_scale * turn_in_boost * max(unwind_taper, 0.0))
|
||||
|
||||
@@ -2091,9 +2282,17 @@ def get_kia_ev6_friction_threshold(v_ego: float, desired_lateral_accel: float =
|
||||
phase = _kia_ev6_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
threshold_scale = 1.0 - (_kia_ev6_side_value(desired_lateral_accel, KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_LEFT, KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_RIGHT) *
|
||||
threshold_scale = 1.0 - (_kia_ev6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.turn_in_threshold_reduction_left", KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.turn_in_threshold_reduction_right", KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_RIGHT),
|
||||
) *
|
||||
transition_envelope * turn_in_weight)
|
||||
threshold_scale += (_kia_ev6_side_value(desired_lateral_accel, KIA_EV6_UNWIND_THRESHOLD_INCREASE_LEFT, KIA_EV6_UNWIND_THRESHOLD_INCREASE_RIGHT) *
|
||||
threshold_scale += (_kia_ev6_side_value(
|
||||
desired_lateral_accel,
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.unwind_threshold_increase_left", KIA_EV6_UNWIND_THRESHOLD_INCREASE_LEFT),
|
||||
_ftm_vehicle_knob("hyundai_kia_ev6.unwind_threshold_increase_right", KIA_EV6_UNWIND_THRESHOLD_INCREASE_RIGHT),
|
||||
) *
|
||||
transition_envelope * unwind_weight)
|
||||
return base_threshold * min(max(threshold_scale, 0.82), 1.16)
|
||||
|
||||
@@ -2114,7 +2313,7 @@ def get_kia_ev6_friction_scale(v_ego: float, desired_lateral_accel: float, desir
|
||||
def get_kia_ev6_center_taper_scale(desired_lateral_accel: float, v_ego: float) -> float:
|
||||
speed_weight = _kia_ev6_sigmoid((v_ego - KIA_EV6_CENTER_TAPER_SPEED) / KIA_EV6_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _kia_ev6_sigmoid((KIA_EV6_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / KIA_EV6_CENTER_TAPER_LAT_WIDTH)
|
||||
reduction = KIA_EV6_CENTER_TAPER_MAX * speed_weight * center_weight
|
||||
reduction = _ftm_vehicle_knob("hyundai_kia_ev6.center_taper_max", KIA_EV6_CENTER_TAPER_MAX) * speed_weight * center_weight
|
||||
return 1.0 - reduction
|
||||
|
||||
|
||||
@@ -2159,6 +2358,388 @@ def get_volt_plexy_center_taper_scale(desired_lateral_accel: float, v_ego: float
|
||||
return 1.0 - (standard_reduction * VOLT_PLEXY_CENTER_TAPER_REDUCTION_MULT)
|
||||
|
||||
|
||||
FTM_UNIVERSAL_PROFILE_KEY = "torque_universal"
|
||||
|
||||
FTM_FULL_SURFACE_SUFFIX_METADATA = {
|
||||
"ff_gain_left": {"min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"ff_gain_right": {"min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"turn_in_boost_left": {"min": -0.10, "max": 2.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"turn_in_boost_right": {"min": -0.10, "max": 2.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"unwind_taper_left": {"min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"unwind_taper_right": {"min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"center_taper_max": {"min": 0.0, "max": 0.18, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"highway_center_taper_max": {"min": 0.0, "max": 0.18, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"turn_in_threshold_reduction_left": {"min": 0.0, "max": 2.00, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"turn_in_threshold_reduction_right": {"min": 0.0, "max": 2.00, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"unwind_threshold_increase_left": {"min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"unwind_threshold_increase_right": {"min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"crawl_turn_in_ff_boost_left": {"min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"crawl_turn_in_ff_boost_right": {"min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"low_speed_angle_assist_max_torque": {"min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_speed_min": {"min": 4.0, "max": 12.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_speed_max": {"min": 14.0, "max": 25.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_turn_in_trim_speed_min": {"min": 8.0, "max": 16.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_turn_in_trim_speed_max": {"min": 14.0, "max": 25.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_turn_in_trim_left": {"min": 0.0, "max": 0.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_turn_in_trim_right": {"min": 0.0, "max": 0.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_unwind_floor_relief_left": {"min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_unwind_floor_relief_right": {"min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_unwind_extra_reduction_left": {"min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
"curvy_unwind_extra_reduction_right": {"min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
}
|
||||
|
||||
FTM_FULL_SURFACE_NEUTRAL_DEFAULTS = {
|
||||
"ff_gain_left": 0.0,
|
||||
"ff_gain_right": 0.0,
|
||||
"turn_in_boost_left": 0.0,
|
||||
"turn_in_boost_right": 0.0,
|
||||
"unwind_taper_left": 0.0,
|
||||
"unwind_taper_right": 0.0,
|
||||
"center_taper_max": 0.0,
|
||||
"highway_center_taper_max": 0.0,
|
||||
"turn_in_threshold_reduction_left": 0.0,
|
||||
"turn_in_threshold_reduction_right": 0.0,
|
||||
"unwind_threshold_increase_left": 0.0,
|
||||
"unwind_threshold_increase_right": 0.0,
|
||||
"crawl_turn_in_ff_boost_left": 0.0,
|
||||
"crawl_turn_in_ff_boost_right": 0.0,
|
||||
"low_speed_angle_assist_max_torque": 0.0,
|
||||
"curvy_speed_min": IONIQ_6_CURVY_SPEED_MIN,
|
||||
"curvy_speed_max": IONIQ_6_CURVY_SPEED_MAX,
|
||||
"curvy_turn_in_trim_speed_min": IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN,
|
||||
"curvy_turn_in_trim_speed_max": IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MAX,
|
||||
"curvy_turn_in_trim_left": 0.0,
|
||||
"curvy_turn_in_trim_right": 0.0,
|
||||
"curvy_unwind_floor_relief_left": 0.0,
|
||||
"curvy_unwind_floor_relief_right": 0.0,
|
||||
"curvy_unwind_extra_reduction_left": 0.0,
|
||||
"curvy_unwind_extra_reduction_right": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def _ftm_profile_symbol(profile_key: str, suffix: str) -> str:
|
||||
return f"{profile_key}.{suffix}"
|
||||
|
||||
|
||||
def ftm_profile_supports_knob(profile_key: str | None, suffix: str) -> bool:
|
||||
if not profile_key:
|
||||
return False
|
||||
return _ftm_profile_symbol(profile_key, suffix) in FTM_SUPPORTED_VEHICLE_KNOBS
|
||||
|
||||
|
||||
def _ftm_full_surface_side_value(profile_key: str, desired_lateral_accel: float,
|
||||
suffix: str, left_default: float = 0.0, right_default: float = 0.0) -> float:
|
||||
if desired_lateral_accel >= 0.0:
|
||||
return _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, f"{suffix}_left"), left_default)
|
||||
return _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, f"{suffix}_right"), right_default)
|
||||
|
||||
|
||||
def _ftm_full_surface_low_speed_factor(v_ego: float) -> float:
|
||||
return 1.0 / (1.0 + (max(v_ego, 0.0) / IONIQ_6_TRANSITION_SPEED) ** 2)
|
||||
|
||||
|
||||
def _ftm_full_surface_transition_phase(desired_lateral_accel: float, desired_lateral_jerk: float) -> float:
|
||||
return math.tanh((desired_lateral_accel * desired_lateral_jerk) / IONIQ_6_PHASE_SCALE)
|
||||
|
||||
|
||||
def _ftm_full_surface_transition_envelope(v_ego: float, desired_lateral_accel: float, desired_lateral_jerk: float) -> float:
|
||||
lat_factor = 1.0 - math.exp(-abs(desired_lateral_accel) / IONIQ_6_FRICTION_LAT_RISE)
|
||||
jerk_factor = 1.0 - math.exp(-abs(desired_lateral_jerk) / IONIQ_6_FRICTION_JERK_RISE)
|
||||
return _ftm_full_surface_low_speed_factor(v_ego) * lat_factor * jerk_factor
|
||||
|
||||
|
||||
def _ftm_full_surface_curvy_speed_weight(profile_key: str, v_ego: float) -> float:
|
||||
curvy_speed_min = _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "curvy_speed_min"), IONIQ_6_CURVY_SPEED_MIN)
|
||||
curvy_speed_max = _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "curvy_speed_max"), IONIQ_6_CURVY_SPEED_MAX)
|
||||
onset = _sigmoid((max(v_ego, 0.0) - curvy_speed_min) / IONIQ_6_CURVY_SPEED_MIN_WIDTH)
|
||||
cutoff = _sigmoid((curvy_speed_max - max(v_ego, 0.0)) / IONIQ_6_CURVY_SPEED_MAX_WIDTH)
|
||||
return onset * cutoff
|
||||
|
||||
|
||||
def _ftm_full_surface_curvy_turn_in_trim_speed_weight(profile_key: str, v_ego: float) -> float:
|
||||
curvy_turn_in_speed_min = _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "curvy_turn_in_trim_speed_min"), IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN)
|
||||
curvy_turn_in_speed_max = _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "curvy_turn_in_trim_speed_max"), IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MAX)
|
||||
onset = _sigmoid((max(v_ego, 0.0) - curvy_turn_in_speed_min) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
cutoff = _sigmoid((curvy_turn_in_speed_max - max(v_ego, 0.0)) / IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_WIDTH)
|
||||
return onset * cutoff
|
||||
|
||||
|
||||
def get_ftm_full_surface_center_taper_scale(profile_key: str | None, desired_lateral_accel: float, v_ego: float,
|
||||
include_base_center: bool = False) -> float:
|
||||
if not profile_key:
|
||||
return 1.0
|
||||
|
||||
reduction = 0.0
|
||||
if include_base_center and ftm_profile_supports_knob(profile_key, "center_taper_max"):
|
||||
speed_weight = _sigmoid((v_ego - IONIQ_6_CENTER_TAPER_SPEED) / IONIQ_6_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _sigmoid((IONIQ_6_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / IONIQ_6_CENTER_TAPER_LAT_WIDTH)
|
||||
reduction += _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "center_taper_max"), 0.0) * speed_weight * center_weight
|
||||
|
||||
if ftm_profile_supports_knob(profile_key, "highway_center_taper_max"):
|
||||
speed_weight = _sigmoid((v_ego - IONIQ_6_HIGHWAY_CENTER_TAPER_SPEED) / IONIQ_6_HIGHWAY_CENTER_TAPER_SPEED_WIDTH)
|
||||
center_weight = _sigmoid((IONIQ_6_HIGHWAY_CENTER_TAPER_LAT - abs(desired_lateral_accel)) / IONIQ_6_HIGHWAY_CENTER_TAPER_LAT_WIDTH)
|
||||
reduction += _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "highway_center_taper_max"), 0.0) * speed_weight * center_weight
|
||||
|
||||
return 1.0 - min(reduction, 0.20)
|
||||
|
||||
|
||||
def get_ftm_full_surface_ff_scale(profile_key: str | None, desired_lateral_accel: float, desired_lateral_jerk: float, v_ego: float,
|
||||
include_base_ff: bool = False) -> float:
|
||||
if not profile_key or desired_lateral_accel == 0.0:
|
||||
return 1.0
|
||||
|
||||
abs_lateral_accel = abs(desired_lateral_accel)
|
||||
phase = _ftm_full_surface_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
low_speed_factor = _ftm_full_surface_low_speed_factor(v_ego)
|
||||
|
||||
curvy_turn_in_speed_weight = _ftm_full_surface_curvy_turn_in_trim_speed_weight(profile_key, v_ego)
|
||||
curvy_turn_in_lat_onset = _sigmoid((abs_lateral_accel - IONIQ_6_CURVY_TURN_IN_TRIM_LAT_START) / IONIQ_6_CURVY_TURN_IN_TRIM_LAT_ONSET_WIDTH)
|
||||
curvy_turn_in_lat_cutoff = _sigmoid((IONIQ_6_CURVY_TURN_IN_TRIM_LAT_END - abs_lateral_accel) / IONIQ_6_CURVY_TURN_IN_TRIM_LAT_CUTOFF_WIDTH)
|
||||
curvy_turn_in_trim_weight = curvy_turn_in_speed_weight * curvy_turn_in_lat_onset * curvy_turn_in_lat_cutoff * turn_in_weight
|
||||
|
||||
curvy_unwind_speed_weight = _ftm_full_surface_curvy_speed_weight(profile_key, v_ego)
|
||||
curvy_unwind_lat_onset = _sigmoid((abs_lateral_accel - IONIQ_6_CURVY_UNWIND_LAT_START) / IONIQ_6_CURVY_UNWIND_LAT_ONSET_WIDTH)
|
||||
curvy_unwind_lat_cutoff = _sigmoid((IONIQ_6_CURVY_UNWIND_LAT_END - abs_lateral_accel) / IONIQ_6_CURVY_UNWIND_LAT_CUTOFF_WIDTH)
|
||||
curvy_unwind_weight = curvy_unwind_speed_weight * curvy_unwind_lat_onset * curvy_unwind_lat_cutoff * unwind_weight
|
||||
|
||||
scale = 1.0
|
||||
if include_base_ff and ftm_profile_supports_knob(profile_key, "ff_gain_left"):
|
||||
gain = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "ff_gain")
|
||||
onset = _sigmoid((abs_lateral_accel - IONIQ_6_FF_ONSET) / IONIQ_6_FF_ONSET_WIDTH)
|
||||
cutoff = _sigmoid((IONIQ_6_FF_CUTOFF - abs_lateral_accel) / IONIQ_6_FF_CUTOFF_WIDTH)
|
||||
extra_scale = gain * onset * cutoff
|
||||
turn_in_boost = 1.0 + (_ftm_full_surface_side_value(profile_key, desired_lateral_accel, "turn_in_boost") * turn_in_weight * low_speed_factor)
|
||||
unwind_reduction = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "unwind_taper") * unwind_weight * (0.30 + 0.70 * low_speed_factor)
|
||||
curvy_unwind_extra = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_extra_reduction") * curvy_unwind_weight
|
||||
curvy_unwind_floor_relief = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_floor_relief") * curvy_unwind_weight
|
||||
unwind_floor = 1.0 - (0.55 * unwind_reduction) - curvy_unwind_floor_relief
|
||||
unwind_scale = max(1.0 - unwind_reduction - curvy_unwind_extra, unwind_floor, 0.0)
|
||||
scale *= 1.0 + (extra_scale * turn_in_boost * unwind_scale)
|
||||
else:
|
||||
curvy_unwind_extra = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_extra_reduction") * curvy_unwind_weight
|
||||
curvy_unwind_floor_relief = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_floor_relief") * curvy_unwind_weight
|
||||
scale *= max(1.0 - curvy_unwind_extra - curvy_unwind_floor_relief, 0.55)
|
||||
|
||||
if ftm_profile_supports_knob(profile_key, "curvy_turn_in_trim_left"):
|
||||
curvy_trim = _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_turn_in_trim") * curvy_turn_in_trim_weight
|
||||
scale *= max(1.0 - curvy_trim, 0.55)
|
||||
|
||||
if ftm_profile_supports_knob(profile_key, "crawl_turn_in_ff_boost_left") and desired_lateral_accel * desired_lateral_jerk > 0.0:
|
||||
crawl_speed_weight = _sigmoid((IONIQ_6_CRAWL_TURN_IN_FF_SPEED - max(v_ego, 0.0)) / IONIQ_6_CRAWL_TURN_IN_FF_SPEED_WIDTH)
|
||||
crawl_lat_weight = _sigmoid((abs_lateral_accel - IONIQ_6_CRAWL_TURN_IN_FF_LAT) / IONIQ_6_CRAWL_TURN_IN_FF_LAT_WIDTH)
|
||||
scale += _ftm_full_surface_side_value(profile_key, desired_lateral_accel, "crawl_turn_in_ff_boost") * crawl_speed_weight * crawl_lat_weight
|
||||
|
||||
return scale
|
||||
|
||||
|
||||
def get_ftm_full_surface_friction_threshold(profile_key: str | None, base_threshold: float, v_ego: float,
|
||||
desired_lateral_accel: float = 0.0, desired_lateral_jerk: float = 0.0,
|
||||
include_base_threshold: bool = False) -> float:
|
||||
if not profile_key or not include_base_threshold:
|
||||
return base_threshold
|
||||
|
||||
transition_envelope = _ftm_full_surface_transition_envelope(v_ego, desired_lateral_accel, desired_lateral_jerk)
|
||||
phase = _ftm_full_surface_transition_phase(desired_lateral_accel, desired_lateral_jerk)
|
||||
turn_in_weight = max(phase, 0.0)
|
||||
unwind_weight = max(-phase, 0.0)
|
||||
unwind_speed_weight = _sigmoid((v_ego - IONIQ_6_UNWIND_HIGH_SPEED_SPEED) / IONIQ_6_UNWIND_HIGH_SPEED_SPEED_WIDTH)
|
||||
threshold_scale = 1.0
|
||||
threshold_scale -= (_ftm_full_surface_side_value(profile_key, desired_lateral_accel, "turn_in_threshold_reduction") *
|
||||
transition_envelope * turn_in_weight)
|
||||
threshold_scale += (_ftm_full_surface_side_value(profile_key, desired_lateral_accel, "unwind_threshold_increase") *
|
||||
transition_envelope * unwind_weight * unwind_speed_weight)
|
||||
return base_threshold * min(max(threshold_scale, 0.82), 1.18)
|
||||
|
||||
|
||||
def get_ftm_full_surface_low_speed_angle_assist_torque(profile_key: str | None, desired_angle_deg: float, actual_angle_deg: float,
|
||||
current_output_torque: float, v_ego: float) -> float:
|
||||
if not profile_key or not ftm_profile_supports_knob(profile_key, "low_speed_angle_assist_max_torque"):
|
||||
return current_output_torque
|
||||
|
||||
max_torque = _ftm_vehicle_knob(_ftm_profile_symbol(profile_key, "low_speed_angle_assist_max_torque"), 0.0)
|
||||
if max_torque <= 1e-4:
|
||||
return current_output_torque
|
||||
|
||||
angle_error = desired_angle_deg - actual_angle_deg
|
||||
if desired_angle_deg * angle_error > 0.0:
|
||||
speed_weight = _sigmoid((IONIQ_6_LOW_SPEED_ANGLE_ASSIST_SPEED - max(v_ego, 0.0)) / IONIQ_6_LOW_SPEED_ANGLE_ASSIST_SPEED_WIDTH)
|
||||
error_weight = _sigmoid((abs(angle_error) - IONIQ_6_LOW_SPEED_ANGLE_ASSIST_ERROR) / IONIQ_6_LOW_SPEED_ANGLE_ASSIST_ERROR_WIDTH)
|
||||
desired_angle_weight = _sigmoid((abs(desired_angle_deg) - IONIQ_6_LOW_SPEED_ANGLE_ASSIST_DESIRED_ANGLE) / IONIQ_6_LOW_SPEED_ANGLE_ASSIST_DESIRED_ANGLE_WIDTH)
|
||||
tracking_ratio = abs(actual_angle_deg) / max(abs(desired_angle_deg), 1e-3)
|
||||
tracking_taper = _sigmoid((tracking_ratio - IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_START) / IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_WIDTH)
|
||||
tracking_scale = max(1.0 - tracking_taper, IONIQ_6_LOW_SPEED_ANGLE_ASSIST_TRACK_RATIO_FLOOR)
|
||||
assist_torque = math.copysign(max_torque * speed_weight * error_weight * desired_angle_weight * tracking_scale, -angle_error)
|
||||
if abs(assist_torque) < 1e-4:
|
||||
return current_output_torque
|
||||
if current_output_torque * assist_torque >= 0.0:
|
||||
add_scale = float(np.interp(abs(current_output_torque), IONIQ_6_LOW_SPEED_ANGLE_ASSIST_ADD_BP, IONIQ_6_LOW_SPEED_ANGLE_ASSIST_ADD_V))
|
||||
return float(np.clip(current_output_torque + (assist_torque * add_scale), -1.0, 1.0))
|
||||
return float(np.clip(current_output_torque + assist_torque, -1.0, 1.0))
|
||||
|
||||
speed_weight = _sigmoid((IONIQ_6_LOW_SPEED_UNWIND_ASSIST_SPEED - max(v_ego, 0.0)) / IONIQ_6_LOW_SPEED_UNWIND_ASSIST_SPEED_WIDTH)
|
||||
error_weight = _sigmoid((abs(angle_error) - IONIQ_6_LOW_SPEED_UNWIND_ASSIST_ERROR) / IONIQ_6_LOW_SPEED_UNWIND_ASSIST_ERROR_WIDTH)
|
||||
actual_angle_weight = _sigmoid((abs(actual_angle_deg) - IONIQ_6_LOW_SPEED_UNWIND_ASSIST_ACTUAL_ANGLE) / IONIQ_6_LOW_SPEED_UNWIND_ASSIST_ACTUAL_ANGLE_WIDTH)
|
||||
assist_torque = math.copysign(IONIQ_6_LOW_SPEED_UNWIND_ASSIST_MAX_TORQUE * speed_weight * error_weight * actual_angle_weight, -angle_error)
|
||||
if abs(assist_torque) < 1e-4:
|
||||
return current_output_torque
|
||||
if current_output_torque * assist_torque >= 0.0:
|
||||
assist_torque *= IONIQ_6_LOW_SPEED_UNWIND_ASSIST_BLEND
|
||||
return float(np.clip(current_output_torque + assist_torque, -1.0, 1.0))
|
||||
|
||||
|
||||
FTM_RICH_PROFILE_CARS = {
|
||||
"gm_bolt_2022_2023": set(BOLT_2022_2023_CARS),
|
||||
"hyundai_ioniq_6": set(IONIQ_6_CARS),
|
||||
"hyundai_kia_ev6": set(KIA_EV6_CARS),
|
||||
"toyota_prius": set(PRIUS_CARS),
|
||||
}
|
||||
|
||||
FTM_RICH_PROFILE_LABELS = {
|
||||
"gm_bolt_2022_2023": "Bolt 2022-2023",
|
||||
"hyundai_ioniq_6": "Ioniq 6",
|
||||
"hyundai_kia_ev6": "EV6",
|
||||
"toyota_prius": "Prius",
|
||||
FTM_UNIVERSAL_PROFILE_KEY: "Torque Controller",
|
||||
}
|
||||
|
||||
FTM_SUPPORTED_VEHICLE_KNOBS = {
|
||||
"gm_bolt_2022_2023.ff_gain_left": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_FF_GAIN_LEFT},
|
||||
"gm_bolt_2022_2023.ff_gain_right": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_FF_GAIN_RIGHT},
|
||||
"gm_bolt_2022_2023.turn_in_boost_left": {"profile": "gm_bolt_2022_2023", "min": -0.10, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_TURN_IN_BOOST_LEFT},
|
||||
"gm_bolt_2022_2023.turn_in_boost_right": {"profile": "gm_bolt_2022_2023", "min": -0.10, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_TURN_IN_BOOST_RIGHT},
|
||||
"gm_bolt_2022_2023.unwind_taper_left": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_UNWIND_TAPER_LEFT},
|
||||
"gm_bolt_2022_2023.unwind_taper_right": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_UNWIND_TAPER_RIGHT},
|
||||
"gm_bolt_2022_2023.center_taper_max": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.25, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_CENTER_TAPER_MAX},
|
||||
"gm_bolt_2022_2023.turn_in_threshold_reduction_left": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_LEFT},
|
||||
"gm_bolt_2022_2023.turn_in_threshold_reduction_right": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_RIGHT},
|
||||
"gm_bolt_2022_2023.unwind_threshold_increase_left": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_LEFT},
|
||||
"gm_bolt_2022_2023.unwind_threshold_increase_right": {"profile": "gm_bolt_2022_2023", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_RIGHT},
|
||||
"hyundai_ioniq_6.ff_gain_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_FF_GAIN_LEFT},
|
||||
"hyundai_ioniq_6.ff_gain_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_FF_GAIN_RIGHT},
|
||||
"hyundai_ioniq_6.turn_in_boost_left": {"profile": "hyundai_ioniq_6", "min": 0.40, "max": 2.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_TURN_IN_BOOST_LEFT},
|
||||
"hyundai_ioniq_6.turn_in_boost_right": {"profile": "hyundai_ioniq_6", "min": 0.40, "max": 2.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_TURN_IN_BOOST_RIGHT},
|
||||
"hyundai_ioniq_6.unwind_taper_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_UNWIND_TAPER_LEFT},
|
||||
"hyundai_ioniq_6.unwind_taper_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_UNWIND_TAPER_RIGHT},
|
||||
"hyundai_ioniq_6.center_taper_max": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.18, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CENTER_TAPER_MAX},
|
||||
"hyundai_ioniq_6.highway_center_taper_max": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.18, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_HIGHWAY_CENTER_TAPER_MAX},
|
||||
"hyundai_ioniq_6.turn_in_threshold_reduction_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 2.00, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_LEFT},
|
||||
"hyundai_ioniq_6.turn_in_threshold_reduction_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 2.00, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_RIGHT},
|
||||
"hyundai_ioniq_6.unwind_threshold_increase_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT},
|
||||
"hyundai_ioniq_6.unwind_threshold_increase_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 12.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_UNWIND_THRESHOLD_INCREASE_RIGHT},
|
||||
"hyundai_ioniq_6.crawl_turn_in_ff_boost_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CRAWL_TURN_IN_FF_BOOST_LEFT},
|
||||
"hyundai_ioniq_6.crawl_turn_in_ff_boost_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CRAWL_TURN_IN_FF_BOOST_RIGHT},
|
||||
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_LOW_SPEED_ANGLE_ASSIST_MAX_TORQUE},
|
||||
"hyundai_ioniq_6.curvy_speed_min": {"profile": "hyundai_ioniq_6", "min": 4.0, "max": 12.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_SPEED_MIN},
|
||||
"hyundai_ioniq_6.curvy_speed_max": {"profile": "hyundai_ioniq_6", "min": 14.0, "max": 25.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_SPEED_MAX},
|
||||
"hyundai_ioniq_6.curvy_turn_in_trim_speed_min": {"profile": "hyundai_ioniq_6", "min": 8.0, "max": 16.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN},
|
||||
"hyundai_ioniq_6.curvy_turn_in_trim_speed_max": {"profile": "hyundai_ioniq_6", "min": 14.0, "max": 25.0, "precision": 0.1, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MAX},
|
||||
"hyundai_ioniq_6.curvy_turn_in_trim_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_TURN_IN_TRIM_LEFT},
|
||||
"hyundai_ioniq_6.curvy_turn_in_trim_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_TURN_IN_TRIM_RIGHT},
|
||||
"hyundai_ioniq_6.curvy_unwind_floor_relief_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_LEFT},
|
||||
"hyundai_ioniq_6.curvy_unwind_floor_relief_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_RIGHT},
|
||||
"hyundai_ioniq_6.curvy_unwind_extra_reduction_left": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_LEFT},
|
||||
"hyundai_ioniq_6.curvy_unwind_extra_reduction_right": {"profile": "hyundai_ioniq_6", "min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_RIGHT},
|
||||
"hyundai_kia_ev6.ff_gain_left": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_FF_GAIN_LEFT},
|
||||
"hyundai_kia_ev6.ff_gain_right": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_FF_GAIN_RIGHT},
|
||||
"hyundai_kia_ev6.turn_in_boost_left": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_TURN_IN_BOOST_LEFT},
|
||||
"hyundai_kia_ev6.turn_in_boost_right": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_TURN_IN_BOOST_RIGHT},
|
||||
"hyundai_kia_ev6.unwind_taper_left": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_UNWIND_TAPER_LEFT},
|
||||
"hyundai_kia_ev6.unwind_taper_right": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_UNWIND_TAPER_RIGHT},
|
||||
"hyundai_kia_ev6.center_taper_max": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_CENTER_TAPER_MAX},
|
||||
"hyundai_kia_ev6.turn_in_threshold_reduction_left": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_LEFT},
|
||||
"hyundai_kia_ev6.turn_in_threshold_reduction_right": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.40, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_RIGHT},
|
||||
"hyundai_kia_ev6.unwind_threshold_increase_left": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_UNWIND_THRESHOLD_INCREASE_LEFT},
|
||||
"hyundai_kia_ev6.unwind_threshold_increase_right": {"profile": "hyundai_kia_ev6", "min": 0.0, "max": 0.80, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": KIA_EV6_UNWIND_THRESHOLD_INCREASE_RIGHT},
|
||||
"toyota_prius.ff_gain_left": {"profile": "toyota_prius", "min": 0.0, "max": 0.25, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_FF_GAIN_LEFT},
|
||||
"toyota_prius.ff_gain_right": {"profile": "toyota_prius", "min": 0.0, "max": 0.25, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_FF_GAIN_RIGHT},
|
||||
"toyota_prius.turn_in_boost_left": {"profile": "toyota_prius", "min": -0.10, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_TURN_IN_BOOST_LEFT},
|
||||
"toyota_prius.turn_in_boost_right": {"profile": "toyota_prius", "min": -0.10, "max": 0.60, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_TURN_IN_BOOST_RIGHT},
|
||||
"toyota_prius.unwind_taper_left": {"profile": "toyota_prius", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_UNWIND_TAPER_LEFT},
|
||||
"toyota_prius.unwind_taper_right": {"profile": "toyota_prius", "min": 0.0, "max": 1.20, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_UNWIND_TAPER_RIGHT},
|
||||
"toyota_prius.center_taper_max": {"profile": "toyota_prius", "min": 0.0, "max": 0.25, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_CENTER_TAPER_MAX},
|
||||
"toyota_prius.turn_in_threshold_reduction_left": {"profile": "toyota_prius", "min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_TURN_IN_THRESHOLD_REDUCTION_LEFT},
|
||||
"toyota_prius.turn_in_threshold_reduction_right": {"profile": "toyota_prius", "min": 0.0, "max": 0.50, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_TURN_IN_THRESHOLD_REDUCTION_RIGHT},
|
||||
"toyota_prius.unwind_threshold_increase_left": {"profile": "toyota_prius", "min": 0.0, "max": 0.90, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_UNWIND_THRESHOLD_INCREASE_LEFT},
|
||||
"toyota_prius.unwind_threshold_increase_right": {"profile": "toyota_prius", "min": 0.0, "max": 0.90, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True, "defaultValue": PRIUS_UNWIND_THRESHOLD_INCREASE_RIGHT},
|
||||
}
|
||||
|
||||
|
||||
def _add_ftm_full_surface_profile_knobs(profile_key: str, defaults: dict[str, float] | None = None) -> None:
|
||||
knob_defaults = dict(FTM_FULL_SURFACE_NEUTRAL_DEFAULTS)
|
||||
if defaults:
|
||||
knob_defaults.update(defaults)
|
||||
|
||||
for suffix, meta in FTM_FULL_SURFACE_SUFFIX_METADATA.items():
|
||||
symbol = _ftm_profile_symbol(profile_key, suffix)
|
||||
if symbol in FTM_SUPPORTED_VEHICLE_KNOBS:
|
||||
continue
|
||||
FTM_SUPPORTED_VEHICLE_KNOBS[symbol] = {
|
||||
"profile": profile_key,
|
||||
"min": meta["min"],
|
||||
"max": meta["max"],
|
||||
"precision": meta["precision"],
|
||||
"deltaType": meta["deltaType"],
|
||||
"safeLiveTrial": meta["safeLiveTrial"],
|
||||
"defaultValue": knob_defaults[suffix],
|
||||
}
|
||||
|
||||
|
||||
for _ftm_profile_key in ("gm_bolt_2022_2023", "hyundai_ioniq_6", "hyundai_kia_ev6", "toyota_prius", FTM_UNIVERSAL_PROFILE_KEY):
|
||||
_add_ftm_full_surface_profile_knobs(_ftm_profile_key)
|
||||
|
||||
|
||||
def get_ftm_supported_vehicle_knobs() -> dict:
|
||||
return _ftm_copy_json(FTM_SUPPORTED_VEHICLE_KNOBS)
|
||||
|
||||
|
||||
def get_ftm_rich_profile_key(car_fingerprint) -> str | None:
|
||||
for profile_key, cars in FTM_RICH_PROFILE_CARS.items():
|
||||
if car_fingerprint in cars:
|
||||
return profile_key
|
||||
return None
|
||||
|
||||
|
||||
def get_ftm_surface_profile_key(car_fingerprint, torque_control: bool = True) -> str | None:
|
||||
profile_key = get_ftm_rich_profile_key(car_fingerprint)
|
||||
if profile_key is not None or not torque_control:
|
||||
return profile_key
|
||||
return FTM_UNIVERSAL_PROFILE_KEY
|
||||
|
||||
|
||||
def get_ftm_capabilities(car_fingerprint, brand: str = "", hyundai_canfd: bool = False, torque_control: bool = True) -> dict:
|
||||
profile_key = get_ftm_surface_profile_key(car_fingerprint, torque_control=torque_control)
|
||||
if hyundai_canfd:
|
||||
friction_family = "hkg_canfd"
|
||||
elif brand == "gm":
|
||||
friction_family = "gm"
|
||||
else:
|
||||
friction_family = "standard"
|
||||
|
||||
dedicated_friction = car_fingerprint in (
|
||||
set(BOLT_2022_2023_CARS) | set(BOLT_2018_2021_CARS) | set(VOLT_STANDARD_CARS) | set(PALISADE_CARS) |
|
||||
set(PRIUS_CARS) | set(IONIQ_5_CARS) | set(IONIQ_6_CARS) | set(KIA_EV6_CARS) | set(KIA_FORTE_CARS) |
|
||||
set(KIA_NIRO_PHEV_2022_CARS) | set(GENESIS_G90_CARS)
|
||||
)
|
||||
dedicated_center_taper = car_fingerprint in (
|
||||
set(PRIUS_CARS) | set(BOLT_CARS) | set(VOLT_STANDARD_CARS) | set(IONIQ_5_CARS) |
|
||||
set(IONIQ_EV_OLD_CARS) | set(IONIQ_6_CARS) | set(SONATA_CARS) | set(SONATA_HYBRID_CARS) |
|
||||
set(KIA_XCEED_CARS) | set(KIA_NIRO_PHEV_2022_CARS) | set(KIA_FORTE_CARS) | set(KIA_EV6_CARS) |
|
||||
set(SILVERADO_CARS)
|
||||
)
|
||||
rich_knobs = [name for name, meta in FTM_SUPPORTED_VEHICLE_KNOBS.items() if meta["profile"] == profile_key]
|
||||
return {
|
||||
"torqueControl": bool(torque_control),
|
||||
"frictionFamily": friction_family,
|
||||
"hasDedicatedFrictionThreshold": bool(dedicated_friction),
|
||||
"hasDedicatedCenterTaper": bool(dedicated_center_taper),
|
||||
"richProfileKey": profile_key,
|
||||
"richProfileLabel": FTM_RICH_PROFILE_LABELS.get(profile_key),
|
||||
"richKnobs": rich_knobs,
|
||||
}
|
||||
|
||||
|
||||
__all__ = [name for name in globals() if not name.startswith("_") and name not in {
|
||||
"math", "np", "GM_CAR", "HYUNDAI_CAR", "TOYOTA_CAR", "CV", "testing_ground",
|
||||
}]
|
||||
|
||||
@@ -19,7 +19,13 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import (
|
||||
get_civic_bosch_modified_pid_output_alpha,
|
||||
get_civic_bosch_modified_pid_output_scale,
|
||||
)
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import get_hkg_canfd_base_friction_threshold
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
|
||||
clear_ftm_runtime_overrides,
|
||||
get_ftm_runtime_overrides,
|
||||
get_hkg_canfd_base_friction_threshold,
|
||||
normalize_ftm_overrides,
|
||||
set_ftm_runtime_overrides,
|
||||
)
|
||||
from openpilot.selfdrive.controls.lib.latcontrol_torque import (
|
||||
get_civic_bosch_modified_a_center_taper_scale,
|
||||
LatControlTorque,
|
||||
@@ -258,6 +264,42 @@ class TestLatControl:
|
||||
assert unwind_left < steady_left
|
||||
assert unwind_right < steady_right
|
||||
|
||||
def test_ftm_standard_friction_curve_override(self):
|
||||
base = get_standard_friction_threshold(10.0)
|
||||
overrides = normalize_ftm_overrides({
|
||||
"baseFrictionThresholds": {
|
||||
"standard": {
|
||||
"values": [0.30, 0.31, 0.32, 0.33, 0.34],
|
||||
},
|
||||
},
|
||||
})
|
||||
try:
|
||||
set_ftm_runtime_overrides(overrides)
|
||||
assert get_ftm_runtime_overrides()["baseFrictionThresholds"]["standard"]["values"] == [0.30, 0.31, 0.32, 0.33, 0.34]
|
||||
assert get_standard_friction_threshold(10.0) == pytest.approx(0.32)
|
||||
assert get_standard_friction_threshold(12.5) > get_standard_friction_threshold(10.0)
|
||||
assert get_standard_friction_threshold(10.0) != pytest.approx(base)
|
||||
finally:
|
||||
clear_ftm_runtime_overrides()
|
||||
assert get_ftm_runtime_overrides() == {}
|
||||
assert get_standard_friction_threshold(10.0) == pytest.approx(base)
|
||||
|
||||
def test_ftm_vehicle_knob_override_ioniq6_center_taper(self):
|
||||
baseline = get_ioniq_6_center_taper_scale(0.0, 32.0)
|
||||
overrides = normalize_ftm_overrides({
|
||||
"vehicleKnobs": {
|
||||
"hyundai_ioniq_6.center_taper_max": 0.0,
|
||||
"hyundai_ioniq_6.highway_center_taper_max": 0.0,
|
||||
},
|
||||
})
|
||||
try:
|
||||
set_ftm_runtime_overrides(overrides)
|
||||
adjusted = get_ioniq_6_center_taper_scale(0.0, 32.0)
|
||||
assert adjusted > baseline
|
||||
assert adjusted <= 1.0
|
||||
finally:
|
||||
clear_ftm_runtime_overrides()
|
||||
|
||||
def test_sonata_hybrid_center_taper_curve(self):
|
||||
assert get_sonata_hybrid_center_taper_scale(0.0, 30.0) < get_sonata_hybrid_center_taper_scale(0.0, 15.0)
|
||||
assert get_sonata_hybrid_center_taper_scale(0.0, 3.0) < get_sonata_hybrid_center_taper_scale(0.0, 10.0)
|
||||
|
||||
@@ -229,6 +229,9 @@ EXCLUDED_KEYS = {
|
||||
"CommunityFavorites",
|
||||
"CurvatureData",
|
||||
"ExperimentalLongitudinalEnabled",
|
||||
"FTMActiveOverrides",
|
||||
"FTMActiveProfileId",
|
||||
"FTMTrialApplied",
|
||||
"InstallDate",
|
||||
"StarPilotCarParamsPersistent",
|
||||
"KonikMinutes",
|
||||
@@ -677,6 +680,13 @@ class StarPilotVariables:
|
||||
advanced_lateral_tuning = self.get_value("AdvancedLateralTune")
|
||||
toggle.force_auto_tune = self.get_value("ForceAutoTune", condition=advanced_lateral_tuning and not has_auto_tune and is_torque_car and not is_angle_car)
|
||||
toggle.force_auto_tune_off = self.get_value("ForceAutoTuneOff", condition=advanced_lateral_tuning and has_auto_tune and is_torque_car and not is_angle_car)
|
||||
toggle.ftm_active_profile_id = self.params.get("FTMActiveProfileId", encoding="utf-8") or ""
|
||||
toggle.ftm_trial_applied = self.params.get_bool("FTMTrialApplied")
|
||||
ftm_overrides_raw = self.params.get("FTMActiveOverrides", encoding="utf-8") or ""
|
||||
try:
|
||||
toggle.ftm_active_overrides = json.loads(ftm_overrides_raw) if ftm_overrides_raw else {}
|
||||
except Exception:
|
||||
toggle.ftm_active_overrides = {}
|
||||
toggle.steerActuatorDelay = self.get_value("SteerDelay", cast=float, condition=advanced_lateral_tuning, default=steerActuatorDelay, min=0.01, max=1.0)
|
||||
toggle.use_custom_steerActuatorDelay = bool(round(toggle.steerActuatorDelay, 2) != round(steerActuatorDelay, 2))
|
||||
toggle.friction = self.get_value("SteerFriction", cast=float, condition=advanced_lateral_tuning, default=friction, min=0, max=1)
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
|
||||
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
||||
import { Home } from "/assets/components/home/home.js"
|
||||
import { LateralManeuvers } from "/assets/components/tools/lateral_maneuvers.js"
|
||||
import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js"
|
||||
import { MapsManager } from "/assets/components/tools/maps.js"
|
||||
import { NavDestination } from "/assets/components/navigation/navigation_destination.js?v=nav-search-context-1"
|
||||
@@ -14,12 +13,13 @@ import { NavKeys } from "/assets/components/navigation/navigation_keys.js?v=app-
|
||||
import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js"
|
||||
import { SettingsView } from "/assets/components/settings.js"
|
||||
import { ScreenRecordings } from "/assets/components/recordings/screen_recordings.js"
|
||||
import { Sidebar } from "/assets/components/sidebar.js?v=app-keys-session-1"
|
||||
import { Sidebar } from "/assets/components/sidebar.js?v=lateral-tuning-1"
|
||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
|
||||
import { LivePlots } from "/assets/components/tools/plots.js"
|
||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||
import { TestingGround } from "/assets/components/tools/testing_ground.js"
|
||||
import { Tuning } from "/assets/components/tools/tuning.js?v=ftm-workspace-4"
|
||||
import { Troubleshoot } from "/assets/components/tools/troubleshoot.js"
|
||||
import { TmuxLog } from "/assets/components/tools/tmux.js"
|
||||
import { ToggleControl } from "/assets/components/tools/toggles.js"
|
||||
@@ -72,7 +72,8 @@ function Root() {
|
||||
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
|
||||
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
|
||||
createRoute("model_manager", "/manage_models", ModelManager),
|
||||
createRoute("lateral_maneuvers", "/lateral_maneuvers", LateralManeuvers),
|
||||
createRoute("tuning", "/tuning", Tuning),
|
||||
createRoute("lateral_maneuvers", "/lateral_maneuvers", Tuning),
|
||||
createRoute("longitudinal_maneuvers", "/longitudinal_maneuvers", LongitudinalManeuvers),
|
||||
createRoute("maps", "/manage_maps", MapsManager),
|
||||
createRoute("plots", "/plots", LivePlots),
|
||||
|
||||
@@ -14,7 +14,7 @@ const MENU_ITEMS = {
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
|
||||
{ name: "Lateral Maneuvers", link: "/lateral_maneuvers", icon: "bi-sign-turn-right" },
|
||||
{ name: "Lateral Tuning", link: "/tuning", icon: "bi-sign-turn-right" },
|
||||
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
|
||||
{ name: "Maps", link: "/manage_maps", icon: "bi-map" },
|
||||
{ name: "Navigation", link: "/set_navigation_destination", icon: "bi-geo-alt-fill" },
|
||||
@@ -33,6 +33,7 @@ const MENU_ITEMS = {
|
||||
|
||||
function matchesPath(currentPath, link) {
|
||||
if (link === "/") return currentPath === "/";
|
||||
if (link === "/tuning" && currentPath === "/lateral_maneuvers") return true;
|
||||
return currentPath === link || currentPath.startsWith(`${link}/`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
.ftmTwoColumn {
|
||||
display: grid;
|
||||
gap: var(--gap-base);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.ftmCard {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-top: var(--margin-base);
|
||||
padding: var(--padding-base);
|
||||
}
|
||||
|
||||
.ftmCardHeader {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ftmCardHeader h3,
|
||||
.ftmCardHeader h4,
|
||||
.ftmCardHeader h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ftmCardSubsection {
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.ftmRouteList,
|
||||
.ftmWorkspaceList,
|
||||
.ftmFindings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.ftmWorkspaceRow {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.ftmRouteList {
|
||||
max-height: 28rem;
|
||||
overflow: auto;
|
||||
padding-right: var(--padding-xs);
|
||||
}
|
||||
|
||||
.ftmRouteItem,
|
||||
.ftmWorkspaceItem {
|
||||
align-items: flex-start;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
padding: var(--padding-sm);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ftmRouteItem input {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.ftmRouteItem span,
|
||||
.ftmWorkspaceItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ftmWorkspaceItem {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.ftmWorkspaceDelete {
|
||||
align-self: stretch;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.ftmRouteItem small,
|
||||
.ftmWorkspaceItem small,
|
||||
.ftmWorkspaceItem span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ftmWorkspaceItem:hover,
|
||||
.ftmRouteItem:hover,
|
||||
.ftmCard button.selected {
|
||||
border-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.ftmProfileGrid {
|
||||
display: grid;
|
||||
gap: var(--gap-base);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.ftmFeedbackButtons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
.ftmDeltaBox {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin-top: var(--margin-sm);
|
||||
padding: var(--padding-sm);
|
||||
}
|
||||
|
||||
.ftmNotes {
|
||||
background: var(--secondary-bg);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
margin-top: var(--margin-base);
|
||||
min-height: 5.5rem;
|
||||
padding: var(--padding-sm);
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ftmPlotWrap {
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.ftmPlotWrap svg {
|
||||
height: 140px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
.ftmTwoColumn,
|
||||
.ftmProfileGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
import { html, reactive } from "/assets/vendor/arrow-core.js"
|
||||
|
||||
const MAX_RENDERED_ROUTES = 250
|
||||
const ROUTE_FLUSH_INTERVAL_MS = 120
|
||||
const STATUS_POLL_MS = 3000
|
||||
|
||||
const state = reactive({
|
||||
loadingRoutes: true,
|
||||
loadingWorkspace: true,
|
||||
runningAction: false,
|
||||
error: "",
|
||||
routes: [],
|
||||
selectedRoutes: [],
|
||||
truncatedRoutes: false,
|
||||
routeProgress: 0,
|
||||
routeTotal: 0,
|
||||
workspace: { reports: [], activeTrial: null, status: {} },
|
||||
status: {},
|
||||
report: null,
|
||||
feedbackAccepted: [],
|
||||
feedbackIgnored: [],
|
||||
feedbackNotes: "",
|
||||
})
|
||||
|
||||
let routesAbortController = null
|
||||
let routesRequestToken = 0
|
||||
let pendingRoutes = []
|
||||
let flushTimerId = null
|
||||
let seenRouteNames = new Set()
|
||||
let initialized = false
|
||||
let statusPollHandle = null
|
||||
|
||||
function isTuningRouteActive() {
|
||||
return window.location.pathname === "/tuning" || window.location.pathname === "/lateral_maneuvers"
|
||||
}
|
||||
|
||||
function formatTimestamp(value) {
|
||||
if (!value) return "Unknown Route"
|
||||
const parsed = new Date(value)
|
||||
if (Number.isNaN(parsed.getTime())) return String(value)
|
||||
return parsed.toLocaleString()
|
||||
}
|
||||
|
||||
function safeCount(value) {
|
||||
const n = Number(value)
|
||||
return Number.isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
function formatStatusAge(updatedAt) {
|
||||
const updated = Number(updatedAt)
|
||||
if (!Number.isFinite(updated) || updated <= 0) return "unknown"
|
||||
const ageSec = Math.max(0, Math.round(Date.now() / 1000 - updated))
|
||||
if (ageSec < 5) return "just now"
|
||||
if (ageSec < 60) return `${ageSec}s ago`
|
||||
if (ageSec < 3600) return `${Math.round(ageSec / 60)}m ago`
|
||||
return `${Math.round(ageSec / 3600)}h ago`
|
||||
}
|
||||
|
||||
function resetRouteStreamState() {
|
||||
pendingRoutes = []
|
||||
seenRouteNames = new Set()
|
||||
if (flushTimerId !== null) {
|
||||
clearTimeout(flushTimerId)
|
||||
flushTimerId = null
|
||||
}
|
||||
}
|
||||
|
||||
function sortedRoutes() {
|
||||
return [...state.routes].sort((a, b) => {
|
||||
const aTime = Date.parse(a.timestamp)
|
||||
const bTime = Date.parse(b.timestamp)
|
||||
if (Number.isFinite(aTime) && Number.isFinite(bTime)) return bTime - aTime
|
||||
return String(b.timestamp || "").localeCompare(String(a.timestamp || ""))
|
||||
})
|
||||
}
|
||||
|
||||
function flushPendingRoutes() {
|
||||
if (!pendingRoutes.length) return
|
||||
|
||||
const availableSlots = Math.max(MAX_RENDERED_ROUTES - state.routes.length, 0)
|
||||
if (availableSlots <= 0) {
|
||||
pendingRoutes = []
|
||||
state.truncatedRoutes = true
|
||||
return
|
||||
}
|
||||
|
||||
const toAppend = pendingRoutes.slice(0, availableSlots)
|
||||
pendingRoutes = []
|
||||
if (toAppend.length > 0) {
|
||||
state.routes = [...state.routes, ...toAppend]
|
||||
}
|
||||
if (state.routes.length >= MAX_RENDERED_ROUTES) {
|
||||
state.truncatedRoutes = true
|
||||
}
|
||||
}
|
||||
|
||||
function enqueueRoutes(rawRoutes) {
|
||||
if (!Array.isArray(rawRoutes) || rawRoutes.length === 0) return
|
||||
const nextRoutes = []
|
||||
for (const route of rawRoutes) {
|
||||
const name = String(route?.name || "")
|
||||
if (!name || seenRouteNames.has(name)) continue
|
||||
seenRouteNames.add(name)
|
||||
nextRoutes.push({
|
||||
...route,
|
||||
timestampLabel: formatTimestamp(route.timestamp),
|
||||
})
|
||||
}
|
||||
if (!nextRoutes.length) return
|
||||
|
||||
pendingRoutes.push(...nextRoutes)
|
||||
if (flushTimerId === null) {
|
||||
flushTimerId = setTimeout(() => {
|
||||
flushTimerId = null
|
||||
flushPendingRoutes()
|
||||
}, ROUTE_FLUSH_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRoutes() {
|
||||
const requestToken = ++routesRequestToken
|
||||
if (routesAbortController) routesAbortController.abort()
|
||||
const controller = new AbortController()
|
||||
routesAbortController = controller
|
||||
|
||||
try {
|
||||
state.loadingRoutes = true
|
||||
state.error = ""
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const response = await fetch(`/api/routes?timezone=${encodeURIComponent(userTimezone)}`, { signal: controller.signal })
|
||||
if (!response.ok) throw new Error("Failed to load local routes.")
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read()
|
||||
if (done) break
|
||||
if (requestToken !== routesRequestToken) return
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split(/\r?\n\r?\n/)
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) continue
|
||||
try {
|
||||
const data = JSON.parse(line.substring(5).trim())
|
||||
if (data.progress !== undefined && data.total !== undefined) {
|
||||
state.routeProgress = safeCount(data.progress)
|
||||
state.routeTotal = safeCount(data.total)
|
||||
}
|
||||
if (Array.isArray(data.routes)) {
|
||||
enqueueRoutes(data.routes)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ftm] failed to parse route payload", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flushPendingRoutes()
|
||||
} catch (error) {
|
||||
if (error?.name !== "AbortError") {
|
||||
state.error = error?.message || "Failed to load local routes."
|
||||
}
|
||||
} finally {
|
||||
if (requestToken === routesRequestToken) {
|
||||
flushPendingRoutes()
|
||||
state.loadingRoutes = false
|
||||
if (routesAbortController === controller) {
|
||||
routesAbortController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWorkspace() {
|
||||
try {
|
||||
state.loadingWorkspace = true
|
||||
const response = await fetch("/api/ftm/workspace")
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to load tuning workspace.")
|
||||
state.workspace = payload
|
||||
state.status = payload.status || {}
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to load tuning workspace."
|
||||
} finally {
|
||||
state.loadingWorkspace = false
|
||||
}
|
||||
}
|
||||
|
||||
function syncFeedbackState(report) {
|
||||
const feedback = report?.feedback || {}
|
||||
state.feedbackAccepted = Array.isArray(feedback.acceptedDimensions) ? [...feedback.acceptedDimensions] : []
|
||||
state.feedbackIgnored = Array.isArray(feedback.ignoredDimensions) ? [...feedback.ignoredDimensions] : []
|
||||
state.feedbackNotes = typeof feedback.notes === "string" ? feedback.notes : ""
|
||||
}
|
||||
|
||||
async function loadReport(reportId) {
|
||||
if (!reportId) return
|
||||
try {
|
||||
const response = await fetch(`/api/ftm/report/${encodeURIComponent(reportId)}`)
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to load tuning report.")
|
||||
state.report = payload
|
||||
syncFeedbackState(payload)
|
||||
await fetchWorkspace()
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to load tuning report."
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteReport(reportId) {
|
||||
if (!reportId || state.runningAction) return
|
||||
if (!window.confirm("Delete this saved tuning report and its generated trial data?")) return
|
||||
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch(`/api/ftm/report/${encodeURIComponent(reportId)}`, { method: "DELETE" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to delete tuning report.")
|
||||
|
||||
if (state.report?.reportId === reportId) {
|
||||
state.report = null
|
||||
syncFeedbackState(null)
|
||||
}
|
||||
state.workspace = payload.workspace || state.workspace
|
||||
state.status = { ...state.status, ...(payload.workspace?.status || {}) }
|
||||
showSnackbar(payload.message || "Deleted tuning report.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to delete tuning report."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function clearWorkspace() {
|
||||
if (state.runningAction) return
|
||||
if (!window.confirm("Clear every saved tuning report, feedback entry, generated profile, and snapshot from the device?")) return
|
||||
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/workspace/clear", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to clear tuning workspace.")
|
||||
|
||||
state.report = null
|
||||
syncFeedbackState(null)
|
||||
state.workspace = payload.workspace || { reports: [], activeTrial: null, status: {} }
|
||||
state.status = { ...state.status, ...(payload.workspace?.status || {}) }
|
||||
showSnackbar(payload.message || "Cleared tuning workspace.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to clear tuning workspace."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch("/api/ftm/status")
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to load tuning status.")
|
||||
state.status = {
|
||||
...(payload.status || {}),
|
||||
isOnroad: !!payload.isOnroad,
|
||||
}
|
||||
if (payload.activeTrial !== undefined) {
|
||||
state.workspace = { ...state.workspace, activeTrial: payload.activeTrial, reports: payload.reports || state.workspace.reports }
|
||||
}
|
||||
const reportId = state.status.reportId
|
||||
if (reportId && state.report?.reportId !== reportId) {
|
||||
await loadReport(reportId)
|
||||
}
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to load tuning status."
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (statusPollHandle) {
|
||||
clearTimeout(statusPollHandle)
|
||||
statusPollHandle = null
|
||||
}
|
||||
}
|
||||
|
||||
function ensurePolling() {
|
||||
if (statusPollHandle) return
|
||||
|
||||
const poll = async () => {
|
||||
if (!isTuningRouteActive()) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
if (document.visibilityState === "visible") {
|
||||
await fetchStatus()
|
||||
}
|
||||
statusPollHandle = setTimeout(poll, STATUS_POLL_MS)
|
||||
}
|
||||
|
||||
statusPollHandle = setTimeout(poll, STATUS_POLL_MS)
|
||||
}
|
||||
|
||||
function toggleRouteSelection(routeName) {
|
||||
const current = new Set(state.selectedRoutes)
|
||||
if (current.has(routeName)) {
|
||||
current.delete(routeName)
|
||||
} else {
|
||||
current.add(routeName)
|
||||
}
|
||||
state.selectedRoutes = [...current]
|
||||
}
|
||||
|
||||
function clearSelections() {
|
||||
state.selectedRoutes = []
|
||||
}
|
||||
|
||||
async function runAnalyze() {
|
||||
if (!state.selectedRoutes.length || state.runningAction) return
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ routes: state.selectedRoutes }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to start tuning analysis.")
|
||||
state.status = payload.status || {}
|
||||
showSnackbar(payload.message || "FTM analysis started.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to start tuning analysis."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function stopAnalyze() {
|
||||
if (state.runningAction) return
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/analyze/stop", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to stop tuning analysis.")
|
||||
state.status = payload.status || {}
|
||||
showSnackbar(payload.message || "FTM analysis stopped.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to stop tuning analysis."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function applyProfile(profileId) {
|
||||
if (!state.report?.reportId || !profileId || state.runningAction) return
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/trials/apply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ reportId: state.report.reportId, profileId }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to apply trial profile.")
|
||||
await fetchWorkspace()
|
||||
showSnackbar(payload.message || "Trial profile applied.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to apply trial profile."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function revertProfile() {
|
||||
if (state.runningAction) return
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/trials/revert", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to revert trial profile.")
|
||||
await fetchWorkspace()
|
||||
showSnackbar(payload.message || "Trial profile reverted.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to revert trial profile."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
function setDimensionFeedback(dimensionId, mode) {
|
||||
const accepted = new Set(state.feedbackAccepted)
|
||||
const ignored = new Set(state.feedbackIgnored)
|
||||
accepted.delete(dimensionId)
|
||||
ignored.delete(dimensionId)
|
||||
if (mode === "accepted") accepted.add(dimensionId)
|
||||
if (mode === "ignored") ignored.add(dimensionId)
|
||||
state.feedbackAccepted = [...accepted]
|
||||
state.feedbackIgnored = [...ignored]
|
||||
}
|
||||
|
||||
async function saveFeedback() {
|
||||
if (!state.report?.reportId || state.runningAction) return
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/feedback", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
reportId: state.report.reportId,
|
||||
acceptedDimensions: state.feedbackAccepted,
|
||||
ignoredDimensions: state.feedbackIgnored,
|
||||
notes: state.feedbackNotes,
|
||||
}),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to save tuning feedback.")
|
||||
state.report = {
|
||||
...state.report,
|
||||
feedback: payload.feedback,
|
||||
profiles: payload.profiles || state.report.profiles,
|
||||
}
|
||||
syncFeedbackState(state.report)
|
||||
await fetchWorkspace()
|
||||
showSnackbar(payload.message || "Feedback saved.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to save tuning feedback."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
function feedbackStateFor(dimensionId) {
|
||||
if (state.feedbackAccepted.includes(dimensionId)) return "accepted"
|
||||
if (state.feedbackIgnored.includes(dimensionId)) return "ignored"
|
||||
return "unset"
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
resetRouteStreamState()
|
||||
fetchRoutes()
|
||||
fetchWorkspace().then(() => {
|
||||
const latestReport = state.workspace?.reports?.[0]?.reportId
|
||||
if (latestReport) loadReport(latestReport)
|
||||
})
|
||||
fetchStatus()
|
||||
ensurePolling()
|
||||
}
|
||||
|
||||
function renderCurve(values) {
|
||||
return `[${(values || []).map((value) => Number(value).toFixed(3)).join(", ")}]`
|
||||
}
|
||||
|
||||
function reportPaths() {
|
||||
if (Array.isArray(state.report?.paths) && state.report.paths.length) {
|
||||
return state.report.paths
|
||||
}
|
||||
if (!state.report) return []
|
||||
return [{
|
||||
key: state.report.primaryPathKey || "cleanup_pass",
|
||||
title: "Recommendations",
|
||||
description: "",
|
||||
whenToUse: "",
|
||||
whySelected: "",
|
||||
isPrimary: true,
|
||||
suggestions: state.report.suggestions || [],
|
||||
profiles: state.report.profiles || [],
|
||||
}]
|
||||
}
|
||||
|
||||
function primaryPath() {
|
||||
const paths = reportPaths()
|
||||
return paths.find((path) => path.isPrimary) || paths[0] || null
|
||||
}
|
||||
|
||||
function renderProfile(profile) {
|
||||
const genericEntries = Object.entries(profile.genericParams || {}).filter(([key]) => key !== "AdvancedLateralTune")
|
||||
const frictionEntries = Object.entries(profile.ftmOverrides?.baseFrictionThresholds || {})
|
||||
const vehicleKnobEntries = Object.entries(profile.ftmOverrides?.vehicleKnobs || {})
|
||||
return html`
|
||||
<div class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h4>${profile.label}</h4>
|
||||
<p class="longManeuverMuted">${profile.description}</p>
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || false}"
|
||||
@click="${() => applyProfile(profile.id)}">
|
||||
Apply Trial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ftmProfileGrid">
|
||||
<div>
|
||||
<h5>Generic Params</h5>
|
||||
<ul>
|
||||
${genericEntries.length
|
||||
? genericEntries.map(([key, value]) => html`<li><code>${key}</code>: ${String(value)}</li>`)
|
||||
: html`<li>None</li>`}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>FTM Overrides</h5>
|
||||
<ul>
|
||||
${frictionEntries.map(([family, payload]) => html`<li><code>${family}</code>: ${renderCurve(payload?.values || [])}</li>`)}
|
||||
${vehicleKnobEntries.map(([key, value]) => html`<li><code>${key}</code>: ${Number(value).toFixed(3)}</li>`)}
|
||||
${!frictionEntries.length && !vehicleKnobEntries.length ? html`<li>None</li>` : ""}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderSuggestion(suggestion) {
|
||||
const currentVsSuggested = suggestion.currentVsSuggested
|
||||
const feedbackState = feedbackStateFor(suggestion.dimensionId)
|
||||
return html`
|
||||
<div class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h4>${suggestion.bucket.replace(/_/g, " ")}</h4>
|
||||
<p class="longManeuverMuted">
|
||||
${suggestion.evidence?.speedBand || "mixed"} |
|
||||
${suggestion.evidence?.directionBias || "center"} |
|
||||
${safeCount(suggestion.evidence?.eventCount)} event(s)
|
||||
</p>
|
||||
</div>
|
||||
<div class="ftmFeedbackButtons">
|
||||
<button
|
||||
class="longManeuverButton ${feedbackState === "accepted" ? "selected" : ""}"
|
||||
@click="${() => setDimensionFeedback(suggestion.dimensionId, feedbackState === "accepted" ? "unset" : "accepted")}">
|
||||
Matches Experience
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton ${feedbackState === "ignored" ? "danger selected" : "danger"}"
|
||||
@click="${() => setDimensionFeedback(suggestion.dimensionId, feedbackState === "ignored" ? "unset" : "ignored")}">
|
||||
Ignore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><strong>Observed behavior:</strong> ${suggestion.observedBehavior}</p>
|
||||
<p><strong>Likely interpretation:</strong> ${suggestion.likelyInterpretation}</p>
|
||||
<p><strong>Primary adjustment:</strong> ${suggestion.primaryAdjustment}</p>
|
||||
<p><strong>What not to touch yet:</strong> ${suggestion.whatNotToTouchYet}</p>
|
||||
<p><strong>If that was wrong, next thing to try:</strong> ${suggestion.ifThatWasWrong}</p>
|
||||
<p><strong>Strongest segments:</strong> ${(suggestion.evidence?.segments || []).map((segment) => segment.label).join(", ") || "none"}</p>
|
||||
|
||||
${currentVsSuggested
|
||||
? html`
|
||||
<div class="ftmDeltaBox">
|
||||
<strong>Current vs suggested:</strong>
|
||||
${currentVsSuggested.type === "friction_curve"
|
||||
? html`
|
||||
<p><code>${currentVsSuggested.family}</code> current: ${renderCurve(currentVsSuggested.current)}</p>
|
||||
<p><code>${currentVsSuggested.family}</code> suggested: ${renderCurve(currentVsSuggested.suggested)}</p>
|
||||
`
|
||||
: html`
|
||||
<p>
|
||||
<code>${currentVsSuggested.paramKey || currentVsSuggested.symbol}</code>:
|
||||
${Number(currentVsSuggested.current).toFixed(3)} -> ${Number(currentVsSuggested.suggested).toFixed(3)}
|
||||
</p>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: html`<p class="longManeuverMuted">No trial adjustment suggested for this dimension.</p>`}
|
||||
|
||||
${suggestion.plotSvg
|
||||
? html`<div class="ftmPlotWrap"><p class="longManeuverMuted">Saved report includes an inline event plot for this finding.</p></div>`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function renderPathSummary(path) {
|
||||
return html`
|
||||
<div class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h4>${path.title}</h4>
|
||||
<p class="longManeuverMuted">${path.isPrimary ? "Recommended path" : "Alternate path"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>${path.description || ""}</p>
|
||||
<p><strong>Why this path:</strong> ${path.whySelected || "No path note available."}</p>
|
||||
<p><strong>When to use it:</strong> ${path.whenToUse || "Use the path that best matches the spread of the problem."}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export function Tuning() {
|
||||
initialize()
|
||||
|
||||
return html`
|
||||
<div class="longManeuverPage">
|
||||
<h2>Lateral Tuning</h2>
|
||||
|
||||
<div class="longManeuverCard">
|
||||
<p class="longManeuverIntro">
|
||||
Analyze one or more local routes, review deterministic lateral findings, apply a bounded trial, drive, then revert or refine.
|
||||
</p>
|
||||
|
||||
<div class="longManeuverActions">
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || state.selectedRoutes.length === 0 || !!state.status?.isOnroad}"
|
||||
@click="${runAnalyze}">
|
||||
Analyze Selected Routes
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton danger"
|
||||
disabled="${() => state.runningAction || !state.status?.running}"
|
||||
@click="${stopAnalyze}">
|
||||
Stop Analysis
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || !state.workspace?.activeTrial}"
|
||||
@click="${revertProfile}">
|
||||
Revert Trial
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction}"
|
||||
@click="${() => {
|
||||
state.routes = []
|
||||
state.routeProgress = 0
|
||||
state.routeTotal = 0
|
||||
state.truncatedRoutes = false
|
||||
resetRouteStreamState()
|
||||
fetchRoutes()
|
||||
fetchWorkspace()
|
||||
fetchStatus()
|
||||
}}">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${() => state.error ? html`<p class="longManeuverError">${state.error}</p>` : ""}
|
||||
|
||||
<div class="longManeuverStatusGrid">
|
||||
<p><strong>Status:</strong> ${() => state.status?.state || "idle"}</p>
|
||||
<p><strong>Running:</strong> ${() => state.status?.running ? "Yes" : "No"}</p>
|
||||
<p><strong>Onroad:</strong> ${() => state.status?.isOnroad ? "Yes" : "No"}</p>
|
||||
<p><strong>Updated:</strong> ${() => formatStatusAge(state.status?.updatedAt)}</p>
|
||||
<p><strong>Selected Routes:</strong> ${() => state.selectedRoutes.length}</p>
|
||||
<p><strong>Progress:</strong> ${() => `${safeCount(state.status?.progress)}/${safeCount(state.status?.total)}`}</p>
|
||||
<p><strong>Active Trial:</strong> ${() => state.workspace?.activeTrial?.profileId || "None"}</p>
|
||||
</div>
|
||||
|
||||
${() => state.status?.isOnroad ? html`
|
||||
<p class="longManeuverError">FTM analysis is offroad-only. Stop the car and go offroad before starting a run.</p>
|
||||
` : ""}
|
||||
|
||||
${() => state.status?.currentSegment ? html`
|
||||
<div class="longManeuverCurrent">
|
||||
<p><strong>Current Segment:</strong> ${state.status.currentSegment}</p>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<div class="ftmTwoColumn">
|
||||
<section class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h3>Local Routes</h3>
|
||||
<p class="longManeuverMuted">
|
||||
Pick up to 8 routes from the device. The analyzer prefers rlogs and falls back to qlogs when needed.
|
||||
</p>
|
||||
</div>
|
||||
<button class="longManeuverButton" @click="${clearSelections}">Clear</button>
|
||||
</div>
|
||||
|
||||
${() => state.loadingRoutes ? html`<p class="longManeuverMuted">Loading local routes...</p>` : ""}
|
||||
${() => state.routeTotal ? html`<p class="longManeuverMuted">Route index: ${state.routeProgress}/${state.routeTotal}</p>` : ""}
|
||||
${() => state.truncatedRoutes ? html`<p class="longManeuverMuted">Showing the first ${MAX_RENDERED_ROUTES} routes only.</p>` : ""}
|
||||
|
||||
<div class="ftmRouteList">
|
||||
${() => sortedRoutes().map((route) => html`
|
||||
<label class="ftmRouteItem">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked="${() => state.selectedRoutes.includes(route.name)}"
|
||||
@change="${() => toggleRouteSelection(route.name)}" />
|
||||
<span>
|
||||
<strong>${route.timestampLabel}</strong>
|
||||
<small>${route.name}</small>
|
||||
</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h3>Workspace</h3>
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton danger"
|
||||
disabled="${() => state.runningAction || !(state.workspace?.reports || []).length}"
|
||||
@click="${clearWorkspace}">
|
||||
Clear Workspace
|
||||
</button>
|
||||
</div>
|
||||
${() => state.loadingWorkspace ? html`<p class="longManeuverMuted">Loading workspace...</p>` : ""}
|
||||
<p class="longManeuverMuted">
|
||||
Recent reports stay on-device under <code>/data/galaxy/ftm</code>. Loading a report refreshes the suggestion and trial view below.
|
||||
</p>
|
||||
<div class="ftmWorkspaceList">
|
||||
${() => (state.workspace?.reports || []).length
|
||||
? state.workspace.reports.map((report) => html`
|
||||
<div class="ftmWorkspaceRow">
|
||||
<button class="ftmWorkspaceItem" @click="${() => loadReport(report.reportId)}">
|
||||
<strong>${report.carFingerprint || "Unknown car"}</strong>
|
||||
<span>${(report.routeNames || []).join(", ")}</span>
|
||||
<small>${formatTimestamp(report.createdAt ? new Date(report.createdAt * 1000).toISOString() : "")}</small>
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton danger ftmWorkspaceDelete"
|
||||
disabled="${() => state.runningAction}"
|
||||
@click="${() => deleteReport(report.reportId)}">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
`)
|
||||
: html`<p class="longManeuverMuted">No tuning reports yet.</p>`}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
${() => state.report ? html`
|
||||
<section class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h3>Report Summary</h3>
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton danger"
|
||||
disabled="${() => state.runningAction || !state.report?.reportId}"
|
||||
@click="${() => deleteReport(state.report.reportId)}">
|
||||
Delete Report
|
||||
</button>
|
||||
</div>
|
||||
<div class="longManeuverStatusGrid">
|
||||
<p><strong>Car:</strong> ${state.report.car?.carFingerprint || "Unknown"}</p>
|
||||
<p><strong>Control Path:</strong> ${state.report.car?.controlPath || "unknown"}</p>
|
||||
<p><strong>Friction Family:</strong> ${state.report.capabilities?.frictionFamily || "standard"}</p>
|
||||
<p><strong>Recommended Path:</strong> ${primaryPath()?.title || "Recommendations"}</p>
|
||||
<p><strong>Processed Segments:</strong> ${safeCount(state.report.summary?.processedSegments)}</p>
|
||||
<p><strong>qlog Fallback:</strong> ${state.report.summary?.usedQlogFallback ? "Yes" : "No"}</p>
|
||||
<p><strong>Samples:</strong> ${safeCount(state.report.summary?.sampleCount)}</p>
|
||||
</div>
|
||||
|
||||
<div class="ftmFindings">
|
||||
${reportPaths().map((path) => renderPathSummary(path))}
|
||||
</div>
|
||||
|
||||
${() => (state.report.warnings || []).length ? html`
|
||||
<div class="ftmCardSubsection">
|
||||
<h4>Warnings</h4>
|
||||
<ul>
|
||||
${(state.report.warnings || []).map((warning) => html`<li>${warning}</li>`)}
|
||||
</ul>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
${() => (state.report.addTheseParametersAndStartHere || []).length ? html`
|
||||
<div class="ftmCardSubsection">
|
||||
<h4>Add These Parameters And Start Here</h4>
|
||||
<ul>
|
||||
${(state.report.addTheseParametersAndStartHere || []).map((line) => html`<li>${line}</li>`)}
|
||||
</ul>
|
||||
</div>
|
||||
` : ""}
|
||||
</section>
|
||||
|
||||
<section class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h3>Recommended Findings: ${primaryPath()?.title || "Recommendations"}</h3>
|
||||
<p class="longManeuverMuted">
|
||||
${primaryPath()?.whySelected || "Mark the dimensions that match what the driver felt."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || !state.report}"
|
||||
@click="${saveFeedback}">
|
||||
Save Feedback
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
class="ftmNotes"
|
||||
placeholder="Optional tuning notes"
|
||||
@input="${(event) => { state.feedbackNotes = event.target.value }}">${() => state.feedbackNotes}</textarea>
|
||||
|
||||
<div class="ftmFindings">
|
||||
${((primaryPath()?.suggestions) || []).map((suggestion) => renderSuggestion(suggestion))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ftmCard">
|
||||
<h3>Trial Profiles</h3>
|
||||
<p class="longManeuverMuted">
|
||||
Apply one bounded profile at a time. Revert restores the exact advanced-lateral and FTM state that existed before the trial.
|
||||
</p>
|
||||
<div class="ftmFindings">
|
||||
${reportPaths().length
|
||||
? reportPaths().map((path) => html`
|
||||
<div>
|
||||
<h4>${path.title} Profiles</h4>
|
||||
<p class="longManeuverMuted">${path.whenToUse || ""}</p>
|
||||
${(path.profiles || []).length
|
||||
? (path.profiles || []).map((profile) => renderProfile(profile))
|
||||
: html`<p class="longManeuverMuted">No trial profiles generated for this path.</p>`}
|
||||
</div>
|
||||
`)
|
||||
: html`<p class="longManeuverMuted">No trial profiles generated for this report.</p>`}
|
||||
</div>
|
||||
</section>
|
||||
` : html`
|
||||
<section class="ftmCard">
|
||||
<h3>No Active Report</h3>
|
||||
<p class="longManeuverMuted">
|
||||
Select local routes, run analysis, or open one of the saved reports from the workspace panel.
|
||||
</p>
|
||||
</section>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/testing_ground.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tuning.css?v=ftm-workspace-3">
|
||||
<link rel="stylesheet" href="/assets/components/tools/troubleshoot.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
@@ -44,7 +45,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
<script type="module">
|
||||
import("/assets/components/router.js?v=favorite-slots-6").catch((err) => {
|
||||
import("/assets/components/router.js?v=ftm-workspace-1").catch((err) => {
|
||||
console.error("[the_galaxy] bootstrap failed", err);
|
||||
const target = document.getElementById("app") || document.body;
|
||||
const pre = document.createElement("pre");
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "ftm_workspace.py"
|
||||
|
||||
|
||||
def _simple_module(name, **attrs):
|
||||
module = ModuleType(name)
|
||||
for attr, value in attrs.items():
|
||||
setattr(module, attr, value)
|
||||
return module
|
||||
|
||||
|
||||
def _install_ftm_import_stubs(tmp_path):
|
||||
class FakeParams:
|
||||
_store = {}
|
||||
|
||||
def __init__(self, return_defaults=False):
|
||||
self.return_defaults = return_defaults
|
||||
|
||||
def get(self, key, block=False, return_default=False, encoding=None, default=None):
|
||||
del block, return_default
|
||||
value = self._store.get(key, default)
|
||||
if encoding and isinstance(value, bytes):
|
||||
return value.decode(encoding, errors="replace")
|
||||
return value
|
||||
|
||||
def get_bool(self, key, default=False):
|
||||
value = self._store.get(key, default)
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
def get_float(self, key, block=False, return_default=False, default=0.0):
|
||||
del block, return_default
|
||||
value = self._store.get(key, default)
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def put(self, key, value):
|
||||
self._store[key] = value
|
||||
|
||||
def put_bool(self, key, value):
|
||||
self._store[key] = bool(value)
|
||||
|
||||
def put_float(self, key, value):
|
||||
self._store[key] = float(value)
|
||||
|
||||
FakeParams._store = {}
|
||||
|
||||
class FakeHyundaiFlags:
|
||||
CANFD = 1
|
||||
|
||||
class FakeSteerControlType:
|
||||
torque = 0
|
||||
|
||||
fake_car_params = SimpleNamespace(SteerControlType=FakeSteerControlType)
|
||||
cereal_car = _simple_module("cereal.car", CarParams=fake_car_params)
|
||||
cereal = _simple_module("cereal", car=cereal_car)
|
||||
sys.modules["cereal"] = cereal
|
||||
sys.modules["cereal.car"] = cereal_car
|
||||
|
||||
sys.modules["opendbc.car.hyundai.values"] = _simple_module("opendbc.car.hyundai.values", HyundaiFlags=FakeHyundaiFlags)
|
||||
sys.modules["openpilot.common.params"] = _simple_module("openpilot.common.params", Params=FakeParams)
|
||||
sys.modules["openpilot.selfdrive.controls.lib.latcontrol_torque"] = _simple_module(
|
||||
"openpilot.selfdrive.controls.lib.latcontrol_torque",
|
||||
KP=1.0,
|
||||
)
|
||||
|
||||
def normalize_ftm_overrides(payload):
|
||||
if isinstance(payload, str):
|
||||
payload = json.loads(payload)
|
||||
payload = payload or {}
|
||||
normalized = {
|
||||
"schemaVersion": 1,
|
||||
"baseFrictionThresholds": {},
|
||||
"vehicleKnobs": {},
|
||||
}
|
||||
for family, family_payload in payload.get("baseFrictionThresholds", {}).items():
|
||||
values = family_payload.get("values", [])
|
||||
if len(values) == 5:
|
||||
normalized["baseFrictionThresholds"][family] = {
|
||||
"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0],
|
||||
"values": [float(value) for value in values],
|
||||
}
|
||||
for key, value in payload.get("vehicleKnobs", {}).items():
|
||||
normalized["vehicleKnobs"][key] = float(value)
|
||||
if not normalized["baseFrictionThresholds"] and not normalized["vehicleKnobs"]:
|
||||
return {}
|
||||
return normalized
|
||||
|
||||
sys.modules["openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes"] = _simple_module(
|
||||
"openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes",
|
||||
FTM_FRICTION_SPEED_KNOTS=[0.0, 5.0, 10.0, 15.0, 25.0],
|
||||
get_ftm_capabilities=lambda *args, **kwargs: {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"},
|
||||
get_ftm_rich_profile_key=lambda *args, **kwargs: "hyundai_ioniq_6",
|
||||
get_ftm_supported_vehicle_knobs=lambda: {
|
||||
"hyundai_ioniq_6.turn_in_boost_left": {"min": 0.4, "max": 2.8, "precision": 0.001, "defaultValue": 1.64, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.unwind_taper_left": {"min": 0.0, "max": 1.2, "precision": 0.001, "defaultValue": 0.4, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": {"min": 0.0, "max": 0.8, "precision": 0.001, "defaultValue": 0.46, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.crawl_turn_in_ff_boost_left": {"min": 0.0, "max": 0.5, "precision": 0.001, "defaultValue": 0.18, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.curvy_turn_in_trim_left": {"min": 0.0, "max": 0.2, "precision": 0.001, "defaultValue": 0.06, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.curvy_unwind_extra_reduction_left": {"min": 0.0, "max": 0.45, "precision": 0.001, "defaultValue": 0.18, "profile": "hyundai_ioniq_6"},
|
||||
},
|
||||
get_gm_base_friction_threshold=lambda v_ego: 0.20 + (0.001 * float(v_ego)),
|
||||
get_hkg_canfd_base_friction_threshold=lambda v_ego: 0.39 + (0.001 * float(v_ego)),
|
||||
get_standard_friction_threshold=lambda v_ego: 0.30 + (0.001 * float(v_ego)),
|
||||
normalize_ftm_overrides=normalize_ftm_overrides,
|
||||
)
|
||||
sys.modules["openpilot.system.hardware"] = _simple_module("openpilot.system.hardware", PC=True)
|
||||
sys.modules["openpilot.system.hardware.hw"] = _simple_module(
|
||||
"openpilot.system.hardware.hw",
|
||||
Paths=SimpleNamespace(comma_home=lambda: str(tmp_path), log_root=lambda **kwargs: str(tmp_path / "logs")),
|
||||
)
|
||||
sys.modules["openpilot.tools.lib.logreader"] = _simple_module("openpilot.tools.lib.logreader", LogReader=lambda *args, **kwargs: [])
|
||||
sys.modules["openpilot.starpilot.system.the_galaxy.utilities"] = _simple_module(
|
||||
"openpilot.starpilot.system.the_galaxy.utilities",
|
||||
get_segments_in_route=lambda route, footage_path: [],
|
||||
)
|
||||
|
||||
return FakeParams
|
||||
|
||||
|
||||
def _load_ftm_workspace_module(tmp_path):
|
||||
fake_params_cls = _install_ftm_import_stubs(tmp_path)
|
||||
module_name = f"test_ftm_workspace_{hash(tmp_path)}"
|
||||
spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module, fake_params_cls
|
||||
|
||||
|
||||
def _sample(module, **kwargs):
|
||||
base = dict(
|
||||
route="route",
|
||||
segment=0,
|
||||
t=0.0,
|
||||
v_ego=28.0,
|
||||
lat_active=True,
|
||||
steering_pressed=False,
|
||||
saturated=False,
|
||||
actual_la=0.0,
|
||||
desired_la=0.0,
|
||||
desired_jerk=0.0,
|
||||
error=0.0,
|
||||
error_rate=0.0,
|
||||
p=0.0,
|
||||
i=0.0,
|
||||
d=0.0,
|
||||
f=0.0,
|
||||
output=0.0,
|
||||
steering_angle_deg=0.0,
|
||||
steering_torque=0.0,
|
||||
cmd_torque=0.0,
|
||||
out_torque=0.0,
|
||||
roll_deg=0.0,
|
||||
)
|
||||
base.update(kwargs)
|
||||
return module.FTMSample(**base)
|
||||
|
||||
|
||||
def test_classify_torque_samples_detects_center_chatter(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
samples = []
|
||||
for idx in range(60):
|
||||
angle = 0.75 * math.sin(idx * 0.9)
|
||||
samples.append(_sample(
|
||||
module,
|
||||
t=idx * 0.1,
|
||||
desired_la=0.04 * math.sin(idx * 0.1),
|
||||
actual_la=0.03 * math.sin(idx * 0.1),
|
||||
steering_angle_deg=angle,
|
||||
output=0.02 * math.sin(idx * 0.9),
|
||||
))
|
||||
|
||||
summaries, stats = module.classify_torque_samples(samples)
|
||||
assert stats["sampleCount"] == len(samples)
|
||||
assert any(summary["bucket"] == "center_chatter" for summary in summaries)
|
||||
|
||||
|
||||
def test_build_suggestions_prefers_rich_low_speed_turn_in_knob(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "low_speed_unwillingness",
|
||||
"dimensionId": "low_speed_unwillingness:left:low",
|
||||
"direction": "left",
|
||||
"speedBand": "low",
|
||||
"severity": 0.9,
|
||||
"evidence": {"speedBand": "low", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
||||
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current)
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "vehicle_knob"
|
||||
assert adjustment["symbol"] == "hyundai_ioniq_6.low_speed_angle_assist_max_torque"
|
||||
|
||||
|
||||
def test_build_suggestions_baseline_prefers_generic_lat_accel_for_understeer(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "understeer",
|
||||
"dimensionId": "understeer:left:mid",
|
||||
"direction": "left",
|
||||
"speedBand": "mid",
|
||||
"severity": 1.0,
|
||||
"evidence": {"speedBand": "mid", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
||||
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current, strategy="baseline")
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "generic_param"
|
||||
assert adjustment["paramKey"] == "SteerLatAccel"
|
||||
assert adjustment["suggested"] > adjustment["current"]
|
||||
|
||||
|
||||
def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "low_speed_unwillingness",
|
||||
"dimensionId": "low_speed_unwillingness:left:low",
|
||||
"direction": "left",
|
||||
"speedBand": "low",
|
||||
"severity": 1.0,
|
||||
"evidence": {"speedBand": "low", "directionBias": "left", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
||||
current = {
|
||||
"SteerLatAccel": 1.8,
|
||||
"SteerFriction": 0.2,
|
||||
"FTMActiveOverrides": {
|
||||
"schemaVersion": 1,
|
||||
"baseFrictionThresholds": {},
|
||||
"vehicleKnobs": {
|
||||
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": 0.62,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current)
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "vehicle_knob"
|
||||
assert adjustment["symbol"] == "hyundai_ioniq_6.low_speed_angle_assist_max_torque"
|
||||
assert adjustment["current"] == pytest.approx(0.62)
|
||||
assert adjustment["suggested"] > adjustment["current"]
|
||||
|
||||
|
||||
def test_build_suggestions_prefers_ioniq_6_curvy_trim_for_mid_speed_turn_in(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "oversteer",
|
||||
"dimensionId": "oversteer:left:fast",
|
||||
"direction": "left",
|
||||
"speedBand": "fast",
|
||||
"severity": 1.0,
|
||||
"evidence": {"speedBand": "fast", "directionBias": "left", "eventCount": 2, "segments": [{"label": "route/4"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"}
|
||||
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current)
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "vehicle_knob"
|
||||
assert adjustment["symbol"] == "hyundai_ioniq_6.curvy_turn_in_trim_left"
|
||||
assert adjustment["suggested"] > adjustment["current"]
|
||||
|
||||
|
||||
def test_build_suggestions_rebases_friction_curve_against_active_override(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "center_chatter",
|
||||
"dimensionId": "center_chatter:center:highway",
|
||||
"direction": "center",
|
||||
"speedBand": "highway",
|
||||
"severity": 1.0,
|
||||
"evidence": {"speedBand": "highway", "directionBias": "center", "eventCount": 4, "segments": [{"label": "route/5"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {"richProfileKey": "torque_universal", "frictionFamily": "standard"}
|
||||
current_curve = [0.34, 0.35, 0.36, 0.37, 0.38]
|
||||
current = {
|
||||
"SteerLatAccel": 1.8,
|
||||
"SteerFriction": 0.2,
|
||||
"FTMActiveOverrides": {
|
||||
"schemaVersion": 1,
|
||||
"baseFrictionThresholds": {
|
||||
"standard": {
|
||||
"speedKnots": [0.0, 5.0, 10.0, 15.0, 25.0],
|
||||
"values": current_curve,
|
||||
},
|
||||
},
|
||||
"vehicleKnobs": {},
|
||||
},
|
||||
}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current)
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "friction_curve"
|
||||
assert adjustment["family"] == "standard"
|
||||
assert adjustment["current"] == current_curve
|
||||
assert adjustment["suggested"][2] > current_curve[2]
|
||||
|
||||
|
||||
def test_select_primary_tuning_path_prefers_baseline_for_broad_mismatch(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summaries = [
|
||||
{"bucket": "understeer", "severity": 1.0},
|
||||
{"bucket": "center_chatter", "severity": 0.9},
|
||||
{"bucket": "unwind_too_slow", "severity": 0.85},
|
||||
{"bucket": "saturation_limited", "severity": 0.8},
|
||||
]
|
||||
stats = {"meanErrorAbs": 0.16}
|
||||
|
||||
decision = module.select_primary_tuning_path(summaries, stats)
|
||||
assert decision["primaryPathKey"] == "baseline_fix"
|
||||
assert decision["alternatePathKey"] == "cleanup_pass"
|
||||
|
||||
|
||||
def test_select_primary_tuning_path_prefers_cleanup_for_localized_issue(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summaries = [
|
||||
{"bucket": "notchy_mid_curve", "severity": 0.7},
|
||||
{"bucket": "center_chatter", "severity": 0.55},
|
||||
]
|
||||
stats = {"meanErrorAbs": 0.07}
|
||||
|
||||
decision = module.select_primary_tuning_path(summaries, stats)
|
||||
assert decision["primaryPathKey"] == "cleanup_pass"
|
||||
assert decision["alternatePathKey"] == "baseline_fix"
|
||||
|
||||
|
||||
def test_build_trial_profiles_suppresses_ignored_dimensions(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
suggestions = [
|
||||
{
|
||||
"dimensionId": "center_chatter:center:highway",
|
||||
"primaryAdjustmentRaw": {
|
||||
"type": "friction_curve",
|
||||
"family": "standard",
|
||||
"current": [0.30, 0.31, 0.32, 0.33, 0.34],
|
||||
"suggested": [0.31, 0.32, 0.33, 0.34, 0.35],
|
||||
"delta": [0.01, 0.01, 0.01, 0.01, 0.01],
|
||||
},
|
||||
},
|
||||
{
|
||||
"dimensionId": "understeer:left:mid",
|
||||
"primaryAdjustmentRaw": {
|
||||
"type": "generic_param",
|
||||
"paramKey": "SteerLatAccel",
|
||||
"current": 1.6,
|
||||
"suggested": 1.7,
|
||||
"delta": 0.1,
|
||||
},
|
||||
},
|
||||
]
|
||||
feedback = {"acceptedDimensions": ["understeer:left:mid"], "ignoredDimensions": ["center_chatter:center:highway"]}
|
||||
profiles = module.build_trial_profiles("report-1", suggestions, feedback, {"richProfileKey": None})
|
||||
|
||||
assert profiles
|
||||
assert profiles[0]["genericParams"]["ForceAutoTuneOff"] is True
|
||||
assert profiles[0]["genericParams"]["SteerLatAccel"] > 1.6
|
||||
assert profiles[0]["ftmOverrides"] == {}
|
||||
|
||||
|
||||
def test_merge_primary_adjustments_averages_conflicting_deltas(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
suggestions = [
|
||||
{
|
||||
"severity": 1.0,
|
||||
"primaryAdjustmentRaw": {
|
||||
"type": "generic_param",
|
||||
"paramKey": "SteerLatAccel",
|
||||
"current": 1.6,
|
||||
"suggested": 1.7,
|
||||
"delta": 0.1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"severity": 0.5,
|
||||
"primaryAdjustmentRaw": {
|
||||
"type": "generic_param",
|
||||
"paramKey": "SteerLatAccel",
|
||||
"current": 1.6,
|
||||
"suggested": 1.55,
|
||||
"delta": -0.05,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
params_delta, overrides, _ = module._merge_primary_adjustments(suggestions, 1.0)
|
||||
assert params_delta["SteerLatAccel"] == pytest.approx(1.65, abs=1e-4)
|
||||
assert overrides == {}
|
||||
|
||||
|
||||
def test_apply_and_revert_trial_profile_round_trip(tmp_path):
|
||||
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-apply"
|
||||
profile_id = f"{report_id}:recommended"
|
||||
profile = {
|
||||
"id": profile_id,
|
||||
"reportId": report_id,
|
||||
"label": "Recommended",
|
||||
"description": "Recommended trial",
|
||||
"genericParams": {
|
||||
"AdvancedLateralTune": True,
|
||||
"SteerLatAccel": 1.9,
|
||||
"ForceAutoTuneOff": True,
|
||||
"ForceAutoTune": False,
|
||||
},
|
||||
"ftmOverrides": {
|
||||
"schemaVersion": 1,
|
||||
"baseFrictionThresholds": {},
|
||||
"vehicleKnobs": {
|
||||
"hyundai_ioniq_6.turn_in_boost_left": 0.08,
|
||||
},
|
||||
},
|
||||
}
|
||||
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
||||
|
||||
fake_params_cls._store = {
|
||||
"AdvancedLateralTune": False,
|
||||
"ForceAutoTune": True,
|
||||
"ForceAutoTuneOff": False,
|
||||
"SteerLatAccel": 1.5,
|
||||
"FTMActiveProfileId": "",
|
||||
"FTMActiveOverrides": {},
|
||||
"FTMTrialApplied": False,
|
||||
}
|
||||
|
||||
result = module.apply_trial_profile(report_id, profile_id)
|
||||
assert result["profile"]["id"] == profile_id
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
||||
assert fake_params_cls._store["FTMActiveProfileId"] == profile_id
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is True
|
||||
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
||||
|
||||
revert_result = module.revert_trial_profile()
|
||||
assert revert_result["snapshot"]["profileId"] == profile_id
|
||||
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
|
||||
|
||||
def test_delete_report_removes_saved_artifacts(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-delete"
|
||||
for path in (
|
||||
workspace["reports"] / f"{report_id}.json",
|
||||
workspace["reports"] / f"{report_id}.html",
|
||||
workspace["profiles"] / f"{report_id}.json",
|
||||
workspace["feedback"] / f"{report_id}.json",
|
||||
workspace["snapshots"] / f"{report_id}-recommended.json",
|
||||
):
|
||||
path.write_text("{}", encoding="utf-8")
|
||||
|
||||
result = module.delete_report(report_id)
|
||||
assert "Deleted tuning report" in result["message"]
|
||||
assert not (workspace["reports"] / f"{report_id}.json").exists()
|
||||
assert not (workspace["reports"] / f"{report_id}.html").exists()
|
||||
assert not (workspace["profiles"] / f"{report_id}.json").exists()
|
||||
assert not (workspace["feedback"] / f"{report_id}.json").exists()
|
||||
assert not (workspace["snapshots"] / f"{report_id}-recommended.json").exists()
|
||||
@@ -76,7 +76,7 @@ from openpilot.starpilot.common.testing_grounds import (
|
||||
)
|
||||
from openpilot.starpilot.navigation.destination_store import normalize_destination_payload, update_recent_destinations
|
||||
from openpilot.starpilot.system.the_galaxy.factory_reset import remove_path as _run_factory_reset_delete
|
||||
from openpilot.starpilot.system.the_galaxy import utilities
|
||||
from openpilot.starpilot.system.the_galaxy import ftm_workspace, utilities
|
||||
from openpilot.starpilot.system.the_galaxy.update_recovery import inspect_interrupted_update, public_recovery_status, recover_interrupted_update
|
||||
|
||||
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
@@ -5648,6 +5648,114 @@ def setup(app):
|
||||
**_serialize_lateral_maneuver_status(status),
|
||||
}), 200
|
||||
|
||||
@app.route("/api/ftm/status", methods=["GET"])
|
||||
def get_ftm_status():
|
||||
workspace = ftm_workspace.list_workspace()
|
||||
return jsonify({
|
||||
"isOnroad": params.get_bool("IsOnroad"),
|
||||
"status": ftm_workspace.read_ftm_status(),
|
||||
"activeTrial": workspace.get("activeTrial"),
|
||||
"reports": workspace.get("reports", [])[:10],
|
||||
}), 200
|
||||
|
||||
@app.route("/api/ftm/analyze", methods=["POST"])
|
||||
def start_ftm_analysis():
|
||||
if params.get_bool("IsOnroad"):
|
||||
return jsonify({"error": "FTM analysis can only run offroad."}), 409
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
route_names = [str(route).strip() for route in data.get("routes", []) if str(route).strip()]
|
||||
if not route_names:
|
||||
return jsonify({"error": "No routes were selected."}), 400
|
||||
|
||||
started = ftm_workspace.start_ftm_background_analysis(route_names, FOOTAGE_PATHS)
|
||||
if not started:
|
||||
return jsonify({"error": "Failed to start FTM analysis."}), 500
|
||||
|
||||
return jsonify({
|
||||
"message": f"Started FTM analysis for {len(route_names[:ftm_workspace.FTM_ANALYZER_ROUTE_LIMIT])} route(s).",
|
||||
"status": ftm_workspace.read_ftm_status(),
|
||||
}), 200
|
||||
|
||||
@app.route("/api/ftm/analyze/stop", methods=["POST"])
|
||||
def stop_ftm_analysis():
|
||||
stopped = ftm_workspace.stop_ftm_background_analysis()
|
||||
return jsonify({
|
||||
"message": "Stopped FTM analysis." if stopped else "No active FTM analysis was running.",
|
||||
"stopped": bool(stopped),
|
||||
"status": ftm_workspace.read_ftm_status(),
|
||||
}), 200
|
||||
|
||||
@app.route("/api/ftm/report/<report_id>", methods=["GET"])
|
||||
def get_ftm_report(report_id):
|
||||
try:
|
||||
return jsonify(ftm_workspace.load_report(report_id)), 200
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "FTM report not found."}), 404
|
||||
|
||||
@app.route("/api/ftm/report/<report_id>", methods=["DELETE"])
|
||||
def delete_ftm_report(report_id):
|
||||
try:
|
||||
return jsonify(ftm_workspace.delete_report(report_id)), 200
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "FTM report not found."}), 404
|
||||
except RuntimeError as error:
|
||||
return jsonify({"error": str(error)}), 409
|
||||
|
||||
@app.route("/api/ftm/workspace", methods=["GET"])
|
||||
def get_ftm_workspace():
|
||||
return jsonify(ftm_workspace.list_workspace()), 200
|
||||
|
||||
@app.route("/api/ftm/workspace/clear", methods=["POST"])
|
||||
def clear_ftm_workspace():
|
||||
try:
|
||||
return jsonify(ftm_workspace.clear_workspace()), 200
|
||||
except RuntimeError as error:
|
||||
return jsonify({"error": str(error)}), 409
|
||||
|
||||
@app.route("/api/ftm/trials/apply", methods=["POST"])
|
||||
def apply_ftm_trial():
|
||||
data = request.get_json(silent=True) or {}
|
||||
report_id = str(data.get("reportId") or "").strip()
|
||||
profile_id = str(data.get("profileId") or "").strip()
|
||||
if not report_id or not profile_id:
|
||||
return jsonify({"error": "Both reportId and profileId are required."}), 400
|
||||
|
||||
try:
|
||||
result = ftm_workspace.apply_trial_profile(report_id, profile_id)
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "FTM profile not found."}), 404
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route("/api/ftm/trials/revert", methods=["POST"])
|
||||
def revert_ftm_trial():
|
||||
try:
|
||||
result = ftm_workspace.revert_trial_profile()
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "No active FTM trial snapshot was found."}), 404
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route("/api/ftm/feedback", methods=["POST"])
|
||||
def save_ftm_feedback():
|
||||
data = request.get_json(silent=True) or {}
|
||||
report_id = str(data.get("reportId") or "").strip()
|
||||
if not report_id:
|
||||
return jsonify({"error": "reportId is required."}), 400
|
||||
|
||||
feedback = {
|
||||
"acceptedDimensions": data.get("acceptedDimensions", []),
|
||||
"ignoredDimensions": data.get("ignoredDimensions", []),
|
||||
"notes": data.get("notes", ""),
|
||||
}
|
||||
try:
|
||||
result = ftm_workspace.record_feedback(report_id, feedback)
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "FTM report not found."}), 404
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route("/api/update/fast/status", methods=["GET"])
|
||||
def get_fast_update_status():
|
||||
state_data = _get_fast_update_state()
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.starpilot.system.the_galaxy import ftm_workspace
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run the Firestar Tuning Method analyzer against local routes.")
|
||||
parser.add_argument("routes", nargs="+", help="One or more local route ids.")
|
||||
parser.add_argument("--report-id", dest="report_id", default=None, help="Optional report id override.")
|
||||
args = parser.parse_args()
|
||||
|
||||
footage_paths = [
|
||||
str(Paths.log_root(HD=True, raw=True)),
|
||||
str(Paths.log_root(konik=True, raw=True)),
|
||||
str(Paths.log_root(raw=True)),
|
||||
]
|
||||
report = ftm_workspace.analyze_routes(args.routes, footage_paths, report_id=args.report_id)
|
||||
print(json.dumps({
|
||||
"reportId": report["reportId"],
|
||||
"htmlPath": report["htmlPath"],
|
||||
"jsonPath": report["jsonPath"],
|
||||
}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user