From 4a7f252983c4b1654e6b92f08a8fc9ab97ee3aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Mon, 26 Oct 2026 12:32:40 -0400 Subject: [PATCH 1/4] gitignore: Ignore webpage build outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e767ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +webpage/public/ +webpage/static/data/ +webpage/node_modules/ +webpage/static/js/params.js From 3d53a05ae5a57f2ff182cfa764b90f1181f2b7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Mon, 26 Oct 2026 12:32:40 -0400 Subject: [PATCH 2/4] webpage: themes: Add abridge 2.5.1 submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .gitmodules | 3 +++ webpage/themes/abridge | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 webpage/themes/abridge diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ac5e4f4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "webpage/themes/abridge"] + path = webpage/themes/abridge + url = https://github.com/Jieiku/abridge diff --git a/webpage/themes/abridge b/webpage/themes/abridge new file mode 160000 index 0000000..d52609f --- /dev/null +++ b/webpage/themes/abridge @@ -0,0 +1 @@ +Subproject commit d52609f456f233ec0845c8a57a1e27583f0813ad From 52224fd177e0d4502d6d2aeb44f08ad690165a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Mon, 26 Oct 2026 12:32:40 -0400 Subject: [PATCH 3/4] webpage: First version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- webpage/bun.lockb | Bin 0 -> 1238 bytes webpage/config.toml | 29 +++++++++ webpage/content/_index.md | 3 + webpage/content/about.md | 14 ++++ webpage/content/params.md | 5 ++ webpage/package.json | 13 ++++ webpage/sass/_extra.scss | 11 ++++ webpage/sass/abridge.scss | 4 ++ webpage/scripts/data.js | 15 +++++ webpage/src/flatten.js | 11 ++++ webpage/src/flatten.test.js | 11 ++++ webpage/src/params.js | 119 ++++++++++++++++++++++++++++++++++ webpage/templates/params.html | 25 +++++++ 13 files changed, 260 insertions(+) create mode 100755 webpage/bun.lockb create mode 100644 webpage/config.toml create mode 100644 webpage/content/_index.md create mode 100644 webpage/content/about.md create mode 100644 webpage/content/params.md create mode 100644 webpage/package.json create mode 100644 webpage/sass/_extra.scss create mode 100644 webpage/sass/abridge.scss create mode 100644 webpage/scripts/data.js create mode 100644 webpage/src/flatten.js create mode 100644 webpage/src/flatten.test.js create mode 100644 webpage/src/params.js create mode 100644 webpage/templates/params.html diff --git a/webpage/bun.lockb b/webpage/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..97b52fb948dc2916b84b7ecbd24f18a9531e8b6f GIT binary patch literal 1238 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p+$vE{wC zf4y!xN={*S;H;Oa+2k&e(e-$(OT`s)ElzJGHx{5GAONWa0S*WSryHRBDwqO>^3mgIo$`5zuS~8=(2@KpG}SteGG;AiD#V z|BwG4vLvyF;!_7SkC6dlF0#=qWlO%rh>2KEUc^1SOjhy(gT+Hr-8q$~YIi?5>84cb zH2?my6;CSd9BdEv_teeE=E_SD-YP zO>t4OUS?i#Nn%cp9xMUsg%qWx+9?<(s#I17~KvMDt-vIANmRbOvX$H5$5W|@ap!5w( z*G!D&dPaH%K-11ZO@Yy<;ZU5KTb5d+=UP#cT9lWV6I@c1nU`*-V2H5D0&dX{s7ZFv zTnO|x&`pL|os^rImsy;eSd^TRQBqP+Y^ASXl$xGdTvAl2msgORRjij^l&+6Zq_2y_ R(FHqQuOz(+63&B<1OQH}z%~E? literal 0 HcmV?d00001 diff --git a/webpage/config.toml b/webpage/config.toml new file mode 100644 index 0000000..30ab4de --- /dev/null +++ b/webpage/config.toml @@ -0,0 +1,29 @@ +base_url = "https://patrickelectric.work/ParameterRepository" +title = "ArduPilot Parameters" +description = "Search ArduPilot parameters across vehicles and firmware versions." +default_language = "en" +theme = "abridge" +compile_sass = true +build_search_index = false +generate_feeds = false + +[extra] +menu = [ + {url = "params", name = "Parameters", slash = true, blank = false, size = "s110"}, + {url = "about", name = "About", slash = true, blank = false, size = "s110"}, +] +menu_footer = [] +textlogo = "ArduPilot Parameters" +author = "ArduPilot" +toc = false +recent = false +series = false +footer_credit = true +seo = false +search_library = false +js_bundle = true +js_switcher = true +js_switcher_default = "dark" +integrity = true +pwa = false +favicon_svg = "favicon.svg" diff --git a/webpage/content/_index.md b/webpage/content/_index.md new file mode 100644 index 0000000..f6dc014 --- /dev/null +++ b/webpage/content/_index.md @@ -0,0 +1,3 @@ ++++ +redirect_to = "params/" ++++ diff --git a/webpage/content/about.md b/webpage/content/about.md new file mode 100644 index 0000000..413614b --- /dev/null +++ b/webpage/content/about.md @@ -0,0 +1,14 @@ ++++ +title = "About" +description = "Where the parameter data comes from." ++++ + +Parameter data is generated from each ArduPilot firmware branch's `apm.pdef.xml` +and collected in the [ParameterRepository](https://github.com/ArduPilot/ParameterRepository), +which a bot refreshes every five days. This site searches them in the browser: +every word typed must start a word in the parameter name or description, with a +fuzzy fallback when nothing matches. + +"latest" is the highest version folder available for a vehicle. Values, bitmasks, +ranges and units come straight from the metadata; defaults are board-specific and +are not part of it. diff --git a/webpage/content/params.md b/webpage/content/params.md new file mode 100644 index 0000000..7a5a9f8 --- /dev/null +++ b/webpage/content/params.md @@ -0,0 +1,5 @@ ++++ +title = "Parameters" +description = "Search ArduPilot parameters by vehicle and firmware version." +template = "params.html" ++++ diff --git a/webpage/package.json b/webpage/package.json new file mode 100644 index 0000000..118855c --- /dev/null +++ b/webpage/package.json @@ -0,0 +1,13 @@ +{ + "name": "webpage", + "private": true, + "scripts": { + "data": "bun scripts/data.js", + "bundle": "bun build src/params.js --outfile static/js/params.js --minify", + "build": "bun run data && bun run bundle && zola build", + "serve": "bun run data && (bun run bundle --watch & zola serve)" + }, + "dependencies": { + "minisearch": "7.2.0" + } +} diff --git a/webpage/sass/_extra.scss b/webpage/sass/_extra.scss new file mode 100644 index 0000000..44df7c2 --- /dev/null +++ b/webpage/sass/_extra.scss @@ -0,0 +1,11 @@ +.pctl { display: flex; flex-wrap: wrap; gap: .5em; align-items: center; margin: 1em 0; } +.pctl input { flex: 1; min-width: 12em; } +#chips { display: flex; flex-wrap: wrap; gap: .3em; max-height: 6em; overflow-y: auto; margin-bottom: 1em; } +#chips button { font-size: .8em; padding: .1em .5em; } +#chips button[aria-pressed="true"] { background: var(--a1); color: var(--c1); } +#params { width: 100%; border-collapse: collapse; } +#params[aria-busy] { opacity: .5; } +#params th { position: sticky; top: 0; background: var(--c2); text-align: left; } +#params td.n { font-family: monospace; white-space: nowrap; } +#params tr.p { cursor: pointer; } +#params tr.x td { background: var(--c2); font-size: .9em; white-space: pre-wrap; } diff --git a/webpage/sass/abridge.scss b/webpage/sass/abridge.scss new file mode 100644 index 0000000..16b380a --- /dev/null +++ b/webpage/sass/abridge.scss @@ -0,0 +1,4 @@ +@use '../themes/abridge/sass/abridge' with ( + $mw: 1200px, +); +@use 'extra'; diff --git a/webpage/scripts/data.js b/webpage/scripts/data.js new file mode 100644 index 0000000..353c1ca --- /dev/null +++ b/webpage/scripts/data.js @@ -0,0 +1,15 @@ +// Copy every ../-/apm.pdef.json into static/data and list them in index.json +import { copyFileSync, mkdirSync, readdirSync, writeFileSync } from 'fs'; + +const out = 'static/data'; +mkdirSync(out, { recursive: true }); +const index = {}; +for (const dir of readdirSync('..').filter(d => /^\w+-[\d.]+$/.test(d))) { + const [vehicle, version] = dir.split('-'); + copyFileSync(`../${dir}/apm.pdef.json`, `${out}/${dir}.json`); + (index[vehicle] ??= []).push(version); +} +for (const versions of Object.values(index)) versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); +// Copter first so it is the default vehicle +const vehicles = Object.entries(index).sort(([a], [b]) => (a !== 'Copter') - (b !== 'Copter') || a.localeCompare(b)); +writeFileSync(`${out}/index.json`, JSON.stringify(Object.fromEntries(vehicles))); diff --git a/webpage/src/flatten.js b/webpage/src/flatten.js new file mode 100644 index 0000000..ced5b60 --- /dev/null +++ b/webpage/src/flatten.js @@ -0,0 +1,11 @@ +// apm.pdef.json groups parameters by prefix; the page wants one sorted list without duplicates +export default function flatten(pdef) { + const byName = new Map(); + for (const [group, params] of Object.entries(pdef)) { + if (group === 'json') continue; // format version marker + for (const [n, p] of Object.entries(params)) { + byName.set(n, { n, d: p.Description || p.DisplayName || '', r: p.Range && [p.Range.low, p.Range.high], u: p.Units, v: p.Values, b: p.Bitmask }); + } + } + return [...byName.values()].sort((a, b) => (a.n > b.n) - (a.n < b.n)); +} diff --git a/webpage/src/flatten.test.js b/webpage/src/flatten.test.js new file mode 100644 index 0000000..1d51847 --- /dev/null +++ b/webpage/src/flatten.test.js @@ -0,0 +1,11 @@ +import { expect, test } from 'bun:test'; +import flatten from './flatten'; + +test('flatten skips the marker, dedupes by name and sorts', () => { + const pdef = { + json: { version: 0 }, + WP_: { WP_SPEED: { Description: 'a', Range: { low: '0', high: '1' } } }, + Rover: { WP_SPEED: { Description: 'b' }, ACRO_X: { DisplayName: 'c', Units: 'm' } }, + }; + expect(flatten(pdef)).toEqual([{ n: 'ACRO_X', d: 'c', u: 'm' }, { n: 'WP_SPEED', d: 'b' }]); +}); diff --git a/webpage/src/params.js b/webpage/src/params.js new file mode 100644 index 0000000..1b79b8f --- /dev/null +++ b/webpage/src/params.js @@ -0,0 +1,119 @@ +import MiniSearch from 'minisearch'; +import flatten from './flatten'; + +const idx = JSON.parse(document.getElementById('idx').textContent); +const $ = id => document.getElementById(id); +const table = $('params'), tbody = table.querySelector('tbody'); +const cache = new Map(); +let rows = [], ms, hits = [], shown = 0, group = '', gen = 0; + +const fmtMap = m => Object.entries(m).map(([k, v]) => `${k}: ${v}`).join(', '); +const esc = t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// text with every matched term wrapped in +function marked(text, terms) { + const frag = document.createDocumentFragment(); + let last = 0; + for (const m of terms.length ? text.matchAll(new RegExp(terms.map(esc).join('|'), 'gi')) : []) { + frag.append(text.slice(last, m.index), Object.assign(document.createElement('mark'), { textContent: m[0] })); + last = m.index + m[0].length; + } + frag.append(text.slice(last)); + return frag; +} + +function td(text, cls, terms = []) { + const e = document.createElement('td'); + e.append(marked(text, terms)); + if (cls) e.className = cls; + return e; +} + +function load(file) { + if (!cache.has(file)) cache.set(file, fetch(`${table.dataset.base}/${file}.json`).then(r => { + if (!r.ok) throw new Error(`${file}: ${r.status}`); + return r.json(); + }).then(flatten).catch(e => { cache.delete(file); throw e; })); // let the next selection retry + return cache.get(file); +} + +function fillVersions() { + $('version').innerHTML = idx[$('vehicle').value].map((v, i) => ``).join(''); +} + +function fillChips() { + const groups = new Set(rows.map(r => r.n.split('_')[0])); + $('chips').innerHTML = [...groups].sort().map(g => ``).join(''); +} + +function search() { + if (!ms) return; // typed before the first load, reload() searches when done + const q = $('q').value.trim(); + // every term must match; fuzzy only as a fallback since results are alphabetical, not ranked + hits = q ? ms.search(q, { prefix: true, combineWith: 'AND' }) : rows.slice(); + if (q && !hits.length) hits = ms.search(q, { prefix: true, combineWith: 'AND', fuzzy: 0.2 }); + hits.sort((a, b) => (a.n > b.n) - (a.n < b.n)); // codepoint order, same as the JSON + const i = hits.findIndex(h => h.n === q.toUpperCase().replace(/\s+/g, '_')); + if (i > 0) hits.unshift(...hits.splice(i, 1)); + if (group) hits = hits.filter(h => h.n.split('_')[0] === group); + $('count').textContent = `${hits.length} parameters`; + tbody.textContent = ''; shown = 0; more(); + history.replaceState(null, '', '#' + new URLSearchParams({ v: $('vehicle').value, r: $('version').value, q, g: group })); +} + +function more() { + const frag = document.createDocumentFragment(); + for (const h of hits.slice(shown, shown + 300)) { + const tr = document.createElement('tr'); + tr.className = 'p'; + const terms = h.match ? Object.keys(h.match) : []; + tr.append(td(h.n, 'n', terms), td(h.d, null, terms), td(h.r ? h.r.join(' to ') : ''), td(h.u || '')); + tr.onclick = () => { + const next = tr.nextElementSibling; + if (next?.classList.contains('x')) return next.remove(); + const x = document.createElement('tr'); x.className = 'x'; + const cell = td([h.v && 'Values: ' + fmtMap(h.v), h.b && 'Bitmask: ' + fmtMap(h.b)].filter(Boolean).join('\n') || 'No values or bitmask'); + cell.colSpan = 4; x.append(cell); tr.after(x); + }; + frag.append(tr); + } + shown += 300; tbody.append(frag); $('more').hidden = shown >= hits.length; +} + +async function reload() { + const file = `${$('vehicle').value}-${$('version').value}`, my = ++gen; + $('count').textContent = 'Loading…'; + table.setAttribute('aria-busy', 'true'); + try { + rows = await load(file); + } catch (e) { + if (my === gen) $('count').textContent = `Failed to load ${file}`; + throw e; + } finally { + if (my === gen) table.removeAttribute('aria-busy'); + } + if (my !== gen) return; // a newer selection won + ms = new MiniSearch({ fields: ['n', 'd'], storeFields: ['n', 'd', 'r', 'u', 'v', 'b'], idField: 'n' }); + ms.addAll(rows); + fillChips(); search(); +} + +$('vehicle').onchange = () => { fillVersions(); reload(); }; +$('version').onchange = reload; +$('q').oninput = search; +$('more').onclick = more; +$('chips').onclick = e => { + const g = e.target.dataset.g; + if (g === undefined) return; + group = group === g ? '' : g; + fillChips(); search(); +}; + +const h = new URLSearchParams(location.hash.slice(1)); +if (Object.hasOwn(idx, h.get('v'))) $('vehicle').value = h.get('v'); +fillVersions(); +$('version').value = h.get('r'); +if (!$('version').value) $('version').selectedIndex = 0; +$('q').value = h.get('q') || ''; +group = h.get('g') || ''; +reload(); diff --git a/webpage/templates/params.html b/webpage/templates/params.html new file mode 100644 index 0000000..01b2ab8 --- /dev/null +++ b/webpage/templates/params.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{%- block seo %} + {{- super() }} + {{- macros_seo::seo(config=config, title=page.title, title_addition=title_separator ~ config.title, description=page.description, is_page=true) }} +{%- endblock seo %} +{%- block content %} +{%- set idx = load_data(path="static/data/index.json") %} +
+

{{ page.title }}

+
+ + + + +
+
+ + + +
NameDescriptionRangeUnits
+ + + +
+{%- endblock content %} From 152154d5f58023f673212ca6285548dc94186d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Mon, 26 Oct 2026 12:35:24 -0400 Subject: [PATCH 4/4] github: workflows: webpage: Add build and deploy workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .github/workflows/webpage.yml | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/webpage.yml diff --git a/.github/workflows/webpage.yml b/.github/workflows/webpage.yml new file mode 100644 index 0000000..2d3406e --- /dev/null +++ b/.github/workflows/webpage.yml @@ -0,0 +1,52 @@ +name: Webpage + +on: + push: + branches: [main] + pull_request: + workflow_run: + workflows: ["Run Python Script and Commit Changes"] + types: [completed] + branches: [main] + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install zola + uses: taiki-e/install-action@v2 + with: + tool: zola@0.20.0 + - name: Install bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.1.24 + - name: Build + run: bun install --frozen-lockfile && bun test && bun run build && zola check + working-directory: webpage + - uses: actions/upload-pages-artifact@v3 + with: + path: webpage/public + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4