docs_definitions.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import re
  2. from collections import namedtuple
  3. from dataclasses import dataclass, field
  4. from enum import Enum
  5. from typing import Dict, List, Optional, Tuple, Union
  6. from cereal import car
  7. from common.conversions import Conversions as CV
  8. GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2
  9. MODEL_YEARS_RE = r"(?<= )((\d{4}-\d{2})|(\d{4}))(,|$)"
  10. class Column(Enum):
  11. MAKE = "Make"
  12. MODEL = "Model"
  13. PACKAGE = "Supported Package"
  14. LONGITUDINAL = "ACC"
  15. FSR_LONGITUDINAL = "No ACC accel below"
  16. FSR_STEERING = "No ALC below"
  17. STEERING_TORQUE = "Steering Torque"
  18. AUTO_RESUME = "Resume from stop"
  19. HARNESS = "Harness"
  20. class Star(Enum):
  21. FULL = "full"
  22. HALF = "half"
  23. EMPTY = "empty"
  24. class Harness(Enum):
  25. nidec = "Honda Nidec"
  26. bosch_a = "Honda Bosch A"
  27. bosch_b = "Honda Bosch B"
  28. toyota = "Toyota"
  29. subaru_a = "Subaru A"
  30. subaru_b = "Subaru B"
  31. fca = "FCA"
  32. ram = "Ram"
  33. vw = "VW"
  34. j533 = "J533"
  35. hyundai_a = "Hyundai A"
  36. hyundai_b = "Hyundai B"
  37. hyundai_c = "Hyundai C"
  38. hyundai_d = "Hyundai D"
  39. hyundai_e = "Hyundai E"
  40. hyundai_f = "Hyundai F"
  41. hyundai_g = "Hyundai G"
  42. hyundai_h = "Hyundai H"
  43. hyundai_i = "Hyundai I"
  44. hyundai_j = "Hyundai J"
  45. hyundai_k = "Hyundai K"
  46. hyundai_l = "Hyundai L"
  47. hyundai_m = "Hyundai M"
  48. hyundai_n = "Hyundai N"
  49. hyundai_o = "Hyundai O"
  50. hyundai_p = "Hyundai P"
  51. hyundai_q = "Hyundai Q"
  52. custom = "Developer"
  53. obd_ii = "OBD-II"
  54. gm = "GM"
  55. nissan_a = "Nissan A"
  56. nissan_b = "Nissan B"
  57. mazda = "Mazda"
  58. ford_q3 = "Ford Q3"
  59. ford_q4 = "Ford Q4"
  60. none = "None"
  61. CarFootnote = namedtuple("CarFootnote", ["text", "column", "docs_only", "shop_footnote"], defaults=(False, False))
  62. class CommonFootnote(Enum):
  63. EXP_LONG_AVAIL = CarFootnote(
  64. "Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `master-ci`. ",
  65. Column.LONGITUDINAL, docs_only=True)
  66. EXP_LONG_DSU = CarFootnote(
  67. "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 " +
  68. "stock ACC. <b><i>NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b>",
  69. Column.LONGITUDINAL)
  70. def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]:
  71. # Returns applicable footnotes given current column
  72. return [fn for fn in footnotes if fn.value.column == column]
  73. # TODO: store years as a list
  74. def get_year_list(years):
  75. years_list = []
  76. if len(years) == 0:
  77. return years_list
  78. for year in years.split(','):
  79. year = year.strip()
  80. if len(year) == 4:
  81. years_list.append(str(year))
  82. elif "-" in year and len(year) == 7:
  83. start, end = year.split("-")
  84. years_list.extend(map(str, range(int(start), int(f"20{end}") + 1)))
  85. else:
  86. raise Exception(f"Malformed year string: {years}")
  87. return years_list
  88. def split_name(name: str) -> Tuple[str, str, str]:
  89. make, model = name.split(" ", 1)
  90. years = ""
  91. match = re.search(MODEL_YEARS_RE, model)
  92. if match is not None:
  93. years = model[match.start():]
  94. model = model[:match.start() - 1]
  95. return make, model, years
  96. @dataclass
  97. class CarInfo:
  98. name: str
  99. package: str
  100. video_link: Optional[str] = None
  101. footnotes: List[Enum] = field(default_factory=list)
  102. min_steer_speed: Optional[float] = None
  103. min_enable_speed: Optional[float] = None
  104. harness: Enum = Harness.none
  105. def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]):
  106. # TODO: set all the min steer speeds in carParams and remove this
  107. if self.min_steer_speed is not None:
  108. assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarInfo and CarParams"
  109. else:
  110. self.min_steer_speed = CP.minSteerSpeed
  111. # TODO: set all the min enable speeds in carParams correctly and remove this
  112. if self.min_enable_speed is None:
  113. self.min_enable_speed = CP.minEnableSpeed
  114. self.car_name = CP.carName
  115. self.car_fingerprint = CP.carFingerprint
  116. self.make, self.model, self.years = split_name(self.name)
  117. op_long = "Stock"
  118. if CP.openpilotLongitudinalControl and not CP.enableDsu:
  119. op_long = "openpilot"
  120. elif CP.experimentalLongitudinalAvailable or CP.enableDsu:
  121. op_long = "openpilot available"
  122. if CP.enableDsu:
  123. self.footnotes.append(CommonFootnote.EXP_LONG_DSU)
  124. else:
  125. self.footnotes.append(CommonFootnote.EXP_LONG_AVAIL)
  126. self.row = {
  127. Column.MAKE: self.make,
  128. Column.MODEL: self.model,
  129. Column.PACKAGE: self.package,
  130. Column.LONGITUDINAL: op_long,
  131. Column.FSR_LONGITUDINAL: f"{max(self.min_enable_speed * CV.MS_TO_MPH, 0):.0f} mph",
  132. Column.FSR_STEERING: f"{max(self.min_steer_speed * CV.MS_TO_MPH, 0):.0f} mph",
  133. Column.STEERING_TORQUE: Star.EMPTY,
  134. Column.AUTO_RESUME: Star.FULL if CP.autoResumeSng else Star.EMPTY,
  135. Column.HARNESS: self.harness.value,
  136. }
  137. # Set steering torque star from max lateral acceleration
  138. assert CP.maxLateralAccel > 0.1
  139. if CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD:
  140. self.row[Column.STEERING_TORQUE] = Star.FULL
  141. self.all_footnotes = all_footnotes
  142. self.year_list = get_year_list(self.years)
  143. self.detail_sentence = self.get_detail_sentence(CP)
  144. return self
  145. def init_make(self, CP: car.CarParams):
  146. """CarInfo subclasses can add make-specific logic for harness selection, footnotes, etc."""
  147. def get_detail_sentence(self, CP):
  148. if not CP.notCar:
  149. sentence_builder = "openpilot upgrades your <strong>{car_model}</strong> with automated lane centering{alc} and adaptive cruise control{acc}."
  150. if self.min_steer_speed > self.min_enable_speed:
  151. 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>,"
  152. else:
  153. alc = ""
  154. # Exception for cars which do not auto-resume yet
  155. acc = ""
  156. if self.min_enable_speed > 0:
  157. acc = f" <strong>while driving above {self.min_enable_speed * CV.MS_TO_MPH:.0f} mph</strong>"
  158. elif CP.autoResumeSng:
  159. acc = " <strong>that automatically resumes from a stop</strong>"
  160. if self.row[Column.STEERING_TORQUE] != Star.FULL:
  161. sentence_builder += " This car may not be able to take tight turns on its own."
  162. return sentence_builder.format(car_model=f"{self.make} {self.model}", alc=alc, acc=acc)
  163. else:
  164. if CP.carFingerprint == "COMMA BODY":
  165. return "The body is a robotics dev kit that can run openpilot. <a href='https://www.commabody.com'>Learn more.</a>"
  166. else:
  167. raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}")
  168. def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str:
  169. item: Union[str, Star] = self.row[column]
  170. if isinstance(item, Star):
  171. item = star_icon.format(item.value)
  172. elif column == Column.MODEL and len(self.years):
  173. item += f" {self.years}"
  174. footnotes = get_footnotes(self.footnotes, column)
  175. if len(footnotes):
  176. sups = sorted([self.all_footnotes[fn] for fn in footnotes])
  177. item += footnote_tag.format(f'{",".join(map(str, sups))}')
  178. return item