mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-08 09:03:42 +08:00
FrogPilot 0.9.7
This commit is contained in:
+208
-10
@@ -3,12 +3,13 @@ import os
|
||||
import numpy as np
|
||||
import tomllib
|
||||
from abc import abstractmethod, ABC
|
||||
from difflib import SequenceMatcher
|
||||
from enum import StrEnum
|
||||
from typing import Any, NamedTuple
|
||||
from collections.abc import Callable
|
||||
from functools import cache
|
||||
|
||||
from cereal import car
|
||||
from cereal import car, custom
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.simple_kalman import KF1D, get_kalman_gain
|
||||
@@ -16,11 +17,14 @@ from openpilot.common.numpy_fast import clip
|
||||
from openpilot.common.realtime import DT_CTRL
|
||||
from openpilot.selfdrive.car import apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, STD_CARGO_KG
|
||||
from openpilot.selfdrive.car.values import PLATFORMS
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, get_friction
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CRUISE_LONG_PRESS, V_CRUISE_MAX, get_friction
|
||||
from openpilot.selfdrive.controls.lib.events import Events
|
||||
from openpilot.selfdrive.controls.lib.vehicle_model import VehicleModel
|
||||
|
||||
from openpilot.selfdrive.frogpilot.frogpilot_variables import get_frogpilot_toggles, params, params_memory
|
||||
|
||||
ButtonType = car.CarState.ButtonEvent.Type
|
||||
FrogPilotButtonType = custom.FrogPilotCarState.ButtonEvent.Type
|
||||
GearShifter = car.CarState.GearShifter
|
||||
EventName = car.CarEvent.EventName
|
||||
|
||||
@@ -29,10 +33,15 @@ ACCEL_MAX = 2.0
|
||||
ACCEL_MIN = -3.5
|
||||
FRICTION_THRESHOLD = 0.3
|
||||
|
||||
NEURAL_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/neural_ff_weights.json')
|
||||
TORQUE_NN_MODEL_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/lat_models')
|
||||
TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/params.toml')
|
||||
TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/override.toml')
|
||||
TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/substitute.toml')
|
||||
|
||||
# dict used to rename activation functions whose names aren't valid python identifiers
|
||||
ACTIVATION_FUNCTION_NAMES = {'σ': 'sigmoid'}
|
||||
|
||||
GEAR_SHIFTER_MAP: dict[str, car.CarState.GearShifter] = {
|
||||
'P': GearShifter.park, 'PARK': GearShifter.park,
|
||||
'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse,
|
||||
@@ -45,6 +54,8 @@ GEAR_SHIFTER_MAP: dict[str, car.CarState.GearShifter] = {
|
||||
'B': GearShifter.brake, 'BRAKE': GearShifter.brake,
|
||||
}
|
||||
|
||||
def similarity(s1: str, s2: str) -> float:
|
||||
return SequenceMatcher(None, s1, s2).ratio()
|
||||
|
||||
class LatControlInputs(NamedTuple):
|
||||
lateral_acceleration: float
|
||||
@@ -85,6 +96,108 @@ def get_torque_params():
|
||||
|
||||
return torque_params
|
||||
|
||||
# Twilsonco's Lateral Neural Network Feedforward
|
||||
class FluxModel:
|
||||
def __init__(self, params_file, zero_bias=False):
|
||||
with open(params_file, "r") as f:
|
||||
params = json.load(f)
|
||||
|
||||
self.input_size = params["input_size"]
|
||||
self.output_size = params["output_size"]
|
||||
self.input_mean = np.array(params["input_mean"], dtype=np.float32).T
|
||||
self.input_std = np.array(params["input_std"], dtype=np.float32).T
|
||||
self.layers = []
|
||||
self.friction_override = False
|
||||
|
||||
for layer_params in params["layers"]:
|
||||
W = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_W'))], dtype=np.float32).T
|
||||
b = np.array(layer_params[next(key for key in layer_params.keys() if key.endswith('_b'))], dtype=np.float32).T
|
||||
if zero_bias:
|
||||
b = np.zeros_like(b)
|
||||
activation = layer_params["activation"]
|
||||
for k, v in ACTIVATION_FUNCTION_NAMES.items():
|
||||
activation = activation.replace(k, v)
|
||||
self.layers.append((W, b, activation))
|
||||
|
||||
self.validate_layers()
|
||||
self.check_for_friction_override()
|
||||
|
||||
# Begin activation functions.
|
||||
# These are called by name using the keys in the model json file
|
||||
@staticmethod
|
||||
def sigmoid(x):
|
||||
return 1 / (1 + np.exp(-x))
|
||||
|
||||
@staticmethod
|
||||
def identity(x):
|
||||
return x
|
||||
# End activation functions
|
||||
|
||||
def forward(self, x):
|
||||
for W, b, activation in self.layers:
|
||||
x = getattr(self, activation)(x.dot(W) + b)
|
||||
return x
|
||||
|
||||
def evaluate(self, input_array):
|
||||
in_len = len(input_array)
|
||||
if in_len != self.input_size:
|
||||
# If the input is length 2-4, then it's a simplified evaluation.
|
||||
# In that case, need to add on zeros to fill out the input array to match the correct length.
|
||||
if 2 <= in_len:
|
||||
input_array = input_array + [0] * (self.input_size - in_len)
|
||||
else:
|
||||
raise ValueError(f"Input array length {len(input_array)} must be length 2 or greater")
|
||||
|
||||
input_array = np.array(input_array, dtype=np.float32)
|
||||
|
||||
# Rescale the input array using the input_mean and input_std
|
||||
input_array = (input_array - self.input_mean) / self.input_std
|
||||
|
||||
output_array = self.forward(input_array)
|
||||
|
||||
return float(output_array[0, 0])
|
||||
|
||||
def validate_layers(self):
|
||||
for W, b, activation in self.layers:
|
||||
if not hasattr(self, activation):
|
||||
raise ValueError(f"Unknown activation: {activation}")
|
||||
|
||||
def check_for_friction_override(self):
|
||||
y = self.evaluate([10.0, 0.0, 0.2])
|
||||
self.friction_override = (y < 0.1)
|
||||
|
||||
def get_nn_model_path(car, eps_firmware) -> tuple[str | None, float]:
|
||||
def check_nn_path(check_model):
|
||||
model_path = None
|
||||
max_similarity = -1.0
|
||||
for f in os.listdir(TORQUE_NN_MODEL_PATH):
|
||||
if f.endswith(".json"):
|
||||
model = f.replace(".json", "").replace(f"{TORQUE_NN_MODEL_PATH}/", "")
|
||||
similarity_score = similarity(model, check_model)
|
||||
if similarity_score > max_similarity:
|
||||
max_similarity = similarity_score
|
||||
model_path = os.path.join(TORQUE_NN_MODEL_PATH, f)
|
||||
return model_path, max_similarity
|
||||
|
||||
if len(eps_firmware) > 3:
|
||||
eps_firmware = eps_firmware.replace("\\", "")
|
||||
check_model = f"{car} {eps_firmware}"
|
||||
else:
|
||||
check_model = car
|
||||
model_path, max_similarity = check_nn_path(check_model)
|
||||
if car not in model_path or 0.0 <= max_similarity < 0.9:
|
||||
check_model = car
|
||||
model_path, max_similarity = check_nn_path(check_model)
|
||||
if car not in model_path or 0.0 <= max_similarity < 0.9:
|
||||
model_path = None
|
||||
return model_path
|
||||
|
||||
def get_nn_model(car, eps_firmware) -> tuple[FluxModel | None, float]:
|
||||
model = get_nn_model_path(car, eps_firmware)
|
||||
if model is not None:
|
||||
model = FluxModel(model)
|
||||
return model
|
||||
|
||||
# generic car and radar interfaces
|
||||
|
||||
class CarInterfaceBase(ABC):
|
||||
@@ -110,8 +223,44 @@ class CarInterfaceBase(ABC):
|
||||
dbc_name = "" if self.cp is None else self.cp.dbc_name
|
||||
self.CC: CarControllerBase = CarController(dbc_name, CP, self.VM)
|
||||
|
||||
def apply(self, c: car.CarControl, now_nanos: int) -> tuple[car.CarControl.Actuators, list[tuple[int, int, bytes, int]]]:
|
||||
return self.CC.update(c, self.CS, now_nanos)
|
||||
# FrogPilot variables
|
||||
self.frogpilot_toggles = get_frogpilot_toggles()
|
||||
|
||||
eps_firmware = str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), ""))
|
||||
|
||||
comma_nnff_supported = self.check_comma_nn_ff_support(CP.carFingerprint)
|
||||
nnff_supported = self.initialize_lat_torque_nn(CP.carFingerprint, eps_firmware)
|
||||
|
||||
self.use_nnff = not comma_nnff_supported and nnff_supported and self.frogpilot_toggles.nnff
|
||||
self.use_nnff_lite = not self.use_nnff and self.frogpilot_toggles.nnff_lite
|
||||
|
||||
self.always_on_lateral_disabled = False
|
||||
self.belowSteerSpeed_shown = False
|
||||
self.disable_belowSteerSpeed = False
|
||||
self.disable_resumeRequired = False
|
||||
self.prev_distance_button = False
|
||||
self.resumeRequired_shown = False
|
||||
self.traffic_mode_active = False
|
||||
self.traffic_mode_changed = False
|
||||
|
||||
self.gap_counter = 0
|
||||
|
||||
self.is_gm = self.CP.carName == "gm"
|
||||
|
||||
def get_ff_nn(self, x):
|
||||
return self.lat_torque_nn_model.evaluate(x)
|
||||
|
||||
def check_comma_nn_ff_support(self, car):
|
||||
with open(NEURAL_PARAMS_PATH, 'r') as file:
|
||||
data = json.load(file)
|
||||
return car in data
|
||||
|
||||
def initialize_lat_torque_nn(self, car, eps_firmware) -> bool:
|
||||
self.lat_torque_nn_model = get_nn_model(car, eps_firmware)
|
||||
return self.lat_torque_nn_model is not None
|
||||
|
||||
def apply(self, c: car.CarControl, now_nanos: int, frogpilot_toggles) -> tuple[car.CarControl.Actuators, list[tuple[int, int, bytes, int]]]:
|
||||
return self.CC.update(c, self.CS, now_nanos, frogpilot_toggles)
|
||||
|
||||
@staticmethod
|
||||
def get_pid_accel_limits(CP, current_speed, cruise_speed):
|
||||
@@ -122,10 +271,10 @@ class CarInterfaceBase(ABC):
|
||||
"""
|
||||
Parameters essential to controlling the car may be incomplete or wrong without FW versions or fingerprints.
|
||||
"""
|
||||
return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False)
|
||||
return cls.get_params(candidate, gen_empty_fingerprint(), list(), False, False, False)
|
||||
|
||||
@classmethod
|
||||
def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[car.CarParams.CarFw], experimental_long: bool, docs: bool):
|
||||
def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[car.CarParams.CarFw], disable_openpilot_long: bool, experimental_long: bool, params: params, docs: bool):
|
||||
ret = CarInterfaceBase.get_std_params(candidate)
|
||||
|
||||
platform = PLATFORMS[candidate]
|
||||
@@ -138,7 +287,15 @@ class CarInterfaceBase(ABC):
|
||||
ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor
|
||||
ret.flags |= int(platform.config.flags)
|
||||
|
||||
ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs)
|
||||
ret = cls._get_params(ret, candidate, fingerprint, car_fw, disable_openpilot_long, experimental_long, docs)
|
||||
|
||||
# Enable torque controller for all cars that do not use angle based steering
|
||||
if ret.steerControlType != car.CarParams.SteerControlType.angle and params.get_bool("LateralTune") and params.get_bool("NNFF"):
|
||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
||||
eps_firmware = str(next((fw.fwVersion for fw in car_fw if fw.ecu == "eps"), ""))
|
||||
model = get_nn_model_path(candidate, eps_firmware)
|
||||
if model is not None:
|
||||
params.put("NNFFModelName", candidate.replace("_", " "))
|
||||
|
||||
# Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
|
||||
if not ret.notCar:
|
||||
@@ -230,14 +387,14 @@ class CarInterfaceBase(ABC):
|
||||
def _update(self, c: car.CarControl) -> car.CarState:
|
||||
pass
|
||||
|
||||
def update(self, c: car.CarControl, can_strings: list[bytes]) -> car.CarState:
|
||||
def update(self, c: car.CarControl, can_strings: list[bytes], frogpilot_toggles) -> car.CarState:
|
||||
# parse can
|
||||
for cp in self.can_parsers:
|
||||
if cp is not None:
|
||||
cp.update_strings(can_strings)
|
||||
|
||||
# get CarState
|
||||
ret = self._update(c)
|
||||
ret, fp_ret = self._update(c, frogpilot_toggles)
|
||||
|
||||
ret.canValid = all(cp.can_valid for cp in self.can_parsers if cp is not None)
|
||||
ret.canTimeout = any(cp.bus_timeout for cp in self.can_parsers if cp is not None)
|
||||
@@ -256,11 +413,18 @@ class CarInterfaceBase(ABC):
|
||||
if ret.cruiseState.speedCluster == 0:
|
||||
ret.cruiseState.speedCluster = ret.cruiseState.speed
|
||||
|
||||
# Add any additional frogpilotCarStates
|
||||
fp_ret.alwaysOnLateralDisabled = self.always_on_lateral_disabled
|
||||
fp_ret.distanceLongPressed = self.frogpilot_distance_functions(frogpilot_toggles)
|
||||
fp_ret.ecoGear |= ret.gearShifter == GearShifter.eco
|
||||
fp_ret.sportGear |= ret.gearShifter == GearShifter.sport
|
||||
fp_ret.trafficModeActive = self.traffic_mode_active
|
||||
|
||||
# copy back for next iteration
|
||||
if self.CS is not None:
|
||||
self.CS.out = ret.as_reader()
|
||||
|
||||
return ret
|
||||
return ret, fp_ret
|
||||
|
||||
|
||||
def create_common_events(self, cs_out, extra_gears=None, pcm_enable=True, allow_enable=True,
|
||||
@@ -310,6 +474,10 @@ class CarInterfaceBase(ABC):
|
||||
if b.type == ButtonType.cancel:
|
||||
events.add(EventName.buttonCancel)
|
||||
|
||||
# FrogPilot button presses
|
||||
if b.type == FrogPilotButtonType.lkas and b.pressed:
|
||||
self.always_on_lateral_disabled = not self.always_on_lateral_disabled
|
||||
|
||||
# Handle permanent and temporary steering faults
|
||||
self.steering_unpressed = 0 if cs_out.steeringPressed else self.steering_unpressed + 1
|
||||
if cs_out.steerFaultTemporary:
|
||||
@@ -340,6 +508,30 @@ class CarInterfaceBase(ABC):
|
||||
|
||||
return events
|
||||
|
||||
def frogpilot_distance_functions(self, frogpilot_toggles):
|
||||
distance_button = self.CS.distance_button or params_memory.get_bool("OnroadDistanceButtonPressed")
|
||||
|
||||
if distance_button:
|
||||
self.gap_counter += 1
|
||||
elif not self.prev_distance_button:
|
||||
self.gap_counter = 0
|
||||
|
||||
if self.gap_counter == CRUISE_LONG_PRESS * (1.5 if self.is_gm else 1) and frogpilot_toggles.experimental_mode_via_distance or self.traffic_mode_changed:
|
||||
if frogpilot_toggles.conditional_experimental_mode:
|
||||
conditional_status = params_memory.get_int("CEStatus")
|
||||
override_value = 0 if conditional_status in {1, 2, 3, 4, 5, 6} else 1 if conditional_status >= 7 else 2
|
||||
params_memory.put_int("CEStatus", override_value)
|
||||
else:
|
||||
experimental_mode = params.get_bool("ExperimentalMode")
|
||||
params.put_bool("ExperimentalMode", not experimental_mode)
|
||||
self.traffic_mode_changed = False
|
||||
|
||||
if self.gap_counter == CRUISE_LONG_PRESS * 5:
|
||||
self.traffic_mode_active = not self.traffic_mode_active
|
||||
self.traffic_mode_changed = frogpilot_toggles.experimental_mode_via_distance
|
||||
|
||||
self.prev_distance_button = distance_button
|
||||
return self.gap_counter >= CRUISE_LONG_PRESS
|
||||
|
||||
class RadarInterfaceBase(ABC):
|
||||
def __init__(self, CP):
|
||||
@@ -379,6 +571,12 @@ class CarStateBase(ABC):
|
||||
K = get_kalman_gain(DT_CTRL, np.array(A), np.array(C), np.array(Q), R)
|
||||
self.v_ego_kf = KF1D(x0=x0, A=A, C=C[0], K=K)
|
||||
|
||||
# FrogPilot variables
|
||||
self.cruise_decreased = False
|
||||
self.cruise_increased = False
|
||||
self.distance_button = False
|
||||
self.lkas_enabled = False
|
||||
|
||||
def update_speed_kf(self, v_ego_raw):
|
||||
if abs(v_ego_raw - self.v_ego_kf.x[0][0]) > 2.0: # Prevent large accelerations when car starts at non zero speed
|
||||
self.v_ego_kf.set_x([[v_ego_raw], [0.0]])
|
||||
|
||||
Reference in New Issue
Block a user