Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/_extensions/zephyr/domain/static/css/board-catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
margin-right: 8px;
}

#compatibles-tags .tag {
font-family: var(--monospace-font-family);
}

.tag:hover {
background-color: #0056b3;
}
Expand Down
150 changes: 137 additions & 13 deletions doc/_extensions/zephyr/domain/static/js/board-catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,34 @@ function populateFormFromURL() {
const features = hashParams.get("features").split(",");
setTimeout(() => {
features.forEach(feature => {
const tagContainer = document.getElementById('tag-container');
const tagInput = document.getElementById('tag-input');
const tagContainer = document.getElementById('hwcaps-tags');
const tagInput = document.getElementById('hwcaps-input');

const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = feature;
tagElement.onclick = () => {
const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
selectedTags.splice(selectedTags.indexOf(feature), 1);
tagElement.remove();
filterBoards();
};
tagContainer.insertBefore(tagElement, tagInput);
});
filterBoards();
}, 0);
}

// Restore compatibles from URL
if (hashParams.has("compatibles")) {
const compatibles = hashParams.get("compatibles").split("|");
setTimeout(() => {
compatibles.forEach(compatible => {
const tagContainer = document.getElementById('compatibles-tags');
const tagInput = document.getElementById('compatibles-input');

const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = compatible;
tagElement.onclick = () => {
tagElement.remove();
filterBoards();
};
Expand Down Expand Up @@ -83,8 +102,12 @@ function updateURL() {
});

// Add supported features to URL
const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
selectedTags.length ? hashParams.set("features", selectedTags.join(",")) : hashParams.delete("features");
const selectedHWTags = [...document.querySelectorAll('#hwcaps-tags .tag')].map(tag => tag.textContent);
selectedHWTags.length ? hashParams.set("features", selectedHWTags.join(",")) : hashParams.delete("features");

// Add compatibles to URL
const selectedCompatibles = [...document.querySelectorAll('#compatibles-tags .tag')].map(tag => tag.textContent);
selectedCompatibles.length ? hashParams.set("compatibles", selectedCompatibles.join("|")) : hashParams.delete("compatibles");

window.history.replaceState({}, "", `#${hashParams.toString()}`);
}
Expand Down Expand Up @@ -126,8 +149,8 @@ function fillSocSocSelect(families, series = undefined, selectOnFill = false) {
function setupHWCapabilitiesField() {
let selectedTags = [];

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

const tagCounts = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => {
Expand Down Expand Up @@ -198,6 +221,80 @@ function setupHWCapabilitiesField() {
updateDatalist();
}

function setupCompatiblesField() {
let selectedCompatibles = [];

const tagContainer = document.getElementById('compatibles-tags');
const tagInput = document.getElementById('compatibles-input');
const datalist = document.getElementById('compatibles-list');

// Collect all unique compatibles from boards
const allCompatibles = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => {
(board.getAttribute('data-compatibles') || '').split(' ').forEach(compat => {
if (compat && !acc.includes(compat)) {
acc.push(compat);
}
});
return acc;
}, []);

allCompatibles.sort();

function addCompatible(compatible) {
if (selectedCompatibles.includes(compatible) || compatible === "") return;
selectedCompatibles.push(compatible);

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

tagInput.value = '';
updateDatalist();
}

function removeCompatible(compatible) {
selectedCompatibles = selectedCompatibles.filter(c => c !== compatible);
document.querySelectorAll('.tag').forEach(el => {
if (el.textContent === compatible && el.parentElement === tagContainer) {
el.remove();
}
});
updateDatalist();
}

function updateDatalist() {
datalist.innerHTML = '';
const filteredCompatibles = allCompatibles.filter(c => !selectedCompatibles.includes(c));

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

filterBoards();
}

tagInput.addEventListener('input', () => {
if (allCompatibles.includes(tagInput.value)) {
addCompatible(tagInput.value);
}
});

tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && tagInput.value) {
addCompatible(tagInput.value);
e.preventDefault();
} else if (e.key === 'Backspace' && tagInput.value === '' && selectedCompatibles.length > 0) {
removeCompatible(selectedCompatibles[selectedCompatibles.length - 1]);
}
});

updateDatalist();
}

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

Expand All @@ -218,6 +315,7 @@ document.addEventListener("DOMContentLoaded", function () {
populateFormFromURL();

setupHWCapabilitiesField();
setupCompatiblesField();

socFamilySelect = document.getElementById("family");
socFamilySelect.addEventListener("change", () => {
Expand Down Expand Up @@ -272,8 +370,12 @@ function resetForm() {
document.getElementById("show-shields").checked = true;

// Clear supported features
document.querySelectorAll('.tag').forEach(tag => tag.remove());
document.getElementById('tag-input').value = '';
document.querySelectorAll('#hwcaps-tags .tag').forEach(tag => tag.remove());
document.getElementById('hwcaps-input').value = '';

// Clear compatibles
document.querySelectorAll('#compatibles-tags .tag').forEach(tag => tag.remove());
document.getElementById('compatibles-input').value = '';

filterBoards();
}
Expand All @@ -289,6 +391,16 @@ function updateBoardCount() {
+ ` ${visibleShields.length} of ${shields.length} shields`;
}

function wildcardMatch(pattern, str) {
// Convert wildcard pattern to regex
// Escape special regex characters except *
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(str);
}

function filterBoards() {
const nameInput = document.getElementById("name").value.toLowerCase();
const archSelect = document.getElementById("arch").value;
Expand All @@ -297,10 +409,14 @@ function filterBoards() {
const showBoards = document.getElementById("show-boards").checked;
const showShields = document.getElementById("show-shields").checked;

const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
// Get selected hardware capability tags
const selectedHWTags = [...document.querySelectorAll('#hwcaps-tags .tag')].map(tag => tag.textContent);

// Get selected compatible tags
const selectedCompatibles = [...document.querySelectorAll('#compatibles-tags .tag')].map(tag => tag.textContent);

const resetFiltersBtn = document.getElementById("reset-filters");
if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedTags.length || !showBoards || !showShields) {
if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedHWTags.length || selectedCompatibles.length || !showBoards || !showShields) {
resetFiltersBtn.classList.remove("btn-disabled");
} else {
resetFiltersBtn.classList.add("btn-disabled");
Expand All @@ -314,6 +430,7 @@ function filterBoards() {
const boardVendor = board.getAttribute("data-vendor") || "";
const boardSocs = (board.getAttribute("data-socs") || "").split(" ").filter(Boolean);
const boardSupportedFeatures = (board.getAttribute("data-supported-features") || "").split(" ").filter(Boolean);
const boardCompatibles = (board.getAttribute("data-compatibles") || "").split(" ").filter(Boolean);
const isShield = board.classList.contains("shield");

let matches = true;
Expand All @@ -323,12 +440,19 @@ function filterBoards() {
if ((isShield && !showShields) || (!isShield && !showBoards)) {
matches = false;
} else {
// Check if board matches all selected compatibles (with wildcard support)
const compatiblesMatch = selectedCompatibles.length === 0 ||
selectedCompatibles.every((pattern) =>
boardCompatibles.some((compatible) => wildcardMatch(pattern, compatible))
);

matches =
!(nameInput && !boardName.includes(nameInput)) &&
!(archSelect && !boardArchs.includes(archSelect)) &&
!(vendorSelect && boardVendor !== vendorSelect) &&
(selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))) &&
(selectedTags.length === 0 || selectedTags.every((tag) => boardSupportedFeatures.includes(tag)));
(selectedHWTags.length === 0 || selectedHWTags.every((tag) => boardSupportedFeatures.includes(tag))) &&
compatiblesMatch;
}

board.classList.toggle("hidden", !matches);
Expand Down
14 changes: 13 additions & 1 deletion doc/_extensions/zephyr/domain/templates/board-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@
{%- endfor -%}
{%- endfor -%}
{{- feature_types|join(' ') -}}
" tabindex="0">
"
data-compatibles="
{%- set all_compatibles = [] -%}
{%- for target_compatibles in board.compatibles.values() -%}
{%- for compatible in target_compatibles -%}
{%- if compatible not in all_compatibles -%}
{%- set _ = all_compatibles.append(compatible) -%}
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
{{- all_compatibles|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
22 changes: 18 additions & 4 deletions doc/_extensions/zephyr/domain/templates/board-catalog.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@
</div>

<div class="form-group" style="flex-basis: 100%">
<label for="tag-input">Supported Hardware Capabilities</label>
<div class="tag-container" id="tag-container">
<input list="tag-list" class="tag-input" id="tag-input"
<label for="hwcaps-input">Supported Hardware Capabilities</label>
<div class="tag-container" id="hwcaps-tags">
<input list="tag-list" class="tag-input" id="hwcaps-input"
placeholder="{% if hw_features_present -%}
Type a tag...
Type a hardware capability, e.g. &quot;display&quot;, &quot;sensor&quot;, &hellip;
{%- else -%}
List of supported hardware capabilities is not available
{%- endif %}"
Expand All @@ -89,6 +89,20 @@
</div>
</div>

<div class="form-group" style="flex-basis: 100%">
<label for="compatibles-input">Supported devices (compatible strings, supports wildcards)</label>
<div class="tag-container" id="compatibles-tags">
<input list="compatibles-list" class="tag-input" id="compatibles-input"
placeholder="{% if hw_features_present -%}
Type a compatible, e.g. &quot;ti,hdc2080&quot; or wildcard like &quot;bosch,bmp*&quot;
{%- else -%}
List of supported devices is not available
{%- endif %}"
{% if not hw_features_present %}disabled{% endif %}>
<datalist id="compatibles-list"></datalist>
</div>
</div>

</form>

<div id="form-options" style="text-align: center; margin-bottom: 20px">
Expand Down
7 changes: 6 additions & 1 deletion doc/_scripts/gen_boards_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,13 @@ def get_catalog(generate_hw_features=False, hw_features_vendor_filter=None):
doc_page = guess_doc_page(board)

supported_features = {}
compatibles = {}

# Use pre-gathered build info and DTS files
if board.name in board_devicetrees:
for board_target, edt in board_devicetrees[board.name].items():
features = {}
target_compatibles = set()
for node in edt.nodes:
if node.binding_path is None:
continue
Expand Down Expand Up @@ -328,6 +330,7 @@ def get_catalog(generate_hw_features=False, hw_features_vendor_filter=None):
locations.add("soc")

existing_feature = features.get(binding_type, {}).get(node.matching_compat)
target_compatibles.add(node.matching_compat)

node_info = {
"filename": str(filename),
Expand All @@ -354,8 +357,9 @@ def get_catalog(generate_hw_features=False, hw_features_vendor_filter=None):

features.setdefault(binding_type, {})[node.matching_compat] = feature_data

# Store features for this specific target
# Store features and compatibles for this specific target
supported_features[board_target] = features
compatibles[board_target] = list(target_compatibles)

board_runner_info = {}
if board.name in board_runners:
Expand Down Expand Up @@ -392,6 +396,7 @@ def get_catalog(generate_hw_features=False, hw_features_vendor_filter=None):
"socs": list(socs),
"revision_default": board.revision_default,
"supported_features": supported_features,
"compatibles": compatibles,
"image": guess_image(board),
# runners
"supported_runners": board_runner_info.get("runners", []),
Expand Down
2 changes: 2 additions & 0 deletions doc/_scripts/gen_devicetree_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,8 @@ def make_sidebar(compatible, vendor_name, vendor_ref_target, driver_path=None):
"",
f" :Name: ``{compatible}``",
f" :Vendor: :ref:`{vendor_name} <{vendor_ref_target}>`",
f" :Used in: :zephyr:board-catalog:`List of boards <#compatibles={compatible}>` using",
" this compatible",
]
if driver_path:
lines.append(f" :Driver: :zephyr_file:`{driver_path}`")
Expand Down