2024-02-21 23:02:43 +00:00
|
|
|
|
import json
|
2023-09-27 15:45:31 -07:00
|
|
|
|
import os
|
2023-11-17 23:53:40 +00:00
|
|
|
|
import numpy as np
|
2025-01-29 09:09:58 +00:00
|
|
|
|
import time
|
2024-02-21 23:02:43 +00:00
|
|
|
|
import tomllib
|
2023-09-27 15:45:31 -07:00
|
|
|
|
from abc import abstractmethod, ABC
|
2025-01-04 20:08:14 +09:00
|
|
|
|
from difflib import SequenceMatcher
|
2024-02-21 23:02:43 +00:00
|
|
|
|
from enum import StrEnum
|
2024-06-11 01:36:40 +00:00
|
|
|
|
from typing import Any, NamedTuple
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
|
from functools import cache
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
from opendbc.car import DT_CTRL, apply_hysteresis, gen_empty_fingerprint, scale_rot_inertia, scale_tire_stiffness, get_friction, STD_CARGO_KG
|
|
|
|
|
from opendbc.car import structs
|
|
|
|
|
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
|
|
|
|
from opendbc.car.common.basedir import BASEDIR
|
|
|
|
|
from opendbc.car.common.conversions import Conversions as CV
|
|
|
|
|
from opendbc.car.common.simple_kalman import KF1D, get_kalman_gain
|
|
|
|
|
from opendbc.car.values import PLATFORMS
|
|
|
|
|
from opendbc.can.parser import CANParser
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-09-03 16:09:00 +09:00
|
|
|
|
from openpilot.common.params import Params
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
GearShifter = structs.CarState.GearShifter
|
|
|
|
|
|
|
|
|
|
V_CRUISE_MAX = 145
|
2023-09-27 15:45:31 -07:00
|
|
|
|
MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS
|
2024-09-03 16:09:00 +09:00
|
|
|
|
ACCEL_MAX = 2.5
|
|
|
|
|
ACCEL_MIN = -4.0 #3.5
|
2023-09-27 15:45:31 -07:00
|
|
|
|
FRICTION_THRESHOLD = 0.3
|
|
|
|
|
|
2025-01-04 20:08:14 +09:00
|
|
|
|
NEURAL_PARAMS_PATH = os.path.join(BASEDIR, 'torque_data/neural_ff_weights.json')
|
|
|
|
|
TORQUE_NN_MODEL_PATH = os.path.join(BASEDIR, 'torque_data/lat_models')
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'torque_data/params.toml')
|
|
|
|
|
TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'torque_data/override.toml')
|
|
|
|
|
TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'torque_data/substitute.toml')
|
2024-02-21 23:02:43 +00:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
GEAR_SHIFTER_MAP: dict[str, structs.CarState.GearShifter] = {
|
2024-06-11 01:36:40 +00:00
|
|
|
|
'P': GearShifter.park, 'PARK': GearShifter.park,
|
|
|
|
|
'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse,
|
|
|
|
|
'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral,
|
|
|
|
|
'E': GearShifter.eco, 'ECO': GearShifter.eco,
|
|
|
|
|
'T': GearShifter.manumatic, 'MANUAL': GearShifter.manumatic,
|
|
|
|
|
'D': GearShifter.drive, 'DRIVE': GearShifter.drive,
|
|
|
|
|
'S': GearShifter.sport, 'SPORT': GearShifter.sport,
|
|
|
|
|
'L': GearShifter.low, 'LOW': GearShifter.low,
|
|
|
|
|
'B': GearShifter.brake, 'BRAKE': GearShifter.brake,
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 20:08:14 +09:00
|
|
|
|
def similarity(s1: str, s2: str) -> float:
|
|
|
|
|
return SequenceMatcher(None, s1, s2).ratio()
|
2024-02-21 23:02:43 +00:00
|
|
|
|
|
|
|
|
|
class LatControlInputs(NamedTuple):
|
|
|
|
|
lateral_acceleration: float
|
|
|
|
|
roll_compensation: float
|
|
|
|
|
vego: float
|
|
|
|
|
aego: float
|
|
|
|
|
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
TorqueFromLateralAccelCallbackType = Callable[[LatControlInputs, structs.CarParams.LateralTorqueTuning, float, float, bool, bool], float]
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
@cache
|
|
|
|
|
def get_torque_params():
|
2024-02-21 23:02:43 +00:00
|
|
|
|
with open(TORQUE_SUBSTITUTE_PATH, 'rb') as f:
|
|
|
|
|
sub = tomllib.load(f)
|
|
|
|
|
with open(TORQUE_PARAMS_PATH, 'rb') as f:
|
|
|
|
|
params = tomllib.load(f)
|
|
|
|
|
with open(TORQUE_OVERRIDE_PATH, 'rb') as f:
|
|
|
|
|
override = tomllib.load(f)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
torque_params = {}
|
|
|
|
|
for candidate in (sub.keys() | params.keys() | override.keys()) - {'legend'}:
|
|
|
|
|
if sum([candidate in x for x in [sub, params, override]]) > 1:
|
|
|
|
|
raise RuntimeError(f'{candidate} is defined twice in torque config')
|
|
|
|
|
|
|
|
|
|
sub_candidate = sub.get(candidate, candidate)
|
|
|
|
|
|
|
|
|
|
if sub_candidate in override:
|
|
|
|
|
out = override[sub_candidate]
|
|
|
|
|
elif sub_candidate in params:
|
|
|
|
|
out = params[sub_candidate]
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"Did not find torque params for {sub_candidate}")
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
torque_params[sub_candidate] = {key: out[i] for i, key in enumerate(params['legend'])}
|
|
|
|
|
if candidate in sub:
|
|
|
|
|
torque_params[candidate] = torque_params[sub_candidate]
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
return torque_params
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-04 20:08:14 +09:00
|
|
|
|
# Twilsonco's Lateral Neural Network Feedforward
|
|
|
|
|
class FluxModel:
|
|
|
|
|
# dict used to rename activation functions whose names aren't valid python identifiers
|
|
|
|
|
activation_function_names = {'σ': 'sigmoid'}
|
|
|
|
|
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 self.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
|
|
|
|
|
|
|
|
|
|
#car1 = car.replace('_', ' ')
|
|
|
|
|
#car1 = car1.replace(' HEV', ' HYBRID')
|
|
|
|
|
#car = car1.replace('EV ', 'ELECTRIC ')
|
|
|
|
|
print("########get_nn_model_path :", car, eps_firmware)
|
|
|
|
|
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
|
|
|
|
|
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# generic car and radar interfaces
|
|
|
|
|
|
|
|
|
|
class CarInterfaceBase(ABC):
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def __init__(self, CP: structs.CarParams, CarController, CarState):
|
2023-09-27 15:45:31 -07:00
|
|
|
|
self.CP = CP
|
|
|
|
|
|
|
|
|
|
self.frame = 0
|
|
|
|
|
self.v_ego_cluster_seen = False
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
self.CS: CarStateBase = CarState(CP)
|
|
|
|
|
self.can_parsers: dict[StrEnum, CANParser] = self.CS.get_can_parsers(CP)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
dbc_names = {bus: cp.dbc_name for bus, cp in self.can_parsers.items()}
|
|
|
|
|
self.CC: CarControllerBase = CarController(dbc_names, CP)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-03-01 22:22:11 +08:00
|
|
|
|
Params().put('LongitudinalPersonalityMax', "4") # 强制调整4
|
2025-01-04 20:08:14 +09:00
|
|
|
|
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 Params().get_bool("NNFF")
|
|
|
|
|
self.use_nnff_lite = not self.use_nnff and Params().get_bool("NNFFLite")
|
2025-03-01 22:22:11 +08:00
|
|
|
|
|
2025-01-04 20:08:14 +09:00
|
|
|
|
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
|
2025-03-01 22:22:11 +08:00
|
|
|
|
|
2024-09-03 16:09:00 +09:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def apply(self, c: structs.CarControl, now_nanos: int | None = None) -> tuple[structs.CarControl.Actuators, list[CanData]]:
|
|
|
|
|
if now_nanos is None:
|
|
|
|
|
now_nanos = int(time.monotonic() * 1e9)
|
2024-06-11 01:36:40 +00:00
|
|
|
|
return self.CC.update(c, self.CS, now_nanos)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_pid_accel_limits(CP, current_speed, cruise_speed):
|
|
|
|
|
return ACCEL_MIN, ACCEL_MAX
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def get_non_essential_params(cls, candidate: str) -> structs.CarParams:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
"""
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def get_params(cls, candidate: str, fingerprint: dict[int, dict[int, int]], car_fw: list[structs.CarParams.CarFw],
|
|
|
|
|
experimental_long: bool, docs: bool) -> structs.CarParams:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret = CarInterfaceBase.get_std_params(candidate)
|
2024-06-11 01:36:40 +00:00
|
|
|
|
|
|
|
|
|
platform = PLATFORMS[candidate]
|
|
|
|
|
ret.mass = platform.config.specs.mass
|
|
|
|
|
ret.wheelbase = platform.config.specs.wheelbase
|
|
|
|
|
ret.steerRatio = platform.config.specs.steerRatio
|
|
|
|
|
ret.centerToFront = ret.wheelbase * platform.config.specs.centerToFrontRatio
|
|
|
|
|
ret.minEnableSpeed = platform.config.specs.minEnableSpeed
|
|
|
|
|
ret.minSteerSpeed = platform.config.specs.minSteerSpeed
|
|
|
|
|
ret.tireStiffnessFactor = platform.config.specs.tireStiffnessFactor
|
|
|
|
|
ret.flags |= int(platform.config.flags)
|
|
|
|
|
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret = cls._get_params(ret, candidate, fingerprint, car_fw, experimental_long, docs)
|
2025-03-01 22:22:11 +08:00
|
|
|
|
|
2025-01-04 20:08:14 +09:00
|
|
|
|
# Enable torque controller for all cars that do not use angle based steering
|
|
|
|
|
if ret.steerControlType != structs.CarParams.SteerControlType.angle 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_nonblocking("NNFFModelName", candidate.replace("_", " "))
|
|
|
|
|
print(f"NNFF loaded... {model}")
|
2025-03-01 22:22:11 +08:00
|
|
|
|
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-09-03 16:09:00 +09:00
|
|
|
|
if Params().get_bool("DisableMinSteerSpeed"):
|
|
|
|
|
ret.minSteerSpeed = 0.
|
|
|
|
|
|
2023-11-17 23:53:40 +00:00
|
|
|
|
# Vehicle mass is published curb weight plus assumed payload such as a human driver; notCars have no assumed payload
|
|
|
|
|
if not ret.notCar:
|
|
|
|
|
ret.mass = ret.mass + STD_CARGO_KG
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2023-11-17 23:53:40 +00:00
|
|
|
|
# Set params dependent on values set by the car interface
|
|
|
|
|
ret.rotationalInertia = scale_rot_inertia(ret.mass, ret.wheelbase)
|
|
|
|
|
ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, ret.tireStiffnessFactor)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
@abstractmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def _get_params(ret: structs.CarParams, candidate, fingerprint: dict[int, dict[int, int]],
|
|
|
|
|
car_fw: list[structs.CarParams.CarFw], experimental_long: bool, docs: bool) -> structs.CarParams:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def init(CP: structs.CarParams, can_recv: CanRecvCallable, can_send: CanSendCallable):
|
2023-09-27 15:45:31 -07:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_steer_feedforward_default(desired_angle, v_ego):
|
|
|
|
|
# Proportional to realigning tire momentum: lateral acceleration.
|
|
|
|
|
return desired_angle * (v_ego**2)
|
|
|
|
|
|
|
|
|
|
def get_steer_feedforward_function(self):
|
|
|
|
|
return self.get_steer_feedforward_default
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def torque_from_lateral_accel_linear(self, latcontrol_inputs: LatControlInputs, torque_params: structs.CarParams.LateralTorqueTuning,
|
2024-02-21 23:02:43 +00:00
|
|
|
|
lateral_accel_error: float, lateral_accel_deadzone: float, friction_compensation: bool, gravity_adjusted: bool) -> float:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# The default is a linear relationship between torque and lateral acceleration (accounting for road roll and steering friction)
|
|
|
|
|
friction = get_friction(lateral_accel_error, lateral_accel_deadzone, FRICTION_THRESHOLD, torque_params, friction_compensation)
|
2024-02-21 23:02:43 +00:00
|
|
|
|
return (latcontrol_inputs.lateral_acceleration / float(torque_params.latAccelFactor)) + friction
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
def torque_from_lateral_accel(self) -> TorqueFromLateralAccelCallbackType:
|
|
|
|
|
return self.torque_from_lateral_accel_linear
|
|
|
|
|
|
|
|
|
|
# returns a set of default params to avoid repetition in car specific params
|
|
|
|
|
@staticmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def get_std_params(candidate: str) -> structs.CarParams:
|
|
|
|
|
ret = structs.CarParams()
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret.carFingerprint = candidate
|
|
|
|
|
|
|
|
|
|
# Car docs fields
|
2024-06-11 01:36:40 +00:00
|
|
|
|
ret.maxLateralAccel = get_torque_params()[candidate]['MAX_LAT_ACCEL_MEASURED']
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret.autoResumeSng = True # describes whether car can resume from a stop automatically
|
|
|
|
|
|
|
|
|
|
# standard ALC params
|
2023-11-17 23:53:40 +00:00
|
|
|
|
ret.tireStiffnessFactor = 1.0
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.steerControlType = structs.CarParams.SteerControlType.torque
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret.minSteerSpeed = 0.
|
|
|
|
|
ret.wheelSpeedFactor = 1.0
|
|
|
|
|
|
|
|
|
|
ret.pcmCruise = True # openpilot's state is tied to the PCM's cruise state on most cars
|
|
|
|
|
ret.minEnableSpeed = -1. # enable is done by stock ACC, so ignore this
|
|
|
|
|
ret.steerRatioRear = 0. # no rear steering, at least on the listed cars aboveA
|
|
|
|
|
ret.openpilotLongitudinalControl = False
|
|
|
|
|
ret.stopAccel = -2.0
|
|
|
|
|
ret.stoppingDecelRate = 0.8 # brake_travel/s while trying to stop
|
|
|
|
|
ret.vEgoStopping = 0.5
|
|
|
|
|
ret.vEgoStarting = 0.5
|
|
|
|
|
ret.longitudinalTuning.kf = 1.
|
|
|
|
|
ret.longitudinalTuning.kpBP = [0.]
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.longitudinalTuning.kpV = [0.]
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret.longitudinalTuning.kiBP = [0.]
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.longitudinalTuning.kiV = [0.]
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# TODO estimate car specific lag, use .15s for now
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.longitudinalActuatorDelay = 0.15
|
2023-09-27 15:45:31 -07:00
|
|
|
|
ret.steerLimitTimer = 1.0
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def configure_torque_tune(candidate: str, tune: structs.CarParams.LateralTuning, steering_angle_deadzone_deg: float = 0.0, use_steering_angle: bool = True):
|
2024-06-11 01:36:40 +00:00
|
|
|
|
params = get_torque_params()[candidate]
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
tune.init('torque')
|
|
|
|
|
tune.torque.useSteeringAngle = use_steering_angle
|
|
|
|
|
tune.torque.kp = 1.0
|
|
|
|
|
tune.torque.kf = 1.0
|
|
|
|
|
tune.torque.ki = 0.1
|
|
|
|
|
tune.torque.friction = params['FRICTION']
|
|
|
|
|
tune.torque.latAccelFactor = params['LAT_ACCEL_FACTOR']
|
|
|
|
|
tune.torque.latAccelOffset = 0.0
|
|
|
|
|
tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def _update(self) -> structs.CarState:
|
|
|
|
|
return self.CS.update(self.can_parsers)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def update(self, can_packets: list[tuple[int, list[CanData]]]) -> structs.CarState:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# parse can
|
2025-01-29 09:09:58 +00:00
|
|
|
|
for cp in self.can_parsers.values():
|
2023-09-27 15:45:31 -07:00
|
|
|
|
if cp is not None:
|
2025-01-29 09:09:58 +00:00
|
|
|
|
cp.update_strings(can_packets)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
# get CarState
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret = self._update()
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.canValid = all(cp.can_valid for cp in self.can_parsers.values())
|
|
|
|
|
ret.canTimeout = any(cp.bus_timeout for cp in self.can_parsers.values())
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
if ret.vEgoCluster == 0.0 and not self.v_ego_cluster_seen:
|
|
|
|
|
ret.vEgoCluster = ret.vEgo
|
|
|
|
|
else:
|
|
|
|
|
self.v_ego_cluster_seen = True
|
|
|
|
|
|
|
|
|
|
# Many cars apply hysteresis to the ego dash speed
|
2025-01-29 09:09:58 +00:00
|
|
|
|
ret.vEgoCluster = apply_hysteresis(ret.vEgoCluster, self.CS.out.vEgoCluster, self.CS.cluster_speed_hyst_gap)
|
|
|
|
|
if abs(ret.vEgo) < self.CS.cluster_min_speed:
|
|
|
|
|
ret.vEgoCluster = 0.0
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
if ret.cruiseState.speedCluster == 0:
|
|
|
|
|
ret.cruiseState.speedCluster = ret.cruiseState.speed
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
# save for next iteration
|
|
|
|
|
self.CS.out = ret
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
return ret
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RadarInterfaceBase(ABC):
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def __init__(self, CP: structs.CarParams):
|
|
|
|
|
self.CP = CP
|
2023-09-27 15:45:31 -07:00
|
|
|
|
self.rcp = None
|
2025-01-29 09:09:58 +00:00
|
|
|
|
self.pts: dict[int, structs.RadarData.RadarPoint] = {}
|
2024-02-21 23:02:43 +00:00
|
|
|
|
self.frame = 0
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def update(self, can_packets: list[tuple[int, list[CanData]]]) -> structs.RadarDataT | None:
|
2024-02-21 23:02:43 +00:00
|
|
|
|
self.frame += 1
|
2025-01-29 09:09:58 +00:00
|
|
|
|
if (self.frame % 5) == 0: # 20 Hz is very standard
|
|
|
|
|
return structs.RadarData()
|
2024-02-21 23:02:43 +00:00
|
|
|
|
return None
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CarStateBase(ABC):
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def __init__(self, CP: structs.CarParams):
|
2023-09-27 15:45:31 -07:00
|
|
|
|
self.CP = CP
|
|
|
|
|
self.car_fingerprint = CP.carFingerprint
|
2025-01-29 09:09:58 +00:00
|
|
|
|
self.out = structs.CarState()
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
self.cruise_buttons = 0
|
|
|
|
|
self.left_blinker_cnt = 0
|
|
|
|
|
self.right_blinker_cnt = 0
|
|
|
|
|
self.steering_pressed_cnt = 0
|
|
|
|
|
self.left_blinker_prev = False
|
|
|
|
|
self.right_blinker_prev = False
|
|
|
|
|
self.cluster_speed_hyst_gap = 0.0
|
|
|
|
|
self.cluster_min_speed = 0.0 # min speed before dropping to 0
|
2025-01-29 09:09:58 +00:00
|
|
|
|
self.secoc_key: bytes = b"00" * 16
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2023-11-17 23:53:40 +00:00
|
|
|
|
Q = [[0.0, 0.0], [0.0, 100.0]]
|
|
|
|
|
R = 0.3
|
|
|
|
|
A = [[1.0, DT_CTRL], [0.0, 1.0]]
|
|
|
|
|
C = [[1.0, 0.0]]
|
|
|
|
|
x0=[[0.0], [0.0]]
|
|
|
|
|
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)
|
2024-09-03 16:09:00 +09:00
|
|
|
|
self.v_ego_clu_kf = KF1D(x0=x0, A=A, C=C[0], K=K)
|
|
|
|
|
|
|
|
|
|
self.softHoldActive = 0
|
2025-02-13 07:44:14 +09:00
|
|
|
|
self.is_metric = True
|
|
|
|
|
self.lkas_enabled = False
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
@abstractmethod
|
|
|
|
|
def update(self, can_parsers) -> structs.CarState:
|
|
|
|
|
pass
|
|
|
|
|
|
2023-09-27 15:45:31 -07:00
|
|
|
|
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
|
2024-02-21 23:02:43 +00:00
|
|
|
|
self.v_ego_kf.set_x([[v_ego_raw], [0.0]])
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
v_ego_x = self.v_ego_kf.update(v_ego_raw)
|
|
|
|
|
return float(v_ego_x[0]), float(v_ego_x[1])
|
2025-03-01 22:22:11 +08:00
|
|
|
|
|
2024-09-03 16:09:00 +09:00
|
|
|
|
def update_clu_speed_kf(self, v_ego_raw):
|
|
|
|
|
if abs(v_ego_raw - self.v_ego_clu_kf.x[0][0]) > 2.0: # Prevent large accelerations when car starts at non zero speed
|
|
|
|
|
self.v_ego_clu_kf.set_x([[v_ego_raw], [0.0]])
|
|
|
|
|
|
|
|
|
|
v_ego_x = self.v_ego_clu_kf.update(v_ego_raw)
|
|
|
|
|
return float(v_ego_x[0]), float(v_ego_x[1])
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
|
|
|
|
def get_wheel_speeds(self, fl, fr, rl, rr, unit=CV.KPH_TO_MS):
|
|
|
|
|
factor = unit * self.CP.wheelSpeedFactor
|
|
|
|
|
|
2025-01-29 09:09:58 +00:00
|
|
|
|
wheelSpeeds = structs.CarState.WheelSpeeds()
|
2023-09-27 15:45:31 -07:00
|
|
|
|
wheelSpeeds.fl = fl * factor
|
|
|
|
|
wheelSpeeds.fr = fr * factor
|
|
|
|
|
wheelSpeeds.rl = rl * factor
|
|
|
|
|
wheelSpeeds.rr = rr * factor
|
|
|
|
|
return wheelSpeeds
|
|
|
|
|
|
|
|
|
|
def update_blinker_from_lamp(self, blinker_time: int, left_blinker_lamp: bool, right_blinker_lamp: bool):
|
|
|
|
|
"""Update blinkers from lights. Enable output when light was seen within the last `blinker_time`
|
|
|
|
|
iterations"""
|
|
|
|
|
# TODO: Handle case when switching direction. Now both blinkers can be on at the same time
|
|
|
|
|
self.left_blinker_cnt = blinker_time if left_blinker_lamp else max(self.left_blinker_cnt - 1, 0)
|
|
|
|
|
self.right_blinker_cnt = blinker_time if right_blinker_lamp else max(self.right_blinker_cnt - 1, 0)
|
|
|
|
|
return self.left_blinker_cnt > 0, self.right_blinker_cnt > 0
|
|
|
|
|
|
|
|
|
|
def update_steering_pressed(self, steering_pressed, steering_pressed_min_count):
|
|
|
|
|
"""Applies filtering on steering pressed for noisy driver torque signals."""
|
|
|
|
|
self.steering_pressed_cnt += 1 if steering_pressed else -1
|
2025-01-29 09:09:58 +00:00
|
|
|
|
self.steering_pressed_cnt = float(np.clip(self.steering_pressed_cnt, 0, steering_pressed_min_count * 2))
|
2023-09-27 15:45:31 -07:00
|
|
|
|
return self.steering_pressed_cnt > steering_pressed_min_count
|
|
|
|
|
|
|
|
|
|
def update_blinker_from_stalk(self, blinker_time: int, left_blinker_stalk: bool, right_blinker_stalk: bool):
|
|
|
|
|
"""Update blinkers from stalk position. When stalk is seen the blinker will be on for at least blinker_time,
|
|
|
|
|
or until the stalk is turned off, whichever is longer. If the opposite stalk direction is seen the blinker
|
|
|
|
|
is forced to the other side. On a rising edge of the stalk the timeout is reset."""
|
|
|
|
|
|
|
|
|
|
if left_blinker_stalk:
|
|
|
|
|
self.right_blinker_cnt = 0
|
|
|
|
|
if not self.left_blinker_prev:
|
|
|
|
|
self.left_blinker_cnt = blinker_time
|
|
|
|
|
|
|
|
|
|
if right_blinker_stalk:
|
|
|
|
|
self.left_blinker_cnt = 0
|
|
|
|
|
if not self.right_blinker_prev:
|
|
|
|
|
self.right_blinker_cnt = blinker_time
|
|
|
|
|
|
|
|
|
|
self.left_blinker_cnt = max(self.left_blinker_cnt - 1, 0)
|
|
|
|
|
self.right_blinker_cnt = max(self.right_blinker_cnt - 1, 0)
|
|
|
|
|
|
|
|
|
|
self.left_blinker_prev = left_blinker_stalk
|
|
|
|
|
self.right_blinker_prev = right_blinker_stalk
|
|
|
|
|
|
|
|
|
|
return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def parse_gear_shifter(gear: str | None) -> structs.CarState.GearShifter:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
if gear is None:
|
|
|
|
|
return GearShifter.unknown
|
2024-06-11 01:36:40 +00:00
|
|
|
|
return GEAR_SHIFTER_MAP.get(gear.upper(), GearShifter.unknown)
|
2023-09-27 15:45:31 -07:00
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
@staticmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def get_can_parsers(CP) -> dict[StrEnum, CANParser]:
|
|
|
|
|
return {}
|
2024-06-11 01:36:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CarControllerBase(ABC):
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def __init__(self, dbc_names: dict[StrEnum, str], CP: structs.CarParams):
|
|
|
|
|
self.CP = CP
|
|
|
|
|
self.frame = 0
|
|
|
|
|
self.secoc_key: bytes = b"00" * 16
|
2024-06-11 01:36:40 +00:00
|
|
|
|
|
|
|
|
|
@abstractmethod
|
2025-01-29 09:09:58 +00:00
|
|
|
|
def update(self, CC: structs.CarControl, CS: CarStateBase, now_nanos: int) -> tuple[structs.CarControl.Actuators, list[CanData]]:
|
2024-06-11 01:36:40 +00:00
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2024-02-21 23:02:43 +00:00
|
|
|
|
INTERFACE_ATTR_FILE = {
|
|
|
|
|
"FINGERPRINTS": "fingerprints",
|
|
|
|
|
"FW_VERSIONS": "fingerprints",
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# interface-specific helpers
|
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
def get_interface_attr(attr: str, combine_brands: bool = False, ignore_none: bool = False) -> dict[str | StrEnum, Any]:
|
2025-01-29 09:09:58 +00:00
|
|
|
|
# read all the folders in opendbc/car and return a dict where:
|
2023-09-27 15:45:31 -07:00
|
|
|
|
# - keys are all the car models or brand names
|
|
|
|
|
# - values are attr values from all car folders
|
|
|
|
|
result = {}
|
2025-01-29 09:09:58 +00:00
|
|
|
|
for car_folder in sorted([x[0] for x in os.walk(BASEDIR)]):
|
2023-09-27 15:45:31 -07:00
|
|
|
|
try:
|
|
|
|
|
brand_name = car_folder.split('/')[-1]
|
2025-01-29 09:09:58 +00:00
|
|
|
|
brand_values = __import__(f'opendbc.car.{brand_name}.{INTERFACE_ATTR_FILE.get(attr, "values")}', fromlist=[attr])
|
2023-09-27 15:45:31 -07:00
|
|
|
|
if hasattr(brand_values, attr) or not ignore_none:
|
|
|
|
|
attr_data = getattr(brand_values, attr, None)
|
|
|
|
|
else:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if combine_brands:
|
|
|
|
|
if isinstance(attr_data, dict):
|
|
|
|
|
for f, v in attr_data.items():
|
|
|
|
|
result[f] = v
|
|
|
|
|
else:
|
|
|
|
|
result[brand_name] = attr_data
|
|
|
|
|
except (ImportError, OSError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return result
|
2024-02-21 23:02:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NanoFFModel:
|
|
|
|
|
def __init__(self, weights_loc: str, platform: str):
|
|
|
|
|
self.weights_loc = weights_loc
|
|
|
|
|
self.platform = platform
|
|
|
|
|
self.load_weights(platform)
|
|
|
|
|
|
|
|
|
|
def load_weights(self, platform: str):
|
2024-06-11 01:36:40 +00:00
|
|
|
|
with open(self.weights_loc) as fob:
|
2024-02-21 23:02:43 +00:00
|
|
|
|
self.weights = {k: np.array(v) for k, v in json.load(fob)[platform].items()}
|
|
|
|
|
|
|
|
|
|
def relu(self, x: np.ndarray):
|
|
|
|
|
return np.maximum(0.0, x)
|
|
|
|
|
|
|
|
|
|
def forward(self, x: np.ndarray):
|
|
|
|
|
assert x.ndim == 1
|
|
|
|
|
x = (x - self.weights['input_norm_mat'][:, 0]) / (self.weights['input_norm_mat'][:, 1] - self.weights['input_norm_mat'][:, 0])
|
|
|
|
|
x = self.relu(np.dot(x, self.weights['w_1']) + self.weights['b_1'])
|
|
|
|
|
x = self.relu(np.dot(x, self.weights['w_2']) + self.weights['b_2'])
|
|
|
|
|
x = self.relu(np.dot(x, self.weights['w_3']) + self.weights['b_3'])
|
|
|
|
|
x = np.dot(x, self.weights['w_4']) + self.weights['b_4']
|
|
|
|
|
return x
|
|
|
|
|
|
2024-06-11 01:36:40 +00:00
|
|
|
|
def predict(self, x: list[float], do_sample: bool = False):
|
2024-02-21 23:02:43 +00:00
|
|
|
|
x = self.forward(np.array(x))
|
|
|
|
|
if do_sample:
|
|
|
|
|
pred = np.random.laplace(x[0], np.exp(x[1]) / self.weights['temperature'])
|
|
|
|
|
else:
|
|
|
|
|
pred = x[0]
|
|
|
|
|
pred = pred * (self.weights['output_norm_mat'][1] - self.weights['output_norm_mat'][0]) + self.weights['output_norm_mat'][0]
|
|
|
|
|
return pred
|