diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
deleted file mode 100644
index 37a915d..0000000
--- a/.github/workflows/build-and-deploy.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: Build & Deploy
-on: [push]
-
-jobs:
- site:
- runs-on: ubuntu-latest
- permissions: { contents: write }
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with: { python-version: '3.12' }
- - run: pip install mkdocs-material
- - run: mkdocs build --strict # makes ./site/
- - uses: peaceiris/actions-gh-pages@v4
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ./site
- cname: docs.hardfoc.dev # writes CNAME file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..0c718de
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,36 @@
+---
+# The portal, https://hardfoc.github.io/: an unversioned site on the shared HardFOC theme. Pull requests build,
+# check (strict build, offline link check, Lighthouse, screenshots) and never publish; a push to main publishes
+# the built site as the whole gh-pages branch. The repository catalogue is generated from the GitHub API at build
+# time (tools/portal/catalogue.py), so every publish shows the current repositories and releases.
+name: ๐ Portal
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '17 5 * * *' # daily: new repositories, releases and Pages sites appear without a commit
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: portal-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ site:
+ # The branch of hardfoc/hf-general-ci-tools#42 (the versioning input) until it is released; then @v1.
+ uses: hardfoc/hf-general-ci-tools/.github/workflows/ru-mkdocs-publish.yml@v1
+ permissions:
+ contents: write # the publish job pushes gh-pages; pull requests never publish
+ with:
+ versioning: false
+ check_commands: |
+ python3 -m unittest discover -s tests -v
+ python3 tools/portal/catalogue.py
+ hardfoc-docs check
+ deploy: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f34327
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+build/
+site/
+__pycache__/
diff --git a/README.md b/README.md
index edd6060..cade5da 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,60 @@
# hardfoc.github.io
-This repository hosts the documentation site for the hardfoc project. It uses
-[MkDocs](https://www.mkdocs.org/) to build the static site and GitHub Pages to
-publish it.
+The HardFOC portal: what HardFOC is, how its repositories fit together, every public repository with its
+documentation, and where to start with each board.
-The site uses the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
-theme, which provides a sidebar and top navigation tabs configured in
-`mkdocs.yml`.
+[](https://hardfoc.github.io/)
+[](LICENSE)
-## Folder structure
+**Site: **
-```
-.
-โโโ docs/ # Markdown sources
-โ โโโ getting-started/
-โ โโโ api/
-โ โโโ porting/
-โ โโโ hardware/
-โ โโโ index.md # Home page
-โ โโโ about.md # About page
-โโโ mkdocs.yml # MkDocs configuration
-โโโ .github/workflows/ # CI workflow
+
+
+
+
+
+
+
+## What it does
+
+- Introduces HardFOC and its two boards, Vortex V1 and Flux V1.
+- Shows the four layers (chip drivers, core, board HALs, products) and how they are pinned together.
+- Lists every public repository as a card with its description, topics, documentation and latest release,
+ generated from the GitHub API and [`org/org-metadata.json`](https://github.com/hardfoc/.github/blob/main/org/org-metadata.json)
+ at every publish, and filterable by text or topic.
+- Gives a getting-started path per board, and for a chip driver on any MCU.
+- Links the contribution rules and the private vulnerability report.
+
+## Build locally
+
+```bash
+pip install "git+https://github.com/hardfoc/hf-general-ci-tools@v1#subdirectory=packages/hardfoc-mkdocs-theme"
+GH_TOKEN=$(gh auth token) python3 tools/portal/catalogue.py # writes build/portal/catalogue.md
+mkdocs serve # http://127.0.0.1:8000
```
-Pushes to `main` trigger the workflow in `.github/workflows/build-and-deploy.yml`
-which builds the site and updates the `gh-pages` branch.
+The site is unversioned: `.github/workflows/docs.yml` calls `ru-mkdocs-publish.yml` from
+[hf-general-ci-tools](https://hardfoc.github.io/hf-general-ci-tools/latest/) with `versioning: false`, and a push
+to `main` (or the daily run) publishes the built site as the whole `gh-pages` branch.
+
+## Documentation
+
+| Page | What it covers |
+| --- | --- |
+| [Get started](https://hardfoc.github.io/get-started/) | A path per board, and for a chip driver on your MCU |
+| [Repositories](https://hardfoc.github.io/repositories/) | Every public repository, searchable |
+| [Architecture](https://hardfoc.github.io/architecture/) | The layers and how they are pinned |
+| [Contribute](https://hardfoc.github.io/contribute/) | Issues, pull requests, vulnerability reports |
+
+## Tests
+
+```bash
+python3 -m unittest discover -s tests && mkdocs build --strict
+```
-### How the workflow works
+## License
-1. **Checkout** โ pulls the repo so the runner has the docs and config.
-2. **Setup Python** โ installs a recent Python release for MkDocs.
-3. **Install MkDocs** โ installs `mkdocs-material` to render the pages.
-4. **Build** โ `mkdocs build --strict` generates the static site in `./site`.
-5. **Publish** โ `peaceiris/actions-gh-pages` pushes that folder to the
- `gh-pages` branch and writes the `CNAME` file so GitHub Pages serves
- `https://docs.hardfoc.dev`.
+See [LICENSE](LICENSE).
diff --git a/docs/.nav.yml b/docs/.nav.yml
new file mode 100644
index 0000000..010eac0
--- /dev/null
+++ b/docs/.nav.yml
@@ -0,0 +1,13 @@
+# Site navigation (mkdocs-awesome-nav): the tabs and their pages, in reading order.
+nav:
+ - Home: index.md
+ - Get started:
+ - get-started/index.md
+ - Vortex V1 motor controller: get-started/vortex.md
+ - Flux V1 valve controller: get-started/flux.md
+ - A chip driver on your MCU: get-started/drivers.md
+ - Repositories: repositories.md
+ - Architecture:
+ - Layers: architecture.md
+ - Figures: diagrams/index.md
+ - Contribute: contribute.md
diff --git a/docs/about.md b/docs/about.md
deleted file mode 100644
index 18faee8..0000000
--- a/docs/about.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# About This Documentation
-
-The `docs/` directory holds all Markdown files that become the published site.
-Each sub-folder provides a page on the site, for example:
-
-```
-docs/
- getting-started/
- api/
- porting/
- hardware/
-```
-
-`mkdocs.yml` at the repository root describes the navigation and theme.
diff --git a/docs/api/README.md b/docs/api/README.md
deleted file mode 100644
index 0d65839..0000000
--- a/docs/api/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# API Documentation
-
-Reference material for the project's API.
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..f73d10e
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,54 @@
+# Architecture
+
+How HardFOC firmware is split into repositories, and how the layers are pinned together.
+
+
+
+
+
+
+
+
+## Layers
+
+| Layer | Repositories | Depends on | Platform code |
+| --- | --- | --- | --- |
+| Products | `hardfoc-vortex-v1`, `hardfoc-flux-v1` | One board HAL | ESP-IDF application, tasks, CANopen (Vortex) |
+| Board HALs | `hf-hal-vortex-v1`, `hf-hal-flux-v1` | `hf-core` | One object per board, one manager per board function |
+| Core | `hf-core`, gathering `hf-core-drivers` and `hf-core-utils` | Drivers, interface wrap, utilities | Device handlers, RTOS wrappers, CANopen helpers |
+| Interface wrap | `hf-internal-interface-wrap`, `hf-pincfg` | ESP-IDF | Peripheral base classes (GPIO, ADC, I2C, SPI, UART, CAN, PWM) and their ESP-IDF implementations; per-board pin maps |
+| Utilities | `hf-utils-general`, `hf-utils-rtos-wrap`, `hf-utils-canopen` | None, or FreeRTOS | Header-only helpers, FreeRTOS wrappers, CANopen frame builders |
+| Chip drivers | `hf-*-driver` | Nothing | None: each driver is a template over a small communication interface |
+
+The [repository catalogue](repositories.md) lists every repository in each layer, with its documentation.
+
+## Submodules, not packages
+
+Each arrow in the figure is a Git submodule pinned to a commit or tag:
+
+```mermaid
+flowchart LR
+ drv["Chip drivers hf-*-driver"] --> cd["hf-core-drivers"]
+ iw["hf-internal-interface-wrap hf-pincfg"] --> cd
+ ut["hf-utils-general hf-utils-rtos-wrap hf-utils-canopen"] --> cu["hf-core-utils"]
+ cd --> core["hf-core"]
+ cu --> core
+ core --> hal["Board HALs"]
+ hal --> prod["Products"]
+```
+
+A product therefore builds exactly the commits it pins, all the way down. Moving a layer forward is a pull request
+that updates one submodule pointer and passes that repository's CI.
+
+## Shared tooling
+
+| Repository | Provides |
+| --- | --- |
+| [`hf-general-ci-tools`](https://hardfoc.github.io/hf-general-ci-tools/latest/) | Reusable workflows (lint, analysis, host builds, the CI gate, releases) and the documentation platform this site is built with |
+| [`hf-espidf-ci-tools`](https://github.com/hardfoc/hf-espidf-ci-tools) | ESP-IDF builds of examples and firmware, releases with artifacts |
+| [`hf-espidf-project-tools`](https://github.com/hardfoc/hf-espidf-project-tools) | Build and flash scripts for ESP-IDF example projects |
+
+Next: [Get started](get-started/index.md).
diff --git a/docs/assets/catalogue.js b/docs/assets/catalogue.js
new file mode 100644
index 0000000..00eab94
--- /dev/null
+++ b/docs/assets/catalogue.js
@@ -0,0 +1,49 @@
+// Repository catalogue filter: the search field and the topic chips hide the cards (and empty groups) that do not
+// match. Works with Material's instant navigation (document$), and leaves every card visible without JavaScript.
+(function () {
+ function setup() {
+ var input = document.getElementById("hf-repo-filter");
+ if (!input || input.dataset.ready) return;
+ input.dataset.ready = "1";
+ var cards = Array.prototype.slice.call(document.querySelectorAll(".hf-repo"));
+ var groups = Array.prototype.slice.call(document.querySelectorAll(".hf-catalogue__group"));
+ var count = document.querySelector(".hf-catalogue__count");
+ var total = count ? count.getAttribute("data-total") : String(cards.length);
+ var topic = "";
+
+ function apply() {
+ var words = input.value.toLowerCase().split(/\s+/).filter(Boolean);
+ var shown = 0;
+ cards.forEach(function (c) {
+ var hay = c.getAttribute("data-search");
+ var topics = " " + c.getAttribute("data-topics") + " ";
+ var ok = words.every(function (w) { return hay.indexOf(w) !== -1; }) &&
+ (!topic || topics.indexOf(" " + topic + " ") !== -1);
+ c.hidden = !ok;
+ if (ok) shown++;
+ });
+ groups.forEach(function (g) { g.hidden = !g.querySelector(".hf-repo:not([hidden])"); });
+ document.querySelectorAll(".hf-chipbtn").forEach(function (b) {
+ b.setAttribute("aria-pressed", b.getAttribute("data-topic") === topic ? "true" : "false");
+ });
+ if (count) {
+ count.textContent = shown === Number(total) ? total + " public repositories." :
+ shown + " of " + total + " repositories" + (topic ? " with topic " + topic : "") + ".";
+ }
+ }
+
+ input.addEventListener("input", apply);
+ document.addEventListener("click", function (e) {
+ var b = e.target.closest && e.target.closest(".hf-chipbtn");
+ if (!b) return;
+ var t = b.getAttribute("data-topic");
+ topic = topic === t ? "" : t;
+ apply();
+ });
+ var q = new URLSearchParams(location.search);
+ if (q.get("q")) input.value = q.get("q");
+ if (q.get("topic")) topic = q.get("topic");
+ apply();
+ }
+ if (window.document$) window.document$.subscribe(setup); else document.addEventListener("DOMContentLoaded", setup);
+})();
diff --git a/docs/contribute.md b/docs/contribute.md
new file mode 100644
index 0000000..5dc9aa0
--- /dev/null
+++ b/docs/contribute.md
@@ -0,0 +1,35 @@
+# Contribute
+
+How to report a problem, propose a change and report a vulnerability, for every HardFOC repository.
+
+## Report a problem or ask a question
+
+Open an issue in the repository closest to the problem: the driver for a chip, the HAL for a board, the product
+for the application. If you cannot tell, open it in the product and say what you saw.
+
+## Propose a change
+
+| Step | Where |
+| --- | --- |
+| Read the rules for branches, commits, pull requests and the CI gate | [CONTRIBUTING.md](https://github.com/hardfoc/.github/blob/main/CONTRIBUTING.md) |
+| Follow the code of conduct | [CODE_OF_CONDUCT.md](https://github.com/hardfoc/.github/blob/main/CODE_OF_CONDUCT.md) |
+| Write documentation to the shared standard | [Documentation style guide](https://hardfoc.github.io/hf-general-ci-tools/latest/DOCS-STYLE-GUIDE/) |
+| Add CI to a repository | [CI quickstart](https://hardfoc.github.io/hf-general-ci-tools/latest/quickstart/) |
+
+A repository with its own `CONTRIBUTING.md` follows that file instead.
+
+## Report a vulnerability
+
+!!! danger "Never in public"
+ Do not open a public issue, pull request or discussion for a suspected vulnerability.
+
+Report it privately with a GitHub security advisory: in the affected repository, **Security** > **Advisories** >
+**Report a vulnerability**. The [security policy](https://github.com/hardfoc/.github/blob/main/SECURITY.md) says
+what to include and what is in scope.
+
+## This site
+
+The portal is [`hardfoc/hardfoc.github.io`](https://github.com/hardfoc/hardfoc.github.io). The repository list is
+built from the GitHub API and the organisation's repository record each time the site is published; to change a
+description or topics, change
+[`org/org-metadata.json`](https://github.com/hardfoc/.github/blob/main/org/org-metadata.json) in `hardfoc/.github`.
diff --git a/docs/diagrams/figures.json b/docs/diagrams/figures.json
new file mode 100644
index 0000000..63e66c4
--- /dev/null
+++ b/docs/diagrams/figures.json
@@ -0,0 +1,13 @@
+{
+ "hardfoc-layers": {
+ "title": "Four layers, each a set of repositories",
+ "alt": "Layer diagram, top to bottom: products (hardfoc-vortex-v1, hardfoc-flux-v1) build on board HALs (hf-hal-vortex-v1, hf-hal-flux-v1), which build on hf-core (hf-core-drivers and hf-core-utils), which gathers the hardware-agnostic chip drivers (hf-*-driver) and the peripheral interface wrap.",
+ "description": "From bottom to top, each layer builds on the one below it as a Git submodule pinned to a commit or tag. Chip drivers are hardware-agnostic C++ libraries with no platform code; each one needs only a small bus adapter. hf-core gathers them with the peripheral interface wrap, the pin configuration and the utility libraries (RTOS wrappers, general helpers, CANopen). A board HAL builds one object per board from hf-core, with one manager per board function. A product is the application firmware for one board, on top of its HAL.",
+ "profile": "overview",
+ "width": 1200,
+ "source": "docs/diagrams/figures/hardfoc-layers.py",
+ "sources": [
+ "mkdocs.yml"
+ ]
+ }
+}
diff --git a/docs/diagrams/figures/hardfoc-layers.py b/docs/diagrams/figures/hardfoc-layers.py
new file mode 100644
index 0000000..8262512
--- /dev/null
+++ b/docs/diagrams/figures/hardfoc-layers.py
@@ -0,0 +1,52 @@
+"""The four layers of HardFOC firmware: chip drivers, core, board HALs, products.
+
+Reads: From bottom to top, each layer builds on the one below it as a Git submodule pinned to a commit or tag.
+Chip drivers are hardware-agnostic C++ libraries with no platform code; each one needs only a small bus adapter.
+hf-core gathers them with the peripheral interface wrap, the pin configuration and the utility libraries
+(RTOS wrappers, general helpers, CANopen). A board HAL builds one object per board from hf-core, with one manager
+per board function. A product is the application firmware for one board, on top of its HAL.
+"""
+
+from hardfoc_mkdocs_theme.figkit import Fig
+
+SOURCES = ["mkdocs.yml"]
+
+
+def build(_data):
+ f = Fig(
+ "hardfoc-layers",
+ "Four layers, each a set of repositories",
+ alt="Layer diagram, top to bottom: products (hardfoc-vortex-v1, hardfoc-flux-v1) build on board HALs "
+ "(hf-hal-vortex-v1, hf-hal-flux-v1), which build on hf-core (hf-core-drivers and hf-core-utils), which "
+ "gathers the hardware-agnostic chip drivers (hf-*-driver) and the peripheral interface wrap.",
+ width=1200,
+ kicker="Architecture",
+ subtitle="Each layer is a Git submodule of the one above it, pinned to a commit or tag.",
+ profile="overview",
+ legend=[("teal", "application firmware"), ("brand", "board and platform"), ("slate", "portable libraries")],
+ )
+ W = f.body_w
+ gap = 24
+ half = (W - gap) / 2
+ third = (W - 2 * gap) / 3
+ vortex = f.card(0, 0, half, "hardfoc-vortex-v1", ["Vortex V1 motor controller firmware"], tone="teal", accent=True)
+ flux = f.card(half + gap, 0, half, "hardfoc-flux-v1", ["Flux V1 valve controller firmware"], tone="teal",
+ accent=True, h=vortex.h)
+ y = vortex.b + 48
+ hv = f.card(0, y, half, "hf-hal-vortex-v1", ["ESP32-C6, TMC9660, AS5047U"], tone="brand")
+ hfx = f.card(half + gap, y, half, "hf-hal-flux-v1", ["ESP32-S3, MAX22200, TLE92466ED"], tone="brand", h=hv.h)
+ y = hv.b + 48
+ core = f.card(0, y, W, "hf-core", ["hf-core-drivers + hf-core-utils: device handlers, RTOS, CANopen"],
+ tone="brand", accent=True)
+ y = core.b + 48
+ drv = f.card(0, y, third, "Chip drivers", ["hf-*-driver, no platform code"], tone="slate")
+ wrap = f.card(third + gap, y, third, "Interface wrap", ["hf-internal-interface-wrap, hf-pincfg"], tone="slate")
+ utils = f.card(2 * (third + gap), y, third, "Utilities", ["hf-utils-general, hf-utils-rtos-wrap, hf-utils-canopen"], tone="slate")
+ for a, b in ((vortex, hv), (flux, hfx)):
+ f.connect(a, b, "bottom", "top", tone="rail")
+ # Straight arrows: each one leaves and enters at the same x.
+ for hal in (hv, hfx):
+ f.connect(hal, core, "bottom", "top", bt=(hal.cx - core.x) / core.w, tone="rail")
+ for b in (drv, wrap, utils):
+ f.connect(core, b, "bottom", "top", at=(b.cx - core.x) / core.w, tone="rail")
+ return f
diff --git a/docs/diagrams/hardfoc-layers.dark.svg b/docs/diagrams/hardfoc-layers.dark.svg
new file mode 100644
index 0000000..b537995
--- /dev/null
+++ b/docs/diagrams/hardfoc-layers.dark.svg
@@ -0,0 +1,56 @@
+
diff --git a/docs/diagrams/hardfoc-layers.svg b/docs/diagrams/hardfoc-layers.svg
new file mode 100644
index 0000000..a196c0e
--- /dev/null
+++ b/docs/diagrams/hardfoc-layers.svg
@@ -0,0 +1,56 @@
+
diff --git a/docs/diagrams/index.md b/docs/diagrams/index.md
new file mode 100644
index 0000000..846798d
--- /dev/null
+++ b/docs/diagrams/index.md
@@ -0,0 +1,25 @@
+# Figures
+
+
+
+Every figure on this site is drawn by a Python module from facts extracted from the code, so it is rebuilt,
+not redrawn, when the code changes. CI runs `hardfoc-docs figures --check` and fails when a
+committed figure no longer matches the code. Each figure below lists the files it is drawn from.
+
+## Four layers, each a set of repositories
+
+
+
+
+
+
+
+
+Figure module: [`docs/diagrams/figures/hardfoc-layers.py`](../../docs/diagrams/figures/hardfoc-layers.py)
+
+Drawn from:
+
+- [`mkdocs.yml`](../../mkdocs.yml)
diff --git a/docs/get-started/drivers.md b/docs/get-started/drivers.md
new file mode 100644
index 0000000..08d5c70
--- /dev/null
+++ b/docs/get-started/drivers.md
@@ -0,0 +1,29 @@
+# A chip driver on your MCU
+
+Use one HardFOC chip driver in your own firmware, on any MCU, without the rest of HardFOC.
+
+Every `hf-*-driver` repository is a C++ library with no platform code. The driver is a class template over a small
+communication interface: you write one class that moves bytes over the bus (SPI, I2C or UART) and toggles the
+chip's control pins, and the driver does the rest. Each one ships ESP32 examples that build in CI.
+
+## Path
+
+
+
+1. **Find the chip** in the [catalogue](../repositories.md?topic=driver) (filter by bus: `spi`, `i2c`, `uart`).
+2. **Read its quickstart**: wiring, the port class and a short example, compiled in CI. For example, the
+ [TMC9660 quickstart](https://hardfoc.github.io/hf-tmc9660-driver/latest/quickstart/).
+3. **Add it as a submodule** and link its CMake target, or add it as an ESP-IDF component.
+4. **Write the port** for your MCU, following the driver's porting page, and run the example.
+
+
+
+## What every driver has
+
+| Page | What it covers |
+| --- | --- |
+| Quickstart | Wiring, the port, a short example, "if it does not work" |
+| Hardware | Pins, bus settings and timing, power, errata |
+| Porting | The interface methods and their contracts |
+| API guide and reference | Task-oriented guide, and Doxygen under `api/` |
+| Examples | The ESP32 examples and what each one shows |
diff --git a/docs/get-started/flux.md b/docs/get-started/flux.md
new file mode 100644
index 0000000..befda20
--- /dev/null
+++ b/docs/get-started/flux.md
@@ -0,0 +1,48 @@
+# Flux V1 valve controller
+
+From a new Flux V1 board to its examples running, and where to read next.
+
+
+
+1. **Bring the board up with the HAL.** The [HAL quickstart](https://hardfoc.github.io/hf-hal-flux-v1/latest/quickstart/)
+ installs ESP-IDF, builds an example and reads its log.
+2. **Check the hardware.** The [pin map](https://hardfoc.github.io/hf-hal-flux-v1/latest/hardware/pin-map/) and the
+ [bring-up guide](https://hardfoc.github.io/hf-hal-flux-v1/latest/bring-up/).
+3. **Drive the valves.** The [ValveManager](https://hardfoc.github.io/hf-hal-flux-v1/latest/managers/valves/) page
+ and the examples.
+
+
+
+The product firmware, `hardfoc-flux-v1`, is a private repository. Its documentation is built in CI as an artifact
+for people with access, and is not published here.
+
+## Documentation
+
+| Site | What it covers |
+| --- | --- |
+| [hf-hal-flux-v1](https://hardfoc.github.io/hf-hal-flux-v1/latest/) | The board: hardware, pin map, managers (valves, ADC, GPIO, LEDs, temperature, NVS, comms), examples |
+| [hf-tle92466ed-driver](https://hardfoc.github.io/hf-tle92466ed-driver/latest/) | The six-channel solenoid driver |
+| [hf-max22200-driver](https://hardfoc.github.io/hf-max22200-driver/latest/) | The eight-channel solenoid driver |
+| [hf-core](https://hardfoc.github.io/hf-core/latest/) | The shared core the HAL is built on |
diff --git a/docs/get-started/index.md b/docs/get-started/index.md
new file mode 100644
index 0000000..105e97d
--- /dev/null
+++ b/docs/get-started/index.md
@@ -0,0 +1,21 @@
+# Get started
+
+Pick the path that matches what you have on the bench: a HardFOC board, or a chip and your own MCU.
+
+| You have | Start with | Then |
+| --- | --- | --- |
+| A Vortex V1 motor controller | [Vortex V1](vortex.md): flash a HAL example, then the product firmware | CANopen, configuration, the HAL managers |
+| A Flux V1 valve controller | [Flux V1](flux.md): flash a HAL example | The valve and ADC managers |
+| A chip from the driver list and any MCU | [A chip driver on your MCU](drivers.md) | The driver's own quickstart and porting guide |
+
+Every path needs the same tools:
+
+| Tool | Version | Notes |
+| --- | --- | --- |
+| ESP-IDF | 5.5 | The boards' firmware and the ESP32 examples; each repository says which release it pins |
+| Git | with submodules | Every layer is a submodule of the one above it |
+| A C++ compiler | C++17 or C++20, as the repository says | For a driver on another MCU, your own toolchain |
+| Python 3 | 3.10 or newer | Only to build documentation locally |
+
+!!! warning "Power"
+ Bring a new board up on a current-limited bench supply, with the motor or valves unloaded.
diff --git a/docs/get-started/vortex.md b/docs/get-started/vortex.md
new file mode 100644
index 0000000..3b7aacd
--- /dev/null
+++ b/docs/get-started/vortex.md
@@ -0,0 +1,53 @@
+# Vortex V1 motor controller
+
+From a new Vortex V1 board to the product firmware running, and where to read next.
+
+
+
+Motor controller board
+Vortex V1
+HardFOC
+
+
+MCU
+: ESP32-C6
+
+Motor control
+: TMC9660 smart gate driver, FOC on chip
+
+Feedback
+: AS5047U magnetic encoder
+
+Field bus
+: CAN, CANopen node in the product firmware
+
+Firmware
+: ESP-IDF 5.5, FreeRTOS, C++
+
+
+## Path
+
+
+
+1. **Bring the board up with the HAL.** The HAL's quickstart installs ESP-IDF, builds an example and reads its
+ log, one step at a time: [HAL quickstart](https://hardfoc.github.io/hf-hal-vortex-v1/latest/quickstart/).
+2. **Check the hardware.** The [pin map](https://hardfoc.github.io/hf-hal-vortex-v1/latest/hardware/pin-map/) and
+ the [bring-up guide](https://hardfoc.github.io/hf-hal-vortex-v1/latest/bring-up/) list every signal and what to
+ check first.
+3. **Build and flash the product firmware.** The
+ [getting-started page](https://hardfoc.github.io/hardfoc-vortex-v1/latest/getting-started/) of
+ `hardfoc-vortex-v1` builds with the pinned ESP-IDF and explains the boot log.
+4. **Talk to it over CAN.** [CANopen](https://hardfoc.github.io/hardfoc-vortex-v1/latest/canopen/) and the
+ [object dictionary](https://hardfoc.github.io/hardfoc-vortex-v1/latest/od-reference/).
+
+
+
+## Documentation
+
+| Site | What it covers |
+| --- | --- |
+| [hardfoc-vortex-v1](https://hardfoc.github.io/hardfoc-vortex-v1/latest/) | The product: architecture, threads, safety, boot and updates, CANopen, configuration, design notes |
+| [hf-hal-vortex-v1](https://hardfoc.github.io/hf-hal-vortex-v1/latest/) | The board: hardware, pin map, managers (motor, encoder, IMU, ADC, LEDs, temperature, NVS, comms), examples |
+| [hf-tmc9660-driver](https://hardfoc.github.io/hf-tmc9660-driver/latest/) | The motor controller chip: bootloader, FOC, telemetry |
+| [hf-as5047u-driver](https://hardfoc.github.io/hf-as5047u-driver/latest/) | The encoder |
+| [hf-core](https://hardfoc.github.io/hf-core/latest/) | The shared core the HAL is built on |
diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md
deleted file mode 100644
index 7a5bf3e..0000000
--- a/docs/getting-started/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Getting Started
-
-Basic introduction and setup instructions will go here.
diff --git a/docs/hardware/README.md b/docs/hardware/README.md
deleted file mode 100644
index 09ba7f1..0000000
--- a/docs/hardware/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Hardware
-
-Notes and schematics for hardware-related topics.
diff --git a/docs/index.md b/docs/index.md
index 1dcb9ba..4bb435f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,3 +1,75 @@
-# Welcome
+---
+hide:
+ - navigation
+ - toc
+---
-This is the starting page for the hardfoc documentation.
+
+Hardware-based field-oriented control
+
+# HardFOC
+
+
Open firmware for ESP32 motor and valve controller boards. Motor commutation runs on dedicated FOC driver chips,
+such as the TMC9660, instead of in MCU software; the firmware around them is C++ on ESP-IDF and FreeRTOS, split
+into portable chip drivers, a shared core, board HALs and one application per board.
+
+- **[Vortex V1 motor controller](get-started/vortex.md)**
+
+ ESP32-C6 with a TMC9660 smart gate driver running FOC on chip, an AS5047U magnetic encoder and CAN
+ (CANopen).
+
+- **[Flux V1 valve controller](get-started/flux.md)**
+
+ ESP32-S3 with on/off and proportional solenoid channels on a TLE92466ED and a MAX22200.
+
+- **[A chip driver on your own MCU](get-started/drivers.md)**
+
+ Every `hf-*-driver` is hardware-agnostic C++: no platform code, one small bus adapter, ESP32 examples.
+
+
+
+## How the code is layered
+
+
+
+
+
+
+
+
+Each box is one or more repositories; each arrow is a Git submodule pinned to a commit or tag. The
+[architecture page](architecture.md) shows the repositories in each layer.
+
+## Documentation
+
+Every repository with a documentation site publishes it at `https://hardfoc.github.io//`, versioned:
+`latest/` is the newest release (or the default branch until the first release), and older versions stay
+available. The [repository catalogue](repositories.md) links each one, with its topics and latest release.
+
+
+
+- **[Repositories](repositories.md)**
+
+ Every public repository, searchable by name, description and topic.
+
+- **[Architecture](architecture.md)**
+
+ Drivers, core, HALs and products, and how they are pinned together.
+
+- **[Contribute](contribute.md)**
+
+ How to report a problem, propose a change, and report a vulnerability privately.
+
+
diff --git a/docs/porting/README.md b/docs/porting/README.md
deleted file mode 100644
index 79bfa5c..0000000
--- a/docs/porting/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Porting Guide
-
-Instructions for porting the project to new hardware or platforms.
diff --git a/docs/repositories.md b/docs/repositories.md
new file mode 100644
index 0000000..1dd11c3
--- /dev/null
+++ b/docs/repositories.md
@@ -0,0 +1,18 @@
+---
+hide:
+ - toc
+---
+
+# Repositories
+
+Every public HardFOC repository, with its topics, documentation and latest release; type to filter, or pick a
+topic.
+
+
+
+--8<-- "build/portal/catalogue.md"
+
+This list is built from the GitHub API and the organisation's repository record
+([`org/org-metadata.json`](https://github.com/hardfoc/.github/blob/main/org/org-metadata.json)) each time the
+portal is published. A link to a topic: `repositories/?topic=spi`.
diff --git a/mkdocs.yml b/mkdocs.yml
index 8b1761c..7297a5a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,13 +1,24 @@
-site_name: hardfoc Documentation
-nav:
- - Home: index.md
- - About: about.md
- - Getting Started: getting-started/README.md
- - API: api/README.md
- - Porting: porting/README.md
- - Hardware: hardware/README.md
+# The HardFOC portal: the organisation's home page at https://hardfoc.github.io/, built with the shared theme from
+# hardfoc/hf-general-ci-tools. Unversioned: one current site, published from main at the root of gh-pages.
+#
+# pip install "git+https://github.com/hardfoc/hf-general-ci-tools@v1#subdirectory=packages/hardfoc-mkdocs-theme"
+# python3 tools/portal/catalogue.py the repository catalogue, from the GitHub API (build/portal/catalogue.md)
+# mkdocs serve http://127.0.0.1:8000
+# mkdocs build --strict what CI runs
+# hardfoc-docs figures rebuild the layer figure after editing docs/diagrams/figures/
+site_name: HardFOC
+site_description: >-
+ HardFOC: open firmware for ESP32 motor and valve controller boards, with hardware-agnostic C++ chip drivers, a
+ shared core, board HALs and application firmware. Every repository, its documentation and how to start.
+repo_url: https://github.com/hardfoc/hardfoc.github.io
+
theme:
- name: material
- features:
- - navigation.tabs
- - navigation.sections
+ name: hardfoc
+
+plugins:
+ - hardfoc
+ - search
+ - awesome-nav
+
+extra_javascript:
+ - assets/catalogue.js
diff --git a/tests/test_catalogue.py b/tests/test_catalogue.py
new file mode 100644
index 0000000..de39cde
--- /dev/null
+++ b/tests/test_catalogue.py
@@ -0,0 +1,44 @@
+"""The catalogue's grouping and cards, without the network."""
+
+from __future__ import annotations
+
+import importlib.util
+import pathlib
+import unittest
+
+spec = importlib.util.spec_from_file_location(
+ "catalogue", pathlib.Path(__file__).resolve().parents[1] / "tools/portal/catalogue.py")
+catalogue = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(catalogue)
+
+
+def repo(name, topics, docs="", release=None, description="A repository."):
+ return {"name": name, "html_url": f"https://github.com/hardfoc/{name}", "description": description,
+ "topics": ["hardfoc", *topics], "docs": docs, "release": release}
+
+
+class CatalogueTest(unittest.TestCase):
+ def test_groups(self):
+ self.assertEqual(catalogue.group_of(["hardfoc", "library", "driver"]), "library")
+ self.assertEqual(catalogue.group_of(["legacy", "driver"]), "legacy")
+ self.assertEqual(catalogue.group_of(["hardfoc", "community-health"]), "other")
+
+ def test_render(self):
+ out = catalogue.render([
+ repo("hf-x-driver", ["driver", "spi"], docs="https://hardfoc.github.io/hf-x-driver/",
+ release=("v1.0.0", "https://github.com/hardfoc/hf-x-driver/releases/tag/v1.0.0")),
+ repo("hf-old", ["legacy"], description='Old <"quoted">'),
+ repo("hardfoc-vortex-v1", ["firmware"]),
+ ])
+ self.assertLess(out.index("## Products"), out.index("## Chip drivers"))
+ self.assertLess(out.index("## Chip drivers"), out.index("## Legacy"))
+ self.assertNotIn("## Board HALs", out) # empty groups are left out
+ self.assertIn('data-topic="spi"', out)
+ self.assertNotIn('data-topic="hardfoc"', out) # on every repository, so not a filter
+ self.assertIn(">Release v1.0.0", out)
+ self.assertIn("Old <"quoted">", out)
+ self.assertIn('data-total="3"', out)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/portal/catalogue.py b/tools/portal/catalogue.py
new file mode 100644
index 0000000..eceaefa
--- /dev/null
+++ b/tools/portal/catalogue.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+"""Write the repository catalogue of the portal from the GitHub API, at build time.
+
+ python3 tools/portal/catalogue.py [--out build/portal/catalogue.md] [--metadata FILE]
+
+Sources, in this order of authority:
+
+* ``org/org-metadata.json`` in ``hardfoc/.github``: the organisation's own record of each repository's description
+ and topics (fetched from the API, or ``--metadata`` for a local copy);
+* the GitHub API: which repositories are public, whether they publish GitHub Pages, their latest release, and the
+ description and topics of a public repository the metadata does not list yet.
+
+Only public repositories are listed. The output is Markdown with HTML cards, included by ``docs/repositories.md``
+(``--8<-- "build/portal/catalogue.md"``); it is a build product and is not committed. ``GH_TOKEN`` (or
+``GITHUB_TOKEN``) raises the API rate limit; ru-mkdocs-publish sets it for its check commands.
+"""
+
+from __future__ import annotations
+
+import argparse
+import base64
+import html
+import json
+import os
+import pathlib
+import sys
+import urllib.error
+import urllib.request
+
+ORG = "hardfoc"
+API = "https://api.github.com"
+ROOT = pathlib.Path(__file__).resolve().parents[2]
+
+# Groups in reading order: (title, one-line purpose, role topic). A repository goes to the group of the first topic
+# of ORDER it carries ("legacy" wins over everything), or to the last group.
+GROUPS = [
+ ("Products", "Application firmware, one repository per board.", "firmware"),
+ ("Core libraries", "The shared platform every board builds on.", "library"),
+ ("Board HALs", "One object per board, one manager per board function.", "hal"),
+ ("Chip drivers", "Hardware-agnostic C++ drivers: no platform code, a small bus adapter per MCU.", "driver"),
+ ("CI and tools", "Reusable workflows, project tooling and the documentation platform.", "ci-tools"),
+ ("Documentation and organisation", "The portal, the handbook, diagrams and the organisation profile.", None),
+ ("Legacy", "Superseded repositories, kept for history.", "legacy"),
+]
+ORDER = ["legacy", "firmware", "library", "hal", "driver", "ci-tools"]
+HIDDEN_TOPICS = {"hardfoc"} # on every repository
+
+
+def api(path: str):
+ req = urllib.request.Request(API + path, headers={"Accept": "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ "User-Agent": "hardfoc-portal"})
+ token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
+ if token:
+ req.add_header("Authorization", f"Bearer {token}")
+ try:
+ with urllib.request.urlopen(req, timeout=30) as r:
+ return json.load(r)
+ except urllib.error.HTTPError as e:
+ if e.code == 404:
+ return None
+ raise SystemExit(f"catalogue: GET {path}: HTTP {e.code} {e.reason}") from e
+
+
+def metadata(path: str | None) -> dict[str, dict]:
+ if path:
+ doc = json.loads(pathlib.Path(path).read_text())
+ else:
+ blob = api(f"/repos/{ORG}/.github/contents/org/org-metadata.json")
+ if not blob:
+ raise SystemExit("catalogue: org/org-metadata.json not found in hardfoc/.github")
+ doc = json.loads(base64.b64decode(blob["content"]))
+ return {r["repo"]: r for r in doc["repos"]}
+
+
+def public_repos() -> list[dict]:
+ out, page = [], 1
+ while True:
+ batch = api(f"/orgs/{ORG}/repos?type=public&per_page=100&page={page}") or []
+ out += batch
+ if len(batch) < 100:
+ return out
+ page += 1
+
+
+def group_of(topics: list[str]) -> str:
+ for role in ORDER:
+ if role in topics:
+ return role
+ return "other"
+
+
+def card(r: dict) -> str:
+ name = r["name"]
+ topics = [t for t in r["topics"] if t not in HIDDEN_TOPICS]
+ search = " ".join([name, r["description"], *topics]).lower()
+ links = [f'Repository']
+ if r["docs"]:
+ links.append(f'Documentation')
+ if r["release"]:
+ tag, url = r["release"]
+ links.append(f'Release {html.escape(tag)}')
+ chips = "".join(f''
+ for t in topics)
+ return (
+ f'