2022-07-18 22:34:50 -07:00
import re
2022-03-16 21:27:05 -07:00
from collections import namedtuple
2023-06-27 16:48:43 -07:00
import copy
2022-07-23 08:00:17 +01:00
from dataclasses import dataclass , field
2022-03-16 21:27:05 -07:00
from enum import Enum
2022-08-18 15:12:18 -07:00
from typing import Dict , List , Optional , Tuple , Union
2022-03-23 13:42:53 -07:00
2022-08-01 13:26:24 -07:00
from cereal import car
from common . conversions import Conversions as CV
2022-06-09 18:51:34 -07:00
GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2
2022-07-18 22:34:50 -07:00
MODEL_YEARS_RE = r " (?<= )(( \ d {4} - \ d {2} )|( \ d {4} ))(,|$) "
2022-06-03 17:41:20 -07:00
2022-03-23 13:42:53 -07:00
class Column ( Enum ) :
MAKE = " Make "
MODEL = " Model "
PACKAGE = " Supported Package "
2022-08-18 15:12:18 -07:00
LONGITUDINAL = " ACC "
FSR_LONGITUDINAL = " No ACC accel below "
FSR_STEERING = " No ALC below "
2022-03-23 13:42:53 -07:00
STEERING_TORQUE = " Steering Torque "
2022-08-30 17:54:40 -07:00
AUTO_RESUME = " Resume from stop "
2023-05-22 10:17:33 -07:00
HARDWARE = " Hardware Needed "
2023-01-11 23:30:11 -08:00
VIDEO = " Video "
2022-03-23 13:42:53 -07:00
class Star ( Enum ) :
FULL = " full "
HALF = " half "
EMPTY = " empty "
2023-05-31 21:30:50 -07:00
# A part + its comprised parts
2023-05-23 00:36:42 -07:00
@dataclass
2023-05-31 21:30:50 -07:00
class BasePart :
2023-05-23 00:36:42 -07:00
name : str
2023-05-31 21:30:50 -07:00
parts : List [ Enum ] = field ( default_factory = list )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
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 ( ) )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
return parts
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
class EnumBase ( Enum ) :
2023-05-22 10:17:33 -07:00
@property
2023-05-31 21:30:50 -07:00
def type ( self ) :
return PartType ( self . __class__ )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
class Mount ( EnumBase ) :
mount = BasePart ( " mount " )
2023-06-01 19:49:42 -07:00
angled_mount_8_degrees = BasePart ( " angled mount (8 degrees) " )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
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) " )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
class Accessory ( EnumBase ) :
harness_box = BasePart ( " harness box " )
comma_power_v2 = BasePart ( " comma power v2 " )
2023-05-22 10:17:33 -07:00
2023-05-31 21:30:50 -07:00
@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 = BaseCarHarness ( " Toyota connector " )
subaru_a = BaseCarHarness ( " Subaru A connector " )
subaru_b = BaseCarHarness ( " Subaru B connector " )
2023-07-31 13:58:33 -07:00
subaru_c = BaseCarHarness ( " Subaru C connector " )
subaru_d = BaseCarHarness ( " Subaru D connector " )
2023-05-31 21:30:50 -07:00
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 " )
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 " )
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 " )
class Device ( EnumBase ) :
three = BasePart ( " comma three " , parts = [ Mount . mount , Cable . right_angle_obd_c_cable_1_5ft ] )
# variant of comma three with angled mounts
2023-06-01 19:49:42 -07:00
three_angled_mount = BasePart ( " comma three " , parts = [ Mount . angled_mount_8_degrees , Cable . right_angle_obd_c_cable_1_5ft ] )
2023-05-31 21:30:50 -07:00
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 ] )
2023-08-01 00:06:21 -07:00
class Tool ( EnumBase ) :
2023-08-01 13:24:51 -07:00
socket_8mm_deep = BasePart ( " Socket Wrench 8mm or 5/16 \" (deep) " )
2023-08-01 00:06:21 -07:00
pry_tool = BasePart ( " Pry Tool " )
2023-05-31 21:30:50 -07:00
class PartType ( Enum ) :
accessory = Accessory
cable = Cable
connector = CarHarness
device = Device
kit = Kit
mount = Mount
2023-08-01 00:06:21 -07:00
tool = Tool
2023-05-31 21:30:50 -07:00
2023-08-01 13:24:51 -07:00
2023-05-31 21:30:50 -07:00
DEFAULT_CAR_PARTS : List [ EnumBase ] = [ Device . three ]
2023-05-15 15:33:54 -07:00
2023-08-01 13:24:51 -07:00
2023-05-15 15:33:54 -07:00
@dataclass
2023-05-22 10:17:33 -07:00
class CarParts :
2023-05-31 21:30:50 -07:00
parts : List [ EnumBase ] = field ( default_factory = list )
2023-05-22 10:17:33 -07:00
2023-06-27 16:48:43 -07:00
def __call__ ( self ) :
return copy . deepcopy ( self )
2023-05-22 10:17:33 -07:00
@classmethod
2023-07-20 21:56:57 -07:00
def common ( cls , add : Optional [ List [ EnumBase ] ] = None , remove : Optional [ List [ EnumBase ] ] = None ) :
2023-05-22 10:17:33 -07:00
p = [ part for part in ( add or [ ] ) + DEFAULT_CAR_PARTS if part not in ( remove or [ ] ) ]
return cls ( p )
2023-05-15 15:33:54 -07:00
2023-05-31 21:30:50 -07:00
def all_parts ( self ) :
parts = [ ]
for part in self . parts :
parts . extend ( part . value . all_parts ( ) )
return self . parts + parts
2023-05-15 15:33:54 -07:00
2022-12-06 15:00:11 -08:00
CarFootnote = namedtuple ( " CarFootnote " , [ " text " , " column " , " docs_only " , " shop_footnote " ] , defaults = ( False , False ) )
2022-03-23 13:42:53 -07:00
2022-11-01 15:55:21 -07:00
class CommonFootnote ( Enum ) :
EXP_LONG_AVAIL = CarFootnote (
2022-11-21 15:59:42 -08:00
" Experimental openpilot longitudinal control is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `master-ci`. " ,
2022-11-02 18:56:26 -07:00
Column . LONGITUDINAL , docs_only = True )
2022-11-03 01:00:33 -07:00
EXP_LONG_DSU = CarFootnote (
2022-11-21 15:59:42 -08:00
" 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> " ,
2022-11-03 01:00:33 -07:00
Column . LONGITUDINAL )
2022-11-01 15:55:21 -07:00
2022-07-23 08:00:17 +01:00
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 ]
2022-03-16 21:27:05 -07:00
2022-08-01 13:26:24 -07:00
# 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
2022-07-18 22:34:50 -07:00
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
2022-03-16 21:27:05 -07:00
@dataclass
class CarInfo :
2023-04-04 17:47:49 -07:00
# make + model + model years
2022-03-16 21:27:05 -07:00
name : str
2023-04-04 17:47:49 -07:00
# 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
2022-03-16 21:27:05 -07:00
package : str
2023-04-04 17:47:49 -07:00
# the minimum compatibility requirements for this model, regardless
# of market. can be a package, trim, or list of features
requirements : Optional [ str ] = None
2022-03-16 21:27:05 -07:00
video_link : Optional [ str ] = None
2022-07-23 08:00:17 +01:00
footnotes : List [ Enum ] = field ( default_factory = list )
2022-03-16 21:27:05 -07:00
min_steer_speed : Optional [ float ] = None
min_enable_speed : Optional [ float ] = None
2023-05-23 01:40:23 -07:00
auto_resume : Optional [ bool ] = None
2023-05-15 15:33:54 -07:00
2023-05-22 10:17:33 -07:00
# all the parts needed for the supported car
2023-06-27 16:48:43 -07:00
car_parts : CarParts = field ( default_factory = CarParts )
2022-03-16 21:27:05 -07:00
2022-07-18 22:34:50 -07:00
def init ( self , CP : car . CarParams , all_footnotes : Dict [ Enum , int ] ) :
2022-04-12 12:45:31 -07:00
self . car_name = CP . carName
2022-07-18 17:53:00 -07:00
self . car_fingerprint = CP . carFingerprint
2022-07-18 22:34:50 -07:00
self . make , self . model , self . years = split_name ( self . name )
2022-11-01 15:55:21 -07:00
2023-01-13 17:52:24 -08:00
# longitudinal column
2022-11-01 15:55:21 -07:00
op_long = " Stock "
2023-06-27 20:19:41 -07:00
if CP . experimentalLongitudinalAvailable or CP . enableDsu :
2022-11-01 15:55:21 -07:00
op_long = " openpilot available "
2022-11-03 01:00:33 -07:00
if CP . enableDsu :
self . footnotes . append ( CommonFootnote . EXP_LONG_DSU )
else :
self . footnotes . append ( CommonFootnote . EXP_LONG_AVAIL )
2023-06-27 20:19:41 -07:00
elif CP . openpilotLongitudinalControl and not CP . enableDsu :
op_long = " openpilot "
2022-11-01 15:55:21 -07:00
2023-01-13 17:52:24 -08:00
# 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 CarInfo 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
2023-05-23 01:40:23 -07:00
if self . auto_resume is None :
self . auto_resume = CP . autoResumeSng
2023-05-22 10:17:33 -07:00
# hardware column
hardware_col = " None "
if self . car_parts . parts :
2023-01-13 17:52:24 -08:00
model_years = self . model + ( ' ' + self . years if self . years else ' ' )
2023-08-02 10:50:07 -07:00
buy_link = f ' <a href= " https://comma.ai/shop/comma-3x.html?make= { self . make } &model= { model_years } " >Buy Here</a> '
2023-08-01 00:06:21 -07:00
2023-08-01 13:24:51 -07:00
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 ) ]
2023-08-01 00:06:21 -07:00
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 ) ) ] )
2023-08-01 13:24:51 -07:00
2023-08-01 00:06:21 -07:00
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> '
2023-01-13 17:52:24 -08:00
2023-05-15 15:33:54 -07:00
self . row : Dict [ Enum , Union [ str , Star ] ] = {
2022-03-23 13:42:53 -07:00
Column . MAKE : self . make ,
Column . MODEL : self . model ,
Column . PACKAGE : self . package ,
2022-11-01 15:55:21 -07:00
Column . LONGITUDINAL : op_long ,
2022-08-18 15:12:18 -07:00
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 " ,
2022-07-18 22:34:50 -07:00
Column . STEERING_TORQUE : Star . EMPTY ,
2023-05-23 01:40:23 -07:00
Column . AUTO_RESUME : Star . FULL if self . auto_resume else Star . EMPTY ,
2023-05-22 10:17:33 -07:00
Column . HARDWARE : hardware_col ,
2023-01-11 23:30:11 -08:00
Column . VIDEO : self . video_link if self . video_link is not None else " " , # replaced with an image and link from template in get_column
2022-03-16 21:27:05 -07:00
}
2022-07-01 19:11:28 -07:00
# Set steering torque star from max lateral acceleration
2022-07-18 17:53:00 -07:00
assert CP . maxLateralAccel > 0.1
if CP . maxLateralAccel > = GOOD_TORQUE_THRESHOLD :
self . row [ Column . STEERING_TORQUE ] = Star . FULL
2022-06-09 18:51:34 -07:00
2022-03-23 13:42:53 -07:00
self . all_footnotes = all_footnotes
2022-08-01 13:26:24 -07:00
self . year_list = get_year_list ( self . years )
self . detail_sentence = self . get_detail_sentence ( CP )
2022-05-11 15:59:08 -07:00
return self
2022-03-16 21:27:05 -07:00
2022-11-21 18:01:08 -08:00
def init_make ( self , CP : car . CarParams ) :
""" CarInfo subclasses can add make-specific logic for harness selection, footnotes, etc. """
2022-08-01 13:26:24 -07:00
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 = " "
2022-08-12 19:10:34 -04:00
# Exception for cars which do not auto-resume yet
2022-08-01 17:44:22 -07:00
acc = " "
if self . min_enable_speed > 0 :
acc = f " <strong>while driving above { self . min_enable_speed * CV . MS_TO_MPH : .0f } mph</strong> "
2023-05-23 01:40:23 -07:00
elif self . auto_resume :
2022-08-01 17:44:22 -07:00
acc = " <strong>that automatically resumes from a stop</strong> "
2022-08-01 13:26:24 -07:00
if self . row [ Column . STEERING_TORQUE ] != Star . FULL :
sentence_builder + = " This car may not be able to take tight turns on its own. "
2023-01-14 16:28:17 -08:00
# 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 } . "
2022-08-01 13:26:24 -07:00
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 } " )
2023-01-11 23:30:11 -08:00
def get_column ( self , column : Column , star_icon : str , video_icon : str , footnote_tag : str ) - > str :
2022-03-23 13:42:53 -07:00
item : Union [ str , Star ] = self . row [ column ]
2022-08-18 15:12:18 -07:00
if isinstance ( item , Star ) :
2022-03-23 13:42:53 -07:00
item = star_icon . format ( item . value )
2022-07-18 22:34:50 -07:00
elif column == Column . MODEL and len ( self . years ) :
item + = f " { self . years } "
2023-01-11 23:30:11 -08:00
elif column == Column . VIDEO and len ( item ) > 0 :
item = video_icon . format ( item )
2022-03-16 21:27:05 -07:00
2022-07-23 08:00:17 +01:00
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 ) ) } ' )
2022-03-16 21:27:05 -07:00
2022-03-23 13:42:53 -07:00
return item