From 85d8997a8a66ef7652b1b186a72d9551292a24ad Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 23 Mar 2022 13:42:53 -0700 Subject: [PATCH] docs: support for automatically generating website vehicles page (#24020) * add vehicles.vue template * add original vue file * stash * stash * clean up a bit * add template for now * implement footnotes and tier copy * no more generator * convert to 2 spaces * should work, now onto vue * does GH handle this html well? * fix * auto-generate descriptions and make tiers' maps non-str * remove old files * move template specific variable into templates, should be a bit simpler * js template is simplier too now js template is simplier too now js template is simplier too now * add video links from the nice car_info * make rows attributes * clean up * fix * remove template * experiment with video links in GH add image how does this look? fix * Revert "experiment with video links in GH" This reverts commit 8375e717b563f7b4ecf009ffae00174de1a0b8e9. * sort tier_car_info in place * unused Tuple * no type check * fix script --- docs/CARS.md | 12 +-- scripts/count_cars.py | 8 +- selfdrive/car/CARS_template.md | 44 +++++----- selfdrive/car/docs.py | 45 ++++++----- selfdrive/car/docs_definitions.py | 129 +++++++++++++++--------------- selfdrive/car/gm/values.py | 5 +- selfdrive/car/tests/test_docs.py | 4 +- selfdrive/car/toyota/values.py | 4 +- 8 files changed, 131 insertions(+), 120 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 339d78b93..72aa3bd5c 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 3fdbe9386..46d849946 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 72ca40d4d..4c27e8a27 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 362b14235..baa573264 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 95591652e..635176678 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -1,70 +1,14 @@ +from cereal import car from collections import namedtuple from dataclasses import dataclass from enum import Enum -from typing import List, Optional - - -@dataclass -class CarInfo: - name: str - package: str - video_link: Optional[str] = None - footnotes: Optional[List[Enum]] = None - min_steer_speed: Optional[float] = None - min_enable_speed: Optional[float] = None - good_torque: bool = False - - def get_stars(self, CP, non_tested_cars): - # 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: - min_steer_speed = self.min_steer_speed - assert CP.minSteerSpeed == 0, f"Minimum steer speed set in both CarInfo and CarParams for {CP.carFingerprint}" - - # TODO: set all the min enable speeds in carParams correctly and remove this - min_enable_speed = CP.minEnableSpeed - if self.min_enable_speed is not None: - min_enable_speed = self.min_enable_speed - - stars = { - Column.LONGITUDINAL: CP.openpilotLongitudinalControl and not CP.radarOffCan, - Column.FSR_LONGITUDINAL: min_enable_speed <= 0., - Column.FSR_STEERING: min_steer_speed <= 0., - Column.STEERING_TORQUE: self.good_torque, - Column.MAINTAINED: CP.carFingerprint not in non_tested_cars, - } - - for column in StarColumns: - stars[column] = Star.FULL if stars[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 - - return [stars[column] for column in StarColumns] - - def get_row(self, all_footnotes, stars): - # TODO: add YouTube vidos - make, model = self.name.split(' ', 1) - row = [make, model, self.package, *stars] - - # 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)" - - return row +from typing import Dict, List, Optional, Union, no_type_check class Tier(Enum): - GOLD = "Gold" - SILVER = "Silver" - BRONZE = "Bronze" + 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): @@ -83,10 +27,6 @@ class Star(Enum): HALF = "half" EMPTY = "empty" - @property - def icon(self): - return f'' - StarColumns = list(Column)[3:] CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) @@ -99,3 +39,62 @@ def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[En if fn.value.column == column: return fn return None + + +@dataclass +class CarInfo: + name: str + package: str + video_link: Optional[str] = None + footnotes: Optional[List[Enum]] = None + min_steer_speed: Optional[float] = None + min_enable_speed: Optional[float] = None + good_torque: bool = False + + 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: + min_steer_speed = self.min_steer_speed + assert CP.minSteerSpeed == 0, f"Minimum steer speed set in both CarInfo and CarParams for {CP.carFingerprint}" + + # TODO: set all the min enable speeds in carParams correctly and remove this + min_enable_speed = CP.minEnableSpeed + if self.min_enable_speed is not None: + min_enable_speed = self.min_enable_speed + + 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., + Column.STEERING_TORQUE: self.good_torque, + Column.MAINTAINED: CP.carFingerprint not in non_tested_cars, + } + + self.all_footnotes = all_footnotes + for column in StarColumns: + 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: + self.row[column] = footnote.value.star + + self.tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(list(self.row.values()).count(Star.FULL), Tier.BRONZE) + + @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) + + footnote = get_footnote(self.footnotes, column) + if footnote is not None: + item += footnote_tag.format(self.all_footnotes[footnote]) + + return item diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 8f5e6d584..65d42579d 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 33ca9a192..05c65fa3d 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 1aab887f5..9a620347f 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.",