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