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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization
* text=auto
*.bat text eol=crlf
123 changes: 123 additions & 0 deletions .github/workflows/SITL.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: SITL Tests

on:
push:
branches: ["main", "ark-release"]
pull_request:
branches: ["main", "ark-release"]
workflow_dispatch:

jobs:
sitl-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Build SITL
run: make AM32_SITL_CAN

- name: Install CI Python deps
run: |
python3 -m venv sitl-venv
sitl-venv/bin/pip install -U pip
sitl-venv/bin/pip install -r Mcu/SITL/requirements-ci.txt

- name: Run SITL pytest suite
run: |
mkdir sitl_test_run && cd sitl_test_run
../sitl-venv/bin/python ../Mcu/SITL/run_ci_tests.py \
--sitl ../obj/AM32_AM32_SITL_CAN_*.elf \
--junitxml=sitl-results.xml

- name: Upload SITL log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: sitl-test-log
path: |
sitl_test_run/sitl_ci.log
sitl_test_run/**/sitl_ci.log
sitl_test_run/sitl-results.xml
if-no-files-found: ignore

- name: Upload junit results
if: always()
uses: actions/upload-artifact@v4
with:
name: sitl-junit
path: sitl_test_run/sitl-results.xml
if-no-files-found: ignore

gui-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- name: Install Qt system libraries
run: sudo apt-get update && sudo apt-get install -y libgl1 libegl1 libfontconfig1 libxkbcommon0

- name: Build SITL
run: make AM32_SITL_CAN

- name: Create GUI environment
run: python3 Mcu/SITL/make_gui_env.py

- name: Run GUI test (offscreen)
run: |
mkdir gui_test_run && cd gui_test_run
python3 ../Mcu/SITL/gui_ci_test.py \
--gui-python ../Mcu/SITL/venv/bin/python3 \
--sitl ../obj/AM32_AM32_SITL_CAN_*.elf

- name: Upload SITL log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: gui-test-log
path: gui_test_run/sitl_ci.log
if-no-files-found: ignore

windows-build:
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- name: Install Cygwin
uses: cygwin/cygwin-install-action@v4
with:
packages: gcc-core make

- name: Build SITL
shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}'
run: |
cd $(cygpath -u "$GITHUB_WORKSPACE")
make AM32_SITL_CAN
ls -la obj/

- name: Boot smoke test
shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}'
run: |
cd $(cygpath -u "$GITHUB_WORKSPACE")
./obj/AM32_AM32_SITL_CAN_*.elf --can-uri none --input-type 1 >boot.log 2>&1 &
sleep 3
kill %1 || true
cat boot.log
grep -q "PWM/DShot input on udp port" boot.log
grep -q "state/model port udp" boot.log

- name: Package binary with cygwin runtime
shell: C:\cygwin\bin\bash.exe --noprofile --norc -o igncr -eo pipefail '{0}'
run: |
cd $(cygpath -u "$GITHUB_WORKSPACE")
mkdir -p sitl-windows
cp obj/AM32_AM32_SITL_CAN_*.elf sitl-windows/AM32_SITL_CAN.exe
cp /bin/cygwin1.dll sitl-windows/

- name: Upload Windows SITL binary
uses: actions/upload-artifact@v4
with:
name: sitl-windows
path: sitl-windows/
131 changes: 131 additions & 0 deletions .github/workflows/hwci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Hardware-in-the-loop CI

# Runs the AM32 firmware on a real ARK 4IN1 ESC mounted on a Tyto Robotics
# Flight Stand, capturing CPU load / loop times (over SWD) and efficiency /
# demag (thrust stand), then gates the result against a committed baseline.
#
# MANUAL TRIGGER ONLY (workflow_dispatch). The rig needs hands-on preparation
# before every run - battery connected and charged, prop/torque arm checked,
# Flight Stand Software up - so a human always starts the run; nothing fires
# automatically from pushes, PRs, or labels. This also means fork code never
# reaches the self-hosted bench machine unless a maintainer explicitly enters
# a PR number in the dispatch form.
#
# Requires a self-hosted runner labelled [self-hosted, hwci] that is
# physically wired to the rig and provisioned per hwci/README.md
# (ARM toolchain, OpenOCD, ST-Link udev rules, Flight Stand Software + gRPC,
# a rig config at $HWCI_RIG_CONFIG).

on:
workflow_dispatch:
inputs:
profile:
description: Test profile to run
type: choice
default: ci_smoke
options: [ci_smoke, efficiency_sweep, demag_step_stress]
pr:
description: "PR number to test (optional: checks out that PR's head instead of the selected branch)"
type: string
default: ""
battery_cells:
description: "Battery cell count on the bench right now (e.g. 6 for 6S); the run refuses to start if pack voltage is too low for this. Leave blank to skip the check."
type: string
default: "6"
save_baseline:
description: Save this run's metrics as the new baseline (artifact)
type: boolean
default: false

concurrency:
group: hwci-hardware # one job at a time owns the physical rig
cancel-in-progress: false

jobs:
hil:
runs-on: [self-hosted, hwci]
timeout-minutes: 30
env:
TARGET: ARK_4IN1_F051
PROFILE: ${{ inputs.profile }}
BATTERY_CELLS: ${{ inputs.battery_cells }}
# Bench-specific config lives on the runner, not in the repo.
HWCI_RIG_CONFIG: ${{ vars.HWCI_RIG_CONFIG }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
ref: ${{ inputs.pr != '' && format('refs/pull/{0}/head', inputs.pr) || github.ref }}

- name: Ensure ARM toolchain
shell: bash
run: |
# make/tools.mk pins ARM_SDK_PREFIX to tools/linux/<xpack>/bin, so a
# system arm-none-eabi-gcc on PATH does NOT satisfy the build; install
# the pinned SDK whenever the pinned path is missing.
if ! ls tools/linux/*/bin/arm-none-eabi-gcc >/dev/null 2>&1; then
make arm_sdk_install
fi

- name: Install harness
shell: bash
working-directory: hwci
run: |
python3 -m venv .venv
. .venv/bin/activate
pip install -e '.[plot,flightstand]'

- name: Run hardware CI
id: hil
shell: bash
working-directory: hwci
run: |
. .venv/bin/activate
RUN_DIR="runs/${GITHUB_SHA::8}-${PROFILE}"
echo "run_dir=hwci/$RUN_DIR" >> "$GITHUB_OUTPUT"
# Blank input -> no flag at all (hwci ci makes --battery-cells opt-in).
BATTERY_ARGS=()
if [ -n "$BATTERY_CELLS" ]; then
BATTERY_ARGS=(--battery-cells "$BATTERY_CELLS")
fi
# A missing baseline (first run on a fresh repo) skips the gate with
# a warning instead of failing, so save_baseline can bootstrap it.
set +e
hwci ci \
--profile "$PROFILE" \
--config "${HWCI_RIG_CONFIG:-$HOME/hwci-rig.yaml}" \
--baseline "baselines/${TARGET}.json" \
"${BATTERY_ARGS[@]}" \
--out "$RUN_DIR"
rc=$?
set -e
echo "rc=$rc" >> "$GITHUB_OUTPUT"
exit $rc

- name: Save baseline (optional)
# Runs even when the gate FAILed (rc=1: intentional re-baseline after an
# accepted change), but never after an aborted/crashed run (rc >= 2).
if: ${{ always() && inputs.save_baseline && contains(fromJSON('["0", "1"]'), steps.hil.outputs.rc) }}
shell: bash
working-directory: hwci
run: |
. .venv/bin/activate
hwci baseline-save "runs/${GITHUB_SHA::8}-${PROFILE}" \
--out "baselines/${TARGET}.json"

- name: Report to job summary
if: always()
shell: bash
run: |
RD="${{ steps.hil.outputs.run_dir }}"
if [ -f "$RD/report.md" ]; then cat "$RD/report.md" >> "$GITHUB_STEP_SUMMARY"; fi

- name: Upload run data
if: always()
uses: actions/upload-artifact@v4
with:
name: hwci-${{ env.PROFILE }}-${{ github.sha }}
path: |
${{ steps.hil.outputs.run_dir }}/
hwci/baselines/${{ env.TARGET }}.json
retention-days: 90
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ Keil_Projects/Listings
/Debug/
windows-tools.zip
.settings/
Mcu/SITL/venv/
.pytest_cache/
**/__pycache__/
**/sitl_ci.log
am32_eeprom.bin
am32_eeprom.bin.lock
am32_eeprom.bin.rtc
21 changes: 21 additions & 0 deletions Inc/functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@
#include "main.h"
#include "targets.h"

/*
get current value of UTILITY_TIMER timer as 16bit microseconds
*/
static inline uint16_t get_timer_us16(void)
{
#if defined(STMICRO)
return UTILITY_TIMER->CNT;
#elif defined(GIGADEVICES)
return TIMER_CNT(UTILITY_TIMER);
#elif defined(ARTERY)
return UTILITY_TIMER->cval;
#elif defined(NXP)
// Return nothing since NXP micro-tick works differently
return 0;
#elif defined(WCH)
return UTILITY_TIMER->CNT >> 1;
#else
#error unsupported MCU
#endif
}

uint32_t getAbsDif(int number1, int number2);
void delayMicros(uint32_t micros);
void delayMillis(uint32_t millis);
Expand Down
Loading
Loading