diff --git a/.github/scripts/extract-size-report.sh b/.github/scripts/extract-size-report.sh new file mode 100755 index 00000000000..24170ac952b --- /dev/null +++ b/.github/scripts/extract-size-report.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# +# Run the size tool on every built .elf and write a small JSON report of +# flash/RAM usage per target, consumed by ci-size-report.yml. +# +# Usage: extract-size-report.sh [size-tool] +# +# flash = .text + .data (what's programmed into flash) +# ram = .data + .bss (what's reserved in RAM at runtime) +# +# Runs inside the (unprivileged) build job on the PR's own checkout, so a +# PR could in principle modify this script to misreport its own numbers. +# Accepted tradeoff: this feature is informational/non-gating, and a real +# overflow still fails the link step regardless of what this script says. + +set -euo pipefail + +BUILD_DIR=${1:?usage: extract-size-report.sh [size-tool]} +OUTPUT_JSON=${2:?usage: extract-size-report.sh [size-tool]} +SIZE_TOOL=${3:-} + +if [ -z "$SIZE_TOOL" ]; then + if command -v arm-none-eabi-size >/dev/null 2>&1; then + SIZE_TOOL=arm-none-eabi-size + else + # cmake/arm-none-eabi-checks.cmake downloads its own toolchain into + # tools/ and adds it to PATH — but only inside that cmake process + # via set(ENV{PATH} ...), which doesn't persist to a later CI step's + # shell. Fall back to the same location CMake would have used, + # relative to the repo root (this script must be run from there). + SIZE_TOOL=$(compgen -G 'tools/arm-gnu-toolchain-*/bin/arm-none-eabi-size' 2>/dev/null | head -n1 || true) + if [ -z "$SIZE_TOOL" ]; then + echo "::error::arm-none-eabi-size not found on PATH or under tools/arm-gnu-toolchain-*/bin/" >&2 + exit 1 + fi + fi +fi + +# CMake's RUNTIME_OUTPUT_DIRECTORY puts built executables under +# /bin/ (see cmake/main.cmake), not directly — search +# instead of assuming a fixed depth, in case that ever changes. +mapfile -t ELFS < <(find "$BUILD_DIR" -maxdepth 3 -name '*.elf' | sort) +if [ "${#ELFS[@]}" -eq 0 ]; then + echo "::warning::No .elf files found under $BUILD_DIR, writing empty size report" + echo '{}' > "$OUTPUT_JSON" + exit 0 +fi + +JQ_ARGS=() +for elf in "${ELFS[@]}"; do + target=$(basename "$elf" .elf) + + # Berkeley format: " text data bss dec hex filename" + read -r text data bss _dec _hex _name < <("$SIZE_TOOL" -B "$elf" | tail -n1) + + flash=$((text + data)) + ram=$((data + bss)) + + JQ_ARGS+=(--argjson "entry_${#JQ_ARGS[@]}" "{\"target\":\"${target}\",\"flash\":${flash},\"ram\":${ram}}") +done + +# Build via jq rather than manual string concatenation, so the target name +# (an .elf basename, not otherwise validated) is JSON-escaped properly +# instead of relying on it never containing a special character. +jq -n "${JQ_ARGS[@]}" 'reduce $ARGS.named[] as $e ({}; .[$e.target] = {flash: $e.flash, ram: $e.ram})' \ + > "$OUTPUT_JSON" + +echo "Wrote size report for ${#ELFS[@]} target(s) to $OUTPUT_JSON" diff --git a/.github/scripts/fetch-size-baseline.sh b/.github/scripts/fetch-size-baseline.sh new file mode 100644 index 00000000000..f2460f6996e --- /dev/null +++ b/.github/scripts/fetch-size-baseline.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# +# WARNING: This script is invoked from a GitHub Actions job via +# `bash .github/scripts/...`. It MUST NEVER be called from a job that has +# not first checked out a known branch (e.g. `- uses: actions/checkout@v4` +# as the job's first step): the runner workspace is empty until then, so +# the script doesn't exist and the call fails with exit 127, silently +# reporting no baseline. Every job calling this script must check out +# first. +# +# Fetch the size baseline for a PR's TRUE base commit — the merge-base of +# the PR head and base ref — rather than the base branch's latest tip, so +# the size-diff delta doesn't include unrelated changes merged to the base +# after the PR forked (see the +9,788 B stale-RAM regression on PR #11785). +# +# Strategy, in order: +# 1. Exact: size-baseline- in the companion builds repo. +# 2. Nearest ancestor: walk first-parents of the merge-base (older commits +# on the base branch) up to MAX_WALK steps, using the first per-commit +# baseline tag found. Nightly baselines exist only for commits that were +# actually pushed to the branch, so the walk finds the closest nightly +# build at-or-before the fork point. +# 3. Otherwise report found=false (caller emits the graceful +# "no size baseline available" comment path). +# +# The base branch's LATEST-tip baseline is deliberately NOT used as a +# fallback: comparing against it is exactly the stale-delta bug this fixes. +# +# Usage: fetch-size-baseline.sh +# the firmware repo, e.g. iNavFlight/inav +# companion repo holding the baselines, e.g. iNavFlight/pr-test-builds +# PR base branch, e.g. maintenance-10.x (validated) +# full 40-hex SHA of the PR's merge-base commit (validated) +# directory to write size-report.json into +# +# Env: GH_TOKEN with read access to both repos (secrets.PR_BUILDS_TOKEN in +# the pr-comment job has it; the companion repo is private). +# +# Prints key=value lines to stdout for the workflow to append to +# $GITHUB_OUTPUT: found, baseline_commit (full SHA), baseline_commit_short, +# baseline_exact (true when the merge-base itself had a stored baseline). + +set -euo pipefail + +MAIN_REPO=${1:?usage: fetch-size-baseline.sh } +BUILDS_REPO=${2:?usage: fetch-size-baseline.sh } +BASE_REF=${3:?usage: fetch-size-baseline.sh } +MERGE_BASE_SHA=${4:?usage: fetch-size-baseline.sh } +OUT_DIR=${5:?usage: fetch-size-baseline.sh } + +MAX_WALK=30 + +if ! [[ "$BASE_REF" =~ ^[A-Za-z0-9._/-]{1,100}$ ]]; then + echo "::error::Invalid base ref: $BASE_REF" >&2 + exit 1 +fi +if ! [[ "$MERGE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Invalid merge-base SHA: $MERGE_BASE_SHA" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" + +# Set of per-commit baseline SHAs currently stored in the builds repo. +# One paginated call; tags are built by publish-size-baseline.sh and match +# the strict 40-hex pattern by construction. +list_baseline_shas() { + gh api "repos/${BUILDS_REPO}/releases?per_page=100" --paginate \ + --jq '.[].tag_name | select(test("^size-baseline-[0-9a-f]{40}$")) | sub("^size-baseline-"; "")' +} + +# publish-size-baseline replaces the asset in place (--clobber), which still +# briefly deletes-then-uploads under the hood; a few short retries absorb +# that window instead of misreporting "no baseline available". +download_baseline() { # $1 = 40-hex sha + local tag="size-baseline-${1}" + for attempt in 1 2 3; do + if gh release download "$tag" --repo "$BUILDS_REPO" \ + --pattern size-report.json --dir "$OUT_DIR" 2>/dev/null; then + return 0 + fi + sleep 3 + done + return 1 +} + +emit_found() { # $1 = 40-hex sha, $2 = true|false (exact) + echo "found=true" + echo "baseline_commit=${1}" + echo "baseline_commit_short=${1:0:7}" + echo "baseline_exact=${2}" +} + +BASELINE_SHAS=$(list_baseline_shas) || true + +# Exact merge-base first, then nearest ancestors along the base branch's +# first-parent chain. The loop STARTS at the merge-base itself so a +# transient download failure on the exact commit is retried here instead +# of silently degrading to a nearest-ancestor baseline. +sha="$MERGE_BASE_SHA" +first=1 +for _ in $(seq 1 $((MAX_WALK + 1))); do + if grep -qx "$sha" <<< "$BASELINE_SHAS" && download_baseline "$sha"; then + if [ "$first" = 1 ]; then + emit_found "$sha" true + else + emit_found "$sha" false + fi + exit 0 + fi + first=0 + parent=$(gh api "repos/${MAIN_REPO}/commits/${sha}" --jq '.parents[0].sha // empty' 2>/dev/null || true) + if [ -z "$parent" ] || ! [[ "$parent" =~ ^[0-9a-f]{40}$ ]]; then + break + fi + sha="$parent" +done + +echo "found=false" diff --git a/.github/scripts/merge-size-reports.sh b/.github/scripts/merge-size-reports.sh new file mode 100755 index 00000000000..fcb79457bc9 --- /dev/null +++ b/.github/scripts/merge-size-reports.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Merge multiple per-shard size-report.json files (see extract-size-report.sh) +# into one aggregate JSON, used by the "upload-artifacts" job in ci.yml. +# +# Usage: merge-size-reports.sh ... + +set -euo pipefail + +OUTPUT_JSON=${1:?usage: merge-size-reports.sh ...} +shift + +if [ "$#" -eq 0 ]; then + echo '{}' > "$OUTPUT_JSON" + exit 0 +fi + +jq -s 'add' "$@" > "$OUTPUT_JSON" +echo "Merged $# size report(s) into $OUTPUT_JSON" diff --git a/.github/scripts/publish-size-baseline.sh b/.github/scripts/publish-size-baseline.sh new file mode 100644 index 00000000000..28154cbb4d7 --- /dev/null +++ b/.github/scripts/publish-size-baseline.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# +# WARNING: This script is invoked from a GitHub Actions job via +# `bash .github/scripts/...`. It MUST NEVER be called from a job that has +# not first checked out a known branch (e.g. `- uses: actions/checkout@v4` +# as the job's first step): the runner workspace is empty until then, so +# the script doesn't exist and the call fails with exit 127, silently +# publishing nothing. Every job calling this script must check out first. +# +# Publish the nightly size report as release assets in the companion +# pr-test-builds repo, keyed by BRANCH (latest-tip pointer, kept for +# backward compatibility) AND by COMMIT SHA (primary — lets PR size-diff +# comparisons use the PR's exact base commit instead of the branch tip), +# then prune old per-commit baselines so the companion repo doesn't grow +# unbounded. +# +# Usage: publish-size-baseline.sh [--dry-run] +# companion repo, e.g. iNavFlight/pr-test-builds +# branch the nightly was pushed to (e.g. maintenance-10.x) +# full 40-hex SHA of the pushed commit (workflow_run.head_sha) +# merged size-report.json artifact +# --dry-run print what would be published/pruned without touching GitHub +# +# Env: GH_TOKEN with write access to (Contents: write). +# +# Per-commit baseline releases carry a machine-readable `branch: ` +# first line in their notes so pruning can group them per branch. Tags are +# `size-baseline-<40-hex-sha>`; branch-tip tags are `size-baseline-`. +# +# Pruning policy: keep the newest KEEP_PER_BRANCH per-commit baselines per +# branch, plus a GLOBAL_CAP safety net for orphaned baselines (deleted +# branches, notes parse failures) so the repo can never grow unbounded. + +set -euo pipefail + +BUILDS_REPO=${1:?usage: publish-size-baseline.sh [--dry-run]} +BRANCH=${2:?usage: publish-size-baseline.sh [--dry-run]} +COMMIT_SHA=${3:?usage: publish-size-baseline.sh [--dry-run]} +REPORT_JSON=${4:?usage: publish-size-baseline.sh [--dry-run]} +DRY_RUN=${5:-} + +KEEP_PER_BRANCH=50 +GLOBAL_CAP=300 + +# --- Validate inputs: never trust artifact content or context values before +# they are used to build shell commands or release tags. +if ! [[ "$BRANCH" =~ ^[A-Za-z0-9._/-]{1,100}$ ]]; then + echo "::error::Invalid branch name in artifact: $BRANCH" >&2 + exit 1 +fi +if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Invalid commit SHA: $COMMIT_SHA" >&2 + exit 1 +fi +if [ ! -f "$REPORT_JSON" ]; then + echo "::error::Size report not found: $REPORT_JSON" >&2 + exit 1 +fi + +# publish one baseline release: create once, then only ever replace the +# asset in place (--clobber) so the release/tag stays continuously +# resolvable for concurrent PR comparisons. +publish_asset() { + local tag="$1" title="$2" notes="$3" + if gh release view "$tag" --repo "$BUILDS_REPO" >/dev/null 2>&1; then + gh release upload "$tag" "$REPORT_JSON" --repo "$BUILDS_REPO" --clobber + else + gh release create "$tag" "$REPORT_JSON" --repo "$BUILDS_REPO" --prerelease \ + --title "$title" --notes "$notes" + fi +} + +BRANCH_TAG="size-baseline-${BRANCH}" +COMMIT_TAG="size-baseline-${COMMIT_SHA}" + +if [ "$DRY_RUN" = "--dry-run" ]; then + echo "[dry-run] would publish ${BRANCH_TAG} and ${COMMIT_TAG} in ${BUILDS_REPO}" +else + publish_asset "$BRANCH_TAG" "Size baseline: ${BRANCH}" \ + "Latest per-target flash/RAM size report for ${BRANCH}. Auto-updated on every push. Not for human consumption." + publish_asset "$COMMIT_TAG" "Size baseline: ${COMMIT_SHA}" \ + "branch: ${BRANCH} +Per-commit per-target flash/RAM size report for ${COMMIT_SHA}. Consumed by PR size-diff comparisons; pruned to the newest ${KEEP_PER_BRANCH} per branch." +fi + +# --------------------------------------------------------------------------- +# Prune per-commit baselines +# --------------------------------------------------------------------------- +# Emit \t\t for every per-commit baseline release, +# newest first (GitHub release listing is newest-first). Branch comes from +# the notes' `branch: ` FIRST LINE; the (?m) flag is required — the +# notes are multi-line, and without it ^/$ anchor to the whole string so +# the capture never matches and every baseline collapses into the '?' +# bucket, which would make pruning treat all branches as one. +list_per_commit_baselines() { + gh api "repos/${BUILDS_REPO}/releases?per_page=100" --paginate \ + --jq '.[] | select(.tag_name | test("^size-baseline-[0-9a-f]{40}$")) | + [.created_at, .tag_name, + ((.body // "") | capture("(?m)^branch: (?[A-Za-z0-9._/-]+)$") | .b // "?")] | @tsv' +} + +prune() { + local tmp + tmp=$(mktemp) + trap 'rm -f "$tmp"' RETURN + + # Pass 1 (NR==FNR): record every tag in `all`, and the per-branch + # keep-set — newest KEEP_PER_BRANCH tags per branch — in `keepTag`. + # Pass 2: apply the global cap to the kept set in input order. + # END: delete every tag in `all` that did not survive to keepFinal. + # (Explicit `all` set so pruning never depends on awk's create-on- + # reference semantics of reading keepTag[$2] in pass 2.) + list_per_commit_baselines | sort -r > "$tmp" + + awk -F '\t' -v keep="$KEEP_PER_BRANCH" -v cap="$GLOBAL_CAP" ' + NR == FNR { + all[$2] = 1 + if (count[$3] < keep) { count[$3]++; keepTag[$2] = 1 } + next + } + { + if (keepTag[$2]) { + if (kept < cap) { kept++; keepFinal[$2] = 1 } + } + } + END { + for (t in all) if (!keepFinal[t]) print t + } + ' "$tmp" "$tmp" | while read -r tag; do + if [ "$DRY_RUN" = "--dry-run" ]; then + echo "[dry-run] would prune ${tag}" + else + gh release delete "$tag" --repo "$BUILDS_REPO" --yes --cleanup-tag \ + || echo "::warning::failed to prune ${tag}" >&2 + fi + done +} + +# Pruning is housekeeping: a failure here must not fail the publish (the +# baseline itself already landed above), or the nightly would look broken +# for a cosmetic reason. Warn loudly instead. +prune || echo "::warning::per-commit baseline pruning failed (see stderr)" >&2 diff --git a/.github/scripts/size-diff-comment.js b/.github/scripts/size-diff-comment.js new file mode 100644 index 00000000000..278036aa970 --- /dev/null +++ b/.github/scripts/size-diff-comment.js @@ -0,0 +1,124 @@ +// Pure logic for the "PR RAM/Flash usage delta" comment: diffs a PR's +// size-report.json against a base-branch baseline and renders the markdown +// comment body. Deliberately has no network/filesystem/GitHub Actions +// dependency so it can be unit tested directly and reused (via require) +// from the actions/github-script step in ci-size-report.yml. +// +// Report shape: { "": { "flash": , "ram": }, ... } + +'use strict'; + +// 4 representative targets spanning flash/RAM size tiers (manager-approved +// set, 2026-08-17). Note MATEKF722 and MATEKF765 are both STM32F7 parts — +// "one per family" in the loose sense of distinct flash/RAM budgets, not +// one per silicon line. No AT32 target is covered; flagged back to the +// manager as a possible coverage gap, not decided unilaterally here. +const REPRESENTATIVE_TARGETS = ['MATEKF405', 'MATEKF722', 'MATEKF765', 'MATEKH743']; + +// Below this magnitude a delta is noise (rounding/toolchain jitter), not a +// real change worth calling out. +const NOISE_THRESHOLD_BYTES = 256; + +function formatDelta(deltaBytes, baseBytes) { + const sign = deltaBytes > 0 ? '+' : deltaBytes < 0 ? '' : '±'; + const pct = baseBytes > 0 ? (deltaBytes / baseBytes) * 100 : 0; + const pctStr = baseBytes > 0 ? ` (${sign}${pct.toFixed(2)}%)` : ''; + return `${sign}${deltaBytes} B${pctStr}`; +} + +// Returns an array of row objects, one per representative target, each +// either a comparison row or a status row (missing from PR/baseline). +function diffSizeReports(prReport, baselineReport) { + return REPRESENTATIVE_TARGETS.map((target) => { + const pr = prReport[target]; + const base = baselineReport ? baselineReport[target] : undefined; + + if (!pr && !base) { + return { target, status: 'not-built' }; + } + if (!pr) { + return { target, status: 'missing-from-pr' }; + } + if (!base) { + return { target, status: 'no-baseline', flash: pr.flash, ram: pr.ram }; + } + + const flashDelta = pr.flash - base.flash; + const ramDelta = pr.ram - base.ram; + return { + target, + status: 'compared', + flash: pr.flash, + ram: pr.ram, + baseFlash: base.flash, + baseRam: base.ram, + flashDelta, + ramDelta, + notable: Math.abs(flashDelta) >= NOISE_THRESHOLD_BYTES || Math.abs(ramDelta) >= NOISE_THRESHOLD_BYTES, + }; + }); +} + +// docLink: string URL to link, or null/undefined to omit the doc-link line. +// baselineCommit: short SHA of the baseline commit the delta was computed +// against, or null/undefined to fall back to the generic "base branch" +// wording. baselineIsNearest: true when the exact base commit had no +// stored baseline and a nearest-ancestor baseline was used instead. +function renderComment({ prReport, baselineReport, shortSha, baselineCommit, baselineIsNearest, docLink, marker }) { + const rows = diffSizeReports(prReport, baselineReport); + // Only name the baseline commit when there is actually a baseline to + // compare against (the workflow only sets baselineCommit in that case, + // but the renderer must not emit a contradictory header otherwise). + const vs = (baselineReport && baselineCommit) + ? `vs. base commit \`${baselineCommit}\`` : 'vs. base branch'; + const lines = [marker, `**RAM / Flash usage ${vs}** — commit \`${shortSha}\``, '']; + + if (!baselineReport) { + lines.push( + '> No size baseline is available yet for this PR\'s base commit ' + + '(no per-commit baseline has been published for it). This comment ' + + 'will show deltas once one exists — rebasing the PR refreshes its ' + + 'base commit.', + '' + ); + } else if (baselineIsNearest) { + lines.push( + '> Using the nearest available size baseline — the PR\'s exact base ' + + 'commit has no stored baseline yet.', + '' + ); + } + + const anyComparable = rows.some((r) => r.status === 'compared' || r.status === 'no-baseline'); + if (anyComparable) { + lines.push('| Target | Flash Δ | RAM Δ |', '|---|---|---|'); + for (const row of rows) { + if (row.status === 'compared') { + const flashCell = formatDelta(row.flashDelta, row.baseFlash); + const ramCell = formatDelta(row.ramDelta, row.baseRam); + const notableMark = row.notable ? ' ⚠️' : ''; + lines.push(`| ${row.target}${notableMark} | ${flashCell} | ${ramCell} |`); + } else if (row.status === 'no-baseline') { + lines.push(`| ${row.target} | ${row.flash} B (no baseline) | ${row.ram} B (no baseline) |`); + } else if (row.status === 'missing-from-pr') { + lines.push(`| ${row.target} | not built by this PR | not built by this PR |`); + } + // 'not-built': omit entirely, nothing meaningful to say + } + lines.push(''); + } else { + lines.push( + '_None of the representative targets (' + REPRESENTATIVE_TARGETS.join(', ') + ') ' + + 'were built by this PR — no size comparison to show._', + '' + ); + } + + if (docLink) { + lines.push(`See [RAM/flash optimization guide](${docLink}) for techniques to reduce usage.`, ''); + } + + return lines.join('\n').trimEnd() + '\n'; +} + +module.exports = { REPRESENTATIVE_TARGETS, NOISE_THRESHOLD_BYTES, diffSizeReports, renderComment, formatDelta }; diff --git a/.github/scripts/size-diff-comment.test.js b/.github/scripts/size-diff-comment.test.js new file mode 100644 index 00000000000..5b10836e47d --- /dev/null +++ b/.github/scripts/size-diff-comment.test.js @@ -0,0 +1,357 @@ +// Unit tests for size-diff-comment.js (RAM/Flash usage delta PR comment). +// +// Run with: node --test .github/scripts/size-diff-comment.test.js +// +// Pure-logic tests only — no filesystem/network access, matching the module +// under test. Uses Node's built-in test runner (node:test / node:assert), +// no dependencies. + +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + REPRESENTATIVE_TARGETS, + NOISE_THRESHOLD_BYTES, + diffSizeReports, + renderComment, + formatDelta, +} = require('./size-diff-comment.js'); + +// --------------------------------------------------------------------------- +// formatDelta +// --------------------------------------------------------------------------- + +test('formatDelta: positive delta gets a + sign and correct percentage', () => { + const result = formatDelta(100, 10000); + assert.equal(result, '+100 B (+1.00%)'); +}); + +test('formatDelta: negative delta gets no extra sign (native minus) and correct percentage', () => { + const result = formatDelta(-100, 10000); + assert.equal(result, '-100 B (-1.00%)'); + // Make sure there's no double-minus like "--100" or "(-+1.00%)". + assert.ok(!result.includes('--')); +}); + +test('formatDelta: zero delta does not show a + sign', () => { + const result = formatDelta(0, 10000); + assert.ok(!result.includes('+'), `expected no "+" in zero-delta output, got: ${result}`); + assert.equal(result, '±0 B (±0.00%)'); +}); + +test('formatDelta: baseBytes of 0 omits the percentage entirely (no divide-by-zero artifact)', () => { + const result = formatDelta(50, 0); + assert.equal(result, '+50 B'); + assert.ok(!result.includes('NaN')); + assert.ok(!result.includes('Infinity')); +}); + +// --------------------------------------------------------------------------- +// diffSizeReports +// --------------------------------------------------------------------------- + +test('diffSizeReports: target present in both PR and baseline with a real delta -> compared, correct deltas', () => { + const pr = { MATEKF405: { flash: 500100, ram: 60000 } }; + const base = { MATEKF405: { flash: 500000, ram: 60200 } }; + const rows = diffSizeReports(pr, base); + const row = rows.find((r) => r.target === 'MATEKF405'); + + assert.equal(row.status, 'compared'); + assert.equal(row.flash, 500100); + assert.equal(row.ram, 60000); + assert.equal(row.flashDelta, 100); + assert.equal(row.ramDelta, -200); +}); + +test('diffSizeReports: notable is gated at exactly NOISE_THRESHOLD_BYTES', () => { + const baseSizes = { flash: 100000, ram: 50000 }; + + const atThreshold = { MATEKF405: { flash: baseSizes.flash + NOISE_THRESHOLD_BYTES, ram: baseSizes.ram } }; + const belowThreshold = { MATEKF405: { flash: baseSizes.flash + NOISE_THRESHOLD_BYTES - 1, ram: baseSizes.ram } }; + const base = { MATEKF405: baseSizes }; + + const rowAtThreshold = diffSizeReports(atThreshold, base).find((r) => r.target === 'MATEKF405'); + const rowBelowThreshold = diffSizeReports(belowThreshold, base).find((r) => r.target === 'MATEKF405'); + + assert.equal(rowAtThreshold.flashDelta, NOISE_THRESHOLD_BYTES); + assert.equal(rowAtThreshold.notable, true, 'a delta of exactly NOISE_THRESHOLD_BYTES should be notable'); + + assert.equal(rowBelowThreshold.flashDelta, NOISE_THRESHOLD_BYTES - 1); + assert.equal(rowBelowThreshold.notable, false, 'a delta one byte below NOISE_THRESHOLD_BYTES should not be notable'); +}); + +test('diffSizeReports: notable also triggers from ramDelta alone, and honors negative deltas via Math.abs', () => { + const base = { MATEKF405: { flash: 100000, ram: 50000 } }; + const pr = { MATEKF405: { flash: 100000, ram: 50000 - 300 } }; // flash unchanged, ram shrank by 300 + const row = diffSizeReports(pr, base).find((r) => r.target === 'MATEKF405'); + + assert.equal(row.flashDelta, 0); + assert.equal(row.ramDelta, -300); + assert.equal(row.notable, true, 'a -300 ram delta exceeds NOISE_THRESHOLD_BYTES in magnitude'); +}); + +test('diffSizeReports: target missing from PR report but present in baseline -> missing-from-pr', () => { + const pr = {}; // MATEKF405 not built this run + const base = { MATEKF405: { flash: 100000, ram: 50000 } }; + const row = diffSizeReports(pr, base).find((r) => r.target === 'MATEKF405'); + + assert.equal(row.status, 'missing-from-pr'); + assert.equal(row.flash, undefined); + assert.equal(row.ram, undefined); +}); + +test('diffSizeReports: target missing from both PR and baseline -> not-built', () => { + const pr = {}; + const base = {}; + const row = diffSizeReports(pr, base).find((r) => r.target === 'MATEKF405'); + + assert.equal(row.status, 'not-built'); +}); + +test('diffSizeReports: baselineReport undefined but target present in PR -> no-baseline', () => { + const pr = { MATEKF405: { flash: 100000, ram: 50000 } }; + const row = diffSizeReports(pr, undefined).find((r) => r.target === 'MATEKF405'); + + assert.equal(row.status, 'no-baseline'); + assert.equal(row.flash, 100000); + assert.equal(row.ram, 50000); +}); + +test('diffSizeReports: baselineReport null (not just undefined) behaves the same as undefined', () => { + const pr = { MATEKF405: { flash: 100000, ram: 50000 } }; + const row = diffSizeReports(pr, null).find((r) => r.target === 'MATEKF405'); + + assert.equal(row.status, 'no-baseline'); +}); + +test('diffSizeReports: returns exactly one row per representative target, in order', () => { + const rows = diffSizeReports({}, {}); + assert.equal(rows.length, REPRESENTATIVE_TARGETS.length); + assert.deepEqual(rows.map((r) => r.target), REPRESENTATIVE_TARGETS); +}); + +// --------------------------------------------------------------------------- +// renderComment +// --------------------------------------------------------------------------- + +function fullReport(sizes) { + // Helper: build a { target: {flash, ram} } report for all 4 representative + // targets from a flat { target: [flash, ram] } shorthand. + const report = {}; + for (const [target, [flash, ram]] of Object.entries(sizes)) { + report[target] = { flash, ram }; + } + return report; +} + +test('renderComment: baseline present, all 4 targets compared, mix of notable/non-notable', () => { + const marker = ''; + const baselineReport = fullReport({ + MATEKF405: [500000, 60000], + MATEKF722: [510000, 61000], + MATEKF765: [520000, 62000], + MATEKH743: [530000, 63000], + }); + const prReport = fullReport({ + MATEKF405: [500300, 60000], // +300 flash -> notable + MATEKF722: [510010, 61000], // +10 flash -> not notable + MATEKF765: [519700, 62000], // -300 flash -> notable + MATEKH743: [530000, 63000], // no change -> not notable + }); + + const body = renderComment({ prReport, baselineReport, shortSha: 'abc1234', docLink: null, marker }); + + // Marker present (and first line, as GitHub comment-identification markers + // are conventionally expected to be). + assert.ok(body.includes(marker)); + assert.equal(body.split('\n')[0], marker); + + // Markdown table present. + assert.ok(body.includes('| Target | Flash Δ | RAM Δ |')); + assert.ok(body.includes('|---|---|---|')); + + // Notable rows flagged with the warning emoji, non-notable rows are not. + const lines = body.split('\n'); + const matekf405Line = lines.find((l) => l.startsWith('| MATEKF405')); + const matekf722Line = lines.find((l) => l.startsWith('| MATEKF722')); + const matekf765Line = lines.find((l) => l.startsWith('| MATEKF765')); + const matekh743Line = lines.find((l) => l.startsWith('| MATEKH743')); + + assert.ok(matekf405Line.includes('⚠️'), 'MATEKF405 (+300 flash) should be flagged notable'); + assert.ok(!matekf722Line.includes('⚠️'), 'MATEKF722 (+10 flash) should NOT be flagged notable'); + assert.ok(matekf765Line.includes('⚠️'), 'MATEKF765 (-300 flash) should be flagged notable'); + assert.ok(!matekh743Line.includes('⚠️'), 'MATEKH743 (no change) should NOT be flagged notable'); + + // No "no baseline available" note when a baseline was supplied. + assert.ok(!body.includes('No size baseline is available yet')); +}); + +test('renderComment: baselineReport falsy -> includes the "no baseline available yet" note', () => { + const prReport = fullReport({ + MATEKF405: [500000, 60000], + MATEKF722: [510000, 61000], + MATEKF765: [520000, 62000], + MATEKH743: [530000, 63000], + }); + + const body = renderComment({ + prReport, + baselineReport: undefined, + shortSha: 'def5678', + docLink: null, + marker: '', + }); + + assert.ok(body.includes('No size baseline is available yet')); +}); + +test('renderComment: docLink set -> link line present', () => { + const prReport = fullReport({ MATEKF405: [500000, 60000] }); + const baselineReport = fullReport({ MATEKF405: [499000, 60000] }); + + const body = renderComment({ + prReport, + baselineReport, + shortSha: 'abc1234', + docLink: 'https://example.com/optimization-guide', + marker: '', + }); + + assert.ok(body.includes('[RAM/flash optimization guide](https://example.com/optimization-guide)')); +}); + +test('renderComment: docLink omitted -> link line absent entirely', () => { + const prReport = fullReport({ MATEKF405: [500000, 60000] }); + const baselineReport = fullReport({ MATEKF405: [499000, 60000] }); + + const body = renderComment({ + prReport, + baselineReport, + shortSha: 'abc1234', + docLink: null, + marker: '', + }); + + assert.ok(!body.includes('optimization guide')); + assert.ok(!body.includes('See [')); +}); + +test('renderComment: docLink undefined (key omitted from options) -> link line absent entirely', () => { + const prReport = fullReport({ MATEKF405: [500000, 60000] }); + const baselineReport = fullReport({ MATEKF405: [499000, 60000] }); + + const body = renderComment({ + prReport, + baselineReport, + shortSha: 'abc1234', + marker: '', + }); + + assert.ok(!body.includes('optimization guide')); +}); + +test('renderComment: none of the 4 representative targets built -> fallback message, not an empty/broken table', () => { + // Neither PR nor baseline built any representative target -> all rows 'not-built'. + const body = renderComment({ + prReport: {}, + baselineReport: {}, + shortSha: 'abc1234', + docLink: null, + marker: '', + }); + + assert.ok( + body.includes('None of the representative targets') && body.includes('were built by this PR'), + `expected fallback message, got:\n${body}` + ); + assert.ok( + REPRESENTATIVE_TARGETS.every((t) => body.includes(t)), + 'fallback message should still name all 4 representative targets' + ); + // No markdown table header should be emitted since there's nothing to show. + assert.ok(!body.includes('| Target | Flash Δ | RAM Δ |')); +}); + +test('renderComment: result always ends with exactly one trailing newline', () => { + const body = renderComment({ + prReport: fullReport({ MATEKF405: [500000, 60000] }), + baselineReport: fullReport({ MATEKF405: [499000, 60000] }), + shortSha: 'abc1234', + docLink: null, + marker: '', + }); + + assert.ok(body.endsWith('\n')); + assert.ok(!body.endsWith('\n\n')); +}); + +// --------------------------------------------------------------------------- +// renderComment: baseline-commit header +// --------------------------------------------------------------------------- + +test('renderComment: baselineCommit supplied -> header names the baseline commit', () => { + const body = renderComment({ + prReport: fullReport({ MATEKF405: [500000, 60000] }), + baselineReport: fullReport({ MATEKF405: [499000, 60000] }), + shortSha: 'abc1234', + baselineCommit: '9e932ba', + baselineIsNearest: false, + docLink: null, + marker: '', + }); + + assert.ok(body.includes('vs. base commit `9e932ba`'), 'header should name the baseline commit'); + assert.ok(body.includes('commit `abc1234`'), 'header should still name the PR head commit'); + assert.ok(!body.includes('vs. base branch'), 'exact baseline should not use the generic "base branch" wording'); + assert.ok(!body.includes('nearest available size baseline'), 'exact baseline should not show the fallback note'); + assert.ok(!body.includes('No size baseline is available yet'), 'baseline present means no "no baseline" note'); +}); + +test('renderComment: baselineCommit + baselineIsNearest -> fallback note shown', () => { + const body = renderComment({ + prReport: fullReport({ MATEKF405: [500000, 60000] }), + baselineReport: fullReport({ MATEKF405: [499000, 60000] }), + shortSha: 'abc1234', + baselineCommit: '7f133b3', + baselineIsNearest: true, + docLink: null, + marker: '', + }); + + assert.ok(body.includes('vs. base commit `7f133b3`'), 'header should name the nearest baseline commit'); + assert.ok(body.includes('nearest available size baseline'), 'fallback note should explain the nearest-baseline choice'); +}); + +test('renderComment: baselineCommit omitted -> generic "vs. base branch" wording kept', () => { + const body = renderComment({ + prReport: fullReport({ MATEKF405: [500000, 60000] }), + baselineReport: fullReport({ MATEKF405: [499000, 60000] }), + shortSha: 'abc1234', + docLink: null, + marker: '', + }); + + assert.ok(body.includes('vs. base branch'), 'legacy callers without baselineCommit keep the old wording'); + assert.ok(!body.includes('base commit `'), 'no baseline commit rendered when none supplied'); +}); + +test('renderComment: baselineCommit supplied but no baseline report -> graceful note wins, no contradictory header', () => { + // Defensive: the workflow only sets baselineCommit when a baseline was + // found, so this combination should not occur; if it ever does, the + // header must NOT claim a base commit was compared (no "vs. base + // commit") and the "no baseline available" note must be present. + const body = renderComment({ + prReport: fullReport({ MATEKF405: [500000, 60000] }), + baselineReport: null, + shortSha: 'abc1234', + baselineCommit: '9e932ba', + docLink: null, + marker: '', + }); + + assert.ok(body.includes('No size baseline is available yet')); + assert.ok(!body.includes('vs. base commit `9e932ba`'), 'header must not name a baseline commit when no baseline exists'); + assert.ok(body.includes('vs. base branch'), 'header should fall back to the generic wording'); +}); diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 967080a66fb..7e28f7cad01 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -7,13 +7,24 @@ This directory contains automated CI/CD workflows for the INAV project. ### Build and Test #### `ci.yml` - Build Firmware -**Triggers:** Pull requests, pushes to maintenance branches +**Triggers:** Pull requests (`workflow_call` too, used by `nightly-build.yml`). +Also declares an `on: push:` trigger, but it's currently a no-op — a +`branches:` list containing only a negative pattern (`'!maintenance-8.x.x'`) +matches nothing per GitHub's own docs, confirmed empirically (zero +push-triggered runs of this workflow exist in this repo's history). Direct +push builds happen via `nightly-build.yml` instead (see below). **Purpose:** Compiles INAV firmware for all targets to verify builds succeed **Matrix:** 15 parallel build jobs for faster CI -#### `nightly-build.yml` - Nightly Builds -**Triggers:** Scheduled nightly -**Purpose:** Creates nightly development builds for testing +#### `nightly-build.yml` - "Build pre-release" +**Triggers:** `push` to `master`, `maintenance-8.x.x`, `maintenance-9.x`, +`maintenance-10.x`, `release/9.1` — **not** a schedule, despite the +filename. Invokes `ci.yml`'s jobs via `workflow_call`, then publishes a +prerelease to the companion `iNavFlight/inav-nightly` repo. +**Purpose:** Creates nightly development builds for testing, and is the +actual per-push validation + baseline-generation point for +`ci-size-report.yml` (see below) since `ci.yml`'s own push trigger doesn't +fire. ### Documentation @@ -48,6 +59,60 @@ This directory contains automated CI/CD workflows for the INAV project. ### Pull Request Helpers +#### `ci-size-report.yml` - RAM/Flash Usage Delta PR Comment +**Triggers:** `workflow_run` after "Build firmware" (`ci.yml`, PR builds) or +"Build pre-release" (`nightly-build.yml`, branch-push builds) completes +**Purpose:** Posts/updates a PR comment showing flash and RAM usage delta vs. +the PR's base branch, for 4 representative targets spanning flash/RAM size +tiers (MATEKF405, MATEKF722, MATEKF765, MATEKH743 — note MATEKF722 and +MATEKF765 are both STM32F7 parts; no AT32 target is currently covered). +Surfaces RAM/flash regressions and creep before they become an overflow, on +every PR. + +**How it works:** +1. `ci.yml` extracts a small per-target size report (`arm-none-eabi-size` + on each built `.elf`) right after each build and uploads it as an + artifact — no second build anywhere in this flow. +2. On pushes to `master`/`maintenance-9.x`/`maintenance-10.x`/`release/9.1`, + `nightly-build.yml` ("Build pre-release") invokes `ci.yml` via + `workflow_call` as part of building nightly releases — this already + produces the size report above at no extra build cost. When that + completes, `ci-size-report.yml` persists it as TWO release assets in the + companion `iNavFlight/pr-test-builds` repo: `size-baseline-` + (latest-tip pointer, kept for backward compatibility) and + `size-baseline-` (primary — lets PR comparisons key off the + exact base commit). Per-commit baselines are pruned to the newest 50 per + branch (plus a global cap) so the companion repo doesn't grow unbounded. + (`ci.yml`'s *own* `on: push:` trigger is broken — a `branches:` list + containing only a negative pattern matches nothing per GitHub's docs — + so this deliberately listens to `nightly-build.yml` instead of trying to + fix that separately; verified empirically that `ci.yml` alone has zero + push-triggered runs in this repo's history.) +3. On PR builds, it computes the PR's TRUE base commit — the merge-base of + the PR head and base ref via the compare API — fetches the per-commit + baseline for that exact commit, falling back to the nearest ancestor + commit that has one (never the branch tip, which would include unrelated + changes merged after the PR forked), diffs it against the PR's own size + report, and posts/updates a comment (marker ``) + naming the baseline commit that was used. + +**Scripts:** `.github/scripts/extract-size-report.sh` (size extraction), +`.github/scripts/merge-size-reports.sh` (merges per-shard reports), +`.github/scripts/publish-size-baseline.sh` (per-commit publish + pruning), +`.github/scripts/fetch-size-baseline.sh` (merge-base baseline resolution), +`.github/scripts/size-diff-comment.js` (pure diff + markdown rendering, +unit tested in `.github/scripts/size-diff-comment.test.js`) + +**⚠️ Every job that calls a `.github/scripts/` file MUST first run +`- uses: actions/checkout@v4` as its first step.** The runner workspace is +empty until a checkout — `bash .github/scripts/...` then fails with exit +127 and the job silently does nothing, so a missing checkout looks like a +missing baseline/artifact instead of a broken job. Repo scripts MUST NEVER +be invoked from a job that has not checked out a known branch first. + +**Uses the same `PR_BUILDS_TOKEN` secret and `workflow_run` trigger pattern +as `pr-test-builds.yml`** (secrets available even for fork PRs). + #### `pr-branch-suggestion.yml` - Branch Targeting Suggestion **Triggers:** PRs targeting master branch **Purpose:** Suggests using maintenance-9.x or maintenance-10.x instead diff --git a/.github/workflows/ci-size-report.yml b/.github/workflows/ci-size-report.yml new file mode 100644 index 00000000000..70bcde57bcd --- /dev/null +++ b/.github/workflows/ci-size-report.yml @@ -0,0 +1,309 @@ +name: CI Size Report + +# Uses workflow_run (rather than pull_request/push directly) so that +# secrets are available even for PRs from forks — same reasoning as +# pr-test-builds.yml. +# +# Listens to two different upstream workflows, for two different jobs: +# - publish-baseline listens for "Build pre-release" (nightly-build.yml), +# NOT "Build firmware" directly. ci.yml's own `on: push:` trigger is +# broken (a branches: list with only a negative pattern matches +# nothing — confirmed empirically, zero push-triggered "Build firmware" +# runs exist). "Build pre-release" is push-triggered correctly and +# already invokes ci.yml's jobs via `uses:` (workflow_call) as part of +# building nightly releases — piggybacking here costs nothing extra, +# no second build. Persists the size report as release assets in the +# companion iNavFlight/pr-test-builds repo, so PR runs never need to +# rebuild the base branch to get a comparison point. Two assets are +# written per push: size-baseline- (latest-tip pointer, kept +# for backward compatibility) and size-baseline- (primary — +# PR comparisons key off the exact base commit). Per-commit baselines +# are pruned to the newest 50 per branch (plus a global cap) so the +# companion repo doesn't grow unbounded; baselines for since-deleted +# branches aren't otherwise cleaned up (same known limitation +# pr-test-builds has for old PR releases). +# - pr-comment listens for "Build firmware" (ci.yml) directly — PR builds +# genuinely do trigger it via `pull_request`, that part isn't broken. +# Resolves the PR's TRUE base commit (merge-base of head and base ref), +# fetches the per-commit baseline for it (falling back to the nearest +# ancestor commit that has one), diffs the 4 representative targets, +# and posts/updates a PR comment showing which baseline was used. +# +# Requires the same repository secret PR_BUILDS_TOKEN as pr-test-builds.yml +# (Contents: write access to iNavFlight/pr-test-builds). +on: + workflow_run: + workflows: ["Build firmware", "Build pre-release"] + types: [completed] + +jobs: + publish-baseline: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.name == 'Build pre-release' && + github.event.workflow_run.event == 'push' + concurrency: + group: size-baseline-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + permissions: + actions: read # to download artifacts and query job conclusions from the triggering workflow run + contents: read # for the checkout step (scripts live in this repo) + steps: + # REQUIRED FIRST STEP: check out a known branch before any step calls + # a script from .github/scripts/. Repo scripts MUST NEVER be invoked + # from a job that has not checked out first: the runner workspace is + # empty until a checkout, so `bash .github/scripts/...` fails with + # exit 127 and the job silently publishes nothing. workflow_run runs + # this file from the default branch, so checkout's default ref is the + # trusted one to use. + - uses: actions/checkout@v4 + # Don't gate on github.event.workflow_run.conclusion: "Build + # pre-release" also runs a separate Release job (nightly upload to + # iNavFlight/inav-nightly) that can fail for reasons unrelated to the + # build itself (e.g. an expired NIGHTLY_TOKEN) and drags the whole + # run's conclusion to failure even when the build succeeded and + # produced the artifacts we actually need. Check the specific job + # that produces them instead. + - name: Check build job succeeded + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN_ID: ${{ github.event.workflow_run.id }} + run: | + CONCLUSION=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs" --paginate \ + --jq '.jobs[] | select(.name == "build / upload-artifacts") | .conclusion') + if [ -z "$CONCLUSION" ]; then + # Job not found at all usually means the caller/callee job names + # changed (e.g. nightly-build.yml's "build" job or ci.yml's + # "upload-artifacts" job got renamed) — that's a workflow + # structure mismatch, not an expected build failure, and is + # exactly the kind of thing that silently broke baseline + # publishing before. Warn louder than a plain failed build. + echo "::warning::build / upload-artifacts job not found in run ${RUN_ID} — job name may have changed, skipping baseline publish" + echo "proceed=false" >> "$GITHUB_OUTPUT" + elif [ "$CONCLUSION" != "success" ]; then + echo "::notice::build / upload-artifacts did not succeed (conclusion: ${CONCLUSION}), skipping baseline publish" + echo "proceed=false" >> "$GITHUB_OUTPUT" + else + echo "proceed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Download size report + if: steps.check.outputs.proceed == 'true' + uses: actions/download-artifact@v4 + with: + name: size-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download branch name + if: steps.check.outputs.proceed == 'true' + uses: actions/download-artifact@v4 + with: + name: branch-name + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish baseline + if: steps.check.outputs.proceed == 'true' + env: + GH_TOKEN: ${{ secrets.PR_BUILDS_TOKEN }} + COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + BRANCH=$(cat branch.txt) + # Publishes size-baseline- (latest-tip pointer) and + # size-baseline- (primary), then prunes old per-commit + # baselines. Never delete+recreate the release: that leaves a + # window where the release doesn't exist at all, which a concurrent + # PR's "Fetch base branch baseline" step could hit and misreport as + # "no baseline available yet". Assets are replaced in place + # (--clobber) on later pushes — the release/tag stays continuously + # resolvable. + bash .github/scripts/publish-size-baseline.sh \ + iNavFlight/pr-test-builds \ + "${BRANCH}" \ + "${COMMIT_SHA}" \ + size-report.json + + pr-comment: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.name == 'Build firmware' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + concurrency: + group: pr-size-report-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + permissions: + actions: read + issues: write + pull-requests: write + contents: read + steps: + # Checks out this workflow's own ref (the default branch — workflow_run + # always runs the workflow file from the default branch), NOT the PR's + # head. We only need our own trusted .github/scripts/ here; the PR's + # untrusted code is never checked out in this privileged context. + - uses: actions/checkout@v4 + + - name: Download PR number + uses: actions/download-artifact@v4 + with: + name: pr-number + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download base ref + uses: actions/download-artifact@v4 + with: + name: base-ref + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download PR size report + uses: actions/download-artifact@v4 + with: + name: size-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read PR number and base ref + id: pr + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + PR_NUM=$(tr -dc '0-9' < pr_number.txt) + if [ -z "$PR_NUM" ]; then + echo "::error::Invalid PR number in artifact" + exit 1 + fi + + # base_ref.txt round-trips through an artifact produced by the + # (less-trusted) PR build, so validate it against safe git-ref + # characters before it's ever used to build a shell command or + # release tag downstream — never trust artifact content blindly. + BASE_REF=$(head -n1 base_ref.txt | tr -d '\r\n') + if ! [[ "$BASE_REF" =~ ^[A-Za-z0-9._/-]{1,100}$ ]]; then + echo "::error::Invalid base ref in artifact: $BASE_REF" + exit 1 + fi + + echo "number=${PR_NUM}" >> "$GITHUB_OUTPUT" + echo "base_ref=${BASE_REF}" >> "$GITHUB_OUTPUT" + echo "short_sha=${HEAD_SHA:0:7}" >> "$GITHUB_OUTPUT" + + - name: Compute PR base commit (merge-base) + id: mergebase + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ steps.pr.outputs.base_ref }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + # The PR's TRUE base commit is the merge-base of the PR head and + # base ref — NOT the base branch's latest tip (comparing against + # the tip includes unrelated changes merged after the PR forked). + # The compare API resolves fork-PR head SHAs too (verified). + # base_ref was already regex-validated in the step above; the + # returned SHA is validated here before any use. + MERGE_BASE=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${HEAD_SHA}" \ + --jq '.merge_base_commit.sha // empty' 2>/dev/null || true) + if [[ "$MERGE_BASE" =~ ^[0-9a-f]{40}$ ]]; then + echo "merge_base=${MERGE_BASE}" >> "$GITHUB_OUTPUT" + else + echo "::warning::could not determine merge-base for ${BASE_REF}...${HEAD_SHA}" + echo "merge_base=" >> "$GITHUB_OUTPUT" + fi + + - name: Fetch size baseline for PR base commit + id: baseline + env: + GH_TOKEN: ${{ secrets.PR_BUILDS_TOKEN }} + BASE_REF: ${{ steps.pr.outputs.base_ref }} + MERGE_BASE: ${{ steps.mergebase.outputs.merge_base }} + run: | + mkdir -p baseline + if [ -n "${MERGE_BASE}" ]; then + # Exact per-commit baseline for the merge-base first, then the + # nearest ancestor commit that has one; the branch-tip baseline + # is deliberately NOT a fallback (stale-delta bug). + bash .github/scripts/fetch-size-baseline.sh \ + "${GITHUB_REPOSITORY}" \ + iNavFlight/pr-test-builds \ + "${BASE_REF}" \ + "${MERGE_BASE}" \ + baseline >> "$GITHUB_OUTPUT" + else + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check for doc on PR head commit + id: doc + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} + run: | + if gh api "repos/${HEAD_REPO}/contents/docs/development/ram-and-flash-optimization.md?ref=${HEAD_SHA}" >/dev/null 2>&1; then + echo "link=https://github.com/${HEAD_REPO}/blob/${HEAD_SHA}/docs/development/ram-and-flash-optimization.md" >> "$GITHUB_OUTPUT" + else + echo "link=" >> "$GITHUB_OUTPUT" + fi + + - name: Post or update PR comment + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + SHORT_SHA: ${{ steps.pr.outputs.short_sha }} + BASELINE_FOUND: ${{ steps.baseline.outputs.found }} + BASELINE_COMMIT: ${{ steps.baseline.outputs.baseline_commit_short }} + BASELINE_EXACT: ${{ steps.baseline.outputs.baseline_exact }} + DOC_LINK: ${{ steps.doc.outputs.link }} + with: + script: | + const fs = require('fs'); + const path = require('path'); + const { renderComment } = require( + path.join(process.env.GITHUB_WORKSPACE, '.github', 'scripts', 'size-diff-comment.js') + ); + + const prNumber = parseInt(process.env.PR_NUMBER, 10); + if (isNaN(prNumber)) throw new Error(`Invalid PR number: ${process.env.PR_NUMBER}`); + + const prReport = JSON.parse(fs.readFileSync('size-report.json', 'utf8')); + const baselineReport = process.env.BASELINE_FOUND === 'true' + ? JSON.parse(fs.readFileSync('baseline/size-report.json', 'utf8')) + : null; + + const marker = ''; + const body = renderComment({ + prReport, + baselineReport, + shortSha: process.env.SHORT_SHA, + baselineCommit: process.env.BASELINE_COMMIT || null, + baselineIsNearest: !!process.env.BASELINE_COMMIT && process.env.BASELINE_EXACT !== 'true', + docLink: process.env.DOC_LINK || null, + marker, + }); + + const comments = await github.paginate( + github.rest.issues.listComments, + { owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber } + ); + + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb48b235904..6232c17b431 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,12 +121,20 @@ jobs: key: ${{ runner.os }}-downloads-${{ hashFiles('CMakeLists.txt') }}-${{ hashFiles('**/cmake/*')}} - name: Build targets (${{ matrix.id }}) run: mkdir -p build && cd build && cmake -DWARNINGS_AS_ERRORS=ON -DCI_JOB_INDEX=${{ matrix.id }} -DCI_JOB_COUNT=${{ strategy.job-total }} -DBUILD_SUFFIX=${{ env.BUILD_SUFFIX }} -DMAIN_COMPILE_OPTIONS=-pipe -G Ninja .. && ninja -j${{ env.NUM_CORES }} ci + - name: Extract size report + run: .github/scripts/extract-size-report.sh build build/size-report.json - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: matrix-${{ env.BUILD_NAME }}.${{ matrix.id }} path: ./build/*.hex retention-days: 1 + - name: Upload size report + uses: actions/upload-artifact@v4 + with: + name: size-report-${{ matrix.id }} + path: ./build/size-report.json + retention-days: 1 build-single-target: needs: [detect] @@ -153,12 +161,20 @@ jobs: key: ${{ runner.os }}-downloads-${{ hashFiles('CMakeLists.txt') }}-${{ hashFiles('**/cmake/*')}} - name: Build targets (${{ needs.detect.outputs.target_names }}) run: mkdir -p build && cd build && cmake -DWARNINGS_AS_ERRORS=ON -DBUILD_SUFFIX=${{ env.BUILD_SUFFIX }} -DMAIN_COMPILE_OPTIONS=-pipe -G Ninja .. && ninja -j${{ env.NUM_CORES }} ${{ needs.detect.outputs.target_names }} + - name: Extract size report + run: .github/scripts/extract-size-report.sh build build/size-report.json - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: matrix-${{ env.BUILD_NAME }}.single path: ./build/*.hex retention-days: 1 + - name: Upload size report + uses: actions/upload-artifact@v4 + with: + name: size-report-single + path: ./build/size-report.json + retention-days: 1 upload-artifacts: runs-on: ubuntu-latest @@ -212,6 +228,47 @@ jobs: name: pr-number path: pr_number.txt retention-days: 1 + - name: Save base ref + if: github.event_name == 'pull_request' + env: + BASE_REF: ${{ github.base_ref }} + run: echo "$BASE_REF" > base_ref.txt + - name: Upload base ref + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: base-ref + path: base_ref.txt + retention-days: 1 + - name: Save branch name + # Covers both a direct push and being invoked via workflow_call + # (e.g. from nightly-build.yml) — github.event_name reports + # 'workflow_call' in the latter case, not 'push', even though + # github.ref_name still correctly reflects the branch either way. + if: github.event_name != 'pull_request' + env: + REF_NAME: ${{ github.ref_name }} + run: echo "$REF_NAME" > branch.txt + - name: Upload branch name + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: branch-name + path: branch.txt + retention-days: 1 + - name: Download size reports + uses: actions/download-artifact@v4 + with: + pattern: size-report-* + path: size-reports + - name: Merge size reports + run: .github/scripts/merge-size-reports.sh size-report.json size-reports/*/size-report.json + - name: Upload merged size report + uses: actions/upload-artifact@v4 + with: + name: size-report + path: size-report.json + retention-days: 1 build-SITL-Linux-arm64: runs-on: ubuntu-22.04-arm diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 51dd16650e5..9744fb85537 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -8,6 +8,8 @@ on: - master - maintenance-8.x.x - maintenance-9.x + - maintenance-10.x + - release/9.1 paths: - 'src/**' - '.github/**' diff --git a/.github/workflows/pr-test-builds.yml b/.github/workflows/pr-test-builds.yml index f969f40218c..64eccbdbc0c 100644 --- a/.github/workflows/pr-test-builds.yml +++ b/.github/workflows/pr-test-builds.yml @@ -149,3 +149,12 @@ jobs: body, }); } + + - name: Checkout code (for cleanup script) + uses: actions/checkout@v4 + + - name: Run cleanup script + env: + GH_TOKEN: ${{ secrets.PR_BUILDS_TOKEN }} + run: | + python3 .github/scripts/cleanup-old-pr-test-builds.py --older-than 14 diff --git a/docs/development/msp/inav_enums.json b/docs/development/msp/inav_enums.json index 63f3eb2941b..6c5ce2b96fe 100644 --- a/docs/development/msp/inav_enums.json +++ b/docs/development/msp/inav_enums.json @@ -849,13 +849,14 @@ "DEVHW_TOF10120_I2C": "48", "DEVHW_TERARANGER_EVO_I2C": "49", "DEVHW_MS4525": "50", - "DEVHW_DLVR": "51", - "DEVHW_M25P16": "52", - "DEVHW_W25N": "53", - "DEVHW_UG2864": "54", - "DEVHW_SDCARD": "55", - "DEVHW_IRLOCK": "56", - "DEVHW_PCF8574": "57" + "DEVHW_MS5525": "51", + "DEVHW_DLVR": "52", + "DEVHW_M25P16": "53", + "DEVHW_W25N": "54", + "DEVHW_UG2864": "55", + "DEVHW_SDCARD": "56", + "DEVHW_IRLOCK": "57", + "DEVHW_PCF8574": "58" }, "deviceFlags_e": { "_source": "inav/src/main/drivers/bus.h", @@ -1764,6 +1765,12 @@ "IBUS_MEAS_VALUE_GPS_LON2": "145", "IBUS_MEAS_VALUE_GPS": "253" }, + "icm42605Variant_e": { + "_source": "inav/src/main/drivers/accgyro/accgyro_icm42605.c", + "ICM42605_VARIANT_42605": "0", + "ICM42605_VARIANT_42686P": "1", + "ICM42605_VARIANT_42688P": "2" + }, "inputSource_e": { "_source": "inav/src/main/flight/servos.h", "INPUT_STABILIZED_ROLL": "0", @@ -2123,6 +2130,7 @@ "LSM6DXX_VAL_CTRL1_XL_LPF2": "1", "LSM6DXX_VAL_CTRL2_G_ODR6664": "10", "LSM6DXX_VAL_CTRL2_G_2000DPS": "3", + "LSM6DXX_VAL_CTRL3_C_BDU": "BIT(6)", "LSM6DXX_VAL_CTRL3_C_H_LACTIVE": "0", "LSM6DXX_VAL_CTRL3_C_PP_OD": "0", "LSM6DXX_VAL_CTRL3_C_SIM": "0", @@ -2140,7 +2148,21 @@ "LSM6DXX_VAL_CTRL7_G_HPM_G_65": "1", "LSM6DXX_VAL_CTRL7_G_HPM_G_260": "2", "LSM6DXX_VAL_CTRL7_G_HPM_G_1040": "3", - "LSM6DXX_VAL_CTRL9_XL_I3C_DISABLE": "BIT(1)" + "LSM6DXX_VAL_CTRL9_XL_I3C_DISABLE": "BIT(1)", + "LSM6DXX_VAL_CTRL1_XL_V_OPMODE_HIGH_ACCURACY": "1", + "LSM6DXX_VAL_CTRL1_XL_V_ODR_1000HZ_HAODR1": "9", + "LSM6DXX_VAL_CTRL2_G_V_OPMODE_HIGH_ACCURACY": "1", + "LSM6DXX_VAL_CTRL2_G_V_ODR_8000HZ_HAODR1": "12", + "LSM6DXX_VAL_HAODR_CFG_MODE1": "1", + "LSM6DXX_VAL_CTRL6_C_V_FS_G_2000DPS": "4", + "LSM6DXX_VAL_CTRL6_C_V_DSK320X_RESERVED_BIT3": "BIT(3)", + "LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_288HZ": "0", + "LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_215HZ": "1", + "LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_157HZ": "2", + "LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_455HZ": "3", + "LSM6DXX_VAL_CTRL7_G_V_LPF1_EN": "BIT(0)", + "LSM6DXX_VAL_CTRL8_XL_V_FS_16G": "3", + "LSM6DXX_VAL_CTRL4_C_V_DRDY_PULSED": "BIT(1)" }, "lsm6dxxRegister_e": { "_source": "inav/src/main/drivers/accgyro/accgyro_lsm6dxx.h", @@ -2172,7 +2194,8 @@ "LSM6DXX_REG_OUTY_L_A": "42", "LSM6DXX_REG_OUTY_H_A": "43", "LSM6DXX_REG_OUTZ_L_A": "44", - "LSM6DXX_REG_OUTZ_H_A": "45" + "LSM6DXX_REG_OUTZ_H_A": "45", + "LSM6DXX_REG_HAODR_CFG": "98" }, "ltm_frame_e": { "_source": "inav/src/main/telemetry/ltm.h", @@ -2335,7 +2358,8 @@ "MULTI_FUNC_3": "3", "MULTI_FUNC_4": "4", "MULTI_FUNC_5": "5", - "MULTI_FUNC_END": "6" + "MULTI_FUNC_6": "6", + "MULTI_FUNC_END": "7" }, "multiFunctionFlags_e": { "_source": "inav/src/main/fc/multifunction.h", @@ -3062,7 +3086,8 @@ "PITOT_VIRTUAL": "4", "PITOT_FAKE": "5", "PITOT_MSP": "6", - "PITOT_DLVR": "7" + "PITOT_DLVR": "7", + "PITOT_MS5525": "8" }, "pollType_e": { "_source": "inav/src/main/io/smartport_master.c", @@ -3301,8 +3326,9 @@ "_source": "inav/src/main/io/displayport_msp_osd.c", "SD_3016": "0", "HD_5018": "1", - "HD_6022": "2", - "HD_5320": "3" + "HD_3016": "2", + "HD_6022": "3", + "HD_5320": "4" }, "resourceOwner_e": { "_source": "inav/src/main/drivers/resource.h", @@ -3469,7 +3495,7 @@ "SDCARD_OPERATION_FAILURE": "3" }, "sdcardReceiveBlockStatus_e": { - "_source": "inav/src/main/drivers/sdcard/sdcard_spi.c", + "_source": "inav/src/main/drivers/sdcard/sdcard_sdio.c", "SDCARD_RECEIVE_SUCCESS": "0", "SDCARD_RECEIVE_BLOCK_IN_PROGRESS": "1", "SDCARD_RECEIVE_ERROR": "2" @@ -3691,7 +3717,12 @@ "HITL_AIRSPEED": "(1 << 6)", "HITL_EXTENDED_FLAGS": "(1 << 7)", "HITL_GPS_TIMEOUT": "(1 << 8)", - "HITL_PITOT_FAILURE": "(1 << 9)" + "HITL_PITOT_FAILURE": "(1 << 9)", + "HITL_CURRENT_SENSOR": "(1 << 10)", + "HITL_SIM_RC_INPUT": "(1 << 11)", + "HITL_RANGEFINDER": "(1 << 12)", + "HITL_FAILSAFE_TRIGGERED": "(1 << 13)", + "HITL_SITL_MODE": "(1 << 14)" }, "smartAudioVersion_e": { "_source": "inav/src/main/io/vtx_smartaudio.h", @@ -3838,7 +3869,7 @@ "THR_HI": "(2 << (2 * THROTTLE))" }, "systemState_e": { - "_source": "inav/src/main/fc/fc_init.h", + "_source": "inav/src/main/fc/fc_init.c", "SYSTEM_STATE_INITIALISING": "0", "SYSTEM_STATE_CONFIG_LOADED": "(1 << 0)", "SYSTEM_STATE_SENSORS_READY": "(1 << 1)", @@ -3850,7 +3881,8 @@ "_source": "inav/src/main/drivers/timer.h", "TCH_DMA_IDLE": "0", "TCH_DMA_READY": "1", - "TCH_DMA_ACTIVE": "2" + "TCH_DMA_ACTIVE": "2", + "TCH_DMA_CIRCULAR": "3" }, "tempSensorType_e": { "_source": "inav/src/main/sensors/temperature.h", diff --git a/docs/development/msp/inav_enums_ref.md b/docs/development/msp/inav_enums_ref.md index 802e813ab43..8ed0540edc9 100644 --- a/docs/development/msp/inav_enums_ref.md +++ b/docs/development/msp/inav_enums_ref.md @@ -150,6 +150,7 @@ - [ibusSensorType1_e](#enum-ibussensortype1_e) - [ibusSensorType_e](#enum-ibussensortype_e) - [ibusSensorValue_e](#enum-ibussensorvalue_e) +- [icm42605Variant_e](#enum-icm42605variant_e) - [inputSource_e](#enum-inputsource_e) - [itermRelax_e](#enum-itermrelax_e) - [led_pin_pwm_mode_e](#enum-led_pin_pwm_mode_e) @@ -1455,13 +1456,14 @@ | `DEVHW_TOF10120_I2C` | 48 | | | `DEVHW_TERARANGER_EVO_I2C` | 49 | | | `DEVHW_MS4525` | 50 | | -| `DEVHW_DLVR` | 51 | | -| `DEVHW_M25P16` | 52 | | -| `DEVHW_W25N` | 53 | | -| `DEVHW_UG2864` | 54 | | -| `DEVHW_SDCARD` | 55 | | -| `DEVHW_IRLOCK` | 56 | | -| `DEVHW_PCF8574` | 57 | | +| `DEVHW_MS5525` | 51 | | +| `DEVHW_DLVR` | 52 | | +| `DEVHW_M25P16` | 53 | | +| `DEVHW_W25N` | 54 | | +| `DEVHW_UG2864` | 55 | | +| `DEVHW_SDCARD` | 56 | | +| `DEVHW_IRLOCK` | 57 | | +| `DEVHW_PCF8574` | 58 | | --- ## `deviceFlags_e` @@ -2797,6 +2799,17 @@ | `IBUS_MEAS_VALUE_GPS_LON2` | 145 | | | `IBUS_MEAS_VALUE_GPS` | 253 | | +--- +## `icm42605Variant_e` + +> Source: ../../../src/main/drivers/accgyro/accgyro_icm42605.c + +| Enumerator | Value | Condition | +|---|---:|---| +| `ICM42605_VARIANT_42605` | 0 | | +| `ICM42605_VARIANT_42686P` | 1 | | +| `ICM42605_VARIANT_42688P` | 2 | | + --- ## `inputSource_e` @@ -3243,6 +3256,7 @@ | `LSM6DXX_VAL_CTRL1_XL_LPF2` | 1 | | | `LSM6DXX_VAL_CTRL2_G_ODR6664` | 10 | | | `LSM6DXX_VAL_CTRL2_G_2000DPS` | 3 | | +| `LSM6DXX_VAL_CTRL3_C_BDU` | BIT(6) | | | `LSM6DXX_VAL_CTRL3_C_H_LACTIVE` | 0 | | | `LSM6DXX_VAL_CTRL3_C_PP_OD` | 0 | | | `LSM6DXX_VAL_CTRL3_C_SIM` | 0 | | @@ -3261,6 +3275,20 @@ | `LSM6DXX_VAL_CTRL7_G_HPM_G_260` | 2 | | | `LSM6DXX_VAL_CTRL7_G_HPM_G_1040` | 3 | | | `LSM6DXX_VAL_CTRL9_XL_I3C_DISABLE` | BIT(1) | | +| `LSM6DXX_VAL_CTRL1_XL_V_OPMODE_HIGH_ACCURACY` | 1 | | +| `LSM6DXX_VAL_CTRL1_XL_V_ODR_1000HZ_HAODR1` | 9 | | +| `LSM6DXX_VAL_CTRL2_G_V_OPMODE_HIGH_ACCURACY` | 1 | | +| `LSM6DXX_VAL_CTRL2_G_V_ODR_8000HZ_HAODR1` | 12 | | +| `LSM6DXX_VAL_HAODR_CFG_MODE1` | 1 | | +| `LSM6DXX_VAL_CTRL6_C_V_FS_G_2000DPS` | 4 | | +| `LSM6DXX_VAL_CTRL6_C_V_DSK320X_RESERVED_BIT3` | BIT(3) | | +| `LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_288HZ` | 0 | | +| `LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_215HZ` | 1 | | +| `LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_157HZ` | 2 | | +| `LSM6DXX_VAL_CTRL6_C_V_LPF1_BW_455HZ` | 3 | | +| `LSM6DXX_VAL_CTRL7_G_V_LPF1_EN` | BIT(0) | | +| `LSM6DXX_VAL_CTRL8_XL_V_FS_16G` | 3 | | +| `LSM6DXX_VAL_CTRL4_C_V_DRDY_PULSED` | BIT(1) | | --- ## `lsm6dxxRegister_e` @@ -3298,6 +3326,7 @@ | `LSM6DXX_REG_OUTY_H_A` | 43 | | | `LSM6DXX_REG_OUTZ_L_A` | 44 | | | `LSM6DXX_REG_OUTZ_H_A` | 45 | | +| `LSM6DXX_REG_HAODR_CFG` | 98 | | --- ## `ltm_frame_e` @@ -3541,7 +3570,8 @@ | `MULTI_FUNC_3` | 3 | | | `MULTI_FUNC_4` | 4 | | | `MULTI_FUNC_5` | 5 | | -| `MULTI_FUNC_END` | 6 | | +| `MULTI_FUNC_6` | 6 | | +| `MULTI_FUNC_END` | 7 | | --- ## `multiFunctionFlags_e` @@ -4544,6 +4574,7 @@ | `PITOT_FAKE` | 5 | | | `PITOT_MSP` | 6 | | | `PITOT_DLVR` | 7 | | +| `PITOT_MS5525` | 8 | | --- ## `pollType_e` @@ -4829,8 +4860,9 @@ |---|---:|---| | `SD_3016` | 0 | | | `HD_5018` | 1 | | -| `HD_6022` | 2 | | -| `HD_5320` | 3 | | +| `HD_3016` | 2 | | +| `HD_6022` | 3 | | +| `HD_5320` | 4 | | --- ## `resourceOwner_e` @@ -5079,7 +5111,7 @@ --- ## `sdcardReceiveBlockStatus_e` -> Source: ../../../src/main/drivers/sdcard/sdcard_sdio.c +> Source: ../../../src/main/drivers/sdcard/sdcard_spi.c | Enumerator | Value | Condition | |---|---:|---| @@ -5090,7 +5122,7 @@ --- ## `sdcardReceiveBlockStatus_e` -> Source: ../../../src/main/drivers/sdcard/sdcard_spi.c +> Source: ../../../src/main/drivers/sdcard/sdcard_sdio.c | Enumerator | Value | Condition | |---|---:|---| @@ -5421,6 +5453,11 @@ | `HITL_EXTENDED_FLAGS` | (1 << 7) | | | `HITL_GPS_TIMEOUT` | (1 << 8) | | | `HITL_PITOT_FAILURE` | (1 << 9) | | +| `HITL_CURRENT_SENSOR` | (1 << 10) | | +| `HITL_SIM_RC_INPUT` | (1 << 11) | | +| `HITL_RANGEFINDER` | (1 << 12) | | +| `HITL_FAILSAFE_TRIGGERED` | (1 << 13) | | +| `HITL_SITL_MODE` | (1 << 14) | | --- ## `smartAudioVersion_e` @@ -5636,7 +5673,7 @@ --- ## `systemState_e` -> Source: ../../../src/main/fc/fc_init.c +> Source: ../../../src/main/fc/fc_init.h | Enumerator | Value | Condition | |---|---:|---| @@ -5650,7 +5687,7 @@ --- ## `systemState_e` -> Source: ../../../src/main/fc/fc_init.h +> Source: ../../../src/main/fc/fc_init.c | Enumerator | Value | Condition | |---|---:|---| @@ -5671,6 +5708,7 @@ | `TCH_DMA_IDLE` | 0 | | | `TCH_DMA_READY` | 1 | | | `TCH_DMA_ACTIVE` | 2 | | +| `TCH_DMA_CIRCULAR` | 3 | | --- ## `tempSensorType_e` diff --git a/docs/development/msp/msp_messages.json b/docs/development/msp/msp_messages.json index e1b128f305d..1a0c8709fe1 100644 --- a/docs/development/msp/msp_messages.json +++ b/docs/development/msp/msp_messages.json @@ -11032,6 +11032,35 @@ "notes": "CH1-CH12 (index 0-11) are protected and will return `MSP_RESULT_ERROR`. Payload size must be 2-49 bytes. Constraint: `startChannel + channelCount <= 32`. Values persist until overwritten; no timeout. Applied as a post-RX overlay in `calculateRxChannelsAndUpdateFailsafe()` after MSP RC Override but before failsafe. Does not require `USE_RX_MSP` or MSP-RC-OVERRIDE flight mode. Does not affect failsafe detection. When MSP is the primary RX provider, channels covered by `MSP_SET_RAW_RC` are automatically skipped. Channels in the `mspOverrideChannels` bitmask are skipped when MSP RC Override mode is active. Recommended to send with `MSP_FLAG_DONT_REPLY` (flags=0x01) to save bandwidth on telemetry passthrough links. 16-bit mode requires even number of data bytes and values are clamped to 750-2250us.", "description": "Bandwidth-efficient auxiliary RC channel update. Sets CH13-CH32 with configurable resolution (2/4/8/16-bit) without affecting primary flight controls. Designed for extending channel count beyond native RC link capacity via MSP passthrough." }, + "MSP2_INAV_WIND": { + "code": 8753, + "mspv": 2, + "request": null, + "reply": { + "payload": [ + { + "name": "windSpeed", + "ctype": "uint16_t", + "desc": "Estimated horizontal wind speed (`getEstimatedHorizontalWindSpeed()`). 0 if unavailable.", + "units": "cm/s" + }, + { + "name": "windAngle", + "ctype": "uint16_t", + "desc": "Estimated wind direction in degrees (0–359, 0 = North). Derived from centidegree value divided by 100. 0 if unavailable.", + "units": "degrees" + }, + { + "name": "flags", + "ctype": "uint8_t", + "desc": "Validity flags. Bit 0: wind estimate valid (`isEstimatedWindSpeedValid()`). Remaining bits reserved.", + "units": "" + } + ] + }, + "notes": "Requires `USE_WIND_ESTIMATOR`; returns zeroes when wind estimation is not compiled in or not yet valid. Check bit 0 of `flags` before using speed/angle values.", + "description": "Retrieves the estimated horizontal wind speed and direction from the internal wind estimator." + }, "MSP2_BETAFLIGHT_BIND": { "code": 12288, "mspv": 2, diff --git a/docs/development/ram-and-flash-optimization.md b/docs/development/ram-and-flash-optimization.md new file mode 100644 index 00000000000..ca5f291c95b --- /dev/null +++ b/docs/development/ram-and-flash-optimization.md @@ -0,0 +1,296 @@ +# RAM and Flash Optimization + +> **Status: draft.** Started from PR #11718; not yet linked from +> `Development.md` and not a complete guide. + +Static RAM headroom is a binding constraint on several boards (128KB-RAM F4 +targets in particular). Treat RAM and flash reduction as an active goal when +writing or reviewing code that adds static buffers or new functions, not +just something to check for overflow after the fact. + +## Buffer sizing + +### Don't reuse another subsystem's size constant + +An existing size constant exists for a specific consumer; reusing it for a +different buffer usually over-allocates. Before reusing a constant, check +what specifically drove that number. + +Example: the MSP-over-MAVLink tunnel reply buffer was sized off +`MSP_PORT_OUTBUF_SIZE` (4112 B), which exists for `MSP_DATAFLASH_READ` +(returning a full flash page in one reply). The tunnel fragments replies +into 128-byte chunks regardless of source buffer size, so it never +benefited; its own 512-byte constant cut ~3.6 KB with no functional loss. + +### Audit every writer before shrinking a shared buffer + +Shrinking a shared buffer can expose code that relied on it being larger +than its own worst case. Check every code path into the buffer, not just +the new consumer. + +Example: shrinking the tunnel buffer (4112 B → 512 B) exposed +`serializeDataflashReadReply()`, which computed its space clamp *before* +writing a 4-byte address header — an unconditional 4-byte overflow once the +buffer had no spare headroom. + +### Measure real message sizes before sizing a buffer + +Audit everything that can reach the buffer, not just the obvious category. +A narrow audit produces a confident-looking wrong answer. + +Example: a first pass on the tunnel buffer checked only MSP2 handlers +(432 B largest reply); MSP1 legacy handlers reachable through the same +buffer needed 512 B. + +### Chunk a large buffer into a circular buffer when the peripheral drains continuously + +If the peripheral consumes the buffer continuously, a circular buffer +refilled from a DMA half/full-transfer interrupt replaces one large static +array. Refill must come from a real hardware DMA interrupt — a +task-scheduler tick leaves gaps that read as a protocol reset. + +Example: the WS2812 driver buffered all 128 possible LEDs in one static +array (12,460 B) for a single DMA burst; a circular buffer holding 2 groups +of 4 LEDs (768 B), refilled from the DMA interrupt as each group finishes, +cut it >16x with no protocol change. + +### A minimum-duration constraint doesn't need buffer space + +A "hold low for at least ~50-60 us" requirement is a minimum, not a +window. Minimums are usually free — idle state already exceeds them — +while maximums aren't. Check which one you have before reserving buffer +space or adding a blocking wait. + +Example: the LED driver's 42-element reset/latch preamble became a +`micros()` compare (the line already idles low longer than the minimum); +the idle-tail became a direct write to the timer's preload-buffered +compare register. + +## Bounding work + +### Bound work to what's actually configured + +Process only the configured count when a runtime count is available, even +if the buffer stays sized for the worst case. This cuts CPU work and DMA +transfer proportionally, at zero cost to full-length users. + +Example: the LED driver processed all 128 possible slots every update; +threading `ledCounts.count` into the fill loop and DMA transfer length cut +work for short strips. + +### Budget speculative reads against the shared cache they consume + +A feature that pre-reads a shared cache (tile cache, lookup tables) must +size its speculative distance against that cache's capacity, not against +what the feature would like to know. Otherwise the speculative consumer can +evict the block the cache's primary consumer needs next, turning the cache +into a thrash loop. Compute the budget from the cache size with margin for +the primary consumer, and additionally cap by a time horizon so the scan +scales with what the aircraft can actually reach. + +Example: PR #11785's terrain lookahead caps its scan at +`(TERRAIN_GRID_BLOCK_CACHE_SIZE - 3) * 540 m` per query cycle — the `-3` +reserves the current block plus margin — and then at `groundspeed * 35 s`. +On an 8-entry cache (F765/H743) that is a 2,700 m ceiling, with the time +horizon usually binding first. The scan can never evict the block the AGL +query reads. + +### Consume an existing cache in place; add no private copy + +When a feature needs data from a cache-backed source, read through the +existing cache and mark misses for loading instead of allocating its own +shadow buffer "for cleanliness". A miss is a `return not-ready`, not a +reason to copy. + +Example: PR #11785's `terrainNavGetHeightAtLocation()` reads the #11438 +terrain grid cache directly and calls `markGridBlockNeedRead()` on a miss; +the whole terrain_nav layer holds zero tile buffers of its own. + +## Sending data + +### Send components separately instead of concatenating + +Concatenating buffers costs the sum of their sizes. If the consumer is +sequential, send the components in order and skip the intermediate buffer. + +Example: a protocol of header + large body + trailer saved 4 KB of RAM by +sending the three components in order over serial instead of assembling a +"message" buffer. + +## Functions, linkage, and headers + +### Don't default to inlining large or multi-call-site functions + +Inlining creates one copy of the function body per call site. Let the +compiler decide — it's usually better at this than manual `inline` hints — +and reach for `NOINLINE` on large functions with several call sites that +don't need to be fast-path. + +### Be wary of `static` in a header + +`static` in a header (with or without `inline`) creates a private copy in +every `.c` file that includes it — of the function *and* of any local +`static` array/struct/table inside it. The linker cannot merge +internal-linkage symbols. `inline` is not the cause; `static` is. When you +don't need internal linkage, prefer `extern`: declare the function/object +in the header, define it exactly once in a matching `.c` file. Vendored +libraries often ship a documented switch for this — check before +hand-patching generated files, which get wiped on regeneration. + +Example: `mavlink_get_msg_entry()` in +`lib/main/MAVLink/mavlink_helpers.h` declares its local +`mavlink_message_crcs[]` table (337 entries, 4,044 B on the storm32 +dialect) with `static` linkage via the `MAVLINK_HELPER` macro. Two firmware +TUs call the function, so the binary carried two copies — 8,088 B on +BLUEBERRYF405. A grep for `"static inline"` missed it because the macro +hides the `static` keyword from the call site. Defining +`MAVLINK_SEPARATE_HELPERS` plus one compiled `mavlink_helpers.c` removed +the duplication. + +Use the audit tool in +`claude/developer/scripts/static-linkage-audit/` (inav-claude tooling +repo) to scan for static-in-header symbols and estimate duplication cost. + +### `static` in a header is okay only for pure, stateless functions + +The exception to the rule above: a `static inline` function with no static +state — no file-scope statics, no local `static` arrays or tables — has +nothing to duplicate, so it costs no RAM. It is not literally free, +though: inline still multiplies copies when there are multiple callers. +Every call site the compiler inlines embeds its own copy of the body in +flash, and if the compiler cannot inline the function and more than one TU +calls it, each TU gets an out-of-line copy that the linker cannot merge. +Keep such functions small and few-call-site, or fall back to the `extern` +pattern above. + +Example: PR #11553's four `*_logic.h` files (~1,700 lines, VTOL +auto-transition) are 100% `static inline` with no top-level static state — +each inlines into its one or two callers or gets discarded, and each has a +unit test that exercises the logic without linking the surrounding +subsystem. + +## State machines + +### Extend an enum-indexed dispatch table — don't grow an if/else-if chain + +New states belong in an existing table/switch as entries, not as branches +bolted onto the dispatch site. A parallel if/else-if chain costs more +flash and adds a second place to keep state in sync. + +Example: PR #11553 added navigation FSM states as +`[NAV_STATE_...] = { ... }` entries in `navFSM[NAV_STATE_COUNT]`, and its +mixer-profile transition FSM uses a dense `switch` the compiler can turn +into a jump table. + +### Keep a feature's whole static state in ONE caller-owned struct + +When a feature's decision logic has many state fields, don't scatter them +as file-scope statics across the module — hold them in one struct that the +caller passes in/out. That makes the feature's RAM cost equal to one +visible `sizeof()`, keeps the core logic pure (unit-testable without +linking the subsystem), and makes reset = one memset-style function +instead of a dozen assignments. + +Example: PR #11785's terrain_nav_hold layer keeps its entire state machine +in `terrainNavHoldState_t` (64 B on MATEKF765, verified via nm) plus one +small output struct — the whole feature's static RAM is ~100 B, not +scattered buffers. `terrain_nav_hold_core.c` is pure logic with zero +file-scope state; `terrain_nav_hold.c` owns the single state struct. + +## Duplication: state vs code + +### Distinguish necessary state duplication from avoidable code duplication + +A large duplicated struct may be necessary state — a second copy evolving +in parallel across time — while near-identical duplicated functions are +usually avoidable code, one parameterization away from being unified. +Trace which pattern a duplication finding is before accepting it as an +inherent cost. + +Example (necessary state): PR #11553's +`autoTransitionTargetPidState[FLIGHT_DYNAMICS_INDEX_COUNT]` is a full +second copy of `pidState_t` (3,072 B on MATEKF405, ~95% of the PR's net +RAM+CCM growth) — a bumpless handoff needs the *target* controller's +filter/integrator state evolving continuously in parallel, not a single +output value computed once. + +Example (avoidable code): the ~10-11 near-line-for-line duplicate +functions in `pid.c` (`applyAutoTransitionTargetDBoost` vs `applyDBoost`, ++6,632 B) exist only because the live PID functions read tuning values +from file-scope globals instead of taking a `pidProfile_t*` — the new +functions already take the profile explicitly, so the duplication is a +refactor away from being unified. + +## Measuring size + +### Sum every RAM-like region when reconciling against CI + +CI's single "RAM Δ" figure on F4/F7/H7 targets can be `RAM` + `CCM`/`DTCM` +summed together, not the `RAM:` linker region alone. Check which regions +the figure spans before suspecting the build or the measurement. + +Example: on PR #11553/MATEKF405, `RAM:` alone grew only 608 B against a +reported CI delta of +3,740 B — the gap was CCM growing another 3,128 B +and CI counting both regions as one number. + +### In an LTO-merged symbol, check every function folded into it + +LTO merges several unrelated functions into one symbol, so an +`nm --size-sort` diff attributes their combined growth to whichever name +survives — usually the caller, not the function that actually grew. Don't +attribute a merged symbol's growth to the most-recently-reviewed nearby +change; check every function folded into the blob. + +Example: a +5,168 B growth in `programmingFrameworkUpdateTask` was +initially guessed to be `logic_condition.c`'s new operands; that symbol +actually folds `programmingPidUpdateTask`, `outputProfileUpdateTask`, and +`logicConditionUpdateTask`, and the growth was entirely +`outputProfileUpdateTask()` in `mixer_profile.c`. + +### Isolate one function's real cost with NOINLINE at both commits + +To measure one function's true standalone cost inside an LTO-merged blob, +add `NOINLINE` to just that function in both the base and HEAD builds so it +gets its own `nm` entry at both points. **Caveat:** `NOINLINE` is +`__attribute__((noinline))` only under `USE_ITCM_RAM` (F7/H7 targets) — +it's a no-op on F4, so run this kind of isolation on an F7/H7 target or +the "before" and "after" builds will look identical for the wrong reason. + +Example: `outputProfileUpdateTask()` measured 148 B → 4,708 B once pulled +out of the merged symbol. + +## Hardware width pitfalls + +### Verify register-width assumptions on real hardware + +A DMA transfer's element width must match the target register width. This +class of bug is invisible to the compiler and to code review — confirm on +real hardware with a scope. + +Example: the LED strip DMA buffer used `uint8_t` elements (the theoretical +minimum for a duty value); TIM3/TIM4's CCR is a 16-bit register, so the +byte-sized DMA writes only updated its low byte and the output pin stuck +high. `uint16_t`, matching the register width, fixed it. + +### Use the codebase's DMA-safe width type, not a narrower derivation + +When the codebase defines a type for "the width DMA needs to write this +register safely," use it instead of deriving a narrower one from a single +datasheet — a width verified on one board doesn't prove it on every board, +because different MCU families route through different code branches. + +Example: `timerDMASafeType_t` is `uint32_t` on every F4/F7/H7/AT32 +`timer_def_*.h`, because DMA writes to a timer CCR must be word-width +across the whole family. A `uint16_t` fix compiled cleanly on H7 but +routed through a previously-unreached branch containing an unrelated +copy-paste bug that silently downgraded the write to a single byte. + +## Related + +- Full strategy pattern library (more entries, other PRs): + `claude/projects/active/ram-reduction-program/ram-flash-strategies.md` in + the `inav-claude` tooling repo (not part of this firmware repo). +- Tracked follow-up: `document-ram-flash-optimization-practices` project — + expands this into a full guide, links it from `Development.md`, and + updates review checklists to treat RAM/flash reduction as an active + review category. diff --git a/src/main/drivers/accgyro/accgyro_icm42605.c b/src/main/drivers/accgyro/accgyro_icm42605.c index b6cd029f9e2..a0538aa3206 100644 --- a/src/main/drivers/accgyro/accgyro_icm42605.c +++ b/src/main/drivers/accgyro/accgyro_icm42605.c @@ -288,13 +288,12 @@ static void icm42605AccAndGyroInit(gyroDev_t *gyro) static bool icm42605DeviceDetect(busDevice_t * dev) { uint8_t tmp; - uint8_t attemptsRemaining = 5; busSetSpeed(dev, BUS_SPEED_INITIALIZATION); busWrite(dev, ICM42605_RA_PWR_MGMT0, 0x00); - do { + for (int attempt = 0; attempt < 5; attempt++) { delay(150); busRead(dev, MPU_RA_WHO_AM_I, &tmp); @@ -315,7 +314,7 @@ static bool icm42605DeviceDetect(busDevice_t * dev) // Retry detection break; } - } while (attemptsRemaining--); + } return false; } diff --git a/src/main/fc/fc_msp.c b/src/main/fc/fc_msp.c index 3ec3f89f3f0..961d1737c95 100644 --- a/src/main/fc/fc_msp.c +++ b/src/main/fc/fc_msp.c @@ -133,6 +133,9 @@ #include "sensors/opflow.h" #include "sensors/temperature.h" #include "sensors/esc_sensor.h" +#ifdef USE_WIND_ESTIMATOR +#include "flight/wind_estimator.h" +#endif #include "telemetry/telemetry.h" @@ -1597,6 +1600,27 @@ static bool mspFcProcessOutCommand(uint16_t cmdMSP, sbuf_t *dst, mspPostProcessF #endif break; + case MSP2_INAV_WIND: +#ifdef USE_WIND_ESTIMATOR + { + uint16_t windAngle = 0; + uint16_t windSpeed = 0; + uint8_t windFlags = 0; + if (isEstimatedWindSpeedValid()) { + windSpeed = (uint16_t)getEstimatedHorizontalWindSpeed(&windAngle); + windFlags = 1; + } + sbufWriteU16(dst, windSpeed); + sbufWriteU16(dst, windAngle / 100); + sbufWriteU8(dst, windFlags); + } +#else + sbufWriteU16(dst, 0); + sbufWriteU16(dst, 0); + sbufWriteU8(dst, 0); +#endif + break; + case MSP2_INAV_MIXER: sbufWriteU8(dst, mixerConfig()->motorDirectionInverted); sbufWriteU8(dst, 0); diff --git a/src/main/flight/servos.h b/src/main/flight/servos.h index 3f8ebf1b4e8..07a337a6d56 100644 --- a/src/main/flight/servos.h +++ b/src/main/flight/servos.h @@ -178,7 +178,6 @@ typedef struct servoConfig_s { uint8_t servo_protocol; // See servoProtocolType_e uint8_t tri_unarmed_servo; // send tail servo correction pulses even when unarmed uint8_t servo_autotrim_rotation_limit; // Max rotation for servo midpoints to be updated - uint8_t servo_autotrim_iterm_threshold; // How much of the Iterm is carried over to the servo midpoints on each update } servoConfig_t; PG_DECLARE(servoConfig_t, servoConfig); diff --git a/src/main/io/displayport_msp_osd.c b/src/main/io/displayport_msp_osd.c index 6b674a300ec..50e8cf3f5a6 100644 --- a/src/main/io/displayport_msp_osd.c +++ b/src/main/io/displayport_msp_osd.c @@ -61,11 +61,15 @@ #define FONT_VERSION 3 +// Values are wire-protocol constants sent as a raw byte over MSP DisplayPort +// (MSP_DP_OPTIONS) and must stay fixed — third-party OSD firmware (e.g. +// WTFOS) hardcodes these ordinals, so don't let enum edits renumber them. typedef enum { // defines are from hdzero code - SD_3016, - HD_5018, - HD_6022, // added to support DJI wtfos 60x22 grid - HD_5320 // added to support Avatar and BetaflightHD + SD_3016 = 0, + HD_5018 = 1, + HD_3016 = 2, // unused by INAV; reserved to hold hdzero's canonical ordinal 2 + HD_6022 = 3, // DJI wtfos 60x22 grid + HD_5320 = 4 // Avatar and BetaflightHD } resolutionType_e; #define DRAW_FREQ_DENOM 4 // 60Hz diff --git a/src/main/msp/msp_protocol_v2_inav.h b/src/main/msp/msp_protocol_v2_inav.h index e50115d99ed..b38cc0e38f9 100755 --- a/src/main/msp/msp_protocol_v2_inav.h +++ b/src/main/msp/msp_protocol_v2_inav.h @@ -132,4 +132,6 @@ #define MSP2_INAV_SET_WP_INDEX 0x2221 //in message jump to waypoint N during active WP mission; payload: U8 wp_index (0-based, relative to mission start) #define MSP2_INAV_SET_CRUISE_HEADING 0x2223 //in message set heading while in Cruise/Course Hold mode; payload: I32 heading_centidegrees (0-35999) -#define MSP2_INAV_SET_AUX_RC 0x2230 \ No newline at end of file +#define MSP2_INAV_SET_AUX_RC 0x2230 + +#define MSP2_INAV_WIND 0x2231 diff --git a/src/main/target/ACCTON_GFF4/CMakeLists.txt b/src/main/target/ACCTON_GFF4/CMakeLists.txt new file mode 100644 index 00000000000..6f671454546 --- /dev/null +++ b/src/main/target/ACCTON_GFF4/CMakeLists.txt @@ -0,0 +1 @@ +target_stm32f405xg(ACCTON_GFF4) diff --git a/src/main/target/ACCTON_GFF4/config.c b/src/main/target/ACCTON_GFF4/config.c new file mode 100644 index 00000000000..4c9b7ca49fa --- /dev/null +++ b/src/main/target/ACCTON_GFF4/config.c @@ -0,0 +1,33 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include + +#include "platform.h" + +#include "fc/fc_msp_box.h" + +#include "io/piniobox.h" +#include "io/serial.h" + +void targetConfiguration(void) +{ + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART1)].functionMask = FUNCTION_ESCSERIAL; + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART3)].functionMask = FUNCTION_GPS; + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART4)].functionMask = FUNCTION_RX_SERIAL; + pinioBoxConfigMutable()->permanentId[0] = BOX_PERMANENT_ID_USER1; +} diff --git a/src/main/target/ACCTON_GFF4/target.c b/src/main/target/ACCTON_GFF4/target.c new file mode 100644 index 00000000000..e9075e2c376 --- /dev/null +++ b/src/main/target/ACCTON_GFF4/target.c @@ -0,0 +1,41 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include + +#include "platform.h" + +#include "drivers/bus.h" +#include "drivers/io.h" +#include "drivers/pwm_mapping.h" +#include "drivers/timer.h" + +timerHardware_t timerHardware[] = { + DEF_TIM(TIM8, CH1, PC6, TIM_USE_OUTPUT_AUTO, 0, 1), // M1 DMA2_S2_Ch7 + DEF_TIM(TIM8, CH2, PC7, TIM_USE_OUTPUT_AUTO, 0, 1), // M2 DMA2_S3_Ch7 + DEF_TIM(TIM8, CH3, PC8, TIM_USE_OUTPUT_AUTO, 0, 1), // M3 DMA2_S4_Ch7 + DEF_TIM(TIM8, CH4, PC9, TIM_USE_OUTPUT_AUTO, 0, 0), // M4 DMA2_S7_Ch7 + + DEF_TIM(TIM3, CH1, PB4, TIM_USE_OUTPUT_AUTO, 0, 0), // M5 DMA1_S4_Ch5 + DEF_TIM(TIM3, CH2, PB5, TIM_USE_OUTPUT_AUTO, 0, 0), // M6 DMA1_S5_Ch5 + DEF_TIM(TIM3, CH3, PB0, TIM_USE_OUTPUT_AUTO, 0, 0), // M7 DMA1_S7_Ch5 + DEF_TIM(TIM3, CH4, PB1, TIM_USE_OUTPUT_AUTO, 0, 0), // M8 DMA1_S2_Ch5 + + DEF_TIM(TIM1, CH1, PA8, TIM_USE_LED, 0, 1), // LED strip DMA2_S1_Ch6 +}; + +const int timerHardwareCount = sizeof(timerHardware) / sizeof(timerHardware[0]); diff --git a/src/main/target/ACCTON_GFF4/target.h b/src/main/target/ACCTON_GFF4/target.h new file mode 100644 index 00000000000..c0553891804 --- /dev/null +++ b/src/main/target/ACCTON_GFF4/target.h @@ -0,0 +1,149 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#pragma once + +#define USE_TARGET_CONFIG + +#define TARGET_BOARD_IDENTIFIER "G-FF4" +#define USBD_PRODUCT_STRING "ACCT" + +// ******** Board LEDs ********************** +#define LED0 PB3 +#define LED1 PC4 + +// ******* Beeper *********** +#define BEEPER PB15 +#define BEEPER_INVERTED + +// ******* SPI ******** +#define USE_SPI + +#define USE_SPI_DEVICE_1 +#define SPI1_NSS_PIN PA4 +#define SPI1_SCK_PIN PA5 +#define SPI1_MISO_PIN PA6 +#define SPI1_MOSI_PIN PA7 + +#define USE_SPI_DEVICE_2 +#define SPI2_NSS_PIN PB12 +#define SPI2_SCK_PIN PB13 +#define SPI2_MISO_PIN PC2 +#define SPI2_MOSI_PIN PC3 + +#define USE_SPI_DEVICE_3 +#define SPI3_NSS_PIN PA15 +#define SPI3_SCK_PIN PC10 +#define SPI3_MISO_PIN PC11 +#define SPI3_MOSI_PIN PC12 + +// ************ IMU ****************************** +#define USE_IMU_ICM42605 +#define IMU_ICM42605_ALIGN CW0_DEG +#define ICM42605_SPI_BUS BUS_SPI1 +#define ICM42605_CS_PIN PA4 +#define ICM42605_EXTI_PIN PC15 + +// ************** OSD **************************** +#define USE_MAX7456 +#define MAX7456_SPI_BUS BUS_SPI2 +#define MAX7456_CS_PIN PB12 + +// *************** I2C *************************** +#define USE_I2C + +#define USE_I2C_DEVICE_1 +#define I2C1_SCL PB6 +#define I2C1_SDA PB7 + +#define USE_BARO +#define BARO_I2C_BUS BUS_I2C1 +#define USE_BARO_DPS310 +#define DPS310_I2C_ADDR (0x77) + +#define USE_MAG +#define MAG_I2C_BUS BUS_I2C1 +#define USE_MAG_IST8310 + +// *************** Serial ports ******************* +#define USE_VCP + +#define USE_UART1 +#define UART1_TX_PIN PA9 +#define UART1_RX_PIN PA10 + +#define USE_UART2 +#define UART2_TX_PIN PA2 +#define UART2_RX_PIN PA3 + +#define USE_UART3 +#define UART3_TX_PIN PB10 +#define UART3_RX_PIN PB11 + +#define USE_UART4 +#define UART4_TX_PIN PA0 +#define UART4_RX_PIN PA1 + +#define USE_UART5 +#define UART5_RX_PIN PD2 +#define UART5_TX_PIN NONE + +#define SERIAL_PORT_COUNT 6 + +#define DEFAULT_RX_TYPE RX_TYPE_SERIAL +#define SERIALRX_UART SERIAL_PORT_USART4 +#define SERIALRX_PROVIDER SERIALRX_CRSF + +// *************** ADC **************************** +#define USE_ADC +#define ADC_INSTANCE ADC1 +#define ADC_CHANNEL_1_PIN PC0 +#define ADC_CHANNEL_2_PIN PC1 + +#define VBAT_ADC_CHANNEL ADC_CHN_1 +#define CURRENT_METER_ADC_CHANNEL ADC_CHN_2 + +// *************** LED strip ********************** +#define USE_LED_STRIP +#define WS2811_PIN PA8 + +// ******* FLASH ******** +#define USE_FLASHFS +#define USE_FLASH_W25N01G +#define W25N01G_SPI_BUS BUS_SPI3 +#define W25N01G_CS_PIN PA15 +#define ENABLE_BLACKBOX_LOGGING_ON_SPIFLASH_BY_DEFAULT + +// ******* FEATURES ******** +#define TARGET_IO_PORTA (0xffff & ~(BIT(13) | BIT(14))) +#define TARGET_IO_PORTB 0xffff +#define TARGET_IO_PORTC 0xffff +#define TARGET_IO_PORTD 0xffff + +#define MAX_PWM_OUTPUT_PORTS 8 + +#define DEFAULT_FEATURES (FEATURE_OSD | FEATURE_TELEMETRY | FEATURE_CURRENT_METER | FEATURE_VBAT | FEATURE_TX_PROF_SEL | FEATURE_BLACKBOX) + +// ESC-related features +#define USE_DSHOT +#define USE_ESC_SENSOR +#define USE_SERIAL_4WAY_BLHELI_INTERFACE + +// *************** VTX ************ +#define USE_PINIO +#define USE_PINIOBOX +#define PINIO1_PIN PB2 // 9V_EN Switch diff --git a/src/main/target/AOCODARCF722AIO/target.c b/src/main/target/AOCODARCF722AIO/target.c index 038b4121b48..84ccbfd6708 100644 --- a/src/main/target/AOCODARCF722AIO/target.c +++ b/src/main/target/AOCODARCF722AIO/target.c @@ -25,6 +25,8 @@ #include "drivers/sensor.h" BUSDEV_REGISTER_SPI_TAG(busdev_mpu6500, DEVHW_MPU6500, MPU6500_SPI_BUS, MPU6500_CS_PIN, NONE, 0, DEVFLAGS_NONE, IMU_MPU6500_ALIGN); +BUSDEV_REGISTER_SPI_TAG(busdev_mpu6000, DEVHW_MPU6000, MPU6000_SPI_BUS, MPU6000_CS_PIN, NONE, 0, DEVFLAGS_NONE, IMU_MPU6000_ALIGN); +BUSDEV_REGISTER_SPI_TAG(busdev_icm42605, DEVHW_ICM42605, ICM42605_SPI_BUS, ICM42605_CS_PIN, NONE, 0, DEVFLAGS_NONE, IMU_ICM42605_ALIGN); timerHardware_t timerHardware[] = { // DEF_TIM(TIM9, CH2, PA3, TIM_USE_PPM, 0, 0), // PPM diff --git a/src/main/target/ATOMRCF405MINI/CMakeLists.txt b/src/main/target/ATOMRCF405MINI/CMakeLists.txt new file mode 100644 index 00000000000..8568fed4c79 --- /dev/null +++ b/src/main/target/ATOMRCF405MINI/CMakeLists.txt @@ -0,0 +1 @@ +target_stm32f405xg(ATOMRCF405MINI) diff --git a/src/main/target/ATOMRCF405MINI/target.c b/src/main/target/ATOMRCF405MINI/target.c new file mode 100644 index 00000000000..7ff7ac3c39d --- /dev/null +++ b/src/main/target/ATOMRCF405MINI/target.c @@ -0,0 +1,49 @@ +/* + * This file is part of INAV Project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Alternatively, the contents of this file may be used under the terms + * of the GNU General Public License Version 3, as described below: + * + * This file is free software: you may copy, redistribute and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +#include + +#include "platform.h" + +#include "drivers/bus.h" +#include "drivers/io.h" +#include "drivers/pwm_mapping.h" +#include "drivers/timer.h" +#include "drivers/pinio.h" +#include "drivers/sensor.h" + +timerHardware_t timerHardware[] = { + DEF_TIM(TIM8, CH3, PC8, TIM_USE_OUTPUT_AUTO, 0, 0), // S1 + DEF_TIM(TIM8, CH4, PC9, TIM_USE_OUTPUT_AUTO, 0, 0), // S2 + DEF_TIM(TIM1, CH1, PA8, TIM_USE_OUTPUT_AUTO, 0, 0), // S3 + DEF_TIM(TIM1, CH3N, PB1, TIM_USE_OUTPUT_AUTO, 0, 0), // S4 + DEF_TIM(TIM2, CH1, PA15, TIM_USE_OUTPUT_AUTO, 0, 0), // S5 + DEF_TIM(TIM2, CH3, PB10, TIM_USE_OUTPUT_AUTO, 0, 0), // S6 + DEF_TIM(TIM2, CH4, PB11, TIM_USE_OUTPUT_AUTO, 0, 0), // S7 + DEF_TIM(TIM3, CH3, PB0, TIM_USE_OUTPUT_AUTO, 0, 0), // S8 + + DEF_TIM(TIM4, CH2, PB7, TIM_USE_LED, 0, 0), // LED_STRIP +}; + +const int timerHardwareCount = sizeof(timerHardware) / sizeof(timerHardware[0]); diff --git a/src/main/target/ATOMRCF405MINI/target.h b/src/main/target/ATOMRCF405MINI/target.h new file mode 100644 index 00000000000..d55864a4c3b --- /dev/null +++ b/src/main/target/ATOMRCF405MINI/target.h @@ -0,0 +1,195 @@ +/* + * This file is part of INAV Project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Alternatively, the contents of this file may be used under the terms + * of the GNU General Public License Version 3, as described below: + * + * This file is free software: you may copy, redistribute and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * This file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +#pragma once +#define TARGET_BOARD_IDENTIFIER "AT4M" + +#define USBD_PRODUCT_STRING "ATOMRCF405MINI" + +#define LED0 PA13 +#define LED1 PA14 + +#define BEEPER PC5 +#define BEEPER_INVERTED + +/* + * SPI defines + */ +#define USE_SPI + +#define USE_SPI_DEVICE_1 +#define SPI1_SCK_PIN PA5 +#define SPI1_MISO_PIN PA6 +#define SPI1_MOSI_PIN PA7 + +#define USE_SPI_DEVICE_2 +#define SPI2_SCK_PIN PB13 +#define SPI2_MISO_PIN PB14 +#define SPI2_MOSI_PIN PB15 + +#define USE_SPI_DEVICE_3 +#define SPI3_SCK_PIN PB3 +#define SPI3_MISO_PIN PB4 +#define SPI3_MOSI_PIN PB5 + +/* + * I2C defines + */ +#define USE_I2C +#define DEFAULT_I2C_BUS BUS_I2C1 + +#define USE_I2C_DEVICE_1 +#define I2C1_SCL PB8 +#define I2C1_SDA PB9 + +//BMI270 +#define USE_IMU_BMI270 +#define IMU_BMI270_ALIGN CW180_DEG +#define BMI270_SPI_BUS BUS_SPI1 +#define BMI270_CS_PIN PA4 + +/* + * Magnetometer + */ +#define USE_MAG +#define MAG_I2C_BUS DEFAULT_I2C_BUS +#define USE_MAG_ALL + +/* + * Barometer + */ +#define USE_BARO +#define BARO_I2C_BUS DEFAULT_I2C_BUS +#define USE_BARO_BMP280 +#define USE_BARO_MS5611 +#define USE_BARO_BMP085 +#define USE_BARO_DPS310 +#define USE_BARO_SPL06 + +/* + * Serial ports + */ +#define USE_VCP +#define USE_UART1 +#define USE_UART2 +#define USE_UART3 +#define USE_UART4 +#define USE_UART5 +#define USE_UART6 + +#define UART1_TX_PIN PA9 +#define UART1_RX_PIN PA10 + +#define UART2_TX_PIN PA2 +#define UART2_RX_PIN PA3 + +#define UART3_TX_PIN PC10 +#define UART3_RX_PIN PC11 + +#define UART4_TX_PIN PA0 +#define UART4_RX_PIN PA1 + +#define UART5_TX_PIN PC12 +#define UART5_RX_PIN PD2 + +#define UART6_TX_PIN PC6 +#define UART6_RX_PIN PC7 + +#define SERIAL_PORT_COUNT 7 + +/* + * ADC + */ +#define USE_ADC +#define ADC_INSTANCE ADC1 +#define ADC1_DMA_STREAM DMA2_Stream0 + +#define ADC_CHANNEL_1_PIN PC0 +#define ADC_CHANNEL_2_PIN PC1 +#define ADC_CHANNEL_3_PIN PC2 + +#define VBAT_ADC_CHANNEL ADC_CHN_3 +#define CURRENT_METER_ADC_CHANNEL ADC_CHN_2 +#define RSSI_ADC_CHANNEL ADC_CHN_1 + +#define CURRENT_METER_SCALE 320 + +/* + * OSD + */ +#define USE_MAX7456 +#define MAX7456_SPI_BUS BUS_SPI2 +#define MAX7456_CS_PIN PB12 + +/* + * SD Card + */ +/* +#define USE_SDCARD +#define USE_SDCARD_SPI +#define SDCARD_SPI_BUS BUS_SPI3 +#define SDCARD_CS_PIN PB6 +#define ENABLE_BLACKBOX_LOGGING_ON_SDCARD_BY_DEFAULT*/ + +/* start kijan Flash chip usage */ +#define USE_FLASHFS +#define USE_FLASH_M25P16 +#define M25P16_SPI_BUS BUS_SPI3 +#define M25P16_CS_PIN PB6 +#define ENABLE_BLACKBOX_LOGGING_ON_SPIFLASH_BY_DEFAULT +/* end kijan*/ + +/* + * LED Strip + */ +#define USE_LED_STRIP +#define WS2811_PIN PB7 + +/* + * Other configs + */ +#define DEFAULT_FEATURES (FEATURE_TX_PROF_SEL | FEATURE_BLACKBOX | FEATURE_TELEMETRY | FEATURE_OSD | FEATURE_CURRENT_METER | FEATURE_VBAT ) +#define DEFAULT_RX_TYPE RX_TYPE_SERIAL +#define SERIALRX_PROVIDER SERIALRX_SBUS +#define SERIALRX_UART SERIAL_PORT_USART2 + +#define USE_RANGEFINDER +#define USE_RANGEFINDER_MSP +#define USE_OPFLOW +#define USE_OPFLOW_MSP + +#define RANGEFINDER_I2C_BUS DEFAULT_I2C_BUS +#define TEMPERATURE_I2C_BUS DEFAULT_I2C_BUS +#define PITOT_I2C_BUS DEFAULT_I2C_BUS + +#define USE_DSHOT +#define USE_ESC_SENSOR +#define USE_SERIAL_4WAY_BLHELI_INTERFACE + +#define MAX_PWM_OUTPUT_PORTS 8 + +#define TARGET_IO_PORTA 0xffff +#define TARGET_IO_PORTB 0xffff +#define TARGET_IO_PORTC 0xffff +#define TARGET_IO_PORTD (BIT(2)) diff --git a/src/main/target/ORBITH743v2/CMakeLists.txt b/src/main/target/ORBITH743v2/CMakeLists.txt new file mode 100644 index 00000000000..d33b7c6d1c3 --- /dev/null +++ b/src/main/target/ORBITH743v2/CMakeLists.txt @@ -0,0 +1 @@ +target_stm32h743xi(ORBITH743v2) diff --git a/src/main/target/ORBITH743v2/config.c b/src/main/target/ORBITH743v2/config.c new file mode 100644 index 00000000000..80de8a800ec --- /dev/null +++ b/src/main/target/ORBITH743v2/config.c @@ -0,0 +1,51 @@ +/* + * This file is part of Cleanflight. + * + * Cleanflight is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Cleanflight is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Cleanflight. If not, see . + */ + +#include +#include +#include +#include "fc/fc_msp_box.h" +#include "io/piniobox.h" +#include "io/serial.h" +#include "common/axis.h" +#include "config/config_master.h" +#include "config/feature.h" +#include "drivers/sensor.h" +#include "drivers/pwm_esc_detect.h" +#include "drivers/pwm_output.h" +#include "drivers/pwm_mapping.h" +#include "drivers/serial.h" +#include "fc/rc_controls.h" +#include "flight/failsafe.h" +#include "flight/mixer.h" +#include "flight/pid.h" +#include "rx/rx.h" +#include "io/serial.h" +#include "sensors/battery.h" +#include "sensors/sensors.h" +#include "telemetry/telemetry.h" + + +void targetConfiguration(void) +{ + pinioBoxConfigMutable()->permanentId[0] = BOX_PERMANENT_ID_USER1; + pinioBoxConfigMutable()->permanentId[1] = BOX_PERMANENT_ID_USER2; + + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART1)].functionMask = FUNCTION_ESCSERIAL; + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART6)].functionMask = FUNCTION_GPS; + serialConfigMutable()->portConfigs[findSerialPortIndexByIdentifier(SERIAL_PORT_USART5)].functionMask = FUNCTION_RX_SERIAL; +} diff --git a/src/main/target/ORBITH743v2/target.c b/src/main/target/ORBITH743v2/target.c new file mode 100644 index 00000000000..1f99c809da7 --- /dev/null +++ b/src/main/target/ORBITH743v2/target.c @@ -0,0 +1,55 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include + +#include "platform.h" + +#include "drivers/bus.h" +#include "drivers/io.h" +#include "drivers/pwm_mapping.h" +#include "drivers/timer.h" +#include "drivers/pinio.h" +#include "drivers/sensor.h" + +BUSDEV_REGISTER_SPI_TAG(busdev_lsm6dxx, DEVHW_LSM6D, LSM6DXX_SPI_BUS, LSM6DXX_CS_PIN, NONE, 0, DEVFLAGS_NONE, IMU_LSM6DXX_ALIGN); +BUSDEV_REGISTER_SPI_TAG(busdev_icm42688_1, DEVHW_ICM42605, ICM42688_SPI_BUS_1, ICM42688_CS_PIN_1, NONE, 0, DEVFLAGS_NONE, IMU_ICM42688_ALIGN_1); +BUSDEV_REGISTER_SPI_TAG(busdev_icm40609, DEVHW_ICM42605, ICM40609_SPI_BUS, ICM40609_CS_PIN, NONE, 0, DEVFLAGS_NONE, IMU_ICM40609_ALIGN); + + +timerHardware_t timerHardware[] = { + DEF_TIM(TIM5, CH1, PA0, TIM_USE_OUTPUT_AUTO, 0, 0), // S1 + DEF_TIM(TIM5, CH2, PA1, TIM_USE_OUTPUT_AUTO, 0, 1), // S2 + DEF_TIM(TIM5, CH3, PA2, TIM_USE_OUTPUT_AUTO, 0, 2), // S3 + DEF_TIM(TIM5, CH4, PA3, TIM_USE_OUTPUT_AUTO, 0, 3), // S4 + + DEF_TIM(TIM3, CH3, PB0, TIM_USE_OUTPUT_AUTO, 0, 4), // S5 + DEF_TIM(TIM3, CH4, PB1, TIM_USE_OUTPUT_AUTO, 0, 5), // S6 + DEF_TIM(TIM3, CH1, PB4, TIM_USE_OUTPUT_AUTO, 0, 6), // S7 + DEF_TIM(TIM3, CH2, PB5, TIM_USE_OUTPUT_AUTO, 0, 7), // S8 + + DEF_TIM(TIM2, CH1, PA15, TIM_USE_OUTPUT_AUTO, 0, 0), // S9 + DEF_TIM(TIM2, CH2, PB3, TIM_USE_OUTPUT_AUTO, 0, 0), // S10 DMA_NONE + DEF_TIM(TIM4, CH1, PD12, TIM_USE_OUTPUT_AUTO, 0, 0), // S11 + DEF_TIM(TIM4, CH2, PD13, TIM_USE_OUTPUT_AUTO, 0, 0), // S12 DMA_NONE + + DEF_TIM(TIM1, CH1, PA8, TIM_USE_LED, 0, 14), // LED_2812 + DEF_TIM(TIM12, CH2, PB15, TIM_USE_PPM, 0, 0), // PPM + +}; + +const int timerHardwareCount = sizeof(timerHardware) / sizeof(timerHardware[0]); diff --git a/src/main/target/ORBITH743v2/target.h b/src/main/target/ORBITH743v2/target.h new file mode 100644 index 00000000000..89df27ed6c8 --- /dev/null +++ b/src/main/target/ORBITH743v2/target.h @@ -0,0 +1,212 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + + +#pragma once + +#define TARGET_BOARD_IDENTIFIER "ORB7V2" +#define USBD_PRODUCT_STRING "ORBITH743v2" + +#define USE_TARGET_CONFIG + +#define LED0 PE3 //Blue +#define LED1 PE4 //Red +#define LED2 PC4 //Green + +#define BEEPER PD7 +#define BEEPER_INVERTED + + +// *************** SPI ***************************** +#define USE_SPI +#define USE_SPI_DEVICE_1 +#define USE_SPI_DEVICE_2 +#define USE_SPI_DEVICE_3 +#define USE_SPI_DEVICE_4 + +#define SPI1_SCK_PIN PA5 +#define SPI1_MISO_PIN PA6 +#define SPI1_MOSI_PIN PA7 + +#define SPI2_SCK_PIN PA9 +#define SPI2_MISO_PIN PC2 +#define SPI2_MOSI_PIN PC3 + +#define SPI3_SCK_PIN PC10 +#define SPI3_MISO_PIN PC11 +#define SPI3_MOSI_PIN PC12 + +#define SPI4_SCK_PIN PE12 +#define SPI4_MISO_PIN PE13 +#define SPI4_MOSI_PIN PE14 + +// *************** I2C ************************** +#define USE_I2C +#define USE_I2C_DEVICE_1 +#define USE_I2C_DEVICE_2 + +#define I2C1_SCL PB6 +#define I2C1_SDA PB7 + +#define I2C2_SCL PB10 +#define I2C2_SDA PB11 + +// *************** UART ***************************** +#define USE_VCP +#define USE_UART1 +#define USE_UART2 +#define USE_UART3 +#define USE_UART4 +#define USE_UART5 +#define USE_UART6 +#define USE_UART7 +#define USE_UART8 + +#define UART1_TX_PIN PB14 +#define UART1_RX_PIN PA10 + +#define UART2_TX_PIN PD5 +#define UART2_RX_PIN PD6 + +#define UART3_TX_PIN PD8 +#define UART3_RX_PIN PD9 + +#define UART4_TX_PIN PD1 +#define UART4_RX_PIN PD0 + +#define UART5_TX_PIN PB13 +#define UART5_RX_PIN PB12 + +#define UART6_TX_PIN PC6 +#define UART6_RX_PIN PC7 + +#define UART7_TX_PIN PE8 +#define UART7_RX_PIN PE7 + +#define UART8_TX_PIN PE1 +#define UART8_RX_PIN PE0 + +#define USE_DRONECAN +#define CAN1_RX PB8 +#define CAN1_TX PB9 +#define CAN1_STANDBY PA4 + +#define SERIAL_PORT_COUNT 9 //VCP, UART1, UART2, UART3, UART4, UART5, UART6, UART7, UART8 + +#define DEFAULT_RX_TYPE RX_TYPE_SERIAL +#define SERIALRX_PROVIDER SERIALRX_CRSF +#define SERIALRX_UART SERIAL_PORT_USART5 + + +// *************** Gyro & ACC ********************** +#define USE_DUAL_GYRO +#define USE_TARGET_IMU_HARDWARE_DESCRIPTORS + +#define USE_IMU_ICM42605 + +#define IMU_ICM42688_ALIGN_1 CW270_DEG +#define ICM42688_CS_PIN_1 PE11 +#define ICM42688_SPI_BUS_1 BUS_SPI1 +#define ICM42688_EXTI_PIN_1 PE10 + +#define IMU_ICM40609_ALIGN CW270_DEG +#define ICM40609_CS_PIN PD3 +#define ICM40609_SPI_BUS BUS_SPI3 +#define ICM40609_EXTI_PIN PD2 + +#define USE_IMU_LSM6DXX +#define IMU_LSM6DXX_ALIGN CW270_DEG +#define LSM6DXX_SPI_BUS BUS_SPI1 +#define LSM6DXX_CS_PIN PE11 +#define LSM6DXX_EXTI_PIN PE10 + + +// *************** OSD ***************************** +#define USE_MAX7456 +#define MAX7456_SPI_BUS BUS_SPI2 +#define MAX7456_CS_PIN PE6 + +// *************** FLASH *************************** +#define M25P16_SPI_BUS BUS_SPI4 +#define M25P16_CS_PIN PE9 + +#define USE_BLACKBOX +#define USE_FLASHFS +#define USE_FLASH_M25P16 +#define ENABLE_BLACKBOX_LOGGING_ON_SPIFLASH_BY_DEFAULT + +// *************** Baro/Mag ********************* + +#define USE_BARO +#define BARO_I2C_BUS BUS_I2C2 +#define USE_BARO_BMP280 +#define USE_BARO_MS5611 +#define USE_BARO_SPL06 +#define USE_BARO_DPS310 + +#define SPL06_I2C_ADDR 0x76 + +#define USE_MAG +#define MAG_I2C_BUS BUS_I2C1 +#define USE_MAG_ALL + +#define TEMPERATURE_I2C_BUS BUS_I2C1 +#define PITOT_I2C_BUS BUS_I2C1 + +#define USE_RANGEFINDER +#define RANGEFINDER_I2C_BUS BUS_I2C1 + + +// *************** ADC ***************************** +#define USE_ADC +#define ADC_INSTANCE ADC1 + +#define ADC_CHANNEL_1_PIN PC0 //ADC123 VBAT1 +#define ADC_CHANNEL_2_PIN PC1 //ADC123 CURR1 +#define ADC_CHANNEL_3_PIN PC5 //ADC12 RSSI + +#define VBAT_ADC_CHANNEL ADC_CHN_1 +#define CURRENT_METER_ADC_CHANNEL ADC_CHN_2 +#define RSSI_ADC_CHANNEL ADC_CHN_3 + +// *************** PINIO *************************** +#define USE_PINIO +#define USE_PINIOBOX +#define PINIO1_PIN PE2 // VTX power switcher +#define PINIO2_PIN PD4 +#define PINIO1_FLAGS PINIO_FLAGS_INVERTED + +// *************** LEDSTRIP ************************ +#define USE_LED_STRIP +#define WS2811_PIN PA8 + +#define DEFAULT_FEATURES (FEATURE_OSD | FEATURE_TELEMETRY | FEATURE_CURRENT_METER | FEATURE_VBAT | FEATURE_TX_PROF_SEL | FEATURE_BLACKBOX) +#define CURRENT_METER_SCALE 100 +#define VBAT_SCALE_DEFAULT 1100 + + +#define USE_SERIAL_4WAY_BLHELI_INTERFACE + +#define TARGET_IO_PORTA 0xffff +#define TARGET_IO_PORTB 0xffff +#define TARGET_IO_PORTC 0xffff +#define TARGET_IO_PORTD 0xffff +#define TARGET_IO_PORTE 0xffff + +#define MAX_PWM_OUTPUT_PORTS 15 +#define USE_DSHOT +#define USE_ESC_SENSOR diff --git a/src/utils/settings.rb b/src/utils/settings.rb index bbc76f9ebd5..19fa0e1ab3b 100644 --- a/src/utils/settings.rb +++ b/src/utils/settings.rb @@ -490,7 +490,7 @@ def write_impl_file(file) foreach_enabled_group do |group| count = 0 group["members"].each do |member| - if is_condition_enabled(member["condition"]) + if is_condition_enabled(member) count += 1 end end @@ -665,16 +665,21 @@ def resolve_range(member) return min, max end - def is_condition_enabled(cond) - return !cond || @true_conditions.include?(cond) + # Condition truth is resolved per group/member occurrence (see + # check_conditions), not by condition string, since the same condition + # string can appear on multiple groups at different header-inclusion + # positions and be true at one but not the other. + def is_condition_enabled(obj) + return true unless obj["condition"] + obj["_condition_enabled"] end def foreach_enabled_member &block enum = Enumerator.new do |yielder| groups.each do |group| - if is_condition_enabled(group["condition"]) + if is_condition_enabled(group) group["members"].each do |member| - if is_condition_enabled(member["condition"]) + if is_condition_enabled(member) yielder.yield group, member end end @@ -763,10 +768,13 @@ def mktmpdir value end + # cstddef for offsetof(). Shared by every probe so they all see the same + # base macro state. + BASE_HEADERS = ["platform.h", "cstddef"] + def compile_test_file(prog) buf = StringIO.new - # cstddef for offsetof() - headers = ["platform.h", "cstddef"] + headers = BASE_HEADERS.dup @data["groups"].each do |group| gh = group["headers"] if gh @@ -780,6 +788,10 @@ def compile_test_file(prog) end buf << "\n" buf << prog.string + compile_raw(buf) + end + + def compile_raw(buf) mktmpdir do |dir| file = File.join(dir, "test.cpp") File.open(file, 'w') {|file| file.write(buf.string)} @@ -792,45 +804,72 @@ def compile_test_file(prog) def check_conditions buf = StringIO.new - conditions = Set.new - add_condition = -> (c) { - if c && !conditions.include?(c) - conditions.add(c) - buf << "#if " - in_word = false - c.split('').each do |ch| - if in_word - if !ch.match(/^[a-zA-Z0-9_]$/) - in_word = false - buf << ")" - end - buf << ch - else - if ch.match(/^[a-zA-Z_]$/) - in_word = true - buf << "defined(" - end - buf << ch - end - end + BASE_HEADERS.each do |h| + buf << "#include \"#{h}\"\n" + end + + # Tag each group/member occurrence with its own unique probe, rather + # than deduping by condition string: the same condition string can + # appear on multiple groups, and since a probe's truth now depends on + # its position (see comment below), one occurrence being true doesn't + # mean another occurrence of the same string is. + tags = {} + seq = 0 + add_condition = -> (obj) { + c = obj["condition"] + next unless c + seq += 1 + tag = "COND#{seq}" + tags[tag] = obj + buf << "#if " + in_word = false + c.split('').each do |ch| if in_word - buf << ")" + if !ch.match(/^[a-zA-Z0-9_]$/) + in_word = false + buf << ")" + end + buf << ch + else + if ch.match(/^[a-zA-Z_]$/) + in_word = true + buf << "defined(" + end + buf << ch end - buf << "\n" - buf << "#pragma message(#{c.inspect})\n" - buf << "#endif\n" end + if in_word + buf << ")" + end + buf << "\n" + buf << "#pragma message(#{tag.inspect})\n" + buf << "#endif\n" } - foreach_member do |group, member| - add_condition.call(group["condition"]) - add_condition.call(member["condition"]) + # Check each condition right after that group's own headers, matching + # the position resolve_types() implicitly relies on: a header's own + # #ifdef guard only sees macros defined by headers earlier in this + # sequence. + @data["groups"].each do |group| + gh = group["headers"] + if gh + gh.each do |h| + buf << "#include \"#{h}\"\n" if h + end + end + add_condition.call(group) + group["members"].each do |member| + add_condition.call(member) + end end - stderr = compile_test_file(buf) - @true_conditions = Set.new + stderr = compile_raw(buf) + true_tags = Set.new stderr.scan(/#pragma message\(\"(.*)\"\)/).each do |m| - @true_conditions << m[0] + true_tags << m[0] + end + tags.each do |tag, obj| + obj["_condition_enabled"] = true_tags.include?(tag) end end