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 8375e717b5.

* sort tier_car_info in place

* unused Tuple

* no type check

* fix script
fpv2-multibus
Shane Smiskol 2022-03-23 13:42:53 -07:00 committed by GitHub
parent fc2f84759d
commit 85d8997a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 120 deletions

View File

@ -1,12 +1,12 @@
# Supported Cars # 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: Cars are organized into three tiers:
- Gold - The best openpilot experience. Great highway driving with continual updates. - Gold - The best openpilot experience. Great highway driving and beyond.
- Silver - A solid highway experience, but is limited by stock longitudinal. - Silver - A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future.
- Bronze - A solid highway experience, but will have limited performance in stop-and-go. May have ACC and ALC speed limitations. - Bronze - A good highway experience, but may have limited performance in traffic and on sharp turns.
How We Rate The Cars How We Rate The Cars
--- ---
@ -209,9 +209,9 @@ How We Rate The Cars
## Footnotes ## Footnotes
<sup>1</sup>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). <br /> <sup>1</sup>Requires an <a href="https://comma.ai/shop/products/comma-car-harness">OBD-II car harness</a> and <a href="https://github.com/commaai/openpilot/wiki/GM#hardware">community built ASCM harness</a>. <b><i>NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).</i></b> <br />
<sup>2</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br /> <sup>2</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
<sup>3</sup>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). <br /> <sup>3</sup>When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace stock Adaptive Cruise Control (ACC). <b><i> NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b> <br />
<sup>4</sup>28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br /> <sup>4</sup>28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
<sup>5</sup>An inaccurate steering wheel angle sensor makes precise control difficult. <br /> <sup>5</sup>An inaccurate steering wheel angle sensor makes precise control difficult. <br />
<sup>6</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br /> <sup>6</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />

View File

@ -2,12 +2,12 @@
from collections import Counter from collections import Counter
from pprint import pprint 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__": if __name__ == "__main__":
tiers = list(get_tier_car_rows()) tiers = get_tier_car_info()
cars = [car for tier_cars in tiers for car in tier_cars[1]] 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") print("\n", "*" * 20, len(cars), "total", "*" * 20, "\n")
pprint(make_count) pprint(make_count)

View File

@ -1,46 +1,50 @@
{% set footnote_tag = '[<sup>{}</sup>](#Footnotes)' -%}
{% set star_icon = '<a href="#"><img valign="top" src="assets/icon-star-{}.svg" width="22" /></a>' -%}
# Supported Cars # 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: Cars are organized into three tiers:
- Gold - The best openpilot experience. Great highway driving with continual updates. {% for tier in tiers %}
- Silver - A solid highway experience, but is limited by stock longitudinal. - {{tier.name.title()}} - {{tier.value}}
- Bronze - A solid highway experience, but will have limited performance in stop-and-go. May have ACC and ALC speed limitations. {% endfor %}
How We Rate The Cars How We Rate The Cars
--- ---
### openpilot Adaptive Cruise Control (ACC) ### openpilot Adaptive Cruise Control (ACC)
- {{Star.FULL.icon}} - openpilot is able to control the gas and brakes. - {{star_icon.format(Star.FULL.value)}} - 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_icon.format(Star.HALF.value)}} - 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.EMPTY.value)}} - The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system.
### Stop and Go ### Stop and Go
- {{Star.FULL.icon}} - Adaptive Cruise Control (ACC) operates down to 0 mph. - {{star_icon.format(Star.FULL.value)}} - 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.EMPTY.value)}} - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed.
### Steer to 0 ### Steer to 0
- {{Star.FULL.icon}} - openpilot can control the steering wheel down to 0 mph. - {{star_icon.format(Star.FULL.value)}} - openpilot can control the steering wheel down to 0 mph.
- {{Star.EMPTY.icon}} - No steering control below certain speeds. - {{star_icon.format(Star.EMPTY.value)}} - No steering control below certain speeds.
### Steering Torque ### Steering Torque
- {{Star.FULL.icon}} - Car has enough steering torque for comfortable highway driving. - {{star_icon.format(Star.FULL.value)}} - Car has enough steering torque for comfortable highway driving.
- {{Star.EMPTY.icon}} - Limited ability to make turns. - {{star_icon.format(Star.EMPTY.value)}} - Limited ability to make turns.
### Actively Maintained ### Actively Maintained
- {{Star.FULL.icon}} - Mainline software support, harness hardware sold by comma, lots of users, primary development target. - {{star_icon.format(Star.FULL.value)}} - 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.EMPTY.value)}} - Low user count, community maintained, harness hardware not sold by comma.
**All supported cars can move between the tiers as support changes.** **All supported cars can move between the tiers as support changes.**
{% for tier, car_rows in tiers %} {% for tier, cars in tiers.items() %}
## {{tier}} Cars ## {{tier.name.title()}} Cars
|{{columns | join('|')}}| |{{Column | map(attribute='value') | join('|')}}|
|---|---|---|:---:|:---:|:---:|:---:|:---:| |---|---|---|:---:|:---:|:---:|:---:|:---:|
{% for row in car_rows %} {% for car_info in cars %}
|{{row | join('|')}}| |{% for column in Column %}{{car_info.get_column(column, star_icon, footnote_tag)}}|{% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View File

@ -1,17 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import jinja2 import jinja2
import os import os
from enum import Enum from enum import Enum
from typing import Dict, Iterator, List, Tuple from typing import Dict, List
from common.basedir import BASEDIR 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.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.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR
from selfdrive.car.tests.routes import non_tested_cars from selfdrive.car.tests.routes import non_tested_cars
def get_all_footnotes(): def get_all_footnotes() -> Dict[Enum, int]:
all_footnotes = [] all_footnotes = []
for _, footnotes in get_interface_attr("Footnote").items(): for _, footnotes in get_interface_attr("Footnote").items():
if footnotes is not None: 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") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md")
def get_tier_car_rows() -> Iterator[Tuple[str, List[str]]]: def get_tier_car_info() -> Dict[Tier, List[CarInfo]]:
tier_car_rows: Dict[Tier, list] = {tier: [] for tier in Tier} tier_car_info: Dict[Tier, List[CarInfo]] = {tier: [] for tier in Tier}
for models in get_interface_attr("CAR_INFO").values(): for models in get_interface_attr("CAR_INFO").values():
for model, car_info in models.items(): 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,) car_info = (car_info,)
for _car_info in car_info: for _car_info in car_info:
stars = _car_info.get_stars(CP, non_tested_cars) _car_info.init(CP, non_tested_cars, ALL_FOOTNOTES)
tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(stars.count(Star.FULL), Tier.BRONZE) tier_car_info[_car_info.tier].append(_car_info)
tier_car_rows[tier].append(_car_info.get_row(ALL_FOOTNOTES, stars))
# Return tier title and car rows for each tier # Sort cars by make and model + year
for tier, car_rows in tier_car_rows.items(): for tier, cars in tier_car_info.items():
yield tier.name.title(), sorted(car_rows) 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: 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] footnotes = [fn.value.text for fn in ALL_FOOTNOTES]
return template.render(tiers=tier_car_rows, columns=[column.value for column in Column], return template.render(tiers=tier_car_info, footnotes=footnotes, Star=Star, Column=Column)
footnotes=footnotes, Star=Star)
if __name__ == "__main__": if __name__ == "__main__":
# Auto generates supported cars documentation parser = argparse.ArgumentParser(description="Auto generates supported cars documentation",
with open(CARS_MD_OUT, 'w') as f: formatter_class=argparse.ArgumentDefaultsHelpFormatter)
f.write(generate_cars_md(get_tier_car_rows(), CARS_MD_TEMPLATE))
print(f"Generated and written to {CARS_MD_OUT}") 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}")

View File

@ -1,70 +1,14 @@
from cereal import car
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import List, Optional from typing import Dict, List, Optional, Union, no_type_check
@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"[<sup>{all_footnotes[footnote]}</sup>](#Footnotes)"
return row
class Tier(Enum): class Tier(Enum):
GOLD = "Gold" GOLD = "The best openpilot experience. Great highway driving and beyond."
SILVER = "Silver" SILVER = "A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future."
BRONZE = "Bronze" BRONZE = "A good highway experience, but may have limited performance in traffic and on sharp turns."
class Column(Enum): class Column(Enum):
@ -83,10 +27,6 @@ class Star(Enum):
HALF = "half" HALF = "half"
EMPTY = "empty" EMPTY = "empty"
@property
def icon(self):
return f'<a href="#"><img valign="top" src="assets/icon-star-{self.value}.svg" width="22" /></a>'
StarColumns = list(Column)[3:] StarColumns = list(Column)[3:]
CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) 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: if fn.value.column == column:
return fn return fn
return None 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

View File

@ -56,8 +56,9 @@ class CAR:
class Footnote(Enum): class Footnote(Enum):
OBD_II = CarFootnote( OBD_II = CarFootnote(
"Requires an [OBD-II](https://comma.ai/shop/products/comma-car-harness) car harness and [community built ASCM harness]" + 'Requires an <a href="https://comma.ai/shop/products/comma-car-harness">OBD-II car harness</a> and ' +
"(https://github.com/commaai/openpilot/wiki/GM#hardware). NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).", '<a href="https://github.com/commaai/openpilot/wiki/GM#hardware">community built ASCM harness</a>. ' +
'<b><i>NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).</i></b>',
Column.MODEL) Column.MODEL)

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import unittest 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): class TestCarDocs(unittest.TestCase):
def test_car_docs(self): 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: with open(CARS_MD_OUT, "r") as f:
current_cars_md = f.read() current_cars_md = f.read()

View File

@ -78,8 +78,8 @@ class CAR:
class Footnote(Enum): class Footnote(Enum):
DSU = CarFootnote( DSU = CarFootnote(
"When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace " + "When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace stock " +
"stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).", "Adaptive Cruise Control (ACC). <b><i> NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b>",
Column.LONGITUDINAL, star=Star.HALF) Column.LONGITUDINAL, star=Star.HALF)
CAMRY = CarFootnote( CAMRY = CarFootnote(
"28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.", "28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.",