This commit is contained in:
firestar5683
2026-07-13 14:56:23 -05:00
parent d305071351
commit 4a98785aa3
24 changed files with 1030 additions and 803 deletions
Binary file not shown.
+4 -4
View File
@@ -319,10 +319,10 @@ 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}},
{"FTMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"FTMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}},
{"FLMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
{"FLMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
{"FLMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
{"FLMTrialApplied", {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.
@@ -106,6 +106,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--consistent-detections", type=int, help="Override matching reads required for an initial publication.")
parser.add_argument("--change-consistent-detections", type=int, help="Override matching reads required to change a publication.")
parser.add_argument("--change-single-read-min-confidence", type=float, help="Override confidence for a one-read speed change.")
parser.add_argument("--change-repeat-min-confidence", type=float, help="Override confidence required from the confirming speed-change read.")
parser.add_argument("--max-cases", type=int, default=0, help="Optional evaluation cap after deduplication.")
return parser.parse_args()
@@ -272,6 +273,8 @@ def main() -> int:
slv.CHANGE_CONSISTENT_DETECTIONS = args.change_consistent_detections
if args.change_single_read_min_confidence is not None:
slv.CHANGE_SINGLE_READ_MIN_CONFIDENCE = args.change_single_read_min_confidence
if args.change_repeat_min_confidence is not None:
slv.CHANGE_REPEAT_MIN_CONFIDENCE = args.change_repeat_min_confidence
cases = load_cases(queue_path, labels_path, args.dedupe_seconds)
if args.focus_eval_csv:
with args.focus_eval_csv.expanduser().resolve().open(encoding="utf-8", newline="") as input_file:
@@ -397,6 +400,7 @@ def main() -> int:
"low_speed_change_consistent_detections": slv.LOW_SPEED_CHANGE_CONSISTENT_DETECTIONS,
"low_speed_change_min_confidence": slv.LOW_SPEED_CHANGE_MIN_CONFIDENCE,
"change_single_read_min_confidence": slv.CHANGE_SINGLE_READ_MIN_CONFIDENCE,
"change_repeat_min_confidence": slv.CHANGE_REPEAT_MIN_CONFIDENCE,
"low_speed_change_allow_strong_consensus": slv.LOW_SPEED_CHANGE_ALLOW_STRONG_CONSENSUS,
"strong_model_consensus_enabled": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_CONSENSUS_ENABLED,
"strong_model_min_proposal_confidence": slv.DETECTOR_CLASSIFIER_STRONG_MODEL_MIN_PROPOSAL_CONFIDENCE,
+15 -15
View File
@@ -90,7 +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)
self.flm_surface_profile_key = get_flm_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
@@ -158,10 +158,10 @@ 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
ftm_profile_active = bool(getattr(starpilot_toggles, "ftm_trial_applied", False) and
getattr(starpilot_toggles, "ftm_active_profile_id", ""))
set_ftm_runtime_overrides(getattr(starpilot_toggles, "ftm_active_overrides", None) if ftm_profile_active else None)
ftm_surface_active = ftm_profile_active and ftm_runtime_overrides_active()
flm_profile_active = bool(getattr(starpilot_toggles, "flm_trial_applied", False) and
getattr(starpilot_toggles, "flm_active_profile_id", ""))
set_flm_runtime_overrides(getattr(starpilot_toggles, "flm_active_overrides", None) if flm_profile_active else None)
flm_surface_active = flm_profile_active and flm_runtime_overrides_active()
if not active:
output_torque = 0.0
pid_log.active = False
@@ -329,15 +329,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 ftm_surface_active and 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,
if flm_surface_active and self.flm_surface_profile_key and not ioniq_6_active:
universal_flm_profile = self.flm_surface_profile_key == FLM_UNIVERSAL_PROFILE_KEY
flm_full_surface_center_taper = get_flm_full_surface_center_taper_scale(self.flm_surface_profile_key, setpoint, CS.vEgo,
include_base_center=universal_flm_profile)
ff *= get_flm_full_surface_ff_scale(self.flm_surface_profile_key, setpoint, desired_lateral_jerk, CS.vEgo,
include_base_ff=universal_flm_profile) * flm_full_surface_center_taper
friction_threshold = get_flm_full_surface_friction_threshold(self.flm_surface_profile_key, friction_threshold, CS.vEgo,
setpoint, desired_lateral_jerk,
include_base_threshold=universal_ftm_profile)
include_base_threshold=universal_flm_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)
@@ -367,10 +367,10 @@ 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 ftm_surface_active and self.ftm_surface_profile_key and not CS.steeringPressed:
elif flm_surface_active and self.flm_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,
output_torque = get_flm_full_surface_low_speed_angle_assist_torque(self.flm_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)
+160 -160
View File
@@ -11,8 +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]
FLM_SCHEMA_VERSION = 1
FLM_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
@@ -695,8 +695,8 @@ 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 = {}
_FLM_ACTIVE_OVERRIDES_TEXT = ""
_FLM_ACTIVE_OVERRIDES = {}
def _sigmoid(x: float) -> float:
@@ -720,11 +720,11 @@ 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):
def _flm_copy_json(value):
return json.loads(json.dumps(value))
def normalize_ftm_overrides(overrides) -> dict:
def normalize_flm_overrides(overrides) -> dict:
if overrides in (None, "", b""):
return {}
@@ -744,7 +744,7 @@ def normalize_ftm_overrides(overrides) -> dict:
return {}
normalized = {
"schemaVersion": FTM_SCHEMA_VERSION,
"schemaVersion": FLM_SCHEMA_VERSION,
"baseFrictionThresholds": {},
"vehicleKnobs": {},
}
@@ -752,11 +752,11 @@ def normalize_ftm_overrides(overrides) -> dict:
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):
if not isinstance(values, (list, tuple)) or len(values) != len(FLM_FRICTION_SPEED_KNOTS):
continue
try:
normalized["baseFrictionThresholds"][family] = {
"speedKnots": list(FTM_FRICTION_SPEED_KNOTS),
"speedKnots": list(FLM_FRICTION_SPEED_KNOTS),
"values": [float(v) for v in values],
}
except Exception:
@@ -776,55 +776,55 @@ def normalize_ftm_overrides(overrides) -> dict:
return normalized
def set_ftm_runtime_overrides(overrides) -> None:
global _FTM_ACTIVE_OVERRIDES, _FTM_ACTIVE_OVERRIDES_TEXT
def set_flm_runtime_overrides(overrides) -> None:
global _FLM_ACTIVE_OVERRIDES, _FLM_ACTIVE_OVERRIDES_TEXT
normalized = normalize_ftm_overrides(overrides)
normalized = normalize_flm_overrides(overrides)
text = json.dumps(normalized, sort_keys=True, separators=(",", ":")) if normalized else ""
if text == _FTM_ACTIVE_OVERRIDES_TEXT:
if text == _FLM_ACTIVE_OVERRIDES_TEXT:
return
_FTM_ACTIVE_OVERRIDES_TEXT = text
_FTM_ACTIVE_OVERRIDES = normalized
_FLM_ACTIVE_OVERRIDES_TEXT = text
_FLM_ACTIVE_OVERRIDES = normalized
def clear_ftm_runtime_overrides() -> None:
set_ftm_runtime_overrides({})
def clear_flm_runtime_overrides() -> None:
set_flm_runtime_overrides({})
def get_ftm_runtime_overrides() -> dict:
return _ftm_copy_json(_FTM_ACTIVE_OVERRIDES) if _FTM_ACTIVE_OVERRIDES else {}
def get_flm_runtime_overrides() -> dict:
return _flm_copy_json(_FLM_ACTIVE_OVERRIDES) if _FLM_ACTIVE_OVERRIDES else {}
def ftm_runtime_overrides_active() -> bool:
return bool(_FTM_ACTIVE_OVERRIDES)
def flm_runtime_overrides_active() -> bool:
return bool(_FLM_ACTIVE_OVERRIDES)
def _ftm_base_friction_threshold(family: str, v_ego: float, default_fn) -> float:
payload = _FTM_ACTIVE_OVERRIDES.get("baseFrictionThresholds", {}).get(family, {})
def _flm_base_friction_threshold(family: str, v_ego: float, default_fn) -> float:
payload = _FLM_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))
if isinstance(values, list) and len(values) == len(FLM_FRICTION_SPEED_KNOTS):
return float(np.interp(v_ego, FLM_FRICTION_SPEED_KNOTS, values))
return float(default_fn(v_ego))
def _ftm_vehicle_knob(name: str, default_value: float) -> float:
def _flm_vehicle_knob(name: str, default_value: float) -> float:
try:
return float(_FTM_ACTIVE_OVERRIDES.get("vehicleKnobs", {}).get(name, default_value))
return float(_FLM_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)
return _flm_base_friction_threshold("gm", v_ego, _gm_base_friction_threshold_default)
def get_standard_friction_threshold(v_ego: float) -> float:
return _ftm_base_friction_threshold("standard", v_ego, _standard_friction_threshold_default)
return _flm_base_friction_threshold("standard", v_ego, _standard_friction_threshold_default)
def get_hkg_canfd_base_friction_threshold(v_ego: float) -> float:
return _ftm_base_friction_threshold("hkg_canfd", v_ego, _hkg_canfd_base_friction_threshold_default)
return _flm_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:
@@ -870,8 +870,8 @@ def get_prius_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: float
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),
_flm_vehicle_knob("toyota_prius.ff_gain_left", PRIUS_FF_GAIN_LEFT),
_flm_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)
@@ -883,14 +883,14 @@ def get_prius_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: float
low_speed_factor = _prius_low_speed_factor(v_ego)
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),
_flm_vehicle_knob("toyota_prius.turn_in_boost_left", PRIUS_TURN_IN_BOOST_LEFT),
_flm_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,
_ftm_vehicle_knob("toyota_prius.unwind_taper_left", PRIUS_UNWIND_TAPER_LEFT),
_ftm_vehicle_knob("toyota_prius.unwind_taper_right", PRIUS_UNWIND_TAPER_RIGHT),
_flm_vehicle_knob("toyota_prius.unwind_taper_left", PRIUS_UNWIND_TAPER_LEFT),
_flm_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))
@@ -904,14 +904,14 @@ def get_prius_friction_threshold(v_ego: float, desired_lateral_accel: float = 0.
unwind_weight = max(-phase, 0.0)
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),
_flm_vehicle_knob("toyota_prius.turn_in_threshold_reduction_left", PRIUS_TURN_IN_THRESHOLD_REDUCTION_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("toyota_prius.unwind_threshold_increase_left", PRIUS_UNWIND_THRESHOLD_INCREASE_LEFT),
_flm_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)
@@ -933,7 +933,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 = _ftm_vehicle_knob("toyota_prius.center_taper_max", PRIUS_CENTER_TAPER_MAX) * speed_weight * center_weight
reduction = _flm_vehicle_knob("toyota_prius.center_taper_max", PRIUS_CENTER_TAPER_MAX) * speed_weight * center_weight
return 1.0 - reduction
@@ -1241,8 +1241,8 @@ def get_bolt_2022_2023_ff_scale(desired_lateral_accel: float, desired_lateral_je
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),
_flm_vehicle_knob("gm_bolt_2022_2023.ff_gain_left", BOLT_2022_2023_FF_GAIN_LEFT),
_flm_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)
@@ -1250,7 +1250,7 @@ def get_bolt_2022_2023_ff_scale(desired_lateral_accel: float, desired_lateral_je
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 - (_ftm_vehicle_knob("gm_bolt_2022_2023.center_taper_max", BOLT_2022_2023_CENTER_TAPER_MAX) * speed_weight * center_weight)
center_taper = 1.0 - (_flm_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)
@@ -1258,15 +1258,15 @@ def get_bolt_2022_2023_ff_scale(desired_lateral_accel: float, desired_lateral_je
unwind_weight = max(-phase, 0.0)
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),
_flm_vehicle_knob("gm_bolt_2022_2023.turn_in_boost_left", BOLT_2022_2023_TURN_IN_BOOST_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("gm_bolt_2022_2023.unwind_taper_left", BOLT_2022_2023_UNWIND_TAPER_LEFT),
_flm_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))
@@ -1280,14 +1280,14 @@ def get_bolt_2022_2023_friction_threshold(v_ego: float, desired_lateral_accel: f
unwind_weight = max(-phase, 0.0)
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),
_flm_vehicle_knob("gm_bolt_2022_2023.turn_in_threshold_reduction_left", BOLT_2022_2023_TURN_IN_THRESHOLD_REDUCTION_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("gm_bolt_2022_2023.unwind_threshold_increase_left", BOLT_2022_2023_UNWIND_THRESHOLD_INCREASE_LEFT),
_flm_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)
@@ -1954,16 +1954,16 @@ def _ioniq_6_transition_envelope(v_ego: float, desired_lateral_accel: float, des
def _ioniq_6_curvy_speed_weight(v_ego: float) -> float:
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)
curvy_speed_min = _flm_vehicle_knob("hyundai_ioniq_6.curvy_speed_min", IONIQ_6_CURVY_SPEED_MIN)
curvy_speed_max = _flm_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:
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)
curvy_turn_in_speed_min = _flm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_speed_min", IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN)
curvy_turn_in_speed_max = _flm_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
@@ -1976,8 +1976,8 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
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),
_flm_vehicle_knob("hyundai_ioniq_6.ff_gain_left", IONIQ_6_FF_GAIN_LEFT),
_flm_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)
@@ -1989,14 +1989,14 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
low_speed_factor = _ioniq_6_low_speed_factor(v_ego)
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),
_flm_vehicle_knob("hyundai_ioniq_6.turn_in_boost_left", IONIQ_6_TURN_IN_BOOST_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("hyundai_ioniq_6.unwind_taper_left", IONIQ_6_UNWIND_TAPER_LEFT),
_flm_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
@@ -2007,8 +2007,8 @@ def get_ioniq_6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
IONIQ_6_CRAWL_TURN_IN_FF_LAT_WIDTH)
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),
_flm_vehicle_knob("hyundai_ioniq_6.crawl_turn_in_ff_boost_left", IONIQ_6_CRAWL_TURN_IN_FF_BOOST_LEFT),
_flm_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:
@@ -2034,14 +2034,14 @@ def get_ioniq_6_friction_threshold(v_ego: float, desired_lateral_accel: float =
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,
_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),
_flm_vehicle_knob("hyundai_ioniq_6.turn_in_threshold_reduction_left", IONIQ_6_TURN_IN_THRESHOLD_REDUCTION_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("hyundai_ioniq_6.unwind_threshold_increase_left", IONIQ_6_UNWIND_THRESHOLD_INCREASE_LEFT),
_flm_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)
@@ -2070,12 +2070,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 = _ftm_vehicle_knob("hyundai_ioniq_6.center_taper_max", IONIQ_6_CENTER_TAPER_MAX) * speed_weight * center_weight
high_speed_reduction = _flm_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 = _ftm_vehicle_knob("hyundai_ioniq_6.highway_center_taper_max", IONIQ_6_HIGHWAY_CENTER_TAPER_MAX) * highway_speed_weight * highway_center_weight
highway_center_reduction = _flm_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)
@@ -2123,8 +2123,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,
_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)) *
_flm_vehicle_knob("hyundai_ioniq_6.curvy_turn_in_trim_left", IONIQ_6_CURVY_TURN_IN_TRIM_LEFT),
_flm_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
@@ -2136,12 +2136,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,
_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)) *
_flm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_floor_relief_left", IONIQ_6_CURVY_UNWIND_FLOOR_RELIEF_LEFT),
_flm_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,
_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)) *
_flm_vehicle_knob("hyundai_ioniq_6.curvy_unwind_extra_reduction_left", IONIQ_6_CURVY_UNWIND_EXTRA_REDUCTION_LEFT),
_flm_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
@@ -2191,7 +2191,7 @@ def get_ioniq_6_low_speed_angle_assist_torque(desired_angle_deg: float, actual_a
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(
_ftm_vehicle_knob("hyundai_ioniq_6.low_speed_angle_assist_max_torque", IONIQ_6_LOW_SPEED_ANGLE_ASSIST_MAX_TORQUE) *
_flm_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,
)
@@ -2254,8 +2254,8 @@ def get_kia_ev6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
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),
_flm_vehicle_knob("hyundai_kia_ev6.ff_gain_left", KIA_EV6_FF_GAIN_LEFT),
_flm_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)
@@ -2267,14 +2267,14 @@ def get_kia_ev6_ff_scale(desired_lateral_accel: float, desired_lateral_jerk: flo
low_speed_factor = _kia_ev6_low_speed_factor(v_ego)
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),
_flm_vehicle_knob("hyundai_kia_ev6.turn_in_boost_left", KIA_EV6_TURN_IN_BOOST_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("hyundai_kia_ev6.unwind_taper_left", KIA_EV6_UNWIND_TAPER_LEFT),
_flm_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))
@@ -2288,14 +2288,14 @@ def get_kia_ev6_friction_threshold(v_ego: float, desired_lateral_accel: float =
unwind_weight = max(-phase, 0.0)
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),
_flm_vehicle_knob("hyundai_kia_ev6.turn_in_threshold_reduction_left", KIA_EV6_TURN_IN_THRESHOLD_REDUCTION_LEFT),
_flm_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,
_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),
_flm_vehicle_knob("hyundai_kia_ev6.unwind_threshold_increase_left", KIA_EV6_UNWIND_THRESHOLD_INCREASE_LEFT),
_flm_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)
@@ -2317,7 +2317,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 = _ftm_vehicle_knob("hyundai_kia_ev6.center_taper_max", KIA_EV6_CENTER_TAPER_MAX) * speed_weight * center_weight
reduction = _flm_vehicle_knob("hyundai_kia_ev6.center_taper_max", KIA_EV6_CENTER_TAPER_MAX) * speed_weight * center_weight
return 1.0 - reduction
@@ -2362,9 +2362,9 @@ 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"
FLM_UNIVERSAL_PROFILE_KEY = "torque_universal"
FTM_FULL_SURFACE_SUFFIX_METADATA = {
FLM_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},
@@ -2392,7 +2392,7 @@ FTM_FULL_SURFACE_SUFFIX_METADATA = {
"curvy_unwind_extra_reduction_right": {"min": 0.0, "max": 0.45, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
}
FTM_FULL_SURFACE_NEUTRAL_DEFAULTS = {
FLM_FULL_SURFACE_NEUTRAL_DEFAULTS = {
"ff_gain_left": 0.0,
"ff_gain_right": 0.0,
"turn_in_boost_left": 0.0,
@@ -2421,148 +2421,148 @@ FTM_FULL_SURFACE_NEUTRAL_DEFAULTS = {
}
def _ftm_profile_symbol(profile_key: str, suffix: str) -> str:
def _flm_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:
def flm_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
return _flm_profile_symbol(profile_key, suffix) in FLM_SUPPORTED_VEHICLE_KNOBS
def _ftm_full_surface_side_value(profile_key: str, desired_lateral_accel: float,
def _flm_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)
return _flm_vehicle_knob(_flm_profile_symbol(profile_key, f"{suffix}_left"), left_default)
return _flm_vehicle_knob(_flm_profile_symbol(profile_key, f"{suffix}_right"), right_default)
def _ftm_full_surface_low_speed_factor(v_ego: float) -> float:
def _flm_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:
def _flm_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:
def _flm_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
return _flm_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)
def _flm_full_surface_curvy_speed_weight(profile_key: str, v_ego: float) -> float:
curvy_speed_min = _flm_vehicle_knob(_flm_profile_symbol(profile_key, "curvy_speed_min"), IONIQ_6_CURVY_SPEED_MIN)
curvy_speed_max = _flm_vehicle_knob(_flm_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)
def _flm_full_surface_curvy_turn_in_trim_speed_weight(profile_key: str, v_ego: float) -> float:
curvy_turn_in_speed_min = _flm_vehicle_knob(_flm_profile_symbol(profile_key, "curvy_turn_in_trim_speed_min"), IONIQ_6_CURVY_TURN_IN_TRIM_SPEED_MIN)
curvy_turn_in_speed_max = _flm_vehicle_knob(_flm_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,
def get_flm_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"):
if include_base_center and flm_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
reduction += _flm_vehicle_knob(_flm_profile_symbol(profile_key, "center_taper_max"), 0.0) * speed_weight * center_weight
if ftm_profile_supports_knob(profile_key, "highway_center_taper_max"):
if flm_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
reduction += _flm_vehicle_knob(_flm_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,
def get_flm_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)
phase = _flm_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)
low_speed_factor = _flm_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_speed_weight = _flm_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_speed_weight = _flm_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")
if include_base_ff and flm_profile_supports_knob(profile_key, "ff_gain_left"):
gain = _flm_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
turn_in_boost = 1.0 + (_flm_full_surface_side_value(profile_key, desired_lateral_accel, "turn_in_boost") * turn_in_weight * low_speed_factor)
unwind_reduction = _flm_full_surface_side_value(profile_key, desired_lateral_accel, "unwind_taper") * unwind_weight * (0.30 + 0.70 * low_speed_factor)
curvy_unwind_extra = _flm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_extra_reduction") * curvy_unwind_weight
curvy_unwind_floor_relief = _flm_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
curvy_unwind_extra = _flm_full_surface_side_value(profile_key, desired_lateral_accel, "curvy_unwind_extra_reduction") * curvy_unwind_weight
curvy_unwind_floor_relief = _flm_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
if flm_profile_supports_knob(profile_key, "curvy_turn_in_trim_left"):
curvy_trim = _flm_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:
if flm_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
scale += _flm_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,
def get_flm_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)
transition_envelope = _flm_full_surface_transition_envelope(v_ego, desired_lateral_accel, desired_lateral_jerk)
phase = _flm_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") *
threshold_scale -= (_flm_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") *
threshold_scale += (_flm_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,
def get_flm_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"):
if not profile_key or not flm_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)
max_torque = _flm_vehicle_knob(_flm_profile_symbol(profile_key, "low_speed_angle_assist_max_torque"), 0.0)
if max_torque <= 1e-4:
return current_output_torque
@@ -2593,22 +2593,22 @@ def get_ftm_full_surface_low_speed_angle_assist_torque(profile_key: str | None,
return float(np.clip(current_output_torque + assist_torque, -1.0, 1.0))
FTM_RICH_PROFILE_CARS = {
FLM_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 = {
FLM_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",
FLM_UNIVERSAL_PROFILE_KEY: "Torque Controller",
}
FTM_SUPPORTED_VEHICLE_KNOBS = {
FLM_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},
@@ -2670,16 +2670,16 @@ FTM_SUPPORTED_VEHICLE_KNOBS = {
}
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)
def _add_flm_full_surface_profile_knobs(profile_key: str, defaults: dict[str, float] | None = None) -> None:
knob_defaults = dict(FLM_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:
for suffix, meta in FLM_FULL_SURFACE_SUFFIX_METADATA.items():
symbol = _flm_profile_symbol(profile_key, suffix)
if symbol in FLM_SUPPORTED_VEHICLE_KNOBS:
continue
FTM_SUPPORTED_VEHICLE_KNOBS[symbol] = {
FLM_SUPPORTED_VEHICLE_KNOBS[symbol] = {
"profile": profile_key,
"min": meta["min"],
"max": meta["max"],
@@ -2690,30 +2690,30 @@ def _add_ftm_full_surface_profile_knobs(profile_key: str, defaults: dict[str, fl
}
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)
for _flm_profile_key in ("gm_bolt_2022_2023", "hyundai_ioniq_6", "hyundai_kia_ev6", "toyota_prius", FLM_UNIVERSAL_PROFILE_KEY):
_add_flm_full_surface_profile_knobs(_flm_profile_key)
def get_ftm_supported_vehicle_knobs() -> dict:
return _ftm_copy_json(FTM_SUPPORTED_VEHICLE_KNOBS)
def get_flm_supported_vehicle_knobs() -> dict:
return _flm_copy_json(FLM_SUPPORTED_VEHICLE_KNOBS)
def get_ftm_rich_profile_key(car_fingerprint) -> str | None:
for profile_key, cars in FTM_RICH_PROFILE_CARS.items():
def get_flm_rich_profile_key(car_fingerprint) -> str | None:
for profile_key, cars in FLM_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)
def get_flm_surface_profile_key(car_fingerprint, torque_control: bool = True) -> str | None:
profile_key = get_flm_rich_profile_key(car_fingerprint)
if profile_key is not None or not torque_control:
return profile_key
return FTM_UNIVERSAL_PROFILE_KEY
return FLM_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)
def get_flm_capabilities(car_fingerprint, brand: str = "", hyundai_canfd: bool = False, torque_control: bool = True) -> dict:
profile_key = get_flm_surface_profile_key(car_fingerprint, torque_control=torque_control)
if hyundai_canfd:
friction_family = "hkg_canfd"
elif brand == "gm":
@@ -2732,14 +2732,14 @@ def get_ftm_capabilities(car_fingerprint, brand: str = "", hyundai_canfd: bool =
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]
rich_knobs = [name for name, meta in FLM_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),
"richProfileLabel": FLM_RICH_PROFILE_LABELS.get(profile_key),
"richKnobs": rich_knobs,
}
+28 -28
View File
@@ -20,11 +20,11 @@ from openpilot.selfdrive.controls.lib.latcontrol_pid import (
get_civic_bosch_modified_pid_output_scale,
)
from openpilot.selfdrive.controls.lib.latcontrol_vehicle_tunes import (
clear_ftm_runtime_overrides,
get_ftm_runtime_overrides,
clear_flm_runtime_overrides,
get_flm_runtime_overrides,
get_hkg_canfd_base_friction_threshold,
normalize_ftm_overrides,
set_ftm_runtime_overrides,
normalize_flm_overrides,
set_flm_runtime_overrides,
)
from openpilot.selfdrive.controls.lib.latcontrol_torque import (
get_civic_bosch_modified_a_center_taper_scale,
@@ -264,9 +264,9 @@ class TestLatControl:
assert unwind_left < steady_left
assert unwind_right < steady_right
def test_ftm_standard_friction_curve_override(self):
def test_flm_standard_friction_curve_override(self):
base = get_standard_friction_threshold(10.0)
overrides = normalize_ftm_overrides({
overrides = normalize_flm_overrides({
"baseFrictionThresholds": {
"standard": {
"values": [0.30, 0.31, 0.32, 0.33, 0.34],
@@ -274,54 +274,54 @@ class TestLatControl:
},
})
try:
set_ftm_runtime_overrides(overrides)
assert get_ftm_runtime_overrides()["baseFrictionThresholds"]["standard"]["values"] == [0.30, 0.31, 0.32, 0.33, 0.34]
set_flm_runtime_overrides(overrides)
assert get_flm_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() == {}
clear_flm_runtime_overrides()
assert get_flm_runtime_overrides() == {}
assert get_standard_friction_threshold(10.0) == pytest.approx(base)
def test_ftm_vehicle_knob_override_ioniq6_center_taper(self):
def test_flm_vehicle_knob_override_ioniq6_center_taper(self):
baseline = get_ioniq_6_center_taper_scale(0.0, 32.0)
overrides = normalize_ftm_overrides({
overrides = normalize_flm_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)
set_flm_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()
clear_flm_runtime_overrides()
@pytest.mark.parametrize(("trial_applied", "profile_id"), [(False, "profile"), (True, "")])
def test_ftm_surface_helpers_require_active_trial(self, monkeypatch, trial_applied, profile_id):
def test_flm_surface_helpers_require_active_trial(self, monkeypatch, trial_applied, profile_id):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(GM.CHEVROLET_BOLT_ACC_2022_2023)
starpilot_toggles.ftm_trial_applied = trial_applied
starpilot_toggles.ftm_active_profile_id = profile_id
starpilot_toggles.ftm_active_overrides = {
starpilot_toggles.flm_trial_applied = trial_applied
starpilot_toggles.flm_active_profile_id = profile_id
starpilot_toggles.flm_active_overrides = {
"vehicleKnobs": {"gm_bolt_2022_2023.highway_center_taper_max": 0.10},
}
def fail_if_called(*_args, **_kwargs):
raise AssertionError("inactive FTM trial reached the runtime shaping path")
raise AssertionError("inactive FLM trial reached the runtime shaping path")
monkeypatch.setattr(latcontrol_torque, "get_ftm_full_surface_ff_scale", fail_if_called)
monkeypatch.setattr(latcontrol_torque, "get_flm_full_surface_ff_scale", fail_if_called)
controller.update(True, CS, VM, params, False, 0.0025, False, 0.2, None, None, starpilot_toggles)
assert get_ftm_runtime_overrides() == {}
assert get_flm_runtime_overrides() == {}
def test_ftm_surface_helpers_run_for_active_trial(self, monkeypatch):
def test_flm_surface_helpers_run_for_active_trial(self, monkeypatch):
controller, VM, CS, params, starpilot_toggles = self._build_torque_controller(GM.CHEVROLET_BOLT_ACC_2022_2023)
starpilot_toggles.ftm_trial_applied = True
starpilot_toggles.ftm_active_profile_id = "report:cleanup:recommended"
starpilot_toggles.ftm_active_overrides = {
starpilot_toggles.flm_trial_applied = True
starpilot_toggles.flm_active_profile_id = "report:cleanup:recommended"
starpilot_toggles.flm_active_overrides = {
"vehicleKnobs": {"gm_bolt_2022_2023.highway_center_taper_max": 0.10},
}
calls = []
@@ -330,13 +330,13 @@ class TestLatControl:
calls.append(True)
return 1.0
monkeypatch.setattr(latcontrol_torque, "get_ftm_full_surface_ff_scale", record_call)
monkeypatch.setattr(latcontrol_torque, "get_flm_full_surface_ff_scale", record_call)
try:
controller.update(True, CS, VM, params, False, 0.0025, False, 0.2, None, None, starpilot_toggles)
assert calls
assert get_ftm_runtime_overrides()["vehicleKnobs"]["gm_bolt_2022_2023.highway_center_taper_max"] == pytest.approx(0.10)
assert get_flm_runtime_overrides()["vehicleKnobs"]["gm_bolt_2022_2023.highway_center_taper_max"] == pytest.approx(0.10)
finally:
clear_ftm_runtime_overrides()
clear_flm_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)
@@ -14,7 +14,7 @@ METRIC_MARGIN = 12
FONT_SIZE = 35
METER_TO_FOOT = 3.28084
_WHITE_DIM = rl.Color(255, 255, 255, 85)
_FTM_OVERRIDE_COLOR = rl.Color(239, 68, 68, 255)
_FLM_OVERRIDE_COLOR = rl.Color(239, 68, 68, 255)
def parse_hex_color(hex_str: str, default_color=rl.WHITE) -> rl.Color:
if not hex_str:
@@ -56,7 +56,7 @@ class DeveloperSidebar:
self._cached_metrics = [0] * 7
self._cached_force_auto_tune_off = False
self._cached_force_auto_tune = False
self._cached_ftm_trial_applied = False
self._cached_flm_trial_applied = False
self._cached_friction_stock = 0.0
self._cached_friction = 0.0
self._cached_lat_stock = 0.0
@@ -73,7 +73,7 @@ class DeveloperSidebar:
self._visible = False
self._metric_color = rl.WHITE
self._active_ids: list[int] = []
self._ftm_override_metric_ids: set[int] = set()
self._flm_override_metric_ids: set[int] = set()
self._metrics: dict[int, tuple[str, str]] = {}
@property
@@ -97,7 +97,7 @@ class DeveloperSidebar:
self._cached_metrics = [self._params.get_int(f"DeveloperSidebarMetric{i}") for i in range(1, 8)]
self._cached_force_auto_tune_off = self._params.get_bool("ForceAutoTuneOff")
self._cached_force_auto_tune = self._params.get_bool("ForceAutoTune")
self._cached_ftm_trial_applied = self._params.get_bool("FTMTrialApplied")
self._cached_flm_trial_applied = self._params.get_bool("FLMTrialApplied")
self._cached_friction_stock = self._params.get_float("SteerFrictionStock")
self._cached_friction = self._params.get_float("SteerFriction")
self._cached_lat_stock = self._params.get_float("SteerLatAccelStock")
@@ -222,15 +222,15 @@ class DeveloperSidebar:
use_custom_friction = bool(ui_state.starpilot_toggles.get("use_custom_friction", force_auto_tune_off))
use_custom_lat_factor = bool(ui_state.starpilot_toggles.get("use_custom_latAccelFactor", force_auto_tune_off))
self._ftm_override_metric_ids = set()
if self._cached_ftm_trial_applied:
ftm_metric_flags = {
self._flm_override_metric_ids = set()
if self._cached_flm_trial_applied:
flm_metric_flags = {
3: bool(ui_state.starpilot_toggles.get("use_custom_steerActuatorDelay", False)),
4: use_custom_friction,
5: use_custom_lat_factor,
6: bool(ui_state.starpilot_toggles.get("use_custom_steerRatio", False)),
}
self._ftm_override_metric_ids = {metric_id for metric_id, enabled in ftm_metric_flags.items() if enabled}
self._flm_override_metric_ids = {metric_id for metric_id, enabled in flm_metric_flags.items() if enabled}
friction_coeff = resolve_effective_torque_value(
use_custom_friction,
@@ -305,6 +305,6 @@ class DeveloperSidebar:
if metric_id <= 0 or metric_id not in self._metrics:
continue
label_first, label_second = self._metrics[metric_id]
ftm_overridden = metric_id in self._ftm_override_metric_ids
self._draw_metric(sidebar_rect, label_first, label_second, _FTM_OVERRIDE_COLOR if ftm_overridden else self._metric_color, y)
flm_overridden = metric_id in self._flm_override_metric_ids
self._draw_metric(sidebar_rect, label_first, label_second, _FLM_OVERRIDE_COLOR if flm_overridden else self._metric_color, y)
y += METRIC_HEIGHT + spacing
+9 -9
View File
@@ -232,10 +232,10 @@ EXCLUDED_KEYS = {
"CommunityFavorites",
"CurvatureData",
"ExperimentalLongitudinalEnabled",
"FTMActiveOverrides",
"FTMActiveProfileId",
"FTMTrialBaseline",
"FTMTrialApplied",
"FLMActiveOverrides",
"FLMActiveProfileId",
"FLMTrialBaseline",
"FLMTrialApplied",
"InstallDate",
"StarPilotCarParamsPersistent",
"KonikMinutes",
@@ -708,13 +708,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 ""
toggle.flm_active_profile_id = self.params.get("FLMActiveProfileId", encoding="utf-8") or ""
toggle.flm_trial_applied = self.params.get_bool("FLMTrialApplied")
flm_overrides_raw = self.params.get("FLMActiveOverrides", encoding="utf-8") or ""
try:
toggle.ftm_active_overrides = json.loads(ftm_overrides_raw) if ftm_overrides_raw else {}
toggle.flm_active_overrides = json.loads(flm_overrides_raw) if flm_overrides_raw else {}
except Exception:
toggle.ftm_active_overrides = {}
toggle.flm_active_overrides = {}
toggle.use_auto_steer_delay = self.get_value("UseAutoSteerDelay", condition=advanced_lateral_tuning, default=True)
toggle.steerActuatorDelay = self.get_value("SteerDelay", cast=float, condition=advanced_lateral_tuning, default=fullSteerActuatorDelay, min=0.01, max=1.0)
toggle.use_custom_steerActuatorDelay = advanced_lateral_tuning and not toggle.use_auto_steer_delay
+8 -1
View File
@@ -21,7 +21,7 @@ RUNTIME_LOOP_HZ = 20
INFERENCE_INTERVAL = 0.15
FOLLOWUP_INFERENCE_INTERVAL = 0.10
FOLLOWUP_WINDOW_SECONDS = 2.0
TEMPORAL_TRACKING_ENABLED = True
TEMPORAL_TRACKING_ENABLED = False
TRACK_CONFIRMED_PROPOSALS_ENABLED = False
TRACK_CLASSIFICATION_INTERVAL = 0.12
TRACK_BUSY_CLASSIFICATION_INTERVAL = 0.35
@@ -58,6 +58,7 @@ CONSISTENT_DETECTIONS = 2
# These counts must remain achievable at the measured 1.5 Hz onroad cadence.
CHANGE_CONSISTENT_DETECTIONS = 2
CHANGE_SINGLE_READ_MIN_CONFIDENCE = 0.83
CHANGE_REPEAT_MIN_CONFIDENCE = 0.70
LOW_SPEED_CHANGE_CONSISTENT_DETECTIONS = 2
LOW_SPEED_CHANGE_MIN_CONFIDENCE = 0.90
LOW_SPEED_CHANGE_ALLOW_STRONG_CONSENSUS = True
@@ -2150,6 +2151,7 @@ class SpeedLimitVisionDaemon:
counts = Counter(entry.speed_limit_mph for entry in self.history)
candidate_speed_limit, candidate_count = counts.most_common(1)[0]
matching_entries = [entry for entry in self.history if entry.speed_limit_mph == candidate_speed_limit]
matching_confidences = sorted((entry.confidence for entry in matching_entries), reverse=True)
best_confidence = max(entry.confidence for entry in matching_entries)
has_strong_consensus = any(entry.strong_consensus for entry in matching_entries)
current_speed_limit = self.published_speed_limit_mph
@@ -2170,6 +2172,11 @@ class SpeedLimitVisionDaemon:
return None
if candidate_count < required_count and not allow_single_frame_confirmation:
return None
if (
not allow_single_frame_confirmation and
matching_confidences[required_count - 1] < CHANGE_REPEAT_MIN_CONFIDENCE
):
return None
if candidate_count <= current_count:
return None
return candidate_speed_limit, best_confidence
@@ -22,6 +22,11 @@ def test_speed_change_requires_two_matching_reads_below_single_read_threshold():
assert daemon._confirm_detection() == pytest.approx((55, 0.82))
def test_speed_change_rejects_weak_confirming_read():
daemon = daemon_with_history(40, [(35, 0.78), (35, 0.48)])
assert daemon._confirm_detection() is None
def test_speed_change_accepts_single_high_confidence_read():
daemon = daemon_with_history(40, [(55, 0.84)])
assert daemon._confirm_detection() == pytest.approx((55, 0.84))
@@ -1,7 +1,7 @@
import { html, reactive } from "/assets/vendor/arrow-core.js"
import { createBrowserHistory, createRouter } from "/assets/vendor/remix-router-1.3.1.js"
import { hideSidebar } from "/assets/js/utils.js"
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=ftm-overrides-1"
import { DeviceSettings } from "/assets/components/tools/device_settings.js?v=flm-overrides-1"
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"
@@ -19,7 +19,7 @@ import { ModelManager } from "/assets/components/tools/model_manager.js?v=202603
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-9"
import { Tuning } from "/assets/components/tools/tuning.js?v=flm-workspace-9"
import { Troubleshoot } from "/assets/components/tools/troubleshoot.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
@@ -179,7 +179,7 @@
gap: 0.45rem;
}
.ds-ftm-badge {
.ds-flm-badge {
background: var(--sidebar-bg);
border: 1px solid var(--main-fg);
border-radius: 999px;
@@ -191,8 +191,8 @@
text-transform: uppercase;
}
.ds-ftm-detail,
.ds-ftm-summary {
.ds-flm-detail,
.ds-flm-summary {
border-left: 2px solid var(--main-fg);
color: var(--text-muted);
font-size: var(--font-size-xs);
@@ -201,11 +201,11 @@
padding-left: 0.65rem;
}
.ds-ftm-summary > div + div {
.ds-flm-summary > div + div {
margin-top: 0.2rem;
}
.ds-ftm-link {
.ds-flm-link {
color: var(--main-fg);
display: inline-block;
font-weight: var(--font-weight-bold);
@@ -213,7 +213,7 @@
text-decoration: none;
}
.ds-ftm-link:hover {
.ds-flm-link:hover {
text-decoration: underline;
}
@@ -12,11 +12,11 @@ const FAVORITE_OPTION_COLLATOR = new Intl.Collator(undefined, { numeric: true, s
// Plain variables — scheduling/routing flags that must NOT be reactive
let syncScheduled = false
let lastParams = null
let ftmWorkspaceInflight = null
let lastFtmWorkspaceFetch = 0
let flmWorkspaceInflight = null
let lastFlmWorkspaceFetch = 0
const DYNAMIC_DEFAULT_DEP_KEYS = new Set(["AccelerationProfile", "EVTuning", "TruckTuning"])
const PANDA_FIRMWARE_TOGGLE_KEYS = new Set(["IgnoreIgnitionLine", "RemoteStartBootsComma", "HKGRemoteStartBootsComma"])
const FTM_ADVANCED_LATERAL_KEYS = new Set([
const FLM_ADVANCED_LATERAL_KEYS = new Set([
"AdvancedLateralTune", "ForceAutoTune", "ForceAutoTuneOff", "UseAutoSteerDelay", "SteerDelay",
"SteerFriction", "SteerKP", "SteerLatAccel", "SteerRatio",
])
@@ -28,7 +28,7 @@ const state = reactive({
paramMetaByKey: {},
values: {},
defaultValues: {},
ftmActiveTrial: null,
flmActiveTrial: null,
loadingLayout: true,
loadingValues: true,
filter: "",
@@ -274,28 +274,28 @@ async function fetchDefaultValues() {
}
}
async function fetchFtmWorkspace(force = false) {
async function fetchFlmWorkspace(force = false) {
const now = Date.now()
if (!force && now - lastFtmWorkspaceFetch < 1500) return
if (ftmWorkspaceInflight) return ftmWorkspaceInflight
if (!force && now - lastFlmWorkspaceFetch < 1500) return
if (flmWorkspaceInflight) return flmWorkspaceInflight
lastFtmWorkspaceFetch = now
ftmWorkspaceInflight = fetch("/api/ftm/workspace", { cache: "no-store" })
lastFlmWorkspaceFetch = now
flmWorkspaceInflight = fetch("/api/flm/workspace", { cache: "no-store" })
.then(async res => {
if (!res.ok) return
const workspace = await res.json()
state.ftmActiveTrial = workspace?.activeTrial || null
state.flmActiveTrial = workspace?.activeTrial || null
})
.catch(error => console.warn("Failed to load active FTM trial state:", error))
.catch(error => console.warn("Failed to load active FLM trial state:", error))
.finally(() => {
ftmWorkspaceInflight = null
flmWorkspaceInflight = null
})
return ftmWorkspaceInflight
return flmWorkspaceInflight
}
async function refreshParamsAndDefaults() {
await Promise.all([fetchDefaultValues(), fetchFtmWorkspace(true)])
await Promise.all([fetchDefaultValues(), fetchFlmWorkspace(true)])
try {
const valuesRes = await fetch("/api/params/all")
@@ -344,7 +344,7 @@ async function fetchLayoutAndParams() {
// Pull params once at page load; local state handles subsequent edits.
try {
const [defaultsLoaded] = await Promise.all([fetchDefaultValues(), fetchFtmWorkspace(true)])
const [defaultsLoaded] = await Promise.all([fetchDefaultValues(), fetchFlmWorkspace(true)])
if (!defaultsLoaded) {
state.defaultValues = {}
}
@@ -1035,9 +1035,9 @@ function valuesEqual(left, right) {
return left === right
}
function getFtmParamStatus(key) {
const trial = state.ftmActiveTrial
if (!trial || !FTM_ADVANCED_LATERAL_KEYS.has(key)) return null
function getFlmParamStatus(key) {
const trial = state.flmActiveTrial
if (!trial || !FLM_ADVANCED_LATERAL_KEYS.has(key)) return null
const applied = trial.appliedGenericParams || {}
const hasExplicitMetadata = Object.prototype.hasOwnProperty.call(applied, key)
@@ -1054,7 +1054,7 @@ function getFtmParamStatus(key) {
}
}
function formatFtmValue(param, value) {
function formatFlmValue(param, value) {
if (value === undefined || value === null) return "not set"
if (param.data_type === "bool") return value ? "On" : "Off"
if (param.ui_type === "numeric") {
@@ -1064,8 +1064,8 @@ function formatFtmValue(param, value) {
return String(value)
}
function getFtmTrialSummary() {
const trial = state.ftmActiveTrial
function getFlmTrialSummary() {
const trial = state.flmActiveTrial
if (!trial) return null
const genericCount = Object.keys(trial.appliedGenericParams || {}).filter(key => key !== "AdvancedLateralTune").length
const thresholdCount = Object.keys(trial.appliedFrictionThresholds || {}).length
@@ -1234,8 +1234,8 @@ function renderSettingRow(p) {
const isChild = p.parent_key ? "ds-child-modifier" : ""
const lockReason = () => getSettingLockReason(p)
const isLocked = () => lockReason() !== ""
const ftmParamStatus = getFtmParamStatus(p.key)
const ftmTrialSummary = p.key === "AdvancedLateralTune" ? getFtmTrialSummary() : null
const flmParamStatus = getFlmParamStatus(p.key)
const flmTrialSummary = p.key === "AdvancedLateralTune" ? getFlmTrialSummary() : null
let rowControl = ""
if (isNumeric) {
@@ -1358,30 +1358,30 @@ function renderSettingRow(p) {
<div class="ds-row-text">
<div class="ds-row-heading">
<span class="ds-row-label">${p.label}</span>
${ftmParamStatus ? html`<span class="ds-ftm-badge">Currently overridden by FTM</span>` : ""}
${flmParamStatus ? html`<span class="ds-flm-badge">Currently overridden by FLM</span>` : ""}
</div>
${p.description ? html`<div class="ds-row-desc">${p.description}</div>` : ""}
${() => {
const reason = lockReason()
return reason ? html`<div class="ds-row-desc"><strong>Locked:</strong> ${reason}</div>` : ""
}}
${ftmParamStatus ? html`
<div class="ds-ftm-detail">
Effective now: <strong>${formatFtmValue(p, ftmParamStatus.effectiveValue)}</strong>.
Revert restores: <strong>${formatFtmValue(p, ftmParamStatus.previousValue)}</strong>.
${flmParamStatus ? html`
<div class="ds-flm-detail">
Effective now: <strong>${formatFlmValue(p, flmParamStatus.effectiveValue)}</strong>.
Revert restores: <strong>${formatFlmValue(p, flmParamStatus.previousValue)}</strong>.
You can still edit this while the trial is active.
</div>
` : ""}
${ftmTrialSummary ? html`
<div class="ds-ftm-summary">
<div><strong>FTM trial active:</strong> ${ftmTrialSummary.title}</div>
${flmTrialSummary ? html`
<div class="ds-flm-summary">
<div><strong>FLM trial active:</strong> ${flmTrialSummary.title}</div>
<div>
${ftmTrialSummary.genericCount} advanced setting${ftmTrialSummary.genericCount === 1 ? "" : "s"},
${ftmTrialSummary.thresholdCount} friction curve${ftmTrialSummary.thresholdCount === 1 ? "" : "s"}, and
${ftmTrialSummary.vehicleKnobCount} vehicle-specific knob${ftmTrialSummary.vehicleKnobCount === 1 ? "" : "s"} active.
${flmTrialSummary.genericCount} advanced setting${flmTrialSummary.genericCount === 1 ? "" : "s"},
${flmTrialSummary.thresholdCount} friction curve${flmTrialSummary.thresholdCount === 1 ? "" : "s"}, and
${flmTrialSummary.vehicleKnobCount} vehicle-specific knob${flmTrialSummary.vehicleKnobCount === 1 ? "" : "s"} active.
</div>
<div>Revert from Lateral Tuning restores the exact settings saved before this trial.</div>
<a class="ds-ftm-link" href="/tuning">Open Lateral Tuning</a>
<a class="ds-flm-link" href="/tuning">Open Lateral Tuning</a>
</div>
` : ""}
@@ -1446,7 +1446,7 @@ function resolveActiveSectionSlug(params) {
export function DeviceSettings({ params }) {
lastParams = params
fetchFtmWorkspace()
fetchFlmWorkspace()
if (!state.fetched) {
state.fetched = true
@@ -1,11 +1,11 @@
.ftmTwoColumn {
.flmTwoColumn {
display: grid;
gap: var(--gap-base);
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.ftmCard {
.flmCard {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--border-radius-md);
@@ -13,45 +13,51 @@
padding: var(--padding-base);
}
.ftmCardHeader {
.flmCardHeader {
align-items: flex-start;
display: flex;
gap: var(--gap-sm);
justify-content: space-between;
}
.ftmCardHeader h3,
.ftmCardHeader h4,
.ftmCardHeader h5 {
.flmCardHeader h3,
.flmCardHeader h4,
.flmCardHeader h5 {
margin: 0;
}
.ftmCardSubsection {
.flmCardSubsection {
margin-top: var(--margin-base);
}
.ftmRouteList,
.ftmWorkspaceList,
.ftmFindings {
.flmRouteList,
.flmWorkspaceList,
.flmFindings {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.ftmWorkspaceRow {
.flmWorkspaceRow {
display: flex;
gap: var(--gap-sm);
}
.ftmRouteList {
.flmRouteList {
max-height: 28rem;
overflow: auto;
padding-right: var(--padding-xs);
}
.ftmRouteItem,
.ftmWorkspaceItem {
.flmRouteRow {
align-items: stretch;
display: flex;
gap: var(--gap-sm);
}
.flmRouteItem,
.flmWorkspaceItem {
align-items: flex-start;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
@@ -64,61 +70,83 @@
text-align: left;
}
.ftmRouteItem input {
.flmRouteItem input {
margin-top: 0.2rem;
}
.ftmRouteItem span,
.ftmWorkspaceItem {
.flmRouteItem {
flex: 1 1 auto;
min-width: 0;
}
.flmConnectLink {
align-items: center;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--border-radius-sm);
color: var(--main-fg);
display: inline-flex;
flex: 0 0 auto;
font-size: 0.78rem;
padding: 0 var(--padding-sm);
text-decoration: none;
}
.flmConnectLink:hover {
border-color: var(--main-fg);
}
.flmRouteItem span,
.flmWorkspaceItem {
display: flex;
flex-direction: column;
}
.ftmWorkspaceItem {
.flmWorkspaceItem {
flex: 1 1 auto;
}
.ftmWorkspaceDelete {
.flmWorkspaceDelete {
align-self: stretch;
flex: 0 0 auto;
}
.ftmRouteItem small,
.ftmWorkspaceItem small,
.ftmWorkspaceItem span {
.flmRouteItem small,
.flmWorkspaceItem small,
.flmWorkspaceItem span {
color: var(--text-muted);
}
.ftmWorkspaceItem:hover,
.ftmRouteItem:hover,
.ftmCard button.selected {
.flmWorkspaceItem:hover,
.flmRouteItem:hover,
.flmCard button.selected {
border-color: var(--main-fg);
}
.ftmProfileGrid {
.flmProfileGrid {
display: grid;
gap: var(--gap-base);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ftmFeedbackButtons {
.flmFeedbackButtons {
display: flex;
flex-wrap: wrap;
gap: var(--gap-xs);
}
.ftmFeedbackButtons .longManeuverButton.selected {
.flmFeedbackButtons .longManeuverButton.selected {
box-shadow: 0 0 0 3px var(--text-color);
filter: brightness(1.2);
}
.ftmTuneComparison {
.flmTuneComparison {
background: rgba(255, 255, 255, 0.025);
border-radius: var(--border-radius-sm);
padding: var(--padding-base);
}
.ftmTuneComparisonTable {
.flmTuneComparisonTable {
align-items: center;
display: grid;
gap: var(--gap-xs) var(--gap-sm);
@@ -126,40 +154,40 @@
margin-top: var(--margin-sm);
}
.ftmTuneComparisonTable > div {
.flmTuneComparisonTable > div {
min-width: 0;
overflow-wrap: anywhere;
padding: var(--padding-xs) 0;
}
.ftmTuneComparisonHeader {
.flmTuneComparisonHeader {
color: var(--text-muted);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
}
.ftmTuneComparisonLabel {
.flmTuneComparisonLabel {
font-weight: var(--font-weight-demi-bold);
}
.ftmTuneComparisonArrow {
.flmTuneComparisonArrow {
color: var(--text-muted);
text-align: center;
}
.ftmTuneComparisonChanged {
.flmTuneComparisonChanged {
color: var(--main-fg);
font-weight: var(--font-weight-demi-bold);
}
.ftmDeltaBox {
.flmDeltaBox {
background: rgba(255, 255, 255, 0.03);
border-radius: var(--border-radius-sm);
margin-top: var(--margin-sm);
padding: var(--padding-sm);
}
.ftmNotes {
.flmNotes {
background: var(--secondary-bg);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--border-radius-sm);
@@ -171,19 +199,19 @@
width: 100%;
}
.ftmTrackingOverview {
.flmTrackingOverview {
background: rgba(255, 255, 255, 0.025);
border-radius: var(--border-radius-sm);
padding: var(--padding-base);
}
.ftmTrackingLegend {
.flmTrackingLegend {
display: flex;
flex-wrap: wrap;
gap: var(--gap-sm);
}
.ftmTrackingLegend span {
.flmTrackingLegend span {
align-items: center;
color: var(--text-muted);
display: inline-flex;
@@ -191,22 +219,22 @@
gap: var(--gap-xs);
}
.ftmTrackingLegend i {
.flmTrackingLegend i {
border-radius: 999px;
display: inline-block;
height: 0.2rem;
width: 1.4rem;
}
.ftmTrackingLegend i.desired {
.flmTrackingLegend i.desired {
background: #ef4444;
}
.ftmTrackingLegend i.actual {
.flmTrackingLegend i.actual {
background: #38bdf8;
}
.ftmTrackingNotice {
.flmTrackingNotice {
border-left: 2px solid var(--main-fg);
color: var(--text-muted);
font-size: var(--font-size-sm);
@@ -214,14 +242,14 @@
padding: var(--padding-xs) var(--padding-sm);
}
.ftmTrackingGrid {
.flmTrackingGrid {
display: grid;
gap: var(--gap-sm);
grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
margin-top: var(--margin-base);
}
.ftmTrackingCard {
.flmTrackingCard {
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--border-radius-sm);
@@ -229,25 +257,25 @@
padding: var(--padding-sm);
}
.ftmTrackingCardHeader {
.flmTrackingCardHeader {
align-items: flex-start;
display: flex;
gap: var(--gap-sm);
justify-content: space-between;
}
.ftmTrackingCardHeader > div {
.flmTrackingCardHeader > div {
display: flex;
flex-direction: column;
}
.ftmTrackingCardHeader span,
.ftmTrackingCard small {
.flmTrackingCardHeader span,
.flmTrackingCard small {
color: var(--text-muted);
font-size: var(--font-size-xs);
}
.ftmTrackingCard small {
.flmTrackingCard small {
display: block;
margin-top: var(--margin-xs);
overflow: hidden;
@@ -255,35 +283,35 @@
white-space: nowrap;
}
.ftmTrackingPlot {
.flmTrackingPlot {
display: block;
height: auto;
margin-top: var(--margin-xs);
width: 100%;
}
.ftmTrackingPlotBackground {
.flmTrackingPlotBackground {
fill: var(--input-bg);
}
.ftmTrackingEventRegion {
.flmTrackingEventRegion {
fill: rgba(139, 108, 197, 0.14);
}
.ftmTrackingAxis,
.ftmTrackingZero {
.flmTrackingAxis,
.flmTrackingZero {
fill: none;
stroke: rgba(255, 255, 255, 0.18);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.ftmTrackingZero {
.flmTrackingZero {
stroke-dasharray: 3 4;
}
.ftmTrackingDesired,
.ftmTrackingActual {
.flmTrackingDesired,
.flmTrackingActual {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
@@ -291,27 +319,27 @@
vector-effect: non-scaling-stroke;
}
.ftmTrackingDesired {
.flmTrackingDesired {
stroke: #ef4444;
}
.ftmTrackingActual {
.flmTrackingActual {
stroke: #38bdf8;
}
.ftmTrackingAxisLabel {
.flmTrackingAxisLabel {
fill: var(--text-muted);
font-size: 9px;
}
.ftmTrackingMeta {
.flmTrackingMeta {
display: flex;
flex-wrap: wrap;
gap: var(--gap-xs);
margin-top: var(--margin-xs);
}
.ftmTrackingMeta span {
.flmTrackingMeta span {
background: var(--input-bg);
border-radius: 999px;
color: var(--text-muted);
@@ -320,12 +348,12 @@
}
@media only screen and (max-width: 900px) {
.ftmTwoColumn,
.ftmProfileGrid {
.flmTwoColumn,
.flmProfileGrid {
grid-template-columns: 1fr;
}
.ftmTuneComparisonTable {
.flmTuneComparisonTable {
font-size: var(--font-size-sm);
grid-template-columns: minmax(8rem, 1.3fr) minmax(5rem, 1fr) auto minmax(5rem, 1fr);
}
@@ -14,6 +14,7 @@ const state = reactive({
truncatedRoutes: false,
routeProgress: 0,
routeTotal: 0,
connectDongleId: "",
workspace: { reports: [], activeTrial: null, status: {} },
status: {},
report: null,
@@ -46,6 +47,13 @@ function safeCount(value) {
return Number.isFinite(n) ? n : 0
}
function connectRouteUrl(routeName) {
const dongleId = String(state.connectDongleId || "").trim()
const routeId = String(routeName || "").trim()
if (!dongleId || !routeId) return ""
return `https://connect.comma.ai/${encodeURIComponent(dongleId)}/${encodeURIComponent(routeId)}`
}
function formatStatusAge(updatedAt) {
const updated = Number(updatedAt)
if (!Number.isFinite(updated) || updated <= 0) return "unknown"
@@ -151,11 +159,14 @@ async function fetchRoutes() {
state.routeProgress = safeCount(data.progress)
state.routeTotal = safeCount(data.total)
}
if (typeof data.connectDongleId === "string") {
state.connectDongleId = data.connectDongleId
}
if (Array.isArray(data.routes)) {
enqueueRoutes(data.routes)
}
} catch (error) {
console.error("[ftm] failed to parse route payload", error)
console.error("[flm] failed to parse route payload", error)
}
}
}
@@ -179,7 +190,7 @@ async function fetchRoutes() {
async function fetchWorkspace() {
try {
state.loadingWorkspace = true
const response = await fetch("/api/ftm/workspace")
const response = await fetch("/api/flm/workspace")
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to load tuning workspace.")
state.workspace = payload
@@ -201,7 +212,7 @@ function syncFeedbackState(report) {
async function loadReport(reportId) {
if (!reportId) return
try {
const response = await fetch(`/api/ftm/report/${encodeURIComponent(reportId)}`)
const response = await fetch(`/api/flm/report/${encodeURIComponent(reportId)}`)
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to load tuning report.")
state.report = payload
@@ -218,7 +229,7 @@ async function deleteReport(reportId) {
state.runningAction = true
try {
const response = await fetch(`/api/ftm/report/${encodeURIComponent(reportId)}`, { method: "DELETE" })
const response = await fetch(`/api/flm/report/${encodeURIComponent(reportId)}`, { method: "DELETE" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to delete tuning report.")
@@ -243,7 +254,7 @@ async function clearWorkspace() {
state.runningAction = true
try {
const response = await fetch("/api/ftm/workspace/clear", { method: "POST" })
const response = await fetch("/api/flm/workspace/clear", { method: "POST" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to clear tuning workspace.")
@@ -262,7 +273,7 @@ async function clearWorkspace() {
async function fetchStatus() {
try {
const response = await fetch("/api/ftm/status")
const response = await fetch("/api/flm/status")
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to load tuning status.")
state.status = {
@@ -323,7 +334,7 @@ async function runAnalyze() {
if (!state.selectedRoutes.length || state.runningAction) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/analyze", {
const response = await fetch("/api/flm/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ routes: state.selectedRoutes }),
@@ -331,7 +342,7 @@ async function runAnalyze() {
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.")
showSnackbar(payload.message || "FLM analysis started.")
} catch (error) {
state.error = error?.message || "Failed to start tuning analysis."
showSnackbar(state.error, "error")
@@ -344,11 +355,11 @@ async function stopAnalyze() {
if (state.runningAction) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/analyze/stop", { method: "POST" })
const response = await fetch("/api/flm/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.")
showSnackbar(payload.message || "FLM analysis stopped.")
} catch (error) {
state.error = error?.message || "Failed to stop tuning analysis."
showSnackbar(state.error, "error")
@@ -361,7 +372,7 @@ async function applyProfile(profileId) {
if (!state.report?.reportId || !profileId || state.runningAction) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/trials/apply", {
const response = await fetch("/api/flm/trials/apply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reportId: state.report.reportId, profileId }),
@@ -385,7 +396,7 @@ async function selectPath(pathKey) {
state.runningAction = true
try {
const response = await fetch(`/api/ftm/report/${encodeURIComponent(state.report.reportId)}/path`, {
const response = await fetch(`/api/flm/report/${encodeURIComponent(state.report.reportId)}/path`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ pathKey }),
@@ -407,7 +418,7 @@ async function revertProfile() {
if (state.runningAction) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/trials/revert", { method: "POST" })
const response = await fetch("/api/flm/trials/revert", { method: "POST" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to revert trial profile.")
state.error = ""
@@ -427,7 +438,7 @@ async function acceptCurrentAsBaseline() {
state.runningAction = true
try {
const response = await fetch("/api/ftm/trials/accept", { method: "POST" })
const response = await fetch("/api/flm/trials/accept", { method: "POST" })
const payload = await response.json()
if (!response.ok) throw new Error(payload.error || "Failed to keep the current tune.")
state.error = ""
@@ -463,7 +474,7 @@ async function saveFeedback() {
if (!state.report?.reportId || state.runningAction) return
state.runningAction = true
try {
const response = await fetch("/api/ftm/feedback", {
const response = await fetch("/api/flm/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -548,9 +559,9 @@ function activeTrialProfile() {
return allReportProfiles().find((profile) => profile.id === activeTrial.profileId) || null
}
function mergedFtmOverrides() {
const current = state.report?.currentParams?.FTMActiveOverrides || {}
const trial = activeTrialProfile()?.ftmOverrides || {}
function mergedFlmOverrides() {
const current = state.report?.currentParams?.FLMActiveOverrides || {}
const trial = activeTrialProfile()?.flmOverrides || {}
return {
baseFrictionThresholds: {
...(current.baseFrictionThresholds || {}),
@@ -590,8 +601,8 @@ function tuneComparisonRows() {
current: Object.hasOwn(trialGeneric, key) ? trialGeneric[key] : current[key],
}))
const overrides = mergedFtmOverrides()
for (const [family, payload] of Object.entries(stock.FTMBaseFrictionThresholds || {})) {
const overrides = mergedFlmOverrides()
for (const [family, payload] of Object.entries(stock.FLMBaseFrictionThresholds || {})) {
rows.push({
key: `friction-threshold-${family}`,
label: `${family} friction threshold`,
@@ -601,11 +612,11 @@ function tuneComparisonRows() {
}
for (const [symbol, currentValue] of Object.entries(overrides.vehicleKnobs || {})) {
if (!Object.hasOwn(stock.FTMVehicleKnobs || {}, symbol)) continue
if (!Object.hasOwn(stock.FLMVehicleKnobs || {}, symbol)) continue
rows.push({
key: symbol,
label: symbol.split(".").slice(1).join("."),
stock: stock.FTMVehicleKnobs[symbol],
stock: stock.FLMVehicleKnobs[symbol],
current: currentValue,
codeLabel: symbol,
})
@@ -625,25 +636,25 @@ function renderTuneComparison() {
if (!rows.length) return ""
const profile = activeTrialProfile()
return html`
<div class="ftmCardSubsection ftmTuneComparison">
<div class="ftmCardHeader">
<div class="flmCardSubsection flmTuneComparison">
<div class="flmCardHeader">
<div>
<h4>Stock vs Current FTM</h4>
<h4>Stock vs Current FLM</h4>
<p class="longManeuverMuted">
${profile ? `Includes active trial: ${profile.pathLabel || "FTM"} / ${profile.label}` : "Current values captured when this route was analyzed."}
${profile ? `Includes active trial: ${profile.pathLabel || "FLM"} / ${profile.label}` : "Current values captured when this route was analyzed."}
</p>
</div>
</div>
<div class="ftmTuneComparisonTable">
<div class="ftmTuneComparisonHeader">Parameter</div>
<div class="ftmTuneComparisonHeader">Stock</div>
<div class="ftmTuneComparisonArrow"></div>
<div class="ftmTuneComparisonHeader">FTM</div>
<div class="flmTuneComparisonTable">
<div class="flmTuneComparisonHeader">Parameter</div>
<div class="flmTuneComparisonHeader">Stock</div>
<div class="flmTuneComparisonArrow"></div>
<div class="flmTuneComparisonHeader">FLM</div>
${rows.map((row) => html`
<div class="ftmTuneComparisonLabel" title="${row.codeLabel || row.key}">${row.label}</div>
<div class="flmTuneComparisonLabel" title="${row.codeLabel || row.key}">${row.label}</div>
<div>${formatTuneComparisonValue(row.stock)}</div>
<div class="ftmTuneComparisonArrow">&gt;</div>
<div class="${comparisonValueChanged(row) ? "ftmTuneComparisonChanged" : ""}">${formatTuneComparisonValue(row.current)}</div>
<div class="flmTuneComparisonArrow">&gt;</div>
<div class="${comparisonValueChanged(row) ? "flmTuneComparisonChanged" : ""}">${formatTuneComparisonValue(row.current)}</div>
`)}
</div>
</div>
@@ -731,18 +742,18 @@ function renderTrackingPlot(plot) {
const zeroY = 136 - ((0 - yMin) / Math.max(yMax - yMin, 0.001)) * 124
return html`
<svg class="ftmTrackingPlot" viewBox="0 0 420 158" role="img" aria-label="Desired versus actual lateral acceleration">
<rect class="ftmTrackingPlotBackground" x="0" y="0" width="420" height="158" rx="10"></rect>
<rect class="ftmTrackingEventRegion" x="${eventX}" y="12" width="${eventWidth}" height="124"></rect>
<line class="ftmTrackingZero" x1="34" y1="${zeroY}" x2="410" y2="${zeroY}"></line>
<line class="ftmTrackingAxis" x1="34" y1="12" x2="34" y2="136"></line>
<line class="ftmTrackingAxis" x1="34" y1="136" x2="410" y2="136"></line>
<polyline class="ftmTrackingDesired" points="${desiredPoints}"></polyline>
<polyline class="ftmTrackingActual" points="${actualPoints}"></polyline>
<text class="ftmTrackingAxisLabel" x="2" y="18">${yMax.toFixed(1)}</text>
<text class="ftmTrackingAxisLabel" x="2" y="138">${yMin.toFixed(1)}</text>
<text class="ftmTrackingAxisLabel" x="34" y="152">0s</text>
<text class="ftmTrackingAxisLabel" x="384" y="152">${duration.toFixed(1)}s</text>
<svg class="flmTrackingPlot" viewBox="0 0 420 158" role="img" aria-label="Desired versus actual lateral acceleration">
<rect class="flmTrackingPlotBackground" x="0" y="0" width="420" height="158" rx="10"></rect>
<rect class="flmTrackingEventRegion" x="${eventX}" y="12" width="${eventWidth}" height="124"></rect>
<line class="flmTrackingZero" x1="34" y1="${zeroY}" x2="410" y2="${zeroY}"></line>
<line class="flmTrackingAxis" x1="34" y1="12" x2="34" y2="136"></line>
<line class="flmTrackingAxis" x1="34" y1="136" x2="410" y2="136"></line>
<polyline class="flmTrackingDesired" points="${desiredPoints}"></polyline>
<polyline class="flmTrackingActual" points="${actualPoints}"></polyline>
<text class="flmTrackingAxisLabel" x="2" y="18">${yMax.toFixed(1)}</text>
<text class="flmTrackingAxisLabel" x="2" y="138">${yMin.toFixed(1)}</text>
<text class="flmTrackingAxisLabel" x="34" y="152">0s</text>
<text class="flmTrackingAxisLabel" x="384" y="152">${duration.toFixed(1)}s</text>
</svg>
`
}
@@ -752,28 +763,28 @@ function renderTrackingOverview() {
if (!items.length) return ""
return html`
<div class="ftmCardSubsection ftmTrackingOverview">
<div class="ftmCardHeader">
<div class="flmCardSubsection flmTrackingOverview">
<div class="flmCardHeader">
<div>
<h4>Tracking Overview</h4>
<p class="longManeuverMuted">
Desired vs actual lateral acceleration (m/s^2) in representative intervention-free windows. The shaded area is the classified event.
</p>
</div>
<div class="ftmTrackingLegend" aria-label="Plot legend">
<div class="flmTrackingLegend" aria-label="Plot legend">
<span><i class="desired"></i>Desired</span>
<span><i class="actual"></i>Actual</span>
</div>
</div>
<div class="ftmTrackingNotice">
<div class="flmTrackingNotice">
No fit score by design. Small phase separation is normal, and closer traces do not automatically mean the steering feels better.
</div>
<div class="ftmTrackingGrid">
<div class="flmTrackingGrid">
${items.map((item) => html`
<article class="ftmTrackingCard">
<div class="ftmTrackingCardHeader">
<article class="flmTrackingCard">
<div class="flmTrackingCardHeader">
<div>
<strong>${item.overviewTitle}</strong>
<span>${String(item.bucket || "event").replace(/_/g, " ")}</span>
@@ -781,7 +792,7 @@ function renderTrackingOverview() {
<span>${Number(item.plotData.meanSpeedMph || 0).toFixed(1)} mph</span>
</div>
${renderTrackingPlot(item.plotData)}
<div class="ftmTrackingMeta">
<div class="flmTrackingMeta">
<span>${item.evidence?.directionBias || item.plotData.direction || "center"}</span>
<span>${item.evidence?.speedBand || item.plotData.speedBand || "mixed"}</span>
<span>${Number(item.plotData.eventDurationSec || 0).toFixed(1)}s event</span>
@@ -796,11 +807,11 @@ function renderTrackingOverview() {
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 || {})
const frictionEntries = Object.entries(profile.flmOverrides?.baseFrictionThresholds || {})
const vehicleKnobEntries = Object.entries(profile.flmOverrides?.vehicleKnobs || {})
return html`
<div class="ftmCard">
<div class="ftmCardHeader">
<div class="flmCard">
<div class="flmCardHeader">
<div>
<h4>${profile.label}</h4>
<p class="longManeuverMuted">${profile.description}</p>
@@ -813,7 +824,7 @@ function renderProfile(profile) {
</button>
</div>
<div class="ftmProfileGrid">
<div class="flmProfileGrid">
<div>
<h5>Generic Params</h5>
<ul>
@@ -823,7 +834,7 @@ function renderProfile(profile) {
</ul>
</div>
<div>
<h5>FTM Overrides</h5>
<h5>FLM 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>`)}
@@ -838,8 +849,8 @@ function renderProfile(profile) {
function renderSuggestion(suggestion) {
const currentVsSuggested = suggestion.currentVsSuggested
return html`
<div class="ftmCard">
<div class="ftmCardHeader">
<div class="flmCard">
<div class="flmCardHeader">
<div>
<h4>${suggestion.bucket.replace(/_/g, " ")}</h4>
<p class="longManeuverMuted">
@@ -848,7 +859,7 @@ function renderSuggestion(suggestion) {
${safeCount(suggestion.evidence?.eventCount)} event(s)
</p>
</div>
<div class="ftmFeedbackButtons">
<div class="flmFeedbackButtons">
<button
class="${() => `longManeuverButton ${feedbackStateFor(suggestion.dimensionId) === "accepted" ? "selected" : ""}`}"
aria-pressed="${() => feedbackStateFor(suggestion.dimensionId) === "accepted" ? "true" : "false"}"
@@ -875,7 +886,7 @@ function renderSuggestion(suggestion) {
${currentVsSuggested
? html`
<div class="ftmDeltaBox">
<div class="flmDeltaBox">
<strong>Current vs suggested:</strong>
${currentVsSuggested.type === "friction_curve"
? html`
@@ -899,8 +910,8 @@ function renderSuggestion(suggestion) {
function renderPathSummary(path) {
const selected = path.key === (state.report?.selectedPathKey || state.report?.primaryPathKey)
return html`
<div class="ftmCard">
<div class="ftmCardHeader">
<div class="flmCard">
<div class="flmCardHeader">
<div>
<h4>${path.title}</h4>
<p class="longManeuverMuted">
@@ -990,7 +1001,7 @@ export function Tuning() {
</div>
${() => state.status?.isOnroad ? html`
<p class="longManeuverError">FTM analysis is offroad-only. Stop the car and go offroad before starting a run.</p>
<p class="longManeuverError">FLM analysis is offroad-only. Stop the car and go offroad before starting a run.</p>
` : ""}
${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html`
@@ -1005,9 +1016,9 @@ export function Tuning() {
</div>
` : ""}
<div class="ftmTwoColumn">
<section class="ftmCard">
<div class="ftmCardHeader">
<div class="flmTwoColumn">
<section class="flmCard">
<div class="flmCardHeader">
<div>
<h3>Local Routes</h3>
<p class="longManeuverMuted">
@@ -1021,24 +1032,33 @@ export function Tuning() {
${() => 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">
<div class="flmRouteList">
${() => 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 class="flmRouteRow">
<label class="flmRouteItem">
<input
type="checkbox"
checked="${() => state.selectedRoutes.includes(route.name)}"
@change="${() => toggleRouteSelection(route.name)}" />
<span>
<strong>${route.timestampLabel}</strong>
<small>${route.name}</small>
</span>
</label>
${() => connectRouteUrl(route.name) ? html`
<a
class="flmConnectLink"
href="${connectRouteUrl(route.name)}"
target="_blank"
rel="noopener noreferrer">Connect</a>
` : ""}
</div>
`)}
</div>
</section>
<section class="ftmCard">
<div class="ftmCardHeader">
<section class="flmCard">
<div class="flmCardHeader">
<div>
<h3>Workspace</h3>
</div>
@@ -1051,19 +1071,19 @@ export function Tuning() {
</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.
Recent reports stay on-device under <code>/data/galaxy/flm</code>. Loading a report refreshes the suggestion and trial view below.
</p>
<div class="ftmWorkspaceList">
<div class="flmWorkspaceList">
${() => (state.workspace?.reports || []).length
? state.workspace.reports.map((report) => html`
<div class="ftmWorkspaceRow">
<button class="ftmWorkspaceItem" @click="${() => loadReport(report.reportId)}">
<div class="flmWorkspaceRow">
<button class="flmWorkspaceItem" @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"
class="longManeuverButton danger flmWorkspaceDelete"
disabled="${() => state.runningAction}"
@click="${() => deleteReport(report.reportId)}">
Delete
@@ -1076,8 +1096,8 @@ export function Tuning() {
</div>
${() => state.report ? html`
<section class="ftmCard">
<div class="ftmCardHeader">
<section class="flmCard">
<div class="flmCardHeader">
<div>
<h3>Report Summary</h3>
</div>
@@ -1107,12 +1127,12 @@ export function Tuning() {
${() => renderTuneComparison()}
<div class="ftmFindings">
<div class="flmFindings">
${reportPaths().map((path) => renderPathSummary(path))}
</div>
${() => (state.report.warnings || []).length ? html`
<div class="ftmCardSubsection">
<div class="flmCardSubsection">
<h4>Warnings</h4>
<ul>
${(state.report.warnings || []).map((warning) => html`<li>${warning}</li>`)}
@@ -1121,7 +1141,7 @@ export function Tuning() {
` : ""}
${() => (state.report.addTheseParametersAndStartHere || []).length ? html`
<div class="ftmCardSubsection">
<div class="flmCardSubsection">
<h4>Add These Parameters And Start Here</h4>
<ul>
${(state.report.addTheseParametersAndStartHere || []).map((line) => html`<li>${line}</li>`)}
@@ -1130,8 +1150,8 @@ export function Tuning() {
` : ""}
</section>
<section class="ftmCard">
<div class="ftmCardHeader">
<section class="flmCard">
<div class="flmCardHeader">
<div>
<h3>Active Findings: ${primaryPath()?.title || "Recommendations"}</h3>
<p class="longManeuverMuted">
@@ -1148,21 +1168,21 @@ export function Tuning() {
</div>
<textarea
class="ftmNotes"
class="flmNotes"
placeholder="Optional tuning notes"
@input="${(event) => { state.feedbackNotes = event.target.value }}">${() => state.feedbackNotes}</textarea>
<div class="ftmFindings">
<div class="flmFindings">
${((primaryPath()?.suggestions) || []).map((suggestion) => renderSuggestion(suggestion))}
</div>
</section>
<section class="ftmCard">
<section class="flmCard">
<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.
Apply one bounded profile at a time. Revert restores the exact advanced-lateral and FLM state that existed before the trial.
</p>
<div class="ftmFindings">
<div class="flmFindings">
${reportPaths().length
? reportPaths().map((path) => html`
<div>
@@ -1177,7 +1197,7 @@ export function Tuning() {
</div>
</section>
` : html`
<section class="ftmCard">
<section class="flmCard">
<h3>No Active Report</h3>
<p class="longManeuverMuted">
Select local routes, run analysis, or open one of the saved reports from the workspace panel.
@@ -34,18 +34,18 @@
<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-6">
<link rel="stylesheet" href="/assets/components/tools/tuning.css?v=flm-workspace-6">
<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">
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=ftm-overrides-1">
<link rel="stylesheet" href="/assets/components/tools/device_settings.css?v=flm-overrides-1">
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script type="module">
import("/assets/components/router.js?v=ftm-workspace-1").catch((err) => {
import("/assets/components/router.js?v=flm-workspace-1").catch((err) => {
console.error("[the_galaxy] bootstrap failed", err);
const target = document.getElementById("app") || document.body;
const pre = document.createElement("pre");
@@ -9,7 +9,7 @@ from types import ModuleType, SimpleNamespace
import pytest
MODULE_PATH = Path(__file__).resolve().parents[1] / "ftm_workspace.py"
MODULE_PATH = Path(__file__).resolve().parents[1] / "flm_workspace.py"
def _simple_module(name, **attrs):
@@ -19,7 +19,7 @@ def _simple_module(name, **attrs):
return module
def _install_ftm_import_stubs(tmp_path):
def _install_flm_import_stubs(tmp_path):
class FakeParams:
_store = {}
@@ -80,7 +80,7 @@ def _install_ftm_import_stubs(tmp_path):
KP=1.0,
)
def normalize_ftm_overrides(payload):
def normalize_flm_overrides(payload):
if isinstance(payload, str):
payload = json.loads(payload)
payload = payload or {}
@@ -104,10 +104,10 @@ def _install_ftm_import_stubs(tmp_path):
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: {
FLM_FRICTION_SPEED_KNOTS=[0.0, 5.0, 10.0, 15.0, 25.0],
get_flm_capabilities=lambda *args, **kwargs: {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"},
get_flm_rich_profile_key=lambda *args, **kwargs: "hyundai_ioniq_6",
get_flm_supported_vehicle_knobs=lambda: {
"hyundai_ioniq_6.ff_gain_left": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.1, "profile": "hyundai_ioniq_6"},
"hyundai_ioniq_6.ff_gain_right": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.12, "profile": "hyundai_ioniq_6"},
"hyundai_ioniq_6.turn_in_boost_left": {"min": 0.4, "max": 2.8, "precision": 0.001, "defaultValue": 1.64, "profile": "hyundai_ioniq_6"},
@@ -120,7 +120,7 @@ def _install_ftm_import_stubs(tmp_path):
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,
normalize_flm_overrides=normalize_flm_overrides,
)
sys.modules["openpilot.system.hardware"] = _simple_module("openpilot.system.hardware", PC=True)
sys.modules["openpilot.system.hardware.hw"] = _simple_module(
@@ -136,9 +136,9 @@ def _install_ftm_import_stubs(tmp_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)}"
def _load_flm_workspace_module(tmp_path):
fake_params_cls = _install_flm_import_stubs(tmp_path)
module_name = f"test_flm_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
@@ -173,11 +173,31 @@ def _sample(module, **kwargs):
roll_deg=0.0,
)
base.update(kwargs)
return module.FTMSample(**base)
return module.FLMSample(**base)
def test_legacy_workspace_is_migrated_to_flm(tmp_path):
module, _ = _load_flm_workspace_module(tmp_path)
legacy_name = "".join(("f", "t", "m"))
legacy_root = tmp_path / "starpilot" / "data" / "galaxy" / legacy_name
legacy_report = legacy_root / "reports" / "legacy.json"
legacy_report.parent.mkdir(parents=True)
legacy_report.write_text(json.dumps({
"reportId": "legacy",
f"{legacy_name}Overrides": {"vehicleKnobs": {"generic.ff_gain_left": 0.1}},
"profileLabel": legacy_name.upper(),
}), encoding="utf-8")
workspace = module.ensure_flm_workspace()
migrated = json.loads((workspace["reports"] / "legacy.json").read_text(encoding="utf-8"))
assert not legacy_root.exists()
assert migrated["flmOverrides"]["vehicleKnobs"]["generic.ff_gain_left"] == pytest.approx(0.1)
assert migrated["profileLabel"] == "FLM"
def test_classify_torque_samples_detects_center_chatter(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
samples = []
for idx in range(60):
angle = 0.75 * math.sin(idx * 0.9)
@@ -199,7 +219,7 @@ def test_classify_torque_samples_detects_center_chatter(tmp_path):
def test_plot_context_stops_at_ineligible_samples(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
samples = [_sample(module, t=idx * 0.1, desired_la=idx * 0.01, actual_la=idx * 0.009) for idx in range(20)]
eligibility = [True] * len(samples)
eligibility[5] = False
@@ -220,7 +240,7 @@ def test_plot_context_stops_at_ineligible_samples(tmp_path):
def test_analysis_eligibility_masks_driver_override_with_settle_buffer(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
samples = [
_sample(module, t=idx * 0.1, steering_pressed=(idx == 20))
for idx in range(50)
@@ -235,7 +255,7 @@ def test_analysis_eligibility_masks_driver_override_with_settle_buffer(tmp_path)
def test_stock_param_state_captures_generic_and_rich_defaults(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
torque_tune = SimpleNamespace(friction=0.09, latAccelFactor=3.0)
lateral_tuning = SimpleNamespace(which=lambda: "torque", torque=torque_tune)
CP = SimpleNamespace(lateralTuning=lateral_tuning, steerActuatorDelay=0.1, steerRatio=14.26)
@@ -247,12 +267,12 @@ def test_stock_param_state_captures_generic_and_rich_defaults(tmp_path):
assert stock["UseAutoSteerDelay"] is True
assert stock["SteerDelay"] == pytest.approx(0.3)
assert stock["SteerRatio"] == pytest.approx(14.26)
assert len(stock["FTMBaseFrictionThresholds"]["hkg_canfd"]["values"]) == 5
assert stock["FTMVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(1.64)
assert len(stock["FLMBaseFrictionThresholds"]["hkg_canfd"]["values"]) == 5
assert stock["FLMVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(1.64)
def test_classify_torque_samples_does_not_bridge_driver_override(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
samples = []
for idx in range(80):
samples.append(_sample(
@@ -271,7 +291,7 @@ def test_classify_torque_samples_does_not_bridge_driver_override(tmp_path):
def test_build_suggestions_prefers_rich_low_speed_turn_in_knob(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "low_speed_unwillingness",
"dimensionId": "low_speed_unwillingness:left:low",
@@ -291,7 +311,7 @@ def test_build_suggestions_prefers_rich_low_speed_turn_in_knob(tmp_path):
def test_build_suggestions_baseline_prefers_generic_lat_accel_for_understeer(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "understeer",
"dimensionId": "understeer:left:mid",
@@ -312,7 +332,7 @@ def test_build_suggestions_baseline_prefers_generic_lat_accel_for_understeer(tmp
def test_build_suggestions_baseline_respects_asymmetric_nonlinear_map(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "understeer",
"dimensionId": "understeer:right:mid",
@@ -342,7 +362,7 @@ def test_build_suggestions_baseline_respects_asymmetric_nonlinear_map(tmp_path):
def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "low_speed_unwillingness",
"dimensionId": "low_speed_unwillingness:left:low",
@@ -356,7 +376,7 @@ def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
current = {
"SteerLatAccel": 1.8,
"SteerFriction": 0.2,
"FTMActiveOverrides": {
"FLMActiveOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {
@@ -374,7 +394,7 @@ def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
def test_build_suggestions_prefers_ioniq_6_curvy_trim_for_mid_speed_turn_in(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "oversteer",
"dimensionId": "oversteer:left:fast",
@@ -395,7 +415,7 @@ def test_build_suggestions_prefers_ioniq_6_curvy_trim_for_mid_speed_turn_in(tmp_
def test_build_suggestions_rebases_friction_curve_against_active_override(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summary = {
"bucket": "center_chatter",
"dimensionId": "center_chatter:center:highway",
@@ -410,7 +430,7 @@ def test_build_suggestions_rebases_friction_curve_against_active_override(tmp_pa
current = {
"SteerLatAccel": 1.8,
"SteerFriction": 0.2,
"FTMActiveOverrides": {
"FLMActiveOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {
"standard": {
@@ -431,7 +451,7 @@ def test_build_suggestions_rebases_friction_curve_against_active_override(tmp_pa
def test_select_primary_tuning_path_prefers_baseline_for_broad_mismatch(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summaries = [
{"bucket": "understeer", "severity": 1.0},
{"bucket": "center_chatter", "severity": 0.9},
@@ -446,7 +466,7 @@ def test_select_primary_tuning_path_prefers_baseline_for_broad_mismatch(tmp_path
def test_select_primary_tuning_path_prefers_cleanup_for_localized_issue(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summaries = [
{"bucket": "notchy_mid_curve", "severity": 0.7},
{"bucket": "center_chatter", "severity": 0.55},
@@ -459,7 +479,7 @@ def test_select_primary_tuning_path_prefers_cleanup_for_localized_issue(tmp_path
def test_select_primary_tuning_path_vetoes_baseline_when_global_fit_is_strong(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summaries = [
{
"bucket": "late_turn_in",
@@ -478,7 +498,7 @@ def test_select_primary_tuning_path_vetoes_baseline_when_global_fit_is_strong(tm
def test_conflicting_summary_resolution_keeps_dominant_direction(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
summaries = [
{"bucket": "early_turn_in", "severity": 1.34, "evidence": {"directionBias": "right", "speedBand": "mid", "eventCount": 1}},
{"bucket": "late_turn_in", "severity": 1.03, "evidence": {"directionBias": "right", "speedBand": "mid", "eventCount": 18}},
@@ -489,7 +509,7 @@ def test_conflicting_summary_resolution_keeps_dominant_direction(tmp_path):
def test_build_trial_profiles_suppresses_ignored_dimensions(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
suggestions = [
{
"dimensionId": "center_chatter:center:highway",
@@ -518,11 +538,11 @@ def test_build_trial_profiles_suppresses_ignored_dimensions(tmp_path):
assert profiles
assert profiles[0]["genericParams"]["ForceAutoTuneOff"] is True
assert profiles[0]["genericParams"]["SteerLatAccel"] > 1.6
assert profiles[0]["ftmOverrides"] == {}
assert profiles[0]["flmOverrides"] == {}
def test_build_trial_profiles_returns_none_when_every_dimension_is_ignored(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
suggestion = {
"dimensionId": "understeer:left:mid",
"severity": 0.8,
@@ -545,7 +565,7 @@ def test_build_trial_profiles_returns_none_when_every_dimension_is_ignored(tmp_p
def test_merge_primary_adjustments_averages_conflicting_deltas(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
suggestions = [
{
"severity": 1.0,
@@ -575,7 +595,7 @@ def test_merge_primary_adjustments_averages_conflicting_deltas(tmp_path):
def test_merge_primary_adjustments_disables_auto_delay_for_manual_delay_trial(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
module, _ = _load_flm_workspace_module(tmp_path)
suggestions = [{
"severity": 1.0,
"primaryAdjustmentRaw": {
@@ -595,8 +615,8 @@ def test_merge_primary_adjustments_disables_auto_delay_for_manual_delay_trial(tm
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()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-apply"
profile_id = f"{report_id}:recommended"
profile = {
@@ -610,7 +630,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
"ForceAutoTuneOff": True,
"ForceAutoTune": False,
},
"ftmOverrides": {
"flmOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {
@@ -625,13 +645,13 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
"ForceAutoTune": True,
"ForceAutoTuneOff": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {
"FLMActiveProfileId": "",
"FLMActiveOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {"hyundai_ioniq_6.unwind_taper_left": 0.55},
},
"FTMTrialApplied": False,
"FLMTrialApplied": False,
}
result = module.apply_trial_profile(report_id, profile_id)
@@ -643,24 +663,24 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert active_snapshot["params"]["SteerLatAccel"] == pytest.approx(1.5)
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["FTMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
assert fake_params_cls._store["FLMActiveProfileId"] == profile_id
assert fake_params_cls._store["FLMTrialApplied"] is True
assert fake_params_cls._store["FLMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
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
assert "FTMTrialBaseline" not in fake_params_cls._store
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
assert fake_params_cls._store["FLMTrialApplied"] is False
assert "FLMTrialBaseline" not in fake_params_cls._store
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
first_report_id = "report-first"
first_profile_id = f"{first_report_id}:cleanup_pass:recommended"
second_report_id = "report-second"
@@ -671,7 +691,7 @@ def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
"pathKey": "cleanup_pass",
"pathLabel": "Cleanup Pass",
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
"ftmOverrides": {
"flmOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {"hyundai_ioniq_6.turn_in_boost_left": 0.08},
@@ -683,7 +703,7 @@ def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
"pathKey": "cleanup_pass",
"pathLabel": "Cleanup Pass",
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.9},
"ftmOverrides": {
"flmOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {"hyundai_ioniq_6.unwind_taper_left": 0.62},
@@ -694,9 +714,9 @@ def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
fake_params_cls._store = {
"AdvancedLateralTune": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": False,
"FLMActiveProfileId": "",
"FLMActiveOverrides": {},
"FLMTrialApplied": False,
}
module.apply_trial_profile(first_report_id, first_profile_id)
@@ -705,23 +725,23 @@ def test_repeated_trial_revisions_revert_to_original_baseline(tmp_path):
active_snapshot = json.loads((workspace["snapshots"] / "active.json").read_text(encoding="utf-8"))
assert active_snapshot["revisionCount"] == 2
assert active_snapshot["params"]["SteerLatAccel"] == pytest.approx(1.5)
assert active_snapshot["params"]["FTMTrialApplied"] is False
assert active_snapshot["params"]["FLMTrialApplied"] is False
assert active_snapshot["appliedGenericParams"]["SteerLatAccel"] == pytest.approx(1.9)
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert active_snapshot["appliedVehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.62)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.62)
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.62)
module.revert_trial_profile()
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
assert fake_params_cls._store["FTMActiveOverrides"] == {}
assert fake_params_cls._store["FLMTrialApplied"] is False
assert fake_params_cls._store["FLMActiveOverrides"] == {}
def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-recovery"
profile_id = f"{report_id}:cleanup_pass:recommended"
profile = {
@@ -730,15 +750,15 @@ def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
"pathKey": "cleanup_pass",
"pathLabel": "Cleanup Pass",
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
"ftmOverrides": {},
"flmOverrides": {},
}
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
fake_params_cls._store = {
"AdvancedLateralTune": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": False,
"FLMActiveProfileId": "",
"FLMActiveOverrides": {},
"FLMTrialApplied": False,
}
module.apply_trial_profile(report_id, profile_id)
(workspace["snapshots"] / "active.json").unlink()
@@ -749,27 +769,27 @@ def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
module.revert_trial_profile()
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FTMTrialApplied"] is False
assert fake_params_cls._store["FLMTrialApplied"] is False
def test_persistent_baseline_recovers_when_snapshot_files_are_missing(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-persistent-recovery"
profile_id = f"{report_id}:cleanup_pass:recommended"
profile = {
"id": profile_id,
"label": "Recommended",
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
"ftmOverrides": {},
"flmOverrides": {},
}
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
fake_params_cls._store = {
"AdvancedLateralTune": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": False,
"FLMActiveProfileId": "",
"FLMActiveOverrides": {},
"FLMTrialApplied": False,
}
module.apply_trial_profile(report_id, profile_id)
@@ -782,12 +802,12 @@ def test_persistent_baseline_recovers_when_snapshot_files_are_missing(tmp_path):
module.revert_trial_profile()
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
assert fake_params_cls._store["FTMTrialApplied"] is False
assert fake_params_cls._store["FLMTrialApplied"] is False
def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-source-recovery"
profile_id = f"{report_id}:baseline_fix:assertive"
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps({
@@ -796,17 +816,17 @@ def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
"currentParams": {
"AdvancedLateralTune": False,
"SteerLatAccel": 1.5,
"FTMActiveProfileId": "",
"FTMActiveOverrides": {},
"FTMTrialApplied": False,
"FLMActiveProfileId": "",
"FLMActiveOverrides": {},
"FLMTrialApplied": False,
},
}), encoding="utf-8")
fake_params_cls._store = {
"AdvancedLateralTune": True,
"SteerLatAccel": 1.9,
"FTMActiveProfileId": profile_id,
"FTMActiveOverrides": {},
"FTMTrialApplied": True,
"FLMActiveProfileId": profile_id,
"FLMActiveOverrides": {},
"FLMTrialApplied": True,
}
active_trial = module.list_workspace()["activeTrial"]
@@ -816,18 +836,18 @@ def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
module.revert_trial_profile()
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
assert fake_params_cls._store["FLMTrialApplied"] is False
def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
module.ensure_flm_workspace()
fake_params_cls._store = {
"AdvancedLateralTune": True,
"SteerLatAccel": 1.9,
"FTMActiveProfileId": "missing-report:baseline_fix:assertive",
"FTMActiveOverrides": {"vehicleKnobs": {"generic.turn_in_boost_left": 0.1}},
"FTMTrialApplied": True,
"FLMActiveProfileId": "missing-report:baseline_fix:assertive",
"FLMActiveOverrides": {"vehicleKnobs": {"generic.turn_in_boost_left": 0.1}},
"FLMTrialApplied": True,
}
active_trial = module.list_workspace()["activeTrial"]
@@ -835,14 +855,14 @@ def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path):
module.accept_trial_as_baseline()
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1)
assert fake_params_cls._store["FTMActiveProfileId"] == ""
assert fake_params_cls._store["FTMTrialApplied"] is False
assert fake_params_cls._store["FLMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1)
assert fake_params_cls._store["FLMActiveProfileId"] == ""
assert fake_params_cls._store["FLMTrialApplied"] is False
def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, _ = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-existing"
profile_id = f"{report_id}:cleanup_pass:recommended"
profile = {
@@ -851,7 +871,7 @@ def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path)
"pathKey": "cleanup_pass",
"pathLabel": "Cleanup Pass",
"genericParams": {"AdvancedLateralTune": True, "SteerFriction": 0.25},
"ftmOverrides": {
"flmOverrides": {
"schemaVersion": 1,
"baseFrictionThresholds": {},
"vehicleKnobs": {"hyundai_ioniq_6.ff_gain_left": 0.15},
@@ -872,8 +892,8 @@ def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path)
def test_delete_report_removes_saved_artifacts(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, _ = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-delete"
for path in (
workspace["reports"] / f"{report_id}.json",
@@ -894,12 +914,12 @@ def test_delete_report_removes_saved_artifacts(tmp_path):
def test_delete_report_is_blocked_while_trial_is_active(tmp_path):
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-active-delete"
report_path = workspace["reports"] / f"{report_id}.json"
report_path.write_text("{}", encoding="utf-8")
fake_params_cls._store = {"FTMTrialApplied": True}
fake_params_cls._store = {"FLMTrialApplied": True}
with pytest.raises(RuntimeError, match="Revert or keep"):
module.delete_report(report_id)
@@ -907,8 +927,8 @@ def test_delete_report_is_blocked_while_trial_is_active(tmp_path):
def test_select_report_path_persists_manual_override(tmp_path):
module, _ = _load_ftm_workspace_module(tmp_path)
workspace = module.ensure_ftm_workspace()
module, _ = _load_flm_workspace_module(tmp_path)
workspace = module.ensure_flm_workspace()
report_id = "report-path"
suggestion_base = {
"evidence": {"speedBand": "mixed", "directionBias": "center", "eventCount": 0, "segments": []},
+66 -52
View File
@@ -77,7 +77,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 ftm_workspace, utilities
from openpilot.starpilot.system.the_galaxy import flm_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")
@@ -85,6 +85,7 @@ DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
GITLAB_API = "https://gitlab.com/api/v4"
GITLAB_SUBMISSIONS_PROJECT_ID = "71992109"
GITLAB_TOKEN = os.environ.get("GITLAB_TOKEN", "")
LEGACY_LATERAL_METHOD_API_PREFIX = "/api/" + "".join(("f", "t", "m"))
GALAXY_DEPS_PATH = "/data/galaxy_deps"
LEGACY_GALAXY_DEPS_PATH = "/data/" + "".join(chr(code) for code in (112, 111, 110, 100)) + "_deps"
@@ -5115,7 +5116,8 @@ def setup(app):
def generate():
routes = [(path, name) for path in FOOTAGE_PATHS for name in utilities.get_routes_names(path)]
total = len(routes)
yield f"data: {json.dumps({'progress': 0, 'total': total})}\n\n"
connect_dongle_id = params.get("StockDongleId", encoding="utf-8") or params.get("DongleId", encoding="utf-8") or ""
yield f"data: {json.dumps({'progress': 0, 'total': total, 'connectDongleId': connect_dongle_id})}\n\n"
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(utilities.process_route, path, name): (path, name) for path, name in routes}
@@ -5685,87 +5687,96 @@ 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()
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/status", methods=["GET"])
@app.route("/api/flm/status", methods=["GET"])
def get_flm_status():
workspace = flm_workspace.list_workspace()
return jsonify({
"isOnroad": params.get_bool("IsOnroad"),
"status": ftm_workspace.read_ftm_status(),
"status": flm_workspace.read_flm_status(),
"activeTrial": workspace.get("activeTrial"),
"reports": workspace.get("reports", [])[:10],
}), 200
@app.route("/api/ftm/analyze", methods=["POST"])
def start_ftm_analysis():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/analyze", methods=["POST"])
@app.route("/api/flm/analyze", methods=["POST"])
def start_flm_analysis():
if params.get_bool("IsOnroad"):
return jsonify({"error": "FTM analysis can only run offroad."}), 409
return jsonify({"error": "FLM 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)
started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS)
if not started:
return jsonify({"error": "Failed to start FTM analysis."}), 500
return jsonify({"error": "Failed to start FLM 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(),
"message": f"Started FLM analysis for {len(route_names[:flm_workspace.FLM_ANALYZER_ROUTE_LIMIT])} route(s).",
"status": flm_workspace.read_flm_status(),
}), 200
@app.route("/api/ftm/analyze/stop", methods=["POST"])
def stop_ftm_analysis():
stopped = ftm_workspace.stop_ftm_background_analysis()
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/analyze/stop", methods=["POST"])
@app.route("/api/flm/analyze/stop", methods=["POST"])
def stop_flm_analysis():
stopped = flm_workspace.stop_flm_background_analysis()
return jsonify({
"message": "Stopped FTM analysis." if stopped else "No active FTM analysis was running.",
"message": "Stopped FLM analysis." if stopped else "No active FLM analysis was running.",
"stopped": bool(stopped),
"status": ftm_workspace.read_ftm_status(),
"status": flm_workspace.read_flm_status(),
}), 200
@app.route("/api/ftm/report/<report_id>", methods=["GET"])
def get_ftm_report(report_id):
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/report/<report_id>", methods=["GET"])
@app.route("/api/flm/report/<report_id>", methods=["GET"])
def get_flm_report(report_id):
try:
return jsonify(ftm_workspace.load_report(report_id)), 200
return jsonify(flm_workspace.load_report(report_id)), 200
except FileNotFoundError:
return jsonify({"error": "FTM report not found."}), 404
return jsonify({"error": "FLM report not found."}), 404
@app.route("/api/ftm/report/<report_id>", methods=["DELETE"])
def delete_ftm_report(report_id):
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/report/<report_id>", methods=["DELETE"])
@app.route("/api/flm/report/<report_id>", methods=["DELETE"])
def delete_flm_report(report_id):
try:
return jsonify(ftm_workspace.delete_report(report_id)), 200
return jsonify(flm_workspace.delete_report(report_id)), 200
except FileNotFoundError:
return jsonify({"error": "FTM report not found."}), 404
return jsonify({"error": "FLM report not found."}), 404
except RuntimeError as error:
return jsonify({"error": str(error)}), 409
@app.route("/api/ftm/report/<report_id>/path", methods=["POST"])
def select_ftm_report_path(report_id):
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/report/<report_id>/path", methods=["POST"])
@app.route("/api/flm/report/<report_id>/path", methods=["POST"])
def select_flm_report_path(report_id):
data = request.get_json(silent=True) or {}
path_key = str(data.get("pathKey") or "").strip()
if not path_key:
return jsonify({"error": "pathKey is required."}), 400
try:
return jsonify(ftm_workspace.select_report_path(report_id, path_key)), 200
return jsonify(flm_workspace.select_report_path(report_id, path_key)), 200
except FileNotFoundError:
return jsonify({"error": "FTM report not found."}), 404
return jsonify({"error": "FLM report not found."}), 404
except ValueError as error:
return jsonify({"error": str(error)}), 400
@app.route("/api/ftm/workspace", methods=["GET"])
def get_ftm_workspace():
return jsonify(ftm_workspace.list_workspace()), 200
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/workspace", methods=["GET"])
@app.route("/api/flm/workspace", methods=["GET"])
def get_flm_workspace():
return jsonify(flm_workspace.list_workspace()), 200
@app.route("/api/ftm/workspace/clear", methods=["POST"])
def clear_ftm_workspace():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/workspace/clear", methods=["POST"])
@app.route("/api/flm/workspace/clear", methods=["POST"])
def clear_flm_workspace():
try:
return jsonify(ftm_workspace.clear_workspace()), 200
return jsonify(flm_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():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/trials/apply", methods=["POST"])
@app.route("/api/flm/trials/apply", methods=["POST"])
def apply_flm_trial():
data = request.get_json(silent=True) or {}
report_id = str(data.get("reportId") or "").strip()
profile_id = str(data.get("profileId") or "").strip()
@@ -5773,34 +5784,37 @@ def setup(app):
return jsonify({"error": "Both reportId and profileId are required."}), 400
try:
result = ftm_workspace.apply_trial_profile(report_id, profile_id)
result = flm_workspace.apply_trial_profile(report_id, profile_id)
except FileNotFoundError:
return jsonify({"error": "FTM profile not found."}), 404
return jsonify({"error": "FLM profile not found."}), 404
except RuntimeError as error:
return jsonify({"error": str(error)}), 409
return jsonify(result), 200
@app.route("/api/ftm/trials/revert", methods=["POST"])
def revert_ftm_trial():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/trials/revert", methods=["POST"])
@app.route("/api/flm/trials/revert", methods=["POST"])
def revert_flm_trial():
try:
result = ftm_workspace.revert_trial_profile()
result = flm_workspace.revert_trial_profile()
except FileNotFoundError:
return jsonify({"error": "No active FTM trial snapshot was found."}), 404
return jsonify({"error": "No active FLM trial snapshot was found."}), 404
return jsonify(result), 200
@app.route("/api/ftm/trials/accept", methods=["POST"])
def accept_ftm_trial():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/trials/accept", methods=["POST"])
@app.route("/api/flm/trials/accept", methods=["POST"])
def accept_flm_trial():
try:
result = ftm_workspace.accept_trial_as_baseline()
result = flm_workspace.accept_trial_as_baseline()
except FileNotFoundError:
return jsonify({"error": "No active FTM trial was found."}), 404
return jsonify({"error": "No active FLM trial was found."}), 404
return jsonify(result), 200
@app.route("/api/ftm/feedback", methods=["POST"])
def save_ftm_feedback():
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/feedback", methods=["POST"])
@app.route("/api/flm/feedback", methods=["POST"])
def save_flm_feedback():
data = request.get_json(silent=True) or {}
report_id = str(data.get("reportId") or "").strip()
if not report_id:
@@ -5812,9 +5826,9 @@ def setup(app):
"notes": data.get("notes", ""),
}
try:
result = ftm_workspace.record_feedback(report_id, feedback)
result = flm_workspace.record_feedback(report_id, feedback)
except FileNotFoundError:
return jsonify({"error": "FTM report not found."}), 404
return jsonify({"error": "FLM report not found."}), 404
return jsonify(result), 200
+35 -1
View File
@@ -20,8 +20,16 @@ LAUNCH_PARAM_MIGRATION_MARKER = ".starpilot_launch_param_migrations_v2"
BRANCH_DEFAULTS_MIGRATION_MARKER = ".starpilot_branch_defaults_migrations_v1"
ACCELERATION_PROFILE_MIGRATION_MARKER = ".starpilot_acceleration_profile_default_v1"
USE_OLD_UI_MIGRATION_MARKER = ".starpilot_use_old_ui_migration_v1"
LATERAL_METHOD_REBRAND_MIGRATION_MARKER = ".starpilot_lateral_method_rebrand_v1"
MARKER_DIRNAME = ".starpilot_param_migrations"
LATERAL_METHOD_PARAM_SUFFIXES = (
"ActiveOverrides",
"ActiveProfileId",
"TrialBaseline",
"TrialApplied",
)
LEGACY_CE_STOPPED_LEAD_DEFAULT = True
LEGACY_FORCE_STOPS_DEFAULT = False
LEGACY_AGGRESSIVE_FOLLOW_HIGH_DEFAULT = 1.25
@@ -88,6 +96,10 @@ def _use_old_ui_marker_path(params: ParamsLike) -> Path:
return _marker_dir_path(params) / USE_OLD_UI_MIGRATION_MARKER
def _lateral_method_rebrand_marker_path(params: ParamsLike) -> Path:
return _marker_dir_path(params) / LATERAL_METHOD_REBRAND_MIGRATION_MARKER
def _marker_dir_path(params: ParamsLike) -> Path:
params_path = Path(params.get_param_path())
# Params.clear_all() removes unknown files inside the params directory, so
@@ -180,10 +192,29 @@ def _apply_use_old_ui_migration(params: ParamsLike, marker: Path) -> None:
marker.touch()
def _apply_lateral_method_rebrand_migration(params: ParamsLike, marker: Path) -> None:
if marker.exists():
return
marker.parent.mkdir(parents=True, exist_ok=True)
legacy_prefix = "".join(("F", "T", "M"))
for suffix in LATERAL_METHOD_PARAM_SUFFIXES:
legacy_path = Path(params.get_param_path(f"{legacy_prefix}{suffix}"))
current_path = Path(params.get_param_path(f"FLM{suffix}"))
if legacy_path.is_file():
current_path.parent.mkdir(parents=True, exist_ok=True)
current_path.write_bytes(legacy_path.read_bytes())
legacy_path.unlink()
marker.touch()
def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None = None,
branch_defaults_marker_path: Path | None = None,
acceleration_profile_marker_path: Path | None = None,
use_old_ui_marker_path: Path | None = None) -> None:
use_old_ui_marker_path: Path | None = None,
lateral_method_rebrand_marker_path: Path | None = None) -> None:
_apply_legacy_launch_param_migrations(params, marker_path or _default_marker_path(params))
# Keep branch-default rollout on its own marker so older installs that already
# have the legacy marker still receive this one-time param reset.
@@ -192,6 +223,9 @@ def apply_launch_param_migrations(params: ParamsLike, marker_path: Path | None =
params, acceleration_profile_marker_path or _acceleration_profile_marker_path(params)
)
_apply_use_old_ui_migration(params, use_old_ui_marker_path or _use_old_ui_marker_path(params))
_apply_lateral_method_rebrand_migration(
params, lateral_method_rebrand_marker_path or _lateral_method_rebrand_marker_path(params)
)
def main() -> int:
@@ -5,6 +5,7 @@ from openpilot.system.manager.launch_param_migrations import (
BRANCH_DEFAULTS_MIGRATION_MARKER,
DEFAULT_STEER_KP,
LAUNCH_PARAM_MIGRATION_MARKER,
LATERAL_METHOD_REBRAND_MIGRATION_MARKER,
MARKER_DIRNAME,
STANDARD_ACCELERATION_PROFILE,
USE_OLD_UI_MIGRATION_MARKER,
@@ -49,6 +50,9 @@ class FileBackedFakeParams:
def put_float(self, key, value):
Path(self.get_param_path(key)).write_text(str(float(value)), encoding="utf-8")
def put(self, key, value):
Path(self.get_param_path(key)).write_text(str(value), encoding="utf-8")
def marker_path(tmp_path: Path, marker_name: str) -> Path:
path = tmp_path / MARKER_DIRNAME / "params" / marker_name
@@ -235,3 +239,23 @@ def test_apply_launch_param_migrations_does_not_overwrite_use_old_ui(tmp_path):
apply_launch_param_migrations(params)
assert not params.get_bool("UseOldUI")
def test_apply_launch_param_migrations_preserves_active_lateral_method_trial(tmp_path):
params = FileBackedFakeParams(tmp_path / "params")
legacy_prefix = "".join(("F", "T", "M"))
legacy_values = {
"ActiveOverrides": '{"vehicleKnobs":{"generic.ff_gain_left":0.12}}',
"ActiveProfileId": "report:cleanup:recommended",
"TrialBaseline": '{"params":{"SteerLatAccel":1.8}}',
"TrialApplied": "1",
}
for suffix, value in legacy_values.items():
params.put(f"{legacy_prefix}{suffix}", value)
apply_launch_param_migrations(params)
for suffix, value in legacy_values.items():
assert params.get(f"FLM{suffix}") == value
assert not Path(params.get_param_path(f"{legacy_prefix}{suffix}")).exists()
assert marker_path(tmp_path, LATERAL_METHOD_REBRAND_MIGRATION_MARKER).is_file()
+1 -1
View File
@@ -32,7 +32,7 @@ KNOWN_READ_ONLY = {
"ClusterOffset", "Compass", "DeveloperSidebarMetric1", "DeveloperSidebarMetric2",
"DeveloperSidebarMetric3", "DeveloperSidebarMetric4", "DeveloperSidebarMetric5",
"DeveloperSidebarMetric6", "DeveloperSidebarMetric7", "DongleId",
"FTMActiveOverrides", "FTMActiveProfileId", "FTMTrialApplied", "FTMTrialBaseline",
"FLMActiveOverrides", "FLMActiveProfileId", "FLMTrialApplied", "FLMTrialBaseline",
"StarPilotCarParamsPersistent", "StarPilotDrives", "StarPilotKilometers",
"StarPilotMinutes", "GitBranch", "GitCommit", "GitCommitDate", "GitDiff",
"GitRemote", "GithubSshKeys", "GithubUsername", "HardwareSerial", "IMEI",
@@ -3,11 +3,11 @@ import argparse
import json
from openpilot.system.hardware.hw import Paths
from openpilot.starpilot.system.the_galaxy import ftm_workspace
from openpilot.starpilot.system.the_galaxy import flm_workspace
def main() -> None:
parser = argparse.ArgumentParser(description="Run the Firestar Tuning Method analyzer against local routes.")
parser = argparse.ArgumentParser(description="Run the Firestar Lateral 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()
@@ -17,7 +17,7 @@ def main() -> None:
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)
report = flm_workspace.analyze_routes(args.routes, footage_paths, report_id=args.report_id)
print(json.dumps({
"reportId": report["reportId"],
"htmlPath": report["htmlPath"],