import re from collections import namedtuple from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Tuple, Union, no_type_check from cereal import car from common.conversions import Conversions as CV GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2 MODEL_YEARS_RE = r"(?<= )((\d{4}-\d{2})|(\d{4}))(,|$)" class Tier(Enum): GOLD = 0 SILVER = 1 BRONZE = 2 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" class Star(Enum): FULL = "full" HALF = "half" EMPTY = "empty" StarColumns = list(Column)[3:] TierColumns = (Column.FSR_LONGITUDINAL, Column.FSR_STEERING, Column.STEERING_TORQUE) CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]: # Returns applicable footnotes given current column return [fn for fn in footnotes if fn.value.column == column] # TODO: store years as a list def get_year_list(years): years_list = [] if len(years) == 0: return years_list for year in years.split(','): year = year.strip() if len(year) == 4: years_list.append(str(year)) elif "-" in year and len(year) == 7: start, end = year.split("-") years_list.extend(map(str, range(int(start), int(f"20{end}") + 1))) else: raise Exception(f"Malformed year string: {years}") return years_list def split_name(name: str) -> Tuple[str, str, str]: make, model = name.split(" ", 1) years = "" match = re.search(MODEL_YEARS_RE, model) if match is not None: years = model[match.start():] model = model[:match.start() - 1] return make, model, years @dataclass class CarInfo: name: str package: str video_link: Optional[str] = None footnotes: List[Enum] = field(default_factory=list) min_steer_speed: Optional[float] = None min_enable_speed: Optional[float] = None harness: Optional[Enum] = None def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]): # TODO: set all the min steer speeds in carParams and remove this if self.min_steer_speed is not None: assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarInfo and CarParams" else: self.min_steer_speed = CP.minSteerSpeed 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 if self.min_enable_speed is None: self.min_enable_speed = CP.minEnableSpeed self.car_name = CP.carName self.car_fingerprint = CP.carFingerprint self.make, self.model, self.years = split_name(self.name) 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 self.min_enable_speed <= 0. else Star.EMPTY, Column.FSR_STEERING: Star.FULL if self.min_steer_speed <= 0. else Star.EMPTY, Column.STEERING_TORQUE: Star.EMPTY, } # Set steering torque star from max lateral acceleration assert CP.maxLateralAccel > 0.1 if CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: self.row[Column.STEERING_TORQUE] = Star.FULL 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 for fn in get_footnotes(self.footnotes, column): if fn.value.star is not None: self.row[column] = fn.value.star # openpilot ACC star doesn't count for tiers full_stars = [s for col, s in self.row.items() if col in TierColumns].count(Star.FULL) if full_stars == len(TierColumns): self.tier = Tier.GOLD elif full_stars == len(TierColumns) - 1: self.tier = Tier.SILVER else: self.tier = Tier.BRONZE self.year_list = get_year_list(self.years) self.detail_sentence = self.get_detail_sentence(CP) return self def get_detail_sentence(self, CP): if not CP.notCar: sentence_builder = "openpilot upgrades your {car_model} with automated lane centering{alc} and adaptive cruise control{acc}." if self.min_steer_speed > self.min_enable_speed: alc = f" above {self.min_steer_speed * CV.MS_TO_MPH:.0f} mph," if self.min_steer_speed > 0 else " at all speeds," else: alc = "" # Exception for Nissan, Subaru, and stock long Toyota which do not auto-resume yet acc = "" if self.min_enable_speed > 0: acc = f" while driving above {self.min_enable_speed * CV.MS_TO_MPH:.0f} mph" elif CP.carName not in ("nissan", "subaru", "toyota") or (CP.carName == "toyota" and CP.openpilotLongitudinalControl): acc = " that automatically resumes from a stop" if self.row[Column.STEERING_TORQUE] != Star.FULL: sentence_builder += " This car may not be able to take tight turns on its own." return sentence_builder.format(car_model=f"{self.make} {self.model}", alc=alc, acc=acc) else: if CP.carFingerprint == "COMMA BODY": return "The body is a robotics dev kit that can run openpilot. Learn more." else: raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}") @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) elif column == Column.MODEL and len(self.years): item += f" {self.years}" footnotes = get_footnotes(self.footnotes, column) if len(footnotes): sups = sorted([self.all_footnotes[fn] for fn in footnotes]) item += footnote_tag.format(f'{",".join(map(str, sups))}') return item class Harness(Enum): nidec = "Honda Nidec" bosch_a = "Honda Bosch A" bosch_b = "Honda Bosch B" toyota = "Toyota" subaru = "Subaru" fca = "FCA" ram = "Ram" 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 Column.FSR_LONGITUDINAL.value: [ [Star.FULL.value, "openpilot operates down to 0 mph."], [Star.EMPTY.value, "openpilot operates only above a minimum speed. 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. See your car's manual for the minimum speed."], ], Column.STEERING_TORQUE.value: [ [Star.FULL.value, "Car has enough steering torque to comfortably take most highway turns."], [Star.EMPTY.value, "Limited ability to make tighter turns."], ], }, }