#!/usr/bin/env python3 import argparse from collections import defaultdict, namedtuple from enum import Enum import jinja2 import os from sortedcontainers import SortedList 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.chrysler.values import CAR as CHRYSLER from selfdrive.car.gm.values import CAR as GM from selfdrive.car.honda.values import CAR as HONDA from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR from selfdrive.car.hyundai.values import CAR as HYUNDAI 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 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") # 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"), ] CAR_VIDEOS = { HYUNDAI.ELANTRA_2021: "https://youtu.be/_EdYQtV52-c", HYUNDAI.ELANTRA_HEV_2021: "https://youtu.be/_EdYQtV52-c", HYUNDAI.KONA_HEV: "https://youtu.be/_EdYQtV52-c", HYUNDAI.PALISADE: "https://youtu.be/TAnDqjF4fDY?t=456", HYUNDAI.SONATA: "https://www.youtube.com/watch?v=ix63r9kE3Fw", HYUNDAI.KIA_NIRO_EV: "https://www.youtube.com/watch?v=lT7zcG6ZpGo", TOYOTA.COROLLA_TSS2: "https://www.youtube.com/watch?v=_66pXk0CBYA", TOYOTA.PRIUS_TSS2: "https://www.youtube.com/watch?v=J58TvCpUd4U", HYUNDAI.KIA_STINGER: "https://www.youtube.com/watch?v=MJ94qoofYw0", TOYOTA.CAMRY: "https://www.youtube.com/watch?v=fkcjviZY9CM", TOYOTA.CAMRYH: "https://www.youtube.com/watch?v=Q2DYY0AWKgk", TOYOTA.SIENNA: "https://www.youtube.com/watch?v=q1UPOo4Sh68", TOYOTA.HIGHLANDER: "https://www.youtube.com/watch?v=0wS0wXSLzoo", TOYOTA.PRIUS: "https://www.youtube.com/watch?v=8zopPJI8XQ0", TOYOTA.RAV4_TSS2: "https://www.youtube.com/watch?v=wJxjDd42gGA", HONDA.ACCORD: "https://www.youtube.com/watch?v=mrUwlj3Mi58", HONDA.CIVIC_BOSCH: "https://www.youtube.com/watch?v=4Iz1Mz5LGF8", CHRYSLER.JEEP_CHEROKEE: "https://www.youtube.com/watch?v=eLR9o2JkuRk", CHRYSLER.JEEP_CHEROKEE_2019: "https://www.youtube.com/watch?v=jBe4lWnRSu4", HYUNDAI.KIA_SORENTO: "https://www.youtube.com/watch?v=Fkh3s6WHJz8", } 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 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): # TODO: can we incorporate this into row()? # 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(): # Keep track of cars while sorting by make, model name, and year tiered_cars = {tier: SortedList(key=lambda car: car.make + car.model) for tier in Tier} 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].add(car) # Return tier name and car rows for each tier for tier, cars in tiered_cars.items(): yield [tier.name.title(), list(map(lambda car: car.row, cars))] def generate_cars_md(tiered_cars, template_fn): # template_fn = os.path.join(BASEDIR, "docs", "CARS_template.md") with open(template_fn, "r") as f: template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) # TODO: remove lstrip_blocks if not needed exceptions = [exception.text for exception in CAR_EXCEPTIONS] return template.render(tiers=tiered_cars, columns=[column.value for column in Column], exceptions=exceptions) if __name__ == "__main__": parser = argparse.ArgumentParser(description='') # parser.add_argument('--template', default=CARS_MD_OUT) parser.add_argument('--template', default=os.path.join(BASEDIR, "docs", "vehicles_template.vue")) args = parser.parse_args() # TODO: add argparse for generating json or html (undecided) # Cars that can disable radar have openpilot longitudinal Params().put_bool("DisableRadar", True) tiered_cars = list(get_tiered_cars()) tiered_cars_dict = {'tiers': {}, 'columns': [column.value for column in Column], 'exceptions': [exception.text for exception in CAR_EXCEPTIONS]} for idx, tier in enumerate(Tier): tiered_cars_dict['tiers'][tier.name.title()] = tiered_cars[idx][1] print(tiered_cars_dict) with open(os.path.join(BASEDIR, "docs", "vehicles.vue"), 'w') as f: json.dumps() # with open(os.path.join(BASEDIR, "docs", "vehicles.vue"), 'w') as f: # f.write(generate_cars_md(tiered_cars, args.template)) # # print('Generated and written to {}'.format(os.path.join(BASEDIR, "docs", "vehicles.vue")))