#!/usr/bin/env python3
from collections import defaultdict, namedtuple
from enum import Enum
import os
from typing import Dict
from common.basedir import BASEDIR
from common.params import Params
from selfdrive.car.car_helpers import interfaces, get_interface_attr
from selfdrive.car.gm.values import CAR as GM
from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR
from selfdrive.car.toyota.values import CAR as TOYOTA
from selfdrive.car.volkswagen.values import CAR as VOLKSWAGEN
from selfdrive.test.test_routes import non_tested_cars
class Tier(Enum):
GOLD = "Gold"
SILVER = "Silver"
BRONZE = "Bronze"
class Column(Enum):
MAKE = "Make"
MODEL = "Model"
PACKAGE = "Supported Package"
LONGITUDINAL = "openpilot Longitudinal"
FSR_LONGITUDINAL = "FSR Longitudinal"
FSR_STEERING = "FSR Steering"
STEERING_TORQUE = "Steering Torque"
SUPPORTED = "Actively Maintained"
StarColumns = list(Column)[3:]
CarException = namedtuple("CarException", ["cars", "text", "column", "star"], defaults=[None])
def make_row(columns):
return "|{}|".format("|".join(columns))
def get_star_icon(variant):
return '
'.format(variant)
def get_exceptions(CP) -> Dict[Column, CarException]:
exceptions = {}
for car_exception in CAR_EXCEPTIONS:
if CP.carFingerprint in car_exception.cars:
exceptions[car_exception.column] = car_exception
return exceptions
CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS_generated.md")
CAR_TABLE_HEADER = make_row(["---"] * 3 + [":---:"] * 5) # first three aren't centered
# TODO: which other makes?
MAKES_GOOD_STEERING_TORQUE = ["toyota", "hyundai", "volkswagen"]
CAR_EXCEPTIONS = [
CarException([TOYOTA.LEXUS_CTH, TOYOTA.LEXUS_ESH, TOYOTA.LEXUS_NX, TOYOTA.LEXUS_NXH, TOYOTA.LEXUS_RX,
TOYOTA.LEXUS_RXH, TOYOTA.AVALON, TOYOTA.AVALONH_2019, TOYOTA.COROLLA, TOYOTA.HIGHLANDER,
TOYOTA.HIGHLANDERH, TOYOTA.PRIUS, TOYOTA.PRIUS_V, TOYOTA.RAV4, TOYOTA.RAV4H, TOYOTA.SIENNA],
"When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace "
"stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).",
Column.LONGITUDINAL, star="half"),
CarException([TOYOTA.CAMRY, TOYOTA.CAMRY_TSS2, TOYOTA.CAMRYH],
"28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.",
Column.FSR_LONGITUDINAL),
CarException([GM.ESCALADE_ESV, GM.VOLT, GM.ACADIA],
"Requires an [OBD-II](https://comma.ai/shop/products/comma-car-harness) car harness and [community built ASCM harness]"
"(https://github.com/commaai/openpilot/wiki/GM#hardware). NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).",
Column.MODEL),
CarException([VOLKSWAGEN.SKODA_KAMIQ_MK1],
"Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.",
Column.MODEL),
CarException([VOLKSWAGEN.PASSAT_MK8],
"Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.",
Column.MODEL),
CarException([VOLKSWAGEN.ARTEON_MK1, VOLKSWAGEN.ATLAS_MK1, VOLKSWAGEN.TRANSPORTER_T61, VOLKSWAGEN.TCROSS_MK1,
VOLKSWAGEN.TROC_MK1, VOLKSWAGEN.TAOS_MK1, VOLKSWAGEN.TIGUAN_MK2],
'Model-years 2021 and beyond may have a new camera harness design, which isn\'t yet available from the comma '
'store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black '
'(older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" '
'from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.',
Column.MODEL),
CarException([TOYOTA.PRIUS, TOYOTA.PRIUS_V],
"An inaccurate steering wheel angle sensor makes precise control difficult.",
Column.STEERING_TORQUE, star="half"),
]
class Car:
def __init__(self, car_info, CP):
self.make, self.model = car_info.name.split(' ', 1)
self.package = car_info.package
self.exceptions = get_exceptions(CP)
self.stars = self._calculate_stars(CP, car_info)
@property
def row(self):
# TODO: add YouTube videos
row = [self.make, self.model, self.package, *map(get_star_icon, self.stars)]
# Check for car exceptions
for row_idx, column in enumerate(Column):
exception = self.exceptions.get(column, None)
if exception is not None:
superscript_number = CAR_EXCEPTIONS.index(exception) + 1
row[row_idx] += "{}".format(superscript_number)
return make_row(row)
@property
def tier(self):
return {5: Tier.GOLD, 4: Tier.SILVER}.get(self.stars.count("full"), Tier.BRONZE)
def _calculate_stars(self, CP, car_info):
# Some minimum steering speeds are not yet in CarParams
min_steer_speed = CP.minSteerSpeed
if car_info.min_steer_speed is not None:
min_steer_speed = car_info.min_steer_speed
assert CP.minSteerSpeed == 0, "Minimum steer speed set in both CarInfo and CarParams for {}".format(CP.carFingerprint)
min_enable_speed = CP.minEnableSpeed
if car_info.min_enable_speed is not None:
min_enable_speed = car_info.min_enable_speed
# TODO: make sure well supported check is complete
stars = [CP.openpilotLongitudinalControl and not CP.radarOffCan, min_enable_speed <= 1e-3, min_steer_speed <= 1e-3,
CP.carName in MAKES_GOOD_STEERING_TORQUE, CP.carFingerprint not in non_tested_cars]
# Check for star demotions from exceptions
for idx, (star, column) in enumerate(zip(stars, StarColumns)):
star = "full" if star else "empty"
exception = self.exceptions.get(column, None)
if exception is not None and exception.star is not None:
star = exception.star.lower()
stars[idx] = star
return stars
def get_tiered_cars():
tiered_cars = defaultdict(list)
for _, models in get_interface_attr("CAR_INFO").items():
for model, car_info in models.items():
# Hyundai exception: all have openpilot longitudinal
fingerprint = defaultdict(dict)
fingerprint[1] = {HKG_RADAR_START_ADDR: 8}
CP = interfaces[model][0].get_params(model, fingerprint=fingerprint)
# Skip community supported
if CP.dashcamOnly:
continue
# Some candidates have multiple variants
if not isinstance(car_info, list):
car_info = [car_info]
for _car_info in car_info:
car = Car(_car_info, CP)
tiered_cars[car.tier].append(car)
return tiered_cars
def generate_cars_md(tiered_cars):
cars_md_doc = []
for tier in Tier:
# Sort by make, model name, and year
cars = sorted(tiered_cars[tier], key=lambda car: car.make + car.model)
cars_md_doc.append("## {} Cars\n".format(tier.name.title()))
cars_md_doc.append(make_row([column.value for column in Column]))
cars_md_doc.append(CAR_TABLE_HEADER)
cars_md_doc.extend(map(lambda car: car.row, cars))
cars_md_doc.append("") # newline
return '\n'.join(cars_md_doc)
if __name__ == "__main__":
# TODO: add argparse for generating json or html (undecided)
# Cars that can disable radar have openpilot longitudinal
Params().put_bool("DisableRadar", True)
tiered_cars = get_tiered_cars()
with open(CARS_MD_OUT, 'w') as f:
f.write(generate_cars_md(tiered_cars))
print('Generated and written to {}'.format(CARS_MD_OUT))