227 lines
9.9 KiB
Python
Executable File
227 lines
9.9 KiB
Python
Executable File
#!/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 '<img src="assets/icon-star-{}.png" width="22" />'.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] += "<sup>{}</sup>".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")))
|