diff --git a/docs/assets/icon-star-empty.png b/docs/assets/icon-star-empty.png new file mode 100644 index 0000000000..c78dd80697 Binary files /dev/null and b/docs/assets/icon-star-empty.png differ diff --git a/docs/assets/icon-star-full.png b/docs/assets/icon-star-full.png new file mode 100644 index 0000000000..66a027ee75 Binary files /dev/null and b/docs/assets/icon-star-full.png differ diff --git a/docs/assets/icon-star-half.png b/docs/assets/icon-star-half.png new file mode 100644 index 0000000000..cd5bc2d361 Binary files /dev/null and b/docs/assets/icon-star-half.png differ diff --git a/docs/assets/icon-star-old.png b/docs/assets/icon-star-old.png new file mode 100644 index 0000000000..c82d435d1d Binary files /dev/null and b/docs/assets/icon-star-old.png differ diff --git a/docs/cars.py b/docs/cars.py index 16194004ac..a2fe12052d 100755 --- a/docs/cars.py +++ b/docs/cars.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 from enum import Enum +import os -from selfdrive.car import CarInfo -from selfdrive.car.car_helpers import interfaces -from selfdrive.car.fingerprints import get_attr_from_cars, all_known_cars -from selfdrive.config import Conversions as CV +from common.basedir import BASEDIR +from selfdrive.car.car_helpers import interfaces, get_interface_attr +from selfdrive.test.test_routes import non_tested_cars +from collections import defaultdict, namedtuple class Tier(Enum): @@ -14,56 +15,95 @@ class Tier(Enum): class Car: - def __init__(self, CP, car_info): - self.CP = CP - self.info = car_info - self.make, self.model = self.info.name.split(' ', 1) + def __init__(self, car_info, CP): + self.make, self.model = car_info.name.split(' ', 1) + + assert len(car_info.years), 'Model {} has no years listed'.format(CP.carFingerprint) + + # TODO: properly format model years + years = ' ' + str(max(car_info.years)) + self.model_string = "{}{}".format(self.model, years) + self.package = car_info.package + + self.stars = Stars( + CP.openpilotLongitudinalControl, + CP.minEnableSpeed <= 1e-3, # TODO: 0 is probably okay + CP.minSteerSpeed <= 1e-3, + CP.carName in MAKES_GOOD_STEERING_TORQUE, + # TODO: make sure this check is complete + CP.carFingerprint not in non_tested_cars, + ) + + def format_stars(self): + # TODO: exceptions and half stars + return [STAR_ICON_FULL if cat else STAR_ICON_EMPTY for cat in self.stars] @property def tier(self): - if self.CP.openpilotLongitudinalControl: - return Tier.GOLD - return Tier.BRONZE + return {5: Tier.GOLD, 4: Tier.SILVER}.get(sum(self.stars), Tier.BRONZE) - @property - def years(self): - years = - for year in self.info.years: +def make_row(columns): + return "|{}|".format("|".join(columns)) - def __str__(self): - min_alc = int(max(0, self.CP.minSteerSpeed) * CV.MS_TO_MPH) - min_acc = int(max(0, self.CP.minEnableSpeed) * CV.MS_TO_MPH) - acc = "openpilot" if self.CP.openpilotLongitudinalControl else "Stock" - years = {} - return f"| {self.make} | {self.model} | {self.info.package} | {acc} | {min_acc}mph | {min_alc}mph |" +# TODO: unify with column names below? +Stars = namedtuple("Stars", ["op_long", "fsr_long", "fsr_lat", "steering_torque", "well_supported"]) + +STAR_ICON_FULL = '' +STAR_ICON_HALF = '' +STAR_ICON_EMPTY = '' + +CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS_generated.md") +CAR_TABLE_COLUMNS = make_row(['Make', 'Model (US Market Reference)', 'Supported Package', 'openpilot Longitudinal', + 'FSR Longitudinal', 'FSR Steering', 'Steering Torque', 'Actively Maintained']) +CAR_TABLE_HEADER = make_row(["---"] * 3 + [":---:"] * 5) # first three aren't centered +CAR_ROW_TEMPLATE = make_row(["{}"] * 8) + +# TODO: which other makes? +MAKES_GOOD_STEERING_TORQUE = ["toyota", "hyundai", "volkswagen"] -if __name__ == "__main__": - print("# Supported Cars") - - print("Cars are sorted into three tiers:\n") - print(" Gold - a full openpilot experience\n") - print(" Silver - a pretty good, albeit limited experience\n") - print(" Bronze - significantly limited\n") - - for t in Tier: - print(f"## {t.value} Cars") - print("| Make | Model (US Market Reference) | Supported Package | ACC | No ACC accel below | No ALC below |") - print("| ----------| ------------------------------| ------------------| -----------------| -------------------| ------------------|") - for fingerprint, car_info in sorted(get_attr_from_cars('CAR_INFO').items(), key=lambda x: x[0]): - # print(car_info) - CI, _, _ = interfaces[fingerprint] - CP = CI.get_params(fingerprint) +def generate_cars_md(): + tiered_cars = defaultdict(list) + + for _, models in get_interface_attr("CAR_INFO").items(): + for model, car_info in models.items(): + CP = interfaces[model][0].get_params(model) # Skip community supported if CP.dashcamOnly: continue - if isinstance(car_info, CarInfo): - car_info = (car_info,) + # 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) + + # Build CARS.md + 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_string) + + cars_md_doc.append("## {} Cars\n".format(tier.name.title())) + cars_md_doc.append(CAR_TABLE_COLUMNS) + cars_md_doc.append(CAR_TABLE_HEADER) + for car in cars: + line = CAR_ROW_TEMPLATE.format(car.make, + car.model_string, + car.package, + *car.format_stars()) + cars_md_doc.append(line) + cars_md_doc.append("") # newline + + return '\n'.join(cars_md_doc) + + +if __name__ == "__main__": + # TODO: add argparse for generating json or html (undecided) - for info in car_info: - car = Car(CP, info) - if car.tier == t: - print(car) + with open(CARS_MD_OUT, 'w') as f: + f.write(generate_cars_md()) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index c347f76627..53062f9586 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -58,19 +58,27 @@ def load_interfaces(brand_names): return ret -def _get_interface_names(): - # read all the folders in selfdrive/car and return a dict where: - # - keys are all the car names that which we have an interface for - # - values are lists of spefic car models for a given car +def get_interface_attr(attr): brand_names = {} for car_folder in [x[0] for x in os.walk(BASEDIR + '/selfdrive/car')]: try: brand_name = car_folder.split('/')[-1] - model_names = __import__(f'selfdrive.car.{brand_name}.values', fromlist=['CAR']).CAR - model_names = [getattr(model_names, c) for c in model_names.__dict__.keys() if not c.startswith("__")] - brand_names[brand_name] = model_names + attr_data = getattr(__import__(f'selfdrive.car.{brand_name}.values', fromlist=[attr]), attr) + brand_names[brand_name] = attr_data except (ImportError, OSError): pass + return brand_names + + +def _get_interface_names(): + # read all the folders in selfdrive/car and return a dict where: + # - keys are all the car names that which we have an interface for + # - values are lists of specific car models for a given brand + + brand_names = {} + for brand_name, model_names in get_interface_attr("CAR").items(): + model_names = [getattr(model_names, c) for c in model_names.__dict__.keys() if not c.startswith("__")] + brand_names[brand_name] = model_names return brand_names diff --git a/selfdrive/car/mock/values.py b/selfdrive/car/mock/values.py index 0dd91565bd..57ffdd39bc 100644 --- a/selfdrive/car/mock/values.py +++ b/selfdrive/car/mock/values.py @@ -1,2 +1,10 @@ +from typing import Dict + +from selfdrive.car import CarInfo + + class CAR: MOCK = 'mock' + + +CAR_INFO: Dict[int, CarInfo] = {} diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 31f3f92228..f301361416 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -91,7 +91,7 @@ class CAR: CAR_INFO = { CAR.ARTEON_MK1: CarInfo("Volkswagen Arteon", {2018, 2021}, "Driver Assistance"), CAR.ATLAS_MK1: CarInfo("Volkswagen Atlas", {2018, 2019, 2022}, "Driver Assistance"), - CAR.GOLF_MK7: ( + CAR.GOLF_MK7: [ CarInfo("Volkswagen e-Golf", {2014, 2019, 2020}, "Driver Assistance"), CarInfo("Volkswagen Golf", {2015, 2016, 2017, 2018, 2019, 2020}, "Driver Assistance"), CarInfo("Volkswagen Golf Alltrack", {2017, 2018}, "Driver Assistance"), @@ -100,26 +100,26 @@ CAR_INFO = { CarInfo("Volkswagen Golf R", {2016, 2017, 2018, 2019}, "Driver Assistance"), CarInfo("Volkswagen Golf SportsVan", {2016}, "Driver Assistance"), CarInfo("Volkswagen Golf SportWagen", {2016}, "Driver Assistance"), - ), - CAR.JETTA_MK7: ( + ], + CAR.JETTA_MK7: [ CarInfo("Volkswagen Jetta", {2018, 2019, 2020}, "Driver Assistance"), CarInfo("Volkswagen Jetta GLI", {2021}, "Driver Assistance"), - ), + ], CAR.PASSAT_MK8: CarInfo("Volkswagen Passat", {2016, 2017, 2018}, "Driver Assistance"), CAR.POLO_MK6: CarInfo("Volkswagen Polo", {2020}, "Driver Assistance"), CAR.TAOS_MK1: CarInfo("Volkswagen Taos", {2022}, "Driver Assistance"), CAR.TCROSS_MK1: CarInfo("Volkswagen T-Cross", {2021}, "Driver Assistance"), CAR.TIGUAN_MK2: CarInfo("Volkswagen Tiguan", {2020}, "Driver Assistance"), CAR.TOURAN_MK2: CarInfo("Volkswagen Touran", {2017}, "Driver Assistance"), - CAR.TRANSPORTER_T61: ( + CAR.TRANSPORTER_T61: [ CarInfo("Volkswagen Caravelle", {2020}, "Driver Assistance"), CarInfo("Volkswagen California", {2021}, "Driver Assistance"), - ), + ], CAR.TROC_MK1: CarInfo("Volkswagen T-Roc", {2021}, "Driver Assistance"), - CAR.AUDI_A3_MK3: ( + CAR.AUDI_A3_MK3: [ CarInfo("Audi A3", {2014, 2015, 2016, 2017, 2018, 2019}, "ACC + Lane Assist"), CarInfo("Audi A3 Sportback e-tron", {2017, 2018}, "ACC + Lane Assist"), - ), + ], CAR.AUDI_Q2_MK1: CarInfo("Audi Q2", {2018}, "ACC + Lane Assist"), CAR.AUDI_Q3_MK2: CarInfo("Audi Q3", {2020, 2021}, "ACC + Lane Assist"), CAR.SEAT_ATECA_MK1: CarInfo("SEAT Ateca", {2018}, "Driver Assistance"), @@ -129,10 +129,10 @@ CAR_INFO = { CAR.SKODA_KODIAQ_MK1: CarInfo("Škoda Kodiaq", {2018, 2019}, "Driver Assistance"), CAR.SKODA_SCALA_MK1: CarInfo("Škoda Scala", {2020}, "Driver Assistance"), CAR.SKODA_SUPERB_MK3: CarInfo("Škoda Superb", {2015, 2017, 2018}, "Driver Assistance"), - CAR.SKODA_OCTAVIA_MK3: ( + CAR.SKODA_OCTAVIA_MK3: [ CarInfo("Škoda Octavia", {2015, 2018, 2019}, "Driver Assistance"), CarInfo("Škoda Octavia RS", {2016}, "Driver Assistance"), - ), + ], } # All supported cars should return FW from the engine, srs, eps, and fwdRadar. Cars