Merge pull request #5 from bfe-noah/qemu-device-sim
qemu/: QEMU device simulator — boot the real kernel + userspace off-hardware
This commit is contained in:
+87
-18
@@ -2,8 +2,9 @@
|
|||||||
#
|
#
|
||||||
# Policy (mirrors flare-edge): only GitHub-owned actions get the repo token; the one
|
# Policy (mirrors flare-edge): only GitHub-owned actions get the repo token; the one
|
||||||
# third-party helper (taiki-e/install-action) is pinned and never handed a token.
|
# third-party helper (taiki-e/install-action) is pinned and never handed a token.
|
||||||
# Host-testable jobs run on GitHub-hosted runners; only the heavy kernel build uses
|
# Every job runs on GitHub-hosted runners — no self-hosted runner may be reachable
|
||||||
# the self-hosted [self-hosted, warden-sdk] runner on bfe-mpc-0640 (added in P5).
|
# from this repo's workflows (ADR-0007: public-repo fork PRs would otherwise be
|
||||||
|
# able to run code on private infrastructure). kernel-build is dispatch-only.
|
||||||
name: ci
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -24,6 +25,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 25
|
||||||
outputs:
|
outputs:
|
||||||
passed: ${{ steps.result.outputs.passed }}
|
passed: ${{ steps.result.outputs.passed }}
|
||||||
coverage: ${{ steps.result.outputs.coverage }}
|
coverage: ${{ steps.result.outputs.coverage }}
|
||||||
@@ -33,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
: > /tmp/test.log
|
: > /tmp/test.log
|
||||||
for d in sim tools/config-lint; do
|
for d in sim tools/config-lint qemu/rs485-bridge; do
|
||||||
echo "== cargo test in $d ==" | tee -a /tmp/test.log
|
echo "== cargo test in $d ==" | tee -a /tmp/test.log
|
||||||
( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log
|
( cd "$d" && cargo test --locked ) 2>&1 | tee -a /tmp/test.log
|
||||||
done
|
done
|
||||||
@@ -57,6 +59,7 @@ jobs:
|
|||||||
mcdc:
|
mcdc:
|
||||||
# 100% MC/DC (condition coverage) enforced on every Tier-1 driver harness.
|
# 100% MC/DC (condition coverage) enforced on every Tier-1 driver harness.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: install gcc-14
|
- name: install gcc-14
|
||||||
@@ -77,6 +80,7 @@ jobs:
|
|||||||
# Smoke-run the sim micro-benchmarks and emit the ns/op trend JSON. Regression
|
# Smoke-run the sim micro-benchmarks and emit the ns/op trend JSON. Regression
|
||||||
# gating against stored history is future work (no flare-edge pattern to copy).
|
# gating against stored history is future work (no flare-edge pattern to copy).
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: run sim benchmarks
|
- name: run sim benchmarks
|
||||||
@@ -85,10 +89,42 @@ jobs:
|
|||||||
cargo bench --locked --bench sim_bench 1> bench.txt 2> bench.json
|
cargo bench --locked --bench sim_bench 1> bench.txt 2> bench.json
|
||||||
echo "== timings =="; cat bench.txt
|
echo "== timings =="; cat bench.txt
|
||||||
echo "== trend json =="; grep '"bench"' bench.json
|
echo "== trend json =="; grep '"bench"' bench.json
|
||||||
|
- name: run rs485-bridge benchmarks
|
||||||
|
working-directory: qemu/rs485-bridge
|
||||||
|
run: |
|
||||||
|
cargo bench --locked --bench bridge_bench 1> bench.txt 2> bench.json
|
||||||
|
echo "== timings =="; cat bench.txt
|
||||||
|
echo "== trend json =="; grep '"bench"' bench.json
|
||||||
|
|
||||||
|
qemu-tools:
|
||||||
|
# The qemu/ device-sim build tooling must stay healthy on a plain hosted
|
||||||
|
# runner: shellcheck the scripts, build the initramfs (pinned busybox,
|
||||||
|
# fail-closed sha), and build the A/B disk image (unprivileged mkfs -d).
|
||||||
|
# Booting needs a zImage and therefore lives in kernel-build's smoke step.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: shellcheck qemu scripts
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
|
||||||
|
shellcheck -x qemu/*.sh qemu/tests/*.sh build/*.sh \
|
||||||
|
qemu/rootfs/etc/warden-lib.sh qemu/rootfs/etc/rc \
|
||||||
|
qemu/rootfs/sbin/init qemu/rootfs/init
|
||||||
|
- name: cache pinned busybox
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: qemu/out/busybox-armv7l
|
||||||
|
key: busybox-armv7l-${{ hashFiles('qemu/busybox.sha256') }}
|
||||||
|
- name: build initramfs
|
||||||
|
run: bash qemu/mkinitramfs.sh
|
||||||
|
- name: build A/B disk image
|
||||||
|
run: bash qemu/mkimage.sh
|
||||||
|
|
||||||
patches-apply:
|
patches-apply:
|
||||||
# The RV1106 series must apply cleanly onto pristine linux-6.18.46.
|
# The RV1106 series must apply cleanly onto pristine linux-6.18.46.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: cache pristine kernel tarball
|
- name: cache pristine kernel tarball
|
||||||
@@ -96,11 +132,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ~/linux-6.18.46.tar.xz
|
path: ~/linux-6.18.46.tar.xz
|
||||||
key: linux-6.18.46-tarball
|
key: linux-6.18.46-tarball
|
||||||
- name: fetch + verify pristine
|
- name: fetch + verify pristine (shared fail-closed fetcher)
|
||||||
run: |
|
run: bash build/fetch-kernel-tarball.sh "$HOME/linux-6.18.46.tar.xz"
|
||||||
[ -f ~/linux-6.18.46.tar.xz ] || \
|
|
||||||
curl -fSL https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.46.tar.xz -o ~/linux-6.18.46.tar.xz
|
|
||||||
echo "$(cat build/linux-6.18.46.tar.xz.sha256) $HOME/linux-6.18.46.tar.xz" | sha256sum -c -
|
|
||||||
- name: apply the series in order
|
- name: apply the series in order
|
||||||
run: |
|
run: |
|
||||||
tar -C /tmp -xf ~/linux-6.18.46.tar.xz
|
tar -C /tmp -xf ~/linux-6.18.46.tar.xz
|
||||||
@@ -119,6 +152,7 @@ jobs:
|
|||||||
prune-artifacts:
|
prune-artifacts:
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
@@ -142,35 +176,69 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
kernel-build:
|
kernel-build:
|
||||||
# Full hermetic build on the warden-sdk self-hosted runner (bfe-mpc-0640,
|
# Full hermetic build on a GitHub-hosted runner (ADR-0007; supersedes the
|
||||||
# ADR-0004). Manual-dispatch by design — a full kernel build is too heavy to run
|
# self-hosted half of ADR-0004 — a self-hosted runner must never be reachable
|
||||||
# on every push; trigger it via `gh workflow run ci.yml` / the Actions UI.
|
# from a public repo's workflows). Manual-dispatch by design — a full kernel
|
||||||
|
# build is heavy; trigger via `gh workflow run ci.yml` / the Actions UI.
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
needs: [prune-artifacts]
|
needs: [prune-artifacts]
|
||||||
runs-on: [self-hosted, warden-sdk]
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: install cross toolchain + kernel build deps + qemu
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq gcc-arm-linux-gnueabihf qemu-system-arm \
|
||||||
|
cpio bc bison flex libssl-dev ccache
|
||||||
- name: provision `python` (SDK quirk — build calls bare python)
|
- name: provision `python` (SDK quirk — build calls bare python)
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$RUNNER_TEMP/bin"
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
ln -sf "$(command -v python3)" "$RUNNER_TEMP/bin/python"
|
ln -sf "$(command -v python3)" "$RUNNER_TEMP/bin/python"
|
||||||
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: cache pristine kernel tarball
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/linux-6.18.46.tar.xz
|
||||||
|
key: linux-6.18.46-tarball
|
||||||
|
# Ephemeral runners rebuild the whole tree every dispatch (~9 min of
|
||||||
|
# compile); ccache recovers most of it for an unchanged/lightly-changed
|
||||||
|
# series. Keyed on the config + patches so a real change misses cleanly.
|
||||||
|
- name: cache ccache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.ccache
|
||||||
|
key: kbuild-ccache-${{ hashFiles('build/warden_defconfig', 'patches/*.patch') }}
|
||||||
|
restore-keys: kbuild-ccache-
|
||||||
- name: build zImage + rv1106-warden.dtb
|
- name: build zImage + rv1106-warden.dtb
|
||||||
env:
|
env:
|
||||||
# WORK must be OUTSIDE the repo checkout: build-kernel.sh applies the patch
|
# WORK must be OUTSIDE the repo checkout: build-kernel.sh applies the patch
|
||||||
# series with `git apply`, which silently ignores out-of-subdir paths when
|
# series with `git apply`, which silently ignores out-of-subdir paths when
|
||||||
# run inside another git repo (issue #1). $RUNNER_TEMP is outside the checkout.
|
# run inside another git repo (issue #1). $RUNNER_TEMP is outside the checkout.
|
||||||
WORK: ${{ runner.temp }}/kbuild-out
|
WORK: ${{ runner.temp }}/kbuild-out
|
||||||
JOBS: 4 # belt-and-braces bound in addition to the runner's cgroup cap
|
# The kernel is freestanding; the generic arm cross toolchain links it.
|
||||||
# The kernel is freestanding; use the stable generic arm cross toolchain
|
|
||||||
# (Debian gcc-arm-linux-gnueabihf on the runner) rather than depending on
|
|
||||||
# the ephemeral Luckfox SDK checkout path.
|
|
||||||
CROSS_COMPILE: arm-linux-gnueabihf-
|
CROSS_COMPILE: arm-linux-gnueabihf-
|
||||||
run: bash build/build-kernel.sh
|
WARDEN_CCACHE: 1
|
||||||
|
CCACHE_DIR: /home/runner/.ccache
|
||||||
|
# KERNEL_TARBALL is exported from the SHELL so $HOME expands — a literal
|
||||||
|
# `~` in a YAML env: value is never tilde-expanded and broke every
|
||||||
|
# dispatch until caught in review.
|
||||||
|
run: |
|
||||||
|
export KERNEL_TARBALL="$HOME/linux-6.18.46.tar.xz"
|
||||||
|
bash build/build-kernel.sh
|
||||||
|
ccache -s | head -4
|
||||||
|
# Boot smoke under QEMU: the zImage this job just built must reach the
|
||||||
|
# initramfs sentinel on -M virt (verified 2026-08-29: the canonical
|
||||||
|
# config boots virt as-is). FAIL-CLOSED on a missing qemu-system-arm.
|
||||||
|
- name: boot smoke (qemu-system-arm -M virt)
|
||||||
|
run: |
|
||||||
|
bash qemu/mkinitramfs.sh
|
||||||
|
bash qemu/tests/boot-smoke.sh \
|
||||||
|
"$RUNNER_TEMP/kbuild-out/linux-6.18.46/arch/arm/boot/zImage"
|
||||||
# Best-effort: the build IS the gate. Uploading the zImage/dtb to GitHub
|
# Best-effort: the build IS the gate. Uploading the zImage/dtb to GitHub
|
||||||
# artifact storage can fail on an account-wide storage-quota hit (recalculated
|
# artifact storage can fail on an account-wide storage-quota hit (recalculated
|
||||||
# every 6-12h) that has nothing to do with this build — don't red-X a good
|
# every 6-12h) that has nothing to do with this build — don't red-X a good
|
||||||
# kernel build over it. The outputs also remain on the self-hosted runner host.
|
# kernel build over it.
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@@ -187,6 +255,7 @@ jobs:
|
|||||||
needs: [test]
|
needs: [test]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+9
-1
@@ -6,6 +6,8 @@ target/
|
|||||||
# build artifacts (firmware/kernel objects are rebuilt from source)
|
# build artifacts (firmware/kernel objects are rebuilt from source)
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
|
*.elf
|
||||||
|
*.map
|
||||||
|
|
||||||
# scratch / editor
|
# scratch / editor
|
||||||
*.swp
|
*.swp
|
||||||
@@ -14,8 +16,14 @@ target/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# local tooling state (code-review harness cross-session memory, etc.)
|
# local tooling state
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
# driver MC/DC harness build dirs
|
# driver MC/DC harness build dirs
|
||||||
**/test/build/
|
**/test/build/
|
||||||
|
|
||||||
|
# qemu device sim: build outputs (initramfs, disk images, cached busybox) and
|
||||||
|
# payload binaries (dropped in from flare-edge builds, never committed)
|
||||||
|
qemu/out/
|
||||||
|
qemu/payload/*
|
||||||
|
!qemu/payload/README.md
|
||||||
|
|||||||
@@ -1,10 +1,338 @@
|
|||||||
warden-sdk is dual-licensed under either of
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
* Apache License, Version 2.0 (LICENSE-APACHE)
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
* MIT license (LICENSE-MIT)
|
<https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
at your option.
|
Preamble
|
||||||
|
|
||||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
The licenses for most software are designed to take away your
|
||||||
for inclusion in this work by you, as defined in the Apache-2.0 license, shall be
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
dual licensed as above, without any additional terms or conditions.
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program 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 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Moe Ghoul>, 1 April 1989
|
||||||
|
Moe Ghoul, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
|
|||||||
-202
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 BlueFlare
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
|
||||||
Third-party components carry their own licenses, which are not superseded by
|
|
||||||
the above:
|
|
||||||
|
|
||||||
- LVGL (the UI library, fetched during build, not vendored here) is MIT
|
|
||||||
licensed by its authors.
|
|
||||||
- The Luckfox / Rockchip RV1106 SDK and its cross-toolchain are covered by
|
|
||||||
their respective vendor licenses.
|
|
||||||
- vendor/asic-rs is 256 Foundation's asic-rs, vendored into this repository
|
|
||||||
with git subtree and licensed under Apache-2.0. Its license text is kept
|
|
||||||
at vendor/asic-rs/LICENSE.txt. Files modified locally must carry a notice
|
|
||||||
saying so, per Apache-2.0 section 4(b); see vendor/README.md.
|
|
||||||
|
|
||||||
The contents of branding/ (BlueFlare logos, wordmarks, and splash artwork) are
|
|
||||||
NOT covered by the MIT license above. They are proprietary to BlueFlare, all
|
|
||||||
rights reserved, and are included only so the firmware builds and displays as
|
|
||||||
shipped. Do not reuse them in derivative works.
|
|
||||||
@@ -12,12 +12,14 @@ the rest of the firmware: tested, benchmarked, reproducible, and honest about
|
|||||||
what runs on real silicon versus what we simulate.
|
what runs on real silicon versus what we simulate.
|
||||||
|
|
||||||
> Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel
|
> Status: **bringup.** The hardware **simulator** and its tests, the RV1106 kernel
|
||||||
> forward-port as a reviewable `patches/` series, the hermetic kernel build, and two
|
> forward-port as a reviewable `patches/` series, the hermetic kernel build, two
|
||||||
> Tier-1 drivers at 100% MC/DC are all in and CI-green on `main`. What
|
> Tier-1 drivers at 100% MC/DC, and the **QEMU device sim** (`qemu/`, ADR-0006:
|
||||||
> remains before this is on the production build path: installing the self-hosted
|
> boots the real kernel + real userspace on `-M virt` — check-in/OTA against the
|
||||||
> kernel-build runner (see `docs/ci-cd.md`) and having flare-edge consume warden-sdk
|
> mock portal, watchdog, RS485-to-sim bridge, 720x720 display + touch, all
|
||||||
> as a dependency — both [maintainer]-gated. Until then, flare-edge still builds firmware from
|
> emulation-verified) are in. What remains before this is on the production build
|
||||||
> the vendored SDK + `sdk-patches/`.
|
> path: having flare-edge consume warden-sdk as a dependency (maintainer-gated).
|
||||||
|
> Until then, flare-edge still builds firmware from the vendored SDK +
|
||||||
|
> `sdk-patches/`.
|
||||||
|
|
||||||
## Why a new SDK
|
## Why a new SDK
|
||||||
|
|
||||||
@@ -90,6 +92,8 @@ than duplicate each other.
|
|||||||
|
|
||||||
```
|
```
|
||||||
sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU.
|
sim/ the hardware simulator (Rust): membus/devmem, HPMCU, CRU, Modbus, RGA, NPU.
|
||||||
|
qemu/ the device simulator (ADR-0006): QEMU -M virt boots the real kernel and
|
||||||
|
real userspace; A/B disk layout, RS485 bridge into sim/, scenario tests.
|
||||||
drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in).
|
drivers/ our own hardened drivers + their seams (relays, freshness; more migrate in).
|
||||||
patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split).
|
patches/ the RV1106 kernel forward-port delta onto pristine linux-6.18.46 (subsystem-split).
|
||||||
kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md).
|
kernel/ forward-port docs + provenance (rv1106-enablement/, PROVENANCE.md).
|
||||||
@@ -117,6 +121,17 @@ Evaluated against the stack philosophy — **openness, hardness, modernness**:
|
|||||||
## Relationship to flare-edge
|
## Relationship to flare-edge
|
||||||
|
|
||||||
flare-edge (WardenOS: the LVGL UI + the `flared` daemon) is the product; warden-sdk
|
flare-edge (WardenOS: the LVGL UI + the `flared` daemon) is the product; warden-sdk
|
||||||
is what builds and tests it. During bootstrap, flare-edge consumes warden-sdk piece
|
is what builds and tests it. flare-edge is BlueFlare's private companion repo —
|
||||||
|
not publicly available — so flare-edge issue references and checkout paths in
|
||||||
|
this repo's docs are context, not reachable links. During bootstrap, flare-edge consumes warden-sdk piece
|
||||||
by piece: first the simulator (as a dev/test dependency), later the image build.
|
by piece: first the simulator (as a dev/test dependency), later the image build.
|
||||||
No flare-edge code moves here — only the SDK/build/sim/driver-seam layer.
|
No flare-edge code moves here — only the SDK/build/sim/driver-seam layer.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
**GPL-2.0-only**, repo-wide (see `LICENSE`; a per-file SPDX identifier governs
|
||||||
|
where one is present, e.g. a few GPL-2.0-or-later kernel files). The kernel
|
||||||
|
material in `patches/` and `kernel/rv1106-enablement/` is derivative of the
|
||||||
|
Linux kernel and of GPL-2.0 vendor code either way — per-driver origin and
|
||||||
|
license are tracked in `kernel/rv1106-enablement/PROVENANCE.md`. Contributions
|
||||||
|
are accepted under the same license (inbound = outbound).
|
||||||
|
|||||||
+62
-18
@@ -10,8 +10,15 @@
|
|||||||
# Env:
|
# Env:
|
||||||
# KERNEL_TARBALL path to a local linux-6.18.46.tar.xz (skips the download)
|
# KERNEL_TARBALL path to a local linux-6.18.46.tar.xz (skips the download)
|
||||||
# SDK_TC dir holding the arm-rockchip830 uclibc cross toolchain bin/
|
# SDK_TC dir holding the arm-rockchip830 uclibc cross toolchain bin/
|
||||||
|
# CROSS_COMPILE cross-compiler prefix (default arm-rockchip830-linux-uclibcgnueabihf-;
|
||||||
|
# CI overrides with the generic arm-linux-gnueabihf-)
|
||||||
# WORK build scratch dir (default: a mktemp under $TMPDIR)
|
# WORK build scratch dir (default: a mktemp under $TMPDIR)
|
||||||
# JOBS parallel make jobs (default: nproc)
|
# JOBS parallel make jobs (default: nproc)
|
||||||
|
# WARDEN_KCONFIG_FRAGMENT
|
||||||
|
# optional kconfig fragment merged onto warden_defconfig
|
||||||
|
# (qemu/configs/virt.fragment builds the QEMU -M virt variant);
|
||||||
|
# every fragment option is verified to have taken effect
|
||||||
|
# WARDEN_CCACHE=1 compile through ccache (CI caches ~/.ccache)
|
||||||
#
|
#
|
||||||
# Requires: `python` (not python3) on PATH — the SDK quirk; the CI runner provides
|
# Requires: `python` (not python3) on PATH — the SDK quirk; the CI runner provides
|
||||||
# a project-local venv. Builds are SERIAL on the shared SDK box — never run two.
|
# a project-local venv. Builds are SERIAL on the shared SDK box — never run two.
|
||||||
@@ -21,9 +28,8 @@ KVER=6.18.46
|
|||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # build/
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # build/
|
||||||
REPO="$(cd "$HERE/.." && pwd)"
|
REPO="$(cd "$HERE/.." && pwd)"
|
||||||
PATCHES="$REPO/patches"
|
PATCHES="$REPO/patches"
|
||||||
SHA_FILE="$HERE/linux-$KVER.tar.xz.sha256"
|
|
||||||
JOBS="${JOBS:-$(nproc)}"
|
JOBS="${JOBS:-$(nproc)}"
|
||||||
URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$KVER.tar.xz"
|
# The tarball URL + sha256 pin live in fetch-kernel-tarball.sh (shared with CI).
|
||||||
|
|
||||||
# A caller-provided WORK (e.g. CI's ${{ github.workspace }}/kbuild-out, from which
|
# A caller-provided WORK (e.g. CI's ${{ github.workspace }}/kbuild-out, from which
|
||||||
# artifacts are uploaded) is left intact; a scratch dir we mktemp'd here is our own
|
# artifacts are uploaded) is left intact; a scratch dir we mktemp'd here is our own
|
||||||
@@ -44,21 +50,9 @@ command -v python >/dev/null || { echo "need 'python' (not python3) on PATH —
|
|||||||
# 1. obtain + verify the pristine tarball
|
# 1. obtain + verify the pristine tarball
|
||||||
mkdir -p "$WORK"
|
mkdir -p "$WORK"
|
||||||
TB="${KERNEL_TARBALL:-$WORK/linux-$KVER.tar.xz}"
|
TB="${KERNEL_TARBALL:-$WORK/linux-$KVER.tar.xz}"
|
||||||
if [ ! -f "$TB" ]; then
|
# Fetch + fail-closed sha256 verification live in ONE place shared with CI
|
||||||
log "downloading $URL"
|
# (a missing pin or a mismatch always refuses to build).
|
||||||
curl -fSL "$URL" -o "$TB"
|
bash "$HERE/fetch-kernel-tarball.sh" "$TB"
|
||||||
fi
|
|
||||||
# Fail closed: a missing pin (e.g. forgotten on a KVER bump) or a KERNEL_TARBALL
|
|
||||||
# pointed at an arbitrary file must refuse to build, never silently skip the check
|
|
||||||
# — the pristine tarball is the ONLY external input and integrity is the whole point.
|
|
||||||
[ -f "$SHA_FILE" ] || {
|
|
||||||
echo "FATAL: no pinned sha256 for linux-$KVER (expected $SHA_FILE) — refusing to build from an unverified tarball" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
want="$(cat "$SHA_FILE")"
|
|
||||||
got="$(sha256sum "$TB" | awk '{print $1}')"
|
|
||||||
[ "$want" = "$got" ] || { echo "tarball sha256 mismatch: want $want got $got" >&2; exit 1; }
|
|
||||||
log "tarball sha256 verified"
|
|
||||||
|
|
||||||
# 2. extract pristine
|
# 2. extract pristine
|
||||||
SRC="$WORK/linux-$KVER"
|
SRC="$WORK/linux-$KVER"
|
||||||
@@ -97,6 +91,19 @@ log "patch series applied ($(basename "$SENTINEL") present)"
|
|||||||
# 4. configure
|
# 4. configure
|
||||||
log "configuring (warden_defconfig)"
|
log "configuring (warden_defconfig)"
|
||||||
cp "$HERE/warden_defconfig" "$SRC/.config"
|
cp "$HERE/warden_defconfig" "$SRC/.config"
|
||||||
|
# Optional kconfig fragment overlay (e.g. qemu/configs/virt.fragment for the
|
||||||
|
# QEMU -M virt device-sim variant). Fail closed if set but unreadable — never
|
||||||
|
# silently build the wrong kernel. Unset => the canonical RV1106 build,
|
||||||
|
# byte-identical to a build without this hook.
|
||||||
|
if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then
|
||||||
|
[ -f "$WARDEN_KCONFIG_FRAGMENT" ] || {
|
||||||
|
echo "FATAL: WARDEN_KCONFIG_FRAGMENT set but not a file: $WARDEN_KCONFIG_FRAGMENT" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
FRAG="$(cd "$(dirname "$WARDEN_KCONFIG_FRAGMENT")" && pwd)/$(basename "$WARDEN_KCONFIG_FRAGMENT")"
|
||||||
|
log "merging kconfig fragment $(basename "$FRAG")"
|
||||||
|
( cd "$SRC" && ARCH=arm ./scripts/kconfig/merge_config.sh -m .config "$FRAG" )
|
||||||
|
fi
|
||||||
# CROSS_COMPILE defaults to the Luckfox SDK uclibc prefix (set SDK_TC to its bin/),
|
# CROSS_COMPILE defaults to the Luckfox SDK uclibc prefix (set SDK_TC to its bin/),
|
||||||
# but the kernel is freestanding, so a caller may override with a generic arm cross
|
# but the kernel is freestanding, so a caller may override with a generic arm cross
|
||||||
# toolchain instead — e.g. CROSS_COMPILE=arm-linux-gnueabihf- (in Debian's
|
# toolchain instead — e.g. CROSS_COMPILE=arm-linux-gnueabihf- (in Debian's
|
||||||
@@ -108,9 +115,46 @@ command -v "${CROSS_COMPILE}gcc" >/dev/null \
|
|||||||
|| { echo "cross toolchain ${CROSS_COMPILE}gcc not on PATH (set SDK_TC, or CROSS_COMPILE to one that is)" >&2; exit 1; }
|
|| { echo "cross toolchain ${CROSS_COMPILE}gcc not on PATH (set SDK_TC, or CROSS_COMPILE to one that is)" >&2; exit 1; }
|
||||||
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" olddefconfig >/dev/null
|
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" olddefconfig >/dev/null
|
||||||
|
|
||||||
|
# Fragment took-effect assertion: merge_config -m only pastes text, and
|
||||||
|
# olddefconfig silently resolves any symbol whose dependencies are unmet —
|
||||||
|
# a fragment option could be dropped without a word. Verify every explicit
|
||||||
|
# request in the fragment survived into the final .config; fail loud if not.
|
||||||
|
if [ -n "${WARDEN_KCONFIG_FRAGMENT:-}" ]; then
|
||||||
|
frag_fail=0
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
CONFIG_*=*)
|
||||||
|
grep -qxF "$line" "$SRC/.config" || {
|
||||||
|
echo "FATAL: fragment option '$line' did not take effect (unmet Kconfig dependency?)" >&2
|
||||||
|
frag_fail=1
|
||||||
|
} ;;
|
||||||
|
"# CONFIG_"*" is not set")
|
||||||
|
# A disable succeeded if the symbol is NOT set: Kconfig writes either
|
||||||
|
# the literal "is not set" line or (when dependencies gate the symbol
|
||||||
|
# out) nothing at all — both are valid outcomes. Only "still =value"
|
||||||
|
# is a failed disable. (A typo'd symbol disables nothing and is
|
||||||
|
# harmless by construction.)
|
||||||
|
opt="${line#\# }"; opt="${opt% is not set}"
|
||||||
|
grep -qE "^$opt=" "$SRC/.config" && {
|
||||||
|
echo "FATAL: fragment disabled '$opt' but it is still set in the final .config" >&2
|
||||||
|
frag_fail=1
|
||||||
|
} ;;
|
||||||
|
esac
|
||||||
|
done < "$FRAG"
|
||||||
|
[ "$frag_fail" = 0 ] || exit 1
|
||||||
|
log "fragment options verified in final .config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optional ccache (CI: cache ~/.ccache across dispatches; harmless if unset).
|
||||||
|
KCC="${CROSS_COMPILE}gcc"
|
||||||
|
if [ "${WARDEN_CCACHE:-0}" = 1 ]; then
|
||||||
|
command -v ccache >/dev/null || { echo "FATAL: WARDEN_CCACHE=1 but ccache not installed" >&2; exit 1; }
|
||||||
|
KCC="ccache ${CROSS_COMPILE}gcc"
|
||||||
|
fi
|
||||||
|
|
||||||
# 5. build zImage + the board dtb
|
# 5. build zImage + the board dtb
|
||||||
log "building zImage + rv1106-warden.dtb (-j$JOBS)"
|
log "building zImage + rv1106-warden.dtb (-j$JOBS)"
|
||||||
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" -j"$JOBS" \
|
make -C "$SRC" ARCH=arm CROSS_COMPILE="$CROSS_COMPILE" CC="$KCC" -j"$JOBS" \
|
||||||
zImage rockchip/rv1106-warden.dtb
|
zImage rockchip/rv1106-warden.dtb
|
||||||
|
|
||||||
Z="$SRC/arch/arm/boot/zImage"
|
Z="$SRC/arch/arm/boot/zImage"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetch (with retries) and sha256-verify the pristine kernel tarball into $1.
|
||||||
|
# Single source of truth for the URL + verification used by build-kernel.sh
|
||||||
|
# and both CI jobs (patches-apply, kernel-build) — a KVER bump edits this file
|
||||||
|
# and build-kernel.sh only. FAILS CLOSED: a missing pin refuses to proceed.
|
||||||
|
#
|
||||||
|
# Usage: fetch-kernel-tarball.sh <destination-path>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
KVER=6.18.46
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # build/
|
||||||
|
SHA_FILE="$HERE/linux-$KVER.tar.xz.sha256"
|
||||||
|
URL="https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-$KVER.tar.xz"
|
||||||
|
|
||||||
|
TB="${1:?usage: fetch-kernel-tarball.sh <destination-path>}"
|
||||||
|
|
||||||
|
# Pin first: a forgotten pin on a KVER bump should refuse BEFORE burning a
|
||||||
|
# 140MB download it will then reject anyway.
|
||||||
|
[ -f "$SHA_FILE" ] || {
|
||||||
|
echo "FATAL: no pinned sha256 for linux-$KVER (expected $SHA_FILE) — refusing an unverified tarball" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if [ ! -f "$TB" ]; then
|
||||||
|
echo "== downloading $URL"
|
||||||
|
curl --retry 3 --retry-delay 5 --retry-connrefused -fSL "$URL" -o "$TB"
|
||||||
|
fi
|
||||||
|
want="$(cat "$SHA_FILE")"
|
||||||
|
got="$(sha256sum "$TB" | awk '{print $1}')"
|
||||||
|
[ "$want" = "$got" ] || { echo "FATAL: tarball sha256 mismatch: want $want got $got" >&2; exit 1; }
|
||||||
|
echo "== tarball sha256 verified: $TB"
|
||||||
+34
-5
@@ -65,13 +65,15 @@ supervisor logic runs in CI with no panel.
|
|||||||
the boot-mode register's survives-warm-reset / cleared-by-POR behaviour (the MaskRom
|
the boot-mode register's survives-warm-reset / cleared-by-POR behaviour (the MaskRom
|
||||||
recovery maneuver). The matching firmware-side `Bus` seam on flared's `devmem` — so
|
recovery maneuver). The matching firmware-side `Bus` seam on flared's `devmem` — so
|
||||||
the shipped ladder can be asserted to poke the confirmed offset, never the wrong-SoC
|
the shipped ladder can be asserted to poke the confirmed offset, never the wrong-SoC
|
||||||
one — lands when flare-edge consumes warden-sdk (§7 item 3, [maintainer]-gated), not yet on
|
one — lands when flare-edge consumes warden-sdk (§8 item 3, maintainer-gated), not yet on
|
||||||
flare-edge `main`.
|
flare-edge `main`.
|
||||||
- **`modbus` — RS-485 device end.** Done. `ModbusSlave`: a byte-in/byte-out RTU slave
|
- **`modbus` — RS-485 device end.** Done. `ModbusSlave`: a byte-in/byte-out RTU slave
|
||||||
(CRC16 byte-identical to the master, FC 0x01–0x06/0x0F/0x10/0x11, exception replies,
|
(CRC16 byte-identical to the master, FC 0x01–0x06/0x0F/0x10/0x11, exception replies,
|
||||||
and fault injection — silent-drop and forced-NAK) so `warden-modbus`'s master can be
|
and fault injection — silent-drop and forced-NAK) so `warden-modbus`'s master can be
|
||||||
hardened to MC/DC against realistic device behaviour with no serial hardware. MEI
|
hardened to MC/DC against realistic device behaviour with no serial hardware. MEI
|
||||||
(0x2B/0x0E) identification is the documented follow-up.
|
(0x2B/0x0E) identification is the documented follow-up. The same slave also serves
|
||||||
|
as the QEMU device sim's field bus: `qemu/rs485-bridge/` feeds it from a serial
|
||||||
|
chardev so the guest's real master polls it over what it believes is /dev/ttyS4 (§7).
|
||||||
- **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact
|
- **`npu` — NPU load model.** Done. `NpuSim` models `/proc/rknpu/load` (the exact
|
||||||
"NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI
|
"NPU load: N%" text the sysmon reads) behind the path seam, so the load-readout UI
|
||||||
is host-testable. NPU *compute* is explicitly out of scope — no inference runs here.
|
is host-testable. NPU *compute* is explicitly out of scope — no inference runs here.
|
||||||
@@ -86,7 +88,7 @@ supervisor logic runs in CI with no panel.
|
|||||||
Integration with flare-edge: flared implements `MemBus` for `/dev/mem` and gains
|
Integration with flare-edge: flared implements `MemBus` for `/dev/mem` and gains
|
||||||
`#[cfg(test)]` tests driving its real arm/beat logic against `HpmcuSim`. This needs
|
`#[cfg(test)]` tests driving its real arm/beat logic against `HpmcuSim`. This needs
|
||||||
warden-sdk reachable as a Cargo dependency in CI — i.e. a remote for this repo,
|
warden-sdk reachable as a Cargo dependency in CI — i.e. a remote for this repo,
|
||||||
which is a **[maintainer]-go-ahead item** (credential/remote creation). Until then the
|
which is a **maintainer go-ahead item** (credential/remote creation). Until then the
|
||||||
firmware-side seam and a local test double land in flare-edge, unified with `sim/`
|
firmware-side seam and a local test double land in flare-edge, unified with `sim/`
|
||||||
once the dependency exists. No duplication of *logic* — only the tiny trait.
|
once the dependency exists. No duplication of *logic* — only the tiny trait.
|
||||||
|
|
||||||
@@ -152,14 +154,41 @@ kernel move as one matched boot+oem image, never a partial reflash.
|
|||||||
series honest against pristine 6.18.46; provenance is in `patches/README.md` and
|
series honest against pristine 6.18.46; provenance is in `patches/README.md` and
|
||||||
`kernel/rv1106-enablement/`.
|
`kernel/rv1106-enablement/`.
|
||||||
|
|
||||||
## 7. Order of work
|
## 7. Device emulation (`qemu/`) — see ADR-0006
|
||||||
|
|
||||||
|
The third simulator, deliberately not named "sim": a QEMU VM (`-M virt,highmem=off`,
|
||||||
|
one Cortex-A7, 256M — the RV1106G3's shape) that boots the real forward-ported
|
||||||
|
kernel and real userspace, entering at `-kernel zImage` because everything below
|
||||||
|
(BootROM, idblock/DDR-init, U-Boot, the BCB A/B machinery) is closed blobs plus
|
||||||
|
mask ROM. The canonical RV1106 zImage boots virt unmodified; an additive kconfig
|
||||||
|
fragment (`qemu/configs/virt.fragment` via `WARDEN_KCONFIG_FRAGMENT`) adds the
|
||||||
|
scenario devices (PCI serial for RS485, i6300esb watchdog, WireGuard,
|
||||||
|
virtio-gpu/input for the 720x720 UI). The virtio disk carries the device's exact
|
||||||
|
12-partition `blkdevparts=` A/B layout and the `/dev/block/by-name/` contract.
|
||||||
|
|
||||||
|
Where the seams meet: the guest runs the *real* binaries (static musl flared,
|
||||||
|
the LVGL fbdev UI); the RS485 bridge (`qemu/rs485-bridge/`) connects a QEMU
|
||||||
|
serial chardev to `sim/`'s `ModbusSlave`, so the register-level models serve as
|
||||||
|
the VM's field bus — behavior lives in one place, `sim/`, and the VM consumes
|
||||||
|
it. Scenario tests: `qemu/tests/boot-smoke.sh` (CI, in kernel-build),
|
||||||
|
`portal-scenario.sh` (check-in + OTA offer download against flare-edge's mock
|
||||||
|
portal), `ui-shot.sh` (QMP screendump + touch injection). Division of labour
|
||||||
|
with the other sims: NPU/RGA/HPMCU *behavior* stays `sim/`; UI *rendering
|
||||||
|
development* stays `lvglsim`; the VM is where processes, the kernel, and the
|
||||||
|
network meet. §5 still applies — no behavioural sim, this one included, catches
|
||||||
|
memory-map faults; and "boots under emulation" is never on-silicon evidence.
|
||||||
|
|
||||||
|
## 8. Order of work
|
||||||
|
|
||||||
1. **Simulator core** — `membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the
|
1. **Simulator core** — `membus`, `hpmcu`, the `cru` reset ladder, `modbus`, plus the
|
||||||
`rga`/`npu` models. **Done.**
|
`rga`/`npu` models. **Done.**
|
||||||
2. **C-driver MC/DC harnesses** — `relays.c` and `freshness.c` at 100% MC/DC, CI-gated
|
2. **C-driver MC/DC harnesses** — `relays.c` and `freshness.c` at 100% MC/DC, CI-gated
|
||||||
via the shared `drivers/enforce-mcdc.sh`. **Done** (the first C coverage gate).
|
via the shared `drivers/enforce-mcdc.sh`. **Done** (the first C coverage gate).
|
||||||
3. **flared devmem/hpmcu seam + tests** — firmware-side trait, unified with `sim/`
|
3. **flared devmem/hpmcu seam + tests** — firmware-side trait, unified with `sim/`
|
||||||
once flare-edge consumes warden-sdk (a separate, [maintainer]-gated step). **Pending.**
|
once flare-edge consumes warden-sdk (a separate, maintainer-gated step). **Pending.**
|
||||||
4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.**
|
4. **Config-lint CI gates** (§5) — the brick-class of bug. **Done.**
|
||||||
5. **Hermetic kernel build** (`build/build-kernel.sh` + the `patches-apply` gate). **Done.**
|
5. **Hermetic kernel build** (`build/build-kernel.sh` + the `patches-apply` gate). **Done.**
|
||||||
6. **Kernel 5.10→6.18.46 forward-port** (§6, ADR-0001). **Done** (hardware-verified).
|
6. **Kernel 5.10→6.18.46 forward-port** (§6, ADR-0001). **Done** (hardware-verified).
|
||||||
|
7. **QEMU device sim** (§7, ADR-0006) — boot smoke, A/B disk harness, RS485 bridge,
|
||||||
|
portal/watchdog/clock scenarios, display+touch. **Done** (emulation-verified;
|
||||||
|
booting the real flare-edge rootfs+oem image pair is a documented later milestone).
|
||||||
|
|||||||
+18
-36
@@ -1,51 +1,33 @@
|
|||||||
# CI/CD
|
# CI/CD
|
||||||
|
|
||||||
`.github/workflows/ci.yml` — everything portable runs on GitHub-hosted
|
`.github/workflows/ci.yml` — every job runs on GitHub-hosted `ubuntu-latest`.
|
||||||
`ubuntu-latest`; only the heavy kernel build uses the self-hosted runner.
|
No self-hosted runner is (or may be) reachable from this repo's workflows —
|
||||||
|
on a public repo, a fork PR that gets one approved run could otherwise
|
||||||
|
execute code on private infrastructure (ADR-0007).
|
||||||
|
|
||||||
## Jobs
|
## Jobs
|
||||||
|
|
||||||
| Job | Runner | What it does |
|
| Job | Runner | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `test` | ubuntu-latest | `cargo test` (sim + config-lint) + `cargo-llvm-cov` line coverage on `sim`; outputs `passed`/`coverage`. |
|
| `test` | ubuntu-latest | `cargo test` (sim + config-lint + qemu/rs485-bridge) + `cargo-llvm-cov` line coverage on `sim`; outputs `passed`/`coverage`. |
|
||||||
| `mcdc` | ubuntu-latest | 100% MC/DC enforced on every `drivers/*/test` (gcc-14 `-fcondition-coverage`). |
|
| `mcdc` | ubuntu-latest | 100% MC/DC enforced on every `drivers/*/test` (gcc-14 `-fcondition-coverage`). |
|
||||||
| `bench` | ubuntu-latest | Smoke-runs the sim micro-benchmarks; emits ns/op trend JSON. |
|
| `bench` | ubuntu-latest | Smoke-runs the sim + rs485-bridge micro-benchmarks; emits ns/op trend JSON. |
|
||||||
| `patches-apply` | ubuntu-latest | Fetches pristine linux-6.18.46 (cached, sha256-verified) and applies `patches/*` in order. |
|
| `patches-apply` | ubuntu-latest | Fetches pristine linux-6.18.46 (cached, sha256-verified) and applies `patches/*` in order. |
|
||||||
| `kernel-build` | **[self-hosted, warden-sdk]** | `build/build-kernel.sh` → `zImage` + `rv1106-warden.dtb`, uploaded as an artifact. Dispatch-gated until the runner is fully provisioned (below). |
|
| `qemu-tools` | ubuntu-latest | shellcheck on `qemu/**.sh`; builds the initramfs (pinned busybox) and the A/B disk image. |
|
||||||
|
| `kernel-build` | ubuntu-latest, **dispatch-only** | apt-installs the cross toolchain + qemu, `build/build-kernel.sh` → `zImage` + `rv1106-warden.dtb`, QEMU `-M virt` boot smoke (fail-closed), artifact upload (best-effort). Trigger: `gh workflow run ci.yml`. |
|
||||||
|
| `prune-artifacts` | ubuntu-latest, dispatch-only | Deletes `kernel-rv1106` artifacts beyond the newest 3. |
|
||||||
| `badges` | ubuntu-latest | Renders loc/tests/coverage shields on push to `main` (`[skip ci]` + `paths-ignore` loop guard). |
|
| `badges` | ubuntu-latest | Renders loc/tests/coverage shields on push to `main` (`[skip ci]` + `paths-ignore` loop guard). |
|
||||||
|
|
||||||
## The self-hosted runner (`bfe-mpc-0640`)
|
## History: the self-hosted runner (retired)
|
||||||
|
|
||||||
A **third** repo-scoped runner instance on `bfe-mpc-0640` (alongside `flare` and
|
`kernel-build` originally ran on a repo-scoped self-hosted runner (ADR-0004,
|
||||||
`flare-edge`), registered with the label **`warden-sdk`** as
|
2026-08-25, verified end-to-end) because hosted minutes were metered on the
|
||||||
`bfe-mpc-0640-warden-sdk`, in `~/actions-runner-warden-sdk`.
|
private repo. Going public made hosted minutes free and made a self-hosted
|
||||||
|
registration a liability, so ADR-0007 moved the job to `ubuntu-latest` and
|
||||||
> **INSTALLED + ONLINE (2026-08-25).** The runner is a running systemd service
|
retired the registration. Site-specific install records for that runner live
|
||||||
> (`actions.runner.bfe-noah-warden-sdk.bfe-mpc-0640-warden-sdk.service`, `enabled`,
|
in our private deployment log, not here.
|
||||||
> cgroup-capped `CPUQuota=400%`/`MemoryMax=6G`) and `kernel-build` has been verified
|
|
||||||
> end-to-end (RV1106 6.18.46 → `zImage` 8.25 MB + `rv1106-warden.dtb`). Steps 1–2
|
|
||||||
> below are the record of that install (they needed `user`'s sudo on 0640); steps
|
|
||||||
> 3–4 are handled inside the workflow, so the host needs no manual toolchain/python.
|
|
||||||
|
|
||||||
1. **Install as a service** (persistence): `cd ~/actions-runner-warden-sdk &&
|
|
||||||
sudo ./svc.sh install user && sudo ./svc.sh start`. Until then the runner is
|
|
||||||
*offline* and `kernel-build` only runs when dispatched against an online runner.
|
|
||||||
2. **Resource cap** (protect the shared host): a drop-in at
|
|
||||||
`/etc/systemd/system/actions.runner.bfe-noah-warden-sdk.*.service.d/*.conf` with
|
|
||||||
`CPUQuota=400%` + `MemoryMax=6G`, then `sudo systemctl daemon-reload`. The build
|
|
||||||
inherits that cgroup. (The workflow also passes `JOBS=4` as a belt-and-braces bound.)
|
|
||||||
3. **Kernel cross toolchain** — done in the workflow: the `kernel-build` job sets
|
|
||||||
`CROSS_COMPILE=arm-linux-gnueabihf-` (Debian `gcc-arm-linux-gnueabihf`, already on
|
|
||||||
the runner) and `build-kernel.sh` honors it. The kernel is freestanding, so the
|
|
||||||
generic arm cross compiler links it — no Luckfox SDK toolchain path needed. (To use
|
|
||||||
the SDK uclibc toolchain instead, set `SDK_TC` to its `bin/` and drop the override.)
|
|
||||||
4. **`python`** (not python3) — done in the workflow: the `kernel-build` job symlinks
|
|
||||||
`python`→`python3` into `$RUNNER_TEMP/bin` and prepends it to `$GITHUB_PATH`. No
|
|
||||||
host-side venv/shim needed.
|
|
||||||
|
|
||||||
Host build deps: `dtc bc flex bison libssl-dev` — already present on 0640.
|
|
||||||
|
|
||||||
## Badges
|
## Badges
|
||||||
|
|
||||||
Static shields SVGs are committed by the `badges` job (private repo can't use
|
Static shields SVGs are committed by the `badges` job. The GitHub-native
|
||||||
dynamic shields). The GitHub-native `ci.yml` status badge works live regardless.
|
`ci.yml` status badge works live regardless.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# ADR 0003 — warden-sdk is a standalone repo
|
# ADR 0003 — warden-sdk is a standalone repo
|
||||||
|
|
||||||
**Status:** Accepted (2026-08-25).
|
**Status:** Accepted (2026-08-25). Repo-visibility half superseded by ADR-0007
|
||||||
|
(2026-08-30) — warden-sdk went public; the "private for now" consequence below
|
||||||
|
no longer holds. Original decision kept for the record.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
Our real SDK changes lived as uncommitted edits in a 2GB opaque vendor fork, with
|
Our real SDK changes lived as uncommitted edits in a 2GB opaque vendor fork, with
|
||||||
@@ -10,11 +12,11 @@ no CI, tests, or versioning of their own. The SDK requirement (future-features-2
|
|||||||
## Decision
|
## Decision
|
||||||
A **private** `bfe-noah/warden-sdk` GitHub repo, standalone from day one with its own
|
A **private** `bfe-noah/warden-sdk` GitHub repo, standalone from day one with its own
|
||||||
CI/versioning. Work lands on a `bringup` branch; the first commit to `main` is gated
|
CI/versioning. Work lands on a `bringup` branch; the first commit to `main` is gated
|
||||||
on a passing code-review-harness run, green CI, and [maintainer]'s fresh explicit go-ahead.
|
on a passing review run, green CI, and the maintainer's fresh explicit go-ahead.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
- flare-edge consumes warden-sdk later (flared depending on `warden-sim`, drivers
|
- flare-edge consumes warden-sdk later (flared depending on `warden-sim`, drivers
|
||||||
built from here) — a separate, [maintainer]-gated integration step; flare-edge is not
|
built from here) — a separate, maintainer-gated integration step; flare-edge is not
|
||||||
edited by the SDK-completion effort.
|
edited by the SDK-completion effort.
|
||||||
- Private for now (references bench devices / in-progress hardening); can be opened
|
- Private for now (references bench devices / in-progress hardening); can be opened
|
||||||
later once scrubbed, matching how `flare-deployment` is handled.
|
later once scrubbed, matching how `flare-deployment` is handled.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# ADR 0004 — CI/CD runner: 3rd repo-scoped self-hosted runner on 0640
|
# ADR 0004 — CI/CD runner: 3rd repo-scoped self-hosted runner on 0640
|
||||||
|
|
||||||
**Status:** Accepted (2026-08-25).
|
**Status:** Superseded in part by ADR-0007 (2026-08-30) — `kernel-build` moved
|
||||||
|
to GitHub-hosted runners for the public repo and the self-hosted registration
|
||||||
|
is retired. Original decision below, kept for the record. (2026-08-25.)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
The heavy kernel/firmware build needs the SDK toolchain and Buildroot's baked-in
|
The heavy kernel/firmware build needs the SDK toolchain and Buildroot's baked-in
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ scratch tree (`flare-edge/research/linux-6.18.46/`).
|
|||||||
Bring **hardened copies into `warden-sdk/drivers/`** as the canonical source-of-truth,
|
Bring **hardened copies into `warden-sdk/drivers/`** as the canonical source-of-truth,
|
||||||
each with its HAL seam and a 100% MC/DC host harness. The RV1106 kernel deltas are
|
each with its HAL seam and a 100% MC/DC host harness. The RV1106 kernel deltas are
|
||||||
formalized as a patch series in `patches/`. flare-edge consumes warden-sdk later
|
formalized as a patch series in `patches/`. flare-edge consumes warden-sdk later
|
||||||
(separate, [maintainer]-gated step).
|
(separate, maintainer-gated step).
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
- Realizes the seam architecture (ADR-referenced in `docs/architecture.md`).
|
- Realizes the seam architecture (ADR-referenced in `docs/architecture.md`).
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# ADR 0006 — QEMU device sim: generic `-M virt`, entered at the kernel
|
||||||
|
|
||||||
|
**Status:** Accepted (2026-08-29).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The two existing simulators cannot test the *device*: `lvglsim` (flare-edge)
|
||||||
|
is an SDL rendering harness, and `sim/` models registers behind driver seams.
|
||||||
|
Init ordering, the daemons as real processes, networking/enrollment against
|
||||||
|
FLARE, OTA, and the watchdog were testable only on a bench panel — flare-edge's
|
||||||
|
fault suite marks five scenarios "HIL, human prompts", its Playwright e2e needs
|
||||||
|
a live panel on the LAN, and its OTA desk test stops at "reached APPLYING".
|
||||||
|
|
||||||
|
Two ways to emulate the panel were considered:
|
||||||
|
|
||||||
|
1. **A custom RV1106 QEMU board model.** Nothing exists upstream or in the
|
||||||
|
community, so this means writing VOP/CRU/GRF/eMMC/HPMCU device models from
|
||||||
|
scratch and maintaining them against QEMU — months of work that duplicates
|
||||||
|
what `sim/` already models in Rust. It still could not run the boot chain:
|
||||||
|
BootROM is mask ROM and the DDR-init/idblock stages are closed rkbin blobs.
|
||||||
|
2. **The generic `-M virt` machine, entering at `-kernel zImage`.** The
|
||||||
|
forward-ported 6.18.46 config is multi_v7-derived and already carries
|
||||||
|
`ARCH_VIRT` plus the full virtio set — the canonical RV1106 zImage boots
|
||||||
|
virt **unmodified** (verified 2026-08-29). Peripherals become virtio
|
||||||
|
substitutes; SoC-block behavior stays in `sim/`, bridged in (the RS485
|
||||||
|
chardev bridge) rather than re-modeled.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Option 2. `qemu/` holds the harness: `qemu-system-arm -M virt,highmem=off
|
||||||
|
-cpu cortex-a7 -smp 1 -m 256M` (the RV1106G3's shape), one canonical kernel
|
||||||
|
image plus an optional additive config fragment (`qemu/configs/virt.fragment`
|
||||||
|
via the `WARDEN_KCONFIG_FRAGMENT` hook — PCI/pci-serial/i6300esb/WireGuard/
|
||||||
|
virtio-gpu/virtio-input; the RV1106 build is byte-identical with the variable
|
||||||
|
unset). The VM carries the device's real 12-partition `blkdevparts=` A/B
|
||||||
|
layout on a virtio disk and populates the `/dev/block/by-name/` contract.
|
||||||
|
The name is `qemu/`, not any variant of "sim" — the wikis already warn that
|
||||||
|
"sim" is two different things.
|
||||||
|
|
||||||
|
Notable mechanics: `highmem=off` because the non-LPAE 32-bit kernel cannot
|
||||||
|
reach virt's default 40-bit PCIe ECAM; `-global virtio-mmio.force-legacy=false`
|
||||||
|
because virtio-gpu/input are VERSION_1-only devices.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Everything below the kernel is **out of scope**: BootROM, idblock, U-Boot,
|
||||||
|
the real BCB-driven A/B selection and bootcount auto-revert. The initramfs
|
||||||
|
`warden.slot=` switch emulates U-Boot's *choice*, not the mechanism. The
|
||||||
|
untested A/B rollback chain stays bench territory.
|
||||||
|
- Display, input, network, and storage are **substitutes** (virtio), not
|
||||||
|
models. "Boots/works under emulation" is never evidence of "works on
|
||||||
|
silicon"; the VM narrows which claims need a panel.
|
||||||
|
- The VM is the first environment that runs production userspace binaries on
|
||||||
|
a non-RV1106 physical memory map, which makes it a canary for baked-in
|
||||||
|
hardware assumptions (it immediately found flare-edge #106, a fatal SIGBUS
|
||||||
|
in flared's HPMCU probe, and #107, a Y2038 time_t truncation in the UI).
|
||||||
|
- The guest deliberately deviates from production in documented ways
|
||||||
|
(`WARDEN_FLARE_INSECURE=1` for the desk mock portal, `WARDEN_HPMCU=0`);
|
||||||
|
qemu/README.md carries the emulated-vs-not table.
|
||||||
|
- The kernel-build CI job gains a fail-closed qemu boot smoke; hosted runners
|
||||||
|
build (but cannot boot) the initramfs and disk image.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# ADR 0007 — Public repo: kernel-build moves to GitHub-hosted runners
|
||||||
|
|
||||||
|
**Status:** Accepted (2026-08-30). Supersedes the runner half of ADR-0004.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The repo is going public (free Actions minutes for hosted runners; open-source
|
||||||
|
alignment with the stack philosophy). Two facts change the ADR-0004 calculus:
|
||||||
|
|
||||||
|
1. **A self-hosted runner on a public repo is a standing hazard.** A fork PR
|
||||||
|
can modify workflow files; once any run of theirs is approved, workflows
|
||||||
|
can target the repo's registered self-hosted runners — i.e. arbitrary code
|
||||||
|
on the private host, which also serves production. GitHub's own guidance is
|
||||||
|
to never attach self-hosted runners to public repos, and personal-account
|
||||||
|
repos have no runner groups to scope the risk away.
|
||||||
|
2. **The build never needed the SDK host.** ADR-0004's premise ("needs the SDK
|
||||||
|
toolchain and Buildroot's baked-in absolute paths") does not apply to
|
||||||
|
`kernel-build`: the hermetic build is freestanding, uses Debian's
|
||||||
|
`gcc-arm-linux-gnueabihf`, and self-provisions `python`. It fits a hosted
|
||||||
|
runner (4 vCPU / 16 GB), and public-repo minutes are free.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
`kernel-build` runs on `ubuntu-latest`, apt-installing its toolchain, kernel
|
||||||
|
build deps, and qemu-system-arm, with the pristine tarball cached like
|
||||||
|
`patches-apply` does. It stays `workflow_dispatch`-only for now (a full build
|
||||||
|
per push is still noisy; flipping it to push-on-main later is one line). The
|
||||||
|
`warden-sdk` self-hosted runner instance is **deregistered from this repo**
|
||||||
|
before it goes public; the flare and flare-edge runner instances on the same
|
||||||
|
host are unaffected (those repos stay private).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- No path from public workflows to private infrastructure; nothing to babysit
|
||||||
|
in fork-PR approval settings beyond GitHub's defaults (still set "require
|
||||||
|
approval for all outside contributors" as belt-and-braces).
|
||||||
|
- Kernel artifacts no longer persist on the runner host; the GitHub artifact
|
||||||
|
(5-day retention + prune job) is the only build output channel.
|
||||||
|
- Hosted kernel builds are slower than the 0640 box but free and parallel;
|
||||||
|
the boot-smoke step rides along unchanged.
|
||||||
+1
-1
@@ -38,7 +38,7 @@ is already modelled and tested here** in `../sim/`:
|
|||||||
**Why the Tier-2 *source* isn't vendored here yet:** `modbus_engine.c` and
|
**Why the Tier-2 *source* isn't vendored here yet:** `modbus_engine.c` and
|
||||||
`warden_rga.c` pull in shared UI headers (`platform.h`, `settings.h`, `lv_*`) and
|
`warden_rga.c` pull in shared UI headers (`platform.h`, `settings.h`, `lv_*`) and
|
||||||
librga. Copying those in would duplicate exactly the shared surface the
|
librga. Copying those in would duplicate exactly the shared surface the
|
||||||
**flare-edge↔warden-sdk unification** (ADR-0003/0005, a separate [maintainer]-gated step) is
|
**flare-edge↔warden-sdk unification** (ADR-0003/0005, a separate maintainer-gated step) is
|
||||||
meant to resolve cleanly. So the Tier-2 *models* (the hardware ends) live here now;
|
meant to resolve cleanly. So the Tier-2 *models* (the hardware ends) live here now;
|
||||||
the Tier-2 *driver sources* migrate in with the unification, at which point their
|
the Tier-2 *driver sources* migrate in with the unification, at which point their
|
||||||
existing flare-edge harnesses point at this repo.
|
existing flare-edge harnesses point at this repo.
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ if ! echo "$cond_line" | grep -q "100.00%"; then
|
|||||||
rc=1
|
rc=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ "$rc" = "0" ] && echo "RESULT: 100% MC/DC + all checks green ✓"
|
[ "$rc" = "0" ] && echo "RESULT: 100% MC/DC + all checks green OK"
|
||||||
exit "$rc"
|
exit "$rc"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RV1106 → Linux 6.18 forward-port (self-built, from vendor 5.10)
|
# RV1106 → Linux 6.18 forward-port (self-built, from vendor 5.10)
|
||||||
|
|
||||||
**Decision ([maintainer], 2026-08-23):** forward-port the RV1106 SoC enablement from the
|
**Decision (2026-08-23):** forward-port the RV1106 SoC enablement from the
|
||||||
Rockchip **vendor 5.10** tree straight to **Linux 6.18 LTS**, ourselves, using **no
|
Rockchip **vendor 5.10** tree straight to **Linux 6.18 LTS**, ourselves, using **no
|
||||||
plan44 code**, built on **our Buildroot 2025.02 LTS + uClibc**. We own the tree.
|
plan44 code**, built on **our Buildroot 2025.02 LTS + uClibc**. We own the tree.
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ findings — they worked on the first hardware try.**
|
|||||||
|
|
||||||
## The safe test path (A/B slot _b, never touch _a)
|
## The safe test path (A/B slot _b, never touch _a)
|
||||||
|
|
||||||
c8a3 is the XPS-connected Warden: eth0 `[bench-ip]` over USB-gadget ([bench-creds],
|
c8a3 is the desk-connected bench Warden: reachable over its USB-gadget ethernet
|
||||||
dropbear — use `scp -O`, no sftp-server), plus the serial console on xps
|
(dev-build dropbear; address and bench credentials live in the private
|
||||||
`/dev/ttyUSB2` @115200 and Zigbee power (`plug_cmd.py cycle`). It runs an A/B
|
deployment notes — use `scp -O`, no sftp-server), plus the serial console at
|
||||||
|
115200 and remotely switchable power. It runs an A/B
|
||||||
firmware (boot_a=mmcblk0p5 / boot_b=mmcblk0p6, 32 MiB each; rootfs_a/_b; AvbABData
|
firmware (boot_a=mmcblk0p5 / boot_b=mmcblk0p6, 32 MiB each; rootfs_a/_b; AvbABData
|
||||||
in `misc` sector 4 / byte 2048).
|
in `misc` sector 4 / byte 2048).
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ With the correct format, U-Boot loaded my kernel + my DTB and printed my DT mode
|
|||||||
string (`Model: WardenOS 86-Panel (RV1106) — M2 earlycon bring-up`), then
|
string (`Model: WardenOS 86-Panel (RV1106) — M2 earlycon bring-up`), then
|
||||||
`Starting kernel ...`.
|
`Starting kernel ...`.
|
||||||
|
|
||||||
## Result: ✅ M2 achieved — the 6.18 kernel boots on hardware
|
## Result: [x] M2 achieved — the 6.18 kernel boots on hardware
|
||||||
|
|
||||||
Six attempts, each auto-recovering to `_a`, then a clean boot:
|
Six attempts, each auto-recovering to `_a`, then a clean boot:
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ pre-MMU), then fixed:
|
|||||||
CP2102 bench adapter — so the M2 DT pins 115200 for readable bring-up; production
|
CP2102 bench adapter — so the M2 DT pins 115200 for readable bring-up; production
|
||||||
overrides to 1.5M.
|
overrides to 1.5M.
|
||||||
|
|
||||||
## M3 (same session): ✅ the full WardenOS runs on the 6.18 kernel
|
## M3 (same session): [x] the full WardenOS runs on the 6.18 kernel
|
||||||
|
|
||||||
Adding the eMMC `dw_mmc` node (`mmc@ffa90000`, clocks from cru + grf_cru) was the
|
Adding the eMMC `dw_mmc` node (`mmc@ffa90000`, clocks from cru + grf_cru) was the
|
||||||
only change M3 needed — the mmc/ext4 drivers are already in-config. The kernel
|
only change M3 needed — the mmc/ext4 drivers are already in-config. The kernel
|
||||||
@@ -96,7 +97,8 @@ won't load (vermagic → M5), and there's no backlight/framebuffer yet (→ M4).
|
|||||||
|
|
||||||
**Console lesson applied:** with `console=ttyS2,115200` the whole boot is readable
|
**Console lesson applied:** with `console=ttyS2,115200` the whole boot is readable
|
||||||
on the CP2102 (the 1.5M vendor rate is garbage on it). Serial login uses the same
|
on the CP2102 (the 1.5M vendor rate is garbage on it). Serial login uses the same
|
||||||
`c8a3_run.py` [bench-creds] helper as the 5.10 firmware — the userspace is unchanged.
|
`c8a3_run.py` helper (dev-build bench credentials, see private deployment notes)
|
||||||
|
as the 5.10 firmware — the userspace is unchanged.
|
||||||
|
|
||||||
Next: M4 (VOP2 display + panel + touch), M5 (AIC8800 SDIO port + our 4 patches),
|
Next: M4 (VOP2 display + panel + touch), M5 (AIC8800 SDIO port + our 4 patches),
|
||||||
M6 (RGA/watchdog/HPMCU/USB-OTG); plus a lean defconfig + Buildroot-on-6.18 cleanup.
|
M6 (RGA/watchdog/HPMCU/USB-OTG); plus a lean defconfig + Buildroot-on-6.18 cleanup.
|
||||||
|
|||||||
@@ -4,44 +4,44 @@ Every block in the vendor SoC DT (`rv1106.dtsi`), classified. Goal: a 6.18 drive
|
|||||||
for every capability the hardware actually has, open-source, verified. Camera/ISP
|
for every capability the hardware actually has, open-source, verified. Camera/ISP
|
||||||
is the only whole class deliberately skipped — the 86-Panel has no camera.
|
is the only whole class deliberately skipped — the 86-Panel has no camera.
|
||||||
|
|
||||||
## ✅ Done / at parity (verified on warden-c8a3)
|
## Done / at parity (verified on warden-c8a3)
|
||||||
CRU clk · pinctrl (+ioc/pmuioc) · GIC · arch timer · pl330 DMA · 8250 uart (×3) ·
|
CRU clk · pinctrl (+ioc/pmuioc) · GIC · arch timer · pl330 DMA · 8250 uart (×3) ·
|
||||||
dw_mmc eMMC · i2c · dw-wdt · **RTC** · **tsadc** · **RGA** (rga2, hw 3.3.87975) ·
|
dw_mmc eMMC · i2c · dw-wdt · **RTC** · **tsadc** · **RGA** (rga2, hw 3.3.87975) ·
|
||||||
PWM backlight · **USB host** (dwc3/xhci) + usb2phy · grf/pmu syscons.
|
PWM backlight · **USB host** (dwc3/xhci) + usb2phy · grf/pmu syscons.
|
||||||
|
|
||||||
## ✅ Verified this run (2026-08-25) — all on warden-c8a3, self-built 6.18.46
|
## Verified this run (2026-08-25) — all on warden-c8a3, self-built 6.18.46
|
||||||
- **AIC8800 wifi** — wlan0 up, scanned the site AP at −43 dBm (modules; `wifi/VERIFIED-on-c8a3.md`).
|
- **AIC8800 wifi** — wlan0 up, scanned the site AP at −43 dBm (modules; `wifi/VERIFIED-on-c8a3.md`).
|
||||||
- **TRNG** — /dev/hwrng, real HW entropy (`rng-otp/`).
|
- **TRNG** — /dev/hwrng, real HW entropy (`rng-otp/`).
|
||||||
- **OTP/nvmem** — rockchip-otp0 reads chip id (`rng-otp/`).
|
- **OTP/nvmem** — rockchip-otp0 reads chip id (`rng-otp/`).
|
||||||
- **GMAC** — eth0 Link Up 100 Mbps/Full (`gmac/`).
|
- **GMAC** — eth0 Link Up 100 Mbps/Full (`gmac/`).
|
||||||
- **SARADC** — iio:device0 reads 2 ch; the −22 was vref, not clk (`adc/SARADC-FIX.md`).
|
- **SARADC** — iio:device0 reads 2 ch; the −22 was vref, not clk (`adc/SARADC-FIX.md`).
|
||||||
|
|
||||||
## ✅ VOP display — VERIFIED this run (2026-08-25)
|
## VOP display — VERIFIED this run (2026-08-25)
|
||||||
Full WardenOS Dashboard renders on the 86-Panel on 6.18 (`_b`), webcam-verified,
|
Full WardenOS Dashboard renders on the 86-Panel on 6.18 (`_b`), webcam-verified,
|
||||||
pixel-identical to stock `_a`. Two VOP driver bugs were the final black-screen
|
pixel-identical to stock `_a`. Two VOP driver bugs were the final black-screen
|
||||||
cause: `rgb_dclk_pol` hardcoded inverted (panel needs 0), and the wrong primary
|
cause: `rgb_dclk_pol` hardcoded inverted (panel needs 0), and the wrong primary
|
||||||
scanout window (rv1106 uses **WIN1**, not rv1126's WIN2). Full chain + `_a`-vs-`_b`
|
scanout window (rv1106 uses **WIN1**, not rv1126's WIN2). Full chain + `_a`-vs-`_b`
|
||||||
register diff in `display/VERIFIED.md`.
|
register diff in `display/VERIFIED.md`.
|
||||||
|
|
||||||
## ✅ GT911 touch — VERIFIED this run (2026-08-25)
|
## GT911 touch — VERIFIED this run (2026-08-25)
|
||||||
UI responds to taps/swipes on the panel ([maintainer]-confirmed); GT911 detected
|
UI responds to taps/swipes on the panel (confirmed by hand at the panel); GT911 detected
|
||||||
(`ID 911, version 1060`), `/dev/input/event0` held by warden-ui. Fix:
|
(`ID 911, version 1060`), `/dev/input/event0` held by warden-ui. Fix:
|
||||||
`CONFIG_TOUCHSCREEN_GOODIX=y` (built-in — the rootfs `goodix.ko` is a 5.10 build
|
`CONFIG_TOUCHSCREEN_GOODIX=y` (built-in — the rootfs `goodix.ko` is a 5.10 build
|
||||||
that can't load on 6.18) + GT911 node on `&i2c3`. Details in `touch/VERIFIED.md`.
|
that can't load on 6.18) + GT911 node on `&i2c3`. Details in `touch/VERIFIED.md`.
|
||||||
|
|
||||||
## 🔨 In flight
|
## In flight
|
||||||
- **i2s-tdm** — DAI builds; needs the codec + card (below).
|
- **i2s-tdm** — DAI builds; needs the codec + card (below).
|
||||||
- **AIC8800 BT** — module built (6.18 vermagic); HCI bring-up not yet exercised.
|
- **AIC8800 BT** — module built (6.18 vermagic); HCI bring-up not yet exercised.
|
||||||
|
|
||||||
## ✅ audio — VERIFIED this run
|
## audio — VERIFIED this run
|
||||||
card `rv1106-acodec` + pcmC0D0p/c (`audio/`); audible test @ bench with display.
|
card `rv1106-acodec` + pcmC0D0p/c (`audio/`); audible test @ bench with display.
|
||||||
|
|
||||||
## Remaining blocks — final status (all real capabilities now verified)
|
## Remaining blocks — final status (all real capabilities now verified)
|
||||||
| Block | verdict | note |
|
| Block | verdict | note |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **mailbox** (HPMCU) | ✅ VERIFIED | A7↔RISC-V-SCR1 round-trip, 5/5 exact echoes (`mailbox/VERIFIED.md`). Took 3 hardware-found fixes: rv1106 num_chans=1 (1 shared IRQ, not 4), CLK_CORE_MCU IGNORE_UNUSED (6.18 was gating the coprocessor clock), + an open SCR1 echo firmware with A2B_INTEN. The /dev/mem SRAM watchdog stays as a separate dead-man's-switch. |
|
| **mailbox** (HPMCU) | VERIFIED | A7↔RISC-V-SCR1 round-trip, 5/5 exact echoes (`mailbox/VERIFIED.md`). Took 3 hardware-found fixes: rv1106 num_chans=1 (1 shared IRQ, not 4), CLK_CORE_MCU IGNORE_UNUSED (6.18 was gating the coprocessor clock), + an open SCR1 echo firmware with A2B_INTEN. The /dev/mem SRAM watchdog stays as a separate dead-man's-switch. |
|
||||||
| **NPU** (rknpu) | ✅ open driver VERIFIED; compute deferred | open GPL rknpu 0.9.2 driver, /dev/dri/card1, version ioctl PASS (`npu/VERIFIED.md`). Open *compute* (a regcmd compiler) is a from-scratch ~person-year register-RE project — no RV1106 prior art, no public TRM Part 2, mainline accel/rocket+Teflon are RK3588-only. Ship the driver, no blob. |
|
| **NPU** (rknpu) | [x] open driver VERIFIED; compute deferred | open GPL rknpu 0.9.2 driver, /dev/dri/card1, version ioctl PASS (`npu/VERIFIED.md`). Open *compute* (a regcmd compiler) is a from-scratch ~person-year register-RE project — no RV1106 prior art, no public TRM Part 2, mainline accel/rocket+Teflon are RK3588-only. Ship the driver, no blob. |
|
||||||
| **pvtm** | ✅ VERIFIED | both core+pmu PVT monitors probe; debugfs ring-osc reads (`pvtm/PORT-DONE.md`). |
|
| **pvtm** | VERIFIED | both core+pmu PVT monitors probe; debugfs ring-osc reads (`pvtm/PORT-DONE.md`). |
|
||||||
| **crypto-v3** (accel) | deferred (documented) | ~100 KB whole-subsystem replacement of mainline's rk3288 crypto + heavy crypto-API deltas; the **CPU crypto extensions (AES/SHA, batch2 =y) already cover the functional need** — an offload optimization, not a capability gap. |
|
| **crypto-v3** (accel) | deferred (documented) | ~100 KB whole-subsystem replacement of mainline's rk3288 crypto + heavy crypto-API deltas; the **CPU crypto extensions (AES/SHA, batch2 =y) already cover the functional need** — an offload optimization, not a capability gap. |
|
||||||
| camera/ISP, SPI | N/A | no such hardware on the 86-Panel. |
|
| camera/ISP, SPI | N/A | no such hardware on the 86-Panel. |
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ deferred item is the crypto *accelerator* (CPU crypto already covers it) and ope
|
|||||||
NPU *compute* (a person-year RE effort, scoped in `npu/OPEN-NPU-PLAN.md`). No
|
NPU *compute* (a person-year RE effort, scoped in `npu/OPEN-NPU-PLAN.md`). No
|
||||||
unexplored gap remains.
|
unexplored gap remains.
|
||||||
|
|
||||||
## ❌ Not applicable (no hardware on the 86-Panel)
|
## Not applicable (no hardware on the 86-Panel)
|
||||||
cif · csi2-dphy · mipi-csi2 · rkisp (all camera/ISP) · SPI (no on-board SPI device).
|
cif · csi2-dphy · mipi-csi2 · rkisp (all camera/ISP) · SPI (no on-board SPI device).
|
||||||
|
|
||||||
## Order (after wifi)
|
## Order (after wifi)
|
||||||
|
|||||||
@@ -3,47 +3,47 @@
|
|||||||
Goal: every driver the panel's 5.10 kernel runs must work at ≥ parity on our
|
Goal: every driver the panel's 5.10 kernel runs must work at ≥ parity on our
|
||||||
self-built 6.18. Source of truth = the running 5.10 system on warden-c8a3
|
self-built 6.18. Source of truth = the running 5.10 system on warden-c8a3
|
||||||
(`lsmod` + `/proc/interrupts`, captured 2026-08-24). Verify each on hardware via
|
(`lsmod` + `/proc/interrupts`, captured 2026-08-24). Verify each on hardware via
|
||||||
the A/B `_b`-slot loop (`../docs/m2-boot-on-c8a3.md`); ✅ means confirmed on
|
the A/B `_b`-slot loop (`../docs/m2-boot-on-c8a3.md`); [x] means confirmed on
|
||||||
c8a3, not just compiled.
|
c8a3, not just compiled.
|
||||||
|
|
||||||
| Driver / node | 5.10 evidence | mainline? | 6.18 status |
|
| Driver / node | 5.10 evidence | mainline? | 6.18 status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CRU clock (clk-rv1106) | — | ported | ✅ M2 |
|
| CRU clock (clk-rv1106) | — | ported | [x] M2 |
|
||||||
| pinctrl-rockchip (rv1106) | — | ported | ✅ M2 |
|
| pinctrl-rockchip (rv1106) | — | ported | [x] M2 |
|
||||||
| GIC-400 / arch_timer | arch_timer | mainline | ✅ M2 |
|
| GIC-400 / arch_timer | arch_timer | mainline | [x] M2 |
|
||||||
| dw_mmc (eMMC) | dw-mci | mainline | ✅ M3 |
|
| dw_mmc (eMMC) | dw-mci | mainline | [x] M3 |
|
||||||
| 8250 uart2 (console) | ttyS2 | mainline | ✅ M2 |
|
| 8250 uart2 (console) | ttyS2 | mainline | [x] M2 |
|
||||||
| GPIO (rockchip, ×5 banks) | gpio-rockchip | mainline | ✅ batch1 (chips 0–4) |
|
| GPIO (rockchip, ×5 banks) | gpio-rockchip | mainline | [x] batch1 (chips 0–4) |
|
||||||
| DMA (pl330, ff420000) | ff420000.dma-controller | mainline | ✅ batch1 |
|
| DMA (pl330, ff420000) | ff420000.dma-controller | mainline | [x] batch1 |
|
||||||
| uart1 / uart4 | ttyS1, ttyS4 | mainline | ✅ batch1 |
|
| uart1 / uart4 | ttyS1, ttyS4 | mainline | [x] batch1 |
|
||||||
| I2C (dw-apb, ff460000=i2c3) | ff460000.i2c | mainline | ✅ batch1 (i2c-3) |
|
| I2C (dw-apb, ff460000=i2c3) | ff460000.i2c | mainline | [x] batch1 (i2c-3) |
|
||||||
| watchdog (dw-wdt, ff5a0000) | ff5a0000.watchdog | mainline | ✅ batch1 (watchdog0) |
|
| watchdog (dw-wdt, ff5a0000) | ff5a0000.watchdog | mainline | [x] batch1 (watchdog0) |
|
||||||
| tsadc thermal (ff3c8000) | rockchip_thermal | ported (data+init+macros) | ✅ soc-thermal reads 39.8°C |
|
| tsadc thermal (ff3c8000) | rockchip_thermal | ported (data+init+macros) | [x] soc-thermal reads 39.8°C |
|
||||||
| SARADC (ff3c0000) | ff3c0000.saradc | ported (2-ch v2 data) | ✅ iio:device0 reads 2ch (adc-keys); fixed -22 via vref-supply |
|
| SARADC (ff3c0000) | ff3c0000.saradc | ported (2-ch v2 data) | [x] iio:device0 reads 2ch (adc-keys); fixed -22 via vref-supply |
|
||||||
| TRNG (rng@ff448000) | rockchip,trngv1 | mainline (rk3588 IP) | ✅ /dev/hwrng, real entropy (`rng-otp/`) |
|
| TRNG (rng@ff448000) | rockchip,trngv1 | mainline (rk3588 IP) | [x] /dev/hwrng, real entropy (`rng-otp/`) |
|
||||||
| OTP/nvmem (ff3d0000) | rockchip,rv1106-otp | ported (px30_otp_read) | ✅ rockchip-otp0, reads chip id |
|
| OTP/nvmem (ff3d0000) | rockchip,rv1106-otp | ported (px30_otp_read) | [x] rockchip-otp0, reads chip id |
|
||||||
| GMAC (ffa80000) | rockchip,rv1106-gmac | ported (dwmac-rk rv1106_ops) | ✅ eth0 Link Up 100M/Full (`gmac/`) |
|
| GMAC (ffa80000) | rockchip,rv1106-gmac | ported (dwmac-rk rv1106_ops) | [x] eth0 Link Up 100M/Full (`gmac/`) |
|
||||||
| GPIO_SYSFS (legacy /sys/class/gpio) | — | mainline (config) | ⬜ goodix script needs it |
|
| GPIO_SYSFS (legacy /sys/class/gpio) | — | mainline (config) | [ ] goodix script needs it |
|
||||||
| PWM (rockchip) | — | mainline (=m) | ⬜ batch2 =y (backlight) |
|
| PWM (rockchip) | — | mainline (=m) | [ ] batch2 =y (backlight) |
|
||||||
| RTC (rv1106-rtc) | — | ported (vendor driver) | ✅ /dev/rtc0 registers + reads |
|
| RTC (rv1106-rtc) | — | ported (vendor driver) | [x] /dev/rtc0 registers + reads |
|
||||||
| USB2 phy (inno, rv1106) | rockchip_usb2phy_* | ported (data, no tuning) | ✅ probes → USB up |
|
| USB2 phy (inno, rv1106) | rockchip_usb2phy_* | ported (data, no tuning) | [x] probes → USB up |
|
||||||
| USB host (DWC3→xhci, ffb00000) | xhci-hcd:usb1 | mainline | ✅ xhci host registered |
|
| USB host (DWC3→xhci, ffb00000) | xhci-hcd:usb1 | mainline | [x] xhci host registered |
|
||||||
| USB OTG gadget (DWC3, eth0) | eth0 | mainline dwc3 | 🔨 host works; eth0 needs dr_mode=peripheral |
|
| USB OTG gadget (DWC3, eth0) | eth0 | mainline dwc3 | [wip] host works; eth0 needs dr_mode=peripheral |
|
||||||
| crypto (aes/ccm/ctr/arc4) | modules | mainline | ⬜ batch2 (config =y) |
|
| crypto (aes/ccm/ctr/arc4) | modules | mainline | [ ] batch2 (config =y) |
|
||||||
| PSCI node (removed) | — | — | ✅ deleted (no secure monitor → SMC fault) |
|
| PSCI node (removed) | — | — | [x] deleted (no secure monitor → SMC fault) |
|
||||||
| VOP display (ff990000) | ff990000.vop | ported (rv1126 sibling) | 🔨 binds+DRM+card0; connector WIP |
|
| VOP display (ff990000) | ff990000.vop | ported (rv1126 sibling) | [wip] binds+DRM+card0; connector WIP |
|
||||||
| PWM backlight (pwm1) | — | mainline (rk3328 fallback) | ✅ backlight up (brightness) |
|
| PWM backlight (pwm1) | — | mainline (rk3328 fallback) | [x] backlight up (brightness) |
|
||||||
| RGB666 720×720 panel | — | panel-dpi | 🔨 probes; bus_format + connector WIP |
|
| RGB666 720×720 panel | — | panel-dpi | [wip] probes; bus_format + connector WIP |
|
||||||
| GT911 touch (goodix) | goodix, gt911 | mainline | ⬜ M4 (needs GPIO_SYSFS ✅ + node) |
|
| GT911 touch (goodix) | goodix, gt911 | mainline | [ ] M4 (needs GPIO_SYSFS [x] + node) |
|
||||||
| GPIO_SYSFS / crypto / CFG80211 | — | mainline (config) | ✅ =y (batch2) |
|
| GPIO_SYSFS / crypto / CFG80211 | — | mainline (config) | [x] =y (batch2) |
|
||||||
| AIC8800 wifi (bsp/fdrv) | aic8800_* | **out-of-tree** | ✅ M5 — wlan0 up, scanned the site AP at −43dBm (modules, `wifi/VERIFIED-on-c8a3.md`) |
|
| AIC8800 wifi (bsp/fdrv) | aic8800_* | **out-of-tree** | [x] M5 — wlan0 up, scanned the site AP at −43dBm (modules, `wifi/VERIFIED-on-c8a3.md`) |
|
||||||
| AIC8800 BT (btlpm) | aic8800_btlpm | **out-of-tree** | 🔨 module built (6.18 vermagic); HCI bring-up not yet exercised |
|
| AIC8800 BT (btlpm) | aic8800_btlpm | **out-of-tree** | [wip] module built (6.18 vermagic); HCI bring-up not yet exercised |
|
||||||
| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | 🔨 M6 built, 0 errors/0 warnings, 99 `rknpu`-prefixed symbols in `System.map`, `&npu {status="okay"}` in the dtb — **not yet flashed/probed on hardware** (build-only session; see `npu/PORT-PROGRESS.md`) |
|
| NPU (rknpu, ff660000) | rknpu, ff660000.npu | **out-of-tree** | [x] open GPL driver VERIFIED on hardware — `/dev/dri/card1`, `rknpu_version_test` PASS (power/clock/reset path exercised); open *compute* (regcmd) remains a from-scratch RE project (`npu/VERIFIED.md`, `npu/OPEN-NPU-PLAN.md`) |
|
||||||
| RGA 2D (rga2) | rga2 | ported (vendor char-dev) | ✅ /dev/rga, hw 3.3.87975 |
|
| RGA 2D (rga2) | rga2 | ported (vendor char-dev) | [x] /dev/rga, hw 3.3.87975 |
|
||||||
| I2S audio (i2s-tdm) | i2s | rv1126 fallback (=y) | ✅ cpu DAI registers (part of the card below) |
|
| I2S audio (i2s-tdm) | i2s | rv1126 fallback (=y) | [x] cpu DAI registers (part of the card below) |
|
||||||
| Audio codec (acodec) | rockchip,rv1106-codec | ported (rv1106_codec.c) | ✅ card `rv1106-acodec`, pcmC0D0p/c (`audio/`); audible test @ bench |
|
| Audio codec (acodec) | rockchip,rv1106-codec | ported (rv1106_codec.c) | [x] card `rv1106-acodec`, pcmC0D0p/c (`audio/`); audible test @ bench |
|
||||||
| HPMCU mailbox (ff5c0000) | rockchip,rv1106-mailbox | rk3368 fallback +rv1106 num_chans=1 | ✅ A7<->SCR1 round-trip, 5/5 exact (`mailbox/VERIFIED.md`) |
|
| HPMCU mailbox (ff5c0000) | rockchip,rv1106-mailbox | rk3368 fallback +rv1106 num_chans=1 | [x] A7<->SCR1 round-trip, 5/5 exact (`mailbox/VERIFIED.md`) |
|
||||||
| PVTM (core+pmu ring-osc) | rockchip,rv1106-*-pvtm | ported (vendor, no mainline) | ✅ both probe; debugfs reads (`pvtm/`) |
|
| PVTM (core+pmu ring-osc) | rockchip,rv1106-*-pvtm | ported (vendor, no mainline) | [x] both probe; debugfs reads (`pvtm/`) |
|
||||||
| FIQ debugger (ttyFIQ0) | fiq_glue | rockchip | ⬜ optional (we use ttyS2) |
|
| FIQ debugger (ttyFIQ0) | fiq_glue | rockchip | [ ] optional (we use ttyS2) |
|
||||||
|
|
||||||
Legend: ✅ verified on hardware · 🔨 built, not yet verified · ⬜ not started.
|
Legend: [x] verified on hardware · [wip] built, not yet verified · [ ] not started.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Overnight autonomous kernel-completion workflow
|
# Overnight autonomous kernel-completion workflow
|
||||||
|
|
||||||
**Directive ([maintainer], 2026-08-24 night):** port/enable **every** remaining RV1106
|
**Directive (2026-08-24 night):** port/enable **every** remaining RV1106
|
||||||
hardware capability on the self-built 6.18 kernel, **open-source-first** — we want
|
hardware capability on the self-built 6.18 kernel, **open-source-first** — we want
|
||||||
*source* we can read, harden, and bend to our needs (vendor SDK source, upstream,
|
*source* we can read, harden, and bend to our needs (vendor SDK source, upstream,
|
||||||
community repos, or reverse-engineering), never binary blobs. Not just ports —
|
community repos, or reverse-engineering), never binary blobs. Not just ports —
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Overnight kernel-enablement run — results (2026-08-24 night → 08-25)
|
# Overnight kernel-enablement run — results (2026-08-24 night → 08-25)
|
||||||
|
|
||||||
Goal ([maintainer]): port/enable **every** remaining RV1106 hardware capability on the
|
Goal: port/enable **every** remaining RV1106 hardware capability on the
|
||||||
self-built Linux 6.18.46, open-source-first, verify on hardware, so the display's
|
self-built Linux 6.18.46, open-source-first, verify on hardware, so the display's
|
||||||
last mile can start in the morning. Runs on warden-c8a3 (`_b` slot = our 6.18).
|
last mile can start in the morning. Runs on warden-c8a3 (`_b` slot = our 6.18).
|
||||||
|
|
||||||
## ✅ Verified on hardware this run
|
## Verified on hardware this run
|
||||||
| Driver | Evidence |
|
| Driver | Evidence |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **AIC8800 wifi** (M5) | wlan0 up ([device-mac]), `iw scan` found the site AP −43 dBm + others. Modules (built-in deadlocks the two-stage SDIO bring-up). |
|
| **AIC8800 wifi** (M5) | wlan0 up, `iw scan` found the site AP at −43 dBm + others. Modules (built-in deadlocks the two-stage SDIO bring-up). |
|
||||||
| **TRNG** | `/dev/hwrng`, `rng_current=rockchip-rng`, real entropy. |
|
| **TRNG** | `/dev/hwrng`, `rng_current=rockchip-rng`, real entropy. |
|
||||||
| **OTP/nvmem** | `rockchip-otp0` reads chip id ("MR1"). |
|
| **OTP/nvmem** | `rockchip-otp0` reads chip id ("MR1"). |
|
||||||
| **GMAC** (wired eth) | `eth0: Link is Up - 100 Mbps/Full`. |
|
| **GMAC** (wired eth) | `eth0: Link is Up - 100 Mbps/Full`. |
|
||||||
@@ -33,7 +33,7 @@ mailbox (binds via rk3368 fallback but no client to exercise), crypto-v3 (CPU
|
|||||||
crypto extensions already cover it; 100 KB port), NPU (no open userspace),
|
crypto extensions already cover it; 100 KB port), NPU (no open userspace),
|
||||||
pvtm (DVFS-only). Camera/ISP/SPI: no hardware. **No unexplored capability gap.**
|
pvtm (DVFS-only). Camera/ISP/SPI: no hardware. **No unexplored capability gap.**
|
||||||
|
|
||||||
## Morning (needs [maintainer] at the bench)
|
## Morning (needs a human at the bench)
|
||||||
1. **Display connector** — VOP binds + DRM card0 + panel probes, but no connector
|
1. **Display connector** — VOP binds + DRM card0 + panel probes, but no connector
|
||||||
link yet; needs eyes on the panel (pixels can't be verified over serial).
|
link yet; needs eyes on the panel (pixels can't be verified over serial).
|
||||||
2. **Audible audio** — `speaker-test`/`aplay` through the acodec.
|
2. **Audible audio** — `speaker-test`/`aplay` through the acodec.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from the **vendor 5.10.160** tree, no plan44 code, built with our
|
|||||||
- rv1106's siblings **rv1126/rv1108 exist in both trees**, so their 5.10→6.18 delta is a
|
- rv1106's siblings **rv1126/rv1108 exist in both trees**, so their 5.10→6.18 delta is a
|
||||||
working template for the framework API changes.
|
working template for the framework API changes.
|
||||||
|
|
||||||
## M1 — clock driver (`clk-rv1106.c`, 1294 lines): ✅ COMPILES CLEAN on 6.18
|
## M1 — clock driver (`clk-rv1106.c`, 1294 lines): [x] COMPILES CLEAN on 6.18
|
||||||
Build-fix loop against 6.18 — `clk-rv1106.o` (85732 bytes) builds with no errors.
|
Build-fix loop against 6.18 — `clk-rv1106.o` (85732 bytes) builds with no errors.
|
||||||
Fixed (captured in `clk/`):
|
Fixed (captured in `clk/`):
|
||||||
1. **Kconfig + Makefile hooks** — added `CONFIG_CLK_RV1106` (mirrors CLK_RV1126).
|
1. **Kconfig + Makefile hooks** — added `CONFIG_CLK_RV1106` (mirrors CLK_RV1126).
|
||||||
@@ -31,7 +31,7 @@ Fixed (captured in `clk/`):
|
|||||||
4. **`rockchip_clk_register_armclk` signature change** — 5.10 took
|
4. **`rockchip_clk_register_armclk` signature change** — 5.10 took
|
||||||
`(num_parents, parent_clk, alt_parent_clk)`; 6.18 takes `(parent_names[], num_parents)`
|
`(num_parents, parent_clk, alt_parent_clk)`; 6.18 takes `(parent_names[], num_parents)`
|
||||||
and drives the mux from `reg_data.mux_core_main/alt`. Adapted the call to the 6.18 form
|
and drives the mux from `reg_data.mux_core_main/alt`. Adapted the call to the 6.18 form
|
||||||
using a parent-names array (sibling-delta from rv1126). **⚠ PORT-VERIFY**: the mux input
|
using a parent-names array (sibling-delta from rv1126). **PORT-VERIFY**: the mux input
|
||||||
list `{ "gpll","cpll","apll" }` and the `mux_core_main/alt=2` mapping are a best-effort
|
list `{ "gpll","cpll","apll" }` and the `mux_core_main/alt=2` mapping are a best-effort
|
||||||
from the 5.10 intent (main=apll) — they set the **CPU clock source**, so they must be
|
from the 5.10 intent (main=apll) — they set the **CPU clock source**, so they must be
|
||||||
checked against the RV1106 `CORECLKSEL_CON` register map (TRM) and validated on hardware
|
checked against the RV1106 `CORECLKSEL_CON` register map (TRM) and validated on hardware
|
||||||
@@ -39,9 +39,9 @@ Fixed (captured in `clk/`):
|
|||||||
|
|
||||||
5. **`CLK_FRAC_DIVIDER_NO_LIMIT`** — Rockchip downstream-only frac-divider flag (6 uses on
|
5. **`CLK_FRAC_DIVIDER_NO_LIMIT`** — Rockchip downstream-only frac-divider flag (6 uses on
|
||||||
the UART frac clocks); mainline has no min/max opt-out, mapped to 0 (default limit).
|
the UART frac clocks); mainline has no min/max opt-out, mapped to 0 (default limit).
|
||||||
**⚠ PORT-VERIFY**: UART fractional baud accuracy.
|
**PORT-VERIFY**: UART fractional baud accuracy.
|
||||||
|
|
||||||
## M1 — pinctrl (`pinctrl-rockchip.c/.h`): ✅ COMPILES CLEAN on 6.18
|
## M1 — pinctrl (`pinctrl-rockchip.c/.h`): [x] COMPILES CLEAN on 6.18
|
||||||
`pinctrl-rockchip.o` (173688 bytes) builds no-errors. Transplanted from vendor 5.10 (effort S,
|
`pinctrl-rockchip.o` (173688 bytes) builds no-errors. Transplanted from vendor 5.10 (effort S,
|
||||||
zero API drift — the survey's assessment held): added `RV1106` to the type enum; a 159-line
|
zero API drift — the survey's assessment held): added `RV1106` to the type enum; a 159-line
|
||||||
block of `RV1106_DRV/PULL/SMT_*` macros + 3 `rv1106_calc_*_reg_and_bit()` functions; `case
|
block of `RV1106_DRV/PULL/SMT_*` macros + 3 `rv1106_calc_*_reg_and_bit()` functions; `case
|
||||||
@@ -58,12 +58,12 @@ RV1106:` in the 3 pull functions + the RK3568 drive-strength group; `rv1106_pin_
|
|||||||
- **Still PORT-VERIFY:** GPIO4 bank pin-count (`pin_banks` says 24, DT `gpio-ranges` says 32) —
|
- **Still PORT-VERIFY:** GPIO4 bank pin-count (`pin_banks` says 24, DT `gpio-ranges` says 32) —
|
||||||
carried from vendor unchanged; needs TRM/hardware.
|
carried from vendor unchanged; needs TRM/hardware.
|
||||||
|
|
||||||
## M1 — mach: ✅ DONE
|
## M1 — mach: DONE
|
||||||
`mach-rockchip` RV1106/RV1103 SoC recognition added as a DT-compat entry (no
|
`mach-rockchip` RV1106/RV1103 SoC recognition added as a DT-compat entry (no
|
||||||
`CPU_RV1106` symbol recreated). Captured as `mach/0001-rv1106-soc-recognition.patch`.
|
`CPU_RV1106` symbol recreated). Captured as `mach/0001-rv1106-soc-recognition.patch`.
|
||||||
**M1 is complete: clk + pinctrl + mach all compile clean on 6.18.**
|
**M1 is complete: clk + pinctrl + mach all compile clean on 6.18.**
|
||||||
|
|
||||||
## M2 — earlycon build: ✅ DONE (boot pending hardware)
|
## M2 — earlycon build: DONE (boot pending hardware)
|
||||||
The first full kernel build with our SoC drivers, 2026-08-24:
|
The first full kernel build with our SoC drivers, 2026-08-24:
|
||||||
- **`multi_v7_defconfig` + `configs/m2-earlycon.fragment` builds an 11.8 MB zImage**
|
- **`multi_v7_defconfig` + `configs/m2-earlycon.fragment` builds an 11.8 MB zImage**
|
||||||
with `clk-rv1106.o` (85732 B) and `pinctrl-rockchip.o` (173688 B, our rv1106 data)
|
with `clk-rv1106.o` (85732 B) and `pinctrl-rockchip.o` (173688 B, our rv1106 data)
|
||||||
@@ -80,7 +80,7 @@ The first full kernel build with our SoC drivers, 2026-08-24:
|
|||||||
console baud (1.5M assumed). A wrong DDR/clock value silently hangs before or just after
|
console baud (1.5M assumed). A wrong DDR/clock value silently hangs before or just after
|
||||||
earlycon.
|
earlycon.
|
||||||
|
|
||||||
## M2 — boot: ✅ DONE — "it's alive" on warden-c8a3 (2026-08-24)
|
## M2 — boot: DONE — "it's alive" on warden-c8a3 (2026-08-24)
|
||||||
The self-built **Linux 6.18.46 boots on real RV1106 hardware**, through our ported
|
The self-built **Linux 6.18.46 boots on real RV1106 hardware**, through our ported
|
||||||
drivers, verified over the serial console. It reaches earlycon, the arch timer
|
drivers, verified over the serial console. It reaches earlycon, the arch timer
|
||||||
(BogoMIPS calibrated), **our `clk-rv1106` CRU driver**, pinctrl, and the mainline
|
(BogoMIPS calibrated), **our `clk-rv1106` CRU driver**, pinctrl, and the mainline
|
||||||
@@ -105,7 +105,7 @@ Three bring-up bugs were found and fixed on hardware, all captured in the DT:
|
|||||||
comes up far enough to clock the UART and the arch timer on hardware. A wrong CPU
|
comes up far enough to clock the UART and the arch timer on hardware. A wrong CPU
|
||||||
mux/PLL would show later (cpufreq / peripheral rates), still to be checked.
|
mux/PLL would show later (cpufreq / peripheral rates), still to be checked.
|
||||||
|
|
||||||
## M3 — rootfs boot: ✅ DONE — the full WardenOS runs on the 6.18 kernel (2026-08-24)
|
## M3 — rootfs boot: DONE — the full WardenOS runs on the 6.18 kernel (2026-08-24)
|
||||||
Adding the eMMC (`dw_mmc`) node to the DT was all M3 needed — the drivers are already
|
Adding the eMMC (`dw_mmc`) node to the DT was all M3 needed — the drivers are already
|
||||||
in the config. On hardware:
|
in the config. On hardware:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Driver provenance & openness ledger
|
# Driver provenance & openness ledger
|
||||||
|
|
||||||
[maintainer]'s directive: **every** driver we run on the self-built 6.18 kernel must be
|
Standing directive: **every** driver we run on the self-built 6.18 kernel must be
|
||||||
open source we can read, harden, and bend — not a binary blob — and this applies
|
open source we can read, harden, and bend — not a binary blob — and this applies
|
||||||
to the drivers *already* ported, not just the new ones. This ledger records, for
|
to the drivers *already* ported, not just the new ones. This ledger records, for
|
||||||
each, where the source came from and under what license. Every entry is GPL-2.0
|
each, where the source came from and under what license. Every entry is GPL-2.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SARADC — ✅ VERIFIED on warden-c8a3 (2026-08-25); the -22 was vref, not clk
|
# SARADC — VERIFIED on warden-c8a3 (2026-08-25); the -22 was vref, not clk
|
||||||
|
|
||||||
The rockchip_saradc probe failed `-22` NOT at clk_set_rate (no "failed to set
|
The rockchip_saradc probe failed `-22` NOT at clk_set_rate (no "failed to set
|
||||||
adc clk rate" ever printed) but at `regulator_get_voltage(info->vref)` — with no
|
adc clk rate" ever printed) but at `regulator_get_voltage(info->vref)` — with no
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ stuck in `deferred probe pending: asoc-simple-card: parse error`, no card.
|
|||||||
i2s0_8ch node already carries the `rockchip,rv1126-i2s-tdm` fallback compatible +
|
i2s0_8ch node already carries the `rockchip,rv1126-i2s-tdm` fallback compatible +
|
||||||
`#sound-dai-cells=<0>`, so the mainline driver binds it).
|
`#sound-dai-cells=<0>`, so the mainline driver binds it).
|
||||||
|
|
||||||
**✅ VERIFIED on c8a3:** `/proc/asound/cards` → `0 [rv1106acodec]: simple-card -
|
**VERIFIED on c8a3:** `/proc/asound/cards` → `0 [rv1106acodec]: simple-card -
|
||||||
rv1106-acodec`; `aplay -l` → `card 0: rv1106acodec, device 0:
|
rv1106-acodec`; `aplay -l` → `card 0: rv1106acodec, device 0:
|
||||||
ffae0000.i2s-rv1106-hifi`; `/dev/snd/` has `controlC0 pcmC0D0p pcmC0D0c` (playback
|
ffae0000.i2s-rv1106-hifi`; `/dev/snd/` has `controlC0 pcmC0D0p pcmC0D0c` (playback
|
||||||
+ capture). Audible speaker test deferred to the bench (with the display).
|
+ capture). Audible speaker test deferred to the bench (with the display).
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# Defaults match this workspace.
|
# Defaults match this workspace.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
FE="${FE:-<flare-edge>}"
|
FE="${FE:?set FE to a flare-edge checkout path}"
|
||||||
KTREE="${KTREE:-$FE/research/linux-6.18.46}"
|
KTREE="${KTREE:-$FE/research/linux-6.18.46}"
|
||||||
SDK_TC="${SDK_TC:-$FE/sdk/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin}"
|
SDK_TC="${SDK_TC:-$FE/sdk/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin}"
|
||||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ rockchip-drm display-subsystem: bound ff990000.vop
|
|||||||
brightness settable). **The VOP driver port is validated** — the register data,
|
brightness settable). **The VOP driver port is validated** — the register data,
|
||||||
version, feature, and resets are right.
|
version, feature, and resets are right.
|
||||||
|
|
||||||
## ✅ RESOLVED — full UI renders on the panel (2026-08-25)
|
## RESOLVED — full UI renders on the panel (2026-08-25)
|
||||||
|
|
||||||
The connector *and* the deeper black-screen chain that followed it are fixed; the
|
The connector *and* the deeper black-screen chain that followed it are fixed; the
|
||||||
86-Panel now draws the full WardenOS Dashboard on 6.18 (`_b`), verified by webcam.
|
86-Panel now draws the full WardenOS Dashboard on 6.18 (`_b`), verified by webcam.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Display (VOP + RGB panel) — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
# Display (VOP + RGB panel) — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
The 86-Panel renders the **full WardenOS Dashboard UI** on our self-built Linux
|
The 86-Panel renders the **full WardenOS Dashboard UI** on our self-built Linux
|
||||||
6.18.46 (`_b` slot), pixel-identical to the stock 5.10 `_a` slot. Verified by
|
6.18.46 (`_b` slot), pixel-identical to the stock 5.10 `_a` slot. Verified by
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# GMAC (wired 10/100 ethernet) — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
# GMAC (wired 10/100 ethernet) — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
**Result: `eth0: Link is Up - 100Mbps/Full - flow control rx/tx`** on our
|
**Result: `eth0: Link is Up - 100Mbps/Full - flow control rx/tx`** on our
|
||||||
self-built 6.18.46. The 86-Panel's RMII MAC + on-die 10/100 FEPHY works; a real
|
self-built 6.18.46. The 86-Panel's RMII MAC + on-die 10/100 FEPHY works; a real
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ watchdog exactly as-is (different threat model, different job).
|
|||||||
- **Controller (Linux side): non-issue.** `drivers/mailbox/rockchip-mailbox.c` is
|
- **Controller (Linux side): non-issue.** `drivers/mailbox/rockchip-mailbox.c` is
|
||||||
upstream in mainline 6.18 and **already binds on our exact kernel** via the
|
upstream in mainline 6.18 and **already binds on our exact kernel** via the
|
||||||
generic `rockchip,rk3368-mailbox` **fallback compatible** with **zero patching**
|
generic `rockchip,rk3368-mailbox` **fallback compatible** with **zero patching**
|
||||||
— recorded in `CAPABILITIES-AUDIT.md:30`, confirmed by source read. RV1106's DT
|
— recorded in `CAPABILITIES-AUDIT.md`'s Remaining-blocks table (mailbox row), confirmed by source read. RV1106's DT
|
||||||
declares both instances with that fallback string. Gated today only by
|
declares both instances with that fallback string. Gated today only by
|
||||||
`status="disabled"` + `CONFIG_ROCKCHIP_MBOX` being absent from the defconfig.
|
`status="disabled"` + `CONFIG_ROCKCHIP_MBOX` being absent from the defconfig.
|
||||||
- **HPMCU firmware (MCU side): we already do the hard part.** WardenOS has a
|
- **HPMCU firmware (MCU side): we already do the hard part.** WardenOS has a
|
||||||
@@ -241,7 +241,7 @@ adopt rpmsg/virtio unless the payload complexity genuinely demands it.
|
|||||||
moot, since our `hpmcu.rs` is an independent hardware-validated reimplementation.
|
moot, since our `hpmcu.rs` is an independent hardware-validated reimplementation.
|
||||||
|
|
||||||
---
|
---
|
||||||
_Cross-refs: `../CAPABILITIES-AUDIT.md:30`, `../REMAINING-PORTS.md §7`,
|
_Cross-refs: `../CAPABILITIES-AUDIT.md`, `../REMAINING-PORTS.md §7`,
|
||||||
`../../luckfox-pico-86-panel/riscv-mcu.md`,
|
`../../luckfox-pico-86-panel/riscv-mcu.md`,
|
||||||
`.../raw/followup-riscv-mcu.md`,
|
`.../raw/followup-riscv-mcu.md`,
|
||||||
`flare-edge/major-app-additions/docs/decisions/0002-hpmcu-watchdog.md`,
|
`flare-edge/major-app-additions/docs/decisions/0002-hpmcu-watchdog.md`,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# HPMCU mailbox — ✅ 100% VERIFIED on warden-c8a3 (2026-08-25)
|
# HPMCU mailbox — [x] 100% VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
A fully-open A7 ↔ HPMCU (RISC-V SCR1) hardware-mailbox round-trip on our self-built
|
A fully-open A7 ↔ HPMCU (RISC-V SCR1) hardware-mailbox round-trip on our self-built
|
||||||
Linux 6.18.46. Open kernel driver + open SCR1 firmware, **zero blobs**.
|
Linux 6.18.46. Open kernel driver + open SCR1 firmware, **zero blobs**.
|
||||||
@@ -7,11 +7,11 @@ Linux 6.18.46. Open kernel driver + open SCR1 firmware, **zero blobs**.
|
|||||||
SCR1 echo firmware running: `DBG_STATE = 0x584F424D` ("MBOX"). Five round-trips,
|
SCR1 echo firmware running: `DBG_STATE = 0x584F424D` ("MBOX"). Five round-trips,
|
||||||
Linux → mailbox → SCR1 → mailbox → Linux, **all exact**:
|
Linux → mailbox → SCR1 → mailbox → Linux, **all exact**:
|
||||||
```
|
```
|
||||||
sent 0x0000beef/0x600df00d -> B2A 0x0000BEEF/0x600DF00D e=4 ✓
|
sent 0x0000beef/0x600df00d -> B2A 0x0000BEEF/0x600DF00D e=4 OK
|
||||||
sent 0x0000c0de/0x12345678 -> B2A 0x0000C0DE/0x12345678 e=5 ✓
|
sent 0x0000c0de/0x12345678 -> B2A 0x0000C0DE/0x12345678 e=5 OK
|
||||||
sent 0x0000face/0xdeadbeef -> B2A 0x0000FACE/0xDEADBEEF e=6 ✓
|
sent 0x0000face/0xdeadbeef -> B2A 0x0000FACE/0xDEADBEEF e=6 OK
|
||||||
sent 0x00001234/0xcafef00d -> B2A 0x00001234/0xCAFEF00D e=7 ✓
|
sent 0x00001234/0xcafef00d -> B2A 0x00001234/0xCAFEF00D e=7 OK
|
||||||
sent 0x0000aa55/0x55aa55aa -> B2A 0x0000AA55/0x55AA55AA e=8 ✓
|
sent 0x0000aa55/0x55aa55aa -> B2A 0x0000AA55/0x55AA55AA e=8 OK
|
||||||
```
|
```
|
||||||
Echo counter increments 1:1 with sends; both CMD and DAT echo back verbatim.
|
Echo counter increments 1:1 with sends; both CMD and DAT echo back verbatim.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# HPMCU mailbox-echo firmware — bare-metal RV32IMC for the RV1106 SCR1 core.
|
# HPMCU mailbox-echo firmware — bare-metal RV32IMC for the RV1106 SCR1 core.
|
||||||
# Same xPack riscv-none-embed-gcc 10.2.0 + flags as the watchdog firmware.
|
# Same xPack riscv-none-embed-gcc 10.2.0 + flags as the watchdog firmware.
|
||||||
|
|
||||||
XPACK ?= <flare-edge>/sdk/sysdrv/source/mcu/prebuilts/gcc/linux-x86/riscv64/xpack-riscv-none-embed-gcc-10.2.0-1.2/bin
|
# Point XPACK at the xpack riscv toolchain bin/ inside a flare-edge SDK checkout.
|
||||||
|
XPACK ?= $(error set XPACK to the xpack-riscv-none-embed-gcc bin/ directory)
|
||||||
CROSS ?= $(XPACK)/riscv-none-embed-
|
CROSS ?= $(XPACK)/riscv-none-embed-
|
||||||
|
|
||||||
CC = $(CROSS)gcc
|
CC = $(CROSS)gcc
|
||||||
|
|||||||
@@ -197,7 +197,8 @@ the whole problem.
|
|||||||
|
|
||||||
`PROVENANCE.md`: the kernel driver is portable GPL; the closed piece is the
|
`PROVENANCE.md`: the kernel driver is portable GPL; the closed piece is the
|
||||||
userspace RKNN runtime + regcmd format (a blob). **Per directive we do not ship
|
userspace RKNN runtime + regcmd format (a blob). **Per directive we do not ship
|
||||||
that blob.** `CAPABILITIES-AUDIT.md:32` rates NPU "not worth shipping" until an
|
that blob.** `CAPABILITIES-AUDIT.md`'s Remaining-blocks table rates the NPU
|
||||||
|
"open driver VERIFIED; compute deferred" until an
|
||||||
open encoder exists.
|
open encoder exists.
|
||||||
|
|
||||||
### URLs
|
### URLs
|
||||||
@@ -299,5 +300,5 @@ ever begun; otherwise this is the documented reason open NPU compute is deferred
|
|||||||
|
|
||||||
---
|
---
|
||||||
_Cross-refs: `PORT-PLAN.md` (authoritative file-by-file kernel port),
|
_Cross-refs: `PORT-PLAN.md` (authoritative file-by-file kernel port),
|
||||||
`../../docs/npu-graphics-feasibility.md`, `../CAPABILITIES-AUDIT.md:32`,
|
`../../docs/npu-graphics-feasibility.md`, `../CAPABILITIES-AUDIT.md`,
|
||||||
`../PROVENANCE.md`, `../DRIVER-PARITY.md:41`, `../REMAINING-PORTS.md §6`._
|
`../PROVENANCE.md`, `../DRIVER-PARITY.md:41`, `../REMAINING-PORTS.md §6`._
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ target as the rest of this port: **Linux 6.18.46 vanilla**
|
|||||||
(`flare-edge/research/linux-6.18.46/`), forward-ported from vendor 5.10.160, built
|
(`flare-edge/research/linux-6.18.46/`), forward-ported from vendor 5.10.160, built
|
||||||
with our `arm-rockchip830-...-gcc 8.3` toolchain — see `../PORT-STATUS.md` and
|
with our `arm-rockchip830-...-gcc 8.3` toolchain — see `../PORT-STATUS.md` and
|
||||||
`../../docs/bringup.md` for the method and milestones this slots into (M6, listed
|
`../../docs/bringup.md` for the method and milestones this slots into (M6, listed
|
||||||
in `../DRIVER-PARITY.md` as "NPU (rknpu, ff660000) | out-of-tree | ⬜ M6").
|
in `../DRIVER-PARITY.md` as "NPU (rknpu, ff660000) | out-of-tree | [ ] M6").
|
||||||
|
|
||||||
Builds on `warden-sdk/docs/npu-graphics-feasibility.md`, which already read this
|
Builds on `warden-sdk/docs/npu-graphics-feasibility.md`, which already read this
|
||||||
same driver source to answer a narrower question (can the NPU do graphics — no).
|
same driver source to answer a narrower question (can the NPU do graphics — no).
|
||||||
|
|||||||
@@ -331,6 +331,6 @@ the value of checking the decoded string, not just the ioctl return code.
|
|||||||
|
|
||||||
## Also updated this session
|
## Also updated this session
|
||||||
|
|
||||||
`../DRIVER-PARITY.md`'s NPU row: `⬜ M6 — plan: npu/PORT-PLAN.md` -> `🔨 M6 built,
|
`../DRIVER-PARITY.md`'s NPU row: `[ ] M6 — plan: npu/PORT-PLAN.md` -> `[wip] M6 built,
|
||||||
0 errors/0 warnings, 99 rknpu-prefixed symbols in System.map, &npu
|
0 errors/0 warnings, 99 rknpu-prefixed symbols in System.map, &npu
|
||||||
{status="okay"} in the dtb — not yet flashed/probed on hardware`.
|
{status="okay"} in the dtb — not yet flashed/probed on hardware`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# NPU (rknpu) open kernel driver — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
# NPU (rknpu) open kernel driver — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
The open GPL rknpu kernel driver runs on our self-built Linux 6.18.46. This is the
|
The open GPL rknpu kernel driver runs on our self-built Linux 6.18.46. This is the
|
||||||
achievable open end state (Tier A in `OPEN-NPU-PLAN.md`); open *compute* remains a
|
achievable open end state (Tier A in `OPEN-NPU-PLAN.md`); open *compute* remains a
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# PVTM (Process-Voltage-Temperature Monitor) — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
# PVTM (Process-Voltage-Temperature Monitor) — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
Whole-driver port: mainline 6.18 has **no** rockchip pvtm driver; the vendor
|
Whole-driver port: mainline 6.18 has **no** rockchip pvtm driver; the vendor
|
||||||
`drivers/soc/rockchip/rockchip_pvtm.c` (GPL-2.0, 1046L) supports rv1106. Copied it in
|
`drivers/soc/rockchip/rockchip_pvtm.c` (GPL-2.0, 1046L) supports rv1106. Copied it in
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ implied by this kernel port.
|
|||||||
reports true (`querystring(RGA_VERSION)` succeeds — `warden_rga.c:458-466`) and the
|
reports true (`querystring(RGA_VERSION)` succeeds — `warden_rga.c:458-466`) and the
|
||||||
Monitor page's "RGA" load metric moves during graph scroll (`warden_rga_load_pct()`,
|
Monitor page's "RGA" load metric moves during graph scroll (`warden_rga_load_pct()`,
|
||||||
`warden_rga.c:226-241`) — the same on-target evidence bar as every other change in
|
`warden_rga.c:226-241`) — the same on-target evidence bar as every other change in
|
||||||
this repo (`../../CLAUDE.md`: "UI/daemon changes are verified on a real panel").
|
this repo (repo policy: "UI/daemon changes are verified on a real panel").
|
||||||
|
|
||||||
## Summary of residual risk (PORT-VERIFY-class items)
|
## Summary of residual risk (PORT-VERIFY-class items)
|
||||||
|
|
||||||
@@ -294,8 +294,9 @@ implied by this kernel port.
|
|||||||
- **CMA pool sizing** — 10 MiB was sized against the 5.10 image's actual usage (graph
|
- **CMA pool sizing** — 10 MiB was sized against the 5.10 image's actual usage (graph
|
||||||
canvas + scanout mirror at 720×720×4B ≈ 2 MiB each); carry the same size unless a
|
canvas + scanout mirror at 720×720×4B ≈ 2 MiB each); carry the same size unless a
|
||||||
future accounting shows it's tight.
|
future accounting shows it's tight.
|
||||||
- **Driver-parity table** (`../DRIVER-PARITY.md`) should move `RGA 2D (rga2)` from ⬜ to
|
- **Driver-parity table** (`../DRIVER-PARITY.md`) should move `RGA 2D (rga2)` from [ ] to
|
||||||
🔨/✅ as these steps land, same convention as every other M-milestone row.
|
[wip]/[x] as these steps land, same convention as every other M-milestone row
|
||||||
|
(the parity row already reads [x]).
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# TRNG + OTP port (batch A) — 6.18
|
# TRNG + OTP port (batch A) — 6.18
|
||||||
|
|
||||||
## TRNG (hardware RNG) — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
## TRNG (hardware RNG) — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
rv1106's `rockchip,trngv1` is the same standalone TRNG_V1 IP as rk3588 (identical
|
rv1106's `rockchip,trngv1` is the same standalone TRNG_V1 IP as rk3588 (identical
|
||||||
register map). Mainline `drivers/char/hw_random/rockchip-rng.c` already drives it.
|
register map). Mainline `drivers/char/hw_random/rockchip-rng.c` already drives it.
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ rv1106's clock/reset names need no special handling.) Kconfig
|
|||||||
`dd if=/dev/hwrng bs=16` → `c697 503d f9db 6b84 50e4 e1ee f232 b2ae` (real HW
|
`dd if=/dev/hwrng bs=16` → `c697 503d f9db 6b84 50e4 e1ee f232 b2ae` (real HW
|
||||||
entropy, non-zero). Hardware entropy source for the panel's crypto/keys.
|
entropy, non-zero). Hardware entropy source for the panel's crypto/keys.
|
||||||
|
|
||||||
## OTP / nvmem — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
## OTP / nvmem — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
Reads real data: `dd .../rockchip-otp0/nvmem bs=1 count=16 | xxd` →
|
Reads real data: `dd .../rockchip-otp0/nvmem bs=1 count=16 | xxd` →
|
||||||
`5211 02fe 084d 5231 0000 0000 3b15 0000` (contains "MR1" chip id) — no timeout.
|
`5211 02fe 084d 5231 0000 0000 3b15 0000` (contains "MR1" chip id) — no timeout.
|
||||||
Mainline `drivers/nvmem/rockchip-otp.c` gains an `rv1106_data` + compatible.
|
Mainline `drivers/nvmem/rockchip-otp.c` gains an `rv1106_data` + compatible.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# GT911 capacitive touch — ✅ VERIFIED on warden-c8a3 (2026-08-25)
|
# GT911 capacitive touch — VERIFIED on warden-c8a3 (2026-08-25)
|
||||||
|
|
||||||
Touch works on our self-built 6.18.46 (`_b`): the WardenOS UI responds to
|
Touch works on our self-built 6.18.46 (`_b`): the WardenOS UI responds to
|
||||||
taps/swipes ([maintainer] confirmed on the physical panel). Objective evidence:
|
taps/swipes (confirmed by hand on the physical panel). Objective evidence:
|
||||||
|
|
||||||
```
|
```
|
||||||
Goodix-TS 3-0014: ID 911, version: 1060
|
Goodix-TS 3-0014: ID 911, version: 1060
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ cfg80211.ko → libarc4.ko → ctr.ko → ccm.ko → libaes.ko → aes_generic.k
|
|||||||
see `insmod_wifi.sh:126-137` and `wifi-bluetooth-aic8800.md`); nothing to
|
see `insmod_wifi.sh:126-137` and `wifi-bluetooth-aic8800.md`); nothing to
|
||||||
replicate there. The crypto modules (arc4/ctr/ccm/aes) are dependencies of the
|
replicate there. The crypto modules (arc4/ctr/ccm/aes) are dependencies of the
|
||||||
driver's internal key-handling, not aic8800-specific — confirm they're already
|
driver's internal key-handling, not aic8800-specific — confirm they're already
|
||||||
`=y`/reachable in the 6.18 config (crypto is currently listed as "⬜ batch2" in
|
`=y`/reachable in the 6.18 config (crypto is currently listed as "[ ] batch2" in
|
||||||
`../DRIVER-PARITY.md`; flip alongside this work).
|
`../DRIVER-PARITY.md`; flip alongside this work).
|
||||||
|
|
||||||
**DT node — correction to the task's framing.** The task description assumed
|
**DT node — correction to the task's framing.** The task description assumed
|
||||||
@@ -295,7 +295,7 @@ No breaking changes found in `sdio_driver`, `sdio_claim_host`/`release_host`,
|
|||||||
`sdio_readb`/`writesb`, `sdio_set_block_size` between 5.10 and 6.18 (low
|
`sdio_readb`/`writesb`, `sdio_set_block_size` between 5.10 and 6.18 (low
|
||||||
research depth on this axis — treat as low-risk, smoke-test rather than
|
research depth on this axis — treat as low-risk, smoke-test rather than
|
||||||
line-audit). The mainline `dw_mmc`/`dw_mmc-rockchip` host driver is already
|
line-audit). The mainline `dw_mmc`/`dw_mmc-rockchip` host driver is already
|
||||||
proven on 6.18 for eMMC (`../DRIVER-PARITY.md`: `dw_mmc (eMMC) | mainline | ✅ M3`)
|
proven on 6.18 for eMMC (`../DRIVER-PARITY.md`: `dw_mmc (eMMC) | mainline | [x] M3`)
|
||||||
and `CONFIG_MMC_DW_ROCKCHIP=y` is already in the live `.config` — the SDIO
|
and `CONFIG_MMC_DW_ROCKCHIP=y` is already in the live `.config` — the SDIO
|
||||||
*controller* side of this port is de-risked; only the AIC8800 *card driver*
|
*controller* side of this port is de-risked; only the AIC8800 *card driver*
|
||||||
above the `sdmmc` bus is new work.
|
above the `sdmmc` bus is new work.
|
||||||
@@ -410,7 +410,7 @@ CONFIG_BT_HCIUART_H4=y # already =y (H4 is the transport this board's BT a
|
|||||||
# per hardware-86-panel.md: UART1/ttyS1, hciattach -s 1500000
|
# per hardware-86-panel.md: UART1/ttyS1, hciattach -s 1500000
|
||||||
# ... any 1500000 flow nosleep)
|
# ... any 1500000 flow nosleep)
|
||||||
CONFIG_CRYPTO_ARC4=y CONFIG_CRYPTO_CTR=y CONFIG_CRYPTO_CCM=y CONFIG_CRYPTO_AES=y # driver's
|
CONFIG_CRYPTO_ARC4=y CONFIG_CRYPTO_CTR=y CONFIG_CRYPTO_CCM=y CONFIG_CRYPTO_AES=y # driver's
|
||||||
# internal key-handling deps, currently "⬜ batch2" in
|
# internal key-handling deps, currently "[ ] batch2" in
|
||||||
# ../DRIVER-PARITY.md — confirm =y, not =m, alongside this work
|
# ../DRIVER-PARITY.md — confirm =y, not =m, alongside this work
|
||||||
```
|
```
|
||||||
Do **not** enable `CONFIG_MAC80211` for this driver — confirmed by source
|
Do **not** enable `CONFIG_MAC80211` for this driver — confirmed by source
|
||||||
@@ -537,7 +537,7 @@ Follow the existing A/B `_b`-slot hardware-verification loop
|
|||||||
attaches and `hciattach -s 1500000 /dev/ttyS1 any 1500000 flow nosleep`
|
attaches and `hciattach -s 1500000 /dev/ttyS1 any 1500000 flow nosleep`
|
||||||
(the known-good invocation) brings up an HCI device.
|
(the known-good invocation) brings up an HCI device.
|
||||||
8. **Update on landing**: mark the `AIC8800 wifi (bsp/fdrv)` and
|
8. **Update on landing**: mark the `AIC8800 wifi (bsp/fdrv)` and
|
||||||
`AIC8800 BT (btlpm)` rows in `../DRIVER-PARITY.md` ✅, with the same
|
`AIC8800 BT (btlpm)` rows in `../DRIVER-PARITY.md` [x], with the same
|
||||||
"hardware-verified, not just compiled" bar every other row uses.
|
"hardware-verified, not just compiled" bar every other row uses.
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ live RF scan.
|
|||||||
`Start app: 00120000`, BSP_RC=0.
|
`Start app: 00120000`, BSP_RC=0.
|
||||||
- `insmod aic8800_fdrv.ko` → `ieee80211 phy0: HT supp 1, VHT supp 1, HE supp 1`,
|
- `insmod aic8800_fdrv.ko` → `ieee80211 phy0: HT supp 1, VHT supp 1, HE supp 1`,
|
||||||
FDRV_RC=0.
|
FDRV_RC=0.
|
||||||
- `wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> ... link/ether [device-mac]`
|
- `wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> ... link/ether <device MAC>`
|
||||||
- `iw dev wlan0 scan` found real APs:
|
- `iw dev wlan0 scan` found real APs (SSIDs/BSSIDs redacted for publication):
|
||||||
- **the site AP [bssid] 2412 MHz −43 dBm**
|
- **the site AP at 2412 MHz, −43 dBm**
|
||||||
- a neighboring guest AP [bssid] 2412 MHz −73 dBm
|
- a neighboring guest AP at 2412 MHz, −73 dBm
|
||||||
- +several more, correct signal strengths → RF path fully functional.
|
- +several more, correct signal strengths → RF path fully functional.
|
||||||
|
|
||||||
## Why MODULES, not built-in (=y)
|
## Why MODULES, not built-in (=y)
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ with `../build/warden_defconfig` → `zImage` + `rv1106-warden.dtb`).
|
|||||||
| `70-audio-codec.patch` | `rv1106_codec` + I2S wiring |
|
| `70-audio-codec.patch` | `rv1106_codec` + I2S wiring |
|
||||||
| `80-misc-thermal-rtc-adc.patch` | tsadc, rtc-rockchip, saradc, trng, otp, gmac, goodix touch |
|
| `80-misc-thermal-rtc-adc.patch` | tsadc, rtc-rockchip, saradc, trng, otp, gmac, goodix touch |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Everything in this directory is a derivative work of the Linux kernel and of
|
||||||
|
GPL-2.0 vendor kernel code: **GPL-2.0-only** (or the per-file SPDX identifier
|
||||||
|
where one is present), the same license the whole repository carries. Per-driver
|
||||||
|
origin and license are tracked in `kernel/rv1106-enablement/PROVENANCE.md`.
|
||||||
|
|
||||||
## Provenance & regeneration
|
## Provenance & regeneration
|
||||||
|
|
||||||
Baseline: `linux-6.18.46` from kernel.org (`build/linux-6.18.46.tar.xz.sha256`
|
Baseline: `linux-6.18.46` from kernel.org (`build/linux-6.18.46.tar.xz.sha256`
|
||||||
|
|||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# qemu/ — the WardenOS device simulator
|
||||||
|
|
||||||
|
A QEMU virtual machine that boots the real forward-ported kernel (`build/` +
|
||||||
|
`patches/`) and real userspace, so the *device* — init, daemons, networking,
|
||||||
|
OTA, watchdog, display — can be tested off-hardware. The third simulator in
|
||||||
|
the stack, deliberately not named "sim":
|
||||||
|
|
||||||
|
- `lvglsim` (flare-edge) — SDL desktop build of the UI. Rendering only.
|
||||||
|
- `sim/` (this repo) — register-level Rust models of RV1106 blocks behind
|
||||||
|
driver seams.
|
||||||
|
- `qemu/` (this) — the whole machine above the kernel entry point, running
|
||||||
|
the real binaries.
|
||||||
|
|
||||||
|
Decision record: `docs/decisions/0006-qemu-device-sim.md`.
|
||||||
|
|
||||||
|
## The boundary (read this before trusting a green run)
|
||||||
|
|
||||||
|
There is no RV1106 machine model in QEMU and everything below the kernel is
|
||||||
|
closed rkbin blobs plus mask ROM, so the VM **enters at `-kernel zImage`** on
|
||||||
|
`-M virt,highmem=off` (single Cortex-A7, 256M — the RV1106G3's shape).
|
||||||
|
|
||||||
|
| Emulated / substituted | Not emulated (stays bench / `sim/` territory) |
|
||||||
|
|---|---|
|
||||||
|
| Kernel boot, init ordering, switch_root | BootROM, idblock/DDR-init, SPL, U-Boot |
|
||||||
|
| A/B *outcome* (`warden.slot=` cmdline) | Real BCB A/B selection, bootcount auto-revert |
|
||||||
|
| Storage: virtio-blk with the device's exact `blkdevparts=` layout + `/dev/block/by-name/` contract | eMMC controller itself |
|
||||||
|
| Network: virtio-net (slirp, hostfwd 22/80/28443) | GMAC, AIC8800 wifi, usb0 gadget |
|
||||||
|
| Display: virtio-gpu 720x720 via fbdev emulation | VOP/RGB666 pipeline, CH32V003 panel init, RGA blits |
|
||||||
|
| Touch: virtio-tablet (QMP `input-send-event`) | GT911 on I2C3 |
|
||||||
|
| Watchdog: i6300esb (PCI), `-action watchdog=reset` | DW watchdog @0xff5a0000, HPMCU supervisor |
|
||||||
|
| RS485: pci-serial chardev bridged to `sim/`'s `ModbusSlave` | Real UART4 timing/electrical behavior |
|
||||||
|
| RTC: PL031 (`--rtc` reproduces the no-RTC 2021-clock incident class) | The unpopulated backup-cell reality |
|
||||||
|
|
||||||
|
**"Boots/works under emulation" is never evidence of "works on silicon."**
|
||||||
|
The VM narrows which claims need a panel; on-device claims still need
|
||||||
|
on-device evidence. Conversely, the VM is the first environment that runs
|
||||||
|
production binaries on a non-RV1106 memory map — it found flare-edge #106
|
||||||
|
(fatal SIGBUS in flared's HPMCU probe) and #107 (Y2038 time_t truncation)
|
||||||
|
on its first two boots of real userspace.
|
||||||
|
|
||||||
|
Documented guest deviations from production, set by stage-2 init:
|
||||||
|
`WARDEN_FLARE_INSECURE=1` (the desk mock portal is plain HTTP) and
|
||||||
|
`WARDEN_HPMCU=0` (no mailbox SRAM on virt; flared >= flare-edge#106 fix
|
||||||
|
required, or the daemon dies of SIGBUS).
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. kernel: canonical build boots the VM as-is; the fragment variant adds
|
||||||
|
# the scenario devices (PCI serial, watchdog, WireGuard, virtio-gpu/input)
|
||||||
|
WORK=$HOME/kbuild-out CROSS_COMPILE=arm-linux-gnueabihf- \
|
||||||
|
WARDEN_KCONFIG_FRAGMENT=qemu/configs/virt.fragment bash build/build-kernel.sh
|
||||||
|
|
||||||
|
# 2. initramfs (sha256-pinned static busybox + qemu/rootfs/) and A/B disk
|
||||||
|
bash qemu/mkinitramfs.sh
|
||||||
|
bash qemu/mkimage.sh # options: --portal-url --state K=V --fw-version
|
||||||
|
|
||||||
|
# 3. run (see run.sh header for all flags)
|
||||||
|
bash qemu/run.sh --kernel $HOME/kbuild-out/linux-6.18.46/arch/arm/boot/zImage --shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Payload: drop static musl armv7 binaries into `qemu/payload/` (see its
|
||||||
|
README) — `warden-flared`, `warden-modbus`, and `warden-ui` (the LVGL
|
||||||
|
fbdev+evdev build from flare-edge `tools/build-ui-vm.sh`) are started by
|
||||||
|
stage-2 init when present.
|
||||||
|
|
||||||
|
## Scenario tests (`qemu/tests/`)
|
||||||
|
|
||||||
|
- `boot-smoke.sh <zImage>` — sentinel-asserting boot; runs in CI inside the
|
||||||
|
kernel-build job.
|
||||||
|
- `portal-scenario.sh <zImage>` (needs `FLARE_EDGE=<checkout>`) — the real
|
||||||
|
flared in the VM against the desk mock portal: authenticated check-in,
|
||||||
|
firmware desired-state pull, and download of a real signed tier-1 `.wfw`
|
||||||
|
offer. Verify/stage/APPLYING run as a dry run (no `WARDEN_FW_ALLOW_APPLY`);
|
||||||
|
flipping it on inside the VM is the documented stretch — apply writes
|
||||||
|
`/dev/block/by-name/rootfs_b` inside disk.img, then `--slot _b` boots it.
|
||||||
|
- `ui-shot.sh <zImage>` — display+touch: boots headless with virtio-gpu,
|
||||||
|
QMP-screendumps the 720x720 UI, taps the Metrics tab via `input-send-event`
|
||||||
|
(a 200 ms hold — an instantaneous press+release lands inside one LVGL poll
|
||||||
|
and never clicks), and asserts the frame changed. `qmp.py` is the tiny QMP
|
||||||
|
client.
|
||||||
|
- Watchdog: `run.sh --watchdog`, arm `/dev/watchdog` in the guest, don't pet —
|
||||||
|
the VM resets ~30 s later (verified). Do NOT combine with a flared payload
|
||||||
|
expecting survival: flared pets only while the UI heartbeat is fresh.
|
||||||
|
|
||||||
|
## Gotchas that cost time (so they cost it once)
|
||||||
|
|
||||||
|
- AF_UNIX socket paths cap at ~108 chars — keep `--rs485`/`--qmp` paths short.
|
||||||
|
- A serial port that is closed discards incoming bytes: hold ONE fd open
|
||||||
|
across write and read when scripting the guest side of the RS485 bridge.
|
||||||
|
- `highmem=off` and `-global virtio-mmio.force-legacy=false` are load-bearing
|
||||||
|
(32-bit ECAM reach; virtio-1-only gpu/input) — both live ONLY in run.sh,
|
||||||
|
which every script (boot smoke included) delegates to.
|
||||||
|
- Never pass `earlyprintk`: DEBUG_UART_PHYS is the RV1106's 0xff4c0000.
|
||||||
|
|
||||||
|
## Host requirements
|
||||||
|
|
||||||
|
`qemu-system-arm` (Debian 13 ships QEMU 10), `curl`, `cpio`, `mkfs.ext4`,
|
||||||
|
`gcc-arm-linux-gnueabihf` (kernel build), `python3` (+`cryptography` for the
|
||||||
|
portal scenario's `.wfw` signing). CI: the hosted `qemu-tools` job builds the
|
||||||
|
tooling; the boot smoke runs inside the (also hosted, dispatch-only)
|
||||||
|
`kernel-build` job, which apt-installs its own toolchain and qemu (ADR-0007).
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# The device's canonical 12-partition A/B layout, expressed for the VM's
|
||||||
|
# virtio disk (vda). On hardware the same string names mmcblk0 and is baked
|
||||||
|
# into the U-Boot env — source: flare-edge docs/decisions/0003-partition-layout.md.
|
||||||
|
# There is no MBR/GPT anywhere: U-Boot and Linux both parse this string, which
|
||||||
|
# is why handing it to the VM kernel on the cmdline reproduces the exact
|
||||||
|
# partition map (vda9 = rootfs_a = hardware mmcblk0p9).
|
||||||
|
#
|
||||||
|
# Sourced by qemu/mkimage.sh (computes byte offsets from it) and qemu/run.sh
|
||||||
|
# (passes it verbatim in -append). Single source of truth — edit only here.
|
||||||
|
WARDEN_BLKDEVPARTS='vda:32K(env),512K@32K(idblock),512K(uboot),512K(misc),32M(boot_a),32M(boot_b),128M(oem_a),128M(oem_b),1G(rootfs_a),1G(rootfs_b),32M(recovery),1G(userdata)'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cd04052b8b6885f75f50b2a280bfcbf849d8710c8e61d369c533acf307eda064
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# QEMU -M virt kernel variant — merged onto build/warden_defconfig via
|
||||||
|
# WARDEN_KCONFIG_FRAGMENT (see build/build-kernel.sh). The RV1106 zImage stays
|
||||||
|
# canonical and byte-identical when the variable is unset.
|
||||||
|
#
|
||||||
|
# The canonical zImage already BOOTS on -M virt as-is (verified 2026-08-29:
|
||||||
|
# the multi_v7 heritage supplies ARCH_VIRT + the virtio set, and DEBUG_LL's
|
||||||
|
# hardcoded RV1106 UART is inert as long as `earlyprintk` is never passed on
|
||||||
|
# the cmdline). This fragment therefore only ADDS what the device-sim
|
||||||
|
# scenarios need beyond the canonical config.
|
||||||
|
|
||||||
|
# -M virt exposes exactly one PL011 (probed: QEMU 10 dtb has a single
|
||||||
|
# pl011@9000000); every additional device below rides the machine's PCIe
|
||||||
|
# (ECAM "host generic") root.
|
||||||
|
CONFIG_PCI=y
|
||||||
|
CONFIG_PCI_HOST_GENERIC=y
|
||||||
|
|
||||||
|
# Second UART for the RS485/Modbus bridge: -device pci-serial (16550-class,
|
||||||
|
# shows up as ttyS0; stage-2 init aliases it to the device's /dev/ttyS4).
|
||||||
|
CONFIG_SERIAL_8250_PCI=y
|
||||||
|
|
||||||
|
# /dev/watchdog for flared's watchdog_loop() — untestable on both existing
|
||||||
|
# sims. i6300esb is the watchdog QEMU offers on arm virt (PCI device):
|
||||||
|
# -device i6300esb -action watchdog=reset.
|
||||||
|
CONFIG_WATCHDOG=y
|
||||||
|
CONFIG_I6300ESB_WDT=y
|
||||||
|
|
||||||
|
# wg0 mesh scenarios (flared owns identity, the panel shells out `wg`).
|
||||||
|
CONFIG_WIREGUARD=y
|
||||||
|
|
||||||
|
# Display + touch: virtio-gpu scanout with fbdev emulation (the VM UI build
|
||||||
|
# uses LVGL's fbdev backend — no libdrm needed in the guest), virtio-tablet
|
||||||
|
# for absolute-coordinate touch injection via QMP.
|
||||||
|
CONFIG_FB=y
|
||||||
|
CONFIG_DRM_VIRTIO_GPU=y
|
||||||
|
CONFIG_DRM_FBDEV_EMULATION=y
|
||||||
|
CONFIG_VIRTIO_INPUT=y
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
# shellcheck shell=bash
|
||||||
|
# Shared helpers for the qemu/ device-sim build scripts. Sourced, not executed.
|
||||||
|
# Callers must run under `set -euo pipefail` and define QEMU_DIR (the qemu/ dir).
|
||||||
|
|
||||||
|
BB_VER=1.31.0
|
||||||
|
BB_URL="https://busybox.net/downloads/binaries/${BB_VER}-defconfig-multiarch-musl/busybox-armv7l"
|
||||||
|
|
||||||
|
qemu_log() { printf '\033[36m== %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# Fetch (or accept via $BUSYBOX) the pinned static armv7 busybox and verify it
|
||||||
|
# against qemu/busybox.sha256. FAILS CLOSED: a missing pin refuses to build,
|
||||||
|
# never silently skips verification — mirroring build/build-kernel.sh's
|
||||||
|
# tarball handling. Sets $BB to the verified binary's path.
|
||||||
|
qemu_get_busybox() {
|
||||||
|
local sha_file="$QEMU_DIR/busybox.sha256"
|
||||||
|
local out="${OUT:-$QEMU_DIR/out}"
|
||||||
|
mkdir -p "$out"
|
||||||
|
BB="${BUSYBOX:-$out/busybox-armv7l}"
|
||||||
|
# Pin first: a missing pin refuses BEFORE downloading, same ordering as
|
||||||
|
# build/fetch-kernel-tarball.sh.
|
||||||
|
[ -f "$sha_file" ] || {
|
||||||
|
echo "FATAL: no pinned sha256 for busybox (expected $sha_file) — refusing to build from an unverified binary" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if [ ! -f "$BB" ]; then
|
||||||
|
qemu_log "downloading $BB_URL"
|
||||||
|
curl --retry 3 --retry-delay 5 --retry-connrefused -fSL "$BB_URL" -o "$BB"
|
||||||
|
fi
|
||||||
|
local want got
|
||||||
|
want="$(cat "$sha_file")"
|
||||||
|
got="$(sha256sum "$BB" | awk '{print $1}')"
|
||||||
|
[ "$want" = "$got" ] || { echo "busybox sha256 mismatch: want $want got $got" >&2; exit 1; }
|
||||||
|
qemu_log "busybox sha256 verified"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stage the shared rootfs skeleton (qemu/rootfs/ + busybox) into $1.
|
||||||
|
# Requires qemu_get_busybox to have run (uses $BB).
|
||||||
|
qemu_stage_rootfs() {
|
||||||
|
local root="$1"
|
||||||
|
mkdir -p "$root/bin" "$root/sbin" "$root/dev" "$root/proc" "$root/sys" \
|
||||||
|
"$root/etc" "$root/tmp" "$root/mnt" "$root/userdata" "$root/oem" \
|
||||||
|
"$root/usr/bin" "$root/usr/share/udhcpc"
|
||||||
|
install -m 0755 "$BB" "$root/bin/busybox"
|
||||||
|
cp -a "$QEMU_DIR/rootfs/." "$root/"
|
||||||
|
chmod 0755 "$root/init" "$root/sbin/init" "$root/etc/rc" \
|
||||||
|
"$root/usr/share/udhcpc/default.script"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse a "SIZE[@OFFSET](NAME)" blkdevparts entry list (without the "vda:"
|
||||||
|
# prefix) and invoke a callback `$1 name offset_bytes size_bytes` per entry.
|
||||||
|
qemu_each_partition() {
|
||||||
|
local cb="$1" parts entry size_s off_s name size off
|
||||||
|
parts="${WARDEN_BLKDEVPARTS#*:}"
|
||||||
|
off=0
|
||||||
|
local IFS=','
|
||||||
|
for entry in $parts; do
|
||||||
|
name="${entry##*(}"; name="${name%)}"
|
||||||
|
size_s="${entry%%(*}"
|
||||||
|
if [ "${size_s#*@}" != "$size_s" ]; then
|
||||||
|
off_s="${size_s#*@}"; size_s="${size_s%@*}"
|
||||||
|
off="$(qemu_to_bytes "$off_s")"
|
||||||
|
fi
|
||||||
|
size="$(qemu_to_bytes "$size_s")"
|
||||||
|
"$cb" "$name" "$off" "$size"
|
||||||
|
off=$((off + size))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
qemu_to_bytes() {
|
||||||
|
local v="$1" n mult=1
|
||||||
|
case "$v" in
|
||||||
|
*K) n="${v%K}"; mult=1024 ;;
|
||||||
|
*M) n="${v%M}"; mult=1048576 ;;
|
||||||
|
*G) n="${v%G}"; mult=1073741824 ;;
|
||||||
|
*) n="$v" ;;
|
||||||
|
esac
|
||||||
|
case "$n" in
|
||||||
|
''|*[!0-9]*) echo "FATAL: bad blkdevparts size/offset token: '$v'" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
echo $((n * mult))
|
||||||
|
}
|
||||||
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the VM's virtio disk image carrying the device's canonical 12-partition
|
||||||
|
# A/B layout (qemu/blkdevparts.conf — the same string U-Boot and Linux parse on
|
||||||
|
# hardware; there is no MBR/GPT). Every partition is placed at the exact offset
|
||||||
|
# the cmdline string declares; rootfs_a/rootfs_b/oem_a/oem_b/userdata get ext4,
|
||||||
|
# the boot-chain partitions (env/idblock/uboot/misc/boot_a/boot_b/recovery)
|
||||||
|
# stay zeroed — the VM enters at -kernel and never reads them.
|
||||||
|
#
|
||||||
|
# Built entirely UNPRIVILEGED: per-partition mkfs.ext4 -d (no loop mounts, no
|
||||||
|
# sudo), then dd'd into a sparse raw image.
|
||||||
|
#
|
||||||
|
# Usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V]
|
||||||
|
# Env:
|
||||||
|
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||||
|
# OUT output dir (default: qemu/out); image at $OUT/disk.img
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=qemu/lib.sh disable=SC1091
|
||||||
|
. "$QEMU_DIR/lib.sh"
|
||||||
|
# shellcheck source=qemu/blkdevparts.conf disable=SC1091
|
||||||
|
. "$QEMU_DIR/blkdevparts.conf"
|
||||||
|
OUT="${OUT:-$QEMU_DIR/out}"
|
||||||
|
# mkfs.ext4 lives in sbin, which user shells on Debian don't have on PATH.
|
||||||
|
PATH="$PATH:/usr/sbin:/sbin"
|
||||||
|
|
||||||
|
PORTAL_URL=""
|
||||||
|
STATE_KV=()
|
||||||
|
FW_VERSION="0.0.1"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--portal-url) PORTAL_URL="${2:?--portal-url needs a value}"; shift 2 ;;
|
||||||
|
--state)
|
||||||
|
case "${2:?--state needs KEY=VALUE}" in
|
||||||
|
*=*) ;;
|
||||||
|
*) echo "FATAL: --state needs KEY=VALUE, got '$2'" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "${2%%=*}" in
|
||||||
|
.|..)
|
||||||
|
echo "FATAL: --state key cannot be '.' or '..'" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
*[!A-Za-z0-9_.]*|'')
|
||||||
|
echo "FATAL: --state key '${2%%=*}' must match [A-Za-z0-9_.]+ (it becomes a filename)" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
STATE_KV+=("$2"); shift 2 ;;
|
||||||
|
--fw-version) FW_VERSION="${2:?--fw-version needs a value}"; shift 2 ;;
|
||||||
|
*) echo "FATAL: unknown argument '$1' (usage: mkimage.sh [--portal-url URL] [--state KEY=VALUE]... [--fw-version V])" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
qemu_get_busybox
|
||||||
|
|
||||||
|
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/warden-qemu-image.XXXXXX")"
|
||||||
|
trap 'rm -rf "$SCRATCH"' EXIT
|
||||||
|
|
||||||
|
# Stage the rootfs tree once (skeleton + payload into /usr/bin), reused for
|
||||||
|
# both slots so A and B start byte-identical, like a factory flash.
|
||||||
|
ROOT="$SCRATCH/root"
|
||||||
|
qemu_stage_rootfs "$ROOT"
|
||||||
|
for p in "$QEMU_DIR"/payload/*; do
|
||||||
|
[ -f "$p" ] || continue
|
||||||
|
case "$(basename "$p")" in README.md) continue ;; esac
|
||||||
|
install -m 0755 "$p" "$ROOT/usr/bin/$(basename "$p")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Firmware version stamp — same path the device build writes; flared reads its
|
||||||
|
# running version here (downgrade rules key off it).
|
||||||
|
printf '%s\n' "$FW_VERSION" > "$ROOT/etc/warden-firmware-version"
|
||||||
|
|
||||||
|
# Seed persistent state (flared: one file per key under /userdata/warden).
|
||||||
|
# Newline-terminated, matching how flare-edge's fw-e2e-test.sh seeds the store.
|
||||||
|
UDATA="$SCRATCH/userdata"
|
||||||
|
mkdir -p "$UDATA/warden"
|
||||||
|
[ -n "$PORTAL_URL" ] && printf '%s\n' "$PORTAL_URL" > "$UDATA/warden/flare.url"
|
||||||
|
for kv in ${STATE_KV[@]+"${STATE_KV[@]}"}; do
|
||||||
|
printf '%s\n' "${kv#*=}" > "$UDATA/warden/${kv%%=*}"
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$SCRATCH/empty"
|
||||||
|
|
||||||
|
# mkfs an ext4 partition image of exactly $2 bytes from staged dir $1.
|
||||||
|
mkfs_part() {
|
||||||
|
local stage="$1" bytes="$2" img="$3"
|
||||||
|
rm -f "$img"
|
||||||
|
truncate -s "$bytes" "$img"
|
||||||
|
mkfs.ext4 -F -q -d "$stage" "$img"
|
||||||
|
}
|
||||||
|
|
||||||
|
DISK="$OUT/disk.img"
|
||||||
|
rm -f "$DISK"
|
||||||
|
|
||||||
|
place_partition() {
|
||||||
|
local name="$1" off="$2" size="$3" stage=""
|
||||||
|
case "$name" in
|
||||||
|
rootfs_a|rootfs_b) stage="$ROOT" ;;
|
||||||
|
userdata) stage="$UDATA" ;;
|
||||||
|
oem_a|oem_b) stage="$SCRATCH/empty" ;;
|
||||||
|
# Boot-chain partitions the VM never reads: present at the right offsets,
|
||||||
|
# left zeroed. Enumerated (not a wildcard) so a typo'd name in
|
||||||
|
# blkdevparts.conf fails HERE, not as a confusing mount error at boot.
|
||||||
|
env|idblock|uboot|misc|boot_a|boot_b|recovery) stage="" ;;
|
||||||
|
*) echo "FATAL: unknown partition name '$name' in blkdevparts.conf" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
# dd in 4K blocks — every offset in the canonical layout is 4K-aligned;
|
||||||
|
# assert rather than assume, a misaligned write would corrupt a neighbor.
|
||||||
|
if [ $((off % 4096)) -ne 0 ] || [ $((size % 4096)) -ne 0 ]; then
|
||||||
|
echo "FATAL: partition $name not 4K-aligned (off=$off size=$size)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Max, not last: blkdevparts grammar permits explicit @offsets out of order.
|
||||||
|
[ $((off + size)) -gt "$DISK_END" ] && DISK_END=$((off + size))
|
||||||
|
[ -z "$stage" ] && return 0
|
||||||
|
local img="$SCRATCH/$name.img"
|
||||||
|
mkfs_part "$stage" "$size" "$img"
|
||||||
|
dd if="$img" of="$DISK" bs=4096 seek=$((off / 4096)) \
|
||||||
|
conv=notrunc,sparse status=none
|
||||||
|
qemu_log " $name: ext4, $((size / 1048576))M @ $off"
|
||||||
|
}
|
||||||
|
|
||||||
|
DISK_END=0
|
||||||
|
qemu_log "building $DISK ($WARDEN_BLKDEVPARTS)"
|
||||||
|
truncate -s 0 "$DISK"
|
||||||
|
qemu_each_partition place_partition
|
||||||
|
truncate -s "$DISK_END" "$DISK"
|
||||||
|
qemu_log "disk image: $DISK ($(du -h "$DISK" | cut -f1) used, $((DISK_END / 1048576))M apparent)"
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the QEMU device-sim initramfs: the pinned static busybox + qemu/rootfs/.
|
||||||
|
# The busybox binary is the ONLY external input (sha256-pinned, fail-closed —
|
||||||
|
# see qemu/lib.sh).
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# BUSYBOX path to a local busybox binary (skips the download; still verified)
|
||||||
|
# OUT output dir (default: qemu/out); initramfs at $OUT/initramfs.cpio.gz
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=qemu/lib.sh disable=SC1091
|
||||||
|
. "$QEMU_DIR/lib.sh"
|
||||||
|
OUT="${OUT:-$QEMU_DIR/out}"
|
||||||
|
|
||||||
|
qemu_get_busybox
|
||||||
|
|
||||||
|
# Assemble in a scratch dir, removed on exit (scratch-dir leaks were a past
|
||||||
|
# review finding in this repo).
|
||||||
|
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/warden-qemu-initramfs.XXXXXX")"
|
||||||
|
trap 'rm -rf "$SCRATCH"' EXIT
|
||||||
|
qemu_stage_rootfs "$SCRATCH/root"
|
||||||
|
|
||||||
|
# Pack (newc cpio, gzip). No device nodes required: /init mounts devtmpfs and
|
||||||
|
# reopens the console itself, so the archive builds unprivileged.
|
||||||
|
( cd "$SCRATCH/root" && find . -print0 | cpio -0 -o -H newc -R +0:+0 2>/dev/null ) \
|
||||||
|
| gzip -9 > "$OUT/initramfs.cpio.gz"
|
||||||
|
qemu_log "initramfs: $OUT/initramfs.cpio.gz ($(du -h "$OUT/initramfs.cpio.gz" | cut -f1))"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# qemu/payload/ — guest binaries (never committed)
|
||||||
|
|
||||||
|
Drop **static musl armv7** binaries here; `qemu/mkimage.sh` copies everything
|
||||||
|
in this directory (except this README) into `/usr/bin/` of both rootfs slots.
|
||||||
|
Static musl is the same target the device uses for its Rust daemons, so the
|
||||||
|
exact production binaries run unmodified in the VM.
|
||||||
|
|
||||||
|
Typical payload, built in a flare-edge checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# flared (static musl armv7)
|
||||||
|
tools/build-flared.sh --local
|
||||||
|
# warden-modbus and friends: see tools/build-firmware.sh for the recipes
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp <flare-edge>/target/armv7-unknown-linux-musleabihf/release/warden-flared qemu/payload/
|
||||||
|
```
|
||||||
|
|
||||||
|
Stage-2 init starts `warden-flared`, `warden-modbus`, and `warden-ui` (the
|
||||||
|
UI additionally needs `--display on|headless` + the virt.fragment kernel for
|
||||||
|
/dev/fb0) automatically when present (logs land in `/tmp/<name>.log` inside
|
||||||
|
the guest). An empty payload is valid — the image boots busybox-only.
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/busybox sh
|
||||||
|
# shellcheck shell=dash
|
||||||
|
# Stage-1 rc: sourced by /init (still PID 1, initramfs root) when a virtio
|
||||||
|
# disk is present. Emulates U-Boot's slot choice — mount the validated slot's
|
||||||
|
# rootfs and switch_root into it. This is an EMULATION of the A/B selection
|
||||||
|
# outcome, not the BCB/bootcount mechanism itself.
|
||||||
|
#
|
||||||
|
# Every guarded failure path `return`s to /init (valid in a sourced script;
|
||||||
|
# /init then falls through to shell/poweroff). The final exec is the one
|
||||||
|
# unguardable step: if switch_root itself fails to launch, the shell — PID 1 —
|
||||||
|
# exits and the kernel panics; the applet-existence check below catches the
|
||||||
|
# only preventable variant of that.
|
||||||
|
|
||||||
|
# shellcheck source=qemu/rootfs/etc/warden-lib.sh disable=SC1091
|
||||||
|
. /etc/warden-lib.sh
|
||||||
|
|
||||||
|
warden_populate_by_name
|
||||||
|
slot="$(warden_slot)"
|
||||||
|
|
||||||
|
root="/dev/block/by-name/rootfs${slot}"
|
||||||
|
if [ ! -e "$root" ]; then
|
||||||
|
echo "rc: $root missing — staying in initramfs"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p /mnt
|
||||||
|
if ! mount -t ext4 "$root" /mnt; then
|
||||||
|
echo "rc: mount of $root failed — staying in initramfs"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ ! -x /mnt/sbin/init ]; then
|
||||||
|
echo "rc: $root has no /sbin/init — staying in initramfs"
|
||||||
|
umount /mnt
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! command -v switch_root >/dev/null; then
|
||||||
|
echo "rc: busybox lacks switch_root — staying in initramfs"
|
||||||
|
umount /mnt
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "rc: switching root to rootfs${slot} ($root)"
|
||||||
|
exec switch_root /mnt /sbin/init
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# shellcheck shell=sh
|
||||||
|
# Shared helpers for the VM's stage-1 (/init + /etc/rc, initramfs) and stage-2
|
||||||
|
# (/sbin/init, disk rootfs) boot scripts. Present in both filesystems because
|
||||||
|
# both are staged from the same qemu/rootfs/ skeleton. ONE copy of each rule —
|
||||||
|
# the slot-validation drift between two hand-copied parsers was a real
|
||||||
|
# review finding.
|
||||||
|
|
||||||
|
# Populate /dev/block/by-name/<PARTNAME> symlinks from sysfs uevents — the
|
||||||
|
# contract flare-edge's slotctl.rs relies on. blkdevparts= gives every vda
|
||||||
|
# partition a PARTNAME.
|
||||||
|
warden_populate_by_name() {
|
||||||
|
mkdir -p /dev/block/by-name
|
||||||
|
for uev in /sys/class/block/vda*/uevent; do
|
||||||
|
[ -f "$uev" ] || continue
|
||||||
|
partname=""
|
||||||
|
devname=""
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
case "$k" in
|
||||||
|
PARTNAME) partname="$v" ;;
|
||||||
|
DEVNAME) devname="$v" ;;
|
||||||
|
esac
|
||||||
|
done < "$uev"
|
||||||
|
[ -n "$partname" ] && [ -n "$devname" ] \
|
||||||
|
&& ln -sf "/dev/$devname" "/dev/block/by-name/$partname"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse warden.slot= from the cmdline (whole-token, never substring) and
|
||||||
|
# VALIDATE it — echoes "_a" or "_b", falling back to _a with a warning.
|
||||||
|
warden_slot() {
|
||||||
|
slot="_a"
|
||||||
|
# shellcheck disable=SC2013 # cmdline TOKENS are the unit here, not lines
|
||||||
|
for tok in $(cat /proc/cmdline); do
|
||||||
|
case "$tok" in
|
||||||
|
warden.slot=*) slot="${tok#warden.slot=}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "$slot" in
|
||||||
|
_a|_b) ;;
|
||||||
|
*) echo "warden-lib: bad warden.slot='$slot', falling back to _a" >&2; slot="_a" ;;
|
||||||
|
esac
|
||||||
|
echo "$slot"
|
||||||
|
}
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/busybox sh
|
||||||
|
# shellcheck shell=dash
|
||||||
|
# WardenOS QEMU device sim: initramfs /init (PID 1).
|
||||||
|
#
|
||||||
|
# Phase-1 duty: prove the kernel booted on -M virt — print the sentinel the
|
||||||
|
# smoke test greps for, then power off (PSCI SYSTEM_OFF, so qemu exits).
|
||||||
|
# `warden.shell` on the kernel cmdline drops to an interactive shell instead.
|
||||||
|
|
||||||
|
/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
||||||
|
|
||||||
|
# The cpio archive carries no device nodes (it is built unprivileged, no
|
||||||
|
# mknod); reopen stdio on the real console now that devtmpfs is mounted.
|
||||||
|
exec </dev/console >/dev/console 2>&1
|
||||||
|
|
||||||
|
/bin/busybox --install -s /bin
|
||||||
|
mount -t proc proc /proc
|
||||||
|
mount -t sysfs sysfs /sys
|
||||||
|
|
||||||
|
echo "WARDEN-QEMU-BOOT-OK"
|
||||||
|
|
||||||
|
# With a virtio disk attached, hand over to the stage-1 rc (by-name symlinks,
|
||||||
|
# slot select, switch_root). It only returns on failure — then fall through to
|
||||||
|
# the diskless shell/poweroff behavior below.
|
||||||
|
if [ -b /dev/vda ]; then
|
||||||
|
# shellcheck source=qemu/rootfs/etc/rc disable=SC1091
|
||||||
|
. /etc/rc
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -qw warden.shell /proc/cmdline; then
|
||||||
|
echo "warden.shell: interactive shell (exit to power off)"
|
||||||
|
setsid cttyhack sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
poweroff -f
|
||||||
Executable
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/busybox sh
|
||||||
|
# shellcheck shell=dash
|
||||||
|
# Stage-2 init: PID 1 on the disk rootfs (rootfs_a or rootfs_b), reached via
|
||||||
|
# switch_root from the initramfs. Brings up the minimum a WardenOS userspace
|
||||||
|
# needs — mounts, by-name symlinks, network, serial alias — then starts any
|
||||||
|
# payload daemons and holds. This stands in for the device's BusyBox SysV
|
||||||
|
# /etc/init.d/S* sequence; it is deliberately tiny, not a model of it.
|
||||||
|
|
||||||
|
/bin/busybox mount -t devtmpfs devtmpfs /dev 2>/dev/null
|
||||||
|
exec </dev/console >/dev/console 2>&1
|
||||||
|
/bin/busybox --install -s /bin
|
||||||
|
|
||||||
|
mount -t proc proc /proc
|
||||||
|
mount -t sysfs sysfs /sys
|
||||||
|
mount -t tmpfs tmpfs /tmp
|
||||||
|
|
||||||
|
# shellcheck source=qemu/rootfs/etc/warden-lib.sh disable=SC1091
|
||||||
|
. /etc/warden-lib.sh
|
||||||
|
|
||||||
|
# Fresh devtmpfs — repopulate the by-name contract; same VALIDATED slot rule
|
||||||
|
# as stage 1 (shared helper, so the two can never drift).
|
||||||
|
warden_populate_by_name
|
||||||
|
slot="$(warden_slot)"
|
||||||
|
|
||||||
|
# The device's matched mounts: persistent state and the slot's oem partition.
|
||||||
|
# Fail-fast: a scenario against an image whose userdata cannot mount would
|
||||||
|
# otherwise burn its whole deadline before failing generically. warden.shell
|
||||||
|
# still gets a shell for post-mortem.
|
||||||
|
mount_fatal() {
|
||||||
|
if ! mount -t ext4 "/dev/block/by-name/$1" "$2"; then
|
||||||
|
echo "WARDEN-QEMU-MOUNT-FAILED $1"
|
||||||
|
if grep -qw warden.shell /proc/cmdline; then
|
||||||
|
echo "warden.shell: post-mortem shell (exit powers off)"
|
||||||
|
setsid cttyhack sh
|
||||||
|
fi
|
||||||
|
poweroff -f
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
mount_fatal userdata /userdata
|
||||||
|
mount_fatal "oem${slot}" /oem
|
||||||
|
mkdir -p /userdata/warden
|
||||||
|
|
||||||
|
# RS485: warden-modbus hardcodes /dev/ttyS4 at compile time; alias it to the
|
||||||
|
# VM's pci-serial UART when one is present (needs the virt.fragment kernel).
|
||||||
|
[ -c /dev/ttyS0 ] && ln -sf /dev/ttyS0 /dev/ttyS4
|
||||||
|
|
||||||
|
# Network: slirp user-mode net on eth0 (DHCP, fallback to QEMU's static map).
|
||||||
|
# The fallback keys off the interface actually having an address — udhcpc
|
||||||
|
# exiting 0 only proves a lease, not that the hook script applied it.
|
||||||
|
ip link set lo up
|
||||||
|
if [ -e /sys/class/net/eth0 ]; then
|
||||||
|
ip link set eth0 up
|
||||||
|
udhcpc -i eth0 -n -q -t 5 -T 2 >/dev/null 2>&1 || true
|
||||||
|
if ! ip -4 addr show dev eth0 | grep -q 'inet '; then
|
||||||
|
ip addr add 10.0.2.15/24 dev eth0 2>/dev/null
|
||||||
|
ip route replace default via 10.0.2.2 dev eth0
|
||||||
|
echo "nameserver 10.0.2.3" > /etc/resolv.conf
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
hostname warden-qemu
|
||||||
|
|
||||||
|
echo "WARDEN-QEMU-ROOTFS-OK slot=${slot}"
|
||||||
|
|
||||||
|
# Payload daemons (dropped into /usr/bin by qemu/mkimage.sh from qemu/payload/).
|
||||||
|
# WARDEN_FLARE_INSECURE=1: the VM's portal is the desk mock over plain HTTP.
|
||||||
|
# This is a dev instrument — a production device build never sets it.
|
||||||
|
export WARDEN_FLARE_INSECURE=1
|
||||||
|
# No HPMCU on -M virt: the mailbox SRAM (0xff6fff00) is unmapped bus space
|
||||||
|
# here, and flared's /dev/mem poke dies with an external abort (SIGBUS). The
|
||||||
|
# SCR1 supervisor state machine is modeled in sim/src/hpmcu.rs instead.
|
||||||
|
export WARDEN_HPMCU=0
|
||||||
|
for d in /usr/bin/warden-flared /usr/bin/warden-modbus; do
|
||||||
|
if [ -x "$d" ]; then
|
||||||
|
name="$(basename "$d")"
|
||||||
|
echo "init: starting $name"
|
||||||
|
"$d" > "/tmp/${name}.log" 2>&1 &
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# The UI (LVGL fbdev+evdev build from flare-edge tools/build-ui-vm.sh) needs
|
||||||
|
# virtio-gpu's fbdev: present only with run.sh --display on|headless AND the
|
||||||
|
# virt.fragment kernel.
|
||||||
|
if [ -x /usr/bin/warden-ui ] && [ -c /dev/fb0 ]; then
|
||||||
|
echo "init: starting warden-ui (fbdev)"
|
||||||
|
/usr/bin/warden-ui > /tmp/warden-ui.log 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -qw warden.shell /proc/cmdline; then
|
||||||
|
echo "warden.shell: interactive shell (exit powers off)"
|
||||||
|
setsid cttyhack sh
|
||||||
|
poweroff -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hold: daemons run, console idles, scenarios drive the VM from outside.
|
||||||
|
while :; do sleep 3600; done
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/busybox sh
|
||||||
|
# Minimal udhcpc hook for the VM (busybox looks here by default).
|
||||||
|
[ -n "$1" ] || exit 1
|
||||||
|
case "$1" in
|
||||||
|
deconfig)
|
||||||
|
ip addr flush dev "$interface"
|
||||||
|
ip link set "$interface" up
|
||||||
|
;;
|
||||||
|
bound|renew)
|
||||||
|
ip addr replace "$ip/${mask:-24}" dev "$interface"
|
||||||
|
[ -n "${router:-}" ] && ip route replace default via "${router%% *}" dev "$interface"
|
||||||
|
if [ -n "${dns:-}" ]; then
|
||||||
|
: > /etc/resolv.conf
|
||||||
|
for d in $dns; do echo "nameserver $d" >> /etc/resolv.conf; done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
Generated
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "warden-rs485-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"warden-sim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "warden-sim"
|
||||||
|
version = "0.1.0"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "warden-rs485-bridge"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Bridges a QEMU serial chardev (unix socket) to the warden-sim Modbus RTU slave, so the VM guest's RS-485 master polls the same simulated field bus the unit tests do — fault injection included."
|
||||||
|
license = "GPL-2.0-only"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "warden_rs485_bridge"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rs485-bridge"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
# Std-only on purpose: the sim crate it wraps is zero-dep, and unix sockets +
|
||||||
|
# read timeouts need nothing external.
|
||||||
|
[dependencies]
|
||||||
|
warden-sim = { path = "../../sim" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
# Dependency-free micro-benchmarks, same pattern as sim/benches/sim_bench.rs.
|
||||||
|
[[bench]]
|
||||||
|
name = "bridge_bench"
|
||||||
|
harness = false
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//! Micro-benchmarks for the RS-485 bridge dispatch path — same dependency-free
|
||||||
|
//! fixed-iteration pattern as sim/benches/sim_bench.rs: human timings to
|
||||||
|
//! stdout, one JSON line per benchmark to stderr for CI trend capture.
|
||||||
|
//!
|
||||||
|
//! Run: `cargo bench` (or `cargo run --release --bench bridge_bench`).
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
use warden_rs485_bridge::{handle_control_line, Bus};
|
||||||
|
use warden_sim::modbus::read_holding;
|
||||||
|
|
||||||
|
fn bench<F: FnMut()>(name: &str, iters: u64, mut f: F) {
|
||||||
|
for _ in 0..(iters / 10).max(1) {
|
||||||
|
f(); // warm up
|
||||||
|
}
|
||||||
|
let t = Instant::now();
|
||||||
|
for _ in 0..iters {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
let ns = t.elapsed().as_nanos() as f64 / iters as f64;
|
||||||
|
println!("{name:<24} {ns:>9.1} ns/op ({iters} iters)");
|
||||||
|
eprintln!("{{\"bench\":\"{name}\",\"ns_per_op\":{ns:.1},\"iters\":{iters}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
const N: u64 = 1_000_000;
|
||||||
|
|
||||||
|
// Full request->reply dispatch through the locked slave (the per-poll cost
|
||||||
|
// a guest master pays on the simulated bus, minus socket I/O).
|
||||||
|
{
|
||||||
|
let bus = Bus::new(1, 128, 64);
|
||||||
|
let req = read_holding(1, 2, 4);
|
||||||
|
bench("bridge_dispatch", N, || {
|
||||||
|
let _ = bus.slave.lock().unwrap().handle_frame(&req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control-channel command parse + register write.
|
||||||
|
{
|
||||||
|
let bus = Bus::new(1, 128, 64);
|
||||||
|
bench("control_line", N, || {
|
||||||
|
let _ = handle_control_line("holding 5=1234", &bus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
//! Bridge a QEMU serial chardev (unix socket) to `warden_sim::ModbusSlave`.
|
||||||
|
//!
|
||||||
|
//! The guest side is the *master* (flare-edge's `warden-modbus` scanner, polling
|
||||||
|
//! what it believes is /dev/ttyS4); this bridge is the wire and every slave on
|
||||||
|
//! it. Frames are delimited by an inter-frame gap of silence: RTU's 3.5-char
|
||||||
|
//! rule cannot survive a socket transport, so a wall-clock gap stands in for it.
|
||||||
|
//! A mis-split frame fails CRC inside `handle_frame`, which answers `None` —
|
||||||
|
//! exactly a real slave staying silent — and the master already treats silence
|
||||||
|
//! as a timeout, so the failure mode degrades to a dropped poll, never a
|
||||||
|
//! phantom reply.
|
||||||
|
//!
|
||||||
|
//! A second unix socket (the control channel) scripts the simulated bus from
|
||||||
|
//! test harnesses: fault injection (`drop`, `exception`, `clear`) and register
|
||||||
|
//! seeding/reading, one command per line.
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
use warden_sim::ModbusSlave;
|
||||||
|
|
||||||
|
/// Default inter-frame gap. Generous next to real RTU (3.5 chars at 9600 baud
|
||||||
|
/// is ~4 ms) because a loaded host can stall a reader; the guest master's
|
||||||
|
/// response timeout is orders of magnitude larger.
|
||||||
|
pub const DEFAULT_GAP: Duration = Duration::from_millis(10);
|
||||||
|
|
||||||
|
/// Accumulation cap: a Modbus RTU ADU is at most 256 bytes, so anything past
|
||||||
|
/// 2x that without an inter-frame gap is a misbehaving master streaming
|
||||||
|
/// continuously — drop the buffer instead of growing without bound.
|
||||||
|
const MAX_PENDING: usize = 512;
|
||||||
|
|
||||||
|
/// The shared bus: the slave plus its declared dimensions. The sim's register
|
||||||
|
/// setters panic on out-of-range indices (deliberate test-harness semantics);
|
||||||
|
/// the control channel must bounds-check first so a typo in a scenario script
|
||||||
|
/// answers `err` instead of killing the VM's whole field bus.
|
||||||
|
pub struct Bus {
|
||||||
|
pub slave: Mutex<ModbusSlave>,
|
||||||
|
pub regs: usize,
|
||||||
|
pub bits: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bus {
|
||||||
|
pub fn new(address: u8, regs: usize, bits: usize) -> Self {
|
||||||
|
Bus {
|
||||||
|
slave: Mutex::new(ModbusSlave::new(address, regs, bits)),
|
||||||
|
regs,
|
||||||
|
bits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pump one serial connection until EOF: accumulate bytes, dispatch a frame to
|
||||||
|
/// the slave after `gap` of silence, write back the reply when the slave
|
||||||
|
/// answers. Any pending bytes are dispatched on EOF so a final unflushed frame
|
||||||
|
/// is not lost.
|
||||||
|
pub fn pump_serial(
|
||||||
|
stream: &UnixStream,
|
||||||
|
slave: &Mutex<ModbusSlave>,
|
||||||
|
gap: Duration,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
stream.set_read_timeout(Some(gap))?;
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut chunk = [0u8; 256];
|
||||||
|
let mut discards: u64 = 0;
|
||||||
|
loop {
|
||||||
|
match (&*stream).read(&mut chunk) {
|
||||||
|
Ok(0) => {
|
||||||
|
if !buf.is_empty() {
|
||||||
|
dispatch(&mut buf, stream, slave)?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
buf.extend_from_slice(&chunk[..n]);
|
||||||
|
if buf.len() > MAX_PENDING {
|
||||||
|
// Rate-limit the log and back off for one gap so a master
|
||||||
|
// streaming continuously cannot peg a core and flood
|
||||||
|
// stderr — mirroring the accept-loop backoff.
|
||||||
|
discards += 1;
|
||||||
|
if discards == 1 || discards.is_multiple_of(256) {
|
||||||
|
eprintln!(
|
||||||
|
"rs485: {} bytes buffered with no inter-frame gap — \
|
||||||
|
discarding (misbehaving master? {} discards so far)",
|
||||||
|
buf.len(),
|
||||||
|
discards
|
||||||
|
);
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
std::thread::sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
||||||
|
{
|
||||||
|
if !buf.is_empty() {
|
||||||
|
dispatch(&mut buf, stream, slave)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(
|
||||||
|
buf: &mut Vec<u8>,
|
||||||
|
stream: &UnixStream,
|
||||||
|
slave: &Mutex<ModbusSlave>,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let reply = slave.lock().unwrap().handle_frame(buf);
|
||||||
|
match &reply {
|
||||||
|
Some(r) => eprintln!("rs485: {} -> {}", hex(buf), hex(r)),
|
||||||
|
None => eprintln!("rs485: {} -> (silence)", hex(buf)),
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
if let Some(r) = reply {
|
||||||
|
(&*stream).write_all(&r)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex(bytes: &[u8]) -> String {
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute one control-channel command against the slave. One command per
|
||||||
|
/// line; the reply is `ok`, `ok <value>`, or `err <reason>`.
|
||||||
|
///
|
||||||
|
/// drop <n> answer the next n requests with silence
|
||||||
|
/// exception <code> NAK everything with this exception code (0x01..)
|
||||||
|
/// clear clear injected faults
|
||||||
|
/// holding <addr>=<v> seed a holding register
|
||||||
|
/// input <addr>=<v> seed an input register
|
||||||
|
/// coil <addr>=<0|1> seed a coil
|
||||||
|
/// discrete <addr>=<0|1> seed a discrete input
|
||||||
|
/// get-holding <addr> read a holding register back
|
||||||
|
/// get-coil <addr> read a coil back
|
||||||
|
/// ping liveness check
|
||||||
|
pub fn handle_control_line(line: &str, bus: &Bus) -> String {
|
||||||
|
let mut words = line.split_whitespace();
|
||||||
|
let cmd = match words.next() {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return "err empty command".into(),
|
||||||
|
};
|
||||||
|
let arg = words.next();
|
||||||
|
if words.next().is_some() {
|
||||||
|
return format!("err trailing arguments after '{cmd}'");
|
||||||
|
}
|
||||||
|
// Each arm states its own bound (bus.regs for register space, bus.bits for
|
||||||
|
// bit space) INLINE — a previous string-keyed lookup defaulted silently to
|
||||||
|
// the bit bound, which would have handed a future `get-input` command the
|
||||||
|
// wrong range and reintroduced the out-of-range panic this check prevents.
|
||||||
|
let mut s = bus.slave.lock().unwrap();
|
||||||
|
match (cmd, arg) {
|
||||||
|
("ping", None) => "ok".into(),
|
||||||
|
("clear", None) => {
|
||||||
|
s.clear_faults();
|
||||||
|
"ok".into()
|
||||||
|
}
|
||||||
|
("drop", Some(n)) => match n.parse::<usize>() {
|
||||||
|
Ok(n) => {
|
||||||
|
s.drop_next(n);
|
||||||
|
"ok".into()
|
||||||
|
}
|
||||||
|
Err(_) => format!("err bad count '{n}'"),
|
||||||
|
},
|
||||||
|
("exception", Some(c)) => match parse_u16(c) {
|
||||||
|
Some(c) if c <= 0xff => {
|
||||||
|
s.force_exception(c as u8);
|
||||||
|
"ok".into()
|
||||||
|
}
|
||||||
|
_ => format!("err bad exception code '{c}'"),
|
||||||
|
},
|
||||||
|
("holding" | "input", Some(kv)) => match parse_addr_val(kv, bus.regs) {
|
||||||
|
Ok((a, v)) => {
|
||||||
|
if cmd == "holding" {
|
||||||
|
s.set_holding(a, v);
|
||||||
|
} else {
|
||||||
|
s.set_input(a, v);
|
||||||
|
}
|
||||||
|
"ok".into()
|
||||||
|
}
|
||||||
|
Err(e) => e,
|
||||||
|
},
|
||||||
|
("coil" | "discrete", Some(kv)) => match parse_addr_val(kv, bus.bits) {
|
||||||
|
Ok((a, v)) => {
|
||||||
|
if cmd == "coil" {
|
||||||
|
s.set_coil(a, v != 0);
|
||||||
|
} else {
|
||||||
|
s.set_discrete(a, v != 0);
|
||||||
|
}
|
||||||
|
"ok".into()
|
||||||
|
}
|
||||||
|
Err(e) => e,
|
||||||
|
},
|
||||||
|
("get-holding", Some(a)) => match parse_addr(a, bus.regs) {
|
||||||
|
Ok(a) => format!("ok {}", s.holding(a)),
|
||||||
|
Err(e) => e,
|
||||||
|
},
|
||||||
|
("get-coil", Some(a)) => match parse_addr(a, bus.bits) {
|
||||||
|
Ok(a) => format!("ok {}", u8::from(s.coil(a))),
|
||||||
|
Err(e) => e,
|
||||||
|
},
|
||||||
|
_ => format!("err unknown or malformed command '{line}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse "<addr>=<value>" with the address bounds-checked against `bound`.
|
||||||
|
fn parse_addr_val(kv: &str, bound: usize) -> Result<(usize, u16), String> {
|
||||||
|
let Some((a, v)) = kv.split_once('=') else {
|
||||||
|
return Err(format!("err expected <addr>=<value>, got '{kv}'"));
|
||||||
|
};
|
||||||
|
let (Some(a), Some(v)) = (parse_u16(a), parse_u16(v)) else {
|
||||||
|
return Err(format!("err expected <addr>=<value>, got '{kv}'"));
|
||||||
|
};
|
||||||
|
if (a as usize) >= bound {
|
||||||
|
return Err(format!("err address {a} out of range (0..{bound})"));
|
||||||
|
}
|
||||||
|
Ok((a as usize, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a bare address, bounds-checked against `bound`.
|
||||||
|
fn parse_addr(a: &str, bound: usize) -> Result<usize, String> {
|
||||||
|
match parse_u16(a) {
|
||||||
|
Some(v) if (v as usize) < bound => Ok(v as usize),
|
||||||
|
Some(v) => Err(format!("err address {v} out of range (0..{bound})")),
|
||||||
|
None => Err(format!("err bad address '{a}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u16(s: &str) -> Option<u16> {
|
||||||
|
if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||||
|
u16::from_str_radix(h, 16).ok()
|
||||||
|
} else {
|
||||||
|
s.parse().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use warden_sim::modbus::{crc_ok, read_holding};
|
||||||
|
|
||||||
|
// Test gap is much larger than DEFAULT_GAP so a loaded CI runner cannot
|
||||||
|
// split a frame the test wrote in two deliberate chunks: the 2ms
|
||||||
|
// inter-chunk pause has a 60x margin against the 120ms dispatch gap
|
||||||
|
// (25ms gave only 12.5x and was flagged as a flake risk on contended
|
||||||
|
// 2-vCPU hosted runners).
|
||||||
|
const GAP: Duration = Duration::from_millis(120);
|
||||||
|
const SETTLE: Duration = Duration::from_millis(400);
|
||||||
|
|
||||||
|
fn bus() -> Bus {
|
||||||
|
let b = Bus::new(1, 16, 16);
|
||||||
|
b.slave.lock().unwrap().set_holding(2, 0xbeef);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_pump<F: FnOnce(&UnixStream)>(bus: &Bus, f: F) {
|
||||||
|
let (master, wire) = UnixStream::pair().unwrap();
|
||||||
|
thread::scope(|sc| {
|
||||||
|
sc.spawn(|| pump_serial(&wire, &bus.slave, GAP).unwrap());
|
||||||
|
f(&master);
|
||||||
|
master.shutdown(std::net::Shutdown::Both).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_reply(master: &UnixStream) -> Vec<u8> {
|
||||||
|
master
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||||
|
.unwrap();
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
let n = (&*master).read(&mut buf).expect("expected a reply frame");
|
||||||
|
buf[..n].to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whole_frame_gets_a_valid_reply() {
|
||||||
|
let s = bus();
|
||||||
|
with_pump(&s, |master| {
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let reply = read_reply(master);
|
||||||
|
assert!(crc_ok(&reply), "reply must carry a valid CRC");
|
||||||
|
// addr, fc, byte count, 0xbeef
|
||||||
|
assert_eq!(&reply[..5], &[1, 0x03, 2, 0xbe, 0xef]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_split_across_writes_within_gap_is_one_frame() {
|
||||||
|
let s = bus();
|
||||||
|
with_pump(&s, |master| {
|
||||||
|
let req = read_holding(1, 2, 1);
|
||||||
|
let (a, b) = req.split_at(3);
|
||||||
|
(&*master).write_all(a).unwrap();
|
||||||
|
thread::sleep(Duration::from_millis(2)); // well inside GAP
|
||||||
|
(&*master).write_all(b).unwrap();
|
||||||
|
let reply = read_reply(master);
|
||||||
|
assert_eq!(&reply[..5], &[1, 0x03, 2, 0xbe, 0xef]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_frames_separated_by_gap_get_two_replies() {
|
||||||
|
let s = bus();
|
||||||
|
with_pump(&s, |master| {
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let first = read_reply(master);
|
||||||
|
assert_eq!(&first[..5], &[1, 0x03, 2, 0xbe, 0xef]);
|
||||||
|
thread::sleep(SETTLE);
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let second = read_reply(master);
|
||||||
|
assert_eq!(second, first);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_drop_is_silence_then_recovery() {
|
||||||
|
let s = bus();
|
||||||
|
assert_eq!(handle_control_line("drop 1", &s), "ok");
|
||||||
|
with_pump(&s, |master| {
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
master
|
||||||
|
// Well past GAP: the dropped frame must have been dispatched
|
||||||
|
// (and answered with silence) before the next request is
|
||||||
|
// written, or the two would merge in the pending buffer.
|
||||||
|
.set_read_timeout(Some(Duration::from_millis(500)))
|
||||||
|
.unwrap();
|
||||||
|
let mut buf = [0u8; 16];
|
||||||
|
assert!(
|
||||||
|
(&*master).read(&mut buf).is_err(),
|
||||||
|
"dropped request must produce silence"
|
||||||
|
);
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let reply = read_reply(master);
|
||||||
|
assert_eq!(&reply[..5], &[1, 0x03, 2, 0xbe, 0xef]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_seeds_and_reads_registers() {
|
||||||
|
let s = bus();
|
||||||
|
assert_eq!(handle_control_line("holding 5=1234", &s), "ok");
|
||||||
|
assert_eq!(handle_control_line("get-holding 5", &s), "ok 1234");
|
||||||
|
assert_eq!(handle_control_line("coil 3=1", &s), "ok");
|
||||||
|
assert_eq!(handle_control_line("get-coil 3", &s), "ok 1");
|
||||||
|
assert_eq!(handle_control_line("holding 0xF=0xff", &s), "ok");
|
||||||
|
assert_eq!(handle_control_line("get-holding 15", &s), "ok 255");
|
||||||
|
// Out of range must answer err, never panic the bus (16-reg slave).
|
||||||
|
assert!(handle_control_line("holding 0x10=0xff", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("get-holding 16", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("coil 16=1", &s).starts_with("err"));
|
||||||
|
assert_eq!(handle_control_line("ping", &s), "ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_rejects_malformed_lines() {
|
||||||
|
let s = bus();
|
||||||
|
assert!(handle_control_line("", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("drop many", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("holding 5", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("exception 300", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("frobnicate 1", &s).starts_with("err"));
|
||||||
|
assert!(handle_control_line("drop 1 2", &s).starts_with("err"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forced_exception_naks_and_clear_recovers() {
|
||||||
|
let s = bus();
|
||||||
|
assert_eq!(handle_control_line("exception 0x02", &s), "ok");
|
||||||
|
with_pump(&s, |master| {
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let nak = read_reply(master);
|
||||||
|
assert_eq!(&nak[..3], &[1, 0x83, 0x02], "fc|0x80 + exception code");
|
||||||
|
assert_eq!(handle_control_line("clear", &s), "ok");
|
||||||
|
thread::sleep(SETTLE);
|
||||||
|
(&*master).write_all(&read_holding(1, 2, 1)).unwrap();
|
||||||
|
let reply = read_reply(master);
|
||||||
|
assert_eq!(&reply[..5], &[1, 0x03, 2, 0xbe, 0xef]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
//! CLI wiring for the RS-485 bridge. All behavior lives in the lib (tested
|
||||||
|
//! there); this file only parses arguments, connects sockets, and spawns the
|
||||||
|
//! control listener.
|
||||||
|
//!
|
||||||
|
//! Typical use (matches qemu/run.sh --rs485). Put the sockets in a private
|
||||||
|
//! per-run directory (mktemp -d) — short (AF_UNIX caps paths at ~108 chars)
|
||||||
|
//! and not guessable/pre-creatable by other local users, unlike a fixed
|
||||||
|
//! /tmp name:
|
||||||
|
//!
|
||||||
|
//! d=$(mktemp -d /tmp/rs485.XXXXXX)
|
||||||
|
//! qemu/run.sh --kernel ... --rs485 "$d/serial.sock" &
|
||||||
|
//! rs485-bridge --serial "$d/serial.sock" --control "$d/ctl.sock"
|
||||||
|
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
use std::process::exit;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use warden_rs485_bridge::{handle_control_line, pump_serial, Bus, DEFAULT_GAP};
|
||||||
|
|
||||||
|
fn usage() -> ! {
|
||||||
|
eprintln!(
|
||||||
|
"usage: rs485-bridge --serial <sock> [--control <sock>] [--address N] \
|
||||||
|
[--regs N] [--bits N] [--gap-ms N]"
|
||||||
|
);
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut serial: Option<String> = None;
|
||||||
|
let mut control: Option<String> = None;
|
||||||
|
let mut address: u8 = 1;
|
||||||
|
let mut regs: usize = 128;
|
||||||
|
let mut bits: usize = 64;
|
||||||
|
let mut gap = DEFAULT_GAP;
|
||||||
|
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(a) = args.next() {
|
||||||
|
let mut val = |name: &str| {
|
||||||
|
let v = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("{name} needs a value");
|
||||||
|
usage()
|
||||||
|
});
|
||||||
|
// A following flag means the value was omitted — report the real
|
||||||
|
// problem instead of swallowing the flag as a bogus value.
|
||||||
|
if v.starts_with("--") {
|
||||||
|
eprintln!("{name} needs a value, got flag '{v}'");
|
||||||
|
usage()
|
||||||
|
}
|
||||||
|
v
|
||||||
|
};
|
||||||
|
match a.as_str() {
|
||||||
|
"--serial" => serial = Some(val("--serial")),
|
||||||
|
"--control" => control = Some(val("--control")),
|
||||||
|
"--address" => address = val("--address").parse().unwrap_or_else(|_| usage()),
|
||||||
|
"--regs" => regs = val("--regs").parse().unwrap_or_else(|_| usage()),
|
||||||
|
"--bits" => bits = val("--bits").parse().unwrap_or_else(|_| usage()),
|
||||||
|
"--gap-ms" => {
|
||||||
|
gap = Duration::from_millis(val("--gap-ms").parse().unwrap_or_else(|_| usage()))
|
||||||
|
}
|
||||||
|
_ => usage(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let serial = serial.unwrap_or_else(|| usage());
|
||||||
|
|
||||||
|
// The bus is shared between the serial pump and the control channel.
|
||||||
|
// 'static so the control thread needs no scoped lifetime: the bridge runs
|
||||||
|
// until killed.
|
||||||
|
let bus: &'static Bus = Box::leak(Box::new(Bus::new(address, regs, bits)));
|
||||||
|
|
||||||
|
if let Some(path) = control {
|
||||||
|
// Clear a stale socket from a previous run. A failure here that is not
|
||||||
|
// "nothing to remove" (e.g. someone else's file behind /tmp's sticky
|
||||||
|
// bit) will make the bind below fail — surface both errors.
|
||||||
|
let removed = std::fs::remove_file(&path);
|
||||||
|
let listener = UnixListener::bind(&path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("FATAL: cannot bind control socket {path}: {e}");
|
||||||
|
if let Err(re) = removed {
|
||||||
|
if re.kind() != std::io::ErrorKind::NotFound {
|
||||||
|
eprintln!(" (removing the pre-existing file also failed: {re})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
eprintln!("rs485: control socket at {path}");
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// Explicit error handling: `.flatten()` would turn a persistent
|
||||||
|
// accept() failure (fd exhaustion etc.) into a silent hot loop.
|
||||||
|
for conn in listener.incoming() {
|
||||||
|
let conn = match conn {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("rs485: control accept failed: {e} — backing off");
|
||||||
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let reader = BufReader::new(conn.try_clone().expect("clone control conn"));
|
||||||
|
let mut writer = conn;
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = match line {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
let reply = handle_control_line(&line, bus);
|
||||||
|
if writeln!(writer, "{reply}").is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// QEMU (chardev server=on) may come up after us: retry the connect briefly
|
||||||
|
// instead of racing the VM launch.
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(15);
|
||||||
|
let stream = loop {
|
||||||
|
match UnixStream::connect(&serial) {
|
||||||
|
Ok(s) => break s,
|
||||||
|
Err(e) if Instant::now() < deadline => {
|
||||||
|
eprintln!("rs485: waiting for {serial} ({e})");
|
||||||
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("FATAL: cannot connect serial socket {serial}: {e}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eprintln!("rs485: connected to {serial}, slave address {address}, gap {gap:?}");
|
||||||
|
|
||||||
|
if let Err(e) = pump_serial(&stream, &bus.slave, gap) {
|
||||||
|
eprintln!("FATAL: serial pump: {e}");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
eprintln!("rs485: serial closed (VM gone), exiting");
|
||||||
|
}
|
||||||
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Launch the WardenOS device VM (qemu-system-arm -M virt, single Cortex-A7,
|
||||||
|
# 256M — the RV1106G3's shape). See qemu/README.md for what this does and does
|
||||||
|
# not emulate.
|
||||||
|
#
|
||||||
|
# Usage: run.sh --kernel <zImage> [options] [-- <extra qemu args>]
|
||||||
|
# --kernel PATH zImage (canonical or virt.fragment variant)
|
||||||
|
# --initrd PATH initramfs (default: qemu/out/initramfs.cpio.gz)
|
||||||
|
# --disk PATH virtio disk image from mkimage.sh (default: qemu/out/disk.img
|
||||||
|
# when present; pass --no-disk for a diskless initramfs boot)
|
||||||
|
# --no-disk boot without a disk (initramfs shell/smoke behavior)
|
||||||
|
# --slot _a|_b rootfs slot to boot (default _a)
|
||||||
|
# --rtc DATE guest RTC base, e.g. 2021-01-01 — reproduces the no-RTC
|
||||||
|
# "device boots believing 2021" incident class
|
||||||
|
# --rs485 SOCK unix socket chardev for the RS485/Modbus bridge
|
||||||
|
# (pci-serial: needs the virt.fragment kernel)
|
||||||
|
# --watchdog add i6300esb watchdog, reset on expiry (fragment kernel)
|
||||||
|
# --qmp SOCK QMP unix socket (screendump, input-send-event, quit)
|
||||||
|
# --display MODE off (default, -nographic) | on (gtk window) | headless
|
||||||
|
# (virtio-gpu without a window; screendump via --qmp)
|
||||||
|
# --ssh-port N hostfwd 127.0.0.1:N -> guest :22 (default 2222; 0 disables)
|
||||||
|
# --http-port N hostfwd 127.0.0.1:N -> guest :80 (default 8080; 0 disables)
|
||||||
|
# --api-port N hostfwd 127.0.0.1:N -> guest :28443 (default 28443; 0 disables)
|
||||||
|
# --shell interactive shell in the guest instead of daemon hold
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
QEMU_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=qemu/blkdevparts.conf disable=SC1091
|
||||||
|
. "$QEMU_DIR/blkdevparts.conf"
|
||||||
|
OUT="${OUT:-$QEMU_DIR/out}"
|
||||||
|
|
||||||
|
KERNEL="" INITRD="$OUT/initramfs.cpio.gz" DISK="" NO_DISK=0 SLOT="_a"
|
||||||
|
RTC="" RS485="" WATCHDOG=0 QMP="" DISPLAY_MODE="off" SHELL_FLAG=0
|
||||||
|
SSH_PORT=2222 HTTP_PORT=8080 API_PORT=28443
|
||||||
|
EXTRA=()
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--kernel) KERNEL="${2:?}"; shift 2 ;;
|
||||||
|
--initrd) INITRD="${2:?}"; shift 2 ;;
|
||||||
|
--disk) DISK="${2:?}"; shift 2 ;;
|
||||||
|
--no-disk) NO_DISK=1; shift ;;
|
||||||
|
--slot) SLOT="${2:?}"; shift 2 ;;
|
||||||
|
--rtc) RTC="${2:?}"; shift 2 ;;
|
||||||
|
--rs485) RS485="${2:?}"; shift 2 ;;
|
||||||
|
--watchdog) WATCHDOG=1; shift ;;
|
||||||
|
--qmp) QMP="${2:?}"; shift 2 ;;
|
||||||
|
--display) DISPLAY_MODE="${2:?}"; shift 2 ;;
|
||||||
|
--ssh-port) SSH_PORT="${2:?}"; shift 2 ;;
|
||||||
|
--http-port) HTTP_PORT="${2:?}"; shift 2 ;;
|
||||||
|
--api-port) API_PORT="${2:?}"; shift 2 ;;
|
||||||
|
--shell) SHELL_FLAG=1; shift ;;
|
||||||
|
--) shift; EXTRA=("$@"); break ;;
|
||||||
|
*) echo "FATAL: unknown argument '$1' (see header of $0)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$KERNEL" ] || [ ! -f "$KERNEL" ]; then
|
||||||
|
echo "FATAL: --kernel <zImage> required and must exist (got '${KERNEL:-}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -f "$INITRD" ] || {
|
||||||
|
echo "FATAL: initramfs not found at $INITRD — run qemu/mkinitramfs.sh" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
case "$SLOT" in _a|_b) ;; *) echo "FATAL: --slot must be _a or _b" >&2; exit 1 ;; esac
|
||||||
|
|
||||||
|
if [ "$NO_DISK" -eq 0 ] && [ -z "$DISK" ] && [ -f "$OUT/disk.img" ]; then
|
||||||
|
DISK="$OUT/disk.img"
|
||||||
|
fi
|
||||||
|
if [ -n "$DISK" ] && [ ! -f "$DISK" ]; then
|
||||||
|
echo "FATAL: disk image $DISK not found — run qemu/mkimage.sh (or pass --no-disk)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NOTE: never add `earlyprintk` — the config's DEBUG_UART_PHYS is the RV1106's
|
||||||
|
# 0xff4c0000, which does not exist on -M virt.
|
||||||
|
APPEND="console=ttyAMA0 rdinit=/init"
|
||||||
|
# Port 0 disables a forward — a boot smoke needs no host ports and must not
|
||||||
|
# fail on a busy default port.
|
||||||
|
NETDEV="user,id=n0"
|
||||||
|
[ "$SSH_PORT" != 0 ] && NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22"
|
||||||
|
[ "$HTTP_PORT" != 0 ] && NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:${HTTP_PORT}-:80"
|
||||||
|
[ "$API_PORT" != 0 ] && NETDEV="$NETDEV,hostfwd=tcp:127.0.0.1:${API_PORT}-:28443"
|
||||||
|
ARGS=(
|
||||||
|
-M "virt,highmem=off" -cpu cortex-a7 -smp 1 -m 256M
|
||||||
|
# virtio-mmio defaults to the legacy (0.9) transport; virtio-gpu and
|
||||||
|
# virtio-input are VERSION_1-only devices and never bind without this.
|
||||||
|
-global "virtio-mmio.force-legacy=false"
|
||||||
|
-kernel "$KERNEL" -initrd "$INITRD"
|
||||||
|
-netdev "$NETDEV"
|
||||||
|
-device "virtio-net-device,netdev=n0"
|
||||||
|
-no-reboot
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$DISK" ] && [ "$NO_DISK" -eq 0 ]; then
|
||||||
|
APPEND="$APPEND blkdevparts=$WARDEN_BLKDEVPARTS warden.slot=$SLOT"
|
||||||
|
ARGS+=( -drive "if=none,file=$DISK,format=raw,id=vd0"
|
||||||
|
-device "virtio-blk-device,drive=vd0" )
|
||||||
|
fi
|
||||||
|
[ "$SHELL_FLAG" -eq 1 ] && APPEND="$APPEND warden.shell"
|
||||||
|
[ -n "$RTC" ] && ARGS+=( -rtc "base=$RTC" )
|
||||||
|
[ "$WATCHDOG" -eq 1 ] && ARGS+=( -device i6300esb -action watchdog=reset )
|
||||||
|
[ -n "$RS485" ] && ARGS+=( -chardev "socket,id=rs485,path=$RS485,server=on,wait=off"
|
||||||
|
-device "pci-serial,chardev=rs485" )
|
||||||
|
[ -n "$QMP" ] && ARGS+=( -qmp "unix:$QMP,server=on,wait=off" )
|
||||||
|
|
||||||
|
case "$DISPLAY_MODE" in
|
||||||
|
off) ARGS+=( -nographic ) ;;
|
||||||
|
on) ARGS+=( -device "virtio-gpu-device,xres=720,yres=720"
|
||||||
|
-device virtio-tablet-device -serial mon:stdio ) ;;
|
||||||
|
headless) ARGS+=( -device "virtio-gpu-device,xres=720,yres=720"
|
||||||
|
-device virtio-tablet-device -display none -serial mon:stdio ) ;;
|
||||||
|
*) echo "FATAL: --display must be off|on|headless" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exec qemu-system-arm "${ARGS[@]}" -append "$APPEND" ${EXTRA[@]+"${EXTRA[@]}"}
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Boot the warden kernel under qemu-system-arm -M virt and assert the initramfs
|
||||||
|
# sentinel appears on the console. FAILS CLOSED: a missing zImage, initramfs,
|
||||||
|
# or qemu binary is an error, never a skip.
|
||||||
|
#
|
||||||
|
# Usage: boot-smoke.sh <zImage> [initramfs.cpio.gz]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||||
|
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||||
|
|
||||||
|
ZIMAGE="${1:-}"
|
||||||
|
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
||||||
|
echo "FATAL: usage: $0 <zImage> [initramfs] — zImage missing or not a file: '${ZIMAGE:-}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
INITRD="${2:-$QDIR/out/initramfs.cpio.gz}"
|
||||||
|
[ -f "$INITRD" ] || {
|
||||||
|
echo "FATAL: initramfs not found at $INITRD — run qemu/mkinitramfs.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
command -v qemu-system-arm >/dev/null || {
|
||||||
|
echo "FATAL: qemu-system-arm not on PATH (apt-get install qemu-system-arm) — see qemu/README.md" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG="$(mktemp "${TMPDIR:-/tmp}/warden-qemu-smoke.XXXXXX")"
|
||||||
|
trap 'rm -f "$LOG"' EXIT
|
||||||
|
|
||||||
|
# Delegate the qemu invocation to run.sh (--no-disk) so the machine shape
|
||||||
|
# (-M virt,highmem=off, cpu, memory, virtio topology) lives in exactly one
|
||||||
|
# place — the two hand-copied invocations had already drifted once.
|
||||||
|
# timeout -k: a wedged qemu that ignores SIGTERM gets SIGKILLed 10s later
|
||||||
|
# instead of holding the job until the workflow-level timeout.
|
||||||
|
timeout -k 10 180 bash "$QDIR/run.sh" \
|
||||||
|
--kernel "$ZIMAGE" --initrd "$INITRD" --no-disk \
|
||||||
|
--ssh-port 0 --http-port 0 --api-port 0 \
|
||||||
|
</dev/null | tee "$LOG" || {
|
||||||
|
echo "FATAL: qemu exited non-zero (or hung until the 180s timeout)" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "WARDEN-QEMU-BOOT-OK" "$LOG" || {
|
||||||
|
echo "FATAL: boot sentinel WARDEN-QEMU-BOOT-OK not found in console log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "boot smoke OK"
|
||||||
Executable
+150
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end device scenario: the REAL warden-flared, running inside the VM,
|
||||||
|
# checks in to flare-edge's mock FLARE portal on the host and pulls its
|
||||||
|
# firmware desired-state — the exact device-initiated HTTPS(-shaped) flow a
|
||||||
|
# panel performs, with zero flare-edge code changes (the portal URL is a state
|
||||||
|
# file; 10.0.2.2 is slirp's host alias).
|
||||||
|
#
|
||||||
|
# FAILS CLOSED on every missing prerequisite — never a soft skip.
|
||||||
|
#
|
||||||
|
# Usage: portal-scenario.sh <zImage-virt>
|
||||||
|
# Env: FLARE_EDGE path to a flare-edge checkout (provides mock-flare-portal.py)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||||
|
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||||
|
|
||||||
|
ZIMAGE="${1:-}"
|
||||||
|
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
||||||
|
echo "FATAL: usage: $0 <zImage> — the virt.fragment kernel variant" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${FLARE_EDGE:-}" ] || [ ! -f "$FLARE_EDGE/tools/mock-flare-portal.py" ]; then
|
||||||
|
echo "FATAL: FLARE_EDGE must point at a flare-edge checkout (mock-flare-portal.py not found under '${FLARE_EDGE:-}')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -x "$QDIR/payload/warden-flared" ] || {
|
||||||
|
echo "FATAL: no qemu/payload/warden-flared — build a static musl armv7 flared (see qemu/payload/README.md)" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
command -v qemu-system-arm >/dev/null || {
|
||||||
|
echo "FATAL: qemu-system-arm not on PATH — see qemu/README.md" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short-named scratch: AF_UNIX socket paths are capped at ~108 chars.
|
||||||
|
WORK="$(mktemp -d /tmp/wqp.XXXXXX)"
|
||||||
|
QEMU_PID="" MOCK_PID=""
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$QEMU_PID" ]; then kill "$QEMU_PID" 2>/dev/null || true; fi
|
||||||
|
if [ -n "$MOCK_PID" ]; then kill "$MOCK_PID" 2>/dev/null || true; fi
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
DEVICE_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
|
||||||
|
API_KEY="$(python3 -c 'import secrets; print(secrets.token_hex(24))')"
|
||||||
|
PORT=$((20000 + RANDOM % 20000))
|
||||||
|
|
||||||
|
# 0. a real signed tier-1 .wfw offer (version above the image's 0.0.1), so the
|
||||||
|
# scenario exercises desired-state -> download -> signature/hash verify, not
|
||||||
|
# just an empty 204. Signed with the committed desk key; the payload flared
|
||||||
|
# is a FLARED_DEV_KEY=1 build that trusts it (desk-testing only).
|
||||||
|
head -c 8388608 /dev/urandom > "$WORK/rootfs-payload.img"
|
||||||
|
FW_SIGNING_KEY_FILE="$FLARE_EDGE/tools/testdata/fw-dev-key.seed" \
|
||||||
|
WARDEN_KERNEL_VERSION=6.18.46 WARDEN_BUILDROOT_VERSION=2025.02 \
|
||||||
|
WARDEN_UBOOT_VERSION=2017.09 \
|
||||||
|
bash "$FLARE_EDGE/tools/mk-wfw.sh" "$WORK/rootfs-payload.img" 1 0.0.2 "$WORK/offer.wfw"
|
||||||
|
|
||||||
|
# 1. mock portal on the host, our device pre-registered (no pairing needed —
|
||||||
|
# the same credential-seeding shortcut fw-e2e-test.sh uses), offering the .wfw.
|
||||||
|
python3 "$FLARE_EDGE/tools/mock-flare-portal.py" \
|
||||||
|
--port "$PORT" --device "$DEVICE_ID:$API_KEY" \
|
||||||
|
--wfw "$WORK/offer.wfw" > "$WORK/mock.log" 2>&1 &
|
||||||
|
MOCK_PID=$!
|
||||||
|
mock_ready=0
|
||||||
|
for _ in $(seq 1 50); do
|
||||||
|
curl -so /dev/null "http://127.0.0.1:$PORT/" && { mock_ready=1; break; }
|
||||||
|
kill -0 "$MOCK_PID" 2>/dev/null || { echo "FATAL: mock portal died:" >&2; cat "$WORK/mock.log" >&2; exit 1; }
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
# The loop must not fall through silently: an alive-but-unresponsive mock
|
||||||
|
# would otherwise surface 420s later as an unrelated assertion timeout.
|
||||||
|
[ "$mock_ready" = 1 ] || {
|
||||||
|
echo "FATAL: mock portal never answered on :$PORT within 10s" >&2
|
||||||
|
tail -10 "$WORK/mock.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "== mock portal on :$PORT, device $DEVICE_ID"
|
||||||
|
|
||||||
|
# 2. image seeded with the portal URL + credentials.
|
||||||
|
bash "$QDIR/mkinitramfs.sh"
|
||||||
|
# All four enrolment keys — flare::enrolment() returns None (and the report
|
||||||
|
# loop parks forever) unless flare.site is present too.
|
||||||
|
bash "$QDIR/mkimage.sh" \
|
||||||
|
--portal-url "http://10.0.2.2:$PORT" \
|
||||||
|
--state "flare.device_id=$DEVICE_ID" \
|
||||||
|
--state "flare.api_key=$API_KEY" \
|
||||||
|
--state "flare.site=qemu-devsim"
|
||||||
|
|
||||||
|
# 3. boot the VM headless (daemons run; console log to file). Random hostfwd
|
||||||
|
# ports can collide with another process — detect the early qemu bind
|
||||||
|
# failure and retry with a fresh base rather than failing spuriously.
|
||||||
|
QEMU_PID=""
|
||||||
|
for _attempt in 1 2 3; do
|
||||||
|
VMBASE=$((20000 + RANDOM % 20000))
|
||||||
|
: > "$WORK/console.log"
|
||||||
|
bash "$QDIR/run.sh" --kernel "$ZIMAGE" \
|
||||||
|
--ssh-port "$VMBASE" --http-port $((VMBASE + 1)) --api-port $((VMBASE + 2)) \
|
||||||
|
> "$WORK/console.log" 2>&1 &
|
||||||
|
QEMU_PID=$!
|
||||||
|
sleep 3
|
||||||
|
if kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -aq 'Could not set up host forwarding' "$WORK/console.log"; then
|
||||||
|
echo "== hostfwd port collision on base $VMBASE — retrying"
|
||||||
|
QEMU_PID=""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "FATAL: VM died at launch:" >&2
|
||||||
|
tail -20 "$WORK/console.log" >&2
|
||||||
|
exit 1
|
||||||
|
done
|
||||||
|
if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||||
|
echo "FATAL: could not launch the VM after 3 port attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. assert: rootfs up, and the portal saw — from OUR device id — an
|
||||||
|
# authenticated check-in, the firmware desired-state pull, and the signed
|
||||||
|
# .wfw asset download (i.e. flared accepted the offer and fetched it; the
|
||||||
|
# verify+stage+APPLYING that follow are a dry run without
|
||||||
|
# WARDEN_FW_ALLOW_APPLY, exactly like fw-e2e-test.sh).
|
||||||
|
deadline=$((SECONDS + 420))
|
||||||
|
ok_boot=0 ok_report=0 ok_fw=0 ok_asset=0
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
[ $ok_boot -eq 0 ] && grep -aq 'WARDEN-QEMU-ROOTFS-OK' "$WORK/console.log" && {
|
||||||
|
ok_boot=1; echo "== VM userspace up"; }
|
||||||
|
grep -aq "POST /api/v1/devices/$DEVICE_ID/report -> 200" "$WORK/mock.log" && ok_report=1
|
||||||
|
grep -aq "GET /api/v1/devices/$DEVICE_ID/firmware -> 200" "$WORK/mock.log" && ok_fw=1
|
||||||
|
grep -aq "GET /api/v1/devices/$DEVICE_ID/firmware/assets/.* -> 200" "$WORK/mock.log" && ok_asset=1
|
||||||
|
[ $ok_report -eq 1 ] && [ $ok_fw -eq 1 ] && [ $ok_asset -eq 1 ] && break
|
||||||
|
grep -aq 'WARDEN-QEMU-MOUNT-FAILED' "$WORK/console.log" && {
|
||||||
|
echo "FATAL: guest partition mount failed (bad image?):" >&2
|
||||||
|
grep -a 'WARDEN-QEMU-MOUNT-FAILED' "$WORK/console.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null || { echo "FATAL: VM exited early" >&2; tail -30 "$WORK/console.log" >&2; exit 1; }
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== portal log (our device's requests):"
|
||||||
|
grep -a "$DEVICE_ID" "$WORK/mock.log" | tail -5 || true
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
[ $ok_boot -eq 1 ] || { echo "FAIL: VM never reached WARDEN-QEMU-ROOTFS-OK"; fail=1; }
|
||||||
|
[ $ok_report -eq 1 ] || { echo "FAIL: no authenticated check-in (POST report 200) seen"; fail=1; }
|
||||||
|
[ $ok_fw -eq 1 ] || { echo "FAIL: no firmware desired-state pull (GET firmware 200) seen"; fail=1; }
|
||||||
|
[ $ok_asset -eq 1 ] || { echo "FAIL: signed .wfw asset was never downloaded"; fail=1; }
|
||||||
|
if [ $fail -eq 0 ]; then echo "PORTAL-SCENARIO-PASS"; else echo "PORTAL-SCENARIO-FAIL"; exit 1; fi
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tiny QMP client for the qemu/ device-sim tests.
|
||||||
|
|
||||||
|
qmp.py <socket> screendump <out.ppm>
|
||||||
|
qmp.py <socket> tap <x> <y> # absolute 0..32767 (virtio-tablet)
|
||||||
|
qmp.py <socket> quit
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def rpc(sock, sock_file, obj):
|
||||||
|
sock.sendall((json.dumps(obj) + "\n").encode())
|
||||||
|
while True:
|
||||||
|
line = sock_file.readline()
|
||||||
|
if not line:
|
||||||
|
raise RuntimeError("QMP connection closed")
|
||||||
|
msg = json.loads(line)
|
||||||
|
if "return" in msg:
|
||||||
|
return msg["return"]
|
||||||
|
if "error" in msg:
|
||||||
|
raise RuntimeError(f"QMP error: {msg['error']}")
|
||||||
|
# asynchronous events are interleaved; skip them
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
path, cmd = sys.argv[1], sys.argv[2]
|
||||||
|
need = {"screendump": 4, "tap": 5, "quit": 3}
|
||||||
|
if cmd not in need:
|
||||||
|
sys.exit(f"unknown command {cmd}\n{__doc__}")
|
||||||
|
if len(sys.argv) < need[cmd]:
|
||||||
|
sys.exit(f"{cmd}: missing argument(s)\n{__doc__}")
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_UNIX)
|
||||||
|
s.connect(path)
|
||||||
|
f = s.makefile("r")
|
||||||
|
f.readline() # greeting banner
|
||||||
|
rpc(s, f, {"execute": "qmp_capabilities"})
|
||||||
|
|
||||||
|
if cmd == "screendump":
|
||||||
|
rpc(s, f, {"execute": "screendump", "arguments": {"filename": sys.argv[3]}})
|
||||||
|
elif cmd == "tap":
|
||||||
|
x, y = int(sys.argv[3]), int(sys.argv[4])
|
||||||
|
press = [
|
||||||
|
{"type": "abs", "data": {"axis": "x", "value": x}},
|
||||||
|
{"type": "abs", "data": {"axis": "y", "value": y}},
|
||||||
|
{"type": "btn", "data": {"down": True, "button": "left"}},
|
||||||
|
]
|
||||||
|
release = [{"type": "btn", "data": {"down": False, "button": "left"}}]
|
||||||
|
rpc(s, f, {"execute": "input-send-event", "arguments": {"events": press}})
|
||||||
|
# Hold the press across several LVGL indev poll periods (33 ms each):
|
||||||
|
# an instantaneous press+release lands inside one poll and no click
|
||||||
|
# is ever registered.
|
||||||
|
time.sleep(0.2)
|
||||||
|
rpc(s, f, {"execute": "input-send-event", "arguments": {"events": release}})
|
||||||
|
elif cmd == "quit":
|
||||||
|
s.sendall(b'{"execute":"quit"}\n')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+150
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Display + touch scenario: boot the VM headless with virtio-gpu, wait for the
|
||||||
|
# LVGL UI (fbdev build) to render a real frame, then inject an absolute touch
|
||||||
|
# tap on the Metrics tab (virtio-tablet) and ASSERT the frame changed — the
|
||||||
|
# device-level analogue of flare-edge's Xvfb/xdotool sim-test.sh. Readiness is
|
||||||
|
# polled from screendumps on bounded deadlines, never guessed with fixed
|
||||||
|
# sleeps: TCG renders CPU-bound and a loaded host can be arbitrarily slow.
|
||||||
|
#
|
||||||
|
# FAILS CLOSED on missing prerequisites.
|
||||||
|
#
|
||||||
|
# Usage: ui-shot.sh <zImage-virt> [out-dir]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # qemu/tests/
|
||||||
|
QDIR="$(cd "$HERE/.." && pwd)" # qemu/
|
||||||
|
|
||||||
|
ZIMAGE="${1:-}"
|
||||||
|
OUTDIR="${2:-$QDIR/out}"
|
||||||
|
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
||||||
|
echo "FATAL: usage: $0 <zImage> [out-dir] — the virt.fragment kernel variant" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -x "$QDIR/payload/warden-ui" ] || {
|
||||||
|
echo "FATAL: no qemu/payload/warden-ui — build it with flare-edge tools/build-ui-vm.sh" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
command -v qemu-system-arm >/dev/null || {
|
||||||
|
echo "FATAL: qemu-system-arm not on PATH — see qemu/README.md" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short-named scratch: AF_UNIX socket paths are capped at ~108 chars.
|
||||||
|
WORK="$(mktemp -d /tmp/wqu.XXXXXX)"
|
||||||
|
QEMU_PID=""
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$QEMU_PID" ]; then kill "$QEMU_PID" 2>/dev/null || true; fi
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
bash "$QDIR/mkinitramfs.sh"
|
||||||
|
bash "$QDIR/mkimage.sh"
|
||||||
|
|
||||||
|
# Random hostfwd ports can collide — detect qemu's early bind failure and
|
||||||
|
# retry with a fresh base rather than failing spuriously.
|
||||||
|
for _attempt in 1 2 3; do
|
||||||
|
PORT=$((21000 + RANDOM % 20000))
|
||||||
|
: > "$WORK/console.log"
|
||||||
|
bash "$QDIR/run.sh" --kernel "$ZIMAGE" --display headless --qmp "$WORK/qmp.sock" \
|
||||||
|
--ssh-port "$PORT" --http-port $((PORT + 1)) --api-port $((PORT + 2)) \
|
||||||
|
> "$WORK/console.log" 2>&1 &
|
||||||
|
QEMU_PID=$!
|
||||||
|
sleep 3
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null && break
|
||||||
|
if grep -aq 'Could not set up host forwarding' "$WORK/console.log"; then
|
||||||
|
echo "== hostfwd port collision on base $PORT — retrying"
|
||||||
|
QEMU_PID=""
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "FATAL: VM died at launch:" >&2
|
||||||
|
tail -20 "$WORK/console.log" >&2
|
||||||
|
exit 1
|
||||||
|
done
|
||||||
|
if [ -z "$QEMU_PID" ] || ! kill -0 "$QEMU_PID" 2>/dev/null; then
|
||||||
|
echo "FATAL: could not launch the VM after 3 port attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
deadline=$((SECONDS + 120))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
grep -aq 'init: starting warden-ui' "$WORK/console.log" && break
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null || { echo "FATAL: VM exited early" >&2; tail -25 "$WORK/console.log" >&2; exit 1; }
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
grep -aq 'init: starting warden-ui' "$WORK/console.log" || {
|
||||||
|
echo "FATAL: warden-ui never started (no fb0? wrong kernel?)" >&2
|
||||||
|
tail -25 "$WORK/console.log" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
qmp() { python3 "$HERE/qmp.py" "$WORK/qmp.sock" "$@"; }
|
||||||
|
|
||||||
|
# Frame is "real" once it has more than a handful of distinct colors (a blank
|
||||||
|
# or console-only frame has very few).
|
||||||
|
frame_rendered() { # $1 = ppm path
|
||||||
|
python3 - "$1" <<'EOF'
|
||||||
|
import sys
|
||||||
|
data = open(sys.argv[1], "rb").read()
|
||||||
|
parts = data.split(b"\n", 3) # P6 header: magic, dims, maxval, raw RGB
|
||||||
|
pixels = parts[3] if len(parts) == 4 else b""
|
||||||
|
distinct = len(set(pixels[i:i+3] for i in range(0, min(len(pixels), 3*720*720), 3)))
|
||||||
|
print(f"{sys.argv[1]}: {len(pixels)} bytes, {distinct} distinct colors")
|
||||||
|
sys.exit(0 if distinct > 16 else 1)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# The VM can die mid-poll (OOM, crash): check liveness before every QMP
|
||||||
|
# call so the failure is OUR message + console evidence, not a python
|
||||||
|
# traceback — and preserve the console log before the trap removes $WORK.
|
||||||
|
vm_alive_or_die() {
|
||||||
|
kill -0 "$QEMU_PID" 2>/dev/null && return 0
|
||||||
|
echo "FATAL: VM exited during the screendump poll" >&2
|
||||||
|
tail -25 "$WORK/console.log" >&2
|
||||||
|
mkdir -p "$OUTDIR"; cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Poll for the first rendered frame (bounded, no guessed sleep).
|
||||||
|
rendered=0
|
||||||
|
deadline=$((SECONDS + 90))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
vm_alive_or_die
|
||||||
|
qmp screendump "$WORK/shot1.ppm"
|
||||||
|
if frame_rendered "$WORK/shot1.ppm"; then rendered=1; break; fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
[ "$rendered" = 1 ] || {
|
||||||
|
echo "FATAL: UI never rendered a non-blank frame within 90s" >&2
|
||||||
|
mkdir -p "$OUTDIR"; cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tap the "Metrics" tab: pixel (373,40) of 720x720 scaled to the QMP absolute
|
||||||
|
# range 0..32767 — switching tabs must repaint the content area. Poll for the
|
||||||
|
# repaint rather than guessing a delay.
|
||||||
|
vm_alive_or_die
|
||||||
|
qmp tap 16975 1820
|
||||||
|
changed=0
|
||||||
|
# 90s, matching the first-frame budget: TCG repaints are CPU-bound and a
|
||||||
|
# contended CI runner can be arbitrarily slower than this dev box (same
|
||||||
|
# margin reasoning as the rs485 test-gap widening).
|
||||||
|
deadline=$((SECONDS + 90))
|
||||||
|
while [ $SECONDS -lt $deadline ]; do
|
||||||
|
sleep 2
|
||||||
|
vm_alive_or_die
|
||||||
|
qmp screendump "$WORK/shot2.ppm"
|
||||||
|
if ! cmp -s "$WORK/shot1.ppm" "$WORK/shot2.ppm"; then changed=1; break; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
cp "$WORK/shot1.ppm" "$OUTDIR/ui-shot1.ppm"
|
||||||
|
cp "$WORK/shot2.ppm" "$OUTDIR/ui-shot2.ppm" 2>/dev/null || true
|
||||||
|
|
||||||
|
[ "$changed" = 1 ] || {
|
||||||
|
echo "FATAL: tapping the Metrics tab did not change the frame within 90s — touch is not reaching the UI" >&2
|
||||||
|
cp "$WORK/console.log" "$OUTDIR/ui-shot-console.log" || true
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "tap on the Metrics tab repainted the frame (touch reached the UI)"
|
||||||
|
echo "UI-SHOT-PASS (screenshots in $OUTDIR/ui-shot{1,2}.ppm)"
|
||||||
+1
-1
@@ -3,7 +3,7 @@ name = "warden-sim"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Host-side hardware simulator for WardenOS: register/SRAM bus, HPMCU (RISC-V) watchdog, RGA, NPU. Lets driver and supervisor logic run and be tested with no panel."
|
description = "Host-side hardware simulator for WardenOS: register/SRAM bus, HPMCU (RISC-V) watchdog, RGA, NPU. Lets driver and supervisor logic run and be tested with no panel."
|
||||||
license = "MIT OR Apache-2.0"
|
license = "GPL-2.0-only"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "warden_sim"
|
name = "warden_sim"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name = "warden-config-lint"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Static target-config checks for WardenOS: catch memory-map faults (the 0x40000 MCU-load brick class) and other flash-time config mistakes before a flash, not on the bench."
|
description = "Static target-config checks for WardenOS: catch memory-map faults (the 0x40000 MCU-load brick class) and other flash-time config mistakes before a flash, not on the bench."
|
||||||
license = "MIT OR Apache-2.0"
|
license = "GPL-2.0-only"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "config-lint"
|
name = "config-lint"
|
||||||
|
|||||||
Reference in New Issue
Block a user