|  |  |  | import re
 | 
					
						
							|  |  |  | from collections import namedtuple
 | 
					
						
							|  |  |  | import copy
 | 
					
						
							|  |  |  | from dataclasses import dataclass, field
 | 
					
						
							|  |  |  | from enum import Enum
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from cereal import car
 | 
					
						
							|  |  |  | from openpilot.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 Column(Enum):
 | 
					
						
							|  |  |  |   MAKE = "Make"
 | 
					
						
							|  |  |  |   MODEL = "Model"
 | 
					
						
							|  |  |  |   PACKAGE = "Supported Package"
 | 
					
						
							|  |  |  |   LONGITUDINAL = "ACC"
 | 
					
						
							|  |  |  |   FSR_LONGITUDINAL = "No ACC accel below"
 | 
					
						
							|  |  |  |   FSR_STEERING = "No ALC below"
 | 
					
						
							|  |  |  |   STEERING_TORQUE = "Steering Torque"
 | 
					
						
							|  |  |  |   AUTO_RESUME = "Resume from stop"
 | 
					
						
							|  |  |  |   HARDWARE = "Hardware Needed"
 | 
					
						
							|  |  |  |   VIDEO = "Video"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Star(Enum):
 | 
					
						
							|  |  |  |   FULL = "full"
 | 
					
						
							|  |  |  |   HALF = "half"
 | 
					
						
							|  |  |  |   EMPTY = "empty"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # A part + its comprised parts
 | 
					
						
							|  |  |  | @dataclass
 | 
					
						
							|  |  |  | class BasePart:
 | 
					
						
							|  |  |  |   name: str
 | 
					
						
							|  |  |  |   parts: list[Enum] = field(default_factory=list)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def all_parts(self):
 | 
					
						
							|  |  |  |     # Recursively get all parts
 | 
					
						
							|  |  |  |     _parts = 'parts'
 | 
					
						
							|  |  |  |     parts = []
 | 
					
						
							|  |  |  |     parts.extend(getattr(self, _parts))
 | 
					
						
							|  |  |  |     for part in getattr(self, _parts):
 | 
					
						
							|  |  |  |       parts.extend(part.value.all_parts())
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return parts
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class EnumBase(Enum):
 | 
					
						
							|  |  |  |   @property
 | 
					
						
							|  |  |  |   def part_type(self):
 | 
					
						
							|  |  |  |     return PartType(self.__class__)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Mount(EnumBase):
 | 
					
						
							|  |  |  |   mount = BasePart("mount")
 | 
					
						
							|  |  |  |   angled_mount_8_degrees = BasePart("angled mount (8 degrees)")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Cable(EnumBase):
 | 
					
						
							|  |  |  |   rj45_cable_7ft = BasePart("RJ45 cable (7 ft)")
 | 
					
						
							|  |  |  |   long_obdc_cable = BasePart("long OBD-C cable")
 | 
					
						
							|  |  |  |   usb_a_2_a_cable = BasePart("USB A-A cable")
 | 
					
						
							|  |  |  |   usbc_otg_cable = BasePart("USB C OTG cable")
 | 
					
						
							|  |  |  |   usbc_coupler = BasePart("USB-C coupler")
 | 
					
						
							|  |  |  |   obd_c_cable_1_5ft = BasePart("OBD-C cable (1.5 ft)")
 | 
					
						
							|  |  |  |   right_angle_obd_c_cable_1_5ft = BasePart("right angle OBD-C cable (1.5 ft)")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Accessory(EnumBase):
 | 
					
						
							|  |  |  |   harness_box = BasePart("harness box")
 | 
					
						
							|  |  |  |   comma_power_v2 = BasePart("comma power v2")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass
 | 
					
						
							|  |  |  | class BaseCarHarness(BasePart):
 | 
					
						
							|  |  |  |   parts: list[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft])
 | 
					
						
							|  |  |  |   has_connector: bool = True  # without are hidden on the harness connector page
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class CarHarness(EnumBase):
 | 
					
						
							|  |  |  |   nidec = BaseCarHarness("Honda Nidec connector")
 | 
					
						
							|  |  |  |   bosch_a = BaseCarHarness("Honda Bosch A connector")
 | 
					
						
							|  |  |  |   bosch_b = BaseCarHarness("Honda Bosch B connector")
 | 
					
						
							|  |  |  |   toyota_a = BaseCarHarness("Toyota A connector")
 | 
					
						
							|  |  |  |   toyota_b = BaseCarHarness("Toyota B connector")
 | 
					
						
							|  |  |  |   subaru_a = BaseCarHarness("Subaru A connector")
 | 
					
						
							|  |  |  |   subaru_b = BaseCarHarness("Subaru B connector")
 | 
					
						
							|  |  |  |   subaru_c = BaseCarHarness("Subaru C connector")
 | 
					
						
							|  |  |  |   subaru_d = BaseCarHarness("Subaru D connector")
 | 
					
						
							|  |  |  |   fca = BaseCarHarness("FCA connector")
 | 
					
						
							|  |  |  |   ram = BaseCarHarness("Ram connector")
 | 
					
						
							|  |  |  |   vw = BaseCarHarness("VW connector")
 | 
					
						
							|  |  |  |   j533 = BaseCarHarness("J533 connector", parts=[Accessory.harness_box, Cable.long_obdc_cable, Cable.usbc_coupler])
 | 
					
						
							|  |  |  |   hyundai_a = BaseCarHarness("Hyundai A connector")
 | 
					
						
							|  |  |  |   hyundai_b = BaseCarHarness("Hyundai B connector")
 | 
					
						
							|  |  |  |   hyundai_c = BaseCarHarness("Hyundai C connector")
 | 
					
						
							|  |  |  |   hyundai_d = BaseCarHarness("Hyundai D connector")
 | 
					
						
							|  |  |  |   hyundai_e = BaseCarHarness("Hyundai E connector")
 | 
					
						
							|  |  |  |   hyundai_f = BaseCarHarness("Hyundai F connector")
 | 
					
						
							|  |  |  |   hyundai_g = BaseCarHarness("Hyundai G connector")
 | 
					
						
							|  |  |  |   hyundai_h = BaseCarHarness("Hyundai H connector")
 | 
					
						
							|  |  |  |   hyundai_i = BaseCarHarness("Hyundai I connector")
 | 
					
						
							|  |  |  |   hyundai_j = BaseCarHarness("Hyundai J connector")
 | 
					
						
							|  |  |  |   hyundai_k = BaseCarHarness("Hyundai K connector")
 | 
					
						
							|  |  |  |   hyundai_l = BaseCarHarness("Hyundai L connector")
 | 
					
						
							|  |  |  |   hyundai_m = BaseCarHarness("Hyundai M connector")
 | 
					
						
							|  |  |  |   hyundai_n = BaseCarHarness("Hyundai N connector")
 | 
					
						
							|  |  |  |   hyundai_o = BaseCarHarness("Hyundai O connector")
 | 
					
						
							|  |  |  |   hyundai_p = BaseCarHarness("Hyundai P connector")
 | 
					
						
							|  |  |  |   hyundai_q = BaseCarHarness("Hyundai Q connector")
 | 
					
						
							|  |  |  |   hyundai_r = BaseCarHarness("Hyundai R connector")
 | 
					
						
							|  |  |  |   custom = BaseCarHarness("Developer connector")
 | 
					
						
							|  |  |  |   obd_ii = BaseCarHarness("OBD-II connector", parts=[Cable.long_obdc_cable, Cable.long_obdc_cable], has_connector=False)
 | 
					
						
							|  |  |  |   gm = BaseCarHarness("GM connector", parts=[Accessory.harness_box])
 | 
					
						
							|  |  |  |   nissan_a = BaseCarHarness("Nissan A connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
 | 
					
						
							|  |  |  |   nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
 | 
					
						
							|  |  |  |   mazda = BaseCarHarness("Mazda connector")
 | 
					
						
							|  |  |  |   ford_q3 = BaseCarHarness("Ford Q3 connector")
 | 
					
						
							|  |  |  |   ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft, Cable.long_obdc_cable,
 | 
					
						
							|  |  |  |                                                        Cable.usbc_coupler])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Device(EnumBase):
 | 
					
						
							|  |  |  |   threex = BasePart("comma 3X", parts=[Mount.mount, Cable.right_angle_obd_c_cable_1_5ft])
 | 
					
						
							|  |  |  |   # variant of comma 3X with angled mounts
 | 
					
						
							|  |  |  |   threex_angled_mount = BasePart("comma 3X", parts=[Mount.angled_mount_8_degrees, Cable.right_angle_obd_c_cable_1_5ft])
 | 
					
						
							|  |  |  |   red_panda = BasePart("red panda")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Kit(EnumBase):
 | 
					
						
							|  |  |  |   red_panda_kit = BasePart("CAN FD panda kit", parts=[Device.red_panda, Accessory.harness_box,
 | 
					
						
							|  |  |  |                                                       Cable.usb_a_2_a_cable, Cable.usbc_otg_cable, Cable.obd_c_cable_1_5ft])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Tool(EnumBase):
 | 
					
						
							|  |  |  |   socket_8mm_deep = BasePart("Socket Wrench 8mm or 5/16\" (deep)")
 | 
					
						
							|  |  |  |   pry_tool = BasePart("Pry Tool")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class PartType(Enum):
 | 
					
						
							|  |  |  |   accessory = Accessory
 | 
					
						
							|  |  |  |   cable = Cable
 | 
					
						
							|  |  |  |   connector = CarHarness
 | 
					
						
							|  |  |  |   device = Device
 | 
					
						
							|  |  |  |   kit = Kit
 | 
					
						
							|  |  |  |   mount = Mount
 | 
					
						
							|  |  |  |   tool = Tool
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | DEFAULT_CAR_PARTS: list[EnumBase] = [Device.threex]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclass
 | 
					
						
							|  |  |  | class CarParts:
 | 
					
						
							|  |  |  |   parts: list[EnumBase] = field(default_factory=list)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def __call__(self):
 | 
					
						
							|  |  |  |     return copy.deepcopy(self)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @classmethod
 | 
					
						
							|  |  |  |   def common(cls, add: list[EnumBase] = None, remove: list[EnumBase] = None):
 | 
					
						
							|  |  |  |     p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])]
 | 
					
						
							|  |  |  |     return cls(p)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def all_parts(self):
 | 
					
						
							|  |  |  |     parts = []
 | 
					
						
							|  |  |  |     for part in self.parts:
 | 
					
						
							|  |  |  |       parts.extend(part.value.all_parts())
 | 
					
						
							|  |  |  |     return self.parts + parts
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | CarFootnote = namedtuple("CarFootnote", ["text", "column", "docs_only", "shop_footnote"], defaults=(False, False))
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class CommonFootnote(Enum):
 | 
					
						
							|  |  |  |   EXP_LONG_AVAIL = CarFootnote(
 | 
					
						
							|  |  |  |     "openpilot Longitudinal Control (Alpha) is available behind a toggle; " +
 | 
					
						
							|  |  |  |     "the toggle is only available in non-release branches such as `devel` or `master-ci`.",
 | 
					
						
							|  |  |  |     Column.LONGITUDINAL, docs_only=True)
 | 
					
						
							|  |  |  |   EXP_LONG_DSU = CarFootnote(
 | 
					
						
							|  |  |  |     "By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. " +
 | 
					
						
							|  |  |  |     "If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace " +
 | 
					
						
							|  |  |  |     "stock ACC. <b><i>NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b>",
 | 
					
						
							|  |  |  |     Column.LONGITUDINAL)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 CarDocs:
 | 
					
						
							|  |  |  |   # make + model + model years
 | 
					
						
							|  |  |  |   name: str
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # Example for Toyota Corolla MY20
 | 
					
						
							|  |  |  |   # requirements: Lane Tracing Assist (LTA) and Dynamic Radar Cruise Control (DRCC)
 | 
					
						
							|  |  |  |   # US Market reference: "All", since all Corolla in the US come standard with LTA and DRCC
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # the simplest description of the requirements for the US market
 | 
					
						
							|  |  |  |   package: str
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # the minimum compatibility requirements for this model, regardless
 | 
					
						
							|  |  |  |   # of market. can be a package, trim, or list of features
 | 
					
						
							|  |  |  |   requirements: str | None = None
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   video_link: str | None = None
 | 
					
						
							|  |  |  |   footnotes: list[Enum] = field(default_factory=list)
 | 
					
						
							|  |  |  |   min_steer_speed: float | None = None
 | 
					
						
							|  |  |  |   min_enable_speed: float | None = None
 | 
					
						
							|  |  |  |   auto_resume: bool | None = None
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   # all the parts needed for the supported car
 | 
					
						
							|  |  |  |   car_parts: CarParts = field(default_factory=CarParts)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def __post_init__(self):
 | 
					
						
							|  |  |  |     self.make, self.model, self.years = split_name(self.name)
 | 
					
						
							|  |  |  |     self.year_list = get_year_list(self.years)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def init(self, CP: car.CarParams, all_footnotes: dict[Enum, int]):
 | 
					
						
							|  |  |  |     self.car_name = CP.carName
 | 
					
						
							|  |  |  |     self.car_fingerprint = CP.carFingerprint
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # longitudinal column
 | 
					
						
							|  |  |  |     op_long = "Stock"
 | 
					
						
							|  |  |  |     if CP.experimentalLongitudinalAvailable or CP.enableDsu:
 | 
					
						
							|  |  |  |       op_long = "openpilot available"
 | 
					
						
							|  |  |  |       if CP.enableDsu:
 | 
					
						
							|  |  |  |         self.footnotes.append(CommonFootnote.EXP_LONG_DSU)
 | 
					
						
							|  |  |  |       else:
 | 
					
						
							|  |  |  |         self.footnotes.append(CommonFootnote.EXP_LONG_AVAIL)
 | 
					
						
							|  |  |  |     elif CP.openpilotLongitudinalControl and not CP.enableDsu:
 | 
					
						
							|  |  |  |       op_long = "openpilot"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # min steer & enable speed columns
 | 
					
						
							|  |  |  |     # 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 CarDocs and CarParams"
 | 
					
						
							|  |  |  |     else:
 | 
					
						
							|  |  |  |       self.min_steer_speed = CP.minSteerSpeed
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # 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
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if self.auto_resume is None:
 | 
					
						
							|  |  |  |       self.auto_resume = CP.autoResumeSng
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # hardware column
 | 
					
						
							|  |  |  |     hardware_col = "None"
 | 
					
						
							|  |  |  |     if self.car_parts.parts:
 | 
					
						
							|  |  |  |       model_years = self.model + (' ' + self.years if self.years else '')
 | 
					
						
							|  |  |  |       buy_link = f'<a href="https://comma.ai/shop/comma-3x.html?make={self.make}&model={model_years}">Buy Here</a>'
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       tools_docs = [part for part in self.car_parts.all_parts() if isinstance(part, Tool)]
 | 
					
						
							|  |  |  |       parts_docs = [part for part in self.car_parts.all_parts() if not isinstance(part, Tool)]
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       def display_func(parts):
 | 
					
						
							|  |  |  |         return '<br>'.join([f"- {parts.count(part)} {part.value.name}" for part in sorted(set(parts), key=lambda part: str(part.value.name))])
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       hardware_col = f'<details><summary>Parts</summary><sub>{display_func(parts_docs)}<br>{buy_link}</sub></details>'
 | 
					
						
							|  |  |  |       if len(tools_docs):
 | 
					
						
							|  |  |  |         hardware_col += f'<details><summary>Tools</summary><sub>{display_func(tools_docs)}</sub></details>'
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.row: dict[Enum, str | Star] = {
 | 
					
						
							|  |  |  |       Column.MAKE: self.make,
 | 
					
						
							|  |  |  |       Column.MODEL: self.model,
 | 
					
						
							|  |  |  |       Column.PACKAGE: self.package,
 | 
					
						
							|  |  |  |       Column.LONGITUDINAL: op_long,
 | 
					
						
							|  |  |  |       Column.FSR_LONGITUDINAL: f"{max(self.min_enable_speed * CV.MS_TO_MPH, 0):.0f} mph",
 | 
					
						
							|  |  |  |       Column.FSR_STEERING: f"{max(self.min_steer_speed * CV.MS_TO_MPH, 0):.0f} mph",
 | 
					
						
							|  |  |  |       Column.STEERING_TORQUE: Star.EMPTY,
 | 
					
						
							|  |  |  |       Column.AUTO_RESUME: Star.FULL if self.auto_resume else Star.EMPTY,
 | 
					
						
							|  |  |  |       Column.HARDWARE: hardware_col,
 | 
					
						
							|  |  |  |       Column.VIDEO: self.video_link if self.video_link is not None else "",  # replaced with an image and link from template in get_column
 | 
					
						
							|  |  |  |     }
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # 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
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self.all_footnotes = all_footnotes
 | 
					
						
							|  |  |  |     self.detail_sentence = self.get_detail_sentence(CP)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return self
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def init_make(self, CP: car.CarParams):
 | 
					
						
							|  |  |  |     """CarDocs subclasses can add make-specific logic for harness selection, footnotes, etc."""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def get_detail_sentence(self, CP):
 | 
					
						
							|  |  |  |     if not CP.notCar:
 | 
					
						
							|  |  |  |       sentence_builder = "openpilot upgrades your <strong>{car_model}</strong> with automated lane centering{alc} and adaptive cruise control{acc}."
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if self.min_steer_speed > self.min_enable_speed:
 | 
					
						
							|  |  |  |         alc = f" <strong>above {self.min_steer_speed * CV.MS_TO_MPH:.0f} mph</strong>," if self.min_steer_speed > 0 else " <strong>at all speeds</strong>,"
 | 
					
						
							|  |  |  |       else:
 | 
					
						
							|  |  |  |         alc = ""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # Exception for cars which do not auto-resume yet
 | 
					
						
							|  |  |  |       acc = ""
 | 
					
						
							|  |  |  |       if self.min_enable_speed > 0:
 | 
					
						
							|  |  |  |         acc = f" <strong>while driving above {self.min_enable_speed * CV.MS_TO_MPH:.0f} mph</strong>"
 | 
					
						
							|  |  |  |       elif self.auto_resume:
 | 
					
						
							|  |  |  |         acc = " <strong>that automatically resumes from a stop</strong>"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if self.row[Column.STEERING_TORQUE] != Star.FULL:
 | 
					
						
							|  |  |  |         sentence_builder += " This car may not be able to take tight turns on its own."
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       # experimental mode
 | 
					
						
							|  |  |  |       exp_link = "<a href='https://blog.comma.ai/090release/#experimental-mode' target='_blank' class='link-light-new-regular-text'>Experimental mode</a>"
 | 
					
						
							|  |  |  |       if CP.openpilotLongitudinalControl or CP.experimentalLongitudinalAvailable:
 | 
					
						
							|  |  |  |         sentence_builder += f" Traffic light and stop sign handling is also available in {exp_link}."
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       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. <a href='https://www.commabody.com'>Learn more.</a>"
 | 
					
						
							|  |  |  |       else:
 | 
					
						
							|  |  |  |         raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}")
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str:
 | 
					
						
							|  |  |  |     item: str | Star = self.row[column]
 | 
					
						
							|  |  |  |     if isinstance(item, Star):
 | 
					
						
							|  |  |  |       item = star_icon.format(item.value)
 | 
					
						
							|  |  |  |     elif column == Column.MODEL and len(self.years):
 | 
					
						
							|  |  |  |       item += f" {self.years}"
 | 
					
						
							|  |  |  |     elif column == Column.VIDEO and len(item) > 0:
 | 
					
						
							|  |  |  |       item = video_icon.format(item)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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
 |