import math from cereal import car from collections import namedtuple from dataclasses import dataclass from enum import Enum from typing import Dict, List, Optional, Union, no_type_check TACO_TORQUE_THRESHOLD = 2.5 # m/s^2 GREAT_TORQUE_THRESHOLD = 1.4 # m/s^2 GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2 class Tier(Enum): GOLD = "The best openpilot experience. Great highway driving and beyond." SILVER = "A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future." BRONZE = "A good highway experience, but may have limited performance in traffic and on sharp turns." class Column(Enum): MAKE = "Make" MODEL = "Model" PACKAGE = "Supported Package" LONGITUDINAL = "openpilot ACC" FSR_LONGITUDINAL = "Stop and Go" FSR_STEERING = "Steer to 0" STEERING_TORQUE = "Steering Torque" MAINTAINED = "Actively Maintained" class Star(Enum): FULL = "full" HALF = "half" EMPTY = "empty" StarColumns = list(Column)[3:] CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[Enum]: # Returns applicable footnote given current column if footnotes is not None: for fn in footnotes: if fn.value.column == column: return fn return None @dataclass class CarInfo: name: str package: str video_link: Optional[str] = None footnotes: Optional[List[Enum]] = None min_steer_speed: Optional[float] = None min_enable_speed: Optional[float] = None good_torque: bool = False harness: Optional[Enum] = None def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dict[Enum, int]): # TODO: set all the min steer speeds in carParams and remove this min_steer_speed = CP.minSteerSpeed if self.min_steer_speed is not None: min_steer_speed = self.min_steer_speed assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarInfo and CarParams" assert self.harness is not None, f"{CP.carFingerprint}: Need to specify car harness" # TODO: set all the min enable speeds in carParams correctly and remove this min_enable_speed = CP.minEnableSpeed if self.min_enable_speed is not None: min_enable_speed = self.min_enable_speed self.car_name = CP.carName self.make, self.model = self.name.split(' ', 1) self.row = { Column.MAKE: self.make, Column.MODEL: self.model, Column.PACKAGE: self.package, # StarColumns Column.LONGITUDINAL: Star.FULL if CP.openpilotLongitudinalControl and not CP.radarOffCan else Star.EMPTY, Column.FSR_LONGITUDINAL: Star.FULL if min_enable_speed <= 0. else Star.EMPTY, Column.FSR_STEERING: Star.FULL if min_steer_speed <= 0. else Star.EMPTY, Column.STEERING_TORQUE: Star.FULL if self.good_torque else Star.EMPTY, # TODO: remove hardcoding and use maxLateralAccel Column.MAINTAINED: Star.FULL if CP.carFingerprint not in non_tested_cars and self.harness is not Harness.none else Star.EMPTY, } if not math.isnan(CP.maxLateralAccel): if CP.maxLateralAccel >= GREAT_TORQUE_THRESHOLD: self.row[Column.STEERING_TORQUE] = Star.FULL elif CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: self.row[Column.STEERING_TORQUE] = Star.HALF else: self.row[Column.STEERING_TORQUE] = Star.EMPTY if CP.notCar: for col in StarColumns: self.row[col] = Star.FULL self.all_footnotes = all_footnotes for column in StarColumns: # Demote if footnote specifies a star footnote = get_footnote(self.footnotes, column) if footnote is not None and footnote.value.star is not None: self.row[column] = footnote.value.star self.tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(list(self.row.values()).count(Star.FULL), Tier.BRONZE) return self @no_type_check def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: item: Union[str, Star] = self.row[column] if column in StarColumns: item = star_icon.format(item.value) footnote = get_footnote(self.footnotes, column) if footnote is not None: item += footnote_tag.format(self.all_footnotes[footnote]) return item class Harness(Enum): nidec = "Honda Nidec" bosch = "Honda Bosch A" toyota = "Toyota" subaru = "Subaru" fca = "FCA" vw = "VW" j533 = "J533" hyundai_a = "Hyundai A" hyundai_b = "Hyundai B" hyundai_c = "Hyundai C" hyundai_d = "Hyundai D" hyundai_e = "Hyundai E" hyundai_f = "Hyundai F" hyundai_g = "Hyundai G" hyundai_h = "Hyundai H" hyundai_i = "Hyundai I" hyundai_j = "Hyundai J" hyundai_k = "Hyundai K" hyundai_l = "Hyundai L" hyundai_m = "Hyundai M" hyundai_n = "Hyundai N" hyundai_o = "Hyundai O" hyundai_p = "Hyundai P" custom = "Developer" obd_ii = "OBD-II" nissan_a = "Nissan A" nissan_b = "Nissan B" mazda = "Mazda" none = "None" STAR_DESCRIPTIONS = { "Gas & Brakes": { # icon and row name "openpilot Adaptive Cruise Control (ACC)": [ # star column [Star.FULL.value, "openpilot is able to control the gas and brakes."], [Star.HALF.value, "openpilot is able to control the gas and brakes with some restrictions."], [Star.EMPTY.value, "The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system."], ], Column.FSR_LONGITUDINAL.value: [ [Star.FULL.value, "Adaptive Cruise Control (ACC) operates down to 0 mph."], [Star.EMPTY.value, "Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed."], ], }, "Steering": { Column.FSR_STEERING.value: [ [Star.FULL.value, "openpilot can control the steering wheel down to 0 mph."], [Star.EMPTY.value, "No steering control below certain speeds."], ], Column.STEERING_TORQUE.value: [ [Star.FULL.value, "Car has enough steering torque to take tighter turns."], [Star.HALF.value, "Car has enough steering torque for comfortable highway driving."], [Star.EMPTY.value, "Limited ability to make turns."], ], }, "Support": { Column.MAINTAINED.value: [ [Star.FULL.value, "Mainline software support, harness hardware sold by comma, lots of users, primary development target."], [Star.EMPTY.value, "Low user count, community maintained, harness hardware not sold by comma."], ], }, }