diff --git a/docs/CARS.md b/docs/CARS.md index 339d78b936..72aa3bd5c2 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -1,12 +1,12 @@ # Supported Cars -A supported vehicle is one that just works when you install openpilot on a compatible device. Every car performs differently with openpilot, but we aim for all supported cars to provide a solid highway experience in the US market. +A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. Cars are organized into three tiers: -- Gold - The best openpilot experience. Great highway driving with continual updates. -- Silver - A solid highway experience, but is limited by stock longitudinal. -- Bronze - A solid highway experience, but will have limited performance in stop-and-go. May have ACC and ALC speed limitations. +- Gold - The best openpilot experience. Great highway driving and beyond. +- Silver - A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future. +- Bronze - A good highway experience, but may have limited performance in traffic and on sharp turns. How We Rate The Cars --- @@ -209,9 +209,9 @@ How We Rate The Cars ## Footnotes -1Requires 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).
+1Requires an OBD-II car harness and community built ASCM harness. NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).
22019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
-3When 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).
+3When 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).
428mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
5An inaccurate steering wheel angle sensor makes precise control difficult.
6Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
diff --git a/scripts/count_cars.py b/scripts/count_cars.py index 3fdbe93860..46d8499461 100755 --- a/scripts/count_cars.py +++ b/scripts/count_cars.py @@ -2,12 +2,12 @@ from collections import Counter from pprint import pprint -from selfdrive.car.docs import get_tier_car_rows +from selfdrive.car.docs import get_tier_car_info if __name__ == "__main__": - tiers = list(get_tier_car_rows()) - cars = [car for tier_cars in tiers for car in tier_cars[1]] + tiers = get_tier_car_info() + cars = [car for tier_cars in tiers.values() for car in tier_cars] - make_count = Counter(l[0] for l in cars) + make_count = Counter(l.make for l in cars) print("\n", "*" * 20, len(cars), "total", "*" * 20, "\n") pprint(make_count) diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 72ca40d4d1..4c27e8a271 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -1,46 +1,50 @@ +{% set footnote_tag = '[{}](#Footnotes)' -%} +{% set star_icon = '' -%} + # Supported Cars -A supported vehicle is one that just works when you install openpilot on a compatible device. Every car performs differently with openpilot, but we aim for all supported cars to provide a solid highway experience in the US market. +A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. Cars are organized into three tiers: -- Gold - The best openpilot experience. Great highway driving with continual updates. -- Silver - A solid highway experience, but is limited by stock longitudinal. -- Bronze - A solid highway experience, but will have limited performance in stop-and-go. May have ACC and ALC speed limitations. +{% for tier in tiers %} +- {{tier.name.title()}} - {{tier.value}} +{% endfor %} How We Rate The Cars --- ### openpilot Adaptive Cruise Control (ACC) -- {{Star.FULL.icon}} - openpilot is able to control the gas and brakes. -- {{Star.HALF.icon}} - openpilot is able to control the gas and brakes with some restrictions. -- {{Star.EMPTY.icon}} - The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system. +- {{star_icon.format(Star.FULL.value)}} - openpilot is able to control the gas and brakes. +- {{star_icon.format(Star.HALF.value)}} - openpilot is able to control the gas and brakes with some restrictions. +- {{star_icon.format(Star.EMPTY.value)}} - The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system. ### Stop and Go -- {{Star.FULL.icon}} - Adaptive Cruise Control (ACC) operates down to 0 mph. -- {{Star.EMPTY.icon}} - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed. +- {{star_icon.format(Star.FULL.value)}} - Adaptive Cruise Control (ACC) operates down to 0 mph. +- {{star_icon.format(Star.EMPTY.value)}} - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed. ### Steer to 0 -- {{Star.FULL.icon}} - openpilot can control the steering wheel down to 0 mph. -- {{Star.EMPTY.icon}} - No steering control below certain speeds. +- {{star_icon.format(Star.FULL.value)}} - openpilot can control the steering wheel down to 0 mph. +- {{star_icon.format(Star.EMPTY.value)}} - No steering control below certain speeds. ### Steering Torque -- {{Star.FULL.icon}} - Car has enough steering torque for comfortable highway driving. -- {{Star.EMPTY.icon}} - Limited ability to make turns. +- {{star_icon.format(Star.FULL.value)}} - Car has enough steering torque for comfortable highway driving. +- {{star_icon.format(Star.EMPTY.value)}} - Limited ability to make turns. ### Actively Maintained -- {{Star.FULL.icon}} - Mainline software support, harness hardware sold by comma, lots of users, primary development target. -- {{Star.EMPTY.icon}} - Low user count, community maintained, harness hardware not sold by comma. +- {{star_icon.format(Star.FULL.value)}} - Mainline software support, harness hardware sold by comma, lots of users, primary development target. +- {{star_icon.format(Star.EMPTY.value)}} - Low user count, community maintained, harness hardware not sold by comma. **All supported cars can move between the tiers as support changes.** -{% for tier, car_rows in tiers %} -## {{tier}} Cars +{% for tier, cars in tiers.items() %} +## {{tier.name.title()}} Cars -|{{columns | join('|')}}| +|{{Column | map(attribute='value') | join('|')}}| |---|---|---|:---:|:---:|:---:|:---:|:---:| -{% for row in car_rows %} -|{{row | join('|')}}| +{% for car_info in cars %} +|{% for column in Column %}{{car_info.get_column(column, star_icon, footnote_tag)}}|{% endfor %} + {% endfor %} {% endfor %} diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 362b142353..baa5732648 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -1,17 +1,18 @@ #!/usr/bin/env python3 +import argparse import jinja2 import os from enum import Enum -from typing import Dict, Iterator, List, Tuple +from typing import Dict, List from common.basedir import BASEDIR -from selfdrive.car.docs_definitions import Column, Star, Tier +from selfdrive.car.docs_definitions import CarInfo, Column, Star, Tier from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR from selfdrive.car.tests.routes import non_tested_cars -def get_all_footnotes(): +def get_all_footnotes() -> Dict[Enum, int]: all_footnotes = [] for _, footnotes in get_interface_attr("Footnote").items(): if footnotes is not None: @@ -24,8 +25,8 @@ CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md") -def get_tier_car_rows() -> Iterator[Tuple[str, List[str]]]: - tier_car_rows: Dict[Tier, list] = {tier: [] for tier in Tier} +def get_tier_car_info() -> Dict[Tier, List[CarInfo]]: + tier_car_info: Dict[Tier, List[CarInfo]] = {tier: [] for tier in Tier} for models in get_interface_attr("CAR_INFO").values(): for model, car_info in models.items(): @@ -41,26 +42,32 @@ def get_tier_car_rows() -> Iterator[Tuple[str, List[str]]]: car_info = (car_info,) for _car_info in car_info: - stars = _car_info.get_stars(CP, non_tested_cars) - tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(stars.count(Star.FULL), Tier.BRONZE) - tier_car_rows[tier].append(_car_info.get_row(ALL_FOOTNOTES, stars)) + _car_info.init(CP, non_tested_cars, ALL_FOOTNOTES) + tier_car_info[_car_info.tier].append(_car_info) - # Return tier title and car rows for each tier - for tier, car_rows in tier_car_rows.items(): - yield tier.name.title(), sorted(car_rows) + # Sort cars by make and model + year + for tier, cars in tier_car_info.items(): + tier_car_info[tier] = sorted(cars, key=lambda x: x.make + x.model) + return tier_car_info -def generate_cars_md(tier_car_rows: Iterator[Tuple[str, List[str]]], template_fn: str) -> str: + +def generate_cars_md(tier_car_info: Dict[Tier, List[CarInfo]], template_fn: str) -> str: with open(template_fn, "r") as f: - template = jinja2.Template(f.read(), trim_blocks=True) + template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) footnotes = [fn.value.text for fn in ALL_FOOTNOTES] - return template.render(tiers=tier_car_rows, columns=[column.value for column in Column], - footnotes=footnotes, Star=Star) + return template.render(tiers=tier_car_info, footnotes=footnotes, Star=Star, Column=Column) if __name__ == "__main__": - # Auto generates supported cars documentation - with open(CARS_MD_OUT, 'w') as f: - f.write(generate_cars_md(get_tier_car_rows(), CARS_MD_TEMPLATE)) - print(f"Generated and written to {CARS_MD_OUT}") + parser = argparse.ArgumentParser(description="Auto generates supported cars documentation", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument("--template", default=CARS_MD_TEMPLATE, help="Override default template filename") + parser.add_argument("--out", default=CARS_MD_OUT, help="Override default generated filename") + args = parser.parse_args() + + with open(args.out, 'w') as f: + f.write(generate_cars_md(get_tier_car_info(), args.template)) + print(f"Generated and written to {args.out}") diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 95591652ee..6351766789 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -1,7 +1,44 @@ +from cereal import car from collections import namedtuple from dataclasses import dataclass from enum import Enum -from typing import List, Optional +from typing import Dict, List, Optional, Union, no_type_check + + +class Tier(Enum): + GOLD = "The best openpilot experience. Great highway driving and beyond." + SILVER = "A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future." + BRONZE = "A good highway experience, but may have limited performance in traffic and on sharp turns." + + +class Column(Enum): + MAKE = "Make" + MODEL = "Model" + PACKAGE = "Supported Package" + LONGITUDINAL = "openpilot ACC" + FSR_LONGITUDINAL = "Stop and Go" + FSR_STEERING = "Steer to 0" + STEERING_TORQUE = "Steering Torque" + MAINTAINED = "Actively Maintained" + + +class Star(Enum): + FULL = "full" + HALF = "half" + EMPTY = "empty" + + +StarColumns = list(Column)[3:] +CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) + + +def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[Enum]: + # Returns applicable footnote given current column + if footnotes is not None: + for fn in footnotes: + if fn.value.column == column: + return fn + return None @dataclass @@ -14,7 +51,7 @@ class CarInfo: min_enable_speed: Optional[float] = None good_torque: bool = False - def get_stars(self, CP, non_tested_cars): + def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dict[Enum, int]): # TODO: set all the min steer speeds in carParams and remove this min_steer_speed = CP.minSteerSpeed if self.min_steer_speed is not None: @@ -26,7 +63,12 @@ class CarInfo: if self.min_enable_speed is not None: min_enable_speed = self.min_enable_speed - stars = { + self.make, self.model = self.name.split(' ', 1) + self.row = { + Column.MAKE: self.make, + Column.MODEL: self.model, + Column.PACKAGE: self.package, + # StarColumns Column.LONGITUDINAL: CP.openpilotLongitudinalControl and not CP.radarOffCan, Column.FSR_LONGITUDINAL: min_enable_speed <= 0., Column.FSR_STEERING: min_steer_speed <= 0., @@ -34,68 +76,25 @@ class CarInfo: Column.MAINTAINED: CP.carFingerprint not in non_tested_cars, } + self.all_footnotes = all_footnotes for column in StarColumns: - stars[column] = Star.FULL if stars[column] else Star.EMPTY + self.row[column] = Star.FULL if self.row[column] else Star.EMPTY # Demote if footnote specifies a star footnote = get_footnote(self.footnotes, column) if footnote is not None and footnote.value.star is not None: - stars[column] = footnote.value.star + self.row[column] = footnote.value.star - return [stars[column] for column in StarColumns] + self.tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(list(self.row.values()).count(Star.FULL), Tier.BRONZE) - def get_row(self, all_footnotes, stars): - # TODO: add YouTube vidos - make, model = self.name.split(' ', 1) - row = [make, model, self.package, *stars] + @no_type_check + def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: + item: Union[str, Star] = self.row[column] + if column in StarColumns: + item = star_icon.format(item.value) - # Check for car footnotes and get star icons - for row_idx, column in enumerate(Column): - if column in StarColumns: - row[row_idx] = row[row_idx].icon - - footnote = get_footnote(self.footnotes, column) - if footnote is not None: - row[row_idx] += f"[{all_footnotes[footnote]}](#Footnotes)" + footnote = get_footnote(self.footnotes, column) + if footnote is not None: + item += footnote_tag.format(self.all_footnotes[footnote]) - return row - - -class Tier(Enum): - GOLD = "Gold" - SILVER = "Silver" - BRONZE = "Bronze" - - -class Column(Enum): - MAKE = "Make" - MODEL = "Model" - PACKAGE = "Supported Package" - LONGITUDINAL = "openpilot ACC" - FSR_LONGITUDINAL = "Stop and Go" - FSR_STEERING = "Steer to 0" - STEERING_TORQUE = "Steering Torque" - MAINTAINED = "Actively Maintained" - - -class Star(Enum): - FULL = "full" - HALF = "half" - EMPTY = "empty" - - @property - def icon(self): - return f'' - - -StarColumns = list(Column)[3:] -CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) - - -def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[Enum]: - # Returns applicable footnote given current column - if footnotes is not None: - for fn in footnotes: - if fn.value.column == column: - return fn - return None + return item diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 8f5e6d5845..65d42579d3 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -56,8 +56,9 @@ class CAR: class Footnote(Enum): OBD_II = CarFootnote( - "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).", + 'Requires an OBD-II car harness and ' + + 'community built ASCM harness. ' + + 'NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).', Column.MODEL) diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index 33ca9a192e..05c65fa3d8 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 import unittest -from selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE, generate_cars_md, get_tier_car_rows +from selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE, generate_cars_md, get_tier_car_info class TestCarDocs(unittest.TestCase): def test_car_docs(self): - generated_cars_md = generate_cars_md(get_tier_car_rows(), CARS_MD_TEMPLATE) + generated_cars_md = generate_cars_md(get_tier_car_info(), CARS_MD_TEMPLATE) with open(CARS_MD_OUT, "r") as f: current_cars_md = f.read() diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 1aab887f54..9a620347fd 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -78,8 +78,8 @@ class CAR: class Footnote(Enum): DSU = CarFootnote( - "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).", + "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=Star.HALF) CAMRY = CarFootnote( "28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.",