Skip to content

Commit

Permalink
doc: extensions: boards: filter boards by hw capability
Browse files Browse the repository at this point in the history
PoC for now

Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
  • Loading branch information
kartben committed Dec 12, 2024
1 parent e383789 commit f8790a4
Show file tree
Hide file tree
Showing 6 changed files with 36,903 additions and 3 deletions.
50 changes: 50 additions & 0 deletions doc/_extensions/zephyr/domain/static/css/board-catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,56 @@
white-space: nowrap;
}





.tag-container {
display: flex;
flex-wrap: wrap;
border: 1px solid #ccc;
border-radius: 50px;
padding: 5px 18px;
}

.tag-container:focus-within {
border-color: var(--input-focus-border-color);
}

.tag {
background-color: var(--admonition-note-background-color);
color: var(--admonition-note-color);
padding: 2px 12px 4px 16px;
border-radius: 30px;
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 14px;
margin-right: 8px;
}

.tag:hover {
background-color: #0056b3;
}

.tag::after {
content: '\00D7'; /* multiplication sign */
margin-left: 8px;
font-size: 12px;
cursor: pointer;
}

.filter-form input.tag-input {
flex: 1;
border: none;
padding: 5px;
outline: none;
background-color: transparent;
}




#catalog {
display: flex;
flex-wrap: wrap;
Expand Down
95 changes: 93 additions & 2 deletions doc/_extensions/zephyr/domain/static/js/board-catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function toggleDisplayMode(btn) {
}

function populateFormFromURL() {
// TODO restore supported_features
const params = ["name", "arch", "vendor", "soc"];
const hashParams = new URLSearchParams(window.location.hash.slice(1));
params.forEach((param) => {
Expand All @@ -33,6 +34,7 @@ function populateFormFromURL() {
}

function updateURL() {
// TODO add backup of supported_features
const params = ["name", "arch", "vendor", "soc"];
const hashParams = new URLSearchParams(window.location.hash.slice(1));

Expand Down Expand Up @@ -84,6 +86,88 @@ function fillSocSocSelect(families, series = undefined, selectOnFill = false) {
});
}

function setupHWCapabilitiesField() {
let selectedTags = [];

const tagContainer = document.getElementById('tag-container');
const tagInput = document.getElementById('tag-input');
const datalist = document.getElementById('tag-list');

const tagCounts = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => {
board.getAttribute('data-supported-features').split(' ').forEach(tag => {
acc[tag] = (acc[tag] || 0) + 1;
});
return acc;
}, {});

const allTags = Object.entries(tagCounts)
.sort((a, b) => b[1] - a[1])
.map(entry => entry[0]);

// Add tag
function addTag(tag) {
if (selectedTags.includes(tag) || tag === "" || !allTags.includes(tag)) return;
selectedTags.push(tag);

const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = tag;
tagElement.onclick = () => removeTag(tag);
tagContainer.insertBefore(tagElement, tagInput);

tagInput.value = ''; // Clear input
updateDatalist(); // Update the available suggestions
}

// Remove tag
function removeTag(tag) {
selectedTags = selectedTags.filter(t => t !== tag);
document.querySelectorAll('.tag').forEach(el => {
if (el.textContent.includes(tag)) el.remove();
});
updateDatalist(); // Update the suggestions when a tag is removed
}

// Update the datalist options based on selected tags
function updateDatalist() {
datalist.innerHTML = ''; // Clear existing options
const filteredTags = allTags.filter(tag => !selectedTags.includes(tag)); // Filter out selected tags

filteredTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
datalist.appendChild(option);
});

filterBoards();
}

// Handle selection from datalist or Enter key to add tag
tagInput.addEventListener('input', () => {
if (allTags.includes(tagInput.value)) {
addTag(tagInput.value);
}
});

// Add tag when pressing the Enter key
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && allTags.includes(tagInput.value)) {
addTag(tagInput.value);
e.preventDefault();
}
});

// Delete tag when pressing the Backspace key
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && tagInput.value === '' && selectedTags.length > 0) {
removeTag(selectedTags[selectedTags.length - 1]);
}
});

// Initialize the datalist
updateDatalist();
}

document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector(".filter-form");

Expand All @@ -101,9 +185,10 @@ document.addEventListener("DOMContentLoaded", function () {
fillSocFamilySelect();
fillSocSeriesSelect();
fillSocSocSelect();

populateFormFromURL();

setupHWCapabilitiesField();

socFamilySelect = document.getElementById("family");
socFamilySelect.addEventListener("change", () => {
const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value);
Expand Down Expand Up @@ -142,6 +227,8 @@ function resetForm() {
fillSocFamilySelect();
fillSocSeriesSelect();
fillSocSocSelect();
// todo clear supported_features

filterBoards();
}

Expand All @@ -160,6 +247,8 @@ function filterBoards() {
const vendorSelect = document.getElementById("vendor").value;
const socSocSelect = document.getElementById("soc");

const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);

const resetFiltersBtn = document.getElementById("reset-filters");
if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length) {
resetFiltersBtn.classList.remove("btn-disabled");
Expand All @@ -174,6 +263,7 @@ function filterBoards() {
const boardArchs = board.getAttribute("data-arch").split(" ");
const boardVendor = board.getAttribute("data-vendor");
const boardSocs = board.getAttribute("data-socs").split(" ");
const boardSupportedFeatures = board.getAttribute("data-supported-features").split(" ");

let matches = true;

Expand All @@ -183,7 +273,8 @@ function filterBoards() {
!(nameInput && !boardName.includes(nameInput)) &&
!(archSelect && !boardArchs.includes(archSelect)) &&
!(vendorSelect && boardVendor !== vendorSelect) &&
(selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc)));
(selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))) &&
(selectedTags.length === 0 || selectedTags.every((tag) => boardSupportedFeatures.includes(tag)));

board.classList.toggle("hidden", !matches);
});
Expand Down
2 changes: 1 addition & 1 deletion doc/_extensions/zephyr/domain/templates/board-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
data-arch="{{ board.archs | join(" ") }}"
data-vendor="{{ board.vendor }}"
data-socs="{{ board.socs | join(" ") }}"
tabindex="0">
data-supported-features="{{ board.supported_features | join(" ") }}" tabindex="0">
<div class="vendor">{{ vendors[board.vendor] }}</div>
{% if board.image -%}
<img alt="A picture of the {{ board.full_name }} board"
Expand Down
8 changes: 8 additions & 0 deletions doc/_extensions/zephyr/domain/templates/board-catalog.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
<select id="soc" name="soc" size="10" multiple></select>
</div>

<div class="form-group" style="flex-basis: 100%">
<label for="hw-capabilities">Supported Hardware Capabilities</label>
<div class="tag-container" id="tag-container">
<input list="tag-list" class="tag-input" id="tag-input" placeholder="Type a tag...">
<datalist id="tag-list"></datalist>
</div>
</div>

</form>

<div id="form-options" style="text-align: center; margin-bottom: 20px">
Expand Down
67 changes: 67 additions & 0 deletions doc/_scripts/gen_boards_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import list_hardware
import yaml
import zephyr_module
from devicetree import edtlib
from gen_devicetree_rest import VndLookup

ZEPHYR_BASE = Path(__file__).parents[2]
Expand Down Expand Up @@ -38,6 +39,7 @@ def guess_image(board_or_shield):

return (img_file.relative_to(ZEPHYR_BASE)).as_posix() if img_file else None


def guess_doc_page(board_or_shield):
patterns = [
"doc/index.{ext}",
Expand All @@ -51,6 +53,31 @@ def guess_doc_page(board_or_shield):
return doc_file


def get_first_sentence(text):
# Split the text into lines
lines = text.splitlines()

# Trim leading and trailing whitespace from each line and ignore completely blank lines
lines = [line.strip() for line in lines]

if not lines:
return ""

# Case 1: Single line followed by blank line(s) or end of text
if len(lines) == 1 or (len(lines) > 1 and lines[1] == ""):
first_line = lines[0]
# Check for the first period
period_index = first_line.find(".")
# If there's a period, return up to the period; otherwise, return the full line
return first_line[: period_index + 1] if period_index != -1 else first_line

# Case 2: Multiple contiguous lines, treat as a block
block = " ".join(lines)
period_index = block.find(".")
# If there's a period, return up to the period; otherwise, return the full block
return block[: period_index + 1] if period_index != -1 else block


def get_catalog():
vnd_lookup = VndLookup(ZEPHYR_BASE / "dts/bindings/vendor-prefixes.txt", [])

Expand Down Expand Up @@ -78,6 +105,7 @@ def get_catalog():
boards = list_boards.find_v2_boards(args_find_boards)
systems = list_hardware.find_v2_systems(args_find_boards)
board_catalog = {}
compat_description_cache = {}

for board in boards.values():
# We could use board.vendor but it is often incorrect. Instead, deduce vendor from
Expand Down Expand Up @@ -107,13 +135,41 @@ def get_catalog():
full_name = board.full_name or board.name
doc_page = guess_doc_page(board)

TWISTER_OUT = ZEPHYR_BASE / "twister-out"
supported_features = {}
if TWISTER_OUT.exists():
dts_files = list(TWISTER_OUT.glob(f"{board.name}/**/zephyr.dts")) + \
list(TWISTER_OUT.glob(f"{board.name}_*/**/zephyr.dts"))

if dts_files:
for dts_file in dts_files:
edt = edtlib.EDT(dts_file, bindings_dirs=[ZEPHYR_BASE / "dts/bindings"])
okay_nodes = [
node
for node in edt.nodes
if node.status == "okay" and node.matching_compat is not None
]

for node in okay_nodes:
binding_path = Path(node.binding_path)
binding_type = binding_path.relative_to(ZEPHYR_BASE / "dts/bindings").parts[
0
]
description = compat_description_cache.setdefault(
node.matching_compat, get_first_sentence(node.description)
)
supported_features.setdefault(binding_type, {}).setdefault(
node.matching_compat, description
)

board_catalog[board.name] = {
"name": board.name,
"full_name": full_name,
"doc_page": doc_page.relative_to(ZEPHYR_BASE).as_posix() if doc_page else None,
"vendor": vendor,
"archs": list(archs),
"socs": list(socs),
"supported_features": supported_features,
"image": guess_image(board),
}

Expand All @@ -123,6 +179,17 @@ def get_catalog():
series = soc.series or "<no series>"
socs_hierarchy.setdefault(family, {}).setdefault(series, []).append(soc.name)

# if there is a board_catalog.yaml file, load it (yes, it's already late but it's a hack for
# showing off the hw capability selection feature so shh)
BOARD_CATALOG_FILE = ZEPHYR_BASE / "doc" / "board_catalog.yaml"
if Path(BOARD_CATALOG_FILE).exists():
with open(BOARD_CATALOG_FILE) as f:
board_catalog = yaml.safe_load(f)
else:
# save the board catalog as a pickle file
with open(BOARD_CATALOG_FILE, "w") as f:
yaml.dump(board_catalog, f)

return {
"boards": board_catalog,
"vendors": {**vnd_lookup.vnd2vendor, "others": "Other/Unknown"},
Expand Down
Loading

0 comments on commit f8790a4

Please sign in to comment.