Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f95193190 | ||
|
|
15fd5eebdc | ||
|
|
b064acf57c | ||
|
|
4413491370 | ||
|
|
b265454b93 | ||
|
|
99d9851173 | ||
|
|
720cfd7c8a | ||
|
|
2c1222789d | ||
|
|
3620abff12 | ||
|
|
a937a5fd20 | ||
|
|
9d863c3d22 | ||
|
|
99cea559f9 | ||
|
|
18d98d9452 | ||
|
|
4eb991a101 | ||
|
|
1c435a0a79 | ||
|
|
3c8f7ed96e | ||
|
|
3b0b8d72df | ||
|
|
ce93244117 | ||
|
|
8a140c1906 | ||
|
|
d779961a55 | ||
|
|
f59fe9d7c2 | ||
|
|
a71634ae8a | ||
|
|
f1ff58afe7 | ||
|
|
4696804b23 | ||
|
|
88daeb8cf2 | ||
|
|
f8750a4a50 | ||
|
|
2c7a0194f4 | ||
|
|
0ef6b96fe1 | ||
|
|
2d5472372f | ||
|
|
efdf97ecc1 | ||
|
|
46d80ffc3a | ||
|
|
0b15c0ebc9 | ||
|
|
682a6f4538 | ||
|
|
a72174c571 | ||
|
|
fd323e483e | ||
|
|
9f0821cf69 | ||
|
|
69fc9efff7 | ||
|
|
20386cf313 | ||
|
|
c4fd6d9b36 | ||
|
|
e223d86ba2 | ||
|
|
52e5ebefbd | ||
|
|
ca44cd1a7b | ||
|
|
5fe0a6a48d | ||
|
|
da1ef6a17d | ||
|
|
b6c2fb5b7d | ||
|
|
b8b70b7df7 | ||
|
|
bb5d903724 | ||
|
|
1e2348abef | ||
|
|
2493185ab8 | ||
|
|
678f63f4f3 | ||
|
|
0aba0fc467 | ||
|
|
6f550c39ed | ||
|
|
e9d0cef610 | ||
|
|
bb22f1b185 | ||
|
|
7bc5fe1a53 | ||
|
|
51e1788c7e | ||
|
|
7b62297f3e | ||
|
|
890e521a3a | ||
|
|
444184f9ef | ||
|
|
7955af5bc0 | ||
|
|
273a2a8536 | ||
|
|
add0755958 | ||
|
|
de5f7edd16 | ||
|
|
69e0729c61 | ||
|
|
9baaf8434f | ||
|
|
27eefb6a7f | ||
|
|
c6f7e8ca0d | ||
|
|
474f1a904e | ||
|
|
ddaa54eba0 | ||
|
|
b3d31510ee | ||
|
|
bce512cb8d | ||
|
|
9950ecc340 | ||
|
|
e0c4207bd5 | ||
|
|
044a41d2de | ||
|
|
8d243a67f4 | ||
|
|
ee6717f035 | ||
|
|
f5a36666fc | ||
|
|
d78a221607 | ||
|
|
db81dce7ba | ||
|
|
3a8ec8139e | ||
|
|
98c1d4a1e9 | ||
|
|
c00deadaf7 | ||
|
|
7c8aeacea2 | ||
|
|
c3764a429f | ||
|
|
985e5d9449 | ||
|
|
b033fd309e | ||
|
|
76af08a9b0 | ||
|
|
951ffcc254 | ||
|
|
cbd84acf6c | ||
|
|
a1d7eb0bfd | ||
|
|
3741e7ba43 | ||
|
|
fabb0c8233 | ||
|
|
da2304985a | ||
|
|
8c6d29bf83 | ||
|
|
5cffe9e7db | ||
|
|
c51a1cf888 | ||
|
|
522374584c | ||
|
|
5755d9f11f | ||
|
|
3bf993275e | ||
|
|
05ea4feaed | ||
|
|
7af5477f04 | ||
|
|
84f5e19e9d | ||
|
|
1a50162598 | ||
|
|
0eb952a9c5 | ||
|
|
08f0f8859b | ||
|
|
8c63d7b558 | ||
|
|
e351d0fce1 | ||
|
|
ea6da967d2 | ||
|
|
f44f25d596 | ||
|
|
8ecac18978 | ||
|
|
e5f88b88fe | ||
|
|
ebb303cbb8 | ||
|
|
c80f618119 | ||
|
|
3b542f8a85 | ||
|
|
ef01c9e2be | ||
|
|
abeca0f0ed | ||
|
|
5a36be17d2 | ||
|
|
559528155f | ||
|
|
f161abf582 | ||
|
|
a4f80e63a2 | ||
|
|
43f5f99108 | ||
|
|
f8ab204711 | ||
|
|
0df5069831 | ||
|
|
f6a3a3579f | ||
|
|
d25fb8192e | ||
|
|
03a897938f | ||
|
|
15d48d4481 | ||
|
|
c764c8916b | ||
|
|
5efc7e2c2c | ||
|
|
39b4ae9ae1 | ||
|
|
4a472926e1 | ||
|
|
c4f9d95e11 | ||
|
|
bf21c8c7a9 | ||
|
|
629e1b9c6c | ||
|
|
78a1474057 | ||
|
|
751b7ae17b | ||
|
|
c4d7ef8086 | ||
|
|
bd7c506076 | ||
|
|
8e12606c6a | ||
|
|
8b857e41cd | ||
|
|
093e3bbeae | ||
|
|
4128661789 | ||
|
|
b6f3abd35e | ||
|
|
45796e4e3e | ||
|
|
3cd360f9e2 | ||
|
|
d31d9daba2 | ||
|
|
b25729acd6 | ||
|
|
8d2be98ed7 | ||
|
|
43f16e5638 | ||
|
|
6112a185df | ||
|
|
888222277e | ||
|
|
1938f0251f | ||
|
|
b90b592d4f | ||
|
|
e1fbbede61 | ||
|
|
aac4155584 |
No files matched your search
@@ -10,6 +10,11 @@
|
||||
"name": "ruview",
|
||||
"source": "./plugins/ruview",
|
||||
"description": "End-to-end RuView toolkit: getting started, ESP32 hardware setup, configuration, sensing applications (presence / vitals / pose / sleep / MAT), camera-free + camera-supervised model training, advanced multistatic sensing, CLI / API / WASM, mmWave radar, and witness verification"
|
||||
},
|
||||
{
|
||||
"name": "ruview-live",
|
||||
"source": "./harness/ruview/mod",
|
||||
"description": "Claude Code mod: /ruview opens a live pane with your CSI nodes (ESP32, Realtek) and ESPHome radar (presence, distance, device-reported vitals), refreshed on a timer, plus a one-line status. Read-only; ships inside @ruvnet/ruview. Mods are early access (CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -135,6 +135,10 @@ async function runProcess(command, args, {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
// A child that never reads stdin (e.g. `git check-ignore`) may exit and
|
||||
// close the pipe before this write flushes; the resulting EPIPE is not a
|
||||
// failure of the command, whose real outcome the 'close' handler reports.
|
||||
child.stdin.on('error', () => {});
|
||||
child.stdin.end(input);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
name: BFI Research Tools
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'tools/bfi/**'
|
||||
- 'tools/bfi_learning/**'
|
||||
- '.github/workflows/bfi-research.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tools/bfi/**'
|
||||
- 'tools/bfi_learning/**'
|
||||
- '.github/workflows/bfi-research.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: bfi-research-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
offline-tests:
|
||||
name: BFI offline tests (Linux, CPU)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- name: Install numeric test dependencies
|
||||
run: python -m pip install numpy==2.4.2 scipy==1.17.1
|
||||
- name: Install CPU PyTorch
|
||||
run: python -m pip install torch==2.11.0 --index-url https://download.pytorch.org/whl/cpu
|
||||
- name: Test capture parsing and bounded runners
|
||||
run: python -B -m unittest discover -s tools/bfi -v
|
||||
- name: Test offline learning and data admission
|
||||
run: python -B -m unittest discover -s tools/bfi_learning -v
|
||||
@@ -204,7 +204,10 @@ jobs:
|
||||
node-version: '22'
|
||||
|
||||
- name: Run UI unit tests
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs ui/services/sensing.service.test.mjs ui/components/DashboardTab.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
|
||||
|
||||
- name: Run virtual-view adapter boundary tests
|
||||
run: node --test integrations/virtual-view/adapter.test.mjs
|
||||
|
||||
# Unit and Integration Tests
|
||||
# Python pytest matrix — runs against the archived v1 Python tree.
|
||||
@@ -501,7 +504,7 @@ jobs:
|
||||
notify:
|
||||
name: Notify
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, test, rust-tests, performance-test, docker-build, docs]
|
||||
needs: [code-quality, test, rust-tests, ui-tests, performance-test, docker-build, docs]
|
||||
if: always()
|
||||
permissions:
|
||||
contents: write # required by softprops/action-gh-release
|
||||
@@ -528,18 +531,22 @@ jobs:
|
||||
text: '❌ CI pipeline failed for ${{ github.ref }}'
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'
|
||||
if: github.ref == 'refs/heads/main' && needs.rust-tests.result == 'success' && needs.ui-tests.result == 'success' && needs.docker-build.result == 'success'
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
|
||||
with:
|
||||
tag_name: v${{ github.run_number }}
|
||||
name: Release v${{ github.run_number }}
|
||||
name: CI snapshot v${{ github.run_number }}
|
||||
# CI snapshots must not replace a curated product release as Latest.
|
||||
make_latest: false
|
||||
body: |
|
||||
Automated release from CI pipeline
|
||||
Source snapshot validated by the CI pipeline. This entry does not contain firmware bundles.
|
||||
|
||||
**Changes:**
|
||||
${{ github.event.head_commit.message }}
|
||||
|
||||
**Docker Image:**
|
||||
`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}`
|
||||
**Source commit:** `${{ github.sha }}`
|
||||
|
||||
Container images publish separately through the [sensing-server release workflow](https://github.com/ruvnet/RuView/actions/workflows/sensing-server-docker.yml).
|
||||
Firmware downloads are attached to the dedicated `vX.Y.Z-esp32` releases.
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -16,6 +16,17 @@ on:
|
||||
- '.github/workflows/firmware-ci.yml'
|
||||
|
||||
jobs:
|
||||
provisioning-tests:
|
||||
name: Provisioning tooling tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Verify provisioning and secret handling without hardware
|
||||
run: python -m unittest discover -s firmware/esp32-csi-node/tests -p 'test_provision*.py' -v
|
||||
|
||||
version-guard:
|
||||
name: Verify version.txt matches release tag
|
||||
runs-on: ubuntu-latest
|
||||
@@ -119,6 +130,10 @@ jobs:
|
||||
fi
|
||||
echo "Binary size OK: $SIZE <= $MAX"
|
||||
|
||||
- name: Verify built application version before packaging
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: python test/test_delivery_contract.py --built-app build/esp32-csi-node.bin
|
||||
|
||||
- name: Verify flash image integrity
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
@@ -208,7 +223,7 @@ jobs:
|
||||
publish-release-assets:
|
||||
name: Publish firmware release assets
|
||||
if: github.ref_type == 'tag'
|
||||
needs: [version-guard, build, build-c6-idf6]
|
||||
needs: [version-guard, provisioning-tests, build, build-c6-idf6]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -232,7 +247,12 @@ jobs:
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
|
||||
with:
|
||||
name: ESP32 firmware ${{ github.ref_name }}
|
||||
prerelease: true
|
||||
make_latest: false
|
||||
body: |
|
||||
Exact-run ESP32-S3 8 MB, ESP32-S3 4 MB, and ESP32-C6 4 MB firmware bundles.
|
||||
Exact-run ESP32-S3 display 8 MB, ESP32-S3 headless 4 MB, and ESP32-C6 4 MB firmware bundles.
|
||||
Verify each archive against `SHA256SUMS.txt` before flashing.
|
||||
CI builds and host tests verify software behavior, not target hardware.
|
||||
This prerelease has no new physical-board qualification or sensing-accuracy claim.
|
||||
Select the matching board configuration using firmware/esp32-csi-node/README.md.
|
||||
files: release-assets/*
|
||||
@@ -41,8 +41,12 @@ jobs:
|
||||
build: false
|
||||
publishable: true
|
||||
# ADR-283/325: brain + local hosts + replay assets + guarded Spaces OAuth adapter;
|
||||
# still runtime-dependency-free. 160 KiB is the reviewed hard ceiling.
|
||||
unpacked_budget: 163840
|
||||
# still runtime-dependency-free. ADR-369..374 add the SDK, firmware
|
||||
# flashing, training runner/gate, doctor, device access, and SSH
|
||||
# hosts. ADR-375 adds the MCP Apps console widget (~12 KB), the
|
||||
# Streamable HTTP transport and the terminal UI (384 KiB); ADR-378 the
|
||||
# ruview-live showcase views (~37 KB): 448 KiB reviewed ceiling.
|
||||
unpacked_budget: 458752
|
||||
- dir: harness/homecore
|
||||
build: false
|
||||
publishable: true
|
||||
@@ -131,13 +135,19 @@ jobs:
|
||||
npm i --no-fund --no-audit "$TGZ"
|
||||
case "${{ matrix.package.dir }}" in
|
||||
harness/ruview)
|
||||
./node_modules/.bin/ruview --version
|
||||
./node_modules/.bin/ruview doctor
|
||||
./node_modules/.bin/ruview-harness --version
|
||||
./node_modules/.bin/ruview-harness doctor
|
||||
# the honesty gate must fail closed on empty input (ADR-263 F1)
|
||||
if ./node_modules/.bin/ruview claim-check; then
|
||||
if ./node_modules/.bin/ruview-harness claim-check; then
|
||||
echo 'claim-check passed with no input — fail-open regression'; exit 1
|
||||
fi
|
||||
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
|
||||
# ADR-369: the typed SDK resolves and the evidence gate refuses a baseline-free score
|
||||
node --input-type=module -e "const { createRuView } = await import('@ruvnet/ruview/sdk'); const r = createRuView(); if (r.tools().length < 22) process.exit(1); const g = await r.training.gate({ model_score: 0.93 }); if (g.verdict !== 'FAIL') process.exit(1);"
|
||||
./node_modules/.bin/ruview-harness doctor --json --group runtime,harness,firmware > doctor.json
|
||||
if ./node_modules/.bin/ruview-harness train-gate --model-score 0.93 > /dev/null; then
|
||||
echo 'train-gate passed without a baseline'; exit 1
|
||||
fi
|
||||
;;
|
||||
harness/homecore)
|
||||
./node_modules/.bin/homecore --version
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: README visual asset validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'README.md'
|
||||
- 'assets/readme/**'
|
||||
- 'docs/readme-visual-system.md'
|
||||
- 'scripts/*readme*'
|
||||
- 'scripts/tests/test_readme_visuals_xml.py'
|
||||
- '.github/workflows/readme-visuals.yml'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'README.md'
|
||||
- 'assets/readme/**'
|
||||
- 'docs/readme-visual-system.md'
|
||||
- 'scripts/*readme*'
|
||||
- 'scripts/tests/test_readme_visuals_xml.py'
|
||||
- '.github/workflows/readme-visuals.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Safe and reproducible README visuals
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install visual tooling dependency
|
||||
run: python -m pip install -r scripts/requirements-readme-visuals.txt
|
||||
- name: Test XML input boundaries
|
||||
run: python -m unittest discover -s scripts/tests -p test_readme_visuals_xml.py
|
||||
- name: Verify reproducible and accessible assets
|
||||
run: |
|
||||
python scripts/render-readme-visuals.py
|
||||
python scripts/verify-readme-visuals.py
|
||||
git diff --exit-code -- assets/readme
|
||||
test -z "$(git ls-files --others --exclude-standard -- assets/readme)"
|
||||
@@ -0,0 +1,68 @@
|
||||
# ADR-373/374 — device-access end-to-end through the real @ruvnet/ruview CLI.
|
||||
# MR60BHA2 (60 GHz) and RPLIDAR are emulated on pseudo-terminals and read via
|
||||
# real pyserial; an ESP32 node is emulated over UDP; the iPhone LiDAR path
|
||||
# uses the repository's real relay. All results are SYNTHETIC: this proves
|
||||
# the transports and parsers, not hardware behaviour.
|
||||
|
||||
name: ruview device e2e
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'harness/ruview/src/**'
|
||||
- 'harness/ruview/bin/**'
|
||||
- 'harness/ruview/test/e2e/**'
|
||||
- 'integrations/iphone-lidar/web/**'
|
||||
- '.github/workflows/ruview-device-e2e.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'harness/ruview/src/**'
|
||||
- 'harness/ruview/bin/**'
|
||||
- 'harness/ruview/test/e2e/**'
|
||||
- 'integrations/iphone-lidar/web/**'
|
||||
- '.github/workflows/ruview-device-e2e.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ruview-device-e2e-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
devices:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install pyserial
|
||||
run: python -m pip install pyserial==3.5
|
||||
|
||||
- name: Install the iPhone LiDAR relay
|
||||
working-directory: integrations/iphone-lidar/web
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Install harness
|
||||
working-directory: harness/ruview
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
# Missing prerequisites fail instead of skipping (RUVIEW_E2E_REQUIRE).
|
||||
# The suite links each emulator pty to /dev/ttyRUVIEWE2E<n> with
|
||||
# `sudo -n`, because the harness refuses /dev/pts/* port names.
|
||||
- name: Device e2e (SYNTHETIC devices)
|
||||
working-directory: harness/ruview
|
||||
env:
|
||||
RUVIEW_E2E_REQUIRE: '1'
|
||||
run: npm run test:e2e:devices
|
||||
@@ -4,6 +4,12 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'harness/ruview/**'
|
||||
- 'scripts/csi-udp-relay.py'
|
||||
- 'scripts/esp32_jsonl_to_rvcsi.py'
|
||||
- 'scripts/overnight-empty-capture.py'
|
||||
- 'scripts/record-csi-udp.py'
|
||||
- 'scripts/collect-training-data.py'
|
||||
- 'scripts/align-ground-truth.js'
|
||||
- '.github/scripts/nightly-sota/**'
|
||||
- '.github/workflows/nightly-sota-agent.yml'
|
||||
- '.github/workflows/ruview-harness-flywheel.yml'
|
||||
@@ -25,8 +31,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify contributor harness
|
||||
runs-on: ubuntu-latest
|
||||
name: Verify contributor harness (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: harness/ruview
|
||||
@@ -39,9 +49,13 @@ jobs:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: harness/ruview/package-lock.json
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: npm ci --ignore-scripts
|
||||
- run: npm audit --omit=optional
|
||||
- run: npm test
|
||||
- run: npm run test:security
|
||||
- run: npm run brain:verify
|
||||
- run: npm run flywheel:plan
|
||||
- run: npm run flywheel:verify
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# ADR-368 — @ruvnet/ruview-kernel: build the WASM and napi-rs transports from
|
||||
# source on every OS, run the Rust ABI tests, the package unit/security/e2e
|
||||
# tests with the native addon REQUIRED, the wasm↔napi parity check, artifact
|
||||
# verification, and the cross-package e2e through the @ruvnet/ruview
|
||||
# metaharness. No publish step: publication stays CI-only via the release
|
||||
# workflow and requires explicit maintainer approval (ADR-265).
|
||||
|
||||
name: ruview-kernel npm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'v2/crates/ruview-kernel/**'
|
||||
- 'v2/crates/ruview-kernel-napi/**'
|
||||
- 'v2/crates/wifi-densepose-vitals/**'
|
||||
- 'harness/ruview-kernel/**'
|
||||
- 'harness/ruview/src/kernel.js'
|
||||
- 'harness/ruview/test/kernel.test.mjs'
|
||||
- '.github/workflows/ruview-kernel-npm.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'v2/crates/ruview-kernel/**'
|
||||
- 'v2/crates/ruview-kernel-napi/**'
|
||||
- 'v2/crates/wifi-densepose-vitals/**'
|
||||
- 'harness/ruview-kernel/**'
|
||||
- 'harness/ruview/src/kernel.js'
|
||||
- 'harness/ruview/test/kernel.test.mjs'
|
||||
- '.github/workflows/ruview-kernel-npm.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ruview-kernel-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
kernel:
|
||||
name: kernel (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# The v2 workspace lists submodule crates as members; cargo must be
|
||||
# able to load every member manifest.
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust + wasm32 target
|
||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
# v2/rust-toolchain pins its own channel; the target must be installed
|
||||
# for that toolchain, not only for the action's default `stable`.
|
||||
- name: Add wasm32 target to the pinned v2 toolchain
|
||||
working-directory: v2
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
||||
with:
|
||||
workspaces: |
|
||||
v2 -> target
|
||||
v2/crates/ruview-kernel-napi -> target
|
||||
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Rust ABI tests
|
||||
run: cargo test --manifest-path v2/Cargo.toml -p ruview-kernel --locked
|
||||
|
||||
- name: Build WASM + napi-rs artifacts from source
|
||||
working-directory: harness/ruview-kernel
|
||||
run: npm run build
|
||||
|
||||
- name: Verify artifacts (checksums, zero WASM imports, ABI)
|
||||
working-directory: harness/ruview-kernel
|
||||
run: node scripts/verify-artifacts.mjs --require-wasm
|
||||
|
||||
- name: Package tests (napi required)
|
||||
working-directory: harness/ruview-kernel
|
||||
env:
|
||||
RUVIEW_KERNEL_REQUIRE_NAPI: '1'
|
||||
run: npm test
|
||||
|
||||
- name: Doctor, self-test, and wasm↔napi parity (SYNTHETIC)
|
||||
working-directory: harness/ruview-kernel
|
||||
run: |
|
||||
node bin/cli.js doctor --strict-napi
|
||||
node bin/cli.js selftest --backend wasm
|
||||
node bin/cli.js selftest --backend napi
|
||||
node bin/cli.js parity
|
||||
|
||||
- name: Pack
|
||||
working-directory: harness/ruview-kernel
|
||||
run: |
|
||||
npm pack --dry-run
|
||||
# Relative destination: Windows runner temp paths contain
|
||||
# backslashes that bash globbing would mangle.
|
||||
mkdir -p dist-pack
|
||||
npm pack --pack-destination dist-pack
|
||||
|
||||
- name: Cross-package e2e through the @ruvnet/ruview metaharness
|
||||
working-directory: harness/ruview
|
||||
run: |
|
||||
npm ci --ignore-scripts
|
||||
npm install --no-save --ignore-scripts ../ruview-kernel/dist-pack/ruvnet-ruview-kernel-*.tgz
|
||||
node --test test/kernel.test.mjs
|
||||
node bin/cli.js kernel --backend auto
|
||||
|
||||
- name: Upload built artifacts
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ruview-kernel-linux-x64
|
||||
path: |
|
||||
harness/ruview-kernel/wasm/ruview_kernel.wasm
|
||||
harness/ruview-kernel/wasm/SHA256SUMS
|
||||
harness/ruview-kernel/native/*.node
|
||||
harness/ruview-kernel/native/SHA256SUMS
|
||||
retention-days: 14
|
||||
@@ -104,8 +104,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ inputs.package }}" in
|
||||
# ADR-283/325: brain + hosts + replay + guarded Spaces OAuth; no runtime deps.
|
||||
harness/ruview) export UNPACKED_BUDGET=163840 ;;
|
||||
# ADR-283/325/369-374: brain + hosts + replay + Spaces OAuth + SDK,
|
||||
# flashing, training, doctor, device access, SSH hosts; ADR-375 console
|
||||
# widget, HTTP transport, terminal UI; ADR-378 mod showcase; no runtime deps.
|
||||
harness/ruview) export UNPACKED_BUDGET=458752 ;;
|
||||
# ADR-285: CLI + MCP + reviewed brain + WASM-kernel adapter.
|
||||
harness/homecore) export UNPACKED_BUDGET=180000 ;;
|
||||
# ADR-264 O2: map-free tarball (was 188 kB with maps).
|
||||
@@ -140,12 +142,12 @@ jobs:
|
||||
npm i --no-fund --no-audit "$TGZ"
|
||||
case "${{ inputs.package }}" in
|
||||
harness/ruview)
|
||||
./node_modules/.bin/ruview --version
|
||||
./node_modules/.bin/ruview doctor
|
||||
./node_modules/.bin/ruview guidance --topic homecore --query restore --limit 1 \
|
||||
./node_modules/.bin/ruview-harness --version
|
||||
./node_modules/.bin/ruview-harness doctor
|
||||
./node_modules/.bin/ruview-harness guidance --topic homecore --query restore --limit 1 \
|
||||
| grep -q '"homecore-runtime-restore"'
|
||||
# the honesty gate must fail closed on empty input (ADR-263 F1)
|
||||
if ./node_modules/.bin/ruview claim-check; then
|
||||
if ./node_modules/.bin/ruview-harness claim-check; then
|
||||
echo 'claim-check passed with no input — fail-open regression'; exit 1
|
||||
fi
|
||||
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
|
||||
|
||||
@@ -322,3 +322,6 @@ v2/crates/ruview-offaxis/pkg/
|
||||
scripts/fal-visual-teacher/training-data/
|
||||
scripts/fal-visual-teacher/training-data.zip
|
||||
scripts/fal-visual-teacher/output/
|
||||
|
||||
# Trained adaptive classifier (auto-created at runtime; #2135)
|
||||
/v2/data/adaptive_model.json
|
||||
@@ -7,8 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Community batch (PRs #2100-#2157 by aepod, reviewed and integrated)
|
||||
|
||||
Integrated as one set: conflicts resolved, review findings fixed, validated on the integrated branch. Hardware figures cited by the PRs are the author's own live runs on ESP32-S3 nodes and were not independently reproduced here.
|
||||
|
||||
- **Security and honest status.** Refuse a routable HTTP bind when API auth is off (#2134). `--privacy-mode` now strips biometrics from REST, WebSocket and recordings, not only MQTT (#2125; the stripped-key list is an exact-match allowlist, so a new biometric field must be added to it). `/api/v1/status` waits for a real frame and unknown `--source` values are rejected (#2126; breaking for `--source macos|linux|windows`). A stale `/sensing/latest` reports `{"status":"stale"}` after 10 s (#2119). Measured stream fps and real WebSocket client counts (#2127). Absent nodes no longer count as one person (#2132). ESP32 frames with zero antennas or subcarriers are dropped (#2118). Dead nodes are evicted on a timer (#2120). Recordings and models stay under `--data-dir` (#2129). Boolean env flags accept 1/0/yes/no/on/off (#2108). Docker UDP ingest posture is explicit (#2107).
|
||||
- **CSI pipeline.** The grid gate locks on the majority frame shape and `/health` reports a stalled pipeline (#2157; a partial fix, mixed 192/306 frame shapes still drop frames, so it does not close #1894). Phase is combined across nodes only when they declare a shared reference (#2156). Point-cloud and hardware parsers use the ADR-018 layout and accept more than 256 subcarriers (#2155; the bandwidth rule for counts above 64 is CLAIMED, inferred from the datasheet). Per-node adaptive training from real recordings (#2101, #2103); a run that fell back to the live buffer now reports `degraded: true`. Gaussian decay no longer compounds across calls (#2140).
|
||||
- **MediaTek (ADR-266/267).** Legacy and unknown PPDU types in MTC1 (#2112), per-device ingest capped at 64 devices (#2113), a labelled amplitude heuristic and activity index (#2114; CLAIMED, not an occupancy estimate), an MT7981 bridge (#2115), and the OpenWrt WN586X3 patch set and guide (#2116; the vendored CSI patch is BSD-3-Clause-Clear, see `NOTICE`).
|
||||
- **MQTT / Home Assistant.** Semantic states wired into the publisher, entities announced only when sourced, persisted client id (#2117). Existing Home Assistant installs keep retained discovery configs for the six removed entities until they are cleared manually.
|
||||
- **Provisioning and firmware.** Wi-Fi password kept owner-only and out of argv and `--state` (#2153); state keyed by board MAC (#2152); `--dry-run` and a failed `--reset` leave state intact (#2111); OTA images confirmed by a bounded health check, which means only "reached the network and sent CSI" (#2100, ADR-379).
|
||||
- **Ground truth and spatial.** Radar window adapter and auto-labeller with a session-disjoint split and a median count baseline; radar labels carry `evidence: CLAIMED` (#2143, #2144). Node self-survey, check-only (#2145, ADR-381). The `spatial.evidence.v1` export defaults to the CODE proof grade and MEASURED requires a reproducer (#2141, ADR-382). Loopback UDP tee (#2102, ADR-380).
|
||||
- **Tooling.** `prove.sh` reports INCOMPLETE instead of PASS when a gate is skipped (#2130); ADR-018 capture scripts (#2131); API token on the ground-truth script (#2104); the UI asks for the API token instead of retrying forever (#2128); macOS redacted-link handling (#2105).
|
||||
- **Not included:** #2106 (MacWifi.app, draft) and #2142 (MR60 probe change, needs a captured MR60BHA2 boot log). `v2/data/adaptive_model.json` (41.5% training accuracy) is no longer tracked (#2135).
|
||||
|
||||
### Added
|
||||
|
||||
- **`@ruvnet/ruview` 0.9.1: live sensing views inside Claude Code (ADR-377, ADR-378).** The package now ships `ruview-live`, a Claude Code mod. Type `/ruview` and a pane opens next to the conversation showing what your sensors see right now, refreshed automatically. It has three views, switched with keys `1` `2` `3` or with `/ruview overview|waterfall|radar`:
|
||||
- **Overview**: the CSI nodes streaming to your machine (rate, packet loss, signal strength) and an optional 60 GHz radar kit (presence, distance, heart and breathing rates).
|
||||
- **CSI waterfall**: the Wi-Fi signal itself, the strength of every subcarrier over time, drawn in colour and scrolling as frames arrive. The new `ruview esp32 --spectrum` option supplies the data.
|
||||
- **Radar**: a top-down range fan with an animated ping out to the measured distance, plus heart, breathing and distance charts.
|
||||
|
||||
Every reading is labelled honestly: MEASURED or SYNTHETIC from the packets themselves; radar vitals as "device-reported, not validated"; range-only radar as "distance, not direction". The pane only reads; it never flashes or configures a device.
|
||||
|
||||
Verified on real hardware: an ESP32-C6 streaming CSI and a Seeed MR60BHA2 radar kit (MEASURED; reproducer `npx @ruvnet/ruview esp32 --seconds 4 --spectrum --json`). The package's size ceiling rises from 384 KiB to 448 KiB; it still has no runtime dependencies.
|
||||
- **`wifi-densepose-sar` — coherent wideband RF tomography research crate (ADR-287).** New standalone leaf crate (the `nvsim` pattern; zero coupling to `wifi-densepose-hardware` or any real ingestion path) implementing the synthetic-aperture-radar reconstruction primitive a handheld through-wall RF imaging device would need — motivated by comparison against Applied Electrodynamics' "WaveSight" launch, and explicitly scoped below ADR-278's RISE/DiffRadar/GeRaF reproduction gates. Ships: (1) a stepped-frequency, multi-position complex forward measurement simulator (`y_{m,k} = Σ σ_j/R² · exp(-i·4π·f·R/c) + noise`, deterministic ChaCha20 seeding); (2) delay-and-sum backprojection reconstruction onto a 3D voxel grid, rayon-parallelized over voxels; (3) threshold + local-maximum point-cloud extraction; (4) closed-form range/cross-range resolution and antenna-pose coherence-budget formulas (`ΔR=c/2B`, `δ_CR≈λR/2L`, `Δp≤λ/8`) checked against the reconstruction's *actual* behavior in `tests/physics_validation.rs` rather than merely documented — forward-simulating two targets at controlled separations and proving they resolve or merge exactly where the formulas predict, and that reconstructed focus at a known target degrades as injected antenna-pose error grows. Every number is SYNTHETIC/L0 (ADR-282) — no real wideband RF hardware backs this crate; see the crate README and `docs/tutorials/coherent-rf-tomography-backprojection.md` for the full honesty boundary and a worked walkthrough. `focus_at_point` exploits the evenly-spaced-by-construction frequency sweep (an arithmetic progression in per-term phase) to evaluate each pose's phasor once and advance it by a fixed complex-multiply step per frequency instead of one `sin`/`cos` pair per frequency — **MEASURED ~4.4-4.5x faster** (criterion regression detection, p < 0.001) than the first-shipped direct-computation version, proven equivalent (not just faster) to an independently reimplemented reference across four sweep sizes and on-/off-target points. 25 tests (22 unit + 3 integration), 0 failed, clippy-clean; MEASURED backprojection throughput ~1.7-2.3M voxels/sec (criterion, 21 poses × 32 freq steps).
|
||||
- **HOMECORE platform runtime completion — secure native/Wasmtime plugins, authenticated HAP IP, expanded Home Assistant APIs, durable restoration/migration, and voice protocols.** `homecore-server` now owns deterministic compiled-in native plugin registration plus explicitly configured, path-bounded, Ed25519 publisher-verified Wasm packages executed through Wasmtime with setup/state-change/teardown lifecycle; arbitrary native dynamic libraries remain intentionally unsupported. The optional HAP server implements persisted accessory identity and controller records, SRP-6a Pair-Setup M1–M6, X25519/Ed25519 Pair-Verify M1–M4, HKDF-SHA512/ChaCha20-Poly1305 record framing, authenticated/admin endpoint gates, replay/tamper closure, live entity synchronization, and paired-state `_hap._tcp` mDNS updates (45 focused tests; external Apple certification is not claimed). Startup restores device/entity registries and deterministic latest recorder states before plugins, and migration now atomically preserves forward-compatible device/config-entry fields. The HA-compatible surface adds events, templates, config checks, components, registries, history/logbook with SQL-enforced global response bounds, calendar/camera provider routes, and modern WebSocket negotiation while retaining a machine-readable limitations matrix for integration-specific behavior. Assist adds bounded PCM16, async STT/TTS contracts, an end-to-end speech pipeline, and an authenticated satellite session protocol; real deployments still provide the speech engines.
|
||||
- **`ruview-unified` increment 3 — Gaussian update-loop completion, separable delay-Doppler, and property-tested boundary hardening.** (1) `GaussianMap::merge_overlapping` (ADR-275 step 5: mutual-Mahalanobis + semantic-compatibility dedup catching drift the insert-time gate misses) and lifetime-aware decay (`τ_eff = τ·(1+ln(1+lifetime/τ))` — confirmed structures outlive transients at equal nominal τ). (2) `delay_doppler_map` reimplemented separably (`O(B²S+S²B)`), proven equivalent to the direct reference to <1e-10 and **measured 8.3× faster** (520 µs vs 4.34 ms at 56×8). (3) `tests/security_boundaries.rs` — 8 `proptest` properties over the boundary surfaces (arbitrary values incl. NaN/±inf via `f64::from_bits`) that found and fixed three input-controlled defects: a BLE-CS phase-unwrap infinite loop on non-finite phases and an ~1e299-iteration loop on finite-huge phases (now O(1) modular unwrap + plausibility bound), and a subnormal Gaussian scale overflowing `1/σ²` to NaN density (now physical σ/occupancy bounds). (4) New criterion benches for all increment-2 hot paths (`to_canonical` 38 µs, `ble_cs_range` 481 ns, AoI planner 647 ns/200 regions, coherent fusion 1.5 µs/32 members, factorized pose 521 ns). ruview-unified now 98 tests (87 lib + 3 acceptance + 8 security), 0 failed, clippy-clean.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
RuView third-party notices
|
||||
|
||||
This repository is licensed MIT OR Apache-2.0 (see LICENSE). The items below
|
||||
are vendored third-party material that keeps its own licence and is not
|
||||
covered by that dual licence.
|
||||
|
||||
firmware/openwrt-wn586x3/patches/1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch
|
||||
What: MediaTek's mt76 mt7915 CSI patch, rebased onto openwrt/mt76
|
||||
eb567bc7 (the revision pinned by OpenWrt 24.10.8).
|
||||
Author: Bo Jiao <Bo.Jiao@mediatek.com> (original author line kept in the
|
||||
patch header); copyright MediaTek Inc.
|
||||
Origin: mediatek/mtk-openwrt-feeds @ a15454c888f4f4144c50e33b5feef4f247c5f78b
|
||||
Licence: BSD-3-Clause-Clear (SPDX identifier,
|
||||
https://spdx.org/licenses/BSD-3-Clause-Clear.html). The patch
|
||||
carries the SPDX tags and MediaTek copyright lines in its added
|
||||
files; the licence text is not reproduced here.
|
||||
Scope: A vendored patch applied to an OpenWrt/mt76 build. It is not part
|
||||
of the Rust crates, is not compiled by the Cargo workspace, and is
|
||||
not linked into any RuView binary.
|
||||
|
||||
See firmware/openwrt-wn586x3/README.md for the other patches in that
|
||||
directory and the licence each one keeps.
|
||||
@@ -1,11 +1,37 @@
|
||||
# π RuView
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="33%"><a href="docs/user-guide.md"><img src="assets/readme/nav-start.svg" width="180" alt="Start here" /></a><br/><a href="docs/user-guide.md">Start here</a></td>
|
||||
<td align="center" width="33%"><a href="firmware/esp32-csi-node/README.md"><img src="assets/readme/nav-hardware.svg" width="180" alt="Hardware" /></a><br/><a href="firmware/esp32-csi-node/README.md">Hardware</a></td>
|
||||
<td align="center" width="33%"><a href="https://huggingface.co/ruvnet/wifi-densepose-pretrained"><img src="assets/readme/nav-models.svg" width="180" alt="Models" /></a><br/><a href="https://huggingface.co/ruvnet/wifi-densepose-pretrained">Models</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="33%"><a href="harness/ruview/README.md"><img src="assets/readme/nav-agents.svg" width="180" alt="AI toolkit" /></a><br/><a href="harness/ruview/README.md">AI toolkit</a></td>
|
||||
<td align="center" width="33%"><a href="docs/integrations/home-assistant.md"><img src="assets/readme/nav-home.svg" width="180" alt="Smart home" /></a><br/><a href="docs/integrations/home-assistant.md">Smart home</a></td>
|
||||
<td align="center" width="33%"><a href="https://cognitum.one/seed"><img src="assets/readme/nav-seed.svg" width="180" alt="Cognitum Seed" /></a><br/><a href="https://cognitum.one/seed">Cognitum Seed</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<details>
|
||||
<summary>Watch the original RuView sensing demo</summary>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cognitum.one/seed">
|
||||
<img src="assets/ruview-hero-h3-v3.gif" alt="RuView - WiFi DensePose — animated visualization of real-time pose estimation, breathing, and heart-rate sensing through WiFi" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</details>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://ruos.cognitum.one">
|
||||
<img src="assets/ruos-animated.svg" alt="RuView — WiFi becomes spatial awareness, with Ruflo coordination and a ruOS sensing workspace" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## **See through walls with WiFi** ##
|
||||
|
||||
@@ -21,6 +47,8 @@ Works natively with the four major smart-home ecosystems: **[Home Assistant](doc
|
||||
|
||||
Every WiFi router already fills your space with radio waves. When people move, breathe, or even sit still, they disturb those waves in measurable ways. RuView captures these disturbances using Channel State Information (CSI) from low-cost ESP32 sensors and turns them into actionable data: who's there, what they're doing, and whether they're okay.
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
**What it senses:**
|
||||
- **Presence and occupancy** — detect people through walls, count them, track entries and exits
|
||||
- **Vital signs** — breathing rate and heart rate, contactless, while sleeping or sitting
|
||||
@@ -37,32 +65,75 @@ Every WiFi router already fills your space with radio waves. When people move, b
|
||||
- **Governed evidence** — attach privacy policy, uncertainty, provenance, and witness records to sensing events
|
||||
- **RuView MetaHarness** — use an AI operator to onboard, calibrate, train, verify, and check sensing claims
|
||||
|
||||
<!-- ruview-capability-cards -->
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><a href="docs/user-guide.md"><img src="assets/readme/feature-presence.svg" width="580" alt="Presence and motion: open documentation and current limitations" /></a><br/><a href="docs/user-guide.md">Presence and motion</a></td>
|
||||
<td width="50%"><a href="v2/crates/wifi-densepose-signal/README.md"><img src="assets/readme/feature-vitals.svg" width="580" alt="Breathing and vital signals: open documentation and current limitations" /></a><br/><a href="v2/crates/wifi-densepose-signal/README.md">Breathing and vital signals</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><a href="docs/benchmarks/pose-estimation-cog.md"><img src="assets/readme/feature-pose.svg" width="580" alt="Pose research and limitations: open documentation and current limitations" /></a><br/><a href="docs/benchmarks/pose-estimation-cog.md">Pose research and limitations</a></td>
|
||||
<td width="50%"><a href="firmware/esp32-csi-node/README.md"><img src="assets/readme/feature-edge.svg" width="580" alt="Edge sensor mesh: open documentation and current limitations" /></a><br/><a href="firmware/esp32-csi-node/README.md">Edge sensor mesh</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><a href="docs/integrations/home-assistant.md"><img src="assets/readme/feature-automation.svg" width="580" alt="Local automation: open documentation and current limitations" /></a><br/><a href="docs/integrations/home-assistant.md">Local automation</a></td>
|
||||
<td width="50%"><a href="harness/ruview/README.md"><img src="assets/readme/feature-agents.svg" width="580" alt="Guided AI operator: open documentation and current limitations" /></a><br/><a href="harness/ruview/README.md">Guided AI operator</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<details>
|
||||
<summary><strong>RuView MetaHarness</strong> — guided operation for humans and AI agents</summary>
|
||||
|
||||
The RuView-specific metaharness we created is published as [`@ruvnet/ruview`](harness/ruview/README.md). It provides source-cited guidance, guarded Claude Code/Codex agents, deterministic verification, an honesty check for accuracy claims, and an explicitly granted OAuth-only Cognitum Spaces read.
|
||||
The RuView-specific metaharness we created is published as [`@ruvnet/ruview`](harness/ruview/README.md). It provides:
|
||||
- source-cited guidance;
|
||||
- device access for CSI nodes, radar and LiDAR;
|
||||
- firmware flashing with boot evidence;
|
||||
- one MCP server, usable over stdio or HTTP for ChatGPT, with a live console widget;
|
||||
- a Claude Code mod;
|
||||
- guarded Claude Code/Codex agents;
|
||||
- deterministic verification;
|
||||
- an honesty check for accuracy claims.
|
||||
|
||||
Companion packages: [`homecore`](harness/homecore/README.md) (Homecore developer metaharness) and [`@ruvnet/ruview-kernel`](harness/ruview-kernel/README.md) (the vitals pipeline as WASM).
|
||||
|
||||
```bash
|
||||
# Check the local setup and get source-cited guidance
|
||||
npx @ruvnet/ruview@0.4.0 doctor
|
||||
npx @ruvnet/ruview@0.4.0 guidance --topic sensing --query "model loading"
|
||||
npx @ruvnet/ruview@0.9.1 doctor
|
||||
npx @ruvnet/ruview@0.9.1 guidance --topic sensing --query "model loading"
|
||||
|
||||
# Hardware: what is plugged in, live CSI from your nodes, a 60 GHz radar kit
|
||||
npx @ruvnet/ruview@0.9.1 devices
|
||||
npx @ruvnet/ruview@0.9.1 esp32 --watch # ESP32 + Realtek RAC1 nodes, live view
|
||||
npx @ruvnet/ruview@0.9.1 esp32 --seconds 45 --analyze # live CSI through the vitals kernel
|
||||
npx @ruvnet/ruview@0.9.1 mmwave --source esphome --host <kit-ip>
|
||||
|
||||
# Agents: MCP over stdio, or over HTTP for ChatGPT (token-protected, read tools only)
|
||||
npx @ruvnet/ruview@0.9.1 mcp start
|
||||
RUVIEW_MCP_GRANTS=device-access npx @ruvnet/ruview@0.9.1 mcp start --http
|
||||
|
||||
# Claude Code: a live sensing pane (/ruview), shipped in the package
|
||||
npx @ruvnet/ruview@0.9.1 mod
|
||||
|
||||
# Run a read-only RuView agent through Codex
|
||||
npx @ruvnet/ruview@0.4.0 agent run --host codex --repo . \
|
||||
npx @ruvnet/ruview@0.9.1 agent run --host codex --repo . \
|
||||
--prompt "Find the nearest tests and cite the source files"
|
||||
|
||||
# Search or verify the reviewed contributor brain
|
||||
npx @ruvnet/ruview@0.4.0 brain search --query "calibration"
|
||||
npx @ruvnet/ruview@0.4.0 brain verify --repo .
|
||||
|
||||
# Check claims, replay the deterministic proof, or expose the MCP server
|
||||
npx @ruvnet/ruview@0.4.0 claim-check --file REPORT.md
|
||||
npx @ruvnet/ruview@0.4.0 verify
|
||||
npx @ruvnet/ruview@0.4.0 spaces
|
||||
npx @ruvnet/ruview@0.4.0 mcp start
|
||||
# Check claims, replay the deterministic proof, search the reviewed brain
|
||||
npx @ruvnet/ruview@0.9.1 claim-check --file REPORT.md
|
||||
npx @ruvnet/ruview@0.9.1 verify
|
||||
npx @ruvnet/ruview@0.9.1 brain search --query "calibration"
|
||||
```
|
||||
|
||||
Agent runs are read-only by default. Workspace writes require both `--allow-write` and `--confirm`; retrieved brain content is evidence, not authority.
|
||||
**Safety:**
|
||||
- Agent runs are read-only by default. Workspace writes require both `--allow-write` and `--confirm`.
|
||||
- Hardware reads need the `device-access` grant.
|
||||
- Flashing and calibration never run over the HTTP transport.
|
||||
- Device-reported vitals are labelled as unvalidated.
|
||||
- Retrieved brain content is evidence, not authority.
|
||||
|
||||
A single `npx ruview` package bundling all of this is ready (ADR-376). It is waiting on npm to release the unscoped name.
|
||||
|
||||
Full walkthrough: [user guide → RuView npm toolkit](docs/user-guide.md#ruview-npm-toolkit-ruvnetruview).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -100,8 +171,8 @@ RuView turns ordinary WiFi into a contactless sensor. A $9 ESP32 board reads the
|
||||
> | 🧱 **Through-wall sensing** | Fresnel-zone geometry + multipath modeling | Up to ~5 m, signal-dependent |
|
||||
> | 🧠 **Edge intelligence** | **105-cog catalog** ([ADR-102](docs/adr/ADR-102-edge-module-registry.md)) live from `app-registry.json` — health, security, building, retail, industrial, research, AI, swarm, signal, network, and developer modules. Optional Cognitum Seed adds persistent vector store + kNN + witness chain | $140 total BOM |
|
||||
> | 🎯 **Camera-free pre-training** | Self-supervised contrastive encoder, 12.2M training steps on 60K frames, shipped on Hugging Face | 84 s/epoch retrain on M4 Pro |
|
||||
> | 📷 **Camera-supervised fine-tune** | MediaPipe + ESP32 CSI paired training, end-to-end Candle pipeline on RTX 5080 ([ADR-079](docs/adr/ADR-079-camera-supervised-pose-finetune.md)) | 2.1 s for 400 epochs (~5 ms/epoch) |
|
||||
> | 📡 **Multi-frequency mesh** | Channel hopping across 6 bands, TDM slot scheduling ([ADR-029](docs/adr/ADR-029-multifrequency-mesh.md)) | 3× sensing bandwidth |
|
||||
> | 📷 **Camera-supervised fine-tune** | MediaPipe + ESP32 CSI paired training, end-to-end Candle pipeline on RTX 5080 ([ADR-079](docs/adr/ADR-079-camera-ground-truth-training.md)) | 2.1 s for 400 epochs (~5 ms/epoch) |
|
||||
> | 📡 **Multi-frequency mesh** | Channel hopping across 6 bands, TDM slot scheduling ([ADR-029](docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md)) | 3× sensing bandwidth |
|
||||
> | 🌐 **3D point cloud fusion** | Camera depth (MiDaS) + WiFi CSI + mmWave radar → unified spatial model | 22 ms pipeline · 19K+ points/frame |
|
||||
>
|
||||
> Browse the full 105-module catalog (with practical descriptions, sizes, and difficulty) below in [🧩 Edge Module Catalog](#-edge-module-catalog), or visit [seed.cognitum.one/store](https://seed.cognitum.one/store).
|
||||
@@ -114,8 +185,11 @@ RuView turns ordinary WiFi into a contactless sensor. A $9 ESP32 board reads the
|
||||
```bash
|
||||
# Option 1: Docker (simulated data, no hardware needed)
|
||||
docker pull ruvnet/wifi-densepose:latest
|
||||
docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
||||
export RUVIEW_API_TOKEN=$(openssl rand -hex 32) # required; the container exits 64 without it
|
||||
docker run -p 127.0.0.1:3000:3000 -e RUVIEW_API_TOKEN ruvnet/wifi-densepose:latest
|
||||
# Open http://localhost:3000
|
||||
# ESP32 nodes in Docker also need a UDP source guard (RUVIEW_UDP_ALLOW);
|
||||
# see docs/user-guide.md "Receiving ESP32 frames in Docker".
|
||||
|
||||
# Option 2a: Live sensing with ESP32-S3 hardware ($9)
|
||||
# Flash firmware, provision WiFi, and start sensing:
|
||||
@@ -227,6 +301,8 @@ The separate **17-keypoint pose-estimation model** is now published at [`ruvnet/
|
||||
|
||||
### Results & proof
|
||||
|
||||
[](docs/benchmarks/pose-estimation-cog.md)
|
||||
|
||||
See the measured benchmarks, witness records, and one-command reproducibility check.
|
||||
|
||||
<details>
|
||||
@@ -281,6 +357,8 @@ number. The path to a first *reproducible* on-device baseline (PCK@20 ≥ 35%) i
|
||||
|
||||
## 🧩 Edge Module Catalog
|
||||
|
||||
[](firmware/esp32-csi-node/README.md)
|
||||
|
||||
Add signed modules for health, security, buildings, industry, research, AI, and more.
|
||||
|
||||
<details>
|
||||
@@ -455,6 +533,8 @@ Browse and install modules at [seed.cognitum.one/store](https://seed.cognitum.on
|
||||
|
||||
## 🔬 How It Works
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
WiFi routers flood every room with radio waves. When a person moves — or even breathes — those waves scatter differently. WiFi DensePose reads that scattering pattern and reconstructs what happened:
|
||||
|
||||
```
|
||||
@@ -579,6 +659,8 @@ These scenarios exploit WiFi's ability to penetrate solid materials — concrete
|
||||
|
||||
## 🧠 Self-Learning WiFi AI
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
Learn compact room fingerprints from raw CSI and adapt the model to each environment.
|
||||
|
||||
<details>
|
||||
@@ -694,7 +776,13 @@ claude --plugin-dir ./plugins/ruview
|
||||
|
||||
Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full details: [`plugins/ruview/README.md`](plugins/ruview/README.md).
|
||||
|
||||
For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.4.0`; the quick commands and fuller explanation are in the collapsed MetaHarness section near the top of this README and in [`harness/ruview/`](harness/ruview/README.md).
|
||||
**Live sensing pane (Claude Code mod):** the same marketplace also lists `ruview-live`, a Claude Code mod.
|
||||
- Install it with `/plugin install ruview-live@ruview`, or run `npx @ruvnet/ruview@0.9.1 mod` for its path inside the npm package.
|
||||
- `/ruview` opens a pane beside the transcript with your CSI nodes and radar, refreshed on a timer, plus a one-line status.
|
||||
- It is read-only.
|
||||
- Mods are early access; if `/ruview` is missing, start Claude Code with `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1`. See [ADR-377](docs/adr/ADR-377-ruview-live-claude-code-mod.md).
|
||||
|
||||
For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.9.1`. The quick commands and a fuller explanation are in the collapsed MetaHarness section near the top of this README, in [`harness/ruview/`](harness/ruview/README.md), and in the [user guide](docs/user-guide.md#ruview-npm-toolkit-ruvnetruview).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -702,6 +790,8 @@ For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.4.0`; the quick c
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
Start with the user, build, and calibration guides; expand for the full reference map.
|
||||
|
||||
<details>
|
||||
@@ -743,6 +833,8 @@ Start with the user, build, and calibration guides; expand for the full referenc
|
||||
>
|
||||
> Contributions and bug reports welcome at [Issues](https://github.com/ruvnet/RuView/issues).
|
||||
|
||||
[](https://cognitum.one)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License — see [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -20,6 +20,10 @@ func main() {
|
||||
exit(1)
|
||||
}
|
||||
let sample: [String: Any] = [
|
||||
// Always the connected link. macOS redacts ssid/bssid without
|
||||
// Location Services; rssi/noise/channel stay real, so mark the
|
||||
// sample as the connected link for the Rust adapter.
|
||||
"connected": true,
|
||||
"ssid": interface.ssid() ?? "",
|
||||
"bssid": interface.bssid() ?? "00:00:00:00:00:00",
|
||||
"channel": channel.channelNumber,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="304" viewBox="0 0 1200 304" role="img" aria-labelledby="title desc"><title id="title">Explore Cognitum One</title><desc id="desc">Ambient Intelligence at the edge of the physical world. Open cognitum.one.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="303" rx="18" fill="url(#wash)" stroke="#245360"/><text x="44" y="42" font-size="12" fill="#72eee0" text-anchor="start" class="mono">COGNITUM ONE</text><text x="44" y="99" font-size="42" fill="#ecfafb" text-anchor="start" >Ambient Intelligence.</text><text x="44" y="147" font-size="25" fill="#72eee0" text-anchor="start" >At the edge of the physical world.</text><text x="44" y="190" font-size="16" fill="#9abac5" text-anchor="start" >Explore the platform behind the ecosystem.</text><rect x="44" y="224" width="256" height="46" rx="11" fill="url(#accent)"/><text x="65" y="254" font-size="18" fill="#041117" text-anchor="start" >Explore Cognitum</text><path d="M263 247H284M277 240L284 247L277 254" fill="none" stroke="#041117" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="947" cy="151" r="173" fill="url(#halo)"/><g transform="translate(945 150) scale(2.4)"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#70b6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g><ellipse cx="945" cy="150" rx="140" ry="30" transform="rotate(-25 945 150)" fill="none" stroke="#3c8897" stroke-dasharray="5 18" class="flow"/><ellipse cx="945" cy="150" rx="160" ry="43" transform="rotate(-25 945 150)" fill="none" stroke="#3c8897" stroke-dasharray="5 18" class="flow"/><ellipse cx="945" cy="150" rx="180" ry="56" transform="rotate(-25 945 150)" fill="none" stroke="#3c8897" stroke-dasharray="5 18" class="flow"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">A guided AI operator</title><desc id="desc">Source-cited setup and verification. CLI, MCP and agent workflows. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 06</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >A guided AI operator</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Source-cited setup and verification.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >CLI, MCP and agent workflows.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><path d="M0 0L0 -33" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#72eee0" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">TOOLKIT GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">Local automation</title><desc id="desc">Connect rooms to your smart home. Home Assistant, MQTT and Matter. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 05</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >Local automation</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Connect rooms to your smart home.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >Home Assistant, MQTT and Matter.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><path d="M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-9" r="5" stroke="#70b6ff" fill="none" class="breathe"/></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">INTEGRATION GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">Edge sensor mesh</title><desc id="desc">Provision, capture and calibrate. Explore ESP32 firmware and runbooks. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 04</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >Edge sensor mesh</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Provision, capture and calibrate.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >Explore ESP32 firmware and runbooks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0a2630" stroke="#72eee0"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#70b6ff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#72eee0" fill="#72eee0" class="breathe"/></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">HARDWARE GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">Pose research</title><desc id="desc">Train and compare body keypoints. Live accuracy differs from benchmarks. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 03</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >Pose research</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Train and compare body keypoints.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >Live accuracy differs from benchmarks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><circle cx="0" cy="-28" r="8" stroke="#72eee0" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-14" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/><circle cx="0" cy="12" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/><circle cx="-24" cy="-6" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/><circle cx="24" cy="-6" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/><circle cx="-18" cy="38" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/><circle cx="18" cy="38" r="3" stroke="#70b6ff" fill="#041117" class="breathe"/></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">READ THE EVIDENCE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">Presence & motion</title><desc id="desc">Explore changes in room occupancy. Inspect motion features from CSI. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 01</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >Presence & motion</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Explore changes in room occupancy.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >Inspect motion features from CSI.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><circle cx="0" cy="-28" r="8" stroke="#72eee0" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><ellipse cx="0" cy="14" rx="40" ry="13" fill="none" stroke="#70b6ff" stroke-dasharray="3 8" class="flow"/></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">SENSING GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="580" height="276" viewBox="0 0 580 276" role="img" aria-labelledby="title desc"><title id="title">Breathing & vital signals</title><desc id="desc">Inspect phase-derived signal trends. Validate estimates on your hardware. Conceptual animation; follow the linked source for capability limits.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#245360"/><text x="30" y="39" font-size="11" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / 02</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" >Breathing & vital signals</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" >Inspect phase-derived signal trends.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" >Validate estimates on your hardware.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(474 168) scale(0.83)"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#214451" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g><path d="M30 204H550" fill="none" stroke="#245360" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#72eee0" text-anchor="start" class="mono">SIGNAL PIPELINE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="148" viewBox="0 0 1200 148" role="img" aria-labelledby="title desc"><title id="title">Choose your next step</title><desc id="desc">Animated section heading. GUIDES / HARDWARE / INTEGRATIONS</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(76 75) scale(0.75)"><path d="M0 0L0 -33" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#72eee0" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" >Choose your next step</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" class="mono">GUIDES / HARDWARE / INTEGRATIONS</text><path d="M865 44Q960 10 1124 52" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.65"/><path d="M865 61Q960 38 1124 69" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.55"/><path d="M865 78Q960 66 1124 86" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.45"/><path d="M865 95Q960 94 1124 103" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.35"/><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="148" viewBox="0 0 1200 148" role="img" aria-labelledby="title desc"><title id="title">Intelligence at the edge</title><desc id="desc">Animated section heading. SENSORS / FIRMWARE / MODULES</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(76 75) scale(0.75)"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0a2630" stroke="#72eee0"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#70b6ff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#72eee0" fill="#72eee0" class="breathe"/></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" >Intelligence at the edge</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" class="mono">SENSORS / FIRMWARE / MODULES</text><path d="M865 44Q960 10 1124 52" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.65"/><path d="M865 61Q960 38 1124 69" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.55"/><path d="M865 78Q960 66 1124 86" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.45"/><path d="M865 95Q960 94 1124 103" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.35"/><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="148" viewBox="0 0 1200 148" role="img" aria-labelledby="title desc"><title id="title">Evidence before accuracy claims</title><desc id="desc">Animated section heading. BENCHMARKS / REPRODUCERS / LIMITS</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(76 75) scale(0.75)"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#70b6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" >Evidence before accuracy claims</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" class="mono">BENCHMARKS / REPRODUCERS / LIMITS</text><path d="M865 44Q960 10 1124 52" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.65"/><path d="M865 61Q960 38 1124 69" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.55"/><path d="M865 78Q960 66 1124 86" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.45"/><path d="M865 95Q960 94 1124 103" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.35"/><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="148" viewBox="0 0 1200 148" role="img" aria-labelledby="title desc"><title id="title">Explore the capabilities</title><desc id="desc">Animated section heading. SENSING / MODELS / AUTOMATION</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(76 75) scale(0.75)"><circle cx="0" cy="-28" r="8" stroke="#72eee0" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><ellipse cx="0" cy="14" rx="40" ry="13" fill="none" stroke="#70b6ff" stroke-dasharray="3 8" class="flow"/></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" >Explore the capabilities</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" class="mono">SENSING / MODELS / AUTOMATION</text><path d="M865 44Q960 10 1124 52" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.65"/><path d="M865 61Q960 38 1124 69" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.55"/><path d="M865 78Q960 66 1124 86" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.45"/><path d="M865 95Q960 94 1124 103" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.35"/><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="148" viewBox="0 0 1200 148" role="img" aria-labelledby="title desc"><title id="title">From recordings to models</title><desc id="desc">Animated section heading. CAPTURE / TRAIN / EVALUATE</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(76 75) scale(0.75)"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#214451" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" >From recordings to models</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" class="mono">CAPTURE / TRAIN / EVALUATE</text><path d="M865 44Q960 10 1124 52" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.65"/><path d="M865 61Q960 38 1124 69" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.55"/><path d="M865 78Q960 66 1124 86" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.45"/><path d="M865 95Q960 94 1124 103" fill="none" stroke="#72eee0" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity="0.35"/><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="450" viewBox="0 0 1200 450" role="img" aria-labelledby="title desc"><title id="title">RuView: spaces become signals</title><desc id="desc">Animated conceptual RF field and room geometry. Explore the RuView user guide. This illustration is not a sensor measurement.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="449" rx="18" fill="url(#wash)" stroke="#245360"/><text x="48" y="44" font-size="13" fill="#72eee0" text-anchor="start" class="mono">RUVIEW / COGNITUM ECOSYSTEM</text><text x="48" y="121" font-size="76" fill="#ecfafb" text-anchor="start" >RuView</text><text x="48" y="184" font-size="38" fill="#ecfafb" text-anchor="start" >Spaces become signals.</text><text x="48" y="234" font-size="32" fill="#72eee0" text-anchor="start" >Signals become understanding.</text><text x="50" y="279" font-size="18" fill="#9abac5" text-anchor="start" >Explore RF sensing, edge inference and local automation.</text><rect x="48" y="326" width="270" height="52" rx="12" fill="url(#accent)"/><text x="72" y="359" font-size="20" fill="#041117" text-anchor="start" >Explore the guide</text><path d="M272 352H292M285 345L292 352L285 359" fill="none" stroke="#041117" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><text x="49" y="419" font-size="11" fill="#9abac5" text-anchor="start" class="mono">RF SENSING / EDGE MODELS / LOCAL ACTION</text><circle cx="950" cy="235" r="235" fill="url(#halo)"/><path d="M750 282L610 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M790 282L690 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M830 282L770 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M870 282L850 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M910 282L930 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M950 282L1010 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M990 282L1090 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M1030 282L1170 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M1070 282L1250 443" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 306H1168" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 336H1168" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 372H1168" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 418H1168" fill="none" stroke="#12343d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M803 157L1008 106L1137 177L930 236ZM803 157V314L930 391L1137 324V177M930 236V391M1008 106V268L803 314M1008 268L1137 324" fill="none" stroke="#4b9ba4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><g transform="translate(976 265) scale(1.35)"><circle cx="0" cy="-28" r="8" stroke="#72eee0" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><ellipse cx="0" cy="14" rx="40" ry="13" fill="none" stroke="#70b6ff" stroke-dasharray="3 8" class="flow"/></g><g transform="translate(780 349) scale(0.7)"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0a2630" stroke="#72eee0"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#70b6ff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 Line truncated
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"style": "Cognitum cyan on midnight",
|
||||
"motion": "Decorative CSS; prefers-reduced-motion supported; no script or remote dependencies",
|
||||
"assets": [
|
||||
{
|
||||
"file": "assets/readme/hero.svg",
|
||||
"title": "RuView: spaces become signals",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 450
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-start.svg",
|
||||
"title": "Start here",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-hardware.svg",
|
||||
"title": "Hardware",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-models.svg",
|
||||
"title": "Models",
|
||||
"target": "https://huggingface.co/ruvnet/wifi-densepose-pretrained",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-agents.svg",
|
||||
"title": "AI toolkit",
|
||||
"target": "harness/ruview/README.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-home.svg",
|
||||
"title": "Smart home",
|
||||
"target": "docs/integrations/home-assistant.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-seed.svg",
|
||||
"title": "Cognitum",
|
||||
"target": "https://cognitum.one/seed",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-presence.svg",
|
||||
"title": "Presence & motion",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-vitals.svg",
|
||||
"title": "Breathing & vital signals",
|
||||
"target": "v2/crates/wifi-densepose-signal/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-pose.svg",
|
||||
"title": "Pose research",
|
||||
"target": "docs/benchmarks/pose-estimation-cog.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-edge.svg",
|
||||
"title": "Edge sensor mesh",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-automation.svg",
|
||||
"title": "Local automation",
|
||||
"target": "docs/integrations/home-assistant.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-agents.svg",
|
||||
"title": "A guided AI operator",
|
||||
"target": "harness/ruview/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/overview.svg",
|
||||
"title": "RuView sensing overview",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 396
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-features.svg",
|
||||
"title": "Explore the capabilities",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-edge.svg",
|
||||
"title": "Intelligence at the edge",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-learning.svg",
|
||||
"title": "From recordings to models",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-evidence.svg",
|
||||
"title": "Evidence before accuracy claims",
|
||||
"target": "docs/benchmarks/pose-estimation-cog.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-docs.svg",
|
||||
"title": "Choose your next step",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/cognitum.svg",
|
||||
"title": "Explore Cognitum One",
|
||||
"target": "https://cognitum.one",
|
||||
"width": 1200,
|
||||
"height": 304
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">AI toolkit</title><desc id="desc">Open AI toolkit. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><path d="M0 0L0 -33" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#70b6ff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#72eee0" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#72eee0" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >AI toolkit</text></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">Hardware</title><desc id="desc">Open Hardware. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0a2630" stroke="#72eee0"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#70b6ff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#72eee0" fill="#72eee0" class="breathe"/></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >Hardware</text></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">Smart home</title><desc id="desc">Open Smart home. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><path d="M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-9" r="5" stroke="#70b6ff" fill="none" class="breathe"/></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >Smart home</text></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">Models</title><desc id="desc">Open Models. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#214451" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >Models</text></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">Cognitum</title><desc id="desc">Open Cognitum. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#70b6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >Cognitum</text></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="116" viewBox="0 0 180 116" role="img" aria-labelledby="title desc"><title id="title">Start here</title><desc id="desc">Open Start here. Animated navigation icon.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#245360"/><g transform="translate(90 45) scale(0.65)"><path d="M-10.4 -5.2Q0 -18.2 10.4 -5.2" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-0.65s"/><path d="M-20.0 -10.0Q0 -35.0 20.0 -10.0" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-1.25s"/><path d="M-30.400000000000002 -15.200000000000001Q0 -53.199999999999996 30.400000000000002 -15.200000000000001" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-1.9s"/><circle cx="0" cy="20" r="4" stroke="#72eee0" fill="#72eee0"/><path d="M0 12V-1" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" >Start here</text></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="396" viewBox="0 0 1200 396" role="img" aria-labelledby="title desc"><title id="title">RuView sensing overview</title><desc id="desc">Conceptual sequence: capture RF, extract context, act locally. Links to architecture and user documentation.</desc><defs><linearGradient id="wash" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0b303a"/><stop offset=".55" stop-color="#06171e"/><stop offset="1" stop-color="#030c13"/></linearGradient><linearGradient id="accent"><stop stop-color="#91f6df"/><stop offset="1" stop-color="#26bad7"/></linearGradient><radialGradient id="halo"><stop stop-color="#35d4db" stop-opacity=".17"/><stop offset="1" stop-color="#35d4db" stop-opacity="0"/></radialGradient></defs><style>text{font-family:Arial,Helvetica,sans-serif}.mono{font-family:ui-monospace,Consolas,monospace;letter-spacing:2px}.flow{stroke-dasharray:5 12;animation:flow 12s linear infinite}.breathe{animation:breathe 6s ease-in-out infinite}.trace{stroke-dasharray:400;stroke-dashoffset:0;animation:trace 10s ease-in-out infinite}.float{animation:float 9s ease-in-out infinite}@keyframes flow{to{stroke-dashoffset:-136}}@keyframes breathe{50%{opacity:.4}}@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}@keyframes float{50%{transform:translateY(-6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x=".5" y=".5" width="1199" height="395" rx="18" fill="url(#wash)" stroke="#245360"/><text x="36" y="44" font-size="12" fill="#72eee0" text-anchor="start" class="mono">SIGNAL TO CONTEXT</text><text x="36" y="89" font-size="34" fill="#ecfafb" text-anchor="start" >A sensing loop you can inspect.</text><rect x="36" y="120" width="336" height="220" rx="14" fill="#071b23" stroke="#285360"/><text x="60" y="153" font-size="12" fill="#72eee0" text-anchor="start" >01</text><text x="60" y="188" font-size="26" fill="#ecfafb" text-anchor="start" >Capture RF</text><text x="60" y="216" font-size="15" fill="#9abac5" text-anchor="start" >CSI from supported hardware</text><g transform="translate(305 281) scale(0.62)"><path d="M-10.4 -5.2Q0 -18.2 10.4 -5.2" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-0.65s"/><path d="M-20.0 -10.0Q0 -35.0 20.0 -10.0" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-1.25s"/><path d="M-30.400000000000002 -15.200000000000001Q0 -53.199999999999996 30.400000000000002 -15.200000000000001" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="breathe" style="animation-delay:-1.9s"/><circle cx="0" cy="20" r="4" stroke="#72eee0" fill="#72eee0"/><path d="M0 12V-1" fill="none" stroke="#72eee0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><text x="60" y="311" font-size="13" fill="#9abac5" text-anchor="start" >Provision and calibrate</text><path d="M380 229H420M412 221L420 229L412 237" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flow"/><rect x="432" y="120" width="336" height="220" rx="14" fill="#071b23" stroke="#285360"/><text x="456" y="153" font-size="12" fill="#72eee0" text-anchor="start" >02</text><text x="456" y="188" font-size="26" fill="#ecfafb" text-anchor="start" >Extract context</text><text x="456" y="216" font-size="15" fill="#9abac5" text-anchor="start" >Signal processing and models</text><g transform="translate(701 281) scale(0.62)"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#214451" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g><text x="456" y="311" font-size="13" fill="#9abac5" text-anchor="start" >Compare against real data</text><path d="M776 229H816M808 221L816 229L808 237" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flow"/><rect x="828" y="120" width="336" height="220" rx="14" fill="#071b23" stroke="#285360"/><text x="852" y="153" font-size="12" fill="#72eee0" text-anchor="start" >03</text><text x="852" y="188" font-size="26" fill="#ecfafb" text-anchor="start" >Act locally</text><text x="852" y="216" font-size="15" fill="#9abac5" text-anchor="start" >Events, APIs and integrations</text><g transform="translate(1097 281) scale(0.62)"><path d="M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32" fill="none" stroke="#72eee0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-9" r="5" stroke="#70b6ff" fill="none" class="breathe"/></g><text x="852" y="311" font-size="13" fill="#9abac5" text-anchor="start" >Apply rules and review outputs</text><text x="36" y="373" font-size="11" fill="#9abac5" text-anchor="start" class="mono">CONCEPTUALine truncated
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,87 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 960 540" role="img" aria-labelledby="title desc">
|
||||
<title id="title">RuView · WiFi becomes spatial awareness.</title><desc id="desc">RuView-focused animation showing WiFi sensors, spatial perception, Ruflo coordination and an illustrative ruOS sensing workspace. Room occupancy and actions are synthetic examples, not live measurements.</desc>
|
||||
<defs><radialGradient id="bg"><stop stop-color="#151518" /><stop offset="1" stop-color="#07080b" /></radialGradient><radialGradient id="aura"><stop stop-color="#edc65a" stop-opacity=".23" /><stop offset=".4" stop-color="#ba8823" stop-opacity=".07" /><stop offset="1" stop-color="#ba8823" stop-opacity="0" /></radialGradient><linearGradient id="gold" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fff4b1" /><stop offset=".5" stop-color="#ebc456" /><stop offset="1" stop-color="#96702f" /></linearGradient><linearGradient id="panel" x2="0" y2="1"><stop stop-color="#191a20" /><stop offset="1" stop-color="#0d0e13" /></linearGradient><pattern id="grid" width="36" height="36" patternUnits="userSpaceOnUse"><circle cx="18" cy="18" r=".65" fill="#d4c78e" opacity=".22" /></pattern><filter id="bloom" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="4" /></filter><clipPath id="frame"><rect width="960" height="540" rx="18" /></clipPath><clipPath id="line1"><rect x="64" y="155" width="850" height="93" /></clipPath><clipPath id="line2"><rect x="64" y="244" width="850" height="100" /></clipPath><g id="crown" fill="url(#gold)"><path d="M0 5l8 7L15 0l7 12 8-7-4 23H4z" /><rect x="4" y="31" width="22" height="3" rx="1" /></g>
|
||||
<style>
|
||||
text{font-family:Arial,Helvetica,sans-serif;fill:#f4f2ed}.mono{font-family:monospace}.muted{fill:#888993}.gold{fill:#e8c565}.scene{opacity:0}.end{opacity:1}.wire{fill:none;stroke:#c2a155;stroke-width:1}.all{animation-duration:18s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(.22,.65,.3,1)}
|
||||
@keyframes intro{0%,17%{opacity:1;transform:translateY(0)}22%,94%{opacity:0;transform:translateY(-12px)}100%{opacity:1;transform:translateY(0)}}
|
||||
@keyframes network{0%,20%{opacity:0;transform:translateY(18px) scale(.98)}25%,44%{opacity:1;transform:translateY(0) scale(1)}49%,100%{opacity:0;transform:translateY(-15px) scale(1.02)}}
|
||||
@keyframes work{0%,46%{opacity:0;transform:translateY(18px)}51%,66%{opacity:1;transform:translateY(0)}71%,100%{opacity:0;transform:translateY(-14px)}}
|
||||
@keyframes finale{0%,69%{opacity:0}74%,92%{opacity:1}97%,100%{opacity:0}}
|
||||
.intro{animation-name:intro}.network{animation-name:network;transform-origin:480px 270px}.work{animation-name:work}.end{animation-name:finale}
|
||||
@keyframes reveal1{0%,69%{transform:translateY(100px)}77%,100%{transform:translateY(0)}}@keyframes reveal2{0%,73%{transform:translateY(110px)}81%,100%{transform:translateY(0)}}.headline1{animation-name:reveal1}.headline2{animation-name:reveal2}
|
||||
@keyframes breathe{50%{opacity:.6;transform:scale(1.12)}}.breathe{animation:breathe 4.5s ease-in-out infinite;transform-origin:480px 239px}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}.orbit{animation:spin 18s linear infinite;transform-origin:480px 239px}
|
||||
@keyframes send{0%,9%{transform:translate(0,0)}14%,100%{transform:translate(-14px,-19px)}}.cursor{animation-name:send}
|
||||
@keyframes type{0%,2%{clip-path:inset(0 100% 0 0)}12%,100%{clip-path:inset(0 0 0 0)}}.typing{animation-name:type;animation-timing-function:steps(26,end)}
|
||||
@keyframes task{0%,49%{transform:translateY(25px);opacity:0}55%,100%{transform:translateY(0);opacity:1}}.task{animation-name:task}.task2{animation-delay:.3s}.task3{animation-delay:.6s}
|
||||
@keyframes grow{0%,54%{transform:scaleX(0)}63%,100%{transform:scaleX(1)}}.grow{animation-name:grow;transform-box:fill-box;transform-origin:left}
|
||||
@keyframes tick{0%,62%{opacity:0}65%,100%{opacity:1}}.tick{animation-name:tick}
|
||||
@keyframes trace{0%,22%{stroke-dashoffset:1}38%,100%{stroke-dashoffset:0}}.trace{stroke-dasharray:1;animation-name:trace}
|
||||
@keyframes drift{50%{transform:translateY(-10px);opacity:.8}}.dust{animation:drift 9s ease-in-out infinite}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}.scene{opacity:0}.end{opacity:1}.moving{display:none}}
|
||||
|
||||
@keyframes signal{0%,13%{transform:translate(480px,205px)}20%{transform:translate(708px,301px)}26%,43%{transform:translate(480px,239px)}49%{transform:translate(215px,155px)}55%{transform:translate(479px,155px)}61%{transform:translate(743px,155px)}72%,93%{transform:translate(782px,307px)}100%{transform:translate(480px,205px)}}
|
||||
.signal{animation-name:signal}.signal-glow{animation:shimmer 3s ease-in-out infinite}@keyframes shimmer{50%{opacity:.45}}
|
||||
@keyframes haloExpand{0%,12%{opacity:0;transform:scale(.5)}16%{opacity:.7}23%,100%{opacity:0;transform:scale(2.5)}}.clickring{animation-name:haloExpand;transform-origin:708px 301px}
|
||||
@keyframes aurora{0%,100%{transform:translate(-30px,10px) rotate(-4deg)}50%{transform:translate(30px,-10px) rotate(4deg)}}.aurora{animation:aurora 18s ease-in-out infinite;transform-origin:480px 270px}
|
||||
@keyframes dash{to{stroke-dashoffset:-80}}.stream{stroke-dasharray:2 18;animation:dash 3s linear infinite}
|
||||
@keyframes count{0%,52%{opacity:0;transform:translateY(8px)}58%,100%{opacity:1;transform:translateY(0)}}.detail{animation-name:count}
|
||||
@keyframes buildLine{0%,51%{stroke-dashoffset:1}64%,100%{stroke-dashoffset:0}}.buildLine{stroke-dasharray:1;animation-name:buildLine}
|
||||
@keyframes underline{0%,78%{transform:scaleX(0)}85%,100%{transform:scaleX(1)}}.underline{animation-name:underline;transform-origin:66px 350px}
|
||||
@keyframes dock{0%,49%{opacity:0;transform:translateY(14px)}55%,100%{opacity:1;transform:translateY(0)}}.dock{animation-name:dock}
|
||||
@media(prefers-reduced-motion:reduce){.signal,.clickring{display:none}}
|
||||
|
||||
@keyframes vortexCW{to{transform:rotate(360deg)}}@keyframes vortexCCW{to{transform:rotate(-360deg)}}
|
||||
.vortex{transform-origin:480px 270px;animation:vortexCW 36s linear infinite}.counter{transform-origin:480px 270px;animation:vortexCCW 54s linear infinite}
|
||||
@keyframes current{to{stroke-dashoffset:-1200}}.current{animation:current 9s linear infinite}
|
||||
@keyframes bgEnergy{0%,12%,100%{opacity:.22}22%,40%{opacity:.6}52%,65%{opacity:.4}74%,84%{opacity:.7}94%{opacity:.28}}.bgEnergy{animation:bgEnergy 18s ease-in-out infinite}
|
||||
@keyframes wave{0%{opacity:0;transform:scale(.3)}20%{opacity:.35}80%{opacity:.07}100%{opacity:0;transform:scale(1.9)}}.wave{transform-origin:480px 270px;animation:wave 9s cubic-bezier(.15,.35,.5,1) infinite}
|
||||
@keyframes glimmer{0%,100%{opacity:.08}50%{opacity:.72}}.glimmer{animation:glimmer 6s ease-in-out infinite}
|
||||
@keyframes comet{0%{opacity:0;transform:translate(-160px,70px)}12%{opacity:.65}58%{opacity:.25}75%,100%{opacity:0;transform:translate(1060px,-300px)}}.comet{animation:comet 9s linear infinite}
|
||||
@keyframes beam{0%,100%{transform:translate(-150px,0) rotate(-12deg);opacity:0}35%{opacity:.18}65%{opacity:.07;transform:translate(1050px,0) rotate(-12deg)}80%{opacity:0}}.beam{animation:beam 18s ease-in-out infinite;transform-origin:center}
|
||||
@media(prefers-reduced-motion:reduce){.dynamic-bg{display:none}}
|
||||
|
||||
.all{animation-duration:24s}
|
||||
@keyframes work{0%,46%{opacity:0;transform:translateY(18px)}50%,72%{opacity:1;transform:translateY(0)}76%,100%{opacity:0;transform:translateY(-14px)}}
|
||||
@keyframes finale{0%,75%{opacity:0}79%,93%{opacity:1}97%,100%{opacity:0}}
|
||||
@keyframes reveal1{0%,75%{transform:translateY(100px)}82%,100%{transform:translateY(0)}}
|
||||
@keyframes reveal2{0%,78%{transform:translateY(110px)}85%,100%{transform:translateY(0)}}
|
||||
@keyframes signal{0%,13%{transform:translate(480px,205px)}20%{transform:translate(708px,301px)}26%,43%{transform:translate(480px,239px)}49%{transform:translate(803px,182px)}55%{transform:translate(803px,235px)}63%,72%{transform:translate(803px,288px)}80%,93%{transform:translate(782px,307px)}100%{transform:translate(480px,205px)}}
|
||||
@keyframes browserOpen{0%,48%{opacity:0;transform:translateY(20px) scale(.96)}53%,100%{opacity:1;transform:translateY(0) scale(1)}}.browserOpen{animation-name:browserOpen;transform-origin:380px 230px}
|
||||
@keyframes terminalOpen{0%,53%{opacity:0;transform:translateY(14px)}58%,100%{opacity:1;transform:translateY(0)}}.terminalOpen{animation-name:terminalOpen}
|
||||
@keyframes terminalText{0%,57%{opacity:0}60%,100%{opacity:1}}.terminalText{animation-name:terminalText}
|
||||
@keyframes done{0%,65%{opacity:0}69%,100%{opacity:1}}.done{animation-name:done}
|
||||
@keyframes pointerMove{0%,50%{transform:translate(350px,275px)}55%{transform:translate(529px,267px)}59%{transform:translate(570px,325px)}66%,100%{transform:translate(738px,346px)}}.desktopPointer{animation-name:pointerMove}
|
||||
@keyframes chartGrow{0%,53%{transform:scaleY(.05)}63%,100%{transform:scaleY(1)}}.chartGrow{animation-name:chartGrow;transform-box:fill-box;transform-origin:bottom}
|
||||
</style><radialGradient id="readability"><stop stop-color="#08090e" stop-opacity=".58" /><stop offset=".65" stop-color="#08090e" stop-opacity=".38" /><stop offset="1" stop-color="#08090e" stop-opacity="0" /></radialGradient><linearGradient id="beamGold"><stop stop-color="#e7bf56" stop-opacity="0" /><stop offset=".5" stop-color="#efd57f" stop-opacity=".45" /><stop offset="1" stop-color="#e7bf56" stop-opacity="0" /></linearGradient></defs><g clip-path="url(#frame)"><rect width="960" height="540" fill="url(#bg)" /><rect width="960" height="540" fill="url(#grid)" opacity=".35" /><ellipse cx="490" cy="250" rx="460" ry="320" fill="url(#aura)" />
|
||||
<g><g class="aurora" fill="none" stroke="#c6a457" opacity=".11"><ellipse cx="480" cy="260" rx="470" ry="156" transform="rotate(-18 480 260)" /><ellipse cx="480" cy="260" rx="480" ry="166" transform="rotate(-18 480 260)" /><ellipse cx="480" cy="260" rx="495" ry="180" transform="rotate(-18 480 260)" /></g></g><g><g class="dynamic-bg"><g class="bgEnergy"><rect class="beam" x="0" y="-120" width="160" height="850" fill="url(#beamGold)" /><ellipse cx="480" cy="270" rx="280" ry="150" fill="none" stroke="#dcb55e" stroke-width=".9" class="wave" style="animation-delay:-0s" /><ellipse cx="480" cy="270" rx="280" ry="150" fill="none" stroke="#dcb55e" stroke-width=".9" class="wave" style="animation-delay:-3s" /><ellipse cx="480" cy="270" rx="280" ry="150" fill="none" stroke="#dcb55e" stroke-width=".9" class="wave" style="animation-delay:-6s" /><g class="vortex" fill="none"><ellipse cx="480" cy="270" rx="355" ry="160" transform="rotate(0 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="355" ry="160" transform="rotate(0 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-0.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="392" ry="184" transform="rotate(19 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="392" ry="184" transform="rotate(19 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-1.5s" opacity=".65" /><ellipse cx="480" cy="270" rx="429" ry="208" transform="rotate(38 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="429" ry="208" transform="rotate(38 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-3.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="466" ry="232" transform="rotate(57 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="466" ry="232" transform="rotate(57 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-4.5s" opacity=".65" /><ellipse cx="480" cy="270" rx="503" ry="256" transform="rotate(76 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="503" ry="256" transform="rotate(76 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-6.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="540" ry="280" transform="rotate(95 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="540" ry="280" transform="rotate(95 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-7.5s" opacity=".65" /></g><g class="counter" fill="none"><ellipse cx="480" cy="270" rx="355" ry="160" transform="rotate(0 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="355" ry="160" transform="rotate(0 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-0.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="392" ry="184" transform="rotate(-19 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="392" ry="184" transform="rotate(-19 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-1.5s" opacity=".65" /><ellipse cx="480" cy="270" rx="429" ry="208" transform="rotate(-38 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="429" ry="208" transform="rotate(-38 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-3.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="466" ry="232" transform="rotate(-57 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="466" ry="232" transform="rotate(-57 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-4.5s" opacity=".65" /><ellipse cx="480" cy="270" rx="503" ry="256" transform="rotate(-76 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="503" ry="256" transform="rotate(-76 480 270)" stroke="#f7d884" stroke-width="2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-6.0s" opacity=".65" /><ellipse cx="480" cy="270" rx="540" ry="280" transform="rotate(-95 480 270)" stroke="#b89342" stroke-width=".6" opacity=".3" /><ellipse cx="480" cy="270" rx="540" ry="280" transform="rotate(-95 480 270)" stroke="#f7d884" stroke-width="1.2" stroke-dasharray="18 6 4 420 65 800" class="current" style="animation-delay:-7.5s" opacity=".65" /></g><circle cx="10.1" cy="303.9" r="1.3" fill="#ffe4a0Line truncated
|
||||
<g class="scene intro all"><circle cx="480" cy="205" r="125" fill="url(#aura)" /><circle cx="480" cy="205" r="28" stroke="#f3d576" stroke-width="1" fill="none" /><text x="480" y="126" text-anchor="middle" font-size="32" letter-spacing="-1">Your room is already sending signals.</text>
|
||||
<rect x="215" y="269" width="530" height="64" rx="12" fill="url(#panel)" stroke="#554b31" /><text x="239" y="307" font-size="17" class="mono typing all">Sense. Understand. Act.</text><rect x="689" y="282" width="38" height="38" rx="9" fill="url(#gold)" /><path d="M708 307v-13m-5 5 5-5 5 5" fill="none" stroke="#222018" stroke-width="2" stroke-linecap="round" />
|
||||
<path class="cursor all" d="M731 323l0 27 7-8 8 1z" fill="#faf8e7" stroke="#151515" stroke-width="1.4" />
|
||||
<text x="480" y="382" text-anchor="middle" font-size="11" letter-spacing="3" class="mono muted">WIFI SENSING. NO CAMERAS.</text><g><circle class="clickring all" cx="708" cy="301" r="25" fill="none" stroke="#fae29b" /></g><g><g fill="none" stroke="#e7c467" opacity=".65"><path d="M437 214Q480 161 523 214" /><path d="M420 205Q480 133 540 205" opacity=".4" /><path d="M404 196Q480 108 556 196" opacity=".2" /></g></g></g>
|
||||
<g class="scene network all"><text x="480" y="107" text-anchor="middle" font-size="30" letter-spacing="-.7">From radio signals to useful actions.</text>
|
||||
<g><path d="M480 239C548 323 639 357 735 350" class="stream" fill="none" stroke="#f1d98f" stroke-width="1.5" opacity=".45" /></g><g><path d="M480 239C420 328 311 358 225 350" class="stream" fill="none" stroke="#f1d98f" stroke-width="1.5" opacity=".45" /></g><g><path d="M480 239C520 143 640 148 714 193" class="stream" fill="none" stroke="#f1d98f" stroke-width="1.5" opacity=".45" /></g><g><path d="M480 239C440 148 322 143 247 193" class="stream" fill="none" stroke="#f1d98f" stroke-width="1.5" opacity=".45" /></g><path d="M480 239C440 148 322 143 247 193" class="wire" opacity=".2" /><path d="M480 239C440 148 322 143 247 193" class="wire trace all" pathLength="1" /><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C440 148 322 143 247 193" dur="2.4s" begin="-0.0s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C440 148 322 143 247 193" dur="2.4s" begin="-0.0s" repeatCount="indefinite" /></circle></g><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C440 148 322 143 247 193" dur="2.4s" begin="-1.2s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C440 148 322 143 247 193" dur="2.4s" begin="-1.2s" repeatCount="indefinite" /></circle></g><path d="M480 239C520 143 640 148 714 193" class="wire" opacity=".2" /><path d="M480 239C520 143 640 148 714 193" class="wire trace all" pathLength="1" /><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C520 143 640 148 714 193" dur="2.4s" begin="-0.2s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C520 143 640 148 714 193" dur="2.4s" begin="-0.2s" repeatCount="indefinite" /></circle></g><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C520 143 640 148 714 193" dur="2.4s" begin="-1.4s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C520 143 640 148 714 193" dur="2.4s" begin="-1.4s" repeatCount="indefinite" /></circle></g><path d="M480 239C420 328 311 358 225 350" class="wire" opacity=".2" /><path d="M480 239C420 328 311 358 225 350" class="wire trace all" pathLength="1" /><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C420 328 311 358 225 350" dur="2.4s" begin="-0.4s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C420 328 311 358 225 350" dur="2.4s" begin="-0.4s" repeatCount="indefinite" /></circle></g><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C420 328 311 358 225 350" dur="2.4s" begin="-1.6s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C420 328 311 358 225 350" dur="2.4s" begin="-1.6s" repeatCount="indefinite" /></circle></g><path d="M480 239C548 323 639 357 735 350" class="wire" opacity=".2" /><path d="M480 239C548 323 639 357 735 350" class="wire trace all" pathLength="1" /><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C548 323 639 357 735 350" dur="2.4s" begin="-0.6000000000000001s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C548 323 639 357 735 350" dur="2.4s" begin="-0.6000000000000001s" repeatCount="indefinite" /></circle></g><g class="moving"><circle r="7" fill="#f6cf63" opacity=".5" filter="url(#bloom)"><animateMotion path="M480 239C548 323 639 357 735 350" dur="2.4s" begin="-1.8s" repeatCount="indefinite" /></circle><circle r="2.6" fill="#ffeaa1"><animateMotion path="M480 239C548 323 639 357 735 350" dur="2.4s" begin="-1.8s" repeatCount="indefinite" /></circle></g><circle cx="480" cy="239" r="98" fill="url(#aura)" class="breathe" /><circle cx="480" cy="239" r="57" fill="#111115" stroke="#6c5830" /><circle cx="480" cy="239" r="66" fill="none" stroke="#eac55e" stroke-opacity=".6" stroke-dasharray="38 170" class="orbit" /><use href="#crown" transform="translate(465 221)" /><rect x="147" y="169" width="200" height="55" rx="10" fill="url(#panel)" stroke="#51462e" /><circle cx="164" cy="191" r="3" fill="#e8c667" /><text x="177" y="195" font-size="18">WiFi sensors</text><text x="177" y="212" font-size="8" class="mono muted" letter-spacing="1.8">CAPTURE</text><rect x="614" y="169" width="200" height="55" rx="10" fill="url(#panel)" stroke="#51462e" /><circle cx="631" cy="191" r="3" fill="#e8c667" /><text x="644" y="195" font-size="18">RuView</text><text x="644" y="212" font-size="8" class="mono muted" letter-spacing="1.8">PERCEIVE</text><rect x="125" y="326" width="200" height="55" rx="10" fill="url(#panel)" stroke="#51462e" /><circle cx="142" cy="348" r="3" fill="#e8c667" /><text x="155" y="352" font-sizeLine truncated
|
||||
<g class="scene work all"><g><text x="480" y="92" text-anchor="middle" font-size="28" letter-spacing="-.6">A sensing workspace, powered by RuView.</text>
|
||||
<rect x="58" y="113" width="844" height="331" rx="12" fill="#090c12" stroke="#6a5931" />
|
||||
<path d="M70 144H890" stroke="#292b31" /><circle cx="76" cy="129" r="3" fill="#dcbf65" /><circle cx="87" cy="129" r="3" fill="#5b5c64" /><circle cx="98" cy="129" r="3" fill="#5b5c64" /><text x="480" y="133" text-anchor="middle" class="mono muted" font-size="9">ruOS / Desktops / ruview-workspace</text>
|
||||
<rect x="59" y="145" width="125" height="287" fill="#0c0e13" />
|
||||
<use href="#crown" transform="translate(72 158) scale(.4)" /><text x="91" y="170" font-size="15" font-weight="700">ruOS</text>
|
||||
<rect x="67" y="209" width="107" height="25" rx="5" fill="#302819" stroke="#63512c" />
|
||||
<rect x="76" y="194" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="201" font-size="10" class="muted">Overview</text><rect x="76" y="222" width="7" height="6" rx="1" fill="none" stroke="#e1c577" /><text x="91" y="229" font-size="10" class="gold">Desktops</text><rect x="76" y="250" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="257" font-size="10" class="muted">Terminal</text><rect x="76" y="278" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="285" font-size="10" class="muted">Swarms</text><rect x="76" y="306" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="313" font-size="10" class="muted">Network</text><rect x="76" y="334" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="341" font-size="10" class="muted">Usage</text><rect x="76" y="362" width="7" height="6" rx="1" fill="none" stroke="#686b79" /><text x="91" y="369" font-size="10" class="muted">Settings</text><text x="200" y="169" font-size="14" font-weight="700">RuView workspace</text><circle cx="364" cy="165" r="3" fill="#b9cb91" /><text x="374" y="169" font-size="9" class="muted">Running</text>
|
||||
<text x="882" y="167" text-anchor="end" font-size="8" class="mono gold">SYNTHETIC DEMO</text>
|
||||
<rect x="197" y="183" width="469" height="244" rx="7" fill="#14131a" stroke="#32313d" /><path d="M197 373Q370 180 666 285M197 330Q440 390 666 210" fill="none" stroke="#b49548" opacity=".18" />
|
||||
<g class="browserOpen all"><rect x="209" y="195" width="308" height="163" rx="7" fill="#191d26" stroke="#5a5961" />
|
||||
<rect x="209" y="195" width="308" height="24" rx="7" fill="#262a33" /><text x="221" y="211" font-size="8" class="mono muted">RuView / Room overview</text><circle cx="503" cy="207" r="2" fill="#e3c477" />
|
||||
<text x="225" y="241" font-size="13" font-weight="700">Room activity</text><text x="225" y="257" font-size="9" class="muted">Synthetic scene · Presence and motion</text>
|
||||
<rect x="224" y="329" width="83" height="16" rx="4" fill="#4a3d22" /><text x="233" y="340" font-size="8" class="gold">Sensor connected</text><g><path d="M229 270H494V318H229zM390 270v18m0 12v18" fill="#121921" stroke="#727781" stroke-width="1.5" /><path d="M235 278h34v15h-34zM464 280h22v24h-22z" fill="#303843" stroke="#707786" /><circle cx="285" cy="300" r="4" fill="#f0cd70" /><circle cx="285" cy="300" r="16" fill="none" stroke="#d8b353" opacity=".4" /><circle cx="326" cy="287" r="5" fill="#b7cbd0" /><path d="M326 293v12m-7-8h14m-7 8-5 8m5-8 5 8" fill="none" stroke="#b7cbd0" stroke-width="2" /><path d="M292 290Q310 273 326 287M294 306Q312 312 326 302" fill="none" stroke="#e1bd5f" stroke-dasharray="3 5" class="stream" /></g></g>
|
||||
<g class="terminalOpen all"><rect x="366" y="287" width="284" height="117" rx="6" fill="#090c12" stroke="#777061" /><rect x="367" y="288" width="282" height="21" rx="5" fill="#25272f" /><text x="380" y="302" font-size="9" class="mono">Terminal</text>
|
||||
<text x="380" y="324" font-size="9" class="mono gold">› Room event received</text><g class="terminalText all"><text x="380" y="343" font-size="9" class="mono muted">✓ Presence detected (demo)</text><text x="380" y="358" font-size="9" class="mono muted">✓ Rule checked</text></g><text x="380" y="384" font-size="10" class="mono gold done all">✓ Action ready for review</text></g>
|
||||
<rect x="680" y="183" width="207" height="244" rx="7" fill="#12151d" stroke="#33333c" /><text x="695" y="204" font-size="12" font-weight="700">Ruflo agent team</text><path d="M694 216h178" stroke="#34323a" />
|
||||
<circle cx="699" cy="235" r="5" fill="#272317" stroke="#be9f54" /><text x="713" y="239" font-size="11">Sense</text><text x="713" y="254" font-size="8" class="muted">Read room events from RuView</text><circle cx="699" cy="288" r="5" fill="#272317" stroke="#be9f54" /><text x="713" y="292" font-size="11">Evaluate</text><text x="713" y="307" font-size="8" class="muted">Check the configured rule</text><circle cx="699" cy="341" r="5" fill="#272317" stroke="#be9f54" /><text x="713" y="345" font-size="11">Act</text><text x="713" y="360" font-size="8" class="muted">Request an approved action</text><g class="done all"><rect x="693" y="381" width="181" height="29" rx="5" fill="#332b1b" stroke="#89713b" /><text x="784" y="400" text-anchor="middle" font-size="10" class="gold">✓ Room event processed</text></g>
|
||||
<g class="desktopPointer all"><path d="M0 0v20l5-6 8 1z" fill="#fff2ce" stroke="#18181a" stroke-width="1" /></g>
|
||||
<text x="480" y="469" text-anchor="middle" class="gold" font-size="14">Example: room activity → rule check → action review.</text></g></g><g class="scene end all"><ellipse cx="810" cy="290" rx="260" ry="240" fill="url(#aura)" />
|
||||
<text x="66" y="133" class="mono gold" font-size="11" letter-spacing="3">RUVIEW PERCEPTION. RUFLO COORDINATION.</text>
|
||||
<g clip-path="url(#line1)"><text class="headline1 all" x="61" y="233" font-size="72" font-weight="700" letter-spacing="-3">WiFi becomes</text></g>
|
||||
<g clip-path="url(#line2)"><text class="headline2 all" x="61" y="322" font-size="72" font-weight="700" letter-spacing="-3">spatial awareness<tspan class="gold">.</tspan></text></g>
|
||||
<path d="M66 382H894" stroke="#3b3528" /><text x="66" y="420" class="mono gold" font-size="17">ruos.cognitum.one</text><text x="892" y="420" text-anchor="end" font-size="10" class="mono muted" letter-spacing="2">BY COGNITUM</text><g><path class="underline all" d="M66 350h310" stroke="url(#gold)" stroke-width="2" /></g></g>
|
||||
<path d="M40 490H920" stroke="#26242a" /><text x="40" y="513" class="mono muted" font-size="9" letter-spacing="1.5">RUVIEW + RUFLO + RUOS</text><circle cx="853" cy="509" r="2.5" fill="#dfbf67" /><text x="866" y="512" class="mono gold" font-size="9">READY</text><g><g class="signal all"><circle r="45" fill="url(#aura)" class="signal-glow" /><circle r="10" fill="#ebc65d" opacity=".5" filter="url(#bloom)" /><circle r="4.5" fill="#ffe5a1" /><circle r="8" fill="none" stroke="#e9c56c" opacity=".3" /></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 37 KiB |
@@ -37,6 +37,31 @@ services:
|
||||
# volumes: ["/path/to/models:/app/models"]
|
||||
# MODELS_DIR=/app/models
|
||||
- MODELS_DIR=${MODELS_DIR:-data/models}
|
||||
# Auth (issue #864). The entrypoint refuses to start (exit 64) unless
|
||||
# RUVIEW_API_TOKEN is set. Export it before `docker compose up`:
|
||||
# export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
|
||||
# Bare names pass the host value through only when it is set.
|
||||
- RUVIEW_API_TOKEN
|
||||
# ESP32 UDP ingest (ADR-296, issue #2089). The UDP receiver defaults to
|
||||
# 127.0.0.1, which cannot receive from the published 5005/udp port.
|
||||
# Set ONE source guard and the entrypoint binds 0.0.0.0:
|
||||
# RUVIEW_UDP_ALLOW=<node-ip-or-cidr> restrict sources (preferred)
|
||||
# RUVIEW_UDP_INSECURE_LAN=true accept the spoofing risk
|
||||
# Docker Desktop and OrbStack on macOS rewrite the source address to the
|
||||
# bridge gateway, so a LAN-subnet allowlist drops every frame. There,
|
||||
# allow the gateway instead:
|
||||
# docker network inspect <project>_default \
|
||||
# --format '{{(index .IPAM.Config 0).Gateway}}'
|
||||
# RUVIEW_UDP_ALLOW=<gateway>/32
|
||||
# The gateway admits anything that reaches the published port, so keep
|
||||
# 5005/udp off untrusted networks.
|
||||
- RUVIEW_UDP_ALLOW
|
||||
- RUVIEW_UDP_INSECURE_LAN
|
||||
- RUVIEW_UDP_BIND
|
||||
# Browsing the UI from another machine also needs the host name or IP
|
||||
# in SENSING_ALLOWED_HOSTS (otherwise 421), and the TCP ports above
|
||||
# published beyond 127.0.0.1.
|
||||
- SENSING_ALLOWED_HOSTS
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
# Default is `auto`. Set CSI_SOURCE=simulated when you want
|
||||
# fake data tagged as such; never set it implicitly.
|
||||
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
|
||||
# RUVIEW_UDP_ALLOW / RUVIEW_UDP_INSECURE_LAN — ADR-296 UDP source guard.
|
||||
# Setting either makes the entrypoint bind UDP on 0.0.0.0 so
|
||||
# ESP32 frames on the published 5005/udp port can arrive.
|
||||
# RUVIEW_UDP_BIND — explicit UDP bind; overrides the choice above.
|
||||
set -e
|
||||
|
||||
# ── Issue #864: fail-closed on default posture ───────────────────────────────
|
||||
@@ -34,8 +38,8 @@ set -e
|
||||
#
|
||||
# Escape hatches (operator must opt in explicitly):
|
||||
# * Set RUVIEW_API_TOKEN to a strong secret → auth enabled on /api/v1/*.
|
||||
# * Set RUVIEW_ALLOW_UNAUTHENTICATED=1 → preserves the pre-fix behaviour;
|
||||
# only safe on an isolated trust boundary.
|
||||
# * Set RUVIEW_ALLOW_UNAUTHENTICATED=1 (or true/yes/on, any case) →
|
||||
# preserves the pre-fix behaviour; only safe on an isolated trust boundary.
|
||||
# * Set RUVIEW_BIND_ADDR to a loopback / private interface → unauth is fine
|
||||
# when the socket isn't reachable. The auto-bind nudges toward 127.0.0.1.
|
||||
#
|
||||
@@ -45,7 +49,12 @@ set -e
|
||||
case "${1:-}" in
|
||||
cog-ha-matter|ha-matter|homecore|homecore-server) ;;
|
||||
*)
|
||||
if [ -z "${RUVIEW_API_TOKEN:-}" ] && [ "${RUVIEW_ALLOW_UNAUTHENTICATED:-}" != "1" ]; then
|
||||
# Same spellings the sensing-server accepts for its boolean env flags.
|
||||
case "${RUVIEW_ALLOW_UNAUTHENTICATED:-}" in
|
||||
1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|[Oo][Nn]) __allow_unauth=1 ;;
|
||||
*) __allow_unauth=0 ;;
|
||||
esac
|
||||
if [ -z "${RUVIEW_API_TOKEN:-}" ] && [ "$__allow_unauth" != "1" ]; then
|
||||
# If the operator hasn't overridden the bind, refuse outright on
|
||||
# the default 0.0.0.0. If they've nailed it to loopback (or a
|
||||
# specific private address they trust), let it run.
|
||||
@@ -97,6 +106,50 @@ esac
|
||||
# server binary so users can just pass flags:
|
||||
# docker run <image> --source esp32 --tick-ms 500
|
||||
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
|
||||
# ── Issue #2089: UDP CSI ingest posture inside a container ───────────────
|
||||
# ADR-296 defaults the UDP receiver to 127.0.0.1. Inside a container that
|
||||
# bind can never receive from a published port, so no node ever appears.
|
||||
# We bind 0.0.0.0 only when the operator has configured an ADR-296 guard
|
||||
# (RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN); without one the server
|
||||
# would refuse a routable bind, so we keep loopback and say why instead.
|
||||
# An explicit RUVIEW_UDP_BIND or --udp-bind always wins.
|
||||
# The binary parses RUVIEW_UDP_INSECURE_LAN itself (1/yes/on/true, any
|
||||
# case); here we only need to know whether it is truthy to pick the bind.
|
||||
__insecure_lan=0
|
||||
case "$(printf '%s' "${RUVIEW_UDP_INSECURE_LAN:-}" | tr 'A-Z' 'a-z')" in
|
||||
1|true|yes|on) __insecure_lan=1 ;;
|
||||
esac
|
||||
__udp_bind_arg=0
|
||||
__udp_guard_arg=0
|
||||
for __arg in "$@"; do
|
||||
case "$__arg" in
|
||||
--udp-bind|--udp-bind=*) __udp_bind_arg=1 ;;
|
||||
--udp-allow|--udp-allow=*|--udp-insecure-lan) __udp_guard_arg=1 ;;
|
||||
esac
|
||||
done
|
||||
if [ -z "${RUVIEW_UDP_BIND:-}" ] && [ "$__udp_bind_arg" = 0 ]; then
|
||||
if [ -n "${RUVIEW_UDP_ALLOW:-}" ] || [ "$__insecure_lan" = 1 ] \
|
||||
|| [ "$__udp_guard_arg" = 1 ]; then
|
||||
export RUVIEW_UDP_BIND=0.0.0.0
|
||||
echo "[entrypoint] UDP CSI receiver: binding 0.0.0.0 inside the container" >&2
|
||||
echo "[entrypoint] (a source guard is set: RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN)." >&2
|
||||
else
|
||||
case "${CSI_SOURCE:-auto}" in
|
||||
simulated|simulate) ;;
|
||||
*)
|
||||
echo "[entrypoint] NOTE: UDP CSI receiver stays on 127.0.0.1 (ADR-296 default)." >&2
|
||||
echo "[entrypoint] Inside a container that bind cannot receive ESP32 frames from" >&2
|
||||
echo "[entrypoint] a published 5005/udp port, so no node will appear. To ingest:" >&2
|
||||
echo "[entrypoint] -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> (restrict sources), or" >&2
|
||||
echo "[entrypoint] -e RUVIEW_UDP_INSECURE_LAN=true (accept spoofing risk)" >&2
|
||||
echo "[entrypoint] Docker Desktop/OrbStack on macOS rewrite the source to the bridge" >&2
|
||||
echo "[entrypoint] gateway, so the allowlist must name <gateway>/32, not the LAN subnet." >&2
|
||||
echo "[entrypoint] See https://github.com/ruvnet/RuView/issues/2089" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
set -- /app/sensing-server \
|
||||
--source "${CSI_SOURCE:-auto}" \
|
||||
--tick-ms 100 \
|
||||
|
||||
@@ -6,7 +6,9 @@ Known issues and fixes from the rebase-to-upstream branch (upstream #301).
|
||||
|
||||
## 1. Node not appearing in /api/v1/nodes
|
||||
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/spatial/nodes`.
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/nodes`.
|
||||
|
||||
**Check the server first:** the UDP receiver listens on `127.0.0.1` by default (ADR-296), so a natively run `sensing-server` never hears nodes on your network unless it was started with `--udp-bind 0.0.0.0 --udp-allow <node-ip-or-cidr>` (or `--udp-insecure-lan`). The startup log line for the UDP receiver says `loopback-only` when this is the problem. For Docker, see "Receiving ESP32 frames in Docker" in the [user guide](user-guide.md).
|
||||
|
||||
**Root cause:** After USB flash, the node enters a limping state where WiFi associates but the UDP CSI sender silently fails. The SoftAP + mDNS stack initializes but the CSI callback never fires.
|
||||
|
||||
@@ -145,18 +147,18 @@ ESP32 nodes still target the host on `--target-ip <host>:5005` — no firmware r
|
||||
|
||||
**Symptom:** `sensing-server` starts cleanly, logs `HTTP server listening on http://localhost:3000`, but loading `http://localhost:3000/` (or `/ui/index.html`) returns `404 Not Found`. Reported in #188.
|
||||
|
||||
**Root cause:** The default `--ui-path ../../ui` is resolved relative to the binary's *current working directory*, not the binary location. When the binary is launched from anywhere other than `crates/wifi-densepose-sensing-server/`, the relative path doesn't reach the UI assets and Axum's static file handler returns 404.
|
||||
**Root cause:** The default `--ui-path ../ui` is resolved relative to the *current working directory*, not the binary location. If it doesn't exist the server also tries `./ui` and `../../ui`. Launching from `v2/` works; from anywhere else (including `v2/crates/wifi-densepose-sensing-server/`) those paths miss the repo's UI, or hit some other `ui` directory, and Axum's static file handler returns 404.
|
||||
|
||||
**Fix:** Pass an absolute UI path, run the binary from the crate directory, or use the Docker image (which bundles the UI under `/app/ui`).
|
||||
**Fix:** Pass an absolute UI path, run the binary from `v2/`, or use the Docker image (which bundles the UI under `/app/ui`).
|
||||
|
||||
```bash
|
||||
# Option A — absolute path (recommended for production)
|
||||
sensing-server --source esp32 --udp-port 5005 --http-port 3000 \
|
||||
--ws-port 3001 --ui-path /absolute/path/to/ui
|
||||
|
||||
# Option B — run from the crate dir (works for local dev / cargo run)
|
||||
cd v2/crates/wifi-densepose-sensing-server
|
||||
cargo run -- --source esp32
|
||||
# Option B — run from v2/ (works for local dev / cargo run)
|
||||
cd v2
|
||||
cargo run -p wifi-densepose-sensing-server -- --source esp32
|
||||
|
||||
# Option C — Docker (no path config needed)
|
||||
docker compose -f docker/docker-compose.yml up sensing-server
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-03-01 |
|
||||
| **Updated** | 2026-10-01: macOS 27 also redacts SSID without Location Services; the connected link is kept with a synthetic id, and `auto`+wifi keeps UDP bound (see Amendment 1) |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **ORCA** — OS-native Radio Channel Acquisition |
|
||||
| **Relates to** | ADR-013 (Feature-Level Sensing Commodity Gear), ADR-022 (Windows WiFi Enhanced Fidelity), ADR-014 (SOTA Signal Processing), ADR-018 (ESP32 Dev Implementation) |
|
||||
@@ -313,3 +314,22 @@ All verification on Mac Mini (M2 Pro, macOS 26.3).
|
||||
- ADR-022: Windows WiFi Enhanced Fidelity (analogous platform adapter)
|
||||
- ADR-013: Feature-Level Sensing from Commodity Gear
|
||||
- Issue [#56](https://github.com/ruvnet/wifi-densepose/issues/56): macOS support request
|
||||
|
||||
---
|
||||
|
||||
## Amendment 1 (2026-10-01): SSID is also redacted; keep the connected link
|
||||
|
||||
**Observed on macOS 27 (M-series Mac, CoreWLAN, unsigned CLI helper).** Without Location Services, `mac_wifi --scan-once` returns `"ssid":""` and `"bssid":"00:00:00:00:00:00"`, while `rssi`, `noise`, `channel` and `tx_rate` are real. The §1 assumption that "SSID + RSSI + channel" survive no longer holds. An unsigned CLI helper never calls `CLLocationManager`, so it never appears in Location Services and cannot be granted access.
|
||||
|
||||
Before this amendment, `resolve_bssid` abstained when both identities were blank (to avoid merging unrelated networks), so every macOS scan was dropped and `--source wifi` produced nothing, with no error.
|
||||
|
||||
**Change.**
|
||||
1. `--scan-once` marks its single sample `"connected": true`. It is always the one connected link, so it cannot collide with another network.
|
||||
2. A redacted sample marked `connected` gets a stable, locally administered synthetic BSSID derived from a reserved key plus the channel. The SSID stays empty; none is invented. Unmarked redacted lines still abstain.
|
||||
3. `--source auto` that resolves to host WiFi now also binds the UDP receiver, and `wifi_task` yields while the source is a live `esp32`. ESP32 nodes that start after the 2 s boot probe still promote the source, matching the #1004 rule for the simulator.
|
||||
4. A missing helper logs one clear error with the build command instead of a warning on every tick.
|
||||
|
||||
**Measured (2026-10-01, this change):** `--source wifi` → source `wifi:unnamed`, sensing ticks advance. `--source auto` on a quiet port → `wifi` with `udp_receiver=true`; relaying real ADR-018 frames promoted the source to `esp32`, with all 5 nodes listed.
|
||||
|
||||
**Still open:** real BSSID/SSID would need a signed `.app` helper with `NSLocationWhenInUseUsageDescription` that requests authorization. The helper still lives under `archive/v1` with no build step.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-03-02 |
|
||||
| **Updated** | 2026-10-04 — multistatic fusion combines phase across nodes only when every frame declares the same shared phase reference; independent ESP32 nodes and the amplitude-only server bridge fuse amplitude only and report the reason (`phase_fusion` in `/health/ready`, issue #1752). |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RuvSense** -- RuVector-Enhanced Sensing for Multistatic Fidelity |
|
||||
| **Relates to** | ADR-012 (ESP32 Mesh), ADR-014 (SOTA Signal Processing), ADR-016 (RuVector Training), ADR-017 (RuVector Signal+MAT), ADR-018 (ESP32 Implementation), ADR-024 (AETHER Embeddings), ADR-026 (Survivor Track Lifecycle), ADR-027 (MERIDIAN Generalization) |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | **Accepted** (MQTT track P1–P7 + P8a + P9 + P10 shipped 2026-05-23 in PR #778, 410 lib tests, witness bundle VERIFIED) / **Proposed** (Matter SDK wiring P8b deferred to v0.7.1 per §9.10) |
|
||||
| **Date** | 2026-05-23 |
|
||||
| **Date** | 2026-05-23 (amended 2026-10-02: P4.5 status note, §11) |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **HA-DISCO** (MQTT) + **HA-FABRIC** (Matter) + **HA-MIND** (semantic primitives) |
|
||||
| **Relates to** | ADR-018 (CSI binary frame format), ADR-021 (ESP32 vitals), ADR-031 (RuView sensing-first), ADR-039 (edge vitals packet 0xC511_0002), ADR-079 (camera ground-truth), ADR-103 (cog-person-count), ADR-110 (ESP32-C6 firmware), ADR-114 (cog-quantum-vitals) |
|
||||
@@ -440,7 +440,7 @@ This means: **adding a new primitive is one file change**. No MQTT schema rev, n
|
||||
| **P2** | HA discovery message emitter. New module `mqtt_discovery.rs`. Emits all entity `config` topics on connect + every `--mqtt-refresh-secs`. Schema-validated against HA's published JSON schema. | pending |
|
||||
| **P3** | State publication. Subscribe to internal `tokio::broadcast` channel (the one `tx.send(json)` writes to on line 3983 of `main.rs`). Translate `edge_vitals` / `sensing_update` / `pose_data` messages into per-entity state payloads. Apply rate-limit + privacy-mode filters. | pending |
|
||||
| **P4** | Integration tests: dockerised mosquitto in CI (extend `.github/workflows/firmware-qemu.yml` pattern), schema-validate every emitted config against HA's `homeassistant/components/mqtt` JSON schemas (pin to a tested HA version). Add a smoke test that brings up sensing-server in `--source mock --mqtt`, subscribes with `paho-mqtt` test client, asserts on entity creation. | pending |
|
||||
| **P4.5** | **Semantic inference layer (HA-MIND).** New module `semantic_inference.rs` implementing the 10 v1 primitives from §3.12. Output broadcast channel consumed by both MQTT publisher (P3) and Matter bridge (P8). Per-primitive precision/recall baselines published to `docs/integrations/semantic-primitives-metrics.md`. Unit tests per FSM + integration tests via replay of ADR-079 paired captures. | pending |
|
||||
| **P4.5** | **Semantic inference layer (HA-MIND).** New module `semantic_inference.rs` implementing the 10 v1 primitives from §3.12. Output broadcast channel consumed by both MQTT publisher (P3) and Matter bridge (P8). Per-primitive precision/recall baselines published to `docs/integrations/semantic-primitives-metrics.md`. Unit tests per FSM + integration tests via replay of ADR-079 paired captures. | partial — 6 of 10 primitives on the live MQTT publisher; see §11 |
|
||||
| **P5** | Docs: new `docs/integrations/home-assistant.md` with screenshots of the HA UI after auto-discovery completes, example HA dashboard YAML (Lovelace card configs), 8 starter blueprints from §3.12.2 (distress notify, wake routine, hallway dim, elderly anomaly alert, meeting lights, bathroom fan, fall-risk escalate, auto-arm security), and the raw-channel example automations: "turn on hall light when presence ON", "send notification on fall_detected event", "log HR/BR to InfluxDB". | pending |
|
||||
| **P6** | Ship `--mqtt` in the next sensing-server release (target: v0.7.0). Demo end-to-end on `cognitum-v0` against a Mosquitto add-on running on a Home Assistant OS install. Update README hardware-options table with "Works with Home Assistant" badge. | pending |
|
||||
| **P7** | Matter Bridge spike: build a throwaway prototype with `matter-rs` exposing one `OccupancySensor` endpoint + one `GenericSwitch` for fall. Pair against Apple Home, Google Home, and HA's Matter integration. Decision gate: if pairing works on all three, proceed to P8; if blocked, switch to chip-tool FFI and re-spike. | pending |
|
||||
@@ -667,4 +667,57 @@ Empty as of 2026-05-23. New questions discovered during implementation will be f
|
||||
|
||||
---
|
||||
|
||||
## 11. Status note: P4.5 semantic states on the live publisher (2026-10-02)
|
||||
|
||||
Issues [#2085](https://github.com/ruvnet/RuView/issues/2085) and [#2093](https://github.com/ruvnet/RuView/issues/2093) found that discovery announced 20 entities per device while only 6 ever received state, that `presence/state` was published on every CSI frame, and that HA device ids changed on every restart. The FSMs in `crate::semantic` existed but had no caller outside their tests. This note records what is now wired and what is not.
|
||||
|
||||
### 11.1 What changed
|
||||
|
||||
- **Semantic primitives run on the live path.** `mqtt::planner` holds one `SemanticBus` per node, sampled at 1 Hz (the rate the FSM constants assume). The bus feeds only the MQTT publisher. Matter does not consume it yet, so the "output broadcast channel shared with Matter" in P4.5 is not built.
|
||||
- **Announce only what has a source.** Six primitives are announced: `room_active`, `no_movement`, `elderly_inactivity_anomaly`, `someone_sleeping`, `possible_distress` and `fall_risk_elevated`. Four are not: `meeting_in_progress` needs a 1–20 % motion level, but per-node motion is a three-level classification; `bathroom_occupied`, `bed_exit` and `multi_room_transition` need zones, and the broadcast carries none (the §3.12.5 zone file is not implemented). For the same reason `zone_occupancy` (§3.1) and `pose` are not announced: `pose_keypoints` is never populated on the broadcast, and `persons[].keypoints` are synthesized.
|
||||
- **Per-entity availability (§3.6).** An announced entity is `online` only while its input exists: vitals while the calibration-gated estimate is published, `fall` while the node sends `edge_vitals`, `motion_energy` and `rssi` while present, and semantic primitives after the 60 s warmup (sleeping and distress also need breathing or heart rate, fall risk needs the fall detector). Otherwise it is `offline` and no state is sent. Transitions publish at once; all availability topics are re-sent every 30 s.
|
||||
- **No default values.** `motion_energy` was always `0.0` (MEASURED: 176 of 176 samples) because the bridge never set it. It now carries the node's `motion_band_power`, or the room value, or nothing. `fall` is driven by the rising edge of `edge_vitals.fall_detected`. Only `sensing_update` produces snapshots, which removes the phantom aggregate device that `edge_vitals` frames created. `present_still` now maps to 0 % motion instead of 100 %.
|
||||
- **Rates (§3.7).** Binary states publish on change, plus a 60 s retained heartbeat, as §3.5 already specified. Sensors keep their configured rates.
|
||||
- **Stable device identity (§3.8).** The default client id is now `wifi-densepose-<8 hex>`, persisted in `<data-dir>/mqtt_client_id`. §3.8 specified `wifi-densepose-<hostname>`; a persisted random id avoids a hostname dependency and survives hostname changes. `--mqtt-client-id` still overrides it.
|
||||
|
||||
### 11.2 Evidence
|
||||
|
||||
Unit tests (CODE-DERIVED, run with `cargo test -p wifi-densepose-sensing-server --features mqtt --lib -- mqtt:: semantic::`): `mqtt::planner::tests` covers the entity → state mapping, unavailable-without-source, warmup, on-change with heartbeat at a 73 frame/s input, sensor rates, reset and privacy mode. `mqtt::bridge::tests` covers message-type filtering, fall edges and motion energy. `mqtt::config::tests` covers client-id persistence across two "restarts" in a temp dir, override and malformed-file recovery.
|
||||
|
||||
Live measurement (MEASURED, 2026-10-02): one ESP32 node streaming CSI and `edge_vitals` over UDP to a macOS host; `sensing-server --source esp32 --mqtt` against `eclipse-mosquitto:2` in Docker on a non-default port; every topic captured with a `paho-mqtt` subscriber on `#`. "Before" is `03a89793` (main), "after" is this change, built `--release --features mqtt`, same command and window.
|
||||
|
||||
```bash
|
||||
# Broker: mosquitto.conf = "listener 1883 0.0.0.0 / allow_anonymous true / persistence false"
|
||||
docker run -d --rm -p 127.0.0.1:18830:1883 \
|
||||
-v "$PWD/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" eclipse-mosquitto:2
|
||||
# Run A (180 s, then SIGTERM) and run B (restart, 45 s) share one --data-dir.
|
||||
# A paho-mqtt subscriber on '#' starts 2 s before each run.
|
||||
RUVIEW_API_TOKEN=<random> sensing-server --source esp32 --udp-port 5006 \
|
||||
--udp-bind 0.0.0.0 --udp-allow <node-subnet> --http-port 3000 --ws-port 3001 \
|
||||
--no-mdns --data-dir <dir> --mqtt --mqtt-host 127.0.0.1 --mqtt-port 18830
|
||||
```
|
||||
|
||||
| Measure (180 s window unless noted) | Before (`03a89793`) | After |
|
||||
|---|---|---|
|
||||
| HA devices announced in run A | 2 (one phantom aggregate + `-node1`) | 1 (`-node1`) |
|
||||
| Entities announced per device | 20 | 15 |
|
||||
| Entities that received state (node device) | 6 | 10 |
|
||||
| Announced entities without state that still reported `online` | 14 | 1 (`fall`: an event, and no fall occurred) |
|
||||
| `presence/state` messages (node device) | 6,814 (37.9 / s) | 4 (one per change, plus 60 s heartbeat) |
|
||||
| `motion_energy` values | `0.0` in 176 of 176 | node band power, 177 samples |
|
||||
| Device id, run A → run B (restart, same `--data-dir`) | `wifi-densepose-<pid A>` → `<pid B>` (4 devices on the broker after B) | same persisted id in both runs (1 device) |
|
||||
| `offline` published on SIGTERM | no | no (unchanged; see 11.3) |
|
||||
| Same, plus `--privacy-mode` (75 s, fresh `--data-dir`) | — | 13 entities announced; no `heart_rate`, `breathing_rate` or `pose` topic for that device |
|
||||
|
||||
After the change, five announced entities had no state: `heart_rate` and `breathing_rate`, because no calibration had been run; `someone_sleeping` and `possible_distress`, which depend on them; and `fall`, which is an event and no fall occurred. The first four were `offline`. `fall` was `online`, because the node sent `edge_vitals`. Before the change all 14 entities without state still reported `online`. Only one node was streaming during this measurement. #2085 used five nodes and measured about 73 `presence` messages per second per node.
|
||||
|
||||
### 11.3 Not done
|
||||
|
||||
- Semantic precision/recall on real captures. The figures in `semantic-primitives-metrics.md` have no reproducer in the repository and stay CLAIMED.
|
||||
- Zone and threshold files, `--semantic*` / `--no-semantic` flags on `sensing-server`, the `reason` attribute (§3.12.4), and `semantic_events.jsonl`.
|
||||
- MQTT last-will and offline-on-shutdown (§3.6). The server registers no LWT and SIGTERM does not publish `offline`, so availability stays as last published after the server stops.
|
||||
- Matter consumption of the semantic states.
|
||||
|
||||
---
|
||||
|
||||
*ADR-115 is the integration story that turns RuView from "another sensing platform" into "drop-in upgrade for any HA install **and** any Matter-controller home." MQTT carries the rich, differentiated telemetry; Matter carries the standardised subset across every controller ecosystem. Numbers 111 and 112 remain reserved per the project ADR-numbering policy.*
|
||||
@@ -66,10 +66,64 @@ hardware claims.
|
||||
timestamps, sequence behavior, calibration, maximum size, and report rate.
|
||||
- Validate OpenWrt One first, then BPI-R3 4x4, before considering MT7996/MLO.
|
||||
|
||||
## Amendment 1 — vendor CSI on MT7981, still unvalidated
|
||||
|
||||
- **Date**: 2026-09-19, recorded here 2026-10-01
|
||||
- **Status**: proposed. One board model (two units). The hardware gates below are still open.
|
||||
|
||||
The 2026-07-18 decision said the public `mt76` tree has no supported CSI export,
|
||||
and that a firmware change might be required before physical CSI exists. Both
|
||||
statements need a narrower reading.
|
||||
|
||||
Mainline `openwrt/mt76` still has no CSI export. MediaTek's public OpenWrt feed
|
||||
does: vendor id `0x0ce7`, MCU event `0xc2`, patch
|
||||
`1001-wifi-mt76-mt7915-csi-implement-csi-support`. On a Wavlink WL-WN586X3
|
||||
Rev A (MT7981BA + MT7976CN + MT7531AE) that patch, rebased onto the `mt76`
|
||||
revision pinned by OpenWrt 24.10.8, produced complex samples without a firmware
|
||||
blob swap. The `0xc2` hook is already in the MT7981 firmware that the OpenWrt
|
||||
24.10.8 image carries. The
|
||||
rebased patch and the two userspace fixes are in `firmware/openwrt-wn586x3/`.
|
||||
|
||||
What the first capture was: one record, 2.4 GHz, 20 MHz, legacy OFDM, 64
|
||||
subcarriers, a single chain. CSI arrived only while a client sent frames.
|
||||
The rate is client traffic, not a sensor sample clock. The capture is not in
|
||||
this repository.
|
||||
|
||||
What it was not:
|
||||
|
||||
- Not a calibration, a sequence check, a timestamp check, or a repeatability run.
|
||||
The label on the wire is `mediatek:physical-unvalidated`. Plain `mediatek` stays
|
||||
reserved for a frame that has passed those gates.
|
||||
- Not a 4x4 array. Each radio is two chains. The four antennas are a 2.4 GHz pair
|
||||
and a 5 GHz pair. Two routers are two receivers. Cross-box phase is not a
|
||||
shared local oscillator, so it is not a beam.
|
||||
- Not the OpenWrt One or the BPI-R3. Those remain the upstream-friendly targets
|
||||
in the original decision. The WN586X3 is the board this path was actually run on.
|
||||
|
||||
`wifi-densepose-mtk-bridge` translates `mt76-vendor` JSON, or an MtkCSIdump
|
||||
datagram, into ADR-267 MTC1 and sends it to the sensing server. The server keeps
|
||||
one snapshot and a bounded ring of per-subcarrier amplitude and phase per
|
||||
`device_id`. It runs `mediatek-amplitude-heuristic-v0` on mean and peak amplitude,
|
||||
and `activity-index-v0` on per-subcarrier fluctuation. Both are `CLAIMED`. Neither
|
||||
is the ESP32 vital-sign path.
|
||||
|
||||
One measured property supports further work, and nothing more. Offline, on six
|
||||
local dump captures from two units (2.4 GHz, HT20, chains rx0 and rx1 of transmit
|
||||
stream 0), the cross-chain phase `arg(H0·conj(H1))`, summed over subcarriers,
|
||||
moved 0.014-0.039 rad (p50) between adjacent frames for well-sampled
|
||||
transmitters. Raw per-chain phase moved about 1.56 rad (p50), which is what
|
||||
uniformly random phase gives (MEASURED; reproducer
|
||||
`scripts/mediatek-csi-phase-stability.py`). That shows a stable observable inside
|
||||
one radio. It does not show that the observable tracks people.
|
||||
|
||||
The operator guide is [`docs/mediatek-router-csi.md`](../mediatek-router-csi.md).
|
||||
|
||||
## Links
|
||||
|
||||
- [ADR-123: BFLD capture path](ADR-123-bfld-capture-path-nexmon-and-esp32.md)
|
||||
- [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- [ADR-267: MediaTek MIMO CSI wire protocol](ADR-267-mediatek-mimo-csi-wire-protocol.md)
|
||||
- [MediaTek router CSI guide](../mediatek-router-csi.md)
|
||||
- [upstream mt76](https://github.com/openwrt/mt76)
|
||||
- [OpenWrt One](https://openwrt.org/toh/openwrt/one)
|
||||
- [MediaTek OpenWrt feed](https://git01.mediatek.com/openwrt/feeds/mtk-openwrt-feeds/)
|
||||
@@ -34,6 +34,81 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
|
||||
- Flags distinguish calibrated, saturated, time-synchronized, dropped-predecessor,
|
||||
and synthetic frames. Synthetic provenance cannot be cleared by downstream code.
|
||||
|
||||
## Amendment 1 — PPDU types `Legacy` and `Unknown`
|
||||
|
||||
- **Date**: 2026-09-19
|
||||
- **Status**: proposed. Demonstrated on one WL-WN586X3 capture. Not a maintainer acceptance.
|
||||
- **Scope**: `PpduType` discriminants only. The 72-byte header layout, CRC,
|
||||
payload ordering, flags, and caps are unchanged.
|
||||
|
||||
### What changed
|
||||
|
||||
`PpduType` gains two variants:
|
||||
|
||||
| Value | Name | Meaning |
|
||||
|---|---|---|
|
||||
| 6 | `Legacy` | Pre-HT: 802.11b CCK or 802.11a/g OFDM |
|
||||
| 7 | `Unknown` | A PPDU format the producer could not classify |
|
||||
|
||||
Values 1-5 (`Ht`, `Vht`, `HeSu`, `HeMu`, `Eht`) are untouched.
|
||||
|
||||
### Why
|
||||
|
||||
The original set assumed every CSI report would be HT or later. The first real
|
||||
MT7981 capture disproved that. A Wavlink WL-WN586X3 (MT7981B + MT7976C) running
|
||||
an OpenWrt 24.10.8 image built with the patches in `firmware/openwrt-wn586x3/`
|
||||
reported `rx_mode = 1` (`MT_PHY_TYPE_OFDM`) for a client associated on 2.4 GHz
|
||||
channel 6 at 20 MHz. The bridge had no way to express it and dropped the frame.
|
||||
|
||||
That was a real data loss, not a corner case. MediaTek's CSI driver treats legacy
|
||||
OFDM as a first-class mode: `mt7915_vendor_csi_tone_mask` skips only
|
||||
`MT_PHY_TYPE_CCK`, and its `mode_map` is keyed by `MT_PHY_TYPE_OFDM`, `_HT`,
|
||||
`_VHT` and `_HE_SU`, giving OFDM its own tone-mask group
|
||||
(`1001-wifi-mt76-mt7915-csi-implement-csi-support.patch:1070-1088`). Association
|
||||
and management exchanges are legacy-rate by design, so a sensing deployment will
|
||||
see these continuously.
|
||||
|
||||
`Unknown` exists so the enum's gaps (5-7 and 12 in `mt76_phy_type`) and future
|
||||
modes are carried explicitly rather than guessed at or dropped. It also covers
|
||||
transports that omit the field: the MtkCSIdump UDP datagram carries no `rx_mode`,
|
||||
and reporting `Unknown` there is honest where defaulting to a concrete format was
|
||||
not.
|
||||
|
||||
### Compatibility
|
||||
|
||||
- **Forward, not backward.** A decoder built before this amendment rejects
|
||||
discriminants 6 and 7 with `UnknownPpduType`, because the original parser
|
||||
rejects unknown types by design. `TryFrom<u8>` returns `Err` rather than
|
||||
panicking, so an old decoder fails safely — it just drops legacy frames, and
|
||||
does so silently. **Rebuild MTC1 consumers after this codec change.**
|
||||
- **Scope is narrower than "the workspace".** `wifi-densepose-hardware` defines
|
||||
three independent `PpduType` enums — `csi_frame.rs` (ESP32),
|
||||
`qualcomm_csi.rs`, and `mediatek_csi.rs` — and this amendment touches only the
|
||||
MediaTek one. Neither of the others changes, so no other vendor bridge is
|
||||
affected. The complete set of `mediatek_csi::PpduType` consumers is:
|
||||
`wifi-densepose-hardware`'s own `mediatek-csi-sim` binary,
|
||||
`wifi-densepose-mtk-bridge`, and `wifi-densepose-sensing-server`, which renders the
|
||||
value with `{:?}` and so needed no code change. In particular the
|
||||
`ppdu_type > 3` bound in `bootstrap_baseline.rs` is fed from the **ESP32**
|
||||
enum via `CsiGridKey::from_frame(&Esp32Frame)`, not from MediaTek frames, so
|
||||
`Legacy` and `Unknown` cannot reach it.
|
||||
- Because the variants were added rather than renumbered, and Rust requires
|
||||
exhaustive matches, the compiler locates any consumer that must be updated.
|
||||
Nothing outside the files above needed one.
|
||||
- Producers must emit 6 only for frames that genuinely are pre-HT, and 7 only
|
||||
where the format is truly unclassified. Neither is a dumping ground for a
|
||||
mapping the producer has not done.
|
||||
- The mapping from `rx_mode` to `PpduType` is now infallible, so no MTC1 producer
|
||||
may drop a frame on PPDU grounds.
|
||||
- Unchanged: synthetic provenance still cannot be cleared by downstream code.
|
||||
|
||||
### Observed dimensions
|
||||
|
||||
The same capture assembled to 1x1x64. That is the client's behaviour, not a
|
||||
protocol limit: the handset sent single-stream frames to a 2.4 GHz 20 MHz AP.
|
||||
Expect 1x2 or 2x2 on 5 GHz, and size consumers for the chipset profile's
|
||||
`max_chains` rather than for what one capture happened to show.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
@@ -59,3 +134,8 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
|
||||
- [ADR-266: MediaTek Filogic CSI platform](ADR-266-mediatek-filogic-csi-platform.md)
|
||||
- [ADR-018: ESP32 binary CSI framing](ADR-018-esp32-csi-frame-protocol.md)
|
||||
- [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- Amendment 1 implementation: `v2/crates/wifi-densepose-hardware/src/mediatek_csi.rs`
|
||||
(codec) and `v2/crates/wifi-densepose-mtk-bridge` (`ppdu_from_rx_mode`). The
|
||||
motivating capture is a lab record and is not in this tree: it contains a
|
||||
client transmitter address and raw CSI. The in-repo check is a fabricated
|
||||
legacy-OFDM record in `wifi-densepose-mtk-bridge/tests/hardware_capture.rs`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# ADR-364: BFI capture admission and continuity
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-29
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: bfi, capture, evidence, continuity, hardware
|
||||
|
||||
## Context
|
||||
|
||||
The Archer BE550, associated iPhone and MT7927 capture host have produced
|
||||
readable beamforming feedback information (BFI). Monitor-mode support alone
|
||||
does not establish this capability, and readable reports do not establish
|
||||
continuous sensing. Router channel width also does not guarantee an identical
|
||||
feedback grid in every report.
|
||||
|
||||
**MEASURED:** two saved trials contain 66 and 96 unique reports. During their
|
||||
approximately 30-second sender intervals, rates were 2.2 and 3.2 reports/s,
|
||||
with maximum silence of 8.392 and 6.437 seconds. No reports followed in the
|
||||
approximately 15-second post-traffic intervals. All 162 reports received an
|
||||
independent tshark angle/control/SNR comparison. Reproducer commands, capture
|
||||
hashes and limitations are in the [capture results](../../tools/bfi/OPTIMIZATION.md)
|
||||
and [continuity report](../../tools/bfi_learning/CAPTURE-CONTINUITY.md).
|
||||
|
||||
## Decision
|
||||
|
||||
Admit a hardware/driver/profile combination only from its actual packet
|
||||
evidence. Apply these boundaries before using its output for sensing:
|
||||
|
||||
1. Keep originals and receipts outside Git, with raw own-network captures on
|
||||
the capture host. Record hardware, driver, channel, source hashes, capture
|
||||
interval and restoration outcome. Select the authorized AP/client direction
|
||||
explicitly; missing provenance cannot be inferred from an SSID.
|
||||
2. Enforce container, radiotap, MAC, length, fragmentation and supported-format
|
||||
checks in [bfi_decode.py](../../tools/bfi/bfi_decode.py). Validate FCS when
|
||||
present and reject flagged corruption; absent FCS is a recorded limitation.
|
||||
Unsupported tuples remain counted rejections. Compare every admitted angle
|
||||
against an independent dissector before extending a format's support claim.
|
||||
3. Deduplicate retries over the whole capture before slicing time intervals.
|
||||
Preserve integer timestamps and separate feedback grids. Sequence numbers
|
||||
and sounding tokens do not measure lost packets. BFI steering matrices are
|
||||
not full CSI; the identity product of a square unitary matrix with its
|
||||
conjugate transpose cannot serve as a changing channel feature.
|
||||
4. Preserve full-observation quality metrics and add separate before-traffic,
|
||||
sender-interval and after-traffic metrics. Include leading/trailing silence,
|
||||
report rate, maximum gap and duration-weighted occupied one-second bins.
|
||||
Sender timestamps bound attempted UDP sends, not verified over-air sounding.
|
||||
5. Use an initial continuity gate of reports in at least 27 of the first 30
|
||||
complete one-second sender bins and no silence longer than two seconds
|
||||
anywhere in the actual sender interval, repeated in three comparable trials.
|
||||
Report any partial final bin separately; never count it as a complete second.
|
||||
This is a project engineering gate, not a published sensing accuracy result.
|
||||
|
||||
The current traffic receipt lacks a PCAP hash and AP/client binding. The
|
||||
analyzer checks caller-selected file associations and hashes all inputs in its
|
||||
output; this establishes content integrity, not authenticated session binding.
|
||||
Stronger binding is required before automated capture admission can treat
|
||||
receipt association as independently verified.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Monitor-mode declarations, average report rate alone and interpolating missing
|
||||
reports would each hide a different failure. Retain explicit packet validation
|
||||
and interval coverage instead. ESP32/Realtek CSI remains a separate modality;
|
||||
multiple unsynchronized boards do not establish a coherent BFI array.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
Packet validity, capture continuity and sensing accuracy have separate,
|
||||
reproducible acceptance criteria. Changes in denominators cannot appear as RF
|
||||
performance gains.
|
||||
|
||||
### Negative
|
||||
|
||||
Strict format and continuity gates can exclude usable but sparse observations.
|
||||
New PHY formats and hardware require additional captures and independent checks.
|
||||
|
||||
### Neutral
|
||||
|
||||
The current trials fail continuity. Further motion modeling requires a new
|
||||
capture result; this ADR does not authorize hardware changes or establish
|
||||
identity recognition.
|
||||
|
||||
## Validation and links
|
||||
|
||||
Run `python -B -m unittest discover -s tools/bfi -v` and the focused
|
||||
`test_traffic_coverage.py` suite under `tools/bfi_learning`. Synthetic tests
|
||||
check parsing and interval rules; acceptance additionally requires retained
|
||||
real captures and the three successful trials above.
|
||||
|
||||
- Extends proposed [ADR-118](ADR-118-bfld-beamforming-feedback-layer-for-detection.md)
|
||||
and [ADR-123](ADR-123-bfld-capture-path-nexmon-and-esp32.md); neither is superseded.
|
||||
- Preserves [ADR-120](ADR-120-bfld-privacy-class-and-hash-rotation.md)'s privacy boundary.
|
||||
- Public-data admission has a separate contract in
|
||||
[ADR-365](ADR-365-public-bfi-dataset-and-decoder-contract.md).
|
||||
@@ -0,0 +1,121 @@
|
||||
# ADR-365: Public BFI dataset and decoder contract
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-29
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: bfi, datasets, decoding, provenance, benchmarks
|
||||
|
||||
## Context
|
||||
|
||||
Public activity recordings offer a separate route to BFI model development
|
||||
while local capture continuity is insufficient. They introduce different frame
|
||||
formats, collection assumptions and licenses. Public HAR activity labels do
|
||||
not reproduce KIT's person-identification experiment.
|
||||
|
||||
**MEASURED:** the pinned CSI-BFI-HAR train/validation download passed exact
|
||||
size/hash checks for 120 captures plus its license card. A separate body decoder
|
||||
matched 13,104 quantized angles across 14 sample bodies against tshark 4.2.2 in
|
||||
two independent executions. Whole-packet admission and activity training remain
|
||||
incomplete. See [results](../../tools/bfi_learning/RESULTS.md) and the
|
||||
[decoder audit and reproducer](../../tools/bfi_learning/PUBLIC-BFI-DECODE.md).
|
||||
|
||||
## Decision
|
||||
|
||||
### Pin provenance and partitions
|
||||
|
||||
Use the [author dataset](https://huggingface.co/datasets/foysalhaque/CSI-BFI-HAR-Dataset/tree/c3013b3edc5a563a9cf6759f5d464a9d9b6f456a)
|
||||
at revision `c3013b3edc5a563a9cf6759f5d464a9d9b6f456a`. The
|
||||
[manifest](../../tools/bfi_learning/manifests/csi-bfi-har-c3013b3-subset.json)
|
||||
pins URLs, lengths, SHA-256, labels and split membership. Preserve the pinned
|
||||
dataset card's GPL-3.0 declaration and attribution separately from code licenses.
|
||||
|
||||
| Partition | Author directory | Domain |
|
||||
| --- | --- | --- |
|
||||
| Training | HAR-1/BFI/M1 | Day 1, kitchen |
|
||||
| Validation | HAR-3/BFI/M1 | Day 3, classroom |
|
||||
| Held out | HAR-5/BFI/M1 and M2 | Day 5, living room; two views |
|
||||
|
||||
Keep all views of one event in the same partition. Day and room are confounded;
|
||||
M1/M2 labels do not establish heterogeneous-chipset transfer. The selected
|
||||
filenames cover A–T activities and P1–P3 participants.
|
||||
|
||||
Default downloads exclude held-out views. The bounded
|
||||
[downloader](../../tools/bfi_learning/download_public_bfi.py) verifies exact
|
||||
bytes before atomic completion, rehashes resumed files, retains failed partials
|
||||
and enforces transfer deadlines and byte limits. Data stays outside Git;
|
||||
downloaded code is never executed by the downloader.
|
||||
|
||||
### Keep body decoding separate from packet admission
|
||||
|
||||
[public_bfi_decode.py](../../tools/bfi_learning/public_bfi_decode.py) accepts
|
||||
only the audited VHT MU 3x1, 80 MHz, Ng=1, codebook-1 complete action body:
|
||||
1,003 bytes containing one SNR byte, 936 angle bytes and 61 opaque MU-exclusive
|
||||
bytes. It exposes quantized/radian angles and tone ordinals. Frequency indices,
|
||||
exclusive-field interpretation and validated matrix reconstruction remain
|
||||
separate acceptance work.
|
||||
|
||||
Twelve sample frames have nonzero fragment-number bits. Successful tshark
|
||||
decoding and valid CRCs do not resolve their semantics. Retain live MAC guards;
|
||||
quarantine these frames until an explicit, independently reviewed dataset
|
||||
policy can justify admission. Require lossless pcapng conversion evidence for
|
||||
integer timestamps, packet lengths, link types and all original packet bytes.
|
||||
Do not silently strip headers or rewrite fragment fields to gain admission.
|
||||
|
||||
The natural circular representation needs 1,404 features, exceeding the current
|
||||
trainer's 1,024-feature limit. Approve a bounded representation or revised limit
|
||||
with allocation tests before integration; do not truncate tones implicitly.
|
||||
|
||||
### Admit recording quality before scoring models
|
||||
|
||||
Inventory every manifest file, including tiny or unusable traces. Freeze a
|
||||
five-second observed-interval rule: at least 20 valid unique reports, with no
|
||||
inter-report gap above one second and no interpolation. Require at least two
|
||||
independent recordings per activity in each partition/view group that supply
|
||||
an eligible interval. Missing A–T coverage fails benchmark admission.
|
||||
Before implementing the census, also freeze interval anchoring, stride and
|
||||
leading/trailing gap rules: an inter-report gap bound alone does not bound
|
||||
silence at interval edges. These details remain required acceptance work.
|
||||
|
||||
Public files lack trusted recorder start/end manifests. Observed packet spans
|
||||
cannot establish unobserved leading/trailing coverage. Report rejection reasons,
|
||||
eligible recording/time coverage and accuracy together. Freeze event/file groups
|
||||
before generating windows; inspect held-out quality without model selection
|
||||
against held-out outcomes.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Reusing the HE/SU parser without format admission, weakening live fragment
|
||||
guards, or silently dropping sparse classes would change the task while hiding
|
||||
the change. Keep this adapter explicit and fail incomplete benchmark coverage.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
Data provenance, protocol support and score coverage remain independently
|
||||
auditable. New public formats cannot silently weaken live capture validation.
|
||||
|
||||
### Negative
|
||||
|
||||
Sparse files or unresolved framing may prevent a benchmark despite a successful
|
||||
download. Format and representation work must precede model optimization.
|
||||
|
||||
### Neutral
|
||||
|
||||
This is offline research/reference tooling. Production integration must reuse
|
||||
the Rust protocols in [ADR-291](ADR-291-public-benchmark-evaluation-harness.md)
|
||||
and scorecards in [ADR-317](ADR-317-benchmark-multi-domain-scorecard.md); this ADR
|
||||
does not establish a second production trainer or an integrated benchmark score.
|
||||
|
||||
## Validation and links
|
||||
|
||||
Use `test_download_public_bfi.py` and `test_public_bfi_decode.py` under
|
||||
`tools/bfi_learning`, then the full packet oracle and quality census specified
|
||||
in the [decode audit](../../tools/bfi_learning/PUBLIC-BFI-DECODE.md) and
|
||||
[benchmark protocol](../../tools/bfi_learning/RESEARCH-NEXT.md).
|
||||
|
||||
- Depends on [ADR-364](ADR-364-bfi-capture-admission-and-continuity.md)'s distinction
|
||||
between packet validity and coverage.
|
||||
- Preserves [ADR-120](ADR-120-bfld-privacy-class-and-hash-rotation.md).
|
||||
- [ADR-119](ADR-119-bfld-frame-format-and-wire-protocol.md) defines a BFLD envelope;
|
||||
that envelope does not replace raw 802.11 decoding.
|
||||
@@ -0,0 +1,115 @@
|
||||
# ADR-366: CSI controls and frozen evaluation
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-29
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: csi, evaluation, controls, holdout, evidence
|
||||
|
||||
## Context
|
||||
|
||||
The public NTU-Fi HumanID experiment uses 14 classes and a custom directory
|
||||
split of 238 training, 56 validation and 546 held-out recordings. It differs
|
||||
from the author's folder mapping. Physical acquisition-session independence
|
||||
is unknown; absence of exact duplicates cannot establish it.
|
||||
|
||||
**MEASURED:** each of six LSTM/GRU runs classified 56/56 validation recordings
|
||||
correctly. A fixed-alpha ridge model using only mean amplitudes also scored
|
||||
56/56. In a matched three-seed GRU diagnostic, raw/original, raw/shuffled and
|
||||
demeaned/original inputs each scored 56/56 for every seed; demeaned/shuffled
|
||||
scored 52/56, 55/56 and 56/56. Chronological motion is unnecessary for perfect
|
||||
accuracy on this validation split. The physical source of the predictive signal
|
||||
remains unresolved. [Measured results and reproduction receipts](../../tools/bfi_learning/RESULTS.md)
|
||||
|
||||
## Decision
|
||||
|
||||
### Separate diagnostics from claims of generalization
|
||||
|
||||
Use static and temporal controls before interpreting a sequence-model score.
|
||||
Keep input files, optimizer budgets and seeds matched across raw/original,
|
||||
raw/shuffled, demeaned/original and demeaned/shuffled conditions. Permute whole
|
||||
feature rows within one recording, using source/seed information without labels.
|
||||
Never mix partitions. Mark shuffled time coordinates as administrative.
|
||||
|
||||
Whole-recording demeaning is an offline diagnostic that sees the complete
|
||||
recording. It cannot substantiate causal streaming inference. These nondefault
|
||||
controls are restricted to the custom public identity protocol and cannot alter
|
||||
next-observation BFI forecasting. Fit normalization on training inputs only.
|
||||
|
||||
Report recording-level accuracy, macro-F1, included recordings and seed variation
|
||||
alongside validation cross entropy averaged over selected windows. Recording
|
||||
predictions aggregate softmax probabilities across their selected windows;
|
||||
the current loss is not computed from those recording aggregates. Treat window
|
||||
accuracy as secondary because overlapping windows and repeated seeds on the same
|
||||
recordings are not independent participant trials. Retain session-independence
|
||||
limitations beside every custom-split result.
|
||||
|
||||
### Freeze selection before final evaluation
|
||||
|
||||
Keep the [predeclared control protocol](../../tools/bfi_learning/CONTROLS.md):
|
||||
the twelve 50-epoch diagnostic runs are excluded from final-model selection.
|
||||
Only the six original raw/original 200-epoch LSTM/GRU runs are eligible. Select
|
||||
lowest window-averaged validation cross entropy, breaking ties by earlier
|
||||
completion. Do not substitute recording-aggregated loss or change this rule in
|
||||
response to a held-out outcome.
|
||||
|
||||
Freeze source, dataset, metadata, configuration and checkpoint hashes before
|
||||
the scheduled final evaluation. Load frozen weights without retraining. The
|
||||
trainer creates an exclusive one-use claim before holdout scoring and retains
|
||||
it even if scoring fails; a failure requires explicit review, not an automatic
|
||||
retry. This claim is scoped to the selected frozen-run directory; the controller
|
||||
enforces one final evaluation across the task's candidate directories. Stop
|
||||
tuning against that holdout after its result is inspected. At this decision's
|
||||
evidence checkpoint, final holdout metrics remain absent.
|
||||
|
||||
### Preserve executable provenance
|
||||
|
||||
Use bounded numeric NPZ inputs/checkpoints with pickle disabled and validate ZIP
|
||||
central-directory bounds before allocation. Verify hashes and restore recorded
|
||||
transformations during frozen evaluation. The launcher checks returned seed,
|
||||
protocol, configuration and `test: null` during tuning. Save incomplete runs
|
||||
and denominator changes explicitly rather than substituting successful runs.
|
||||
|
||||
Current Python tools are bounded offline reference experiments. Production
|
||||
benchmark integration continues through the Rust protocols and per-domain
|
||||
scorecards of ADR-291/317. A fair state-of-the-art comparison needs matched data,
|
||||
task, representation, partition, adaptation access and evaluation; this custom
|
||||
CSI result establishes neither BFI identity performance nor paper reproduction.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Validation accuracy alone cannot distinguish temporal learning from static
|
||||
cues. Random window splits would leak recordings across partitions. Expanding
|
||||
the eligible model set after diagnostics would change the predeclared selection
|
||||
procedure. Retain the frozen recording split and candidate set.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
Controls expose when model complexity or temporal claims lack experimental
|
||||
support. The final evaluation has a traceable selection rule and one-use record.
|
||||
|
||||
### Negative
|
||||
|
||||
A saturated validation set limits further optimization, and strict one-use
|
||||
evaluation makes interrupted scoring an explicit recovery decision. New-session
|
||||
evidence requires new data rather than additional fitting to this split.
|
||||
|
||||
### Neutral
|
||||
|
||||
The GRU has 24.8% fewer parameters than the tested LSTM, but measured peak GPU
|
||||
allocation was higher and elapsed times do not isolate an architecture speedup.
|
||||
Preserve these separate resource findings when comparing models.
|
||||
|
||||
## Validation and links
|
||||
|
||||
Run the focused `test_prepare.py`, `test_train.py`, `test_static_baseline.py`
|
||||
and `test_run_batch.py` suites under `tools/bfi_learning`. Synthetic isolation
|
||||
and restore tests complement the retained real-data receipts; neither establishes
|
||||
new-session generalization.
|
||||
|
||||
- Extends [ADR-291](ADR-291-public-benchmark-evaluation-harness.md) and
|
||||
[ADR-317](ADR-317-benchmark-multi-domain-scorecard.md) without superseding them.
|
||||
- [Comparison contract](../../tools/bfi_learning/RESEARCH.md),
|
||||
[trainer](../../tools/bfi_learning/train.py) and
|
||||
[overnight frozen-selection procedure](../../tools/bfi_learning/OVERNIGHT.md).
|
||||
@@ -0,0 +1,110 @@
|
||||
# ADR-367: Bounded research swarm and compute ownership
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-29
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: orchestration, ruflo, codex, compute, provenance
|
||||
|
||||
## Context
|
||||
|
||||
Research, implementation and independent review can run concurrently, while
|
||||
GPU training, capture hardware and final evaluation need clear ownership.
|
||||
Registration in a swarm registry does not prove that a worker executed a task.
|
||||
Unbounded retries, duplicate schedulers or overlapping jobs can corrupt evidence
|
||||
and spend resources without improving an experiment.
|
||||
|
||||
**MEASURED at the September 29 checkpoint:** local GPU runs and Codex reviews
|
||||
completed with receipts. Federation enrollment returned `registration paused`
|
||||
and relay membership was rejected; no cross-host worker accepted a task.
|
||||
Shared-memory writes were blocked by the native-WAL guard. These limitations
|
||||
and the actual work locations are recorded in the
|
||||
[overnight procedure](../../tools/bfi_learning/OVERNIGHT.md).
|
||||
|
||||
## Decision
|
||||
|
||||
### Assign one owner to each mutable resource
|
||||
|
||||
The root controller owns each capture action, GPU batch, checkpoint-selection
|
||||
decision and final evaluation. Delegated workers receive bounded source/review
|
||||
tasks and explicit file ownership. Worker findings are untrusted proposals for
|
||||
controller review. Distinguish registration, dispatch, execution and verified
|
||||
completion in receipts; only execution evidence supports a completed-work claim.
|
||||
|
||||
Use the existing app heartbeat as the sole recurring scheduler for this task.
|
||||
Finite serial GPU loops are jobs within a wake, not independent recurring
|
||||
schedulers. Resume from receipts and verified lock ownership; completed runs
|
||||
are not repeated merely because a wake or supervisor restarts.
|
||||
|
||||
### Bound execution and audit outputs
|
||||
|
||||
The [GPU launcher](../../tools/bfi_learning/run_batch.py) uses Linux process
|
||||
groups, an exclusive batch lock, a 900-second trainer budget and a 1,020-second
|
||||
child timeout. The current experiment requires at least 4 GiB free GPU memory,
|
||||
caps the trainer's PyTorch allocator fraction at 20%, and bounds logs/metrics
|
||||
to 8 MiB. This allocator limit is not a whole-device memory reservation.
|
||||
Verify child hashes, seed, protocol, configuration and absence of test metrics
|
||||
before declaring success. Preserve unrelated GPU services; cleanup targets only
|
||||
owned processes. Audit matrix and batch ownership before changing stale locks.
|
||||
|
||||
The task-specific [Codex review wrapper](../../tools/bfi_learning/codex_iteration.py)
|
||||
uses bounded source snapshots on stdin, read-only sandboxing, execution rules,
|
||||
an exclusive lock, timeout and private event logs. This differs from the generic
|
||||
contributor adapter's configuration contract. Check events for unexpected tool
|
||||
execution; never execute suggestions automatically or grant child text authority.
|
||||
|
||||
Preserve federation and memory safety guards when a backend is unavailable.
|
||||
Use private local receipts as the fallback and report degraded coordination.
|
||||
Retry only after a relevant service/writer change or a concrete causal fix.
|
||||
|
||||
### Bind compute to an explicit task budget and end time
|
||||
|
||||
Use local capacity first. A paid GPU needs a measured capacity requirement and
|
||||
an explicitly authorized cumulative budget covering actual spend, outstanding
|
||||
reservations and fees. Before rental, check current prices, active instances
|
||||
and remaining allowance; reserve maximum bounded cost, enforce termination and
|
||||
verify resource cleanup. Transfer only authorized public licensed data and
|
||||
reviewed source. A future run needs its own applicable authorization.
|
||||
|
||||
For the current run, the authorized cap is USD 200, local RTX 5080 capacity has
|
||||
been sufficient and the recorded spend is USD 0. The existing schedule ends
|
||||
September 30, 2026 at 07:00 America/Toronto. Its final wake performs the frozen
|
||||
evaluation, reconciles owned jobs/resources and the ledger, records results,
|
||||
then pauses the heartbeat. These are run-specific values, not durable spending
|
||||
or scheduling authority granted by this ADR.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Multiple controllers per GPU, detached indefinite loops, and treating a
|
||||
registration response as an executed agent task make ownership unverifiable.
|
||||
Retain explicit owners, finite jobs and execution receipts instead.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
Parallel review remains useful while resource mutation and final evaluation
|
||||
have one accountable owner. Receipts support recovery without silent reruns.
|
||||
|
||||
### Negative
|
||||
|
||||
Serial GPU ownership can leave capacity unused. Federation or memory outages
|
||||
reduce coordination features, and bounded jobs may need reviewed continuation.
|
||||
|
||||
### Neutral
|
||||
|
||||
Source and sanitized documentation belong in the isolated worktree. Raw RF
|
||||
data, subject recordings, weights, logs, credentials and transcripts stay outside
|
||||
Git. A completed experiment does not authorize model promotion, firmware
|
||||
flashing, merging or publication.
|
||||
|
||||
## Validation and links
|
||||
|
||||
Run `test_run_batch.py` and `test_codex_iteration.py` under `tools/bfi_learning`.
|
||||
Audit real execution/cleanup receipts, lock owners, ledger and scheduler state
|
||||
at completion; successful synthetic tests alone cannot establish these outcomes.
|
||||
|
||||
- Preserves [ADR-283](ADR-283-ruview-community-metaharness-flywheel.md)'s
|
||||
proposal-only learning and maintainer promotion boundary.
|
||||
- Follows [ADR-321](ADR-321-decision-policy-action-authorization.md).
|
||||
- Depends on [ADR-366](ADR-366-csi-controls-and-frozen-evaluation.md) for frozen
|
||||
candidate selection and final holdout handling.
|
||||
@@ -0,0 +1,187 @@
|
||||
# ADR-368: RuView compute kernel as a WASM + napi-rs npm package
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: npm, wasm, napi-rs, metaharness, vitals, supply-chain
|
||||
- **Related**: ADR-021 (vitals), ADR-263/265 (npm harness review and
|
||||
distribution), ADR-283 (community metaharness), ADR-285 (WASM-first Homecore
|
||||
metaharness)
|
||||
|
||||
## Context
|
||||
|
||||
`@ruvnet/ruview` (ADR-283) is a dependency-free operator/contributor harness:
|
||||
it guides, verifies, and delegates, but it cannot run RuView signal processing.
|
||||
JavaScript consumers (MCP hosts, dashboards, browser demos, agents) that want
|
||||
real RuView computation must today start the Rust sensing server or re-implement
|
||||
DSP in JS, which drifts from the Rust source of truth.
|
||||
|
||||
We want one Rust implementation reachable from npm with:
|
||||
|
||||
1. a portable default that works on every Node platform and in browsers;
|
||||
2. an optional native path for hosts that want it;
|
||||
3. the metaharness trust posture: least authority by default, the backend that
|
||||
actually ran is always reported, fail-closed on missing or tampered
|
||||
artifacts, and no accuracy claim without an evidence tag.
|
||||
|
||||
## Decision
|
||||
|
||||
### One core, one ABI, two transports
|
||||
|
||||
- **`v2/crates/ruview-kernel`** (workspace member) wraps the ADR-021 pipeline
|
||||
from `wifi-densepose-vitals` (EMA preprocessing, breathing 0.1–0.5 Hz, heart
|
||||
0.8–2.0 Hz, anomaly z-score) in a streaming `Session`, a deterministic
|
||||
SplitMix64 synthetic-CSI generator, and a bounded session registry.
|
||||
- The entire ABI is one function: `call(op, request_json) -> envelope_json`,
|
||||
returning `{"ok":true,"result":…}` or
|
||||
`{"ok":false,"error":{"code","message"}}`. Operations: `info`,
|
||||
`validate_config`, `analyze`, `synthesize`, `session_open|push|summary|close`.
|
||||
Keeping the ABI to a single string function makes backend parity a property
|
||||
of the transport rather than of duplicated bindings.
|
||||
- **WASM transport** (`src/wasm_abi.rs`, `wasm32-unknown-unknown` only):
|
||||
`rvk_alloc`/`rvk_free`/`rvk_call`/`rvk_abi_version`. The module has **zero
|
||||
imports** — no filesystem, network, clock, or randomness. The JS loader
|
||||
refuses any module that imports host functions.
|
||||
- **napi-rs transport** (`v2/crates/ruview-kernel-napi`): exports only
|
||||
`abiVersion()` and `call(op, json)`. It is a standalone Cargo workspace
|
||||
(listed in the v2 `exclude`) so napi never enters
|
||||
`cargo test --workspace`, and it builds with `panic = "unwind"` plus
|
||||
`catch_unwind` so a defect becomes an error envelope, not a Node crash.
|
||||
|
||||
### Input is untrusted at the ABI
|
||||
|
||||
Requests are bounded before processing: 16 MiB per request, 512 subcarriers,
|
||||
100 000 frames per call or synthesis, 64 concurrent sessions. Unknown JSON
|
||||
fields are rejected (`deny_unknown_fields`); frame widths must match the
|
||||
configured subcarrier count; values must be finite with magnitude ≤ 1e9; all
|
||||
frames of a batch are validated before any is processed. The core returns an
|
||||
error envelope instead of panicking on caller data (release builds of the WASM
|
||||
module use `panic = "abort"`, which the loader surfaces as a `trap` and
|
||||
recovers from with a fresh instance).
|
||||
|
||||
### npm package `@ruvnet/ruview-kernel` (`harness/ruview-kernel`)
|
||||
|
||||
- Zero runtime dependencies. `loadKernel({backend})` with
|
||||
`backend ∈ {wasm (default), napi, auto}` or `RUVIEW_KERNEL_BACKEND`.
|
||||
WASM is the default because it carries no host authority. `napi` never
|
||||
silently falls back; `auto` prefers napi and records `fallbackReason`.
|
||||
- Artifacts are built from source by `npm run build` (`scripts/build.mjs`,
|
||||
no shell) into `wasm/` and `native/ruview_kernel.<triple>.node`, with
|
||||
`SHA256SUMS`. The loader verifies checksums; a mismatch is always fatal and
|
||||
never an `auto` fallback trigger; `requireIntegrity` also refuses
|
||||
unchecksummed artifacts. Built artifacts are git-ignored and never committed.
|
||||
- `src/wasm.js` is filesystem-free, so browsers can use
|
||||
`loadWasmKernelFromBytes(bytes)`.
|
||||
- CLI `ruview-kernel`: `doctor`, `info`, `selftest`, `parity`, `synth`,
|
||||
`analyze --file`, `bench`. `synth --out` refuses to overwrite files;
|
||||
`analyze` reads only a bounded regular file.
|
||||
- `selfTest()` runs synthesize → analyze → chunked streaming session → hostile
|
||||
input, and asserts known-tone recovery. `parity()` diffs every output of
|
||||
both backends with a 1e-9 relative tolerance, because `sin`/`exp` come from
|
||||
different libm implementations on wasm32 and native targets; exact JSON
|
||||
equality is reported separately and is not required.
|
||||
|
||||
### Metaharness integration
|
||||
|
||||
`@ruvnet/ruview` gains the read-only tool `ruview_kernel_selftest`
|
||||
(CLI: `ruview kernel [--backend …]`). It dynamically imports the fixed
|
||||
specifier `@ruvnet/ruview-kernel`, so `@ruvnet/ruview` keeps zero runtime
|
||||
dependencies, and it returns `kernel_not_installed` when the package is absent.
|
||||
MCP arguments are limited to `backend` and `seconds`; they cannot choose a
|
||||
module or artifact path. The capability appears in `ruview_guidance` as
|
||||
`portable-compute-kernel`.
|
||||
|
||||
## Evidence
|
||||
|
||||
- **SYNTHETIC** (`cd harness/ruview-kernel && node bin/cli.js selftest`):
|
||||
60 s at 20 Hz, 56 subcarriers, 15 bpm breathing and 72 bpm heart tones →
|
||||
recovered 15.0 bpm respiratory (valid) and 70.6 bpm heart (degraded
|
||||
confidence 0.44). Tolerances asserted: ±3 bpm respiratory, ±8 bpm heart.
|
||||
- **SYNTHETIC** parity (`node bin/cli.js parity`): 900 frames / 45 readings,
|
||||
no difference above 1e-9 relative between wasm32 and linux-x64-gnu napi;
|
||||
outputs are not byte-identical.
|
||||
- **MEASURED** on one Linux x64 development container
|
||||
(`node bin/cli.js bench --backend wasm|napi --seconds 120`): about 78 ms
|
||||
(wasm) and 63 ms (napi) per 2 400-frame `analyze`, including JSON transfer.
|
||||
Single-host timing, not a cross-platform claim.
|
||||
- The WASM release artifact is about 230 KB and imports nothing
|
||||
(`node scripts/verify-artifacts.mjs --require-wasm`).
|
||||
|
||||
None of these numbers says anything about real-world vital-sign accuracy.
|
||||
That still requires the ADR-293 ground-truth protocol with a reference device.
|
||||
|
||||
## Amendment 1 (2026-10-01): binary transport and optimization evidence
|
||||
|
||||
**Profile (MEASURED on one Linux x64 development container, 2 400 frames ×
|
||||
56 subcarriers).** JSON was the dominant cost:
|
||||
|
||||
- JS `JSON.stringify` of the request alone took 52–78 ms against
|
||||
107–124 ms for a whole `analyze`.
|
||||
- Rust then re-parsed about 270 000 numbers.
|
||||
|
||||
**Decision.** Add a binary fast path without changing the ABI version.
|
||||
|
||||
- New operations `analyze_flat` and `session_push_flat`, carried by
|
||||
`call_f64(op, json, &[f64])`:
|
||||
- WASM: `rvk_call_f64`, which reads bytes so alignment does not matter;
|
||||
- napi: `callF64(op, json, Float64Array)`.
|
||||
- `analyze()` and `session.push()` pack uniform frames into one
|
||||
`Float64Array` (all amplitudes, then all phases) and use the binary path
|
||||
automatically. Irregular input still takes the JSON path so its error names
|
||||
the bad frame.
|
||||
- `analyzeFlat(Float64Array, …)` is the zero-copy entry.
|
||||
- `Session::step` is shared by both paths.
|
||||
|
||||
**Evidence.**
|
||||
|
||||
- **Byte-identical outputs.** On identical `f64` inputs the two paths
|
||||
produce byte-identical JSON (`tests/abi.rs`).
|
||||
- **A JSON precision finding.** serde_json's default float parser is not
|
||||
correctly rounded, so the JSON transport perturbs inputs by ULPs (34 of 160
|
||||
amplitude values in a 2 s, 4-subcarrier SYNTHETIC sample). The binary path is bit-exact.
|
||||
- **Speed.** MEASURED, `ruview-kernel bench --seconds 120`, same host:
|
||||
|
||||
| Backend | JSON transport | Binary transport | Speed-up |
|
||||
|---|---|---|---|
|
||||
| wasm | 134.8 ms | 47.8 ms (~50 000 frames/s) | 2.82× |
|
||||
| napi | 109.7 ms | 44.8 ms (~53 600 frames/s) | 2.45× |
|
||||
|
||||
- **Remaining cost is DSP.** Native timing gives heart-rate extraction
|
||||
20.8–22.9 ms of 30.6–38.3 ms (about 60–68%, MEASURED; reproducer:
|
||||
`cargo run --release -p ruview-kernel --example profile_stages`): the per-frame autocorrelation over the 15 s window
|
||||
in `wifi-densepose-vitals`. That crate is shared with the production sensing
|
||||
server, and an incremental ACF would change floating-point summation order
|
||||
and therefore outputs. It is left as a follow-up that needs its own review
|
||||
and regression evidence.
|
||||
|
||||
**Rejected after measurement.** MEASURED, three runs on the same host;
|
||||
reproducer: `node bench/wasm-variants.mjs` in `harness/ruview-kernel`. Each
|
||||
run is the median of 15 `analyzeFlat` calls on 2 400 frames. All variants
|
||||
produced byte-identical outputs.
|
||||
|
||||
| Variant | Size | Median ms (3 runs) | Decision |
|
||||
|---|---|---|---|
|
||||
| baseline (`opt-level = 3`) | 244 KB | 38.3–40.6 | kept |
|
||||
| `+simd128` | 242 KB | 37.4–54.7 | rejected: no consistent gain, and it needs SIMD-capable engines |
|
||||
| `opt-level = "s"` | 227 KB | 40.8–70.1 | rejected: consistently slower for a 17 KB size saving |
|
||||
|
||||
## Consequences
|
||||
|
||||
- JavaScript consumers get real RuView DSP without a server, in Node or the
|
||||
browser, with one Rust source of truth.
|
||||
- A second native build matrix exists. Until per-platform packages
|
||||
(`@ruvnet/ruview-kernel-<triple>`) are published by CI, only WASM is
|
||||
portable; the loader already resolves those package names.
|
||||
- JSON transfer dominates the cost of small calls; a typed-array fast path can
|
||||
be added later behind a new ABI version.
|
||||
|
||||
## Rollout and rollback
|
||||
|
||||
1. CI (`ruview-kernel-npm.yml`) builds both transports from source, runs the
|
||||
Rust tests, the package tests with `RUVIEW_KERNEL_REQUIRE_NAPI=1`, parity,
|
||||
artifact verification, and `npm pack --dry-run`.
|
||||
2. Publication stays CI-only with provenance (ADR-265) and requires explicit
|
||||
maintainer approval; this ADR does not add a publish job.
|
||||
3. Rollback: remove the `ruview_kernel_selftest` tool (the harness has no
|
||||
dependency on the package) and deprecate the npm version; the Rust crates
|
||||
are additive.
|
||||
@@ -0,0 +1,74 @@
|
||||
# ADR-369: npm RuView operator surface — CLI, MCP, and SDK over one registry
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: npm, mcp, sdk, cli, metaharness, least-authority
|
||||
- **Related**: ADR-263/265 (npm harness review and distribution), ADR-283
|
||||
(community metaharness), ADR-368 (compute kernel), ADR-370 (flashing),
|
||||
ADR-371 (training), ADR-372 (doctor)
|
||||
|
||||
## Context
|
||||
|
||||
`@ruvnet/ruview` 0.5.x could onboard, lint claims, verify the proof, monitor a
|
||||
node, and read Cognitum Spaces. Three operator jobs still left the package:
|
||||
|
||||
- **Flashing** was a Windows-only stub that returned `manual_step_required`.
|
||||
- **Training** had only a skill document; nothing ran a trainer or checked
|
||||
results mechanically.
|
||||
- **Diagnostics**: `doctor` printed six PASS/FAIL lines without remedies.
|
||||
|
||||
There was also no programmatic API. Integrators had to shell out to the CLI or
|
||||
speak MCP.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **One registry, three front ends.** Every capability is a `ruview_*` tool
|
||||
in `src/tools.js`, with a JSON schema, a `TOOL_POLICY` class, and a
|
||||
structured, fail-closed result. The CLI verbs, the MCP server, and the new
|
||||
SDK all go through `runTool`. There is one argument validator and one
|
||||
authorization path.
|
||||
2. **SDK** (`@ruvnet/ruview/sdk`, typed by `src/sdk.d.ts`). `createRuView()`
|
||||
returns a frozen object with `doctor`, `firmware.{ports,plan,flash,monitor}`,
|
||||
`training.{plan,run,gate,calibrate}`, `kernel.{selfTest,load}`, `guidance`,
|
||||
`claimCheck`, `verify`, `spaces`, `memorySearch`, `call`, `tools`, and
|
||||
`startMcpServer`.
|
||||
- Calls run with `source: 'sdk'`: trusted in-process code, like the CLI.
|
||||
Mutating tools still refuse to act without `confirm: true`.
|
||||
- `{ strict: true }` turns `ok:false` into a `RuViewError`.
|
||||
3. **Plan/act split for every mutation.**
|
||||
- `ruview_firmware_plan` and `ruview_train_plan` are read-only and their
|
||||
schemas do not accept `confirm`. An MCP client without write grants can
|
||||
review the exact command.
|
||||
- The acting tools (`ruview_node_flash`, `ruview_train`) keep the existing
|
||||
grant-plus-confirm gate: `hardware-write` and `workspace-write`.
|
||||
4. **New tools**: `ruview_doctor`, `ruview_firmware_plan`,
|
||||
`ruview_firmware_ports`, `ruview_train`, `ruview_train_plan`,
|
||||
`ruview_train_gate`. `ruview_node_flash` is re-implemented (ADR-370).
|
||||
`TOOL_POLICY`, `.harness/claims.json`, and `.harness/mcp-policy.json` are
|
||||
kept in lock-step, and a test fails on any drift.
|
||||
5. **Process execution** for operator tools goes through `src/exec.js`:
|
||||
- argv only, never a shell;
|
||||
- a scrubbed environment: the base allowlist plus toolchain locations such
|
||||
as `LIBTORCH`, `CARGO_HOME`, `VIRTUAL_ENV` and `IDF_PATH`, and no
|
||||
credentials;
|
||||
- bounded output, and secrets redacted.
|
||||
6. **Zero runtime dependencies stays.** The optional compute kernel
|
||||
(ADR-368) is loaded dynamically. Version 0.6.0. The unpacked-size ceiling
|
||||
rises from 160 KiB to 224 KiB for the new modules, with the budget recorded
|
||||
in both npm workflows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Agents, scripts, and humans get the same behaviour and the same refusals.
|
||||
- The package now drives real hardware and training. Keeping the plan/act
|
||||
split and argv-only execution is essential, and tests pin both.
|
||||
- The surface is larger (16 MCP tools). Hosts that cap tool counts can
|
||||
filter by annotations (`readOnlyHint`).
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `cd harness/ruview && npm test && npm run test:security` (drift test in
|
||||
`test/sdk.test.mjs`).
|
||||
- `npx @ruvnet/ruview tools` lists 16 tools. `node -e "import('@ruvnet/ruview/sdk')"`
|
||||
resolves from an installed tarball.
|
||||
@@ -0,0 +1,83 @@
|
||||
# ADR-370: Cross-platform, verified ESP32 firmware flashing from npm
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: firmware, esp32, esptool, supply-chain, hardware-evidence
|
||||
- **Related**: ADR-028 (capability audit / witness), ADR-369, firmware
|
||||
`README.md` §0 and §2
|
||||
|
||||
## Context
|
||||
|
||||
The documented flash procedure is four `esptool write_flash` offsets. It is
|
||||
typed by hand, and a wrong chip, wrong flash size, or tampered image can brick
|
||||
a board or silently run the wrong firmware. The previous `ruview_node_flash`
|
||||
was Windows-only and never flashed. Project rules also say that a successful
|
||||
build or write is **not** hardware validation; that requires a captured
|
||||
boot/runtime log from real silicon.
|
||||
|
||||
## Decision
|
||||
|
||||
`src/firmware.js` implements plan → verify → preflight → write → evidence:
|
||||
|
||||
1. **Variants**:
|
||||
- `s3-8mb` → `esp32s3` / 8MB
|
||||
- `s3-4mb` → `esp32s3` / 4MB; prefers `*-4mb.bin` images
|
||||
- `c6` → `esp32c6` / 4MB
|
||||
|
||||
Offsets follow the shipped partition tables: bootloader `0x0`, partition
|
||||
table `0x8000`, optional OTA data `0xf000`, app `0x20000`. NVS is never
|
||||
written, so WiFi and node configuration survive.
|
||||
2. **Bundle verification**. A bundle is an extracted release flash bundle,
|
||||
`release_bins/<variant>`, or an ESP-IDF `build/` directory.
|
||||
- Every image must match `SHA256SUMS` or `SHA256SUMS.txt`. A mismatch is
|
||||
always fatal.
|
||||
- Missing checksums are refused unless `allow_unverified` is set, which is
|
||||
for local builds only.
|
||||
- Images must be regular, non-empty files of at most 16 MiB, inside the
|
||||
bundle; symlinks that escape the bundle are refused.
|
||||
- An 8 MB variant refuses a 4 MB partition table.
|
||||
- A bundle without OTA data produces an explicit warning (a node on `ota_1`
|
||||
keeps booting `ota_1`).
|
||||
3. **Argv, not shell.**
|
||||
- The port must match `COM<n>` or `/dev/(tty|cu)*`.
|
||||
- Baud comes from a fixed set.
|
||||
- The command is `python -m esptool --chip … --port … --baud … write_flash
|
||||
--flash_mode dio --flash_size … <offset> <file>…`. The underscore forms
|
||||
work in both esptool 4.x and 5.x.
|
||||
4. **Chip preflight.** `esptool chip_id` must report the variant's chip. The
|
||||
parser covers the 4.x "Chip is" and 5.x "Chip type:" / "Detecting chip
|
||||
type…" formats. A mismatch or missing response stops the flow before any
|
||||
write.
|
||||
5. **Evidence.** After a write, a read-only pyserial capture (default 15 s,
|
||||
port and duration passed as argv) is summarized: line count, CSI
|
||||
callbacks, MGMT+DATA upgrade, panic markers, firmware version.
|
||||
- `hardwareValidated: true` only when CSI callbacks appear with no panic;
|
||||
the result's `evidence` is then `MEASURED` with a boot-log reason.
|
||||
- Otherwise the result states the board is not validated.
|
||||
6. **Authority.** Without `confirm` the result is a plan. MCP needs the
|
||||
`hardware-write` grant plus `confirm`. `ruview_firmware_plan` and
|
||||
`ruview_firmware_ports` are read-only.
|
||||
7. **Provisioning stays out.** WiFi credentials remain in `provision.py`, so
|
||||
they never enter harness arguments, logs, or MCP transcripts.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `npx @ruvnet/ruview doctor --group firmware` verified all four checked-in
|
||||
`release_bins` bundles (c6-adr110, c6-onboarding, s3-adr110,
|
||||
s3-fair-adr110) against their `SHA256SUMS.txt` in this change's development
|
||||
container.
|
||||
- The write and preflight paths are covered by tests with injected fake
|
||||
hardware: mismatch refusal, probe failure, flash failure, boot-log
|
||||
validation, and a silent log that yields no validation.
|
||||
- **No real board was flashed** during this change, so real-silicon validation
|
||||
is outstanding.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Operators on any OS get one verified command. Agents can review plans
|
||||
without hardware authority.
|
||||
- The workflow depends on Python, esptool, and pyserial. `doctor` reports each
|
||||
one with a fix.
|
||||
- Follow-up: an ADR-028 witness record could store the boot-log summary and
|
||||
image digests.
|
||||
@@ -0,0 +1,91 @@
|
||||
# ADR-371: Harness training runner and mean-pose evidence gate
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: training, evaluation, evidence, pose, honesty
|
||||
- **Related**: ADR-079 (camera-supervised pose), ADR-151 (room calibration),
|
||||
ADR-181 (baseline-relative PCK), ADR-369
|
||||
|
||||
## Context
|
||||
|
||||
RuView has a retracted pose result (a CLAIMED 92.9%/100% that did not survive
|
||||
baseline and leakage review). The project rule is that pose
|
||||
PCK is quoted only with the mean-pose baseline, on a leakage-free held-out
|
||||
split, with an evidence tag. The `train-pose` skill says this in prose, but
|
||||
nothing enforces it. Running the trainers also needs repository knowledge:
|
||||
the `tch-backend` feature, a libtorch version match, and where checkpoints go.
|
||||
|
||||
## Decision
|
||||
|
||||
### Gate (`ruview_train_gate`, pure, read-only)
|
||||
|
||||
The input is a small report: metric, model and baseline score, split,
|
||||
train/test subjects, train end and test start, `n_test`, reproducer, and data
|
||||
source.
|
||||
|
||||
**Hard failures:**
|
||||
- no baseline;
|
||||
- a random-frame split, or any split outside chronological / blocked-gap /
|
||||
grouped-subject / grouped-session;
|
||||
- subject overlap in a grouped-subject split;
|
||||
- missing or overlapping time bounds for chronological or blocked-gap splits;
|
||||
- no improvement over the baseline.
|
||||
|
||||
**Warnings:**
|
||||
- subjects not reported;
|
||||
- subjects overlapping in non-subject splits;
|
||||
- `n_test` < 100;
|
||||
- a near-perfect score: at or above the `implausible_score` threshold in
|
||||
`src/training.js`.
|
||||
|
||||
**Evidence tag:** `SYNTHETIC` when `data: synthetic`, `MEASURED` only with a
|
||||
reproducer, otherwise `CLAIMED`.
|
||||
|
||||
On PASS the gate returns the one acceptable sentence:
|
||||
`Held-out <metric> X% vs Y% mean-pose baseline = +Z pp (<tag>, …, <split> split).`
|
||||
|
||||
### Runner (`ruview_train`, `ruview_train_plan`)
|
||||
|
||||
Modes:
|
||||
- `pose-smoke`: `wifi-densepose-train` `train --dry-run` on the synthetic
|
||||
dataset.
|
||||
- `pose`: `--data-dir`, MM-Fi recordings.
|
||||
- `room`: `wifi-densepose train-room`, or `cargo run -p wifi-densepose-cli`
|
||||
when the binary is absent.
|
||||
|
||||
Rules:
|
||||
- All caller paths (config, data, checkpoints, enrollment, output) must
|
||||
resolve inside the trusted checkout. Realpaths are checked for inputs that
|
||||
already exist.
|
||||
- Pose runs use `cargo run --manifest-path v2/Cargo.toml` with the working
|
||||
directory set to the checkpoint directory, default
|
||||
`v2/target/ruview-train/<mode>`. The trainer's relative `logs/` therefore
|
||||
never lands in the source tree.
|
||||
- Without `confirm` it returns a plan. MCP requires `workspace-write` plus
|
||||
`confirm`.
|
||||
- Failures are classified with remedies:
|
||||
- libtorch absent;
|
||||
- libtorch version mismatch (tch 0.24 expects torch 2.11.0);
|
||||
- link failure.
|
||||
|
||||
## Evidence
|
||||
|
||||
- **SYNTHETIC** (`npx @ruvnet/ruview train --mode pose-smoke --samples 16
|
||||
--confirm`, CPU PyTorch 2.11.0, `LIBTORCH_USE_PYTORCH=1`, Linux x64): the
|
||||
real trainer compiled, ran 20 epochs with early stopping, and wrote
|
||||
safetensors checkpoints and `logs/training_log.csv` under
|
||||
`v2/target/ruview-train/pose-smoke`. The trainer labels its own smoke
|
||||
metrics "DO NOT REPORT". None are reported here.
|
||||
- With PyTorch 2.14.1 the tch 0.24 C++ shim failed to compile. The runner
|
||||
classified this as a version mismatch rather than a missing install.
|
||||
- The gate is covered by `test/training.test.mjs`, including the leakage
|
||||
patterns behind the retraction.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A number can pass through the harness only with its baseline and split
|
||||
attached. The gate does not verify the arithmetic that produced the input;
|
||||
reviewers must still re-run the reproducer.
|
||||
- Training needs the Rust toolchain and a matching libtorch. `doctor` covers
|
||||
Rust; libtorch is detected at run time.
|
||||
@@ -0,0 +1,66 @@
|
||||
# ADR-372: Structured debugging doctor
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: diagnostics, onboarding, support, mcp
|
||||
- **Related**: ADR-369, ADR-370, ADR-371, docs/TROUBLESHOOTING.md
|
||||
|
||||
## Context
|
||||
|
||||
Most failed RuView setups come down to a missing driver, pyserial, esptool, a
|
||||
wrong port, uninitialized submodules, a missing wasm32 target, or a sensing
|
||||
server that is not running. The old `doctor` checked only the harness itself,
|
||||
gave no remedies, and had no machine-readable output for agents.
|
||||
|
||||
## Decision
|
||||
|
||||
`src/doctor.js` returns
|
||||
`{ok, summary{pass,warn,fail,skip}, checks[{group,id,status,detail,remedy}], nextSteps}`.
|
||||
|
||||
Groups:
|
||||
|
||||
| Group | Checks |
|
||||
|---|---|
|
||||
| `runtime` | Node ≥ 20 |
|
||||
| `harness` | packaged-file manifest integrity, claim-guardrail self-test, reviewed brain load |
|
||||
| `hosts` | `claude` / `codex` on PATH |
|
||||
| `repo` | checkout, workspace submodules |
|
||||
| `rust` | cargo, wasm32 target (run inside `v2/` so the pinned toolchain is used), `wifi-densepose` binary |
|
||||
| `python` | interpreter, pyserial, esptool version |
|
||||
| `firmware` | checksum verification of every `release_bins` bundle |
|
||||
| `serial` | pyserial port enumeration; optional named port; optional chip probe |
|
||||
| `sensing` | optional sensing-server `/health` |
|
||||
| `kernel` | optional `@ruvnet/ruview-kernel` WASM/napi status |
|
||||
|
||||
Rules:
|
||||
|
||||
- `fail` is reserved for a requested capability that cannot work. Absent
|
||||
optional tooling is `warn`. Every non-pass check carries a remedy. The CLI
|
||||
exit code is non-zero only on `fail`.
|
||||
- The doctor is read-only. Two exceptions exist, both CLI/SDK options that are
|
||||
absent from the MCP schema:
|
||||
- `--probe` runs `esptool chip_id`, which resets the board but writes
|
||||
nothing.
|
||||
- `--url` makes an HTTP(S) request.
|
||||
- Output is human-readable by default. `--json` gives the structured report.
|
||||
`--group a,b` limits the scope.
|
||||
|
||||
## Evidence
|
||||
|
||||
Development container (Linux x64, Node 22):
|
||||
|
||||
- Before `pip install pyserial esptool`, the doctor reported them as `warn`
|
||||
with the exact pip commands.
|
||||
- After installation it showed `esptool 5.4.0` and enumerated `/dev/ttyS0`.
|
||||
- All four firmware bundles verified.
|
||||
- It caught manifest drift during development, and a missing wasm32 target
|
||||
when run outside `v2/`. That second case led to the `v2/` working-directory
|
||||
rule.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Support threads can ask for `npx @ruvnet/ruview doctor --json`, and agents
|
||||
can act on `nextSteps`.
|
||||
- Checks run real subprocesses, so a full run takes seconds; `--group` keeps
|
||||
targeted runs fast.
|
||||
@@ -0,0 +1,244 @@
|
||||
# ADR-373: Host device access layer — ESP32, mmWave, LiDAR
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: hardware, esp32, mmwave, lidar, serial, udp, mcp, least-authority
|
||||
- **Related**: ADR-018 (CSI frame), ADR-063 (mmWave fusion), ADR-320
|
||||
(sensor HAL), ADR-369 (npm operator surface), ADR-370 (flashing),
|
||||
ADR-374 (remote hosts), `integrations/iphone-lidar`
|
||||
|
||||
## Context
|
||||
|
||||
Operators and agents need to answer basic hardware questions, such as "is the
|
||||
node streaming?", "is the radar talking?" and "does the LiDAR see the room?",
|
||||
before calibration, fusion or training. Until now each modality had its own
|
||||
tool:
|
||||
|
||||
- the ESP32 serial monitor;
|
||||
- `scripts/mmwave_fusion_bridge.py`;
|
||||
- the iPhone LiDAR web relay;
|
||||
- the sensing server itself.
|
||||
|
||||
None of these was reachable through the npm harness, its MCP server or its
|
||||
SDK.
|
||||
|
||||
The package must stay free of runtime dependencies (ADR-263). Node has no
|
||||
serial API. Hardware access is also authority: an MCP client should not get
|
||||
it by default.
|
||||
|
||||
## Decision
|
||||
|
||||
Add `src/devices/` to `@ruvnet/ruview` (0.7.0) with four read-oriented tools.
|
||||
|
||||
| Tool | Transport | What it returns |
|
||||
|---|---|---|
|
||||
| `ruview_devices_scan` | pyserial port list | ports with USB VID:PID, bridge chip, likely roles (esp32 / mmwave / rplidar), and the command that confirms each role |
|
||||
| `ruview_esp32_capture` | `node:dgram` UDP, receive-only | per node: packet counts by kind (magics `0xC5110001`–`0xC5110007`), CSI rate, sequence loss, RSSI, antennas/subcarriers/frequency, latest device vitals |
|
||||
| `ruview_mmwave_read` | serial | MR60BHA2 (60 GHz, 115200) and LD2410 (24 GHz, 256000) frames, checksum errors, presence, distance, device-reported heart and breathing rate |
|
||||
| `ruview_lidar_read` | serial or WebSocket | RPLIDAR SCAN summary (points, revolutions, range, angular coverage), or iPhone relay depth statistics |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **Firmware-identical parsing.**
|
||||
- The MR60BHA2 and LD2410 parsers are ports of
|
||||
`firmware/esp32-csi-node/main/mmwave_sensor.c`: same SOF, `~XOR` header
|
||||
and data checksums, 30-byte payload cap, and F4F3F2F1…F8F7F6F5 framing.
|
||||
- ESP32 packets are parsed as the little-endian structs in `csi_collector`
|
||||
and `edge_processing.h`.
|
||||
- Host and node therefore agree on what a valid frame is.
|
||||
2. **Serial without dependencies.** A fixed Python script (the pyserial
|
||||
"pump") reads the port for a bounded time and number of bytes.
|
||||
- Port, baud, duration and command bytes are passed as argv and validated
|
||||
first: the port regex, a fixed baud set, and command bytes of at most 64
|
||||
hex bytes.
|
||||
- Output is lowercase hex, so the secret-redaction layer cannot corrupt
|
||||
binary data.
|
||||
- DTR control is best-effort and reported when a port does not support it.
|
||||
3. **Receive-only network.**
|
||||
- The ESP32 capture binds a local UDP port (bind address from a fixed set,
|
||||
port ≥ 1024) and never sends to a node.
|
||||
- `EADDRINUSE` is reported as "sensing server already bound".
|
||||
4. **Minimal actuation.** An RPLIDAR scan sends `A5 20` (start) and `A5 25`
|
||||
(stop), and drives DTR low (the A1 motor). It spins the motor but writes no
|
||||
configuration. Nothing else is written to any device.
|
||||
5. **No raw sensor data in results.**
|
||||
- iPhone depth frames are validated with the same rules as
|
||||
`integrations/iphone-lidar/web/codec.mjs` and reduced to statistics.
|
||||
- The relay token comes only from `RUVIEW_LIDAR_TOKEN`. It is rejected in
|
||||
the URL and never echoed.
|
||||
6. **Authority.**
|
||||
- All four tools require the new `device-access` MCP grant.
|
||||
- The CLI and SDK run them directly, as trusted local code.
|
||||
- Serial port names are pattern-checked in the schema and again in the
|
||||
driver. `/dev/pts/*` is deliberately excluded: it could read another
|
||||
terminal's input.
|
||||
7. **Evidence.**
|
||||
- A successful read is tagged `MEASURED` as a *reading* on this host.
|
||||
- mmWave vitals are labelled as the radar's own estimates.
|
||||
- None of these outputs is an accuracy claim.
|
||||
|
||||
## Evidence
|
||||
|
||||
Development container, Linux x64, Node 22. No physical sensors were attached.
|
||||
|
||||
- **ESP32 UDP**: a simulated node sent 60 ADR-018 CSI frames plus 3 vitals
|
||||
packets to `127.0.0.1:5005` through `ruview esp32`.
|
||||
- Result: 1 node, CSI rate 15 Hz, zero sequence loss, vitals decoded
|
||||
(15.2 bpm / 69 bpm, SYNTHETIC).
|
||||
- Unit test on real loopback: 2 dropped sequence numbers out of 7 produce a
|
||||
loss fraction of 0.2857.
|
||||
- **MR60BHA2**, emulated on a pseudo-terminal and read through real pyserial:
|
||||
- auto-detected `mr60bha2`;
|
||||
- 200 frames, 0 checksum errors;
|
||||
- presence 1.0, distance 87 cm, 14.8 / 68.5 bpm (the emulator's values,
|
||||
SYNTHETIC).
|
||||
- **RPLIDAR**, emulated on a pseudo-terminal that answers `A5 20`:
|
||||
- 3564 points, 99 revolutions, full angular coverage;
|
||||
- the missing DTR support was reported.
|
||||
- **iPhone LiDAR**, the real relay (`integrations/iphone-lidar/web/relay.mjs`)
|
||||
with a simulated phone:
|
||||
- 30 frames at 10 fps, median depth 1.824 m (SYNTHETIC);
|
||||
- a wrong token gives `connect_failed` with a remedy.
|
||||
- **Not yet done**: real ESP32-S3/C6, MR60BHA2, LD2410, RPLIDAR and iPhone
|
||||
captures. Each modality needs one real capture before its parser is called
|
||||
hardware-validated.
|
||||
|
||||
## Amendment 1 (2026-10-01): e2e suite and latency
|
||||
|
||||
- `harness/ruview/test/e2e/devices.e2e.mjs` (`npm run test:e2e:devices`, CI
|
||||
workflow `ruview-device-e2e.yml`) automates the emulated-device runs above
|
||||
through the real CLI:
|
||||
- MR60BHA2 and RPLIDAR on pseudo-terminals, read via real pyserial;
|
||||
- an ESP32 UDP node;
|
||||
- the real iPhone relay.
|
||||
- The harness still refuses `/dev/pts/*` port names. The suite links each pty
|
||||
to `/dev/ttyRUVIEWE2E<n>` with root or `sudo -n`.
|
||||
- `RUVIEW_E2E_REQUIRE=1` turns missing prerequisites into failures.
|
||||
- mmWave auto-detect keeps its probe frames (≤ 2 s per model) and reads only
|
||||
the remainder of the window. A 5 s request previously cost about 9 s.
|
||||
- `ruview_lidar_read` (iPhone) gains `max_frames` and returns immediately on
|
||||
a refused connection.
|
||||
- MEASURED wall time for the device e2e suite on the development container:
|
||||
19.6 s before these changes, 5.6 s after (concurrent cases, no redundant
|
||||
re-reads).
|
||||
|
||||
## Amendment 2 (2026-10-01): Realtek nodes, honest capture, live analysis
|
||||
|
||||
Driven by a hardware run of this branch against an RTL8721Dx board (PL2303GC,
|
||||
COM10) and an ESP32-C6 (CP210x, COM16) on Windows 11:
|
||||
|
||||
- **Realtek RAC1/RHB1 (ADR-323).** `ruview_esp32_capture` decodes the 49-byte
|
||||
RAC1 envelope (8- and 16-bit tones, CRC-32 verified, SYNTHETIC flag kept)
|
||||
and counts RHB1 heartbeats per sender. Before this, 3,439 live RAC1 frames
|
||||
in 15 s were all reported as unknown.
|
||||
- **No false success.** The capture now fails with `no_decodable_packets`
|
||||
or `heartbeat_only` when packets arrive but none decode. It previously
|
||||
returned `ok: true` with a MEASURED label for zero decoded packets.
|
||||
`heartbeatOnlySenders` names boards that are alive but deliver no CSI — the
|
||||
stall observed on a Realtek board before a reset.
|
||||
- **More ESP32 packet kinds.** ADR-110 sync (`0xC511A110`) and ADR-081 mesh
|
||||
envelopes (`0xC5118100`, counted by message type, never a phantom node).
|
||||
A node sending several CSI shapes (the C6 sends 1x256, 1x64 and 1x128)
|
||||
reports each in `csiShapes` instead of only the last one.
|
||||
- **Live CSI → kernel.** `--analyze` runs the busiest (or `--node-id`) node's
|
||||
dominant single-antenna shape through `@ruvnet/ruview-kernel` at the
|
||||
measured arrival rate, with a bounded frame buffer (`analyze_max_frames`,
|
||||
default 6,000). Results are signal-processing estimates with no reference.
|
||||
- **Serial monitor no longer reboots nodes.** The monitor opens the port with
|
||||
DTR/RTS deasserted. MEASURED on the C6: uptime kept rising across a monitor
|
||||
run (1,612 s → 1,625 s); before, opening the port reset it (uptime ~15 s).
|
||||
It takes `--baud` (Realtek logs at 1,500,000) and explains the C6/S3
|
||||
USB-Serial/JTAG console when the UART is silent.
|
||||
- **Device scan.** Prolific `067B:23A3` (PL2303GC) is classified `realtek`.
|
||||
- **Windows.** The hosts file gets an owner-only ACL (`icacls`), failing
|
||||
closed; POSIX `0o600` is ignored on Windows. The symlink-escape test skips
|
||||
only where Windows refuses to create symlinks.
|
||||
- **Parser speed.** Int8Array view and `sqrt` instead of per-byte reads and
|
||||
`Math.hypot`: MEASURED 0.42–0.52 M → 1.33–1.67 M packets/s (about 3.2x;
|
||||
three interleaved runs, median of seven each, Windows 11 x64, Node 24).
|
||||
|
||||
Hardware evidence (MEASURED, this host): C6 node 42, 45 s, 1,160 packets,
|
||||
1,157 decoded, 0 sequence loss, 541 1x256 frames at 12.1 Hz analyzed by the
|
||||
napi kernel (integrity verified). The RAC1 decoder is cross-checked against
|
||||
the Rust `realtek-csi-sim` encoder (200/200 frames at 8- and 16-bit tones);
|
||||
a live RAC1 re-run is pending reconnection of the Realtek board.
|
||||
|
||||
Real flash (MEASURED, same C6, COM16): `ruview flash --variant c6 --confirm`
|
||||
with the checksum-verified `release_bins/c6-adr110` bundle wrote 4 images
|
||||
(hash verified by esptool, NVS kept: node 42 rejoined). The boot log showed
|
||||
13 CSI callbacks, the MGMT+DATA upgrade and no panic. After boot, `monitor`
|
||||
saw CSI without a reset (uptime 36 s → 62 s) and the UDP capture decoded
|
||||
183/183 packets with zero loss. The run exposed one more Windows bug, now
|
||||
fixed: the boot-log capture wrote decoded text to a cp1252 stdout (the
|
||||
scrubbed child env) and aborted on the firmware's U+2192 log character, so
|
||||
`bootLog.captured` was false and an earlier abort would have hidden the CSI
|
||||
evidence. It now writes raw bytes; a regression test forces cp1252.
|
||||
|
||||
Live RTL8721Dx re-run (MEASURED, board 4 on COM10, node 3, alongside the C6):
|
||||
`devices` classified COM10 as `realtek`; a 20 s capture decoded 7,404/7,404
|
||||
packets (6,959 RAC1 frames at 348 Hz, RSSI −38.9 dBm, 200 RHB1 heartbeats);
|
||||
`monitor --baud 1500000` saw 62 CSI log lines and did not reset the board
|
||||
(sequence kept rising, 80,856 → 88,257). `--analyze --node-id 3` ran 12,549
|
||||
live frames at a measured 313.7 Hz through the WASM kernel (integrity
|
||||
verified). The board also exposed a loss-accounting bug carried over from the
|
||||
original code: one stray sequence value (+52,835, then straight back) was
|
||||
counted as 52,834 lost frames, giving 97% "loss". The tracker now treats a
|
||||
single out-of-window value as a stray, needs two agreeing frames to accept a
|
||||
counter reset, and lets a late packet fill its own gap. Against ground truth
|
||||
(unique sequence numbers over the span, 15 s): 4,783/4,783 present, true loss
|
||||
0, 9 strays — the tracker reported loss 0 and 9 strays.
|
||||
|
||||
## Amendment 3 (2026-10-01): ESPHome radar kits and CSI buffer starvation
|
||||
|
||||
- **ESPHome source for radar.** The Seeed MR60BHA2 kit (a XIAO ESP32-C6 with
|
||||
the 60 GHz radar) runs ESPHome, which owns the radar UART. Raw frames never
|
||||
reach USB, so `mmwave --model auto` correctly returned `no_valid_frames`.
|
||||
`ruview_mmwave_read` now takes `source: "esphome"` with a `host`.
|
||||
- **Client:** a dependency-free, read-only client for the plaintext ESPHome
|
||||
native API (TCP 6053) runs hello → connect → device info → list entities
|
||||
→ subscribe states for a bounded window. It never sends a command.
|
||||
- **Roles:** presence, heart, breathing, distance and target count are
|
||||
found by entity name. The result uses the same summary fields as the
|
||||
serial path.
|
||||
- **Decoding:** ESPHome's `missing_state` flag and proto3 zero defaults are
|
||||
honoured, so absent readings are excluded rather than averaged as 0.
|
||||
- **Address policy:** hosts must resolve to private, link-local, CGNAT
|
||||
(Tailscale) or loopback addresses.
|
||||
- **Unsupported cases:** encrypted (Noise) and password-protected APIs are
|
||||
reported as unsupported.
|
||||
- **Live kit (MEASURED, 192.168.1.102, ESPHome 2026.9.0, project
|
||||
`seeedstudio.mr60bha2_kit` `spaces-1.0`).**
|
||||
- The kit had been configured for an out-of-range SSID. It was moved to
|
||||
the operator's network through its own captive portal, without
|
||||
reflashing.
|
||||
- A 15 s read returned 154 state updates:
|
||||
- presence: detected, 1 target;
|
||||
- mean distance: 40.0 cm;
|
||||
- device-reported heart rate: mean 75.3 bpm;
|
||||
- device-reported breathing rate: mean 10.7 bpm.
|
||||
- These are firmware-computed values, not validated against a reference.
|
||||
- **Realtek heartbeat-only stall: root cause.** Reading the stalled board's
|
||||
console without a reset showed continuous `[WLAN-W] lack of csi buf!` /
|
||||
`csi buf not enough`.
|
||||
- The radio produces CSI reports, but the firmware does not return report
|
||||
buffers, so CSI stops while heartbeats continue.
|
||||
- `ruview monitor` now counts these lines and fails with
|
||||
`csi_buffer_starvation`. Observed live: 399 lines in 8 s, 0 CSI.
|
||||
- The lasting fix belongs in the RTL8721Dx firmware (outside this
|
||||
repository).
|
||||
- **Views.** Radar results render in the terminal UI and in the console
|
||||
widget (`ruview_mmwave_read` is now a UI tool). Both label vitals
|
||||
"device-reported, not validated".
|
||||
|
||||
## Consequences
|
||||
|
||||
- One command per modality works across CLI, MCP and SDK, with actionable
|
||||
failures (`pyserial_missing`, `port_open_failed`, `no_valid_frames`,
|
||||
`port_in_use`, `connect_failed`).
|
||||
- Python and pyserial become the serial dependency. `doctor` reports both.
|
||||
- The package's unpacked-size ceiling rises to 320 KiB (from 224 KiB) in
|
||||
both npm workflows, with the published package still free of runtime
|
||||
dependencies.
|
||||
- Follow-up: feed these readers into `ruview-hal` observations (ADR-320)
|
||||
for fusion.
|
||||
@@ -0,0 +1,78 @@
|
||||
# ADR-374: Remote host access over SSH (read-only)
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-09-30
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: remote, ssh, hardware, least-authority, mcp
|
||||
- **Related**: ADR-369, ADR-373
|
||||
|
||||
## Context
|
||||
|
||||
RuView hardware usually hangs off a different machine from the one the
|
||||
operator or agent works on: a Raspberry Pi next to the ESP32s, a NUC running
|
||||
the sensing server, a Cognitum Seed. Cloud agent sessions cannot reach local
|
||||
USB at all.
|
||||
|
||||
"Host access" should not mean opening a new network service with its own
|
||||
credentials: that adds attack surface and secret handling. Operators already
|
||||
have SSH.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Where work runs.**
|
||||
- Hardware tools always execute on the machine the hardware is attached
|
||||
to.
|
||||
- Interactive work runs in a local Claude Code session on that machine. It
|
||||
can be driven from the Claude app through Remote Control
|
||||
(`claude remote-control` in the repo).
|
||||
- Other machines are reached over SSH.
|
||||
2. **Hosts registry.**
|
||||
- Stored in `~/.config/ruview/hosts.json` (directory 0700, file 0600).
|
||||
- Written only by the CLI: `ruview hosts add --name --ssh [--port]
|
||||
[--version]`. MCP can list hosts but never add them.
|
||||
- Names must match `^[a-z0-9][a-z0-9-]{0,31}$`. Targets must be
|
||||
`[user@]hostname`: no options, spaces, shell metacharacters or leading
|
||||
dash.
|
||||
3. **Transport.**
|
||||
- Local ssh arguments: `ssh -o BatchMode=yes -o ConnectTimeout=10 -o
|
||||
StrictHostKeyChecking=yes -T [-p N] -- <target> <command>`.
|
||||
- No password prompts, and no trust-on-first-use from the tool. The
|
||||
operator pins the host key once interactively.
|
||||
- The remote command is `npx -y @ruvnet/ruview@<exact version> call <tool>
|
||||
--read-only --args-json <json>`, with every token single-quoted for the
|
||||
remote POSIX shell.
|
||||
4. **Read-only, twice.**
|
||||
- Locally, `ruview_host_run` accepts only tools that are read-only, use no
|
||||
credentials, and are not `ruview_host_*` (no host hopping). It validates
|
||||
arguments against the tool schema before sending.
|
||||
- Remotely, `call --read-only` repeats the check and schema validation.
|
||||
- Flash, train, calibrate and Cognitum Spaces reads are never forwarded.
|
||||
5. **Grants.** Over MCP, `ruview_host_run` needs `remote-host` plus the
|
||||
remote tool's own grant (for example `device-access`).
|
||||
6. **Failures** are classified with remedies: `host_key_unverified`,
|
||||
`ssh_auth_failed`, `host_unreachable`, `remote_node_missing`,
|
||||
`unknown_host`, `remote_tool_not_allowed`.
|
||||
|
||||
## Evidence
|
||||
|
||||
Tests in `harness/ruview/test/remote.test.mjs` cover:
|
||||
|
||||
- host-entry injection cases (`-oProxyCommand=…`, `;`, spaces, `$(…)`,
|
||||
backticks);
|
||||
- quoting, verified by running the quoted command through a real `sh`;
|
||||
- the refusal list;
|
||||
- grant composition;
|
||||
- the file mode;
|
||||
- `call --read-only` refusing `ruview_node_flash` through the real CLI.
|
||||
|
||||
No SSH server was available in the development container, so a live
|
||||
round-trip to a real host is still to be done.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Remote hosts get the same tools with no new daemon, port or credential
|
||||
store.
|
||||
- The remote needs Node 20+ and npm registry access (or a pre-installed
|
||||
version), and `ssh` must be on the local PATH. `doctor --group remote`
|
||||
checks the local side.
|
||||
- Remote mutations stay manual by design.
|
||||
@@ -0,0 +1,202 @@
|
||||
# ADR-375: MCP Apps console, HTTP transport and terminal UI for `@ruvnet/ruview`
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-01
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: npm, mcp, mcp-apps, chatgpt, ui, cli, http, least-authority, performance
|
||||
- **Related**: ADR-263 (harness hardening), ADR-265 (distribution and size
|
||||
budget), ADR-369 (operator surface and SDK), ADR-373 (device access),
|
||||
ADR-374 (remote hosts)
|
||||
|
||||
## Context
|
||||
|
||||
The harness exposes one tool registry through the CLI, an MCP stdio server and
|
||||
an SDK (ADR-369). Hardware testing of the device tools (ADR-373 Amendment 2)
|
||||
showed three gaps in how results reach people and models:
|
||||
|
||||
- **MCP results were text only.** Every tool returned pretty-printed JSON in a
|
||||
text block, with no `structuredContent`. The server declared protocol
|
||||
`2024-11-05`, and `resources/list` was always empty, so MCP Apps hosts and
|
||||
ChatGPT had nothing to render.
|
||||
- **ChatGPT could not connect.** ChatGPT connectors reach MCP servers over
|
||||
HTTPS. The harness spoke stdio only.
|
||||
- **The CLI printed raw JSON to humans.** A live capture of two nodes was a
|
||||
1.2 kB JSON object in the terminal, with alerts such as heartbeat-only
|
||||
senders easy to miss.
|
||||
|
||||
Reference implementations for the target hosts (the `web-based-chatgpt-mcp-starter`
|
||||
and `signal-to-swarm` sites) declare a `ui://` HTML resource and point UI tools
|
||||
at it through both the MCP Apps key (`_meta.ui.resourceUri`) and the ChatGPT
|
||||
keys (`openai/outputTemplate`, `openai/widgetAccessible`).
|
||||
|
||||
The package must stay free of runtime dependencies (ADR-263) and inside a
|
||||
reviewed size budget (ADR-265).
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. One console widget, two host families
|
||||
|
||||
`src/ui/console-widget.js` holds one self-contained HTML resource,
|
||||
`ui://ruview/console-v1.html`, with mimeType `text/html;profile=mcp-app`.
|
||||
|
||||
- **Tools.** `ruview_esp32_capture`, `ruview_devices_scan` and `ruview_doctor`
|
||||
carry `_meta.ui.resourceUri`, `ui/resourceUri`, `openai/outputTemplate` and
|
||||
`openai/widgetAccessible`.
|
||||
- **Data intake.** The widget reads results from either host family:
|
||||
- MCP Apps: a `ui/initialize` handshake over `postMessage`, then
|
||||
`ui/notifications/tool-input` and `ui/notifications/tool-result`;
|
||||
- ChatGPT: `window.openai.toolOutput` and the `openai:set_globals` event.
|
||||
- **Refresh.** A Refresh button re-runs the same tool with the same
|
||||
arguments, through `window.openai.callTool` or an MCP Apps `tools/call`
|
||||
request.
|
||||
- **Safety.**
|
||||
- The resource declares an empty CSP allowlist and makes no network
|
||||
requests.
|
||||
- Every value reaches the DOM through `textContent`; there is no
|
||||
`innerHTML`.
|
||||
- Messages are accepted only from the parent frame.
|
||||
- A test pins all of this.
|
||||
- **Design.** The visual language follows the reference sites: dark ground,
|
||||
`#d0ff72` accent, monospace labels, and plain wording about what each value
|
||||
is. Device vitals are labelled "device-reported (not validated)". Kernel
|
||||
outputs carry the "no reference measurement" note, and SYNTHETIC frames
|
||||
raise a warning.
|
||||
|
||||
### 2. Structured results and protocol negotiation
|
||||
|
||||
- **Protocol negotiation.** `initialize` negotiates `2025-06-18`,
|
||||
`2025-03-26` or `2024-11-05`, and advertises `resources`.
|
||||
- **Result shape.** `tools/call` returns `structuredContent` (always an
|
||||
object), a text block with the same JSON in compact form, and
|
||||
`_meta['ruview/tool']` set to the canonical tool name (so an aliased call
|
||||
still refreshes correctly).
|
||||
- **Compact text.** Compact rather than pretty-printed JSON cuts each result
|
||||
by 17–33% (MEASURED on this host: guidance 17.5%, device scan 20.7%, live
|
||||
capture 33.4%). Clients that read only `content` still receive the
|
||||
complete result.
|
||||
- **Titles.** Tools gain `title` and `annotations.title`.
|
||||
|
||||
`handleRpc` is transport-neutral, and `createDispatcher` holds the FIFO tool
|
||||
chain both transports share (ADR-263 O2).
|
||||
|
||||
### 3. Streamable HTTP transport
|
||||
|
||||
`ruview mcp start --http [--host 127.0.0.1] [--port 8790] [--allow-origin URL]`
|
||||
starts `src/mcp-http.js`.
|
||||
|
||||
- **Authentication on every request.** A request must carry either
|
||||
`Authorization: Bearer <token>`, or the secret-path form `/mcp/<token>`.
|
||||
The second exists because ChatGPT connectors cannot send a static header.
|
||||
- Tokens are compared in constant time and must be at least 16 characters.
|
||||
- The token comes from `RUVIEW_MCP_TOKEN`, never argv. If unset, one is
|
||||
generated and printed once to stderr.
|
||||
- **Least authority.**
|
||||
- It binds to loopback by default.
|
||||
- Browser `Origin`s are refused unless allowlisted (DNS-rebinding defence).
|
||||
Server-to-server clients send no Origin.
|
||||
- `workspace-write` and `hardware-write` are never honoured over HTTP, even
|
||||
when granted in `RUVIEW_MCP_GRANTS`. Flashing and calibration stay
|
||||
stdio/CLI-only.
|
||||
- **Protocol surface.**
|
||||
- POST only; single JSON responses with no SSE stream and no sessions.
|
||||
- Batches and unsupported `MCP-Protocol-Version` values are refused.
|
||||
- Bodies are capped at 256 KiB.
|
||||
- Notifications return 202.
|
||||
- `/healthz` reports only the name and version.
|
||||
|
||||
Exposing the port to ChatGPT (a tunnel or reverse proxy with TLS) is the
|
||||
operator's choice. The startup banner says so and warns on a non-loopback
|
||||
bind.
|
||||
|
||||
### 4. Terminal UI
|
||||
|
||||
`src/cli-ui.js` renders results for a TTY:
|
||||
- aligned tables, with unicode rate bars;
|
||||
- colour that respects `NO_COLOR` and `TERM=dumb`;
|
||||
- alerts and fixes ahead of the data;
|
||||
- a stderr countdown for timed captures.
|
||||
|
||||
Pipes and `--json` keep receiving exact JSON. `ruview call` always prints
|
||||
JSON, because remote hosts parse it.
|
||||
|
||||
`ruview esp32 --watch [--seconds 3]` redraws repeated capture windows with a
|
||||
per-node rate sparkline.
|
||||
|
||||
`ruview monitor` now flags Ameba CSI report-buffer starvation (`lack of csi
|
||||
buf` / `csi buf not enough`). This is the root cause of the Realtek
|
||||
heartbeat-only stall, observed live: 399 starvation lines in 8 s, 0 CSI.
|
||||
|
||||
### 5. Startup
|
||||
|
||||
- **Launcher split.** `bin/cli.js` is a thin launcher, and `src/cli.js` loads
|
||||
the tool registry, doctor, brain, host adapters and transports on demand.
|
||||
- **Measured effect.**
|
||||
- `--help`/`--version`/`skills` skip the registry: about 20% faster, median
|
||||
of 9 interleaved runs against the previous build on this host (MEASURED).
|
||||
- The MCP path needs the whole registry to list tools, so its start time is
|
||||
unchanged within noise.
|
||||
- **Compile cache rejected.** Node's on-disk compile cache
|
||||
(`module.enableCompileCache`) was evaluated: module load was 27.7 ms with
|
||||
it and 28.5 ms without (25 interleaved runs, MEASURED). That is within
|
||||
noise, so it was not adopted.
|
||||
|
||||
### 6. Size budget
|
||||
|
||||
The additions cost about 38 kB:
|
||||
- widget, 12.6 kB;
|
||||
- HTTP transport;
|
||||
- terminal UI;
|
||||
- CLI split.
|
||||
|
||||
The `@ruvnet/ruview` unpacked budget rises from 320 KiB to 384 KiB in
|
||||
`npm-packages.yml` and `ruview-npm-release.yml`. The package is about 338 kB.
|
||||
|
||||
## Evidence
|
||||
|
||||
All on Windows 11 x64, Node 24, with an ESP32-C6 (CP210x, COM16) and an
|
||||
RTL8721Dx (PL2303GC, COM10) attached. All results MEASURED unless noted.
|
||||
|
||||
**Browser e2e.** A real MCP Apps host flow in headless Chrome:
|
||||
- The host lists tools and reads the `ui://` resource over HTTP MCP, then
|
||||
loads it in a sandboxed iframe (`allow-scripts` only).
|
||||
- It answers `ui/initialize` and delivers a live `ruview_esp32_capture`
|
||||
result.
|
||||
- The widget's Refresh button triggered a second real capture through the
|
||||
host (108 → 105 packets).
|
||||
- The devices and doctor views rendered, in dark and light themes. The only
|
||||
console error was the test host's own missing favicon.
|
||||
|
||||
**Security probes** against the live HTTP server:
|
||||
- no token: 401;
|
||||
- wrong token: 401;
|
||||
- foreign Origin: 403;
|
||||
- GET: 405;
|
||||
- `ruview_node_flash` with `hardware-write` granted in the environment:
|
||||
`authority_denied`.
|
||||
|
||||
**Tests.**
|
||||
- `npm test`: 160 pass, 1 skip.
|
||||
- `test:security`: 83 pass, 1 skip.
|
||||
- New suites: `mcp-apps.test.mjs` (protocol, metadata, widget safety, HTTP
|
||||
auth/origin/size/write denial) and `cli-ui.test.mjs` (renderers, colour
|
||||
rules, the starvation detector run against a stand-in serial module).
|
||||
|
||||
**Tarball smoke** from a clean install:
|
||||
- CLI and SDK work;
|
||||
- stdio MCP negotiates `2025-06-18` and serves the widget and structured
|
||||
results;
|
||||
- HTTP MCP lists 22 tools, 3 with `ui://` metadata.
|
||||
|
||||
## Consequences
|
||||
|
||||
- MCP Apps hosts and ChatGPT render node streams, device scans and
|
||||
diagnostics instead of raw JSON, and can refresh them in place.
|
||||
- Remote MCP clients can use the read tools over authenticated HTTP. Write
|
||||
tools remain local by construction.
|
||||
- Each tool result costs fewer model tokens.
|
||||
- The widget is a second place where result shapes are interpreted. Changes
|
||||
to the capture, devices or doctor result shapes must update it, and its
|
||||
version suffix (`console-v1`) changes when the contract does.
|
||||
- Host behaviour for MCP Apps and ChatGPT keys is followed as documented by
|
||||
the reference implementations. A live ChatGPT connector session (public
|
||||
HTTPS URL) has not been run yet: CLAIMED, pending.
|
||||
@@ -0,0 +1,142 @@
|
||||
# ADR-376: `ruview` — one npm install for every RuView component
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-01
|
||||
- **Deciders**: rUv (requested and approved the release); RuView maintainers
|
||||
- **Tags**: npm, packaging, umbrella, mcp, homecore, kernel, release
|
||||
- **Related**: ADR-263 (harness hardening), ADR-265 (distribution, D2 CI-only
|
||||
publishing, D4 bin ownership), ADR-285 (Homecore metaharness), ADR-368
|
||||
(kernel), ADR-369 (operator surface), ADR-373 (device access), ADR-375 (MCP
|
||||
Apps console, HTTP transport, terminal UI)
|
||||
|
||||
## Context
|
||||
|
||||
RuView is positioned as an ambient intelligence platform rather than a sensor.
|
||||
Its sensing is ahead of comparable packaged options; packaging is the
|
||||
bottleneck. Today the capabilities are split across three packages:
|
||||
|
||||
| Package | Contents | Published before this ADR |
|
||||
|---|---|---|
|
||||
| `@ruvnet/ruview` | operator harness | 0.5.1 |
|
||||
| `homecore` | Homecore developer metaharness | never |
|
||||
| `@ruvnet/ruview-kernel` | WASM vitals kernel | never |
|
||||
|
||||
The unscoped name `ruview` was unclaimed. A user has to know all three
|
||||
packages, install them separately, and run three CLIs and two MCP servers.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **The umbrella.** Publish an unscoped `ruview` package
|
||||
(`harness/ruview-umbrella`). It depends on exact versions of the three
|
||||
component packages and adds no capability of its own:
|
||||
- **CLI.** One binary, `ruview`:
|
||||
- `ruview homecore …` → the Homecore CLI;
|
||||
- `ruview kernel <doctor|info|selftest|parity|synth|analyze|bench>` → the
|
||||
kernel CLI;
|
||||
- bare `ruview kernel [--backend …]` keeps its harness meaning (the
|
||||
SYNTHETIC self-test);
|
||||
- `ruview capabilities` lists every component, version and tool;
|
||||
- everything else goes to the `@ruvnet/ruview` CLI unchanged.
|
||||
- **MCP server.** One server, `ruview mcp start [--http]`. It serves the
|
||||
harness tools (`ruview_*`, including the `ui://` console) plus the
|
||||
Homecore tools (`homecore_*`, already prefixed, so no collision).
|
||||
- Non-Homecore traffic goes to the harness handler, so the harness
|
||||
authority model, grants and HTTP rules (ADR-375) apply unchanged.
|
||||
- Homecore calls go to Homecore's `runTool`, with Homecore's own
|
||||
policy.
|
||||
2. **Bin ownership (amends ADR-265 D4).** The `ruview` command name moves to
|
||||
the umbrella. `@ruvnet/ruview`'s bin is renamed `ruview-harness`.
|
||||
- **Why:** a clean-directory tarball install showed the collision. With
|
||||
both packages declaring `ruview`, `node_modules/.bin/ruview` linked to
|
||||
the harness CLI, so `ruview capabilities` and `ruview homecore` failed.
|
||||
Which package wins that link is not guaranteed.
|
||||
- **Unaffected:** `npx @ruvnet/ruview …` still works, because npx runs a
|
||||
package's only bin whatever its name. The umbrella delegates every
|
||||
non-umbrella verb to the harness, so `ruview <verb>` behaves as before.
|
||||
- **Changed:** a global install of `@ruvnet/ruview` alone now provides
|
||||
`ruview-harness`.
|
||||
3. **Harness changes needed to embed it** (no new runtime dependencies):
|
||||
- **New exports:** `./cli`, `./cli-ui`, `./mcp`, `./mcp-http`, `./ui`,
|
||||
`./esphome` and `./kernel`.
|
||||
- **Injectable MCP handler:** `run(args, { mcpHandler })` and
|
||||
`startMcpServer({ handler })`, so the merged server reuses the stdio and
|
||||
HTTP transports.
|
||||
- **Kernel declared as an optional peer:** `@ruvnet/ruview-kernel`.
|
||||
- **`setKernelImporter`:** lets the umbrella resolve the kernel from its
|
||||
own dependency tree. Node resolves bare specifiers from the importing
|
||||
file's real path, which misses a sibling dependency under linked or
|
||||
pnpm-style installs.
|
||||
- Found while testing: `kernel_not_installed` through a linked umbrella.
|
||||
- Only embedding code can call it; MCP arguments cannot choose the
|
||||
module.
|
||||
- **Homecore** gains a `./cli` export; **the kernel** gains `./cli`.
|
||||
4. **Versions for this release.**
|
||||
|
||||
| Package | Version | Notes |
|
||||
|---|---|---|
|
||||
| `@ruvnet/ruview` | 0.8.0 | 0.7.0 was never published |
|
||||
| `homecore` | 0.1.0 | first publish |
|
||||
| `@ruvnet/ruview-kernel` | 0.1.0 | first publish; WASM only |
|
||||
| `ruview` | 0.8.0 | umbrella |
|
||||
|
||||
The kernel ships WASM only. The napi backend stays optional and absent,
|
||||
because a workstation build would cover only win32-x64. `loadKernel({
|
||||
backend: 'auto' })` reports the honest fallback.
|
||||
5. **Publishing exception (one-off, amends ADR-265 D2 for this release).**
|
||||
- rUv explicitly chose a local `npm publish` from the operator
|
||||
workstation (ruvzen) over the CI-only release workflow.
|
||||
- Packages are published in dependency order: kernel → homecore →
|
||||
@ruvnet/ruview → ruview.
|
||||
- Each is published only after its full gate passes locally (tests,
|
||||
security tests, manifest, version-literal and pack-content/size gates),
|
||||
plus a clean-directory tarball install and smoke test of the umbrella.
|
||||
- These versions carry no npm provenance attestation.
|
||||
- Subsequent releases return to the CI workflow. Adding `ruview` and
|
||||
`@ruvnet/ruview-kernel` to `ruview-npm-release.yml` is follow-up work.
|
||||
|
||||
## Release outcome (2026-10-01)
|
||||
|
||||
**Published from ruvzen** (exact tarballs that passed the clean-directory
|
||||
smoke test; sha256 in the release thread):
|
||||
|
||||
| Package | Version |
|
||||
|---|---|
|
||||
| `@ruvnet/ruview-kernel` | 0.1.0 |
|
||||
| `homecore` | 0.1.0 |
|
||||
| `@ruvnet/ruview` | 0.8.0 |
|
||||
|
||||
All three were re-installed from the registry and smoke-tested:
|
||||
- `npx -y @ruvnet/ruview@0.8.0` works;
|
||||
- the harness self-test resolves the published kernel;
|
||||
- a live ESPHome radar read succeeded.
|
||||
|
||||
**`ruview` was refused by npm:** `403 Forbidden — Package name too similar to
|
||||
existing package iview` (typosquat protection).
|
||||
- The umbrella is ready in `harness/ruview-umbrella`. Until npm support grants
|
||||
the name, it is unpublished.
|
||||
- If the request is declined, the fallback is a scoped name
|
||||
(`@ruvnet/ruview-platform`).
|
||||
|
||||
## Consequences
|
||||
|
||||
- `npx ruview` is the single entry point for operators and agents. One MCP
|
||||
connector gives an agent, or ChatGPT, every RuView and Homecore tool.
|
||||
- The component packages stay independently installable and testable.
|
||||
`@ruvnet/ruview` remains free of runtime dependencies.
|
||||
- The umbrella pins exact component versions, so each component release needs
|
||||
an umbrella release to reach `npx ruview` users.
|
||||
- The local publish trades provenance for speed for this release only. The
|
||||
exception is recorded here and in the #swarm coordination thread.
|
||||
|
||||
## Evidence
|
||||
|
||||
- **Umbrella tests:** 4/4, covering CLI dispatch to all three components,
|
||||
merged MCP tool listing and routing, the harness authority model through
|
||||
the merged server, and stdio and HTTP transports.
|
||||
- **Component gates:**
|
||||
- harness: 166 tests, 89 security tests;
|
||||
- Homecore: 27 tests, 23 security tests;
|
||||
- kernel: 21 tests, 3 skipped native cases; 12 security tests;
|
||||
- all manifests verified, 0 audit findings.
|
||||
- **Clean-directory tarball smoke:** recorded in the release commit and the
|
||||
#development post.
|
||||
@@ -0,0 +1,101 @@
|
||||
# ADR-377: `ruview-live` — a Claude Code mod shipped inside `@ruvnet/ruview`
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-01
|
||||
- **Deciders**: rUv (requested integration into the npm package); RuView maintainers
|
||||
- **Tags**: claude-code, mods, function-hooks, ui, npm, least-authority
|
||||
- **Related**: ADR-373 (device access), ADR-375 (MCP Apps console, terminal UI),
|
||||
ADR-376 (umbrella package); Claude Code mods (function hooks): the
|
||||
claude-code issue 91870 and the `mods/` built-ins (`diff` is the closest
|
||||
analogue)
|
||||
|
||||
## Context
|
||||
|
||||
Claude Code mods are plugins whose behaviour is a function-hooks module: one
|
||||
`register(on, options)` that hooks engine events as `($, e, next)`. They can:
|
||||
- register slash commands;
|
||||
- open panes beside the transcript and draw them with the surface's elements;
|
||||
- set a status line;
|
||||
- run processes through `$.process.run`.
|
||||
|
||||
That fits RuView's goal of putting the live state of the sensors where an
|
||||
operator or agent already works.
|
||||
|
||||
RuView already has:
|
||||
- a read-only CLI with `--json` results (ADR-373, ADR-375);
|
||||
- an MCP server and the ChatGPT/MCP Apps console;
|
||||
- a plugin marketplace in this repository.
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Ship a mod, `ruview-live`, inside `@ruvnet/ruview`** (`mod/`, packaged
|
||||
files `mod/.claude-plugin/`, `mod/hooks/`).
|
||||
- It is listed in the repository marketplace as `ruview-live@ruview`, with
|
||||
source `./harness/ruview/mod`.
|
||||
- One copy serves both installs: the npm package and the marketplace.
|
||||
2. **Behaviour.**
|
||||
- **`/ruview`** toggles a pane (`ruview-live`). While it is open, a
|
||||
`$.clock.every` timer (default 15 s, minimum 5 s) runs one read-only
|
||||
capture.
|
||||
- **`/ruview refresh`** runs one capture and returns the summary.
|
||||
- **`/ruview off`** closes the pane.
|
||||
- **The pane** shows nodes (rate, loss, RSSI, shape, SYNTHETIC flag), the
|
||||
optional ESPHome radar (presence, distance, device-reported vitals), and
|
||||
alerts, including heartbeat-only boards with the CSI-buffer-starvation
|
||||
hint. It has Refresh (`r`) and Close (`c`) buttons.
|
||||
- **The status line** is set on every refresh.
|
||||
3. **Data comes only from the harness CLI beside the mod.**
|
||||
- The mod runs `node <plugin root>/../bin/cli.js esp32 … --json` and
|
||||
`mmwave --source esphome … --json`, as fixed argv arrays, through
|
||||
`$.process.run`. So the pane shows what the tested tools return.
|
||||
- Mods may import nothing but their own files and `claude-code` (the
|
||||
validator refuses `node:url`). The CLI is therefore located through
|
||||
`$.plugin.root`.
|
||||
4. **Least authority.**
|
||||
- The mod never runs a write verb (flash, calibrate, train). A unit test
|
||||
pins the argv.
|
||||
- The radar host is validated before use.
|
||||
- Options are bounded: ports 1024–65535, refresh ≥ 5 s, capture 1–10 s.
|
||||
5. **Discovery.** `ruview mod` (and `ruview mod --json`) prints the installed
|
||||
path, the `claude --plugin-dir` command, the marketplace commands, usage,
|
||||
settings, and the early-access note.
|
||||
|
||||
## Evidence
|
||||
|
||||
All on Claude Code 2.1.286, Windows 11 x64.
|
||||
- **Validation:** `claude plugin validate harness/ruview/mod` passes. It lists
|
||||
hooks `session.start`, `command.run{ruview}`, `ui.render{Pane}`,
|
||||
`ui.close{ruview-live}` and `session.end`, and calls `$.clock.every`,
|
||||
`$.clock.now`, `$.command.register`, `$.process.run` and `$.ui.*`.
|
||||
`claude plugin validate .` passes for the marketplace.
|
||||
- **Engine tests:** `claude plugin test harness/ruview/mod`, with
|
||||
`CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1`, passed 2/2 in three consecutive
|
||||
runs. They check:
|
||||
- `/ruview` opens the pane and runs the exact read-only argv;
|
||||
- the status reads `RuView · 2 nodes · 1 alert`;
|
||||
- the timer refreshes while open and **never after close**;
|
||||
- an honest `no_packets` status.
|
||||
- **Harness unit tests:** `test/mod.test.mjs`, 6/6, under plain Node:
|
||||
settings bounds, host validation, argv, failure parsing, model/status, pane
|
||||
drawing and button wiring, and the no-imports rule.
|
||||
- **Real engine, headless (MEASURED):**
|
||||
`claude --plugin-dir ./mod -p "/ruview refresh"` answered
|
||||
`RuView · 0 nodes · 1 alert` (no CSI node was streaming). With `radarHost`
|
||||
set to the live Seeed MR60BHA2 kit, it answered
|
||||
`RuView · 0 nodes · radar clear · 1 alert`.
|
||||
- **Note:** under Git Bash a leading `/ruview` is rewritten into a Windows path
|
||||
unless `MSYS_NO_PATHCONV=1` is set. This is not an issue inside the Claude
|
||||
Code prompt.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An operator sees node and radar health beside the conversation without
|
||||
asking for it. Agents see the same facts through MCP.
|
||||
- **Mods are early access.** The API may change between Claude Code releases,
|
||||
and hooks modules load only where function hooks are enabled. The mod is
|
||||
therefore optional, and nothing else in the package depends on it.
|
||||
- **Trust.** Like every mod, it runs inside Claude Code with Claude Code's
|
||||
access. It is read-only by construction, and the README says to install it
|
||||
only from a trusted source.
|
||||
- A refresh binds UDP 5005 for the capture window. While the sensing server
|
||||
holds the port, the pane reports `port_in_use` instead of reading.
|
||||
@@ -0,0 +1,144 @@
|
||||
# ADR-378: `ruview-live` showcase views — CSI waterfall, radar fan, animated cells
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-01
|
||||
- **Deciders**: rUv ("make this a showcase … the art of the possible for mods"); RuView maintainers
|
||||
- **Tags**: claude-code, mods, ui, raster, animation, csi, radar, honesty
|
||||
- **Related**: ADR-377 (the `ruview-live` mod), ADR-373 (device access),
|
||||
ADR-375 (terminal UI), ADR-018 / ADR-323 (CSI formats)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-377 shipped `/ruview`: a pane of cards built from text and sparklines. The
|
||||
mod engine can do much more on the terminal surface:
|
||||
|
||||
- `Raster` is a grid of cells, each with a glyph and a 24-bit foreground and
|
||||
background.
|
||||
- `$.ui.blit` repaints a mounted Raster in place, up to 60 frames a second,
|
||||
without redrawing the tree.
|
||||
- Buttons take hotkeys.
|
||||
|
||||
The pane only showed summaries. It never showed the signal itself: per-subcarrier
|
||||
CSI amplitude over time, or the radar's range as geometry.
|
||||
|
||||
## Decision
|
||||
|
||||
### Three views, keys `1` `2` `3`
|
||||
|
||||
| View | What it draws | Data |
|
||||
|---|---|---|
|
||||
| Overview | the ADR-377 cards | unchanged |
|
||||
| CSI waterfall | subcarrier bins across, time down; two frames per text row through `▀` (foreground = upper frame, background = lower); 5th–95th percentile colour limits with a colour bar | `ruview esp32 --spectrum` (new) |
|
||||
| Radar | top-down 120° fan: range rings, a fading trail of past distances, the current distance as an arc; braille line charts of heart, breathing and distance | ESPHome radar kit (ADR-373) |
|
||||
|
||||
`n` cycles nodes in the waterfall when more than one streams. The live views
|
||||
refresh every `liveRefreshSeconds` (default 4, range 2–60); the overview keeps
|
||||
`refreshSeconds`.
|
||||
|
||||
### Harness: `--spectrum`
|
||||
|
||||
`captureEsp32` and the `ruview_esp32_capture` tool take three new arguments:
|
||||
|
||||
- `spectrum`: boolean;
|
||||
- `spectrum_bins`: 8–128, default 48;
|
||||
- `spectrum_frames`: 8–256, default 64.
|
||||
|
||||
A `SpectrumCollector` keeps a ring of the newest frames for each node and CSI
|
||||
shape. It averages each frame's amplitudes into equal bins, and reports the
|
||||
dominant shape per node with its arrival rate and `synthetic` flag. The
|
||||
collector is read-only and bounded (8 nodes), and it adds nothing to a capture
|
||||
that does not ask for it.
|
||||
|
||||
### Animation with `$.ui.blit`
|
||||
|
||||
One 80 ms timer (about 12 fps) repaints only these keyed Rasters:
|
||||
|
||||
- **shimmer**: a highlight sweeping along the rule under the title. Pure
|
||||
decoration.
|
||||
- **pulse**: a ♥ that beats at the device-reported heart rate and a gauge that
|
||||
fills and empties at the device-reported breathing rate. It is labelled *a
|
||||
metronome, not a waveform*.
|
||||
- **fan**: a sonar ring that leaves the sensor and reaches the measured range.
|
||||
- **waterfall replay**: frames arrive a capture at a time, so the view drains
|
||||
them at their measured arrival rate instead of jumping every refresh. It is
|
||||
labelled *replayed at the arrival rate*.
|
||||
|
||||
The first paint and every frame come from the same function (`picturesOf`). A
|
||||
test asserts that each picture keeps its size across frames, because a blit of
|
||||
another size is refused.
|
||||
|
||||
Blits are fire-and-forget: a blit resolves only once a frame is painted, and
|
||||
the surface folds blits between frames anyway.
|
||||
|
||||
### Honesty rules (CLAUDE.md)
|
||||
|
||||
- The waterfall says **MEASURED — live CSI amplitude received on this host** or
|
||||
**SYNTHETIC — simulator frames**, from the packets' own flag.
|
||||
- The radar view states *range only: this kit reports distance, not bearing*.
|
||||
The arc covers every bearing at that range; nothing implies a position.
|
||||
- Vitals stay *device-reported, not validated*. Values outside 40–180 bpm
|
||||
(heart) or 4–40 bpm (breathing) are flagged.
|
||||
- Chart scales fit the data so a rhythm is visible. The plausible band is
|
||||
shaded only where it overlaps the scale.
|
||||
- Surfaces without `Raster` (desktop, VS Code, mobile) get a note in the
|
||||
showcase views instead of a tree the surface would refuse. The overview works
|
||||
everywhere.
|
||||
- Only nodes in the latest capture stay in the waterfall. A node that stops
|
||||
streaming, or a failed capture, clears its frames, so old frames are never
|
||||
shown as live. Frames join across captures only when the CSI shape and bin
|
||||
count both match.
|
||||
- Multi-antenna ESP32 nodes are drawn from their first chain. ADR-018 lays the
|
||||
tones out antenna-major; this applies to the waterfall only, and the vitals
|
||||
kernel still takes single-chain frames.
|
||||
- Animation runs on the real clock, the same one the full render reads. The
|
||||
pulses keep the reported rates even when a frame fires late.
|
||||
|
||||
### Structure
|
||||
|
||||
Mods may import only their own files, so the hooks module is split into:
|
||||
|
||||
- `model.mjs`: settings, argv, results, model and history (pure);
|
||||
- `raster.mjs`: Grid, base64, colour map, waterfall, braille, radar fan, line
|
||||
chart (pure);
|
||||
- `anim.mjs`: frame functions of time (pure);
|
||||
- `views.mjs`: drawing;
|
||||
- `register.mjs`: wiring, timers and blits. It re-exports the rest so tests
|
||||
import one module.
|
||||
|
||||
## Evidence
|
||||
|
||||
- **MEASURED** (real silicon, 2026-10-01):
|
||||
- Hardware: an ESP32-C6 on COM11, flashed with the hardware-verified
|
||||
`release_bins/c6-adr110` image (v0.7.0; SHA-256 checked) and provisioned to
|
||||
ruv.net, node 6.
|
||||
- Boot log: `Got IP: 192.168.1.103` and `UDP sender initialized:
|
||||
192.168.1.45:5005`.
|
||||
- Captures: `ruview esp32 --spectrum` received 29–50 frames per 2 s at 7–18
|
||||
Hz and −37 to −78 dBm, in 1×64 and 1×256 shapes.
|
||||
- Waterfall: a 1×256 capture rendered as 114 bins showing the occupied band
|
||||
against guard and null tones.
|
||||
- Reproducer: `npx @ruvnet/ruview esp32 --seconds 4 --spectrum --json` with
|
||||
a node targeting the host.
|
||||
- **MEASURED** (device-reported): the Seeed MR60BHA2 kit at 192.168.1.102 drives
|
||||
the radar view.
|
||||
- **SYNTHETIC**: the `realtek-csi-sim` simulator exercises the waterfall in
|
||||
development; its frames carry the synthetic flag and the view says so.
|
||||
- **Tests**:
|
||||
- 11 mod tests and 11 showcase tests: Grid/base64 against Node's encoder,
|
||||
colour map monotone, waterfall ordering, braille bits, fan scaling and
|
||||
ping, chart scaling, frame functions, constant picture sizes, MEASURED and
|
||||
SYNTHETIC labels, surfaces without Raster;
|
||||
- a loopback test of `--spectrum`;
|
||||
- 4 engine tests (`claude plugin test mod`), including the waterfall tab
|
||||
sending `--spectrum`, drawing the Raster and animating it with blits.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The package grows by about 37 KB. The reviewed unpacked ceiling rises from
|
||||
384 KiB to 448 KiB in `npm-packages.yml` and `ruview-npm-release.yml`. There
|
||||
are still no runtime dependencies.
|
||||
- The live views poll more often: a 2 s capture every 4 s by default. A
|
||||
capture binds the UDP port briefly, so a sensing server on the same port
|
||||
still reports `port_in_use`, as in ADR-377.
|
||||
- The animation timer runs only while the pane is open, and stops on close and
|
||||
session end.
|
||||
@@ -0,0 +1,199 @@
|
||||
# ADR-379: An OTA'd image confirms itself with a bounded health check, or rolls back
|
||||
|
||||
**Status:** Proposed. Hardware-verified on 2026-09-29 on an ESP32-S3 (node 4) and an ESP32-C6 (node 5).
|
||||
**Date:** 2026-09-29
|
||||
**Numbering:** On 2026-10-02 `main` had up to ADR-378 and open PRs claimed
|
||||
nothing higher. If another branch also takes 379, renumber whichever merges
|
||||
second.
|
||||
|
||||
## Context
|
||||
|
||||
Every 16 MB node was flashed with `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`
|
||||
(`sdkconfig.defaults.16mb`). With that option the bootloader starts a freshly
|
||||
OTA'd image in `ESP_OTA_IMG_PENDING_VERIFY`. The app must then call
|
||||
`esp_ota_mark_app_valid_cancel_rollback()`. If it doesn't, the bootloader marks
|
||||
the image `ABORTED` at the next reset and boots the previous slot.
|
||||
|
||||
Nothing in `firmware/esp32-csi-node/main` made that call, and `nm` on the ELF
|
||||
confirmed the symbol was not linked. RUNBOOK.md said an
|
||||
`ota_rollback_boot_check()` "sits in the app", but no such function existed
|
||||
anywhere in the tree. The consequences:
|
||||
|
||||
- An image delivered by `POST /ota` ran until the next reset, then reverted
|
||||
silently. OTA updates were not durable.
|
||||
- While the image was pending, `esp_ota_begin()` refused a second OTA with
|
||||
`ESP_ERR_OTA_ROLLBACK_INVALID_STATE`.
|
||||
- Nothing checked health, so a bad image was never rolled back on purpose. It
|
||||
only reverted if it happened to crash or be power-cycled.
|
||||
|
||||
A second defect meant none of this was ever reached: **OTA never succeeded
|
||||
on this firmware before this change.** MEASURED on 2026-09-29, on node 4 (an
|
||||
ESP32-S3 running the v0.8.12 16 MB build, with the OTA key provisioned):
|
||||
|
||||
- A `POST /ota` of a valid 1,228,672 B image logged `OTA update started`, then
|
||||
`stack overflow in task httpd`, then `rst:0xc RTC_SW_CPU_RST`. The node came
|
||||
back on its old slot. Both attempts failed the same way.
|
||||
- The cause: the server used `HTTPD_DEFAULT_CONFIG()`, which gives a 4096 B
|
||||
task stack. The handler held a 1 KB receive buffer on that stack, while
|
||||
calling `esp_ota_*` and formatting a float progress log.
|
||||
|
||||
## Decision
|
||||
|
||||
At boot, `ota_health_start()` (in `main/ota_health.c`, called early in
|
||||
`app_main`) reads the running partition's state:
|
||||
|
||||
| state | meaning here | action |
|
||||
|---|---|---|
|
||||
| `PENDING_VERIFY` | just OTA'd, rollback armed | run the health check |
|
||||
| `VALID` | USB-flashed with `ota_data_initial.bin` (the bootloader writes VALID), or already confirmed | nothing |
|
||||
| `UNDEFINED` | selected by a build without rollback; boots unconditionally | nothing |
|
||||
| `NEW` | bootloader lacks rollback, so it never promoted the image | nothing |
|
||||
| error (`NOT_FOUND`/`NOT_SUPPORTED`) | no otadata entry, or factory partition | nothing |
|
||||
|
||||
For this table I read the ESP-IDF v5.5 source: `bootloader_utility.c`
|
||||
(`set_actual_ota_seq`, the NEW to PENDING_VERIFY to ABORTED transitions) and
|
||||
`esp_ota_ops.c`.
|
||||
|
||||
**The health check.** A low-priority task polls once a second and feeds
|
||||
`ota_health_step()`, which is a pure function in `ota_health.h`:
|
||||
|
||||
- **Signals, both latched:** the STA got an IP (`IP_EVENT_STA_GOT_IP`, via
|
||||
`ota_health_note_got_ip()` in `main.c`'s handler), and the stream sender
|
||||
accepted at least one CSI frame (`csi_collector_get_send_ok_count() > 0`).
|
||||
The second signal covers driver callback, serialization and `sendto()`
|
||||
together.
|
||||
- **Minimum uptime** `CONFIG_OTA_HEALTH_MIN_UPTIME_S` (default 30 s): the image
|
||||
is not confirmed before this point even with both signals present. An image
|
||||
that crashes soon after starting its pipelines still reverts, because a crash
|
||||
while pending is a rollback.
|
||||
- **Deadline** `CONFIG_OTA_HEALTH_TIMEOUT_S` (default 120 s, measured from
|
||||
boot). Both signals are required by then. The minimum uptime is clamped to
|
||||
the deadline.
|
||||
- **Pass:** `esp_ota_mark_app_valid_cancel_rollback()`, which logs
|
||||
`marked valid after <reason> in <ms> ms`.
|
||||
- **Fail:** logs `health check failed: <reason>, rolling back`, then calls
|
||||
`esp_ota_mark_app_invalid_rollback_and_reboot()`. If no other bootable slot
|
||||
exists, that call returns an error. The node then logs and stays up rather
|
||||
than reboot-looping.
|
||||
- **Terminal verdict:** once the check has confirmed or rolled back, it never
|
||||
changes.
|
||||
|
||||
**Negative-test hook.** `CONFIG_OTA_HEALTH_FORCE_FAIL` (default n, test only)
|
||||
makes the check roll back at the point where it would have confirmed. Use it to
|
||||
prove rollback end to end.
|
||||
|
||||
**Remote visibility.** `GET /ota/status` now includes `ota_state` (`valid`,
|
||||
`pending_verify`, `new`, `undefined`, `other`, or `none`). An operator can
|
||||
confirm durability without a serial console.
|
||||
|
||||
**The httpd stack.** The OTA/WASM HTTP server now sets `config.stack_size =
|
||||
CONFIG_OTA_HTTPD_STACK_SIZE`, which defaults to 8192. Three smaller changes go
|
||||
with it:
|
||||
|
||||
- The OTA receive buffer is static rather than on the stack. Handlers run one
|
||||
at a time on the single httpd task, so one buffer is enough.
|
||||
- The progress log prints an integer percent instead of a float.
|
||||
- `GET /wasm/list` keeps its 2 KB JSON buffer on the heap, not the stack.
|
||||
|
||||
`recv_wait_timeout` stays at 30 s. Each `POST /ota` and `POST /wasm/upload` now
|
||||
logs `httpd stack after <handler>: N of M bytes never used`, so the size can
|
||||
be re-tuned from device measurements.
|
||||
|
||||
Sizing: this is an estimate from static data, not a device measurement. It
|
||||
comes from a `-fstack-usage` build of the S3 16 MB configuration with IDF v5.5.
|
||||
|
||||
- **Frame sizes along the OTA path** (bytes):
|
||||
|
||||
| function | bytes |
|
||||
|---|---|
|
||||
| `httpd_thread` | 144 |
|
||||
| `httpd_sess_process` | 32 |
|
||||
| `httpd_req_new` | 176 |
|
||||
| `httpd_uri` | 64 |
|
||||
| `ota_upload_handler` | 192 (1,216 with the old 1 KB buffer) |
|
||||
| `esp_ota_end` | 48 |
|
||||
| `image_validate` | 304 |
|
||||
| `esp_image_verify` | 32 |
|
||||
| `image_load` | 80 |
|
||||
| `process_segments` | 112 |
|
||||
| SHA-256 | about 110 |
|
||||
| mmap/flash read | about 190 |
|
||||
|
||||
- That chain comes to about 1.5 KB now, and about 2.5 KB with the old buffer.
|
||||
- Two things are not in the static data:
|
||||
- libc's `vfprintf` behind every `ESP_LOG`. This build uses full newlib,
|
||||
not nano, and the old progress line formatted a float.
|
||||
- The flash-chip driver, which is reached through function pointers.
|
||||
|
||||
Together they plausibly add 1 to 1.5 KB. That explains an overflow at 4096
|
||||
with the old handler.
|
||||
- The WASM upload path adds `wasm_upload_process` (336) and
|
||||
`rvf_verify_signature` (352), plus a wasm3 module load that wasn't measured.
|
||||
- 8192 leaves roughly 4 to 5 KB above the estimated worst case, and costs
|
||||
4 KB of internal RAM. The high-water-mark log is the check on that estimate.
|
||||
|
||||
When the build has no `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` (the 8 MB S3 and
|
||||
4 MB C6 CI lanes), both entry points compile to no-ops.
|
||||
|
||||
## Consequences
|
||||
|
||||
- OTA updates become durable on rollback-enabled nodes once the image passes
|
||||
the check. A second OTA is accepted after confirmation, which can come as
|
||||
early as `MIN_UPTIME_S`.
|
||||
- A bad image now reverts instead of limping, but "bad" only means it failed
|
||||
to reach the network or deliver CSI. An image that passes and then
|
||||
misbehaves later is not caught.
|
||||
- If the AP is down when a node boots an OTA'd image, a good image rolls back.
|
||||
That is the safe direction: push it again later.
|
||||
- Mock/QEMU builds that skip Wi-Fi can never pass. They must not enable
|
||||
bootloader rollback, and none do today.
|
||||
- Bootloader capability is still invisible remotely (RUNBOOK §2). A node
|
||||
whose bootloader lacks rollback reports `ota_state: "new"` after an OTA. That
|
||||
is the first remote hint of which bootloader a board has.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Host test `firmware/esp32-csi-node/test/test_ota_health.c` runs in
|
||||
`make host_tests`, which CI runs. It covers pass, each timeout reason, the
|
||||
terminal verdict, force-fail, the clamp, and zero soak. SYNTHETIC: it
|
||||
exercises the decision function, not a device.
|
||||
- ESP-IDF v5.5 S3 and C6 builds with rollback enabled link
|
||||
`esp_ota_mark_app_valid_cancel_rollback` (verified with `nm`). A build is not
|
||||
hardware evidence.
|
||||
- The httpd overflow is MEASURED on hardware (node 4, above).
|
||||
- Hardware verification, MEASURED on 2026-09-29 on node 4 (ESP32-S3,
|
||||
16 MB, rollback bootloader, OTA key provisioned). The
|
||||
build from this branch was first written over USB. The pushes were made from
|
||||
a Pi 5 with the `sensor-ota-push` cog.
|
||||
1. Positive (MEASURED, S3). An image built from this branch was pushed to `ota_1`: HTTP 200,
|
||||
1,230,848 B in 22 s.
|
||||
- The node logged "httpd stack after POST /ota: 4900 of 8192 bytes never
|
||||
used", so the upload's peak stack use is about 3.3 KB. With the old 1 KB
|
||||
on-stack buffer that is over 4 KB, which is consistent with the
|
||||
overflow.
|
||||
- It rebooted and logged "OTA image pending verify on ota_1".
|
||||
- `/ota/status` then read `ota_state: valid`.
|
||||
- After a hard reset it came back on the same build in `ota_1`, still
|
||||
`valid`, so the update is durable.
|
||||
2. Negative (MEASURED, S3 forced-fail rollback). A `CONFIG_OTA_HEALTH_FORCE_FAIL=y` build was pushed to `ota_0`:
|
||||
HTTP 200.
|
||||
- `/ota/status` showed it running as `pending_verify` for about 30 s.
|
||||
- The node then rebooted and came back on the previous build in `ota_1`,
|
||||
`valid`, so rollback works.
|
||||
3. One push attempt failed mid-upload when a serial capture was opened on the
|
||||
node's USB-Serial-JTAG port at the same moment. The node reset and was
|
||||
unharmed. Don't hold the USB console open while pushing OTA to a
|
||||
USB-attached node.
|
||||
- C6 positive, MEASURED on 2026-09-29 on node 5 (ESP32-C6, 8 MB
|
||||
flash, 4 MB layout, rollback bootloader).
|
||||
- The branch build was first written over USB and the OTA key provisioned.
|
||||
- A second build was pushed from the Pi 5 cog to `ota_1`: HTTP 200 in 11 s.
|
||||
- `/ota/status` showed `pending_verify`, then `valid` about 28 s later.
|
||||
- After a hard reset it stayed on the new build, `valid`.
|
||||
- Not yet verified, so CLAIMED (no log supports these):
|
||||
- the 120 s no-IP timeout path (CLAIMED);
|
||||
- a true power cycle (CLAIMED; a hard reset via RTS was used instead);
|
||||
- the C6 negative/rollback test (CLAIMED; run on the S3 only).
|
||||
- A pass of this check means only that the node reached the network and sent
|
||||
CSI. It does not validate sensing quality, calibration, or anything else the
|
||||
image does.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ADR 380: Loopback UDP tee for second consumers of the CSI stream
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. Unit tested, and smoke tested on loopback: a datagram sent to the
|
||||
server arrived byte for byte at the tee target. Not yet exercised against a
|
||||
live multi node fleet.
|
||||
|
||||
Date: 2026-09-28
|
||||
|
||||
Numbering: On 2026-10-02 `main` had up to ADR 378 and open PRs claimed nothing
|
||||
higher; ADR 379 is the OTA first-boot health check. If another branch also
|
||||
takes 380, renumber whichever merges second.
|
||||
|
||||
## Context
|
||||
|
||||
A five node deployment (four ESP32 S3, one ESP32 C6) streams ADR 018 frames to
|
||||
one sensing server. A second consumer cannot see the raw stream.
|
||||
|
||||
Each node sends to a single `target_ip` (`firmware/esp32-csi-node/main/stream_sender.c`).
|
||||
The server's WebSocket and REST outputs carry per node amplitude only: no
|
||||
phase, no I/Q, no sequence number, and no `0xC511A110` sync packets. A fusion
|
||||
host that aligns CSI with camera, radar, or lidar needs all of those. Binding a
|
||||
second listener to the CSI port is not an option, and an external relay in
|
||||
front of the port would make every source loopback, which the ADR 296
|
||||
allowlist always admits.
|
||||
|
||||
## Decision
|
||||
|
||||
`--udp-tee` (env `RUVIEW_UDP_TEE`) copies every datagram the source allowlist
|
||||
admits, byte for byte, to up to four `ip:port` targets.
|
||||
|
||||
- Targets must be loopback, so raw CSI never leaves the host through the tee.
|
||||
- A target may not use port 0 or the listener's own port, which would loop.
|
||||
- Sends are non blocking. A copy that cannot be queued is dropped and counted,
|
||||
so a slow consumer never stalls the receive loop.
|
||||
- The copy happens after the ADR 296 allowlist check and before any parsing,
|
||||
so consumers see exactly what the server admitted, in every wire format.
|
||||
- The tee is off unless configured.
|
||||
|
||||
## Consequences
|
||||
|
||||
A fusion host or Cognitum cog can subscribe to the full data plane by reading
|
||||
a loopback port. A consumer that today binds the CSI port itself can keep its
|
||||
code and move to the tee port instead.
|
||||
|
||||
The tee is byte transparent, so it needs no change when the ADR 018 header or
|
||||
a later wire version changes.
|
||||
|
||||
Each target costs one `send_to` per admitted datagram. Four targets is the
|
||||
limit.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-sensing-server --no-default-features --lib udp_tee
|
||||
```
|
||||
@@ -0,0 +1,272 @@
|
||||
# ADR-381: Node self-survey from pairwise ranging (check mode first)
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-10-04
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: node-positions, ranging, ftm, uwb, multistatic, least-authority
|
||||
- **Related**: ADR-018 (ESP32 wire format), ADR-110 (ESP-NOW time sync),
|
||||
ADR-141 (privacy control plane), ADR-144 (UWB range-constraint fusion),
|
||||
ADR-281 (BLE Channel Sounding ranging evidence), ADR-308 (placement
|
||||
optimizer), rufield ADR-261 (BLE evidence and Channel Sounding adapter
|
||||
boundary, `vendor/rufield/docs/ADR-261-ble-evidence-and-channel-sounding.md`)
|
||||
- **Issues and PRs**: #1804, #1866, #2109 (show which nodes use the default
|
||||
position), #2133 (give the governed fuser the configured node positions)
|
||||
|
||||
## Context
|
||||
|
||||
Node positions reach the sensing server only through `--node-positions`. The
|
||||
operator types it by hand, keyed by node id (`id:x,y,z;...`) or by list index
|
||||
when the prefix is omitted. Three kinds of mistake pass silently today:
|
||||
|
||||
- **Wrong ids.** A config keyed by ids that do not match the live nodes looks
|
||||
applied and does nothing (#1804, #1866). #2109 makes the fallback to the
|
||||
default position visible, and #2133 makes sure both fusers receive the
|
||||
map. Neither can tell whether a correctly-keyed position is *right*.
|
||||
- **Wrong units.** Positions measured in feet and typed as metres are 3.28×
|
||||
too large. Nothing checks the scale.
|
||||
- **Stale positions.** A node moved after setup keeps its old coordinates.
|
||||
|
||||
Each one degrades multistatic fusion without an error. The repository has no
|
||||
node-to-node ranging and no anchor-free solver. ADR-144 models
|
||||
anchor-to-*tag* UWB ranges for people tracking
|
||||
(`wifi-densepose-mat/src/localization/range_constraint.rs`), not ranges
|
||||
between the fixed nodes themselves. ADR-281 computes BLE Channel Sounding
|
||||
ranging evidence, but there is no CS-capable radio in the fleet: rufield
|
||||
ADR-261 notes that the ESP32-S3 does not implement CS.
|
||||
|
||||
The ESP32-S3 and C6 nodes do support Wi-Fi Fine Timing Measurement (802.11mc
|
||||
RTT, station initiator ↔ SoftAP responder) in mainline ESP-IDF. So a ranging
|
||||
source exists on hardware we already have. The question is how good it is.
|
||||
|
||||
### What FTM measured
|
||||
|
||||
Bench, 2026-10-04 (UTC): two ESP32-S3 boards (rev v0.2), ESP-IDF v5.5
|
||||
`examples/wifi/ftm`, SoftAP responder on channel 1 at 20 MHz, 32 FTM frames
|
||||
per session, line of sight, tape ground truth. All rows MEASURED; the
|
||||
reproducer is in Appendix A.
|
||||
|
||||
| Tape (m) | Result |
|
||||
|---|---|
|
||||
| 0.00 | median 0.00 m |
|
||||
| 0.76 | median 0.00 m. Readings clamp to 0 below about 0.8 m. The first three sessions after a responder start read 1.8 to 3.0 m |
|
||||
| 1.14 | five responder starts: medians 0.60, 1.50, 2.25 and 3.08 m, plus a 7.7 m run at −68 dBm with a 1.92 m session spread (placement outlier) |
|
||||
| 3.71 | median 7.80 m (one start) |
|
||||
|
||||
- **Precision within one responder start:** ±0.2 to 0.4 m (standard
|
||||
deviation 0.19 to 0.31 m). A 40-session run over about two minutes showed
|
||||
no drift beyond that.
|
||||
- **Bias between responder starts:** −0.5 to +1.9 m at 1.14 m, with the
|
||||
boards untouched. A plain-versus-auto-start responder A/B test showed the
|
||||
responder build is not the cause.
|
||||
- **Quantisation:** every distance is a multiple of 0.15 m (1 ns of RTT).
|
||||
- **Not tested:** 40 MHz, other channels, `ftm -R -o` offset calibration, a
|
||||
second pair, non-line-of-sight, CSI coexistence, raw t1..t4 timestamps.
|
||||
|
||||
FTM is precise inside one start and inaccurate across starts. It can catch
|
||||
mistakes worth metres. It cannot place a node to decimetres. UWB (Qorvo
|
||||
DW3000 family) is the expected accuracy path: about 0.1 m line of sight is
|
||||
CLAIMED by the vendor, and we have no measurement of our own yet.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. A source-agnostic `RangeObservation` record
|
||||
|
||||
```text
|
||||
RangeObservation {
|
||||
initiator: u8, // node_id, as in --node-positions
|
||||
responder: u8,
|
||||
range_m: f64, // session median
|
||||
sigma_m: f64, // session spread, 0 if unknown
|
||||
method: Ftm | Uwb | BleCs | Manual,
|
||||
samples: u16, // valid frames / exchanges
|
||||
rssi_dbm: Option<i16>,
|
||||
bandwidth_mhz: Option<u16>,
|
||||
at_us: u64, // host receive time
|
||||
responder_boot: Option<u32>, // boot counter or nonce of the responder
|
||||
calibrated: bool, // set only by the host calibration step
|
||||
}
|
||||
```
|
||||
|
||||
`responder_boot` is required for calibration. The FTM bias moved with every
|
||||
responder start, so an offset is only valid within the boot it was measured
|
||||
in.
|
||||
|
||||
On the wire, nodes report ranges either in a new ADR-018-style packet with
|
||||
its own magic, or as an extension of the existing sync packet. This ADR does
|
||||
not choose between them. The host-side record and solver come first. The
|
||||
wire format follows the first firmware that emits ranges.
|
||||
|
||||
### 2. Host-side check mode (warn-only)
|
||||
|
||||
Given the configured positions and a batch of observations, check mode
|
||||
reports:
|
||||
|
||||
- **Per-link residuals.** `measured − ‖p_a − p_b‖`, with a tolerance of
|
||||
`floor + k·σ`. σ is the larger of the median session sigma and the spread
|
||||
across sessions. Each link is `ok`, `outlier`, `too_short` (configured
|
||||
length below the source's minimum), `weak` (every session below the RSSI
|
||||
gate), or `noisy` (σ above the source's maximum).
|
||||
- **Unit mismatch.** A least-squares scale `s` in `measured ≈ s·expected`.
|
||||
If `s` sits within a band of 0.3048 (feet), 0.0254 (inches), 0.01 (cm) or
|
||||
0.001 (mm), and rescaling removes outliers, the check reports the unit.
|
||||
- **Id swap.** For up to 8 ranged nodes, it scores every assignment of the
|
||||
configured positions to ids. The cost is a truncated quadratic in residual
|
||||
over tolerance, so one bad link cannot dominate. If another assignment
|
||||
cuts the cost to at most half and the configured cost is material, the
|
||||
check suggests that assignment, reported as `(node, position_of)` pairs.
|
||||
- **Moved node.** If most of a node's links are outliers (at least 2, and a
|
||||
strict majority), the node was probably moved. Outlier links left over are
|
||||
reported one at a time as possible bias, a body or wall in the path, or a
|
||||
bad session, and not as a configuration error.
|
||||
- **Unknown and unranged nodes.** Observations naming unconfigured ids, and
|
||||
configured nodes that no scored link reaches.
|
||||
|
||||
Distances do not change under rotation, reflection or translation, so check
|
||||
mode compares distances and never coordinates. Where the layout is
|
||||
symmetric, it says so instead of guessing:
|
||||
|
||||
- A relabelling that is a symmetry of the layout is **invisible**. For
|
||||
example, ids rotated 180° in a rectangle of corner nodes. Check mode
|
||||
reports such a layout as consistent.
|
||||
- When several assignments fit equally well, check mode prefers the one that
|
||||
changes the fewest ids, since a typo moves few. It reports how many
|
||||
assignments tied. If the fewest-changes assignments are themselves tied,
|
||||
as with a swap along one wall of a rectangle (its mirror image is the
|
||||
opposite wall's swap), it reports `id_swap_ambiguous` and names nothing.
|
||||
|
||||
Corner-mounted nodes in a rectangular room are the common case and are
|
||||
nearly symmetric. Operators should expect ambiguity reports there, and a
|
||||
fifth node off the axes removes most of them.
|
||||
|
||||
**Authority.** Check mode never writes positions, never edits config, and
|
||||
never changes fusion. Its output is a report for the startup log and a
|
||||
read-only status field. The report shape is JSON-serialisable; the field
|
||||
name follows #2109's `node_positions` block once that lands.
|
||||
|
||||
**Tolerances come from measured data:**
|
||||
|
||||
| Source | `floor_m` | `calibrated_floor_m` | `max_sigma_m` | `min_expected_m` | Basis |
|
||||
|---|---|---|---|---|---|
|
||||
| FTM, ESP32-S3, 20 MHz | 2.0 | 0.6 | 1.0 | 1.0 | MEASURED, bench above: bias −0.5 to +1.9 m between starts; spread ≤ 0.31 m within a start (the 1.92 m outlier excepted); clamp below about 0.8 m |
|
||||
| UWB | 0.3 | 0.3 | 0.3 | 0.0 | ESTIMATE until our own UWB bench. Vendor accuracy is CLAIMED |
|
||||
|
||||
With `floor_m = 2.0`, the one 3.71 m start (+4.1 m) would be flagged as a
|
||||
single-link outlier. That is uncalibrated FTM as it stands. Single-link
|
||||
outliers stay low-severity on purpose, and the strong findings (unit, swap,
|
||||
moved node) need several links to agree.
|
||||
|
||||
### 3. Per-boot calibration
|
||||
|
||||
An offset is recorded per `(initiator, responder, responder_boot)` from
|
||||
sessions at a known reference distance of at least 1.0 m:
|
||||
`offset = median(range) − reference`. It applies only to observations with
|
||||
the same key. After a responder reboot, observations pass through
|
||||
uncalibrated and get the wider floor.
|
||||
|
||||
Zero-separation calibration (ESP-IDF's `cm0` / `ftm -R -o`) is not used,
|
||||
because short ranges clamp to 0 on this hardware. In practice, the reference
|
||||
is one tape-measured link per responder per boot, or a UWB link once UWB
|
||||
exists. That the bias is stable over a whole boot, and not only for two
|
||||
minutes, is untested (ESTIMATE). Measuring it is the first FTM follow-up.
|
||||
|
||||
### 4. Fill mode, gated off
|
||||
|
||||
Fill mode (solve positions with classical-MDS initialisation, weighted
|
||||
robust SMACOF and gauge fixing, then write a file the operator passes to
|
||||
`--node-positions`) is **not implemented** and stays disabled for a source
|
||||
until all of these hold:
|
||||
|
||||
- the source has **MEASURED** link-error evidence with a reproducer;
|
||||
- the evidence covers at least 6 ground-truth links;
|
||||
- the p90 link error is **≤ 0.3 m**.
|
||||
|
||||
FTM's measured errors are metres, so it fails this gate. UWB is expected to
|
||||
pass it, but that is unproven. Even when the gate opens, fill mode writes a
|
||||
proposal file. The server never rewrites its own positions.
|
||||
|
||||
### 5. Pluggable sources
|
||||
|
||||
Every source produces `RangeObservation`, and check mode does not care which
|
||||
one did. Only the tolerance profile changes.
|
||||
|
||||
| Source | Status | How it plugs in |
|
||||
|---|---|---|
|
||||
| **FTM** (ESP32-S3/C6) | Now, for check mode | APSTA node runs a short scheduled survey window. One node is the SoftAP responder and the others initiate a directed session. `rtt_ns`, RSSI and session spread map to `range_m`, `rssi_dbm`, `sigma_m`. Airtime cost on the CSI channel is unmeasured |
|
||||
| **UWB** (DW3000 family, e.g. DWM3001CDK over USB) | After its own bench | Two-way ranging between nodes. Shares hardware with ADR-144's anchor-tag model, but node↔node ranges feed this ADR and anchor↔tag ranges keep feeding ADR-144 |
|
||||
| **BLE CS** | When a CS-capable radio exists | Through the rufield ADR-261 adapter boundary, using ADR-281's cross-validated phase/RTT ranging. A `Divergent` anomaly drops the observation. Single-mechanism evidence gets the wider floor |
|
||||
| **Manual** | Now | A taped distance entered by the operator. Mainly useful as the per-boot calibration reference |
|
||||
|
||||
### 6. Prototype
|
||||
|
||||
`v2/crates/ruview-survey` implements sections 1 to 4 on the host, with no
|
||||
I/O, no clock and no randomness, and bounded inputs: 64 nodes, 10 000
|
||||
observations, 100 m range, and the swap search capped at 8 nodes.
|
||||
Integration with the sensing server is a follow-up and depends on #2109 for
|
||||
where the report surfaces. The crate's tests are SYNTHETIC: generated layouts
|
||||
with FTM-like bias and noise from the envelope above. They cover consistent
|
||||
configs, feet and centimetre mistakes, a two-id swap, a three-id rotation,
|
||||
symmetric invisibility and ambiguity, a moved node, a bench-style single-link
|
||||
outlier, a noisy link, per-boot calibration, and the fill gate.
|
||||
`examples/check_ranges.rs` runs check mode on a JSONL file.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An operator gets a warning when ranges and `--node-positions` disagree
|
||||
grossly, with no new authority: check mode is read-only.
|
||||
- FTM on nodes needs APSTA and a SoftAP beacon on the sensing channel. Its
|
||||
cost to CSI yield must be measured before the firmware side ships.
|
||||
- The FTM tolerances rest on one evening with one pair. They are parameters,
|
||||
not constants, and must be revised from more data (40 MHz, more pairs,
|
||||
non-line-of-sight).
|
||||
- Ranges between fixed nodes are not person data. Ranges to phones or tags
|
||||
are, and remain under ADR-141 and ADR-144.
|
||||
- A symmetric layout can hide a relabelling entirely. Check mode documents
|
||||
this and does not claim to catch it.
|
||||
|
||||
## Evidence rule
|
||||
|
||||
No self-survey accuracy claim without a MEASURED reproducer: tape or laser
|
||||
ground truth, at least 4 nodes, line of sight plus at least one
|
||||
non-line-of-sight case, every number tagged. The prototype's tests are
|
||||
SYNTHETIC and show logic, not radio performance.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Treat FTM as accurate enough to fill positions.** Rejected: the measured
|
||||
bias between starts (−0.5 to +1.9 m at 1.14 m) and the +4.1 m at 3.71 m
|
||||
rule it out.
|
||||
- **Drop FTM.** Rejected for now. It costs nothing on existing hardware, it
|
||||
catches metre-scale mistakes, and it may suit coarse gating, calibrated
|
||||
pairs within a boot, and time sync. It is parked and revisited after UWB.
|
||||
- **Zero-separation `cm0` calibration.** Rejected: readings clamp to 0 below
|
||||
about 0.8 m.
|
||||
- **Server auto-corrects positions.** Rejected under least authority. Fill
|
||||
mode, when enabled, writes a proposal file only.
|
||||
- **LoRa SX1280 ranging, router (MT7981) FTM, raw I/Q phase ranging,
|
||||
CSI-amplitude ranging.** Rejected or deferred: wrong distance regime, no
|
||||
driver support, unproven, or too inaccurate respectively.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does one calibration offset hold for a whole responder boot? For both
|
||||
roles? For other initiators against the same responder?
|
||||
- Is the post-start transient (1.8 to 3.0 m in the first three sessions at
|
||||
0.76 m) real, and how long does it last?
|
||||
- Does 40 MHz shrink the bias between starts?
|
||||
- What does an FTM survey window cost in CSI frames on the same channel?
|
||||
- Can FTM t1..t4 timestamps be tied to the CSI timestamp clock for sync?
|
||||
- The wire format: new magic, or an extension of the sync packet?
|
||||
|
||||
## Appendix A: FTM bench reproducer
|
||||
|
||||
1. Flash ESP-IDF v5.5 `examples/wifi/ftm` on two ESP32-S3 boards
|
||||
(`CONFIG_ESP_WIFI_FTM_ENABLE=y`, the example's default).
|
||||
2. Responder console: `ap <ssid> <password>` (channel 1, 20 MHz defaults).
|
||||
Use throwaway credentials and do not commit them.
|
||||
3. Initiator console, at a taped separation: `ftm -I -s <ssid>`, 32 frames,
|
||||
10 to 40 sessions per distance. Record `Avg raw RTT`, `Avg RSSI`,
|
||||
`Estimated Distance` and the valid-frame count for each session.
|
||||
4. Reset the responder and repeat at the same distance to see the bias move
|
||||
between starts.
|
||||
5. Report the median, spread and error per run, tagged MEASURED, with the
|
||||
raw per-session data alongside.
|
||||
@@ -0,0 +1,137 @@
|
||||
# ADR-382: `spatial.evidence.v1` export of RF Gaussians and link observations
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-03
|
||||
- **Deciders**: RuView maintainers
|
||||
- **Tags**: rf, gaussian, spatial, evidence, wire-format, interop
|
||||
- **Related**: ADR-273 (unified RF world model), ADR-275 (RF-aware Gaussian
|
||||
memory), ADR-142 (temporal voxel evidence). Origin of the wire format:
|
||||
WeftOS ADR-107 §7 ("Spatial evidence engine and workspace"), amended
|
||||
2026-10-03.
|
||||
- **Crate**: `v2/crates/ruview-spatial-evidence`
|
||||
|
||||
Numbering: on 2026-10-03 `main` had up to ADR-378, open PRs claimed 379
|
||||
and 380, and 381 was taken by another branch.
|
||||
|
||||
## Context
|
||||
|
||||
`ruview-unified` keeps a map of RF Gaussians (ADR-275) and a Friis channel
|
||||
model that turns a TX/RX link into an expected amplitude. A spatial evidence
|
||||
engine fuses many sensor types (tape measurements, radar, ToF, UWB, RF) into
|
||||
an occupancy octree and promotes stable geometry (walls, furniture) into room
|
||||
models. That engine reads a versioned JSONL format, `spatial.evidence.v1`,
|
||||
specified in WeftOS ADR-107 §7.
|
||||
|
||||
RuView should be able to feed it without depending on it, and without a
|
||||
reader of this repository having to go to another project to learn what the
|
||||
export writes. This ADR records the part of the contract RuView emits.
|
||||
|
||||
## Decision
|
||||
|
||||
1. A leaf crate, `ruview-spatial-evidence`, converts `ruview-unified` output
|
||||
to `spatial.evidence.v1` lines. Its only internal dependency is
|
||||
`ruview-unified`. The wire shapes are local serde structs; the contract is
|
||||
the JSON, not a shared crate.
|
||||
2. It emits two record types, `rf_gaussian` and `rf_link_observation`. It
|
||||
does not mirror the other v1 types and rejects them as unknown when
|
||||
parsing.
|
||||
3. Every record is validated against the v1 rules below before it is
|
||||
serialised, so a bad value fails in RuView rather than in the consumer.
|
||||
4. If WeftOS ADR-107 §7 changes the envelope or either RF body, this ADR is
|
||||
amended in the same change that updates the crate.
|
||||
|
||||
### Envelope (every record)
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `schema` | exactly `spatial.evidence.v1` |
|
||||
| `type` | `rf_gaussian` or `rf_link_observation` here |
|
||||
| `t_ns` | u64, ns since the Unix epoch |
|
||||
| `frame` | `room_enu` |
|
||||
| `region` | region id, e.g. `region/urth/meso/<room>` |
|
||||
| `source_id` | device or operator id, never a person |
|
||||
| `uncertainty_m` | 1σ position uncertainty, metres, in (0, 100] |
|
||||
| `provenance` | `{receipt, producer, proof}`; `proof` is `MEASURED`, `CODE` or `SYNTHETIC` |
|
||||
|
||||
Ids (`region`, `source_id`, `receipt`, `producer`) match
|
||||
`[A-Za-z0-9._:/-@]{1,128}`. A line is at most 16 KiB.
|
||||
|
||||
**Frame.** `room_enu` is room-local, origin at the room's SW floor corner,
|
||||
x east, y north, z up, metres. Coordinates are within ±1000 m.
|
||||
|
||||
**Angles.** Every yaw in v1 is degrees counter-clockwise from room +x
|
||||
(east): 0 faces +x, 90 faces +y. The two RF types have no yaw field. The
|
||||
`rf_gaussian` orientation is a body-to-room quaternion, and a positive
|
||||
rotation about +z turns the Gaussian's local x axis counter-clockwise from
|
||||
east, the same sense.
|
||||
|
||||
### `rf_gaussian`
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `position` | [x, y, z] m |
|
||||
| `scale` | per-axis σ, m, each in [1e-6, 1e4] |
|
||||
| `orientation` | quaternion `[w, x, y, z]`, finite, norm > 1e-6 |
|
||||
| `occupancy` | peak extinction, nepers/m, in [0, 1e6] |
|
||||
| `confidence` | [0, 1] |
|
||||
| `motion` | `static`, `slow` or `fast` |
|
||||
| `role`? | `absorber` or `reflector`; set only on priors sent back to RuView |
|
||||
|
||||
The exporter maps `RfGaussian` field for field. `uncertainty_m` is the
|
||||
geometric mean of the three σ, clamped to the envelope range, because
|
||||
`RfGaussian` has no separate position covariance. The receipt is
|
||||
`gauss:<device>:<t_ns>:<index>`. A Gaussian whose provenance is `synthetic`
|
||||
is always tagged `SYNTHETIC`; a caller can lower the proof tag, never raise
|
||||
it.
|
||||
|
||||
### `rf_link_observation`
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `tx`, `rx` | antenna positions, m |
|
||||
| `freq_hz` | [1e8, 3e11] |
|
||||
| `excess_loss_db` | `20·log10(free / measured)`, in [−60, 200]; negative is constructive multipath |
|
||||
|
||||
The free-space reference comes from `ruview-unified`'s channel model with an
|
||||
empty map, so it is the same Friis amplitude the ADR-275 inverse update uses.
|
||||
|
||||
### Examples
|
||||
|
||||
These two lines are the WeftOS ADR-107 §7 examples. The crate's tests parse
|
||||
them and check that its own output has the same shape.
|
||||
|
||||
```jsonl
|
||||
{"schema":"spatial.evidence.v1","type":"rf_link_observation","t_ns":1759500001300000000,"frame":"room_enu","region":"region/urth/meso/test-room","source_id":"node-2","uncertainty_m":1.0,"provenance":{"receipt":"csi:node1-node2:000881","producer":"ruview-adapter@0.1","proof":"MEASURED"},"tx":[6.20,3.50,1.10],"rx":[0.20,0.20,1.10],"freq_hz":2437000000.0,"excess_loss_db":4.2}
|
||||
{"schema":"spatial.evidence.v1","type":"rf_gaussian","t_ns":1759500002000000000,"frame":"room_enu","region":"region/urth/meso/test-room","source_id":"ruview-unified","uncertainty_m":0.5,"provenance":{"receipt":"gauss:7f3a","producer":"ruview-unified@0.3","proof":"CODE"},"position":[3.2,0.0,1.2],"scale":[0.05,1.6,1.2],"orientation":[1,0,0,0],"occupancy":0.7,"confidence":0.8,"motion":"static","role":"absorber"}
|
||||
```
|
||||
|
||||
### Versioning
|
||||
|
||||
Additive optional fields may appear inside v1 and older readers ignore them.
|
||||
New record types may also be added inside v1 (the 2026-10-03 amendment added
|
||||
`tof_depth` and `radar_range`, and an optional `sensor` block on
|
||||
`radar_track_point`); a reader that does not know a type rejects it as
|
||||
unknown rather than misreading it. Removing, renaming or changing the
|
||||
meaning of a field requires `spatial.evidence.v2`, and the parser reports a
|
||||
v2 line as a version mismatch.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView output can feed a spatial evidence engine with no new runtime
|
||||
dependency, and the contract is readable from this repository.
|
||||
- The engine can send promoted walls and objects back as `rf_gaussian`
|
||||
priors with a `role`. That gives ADR-275's inverse update a geometric
|
||||
starting point instead of an empty map.
|
||||
- The struct definitions are duplicated between the two projects. The
|
||||
golden-line tests catch drift in the RF types; a change on the WeftOS side
|
||||
still needs someone to amend this ADR and the crate.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Nothing calls the crate yet. The sensing server does not export records,
|
||||
and there is no adapter that turns an inbound `rf_gaussian` prior into an
|
||||
`RfGaussian` in the live map.
|
||||
- The tests are CODE-level (deterministic conversions and validation). No
|
||||
MEASURED claim is made about what the exported evidence does to a room
|
||||
model.
|
||||
- `uncertainty_m` for a Gaussian is a stand-in derived from its extent.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-383: Reject malformed HTTP request targets without stopping the transport
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
ADR-375 introduces the authenticated Streamable HTTP transport. Node accepts an HTTP request target that the WHATWG URL parser rejects. The current request listener lets that parser exception escape, terminating the process before route selection. A bounded loopback fixture using the public `startMcpHttp` API reproduces this with `http://[`; the original code resets the connection and exits.
|
||||
|
||||
## Decision
|
||||
|
||||
Catch URL construction failures at the request boundary and return HTTP 400 with the generic `invalid_request_target` error. Do not include the request target or parser exception in the response, since the target can carry a secret path. Keep route selection, token checks, origin policy, grants, protocol negotiation and JSON handling unchanged.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Relying on Node's HTTP parser does not suffice: a syntactically accepted request target can still fail URL construction. A process-level exception handler would obscure unrelated faults and would not provide a request-specific response.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED software fixture: the actual child-process HTTP transport fails the new regression on the original source (nine existing controls pass); the patched transport passes all ten focused tests. The new test also verifies subsequent health, rejected unauthenticated calls and successful authenticated initialization. Broader package checks are recorded separately against the final implementation revision. No external service, device, hardware, throughput or production deployment claim is made.
|
||||
|
||||
## Consequences
|
||||
|
||||
Malformed URL targets receive a stable client error and do not interrupt later requests. This change does not add SSE or sessions, change authentication, or authorize additional operations.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-384: Preserve explicitly supplied SDK search limits
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The public `memorySearch(query, limit)` helper uses a truthiness check before forwarding its optional limit. Explicit zero and NaN therefore disappear, even though the shared registry rejects them. The helper silently returns success where the equivalent raw SDK call returns `invalid_arguments`; strict SDK users also miss the expected `RuViewError`.
|
||||
|
||||
## Decision
|
||||
|
||||
Omit the optional limit only when it is undefined. Forward every explicitly supplied value to the existing shared validator. Do not duplicate validation or change the registry range, search algorithm, result count, strict error policy or defaults.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Validating separately in the helper would duplicate schema rules and risk drift. Retaining truthiness makes invalid caller inputs indistinguishable from an omitted option.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED local software regression exercises the public SDK and actual registry. Original code fails for zero; the corrected helper rejects zero, NaN, negative and excessive limits, preserves strict errors, and matches raw registry results for omitted and valid limits. Package gate receipts are recorded separately against the final revision. No external retrieval, training, hardware or production service claim is made.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-385: Stop watch mode on permanent input errors in every output mode
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The CLI's `esp32 --watch` repeats short capture windows. Permanent bind/port errors stop the terminal view, but the piped/JSON branch continues before reaching that check. Schema-level invalid arguments also never stop either branch. Actual CLI children with a fractional UDP port or rejected bind value repeatedly emit errors despite having no usable capture configuration. These fixtures fail before opening a socket or accessing a device.
|
||||
|
||||
## Decision
|
||||
|
||||
Classify existing permanent port/bind errors and schema-invalid arguments before selecting output mode. Emit the error once and return exit status 1 for both terminal and JSON output. Keep repeated capture behavior for nonterminal results, including windows with no CSI packets; retain the normal watch output contract.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Adding a delay would still repeatedly retry an unchanged invalid configuration. Stopping every unsuccessful capture would incorrectly terminate legitimate empty capture windows.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED bounded actual CLI regression checks piped `--watch --json` with invalid inputs, one structured failure and nonzero exit. No network socket, hardware, external provider or live capture is exercised. Whole-package validation receipts accompany the final revision.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-386: Recover CSI packet loss by missing sequence identity
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The RF capture diagnostics report CSI packet loss and account for UDP reordering. The existing tracker decrements its gap count for any late sequence while gaps exist. A duplicated recovery or previously received packet therefore erases a different missing packet. For example, sequences 100, 103, 101, 101 leave 102 missing but currently report zero loss. Accurate loss diagnostics matter when distinguishing transport problems from healthy sensing input.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain the identities of counted missing sequences within the existing 1024-sequence reorder window. Recover a gap only when deleting its identity succeeds, so a duplicate cannot recover it twice. Prune identities outside the window without erasing their historical gap counts. A confirmed counter reset clears recovery identities but preserves accumulated loss. Keep the existing frame count/rate, loss denominator, stray/reset classification and packet format unchanged.
|
||||
|
||||
## Alternatives
|
||||
|
||||
A duplicate-only set cannot distinguish packets observed before the capture baseline from genuinely missing ones. Keeping all missing identities indefinitely is unnecessary and could grow without bound.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED software fixtures use byte-accurate RAC1 envelopes, the public capture API and real loopback UDP. Regressions cover duplicated recoveries, already-received packets, pre-capture packets and distinct valid recoveries. A direct public parser/NodeStats control checks bounded bookkeeping and reset semantics. Existing sequence/stray/reset tests remain applicable. This is a software diagnostic correction, not physical RF, device throughput or physiological accuracy validation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Loss no longer disappears when unrelated late packets arrive. Recovery remains limited to the established reorder window; older gaps stay counted. Stored identities are bounded per node.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-389: Measure CSI rates over the observed capture interval
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (receive-only device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The public `captureEsp32` producer may stop at `max_packets` before its configured
|
||||
`seconds` deadline. Its node CSI rates still divided received packets by that
|
||||
requested deadline. Two packets received in approximately 150 milliseconds with a
|
||||
30-second deadline therefore appeared as 0.07 Hz, misleading operators evaluating
|
||||
whether a node streams normally.
|
||||
|
||||
## Decision
|
||||
|
||||
Start a monotonic clock when the UDP socket successfully binds. At completion,
|
||||
calculate `elapsedSeconds` and use it as the denominator for node CSI rates.
|
||||
Retain `seconds` as the configured deadline for compatibility and expose the
|
||||
observed interval separately. Close and analysis processing time are excluded.
|
||||
A socket error before binding has a zero observed interval and no received data.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
A deterministic-format ESP32 fixture is transmitted through an owned loopback UDP
|
||||
socket into the public capture function. The baseline fails because the observed
|
||||
interval is absent and the rate is 0.07 Hz. The fixed capture checks early
|
||||
termination, preserves the deadline, and checks the rate against observed elapsed
|
||||
time. Existing CSI decoding and capture tests remain controls. This is software
|
||||
transport verification; it makes no radio, hardware, sensing-accuracy, or firmware
|
||||
throughput claim. Normal deadline captures may report a slightly longer interval
|
||||
because timer callbacks are scheduled by the host event loop.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-390: Reduce LiDAR scan extrema without variadic argument limits
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
`readRplidar` accepts up to 4 MiB of serial scan bytes. Its summary passed every
|
||||
valid distance as an argument to `Math.min` and `Math.max`. A valid 1 MiB scan
|
||||
containing 200,000 measurement nodes exceeds the JavaScript argument-stack limit
|
||||
and throws `RangeError` instead of returning diagnostics.
|
||||
|
||||
## Decision
|
||||
|
||||
Reduce extrema with a loop and constant auxiliary state. Retain current median,
|
||||
zero-distance filtering, coverage, units, and empty-scan behavior. This avoids
|
||||
runtime-specific argument limits across the full supported capture size.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
An offline serial-pump dependency supplies protocol-valid bytes to the public
|
||||
`readRplidar` producer. The 3-point control passes on the baseline; the 200,000
|
||||
point fixture fails with `RangeError`. Both pass after this change with exact
|
||||
minimum, median, maximum, and coverage checks. Existing device tests provide
|
||||
empty and mixed-input controls. The fixture uses no serial device or motor and
|
||||
proves software decoding/aggregation only, not real hardware performance.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-391: Retain synthetic CSI provenance in analysis input
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-323 (RAC1 synthetic flag), ADR-373 (CSI analysis)
|
||||
|
||||
## Context
|
||||
|
||||
The RAC1 decoder exposes the firmware's synthetic flag and the waterfall retains
|
||||
it, but the kernel-analysis frame collector discarded it. Analysis of synthetic
|
||||
input consequently returned the same live-CSI explanatory note as unmarked input,
|
||||
preventing callers from distinguishing synthetic-inclusive estimates.
|
||||
|
||||
## Decision
|
||||
|
||||
Track whether any retained analysis frame is firmware-marked synthetic, expose
|
||||
that boolean in `analysis.input.synthetic`, and explicitly describe synthetic
|
||||
input in the estimate note. Frames rejected by the analysis buffer cap do not
|
||||
change the provenance of the retained computation. Keep arrival-rate measurements
|
||||
and raw packet diagnostics unchanged. An unmarked packet is not proof of physical
|
||||
radio origin; this records an explicit upstream flag rather than certifying truth.
|
||||
|
||||
## Verification
|
||||
|
||||
The public capture producer receives protocol-valid RAC1 datagrams through owned
|
||||
loopback UDP and invokes an injected deterministic analyzer. A mixed marked and
|
||||
unmarked window fails the baseline provenance assertion. The fixed path retains
|
||||
true for the mixed window and false for an unmarked control. This validates
|
||||
software provenance propagation only, not sensing accuracy or hardware behavior.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-392: Return single valid frames for explicitly selected radar models
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (host device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The serial radar reader requires two valid frames before auto-detecting a model.
|
||||
That confidence threshold was also applied to an explicitly selected model,
|
||||
rejecting a valid short capture with one decoded presence or vital report as
|
||||
`no_valid_frames`. The caller already supplied the model and baud selection.
|
||||
|
||||
## Decision
|
||||
|
||||
Require two frames for auto detection and one for an explicitly selected model.
|
||||
Retain the empty-capture failure, checksums, parsing, capture permissions, and
|
||||
all reported values. A single decoded frame is a successful software read and
|
||||
is not evidence of clinical accuracy or sustained radar performance.
|
||||
|
||||
## Verification
|
||||
|
||||
An offline serial-pump fixture supplies one checksum-valid MR60BHA2 presence
|
||||
report through the public reader. Explicit model selection fails on the baseline
|
||||
and returns one frame with presentFraction=1 after the change. The same input in
|
||||
auto mode remains refused. Existing multi-frame and invalid-input device tests
|
||||
remain controls. No serial port or hardware was opened by this verification.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-393: Use the arithmetic median for even LiDAR samples
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (range and depth diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The shared LiDAR median helper selected the upper middle sample for even-sized
|
||||
sets. The public depth reader reported 4 metres for confident depths of 1 and
|
||||
4 metres, and the serial scan reader likewise returned 4000 mm. Fields named
|
||||
median should report the arithmetic median, 2.5 metres or 2500 mm respectively.
|
||||
|
||||
## Decision
|
||||
|
||||
Average the two central sorted samples for even counts and preserve the central
|
||||
sample for odd counts. Empty sets remain null. Existing distance/confidence
|
||||
filtering and metre/millimetre conversion remain in the public producers.
|
||||
Update existing even-sample expectations to reflect the corrected numerical
|
||||
contract; this intentionally changes these statistics for even counts.
|
||||
|
||||
## Verification
|
||||
|
||||
Offline protocol fixtures enter both public readers through their existing
|
||||
transport injection boundaries. The baseline fails two even-sample cases and
|
||||
passes an unsorted odd-sample control. The correction verifies both units and
|
||||
zero-depth exclusion. No real LiDAR, serial device, or radar was used, and this
|
||||
software calculation does not prove measurement accuracy.
|
||||
@@ -0,0 +1,26 @@
|
||||
# ADR-394: Apply strict SDK errors to diagnostic reports
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (SDK operator interface)
|
||||
|
||||
## Context
|
||||
|
||||
A strict SDK instance throws `RuViewError` when an operation returns `ok:false`.
|
||||
The doctor helper directly invokes the diagnostic producer to retain injectable
|
||||
operator dependencies, bypassing that shared result check. Failed diagnostics
|
||||
therefore silently resolved even when the caller selected strict mode.
|
||||
|
||||
## Decision
|
||||
|
||||
Extract the existing result check and apply it to both registry calls and doctor
|
||||
reports. Preserve injected dependencies and retain the original structured report
|
||||
on the thrown error. Non-strict calls and successful diagnostics continue to
|
||||
resolve reports; the existing dry-run exception remains unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
The public SDK doctor runs a deterministic offline invalid-protocol diagnostic.
|
||||
The baseline fails the promised strict rejection. The correction checks the exact
|
||||
error class, tool identifier, and failed report; controls verify non-strict failure
|
||||
reports and successful strict reports. No external service or device is accessed.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-395: Preserve missing numeric CLI values for schema rejection
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (operator CLI)
|
||||
|
||||
## Context
|
||||
|
||||
The common device/training CLI flag adapter coerces values with `Number`. A bare
|
||||
`--node-id` is parsed as boolean true and becomes node 1; `--node-id=` becomes
|
||||
node 0. Both silently select a real node rather than reporting the missing
|
||||
numeric argument. The same adapter handles capture deadlines and sample limits.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain boolean and empty-string values in the common schema argument adapter so
|
||||
existing numeric schemas reject them. Convert explicit non-empty numeric values
|
||||
as before, including zero. Keep presentation flags and tool schemas unchanged.
|
||||
This change is limited to verbs using the common snake-case argument adapter.
|
||||
|
||||
## Verification
|
||||
|
||||
Execute the actual CLI process with both missing-value forms and explicit zero.
|
||||
An intentionally invalid UDP port ensures argument validation completes before
|
||||
opening a socket. Baseline errors omit node_id for both missing forms; the fixed
|
||||
CLI reports those numeric type errors while preserving the explicit zero control.
|
||||
This is offline CLI/schema verification, not a radio capture.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-396: Convert ESPHome millimetre distances to reported centimetres
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (ESPHome radar reader)
|
||||
|
||||
## Context
|
||||
|
||||
ESPHome sensor discovery advertises each entity's unit. The reader maps entities
|
||||
named distance into `distanceCmMean`, converting metres to centimetres but leaving
|
||||
millimetres unchanged. An advertised 500 mm measurement therefore became 500 cm
|
||||
instead of 50 cm, a factor-of-ten numerical error.
|
||||
|
||||
## Decision
|
||||
|
||||
Apply unit scales of 100 for metres and 0.1 for millimetres. Preserve centimetre
|
||||
and existing unspecified/other-unit behavior, positive-distance filtering, and
|
||||
entity metadata. Unit matching remains case insensitive. This is a correction
|
||||
to reported units, not a change to sensor discovery or access permissions.
|
||||
|
||||
## Verification
|
||||
|
||||
The public reader connects to an owned loopback TCP fixture speaking the native
|
||||
ESPHome protocol. Discovery and state updates carry 500 mm, 0.5 m, and 50 cm.
|
||||
The millimetre baseline fails with 500 cm; metre and centimetre controls pass.
|
||||
All three report 50 cm after the correction. This proves software protocol/unit
|
||||
handling only; no real radar or external network service was accessed.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-397: Distinguish ESPHome received updates from retained samples
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (bounded ESPHome sessions)
|
||||
|
||||
## Context
|
||||
|
||||
ESPHome sessions count every incoming state update, but each entity retains only
|
||||
its first 10,000 samples. The entity report used the retained length as `updates`,
|
||||
understating actual arrivals once this cap was reached. Mean values also came from
|
||||
that bounded subset without exposing how many updates were omitted.
|
||||
|
||||
## Decision
|
||||
|
||||
Maintain a separate arrival counter per entity. Report that count as `updates`,
|
||||
retain `sampledUpdates` for the aggregation subset, and disclose `droppedSamples`.
|
||||
Preserve the existing buffer bound and latest-value behavior. Existing hand-built
|
||||
summary sessions without the new counter fall back to their retained sample count.
|
||||
|
||||
## Verification
|
||||
|
||||
An owned loopback native-protocol fixture emits 10,001 distance state updates
|
||||
through the public reader. The baseline reports session=10,001 but entity=10,000.
|
||||
The correction reports received=10,001, retained=10,000, omitted=1, with unchanged
|
||||
50 cm mean. An existing small multi-entity fixture verifies zero omitted samples
|
||||
and correct ordinary counts. This is software aggregation verification only.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-398: Treat nonfinite ESPHome sensor states as unavailable
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (native sensor-state semantics)
|
||||
|
||||
## Context
|
||||
|
||||
Native protobuf float states can encode NaN and infinities without setting the
|
||||
missing-state flag. Positive infinity passed the distance filter and poisoned an
|
||||
otherwise finite distance mean; last entity states also exposed nonfinite values
|
||||
that become null only after JSON serialization, producing inconsistent reports.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat a nonfinite sensor float as missing when ingesting a state. Preserve state
|
||||
arrival counts, normal finite values, proto3 absent-float zero, explicit missing
|
||||
flags, and binary/text state behavior. Existing summary rules then exclude the
|
||||
invalid sample from means and report unavailable last state as null.
|
||||
|
||||
## Verification
|
||||
|
||||
Owned loopback native-protocol fixtures emit a 50 cm state followed by explicitly
|
||||
encoded NaN, positive infinity, or negative infinity. Corrected baseline fixtures
|
||||
fail on the nonfinite mean or last-state result; all fixed cases retain the 50 cm
|
||||
mean, null latest state, and two arrival counts. The existing fixture controls
|
||||
finite vitals, explicit missing states, and omitted proto3 zero values. No real
|
||||
sensor or external service is accessed; this proves software numeric handling.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-399: Select the busiest CSI stream using total arrivals
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded live CSI analysis)
|
||||
|
||||
## Context
|
||||
|
||||
The analysis collector advertises selection of the busiest node's dominant shape.
|
||||
It ranks buffers by their retained frame length. Once multiple streams reach the
|
||||
analysis buffer cap, their retained lengths tie and insertion order wins even
|
||||
when a later stream received substantially more frames. A 70-frame first node
|
||||
therefore beats a 100-frame second node under a 64-frame cap.
|
||||
|
||||
## Decision
|
||||
|
||||
Rank candidate streams by retained frames plus their existing dropped-over-cap
|
||||
counter. Preserve the frame cap, first-window sample rate, explicit node filter,
|
||||
and deterministic insertion order for actual arrival-count ties. The analyzer
|
||||
still consumes only the retained bounded samples; ranking does not invent data.
|
||||
|
||||
## Verification
|
||||
|
||||
The public capture receives two protocol-valid node streams through owned
|
||||
loopback UDP. The baseline selects the 70-frame node despite the second node's
|
||||
100 arrivals. The correction selects node 2 and reports 64 retained/36 omitted
|
||||
frames. An explicit node-1 control still selects node 1 and reports 64/6.
|
||||
Existing uncapped analysis and decoding tests remain controls. This is software
|
||||
stream-selection verification, not hardware throughput or sensing accuracy.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-400: Decode fused vitals into node diagnostics
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-063 (fusion), ADR-373 (node diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The capture parser recognizes firmware magic `0xC5110004` as fused vitals but
|
||||
returns only a byte count. Node aggregation then omits its RSSI and physiological
|
||||
values. A valid fused-only stream therefore has null `lastVitals` even though the
|
||||
firmware supplies the current fused heart and breathing estimates.
|
||||
|
||||
## Decision
|
||||
|
||||
Decode the exact 48-byte packed `edge_fused_vitals_pkt_t` from
|
||||
`firmware/esp32-csi-node/main/edge_processing.h`. Share the common fields and fixed
|
||||
point unit conversions with ordinary vitals, then expose the mmWave presence,
|
||||
type, confidence, raw heart/breathing values, distance in centimetres, and target
|
||||
count from their actual offsets. Aggregate fused reports into RSSI and latest
|
||||
vitals while retaining their distinct packet kind counter. Reject truncated
|
||||
fused reports before they can count as decoded node data.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
A firmware-layout fixture enters the public capture over owned loopback UDP.
|
||||
The baseline counts the packet but returns null RSSI and latest vitals. Fixed
|
||||
assertions check 72 bpm fused heart, 15 bpm breathing, 120 cm distance, confidence
|
||||
and target values, and timestamp units. Truncated-prefix tests and existing
|
||||
ordinary-vitals tests remain controls. These are decoded device-reported values;
|
||||
no clinical accuracy, real radio, or sensor-fusion quality is established.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-401: Validate auxiliary node datagrams before counting decoded reports
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-069 (features), ADR-039 (compression), feature-state and WASM firmware formats
|
||||
|
||||
## Context
|
||||
|
||||
Known auxiliary magics were sufficient to count a datagram as decoded. A
|
||||
four-byte feature, compressed, feature-state, or WASM prefix created a phantom
|
||||
node and successful diagnostic counts. Corrupt feature-state CRCs and truncated
|
||||
variable payloads were also accepted, hiding actual data corruption.
|
||||
|
||||
## Decision
|
||||
|
||||
Validate firmware-defined payload envelopes before retaining their existing
|
||||
metadata-only report shape: features require their 48-byte record, compressed
|
||||
reports require a 10-byte header and declared non-empty payload, feature-state
|
||||
requires the fixed 60-byte record and its IEEE CRC, and WASM output requires
|
||||
an 8-byte header plus five bytes for each of at most 16 declared events. Keep
|
||||
raw auxiliary payloads out of the response and retain existing kind counters.
|
||||
|
||||
Source contracts are `edge_processing.h`/`send_compressed_frame` in
|
||||
`edge_processing.c`, `rv_feature_state.h`, and the variable-length producer in
|
||||
`wasm_runtime.c`. This validates envelope integrity without claiming to decompress
|
||||
CSI or interpret feature/event semantics.
|
||||
|
||||
## Verification
|
||||
|
||||
Owned loopback UDP drives the public capture with four complete envelope controls,
|
||||
four magic-only prefixes, one corrupt CRC, and three truncated payloads. The
|
||||
baseline counts all twelve as decoded and hides corruption. The correction counts
|
||||
four decoded and eight malformed, with one valid node and exact kind counters.
|
||||
Existing CSI, vitals, and other packet tests remain controls. No firmware or real
|
||||
sensor was exercised; the claim is software format and diagnostic correctness.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-402: Preserve synthetic provenance for the whole CSI shape window
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-323 (RAC1 flags), ADR-373 (per-shape node diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
A normal capture's shape summary averages all frames but reports the synthetic
|
||||
flag from only its latest packet. A marked frame followed by an unmarked frame
|
||||
therefore contributes to the mean while removing the synthetic label shown by
|
||||
normal CLI diagnostics. The waterfall already preserves a window-wide flag;
|
||||
kernel-analysis input has a separate collector and provenance boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
Track whether any frame in each statistical shape window is firmware-marked
|
||||
synthetic and report that flag alongside the window mean/count. Preserve the
|
||||
latest packet's other descriptor fields and keep provenance independent for each
|
||||
shape. An unmarked window still reports false; that is not certification of its
|
||||
physical origin. No capture or hardware authority changes.
|
||||
|
||||
## Verification
|
||||
|
||||
The public ordinary capture receives a marked RAC1 frame then an unmarked frame
|
||||
through owned loopback UDP. The baseline reports two aggregated frames and false
|
||||
synthetic provenance. The correction reports true; an entirely unmarked control
|
||||
remains false. Existing shape and source statistics remain controls. This verifies
|
||||
software window provenance, not sensing accuracy or authentic radio origin.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-403: Normalize chronological report timestamps without truthiness
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-371 (offline training evidence gate)
|
||||
|
||||
## Context
|
||||
|
||||
The gate used `Date.parse(value) || numericFallback`. Valid epoch-zero ISO times
|
||||
parse to zero and were then treated as missing. Documented numeric millisecond
|
||||
boundaries were first interpreted as calendar strings by Date.parse, so 5 ms and
|
||||
30 ms could be misordered and falsely refused as temporal leakage.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain finite numeric boundaries as milliseconds and parse string boundaries
|
||||
once. Test finiteness explicitly rather than using truthiness. Preserve refusal
|
||||
of invalid/missing dates and non-increasing train/test chronology. The tool schema
|
||||
continues accepting its existing ISO string input; the public training export
|
||||
retains its documented numeric input support.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual SDK evaluates an epoch-zero ISO boundary with a later test timestamp.
|
||||
The public training export evaluates numeric 5 ms and 30 ms boundaries. Both
|
||||
baseline cases are falsely refused and pass after normalization. Equal-time and
|
||||
invalid-date controls remain refused. Fixtures are explicitly synthetic; no
|
||||
training, model calls, dataset claims, or publication is performed by these tests.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-404: Preserve missing capture windows in CLI watch trends
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-375 (per-node watch rate sparklines)
|
||||
|
||||
## Context
|
||||
|
||||
Watch draws repeated timed capture windows, but appends rate history only for
|
||||
nodes present in a window. A present–absent–present node is therefore shown as
|
||||
two adjacent observations, hiding the intervening outage. The sparkline helper
|
||||
also discards missing positions, so merely adding null history entries would
|
||||
still compress time. An actual terminal-mode CLI and owned loopback UDP replay
|
||||
reproduces the false continuous two-bar trend.
|
||||
|
||||
## Decision
|
||||
|
||||
Append a missing entry for each previously observed node absent from a capture
|
||||
window. Keep the existing 30-window bound. Render each missing position as `·`,
|
||||
while scaling finite rates using only finite observations. Missing means no
|
||||
observation in that window; it is not a measured zero rate. Initial history
|
||||
still starts when a node is first observed. JSON capture output is unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
The public CLI receives one valid packet, an empty one-second capture, and one
|
||||
returning packet. Its terminal row must show `▄·▄` instead of `▄▄`. A real child
|
||||
process executes the CLI; only stdout's TTY capability is supplied by the test.
|
||||
The test owns and cleans up its loopback sockets and child. Finite, zero,
|
||||
all-missing and empty sparkline controls preserve scale and positions. This is
|
||||
software replay, not a claim about radio hardware availability.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-405: Complete iPhone LiDAR captures when the relay stream closes
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded device readers)
|
||||
|
||||
## Context
|
||||
|
||||
The iPhone reader completes at its packet limit, timeout, or pre-open failure.
|
||||
After an opened WebSocket closes normally, however, it keeps the capture timer
|
||||
running despite having no remaining source of frames. A one-frame stream that
|
||||
closes immediately waits the entire requested window; an empty normal stream
|
||||
also delays its no_frames result. This wastes the user's capture time and uses
|
||||
the idle post-close interval in its reported receive rate.
|
||||
|
||||
## Decision
|
||||
|
||||
Finish on every close event. Retain accepted depth statistics. Preserve the
|
||||
existing relay_closed refusal for abnormal empty streams, pre-open connection
|
||||
failure, and max_frames termination. A normal empty stream still returns
|
||||
no_frames; a stream with valid depth frames remains a successful capture. The
|
||||
requested seconds field retains its existing meaning.
|
||||
|
||||
## Verification
|
||||
|
||||
An owned loopback HTTP server performs a real WebSocket upgrade and sends a
|
||||
valid one-pixel depth frame followed by a normal close handshake. The public
|
||||
reader must return promptly instead of waiting its two-second capture timeout.
|
||||
A normal empty stream and abnormal empty code1008 stream check refusal behavior;
|
||||
a max_frames=1 control checks existing early termination. These are transport
|
||||
fixtures, not iPhone hardware or depth-accuracy evidence.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-406: Assign numeric radar roles only to ESPHome sensor entities
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (ESPHome native API reader)
|
||||
|
||||
## Context
|
||||
|
||||
The reader infers heart, breathing and distance roles from entity names without
|
||||
checking their protocol kind. A binary Heart alarm or text Distance status can
|
||||
therefore be selected before the actual numeric sensor. Boolean true becomes
|
||||
1 bpm through arithmetic coercion; a numeric-looking text status becomes a
|
||||
fabricated distance. The actual numeric sensor is then ignored. A loopback
|
||||
native API producer reproduces this with valid entity metadata and states.
|
||||
|
||||
## Decision
|
||||
|
||||
Require sensor kind for heartBpm, breathingBpm and distanceCm roles, matching
|
||||
the existing targets rule. Keep binary presence inference. Preserve binary and
|
||||
text entities and their raw states in the entities result, without assigning
|
||||
them numeric radar roles. Numeric scalar filtering and unit handling remain
|
||||
unchanged. No medical accuracy or sensor-name taxonomy is introduced.
|
||||
|
||||
## Verification
|
||||
|
||||
A real TCP producer lists binary alarm and text status entities before valid
|
||||
numeric heart, distance and breathing sensors. The public reader must select
|
||||
72 bpm,120 cm and15 bpm rather than1,50 and1. A producer with only nonnumeric
|
||||
entities must return null numeric summaries while retaining its3 raw updates.
|
||||
Existing ESPHome presence, missing-state and numeric sensor controls run too.
|
||||
These are deterministic native API fixtures, not radio or clinical evidence.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-407: Preserve interrupted serial-capture state in device summaries
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded serial pump and device readers)
|
||||
|
||||
## Context
|
||||
|
||||
The pump can emit valid HEX chunks before its process fails. readSerial retains
|
||||
those useful bytes, but discards the failure whenever bytes exist. Radar and
|
||||
LiDAR callers consequently present an ordinary successful capture without any
|
||||
indication that the requested window was interrupted. Auto-detected radar also
|
||||
silently retains only its probe if the remainder capture fails.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep valid partial bytes and measurements. Mark the capture as incomplete and
|
||||
include a warning when the pump process fails after producing data. Propagate
|
||||
that state through both serial device summaries, including an auto-detection
|
||||
remainder failure. Do not expose subprocess stderr in the warning. Existing
|
||||
failures without bytes and normal complete capture behavior remain unchanged.
|
||||
The flag describes transport completeness, not measurement accuracy.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual Python PUMP_SCRIPT runs through the production execTool process
|
||||
runner, with a temporary fixture serial module that emits valid radar or scan
|
||||
bytes then raises. The public serial, radar and LiDAR readers must retain data
|
||||
and report captureIncomplete. A real normally completing pump is the control.
|
||||
Auto-detection also receives a normal probe followed by a failing empty pump.
|
||||
All serial port operations stay inside the fixture module; no hardware opens,
|
||||
physical motor commands, radio transmissions or model calls occur.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-408: Exclude absent LD2410 targets from distance means
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (shared mmWave summary)
|
||||
|
||||
## Context
|
||||
|
||||
The LD2410 decoder explicitly emits present=false and distanceCm=0 for target
|
||||
state none. The aggregate treats that no-target sentinel as a measured range.
|
||||
A 90 cm detection followed by an absent target consequently reports 45 cm; an
|
||||
all-absent stream reports 0 cm instead of having no distance observation. The
|
||||
presence fraction already identifies these absent frames correctly.
|
||||
|
||||
## Decision
|
||||
|
||||
Exclude explicitly absent target frames only from the distance mean. Keep their
|
||||
raw distance sentinel, frame counts, rates, and presence contribution. Continue
|
||||
averaging moving and stationary target measurements normally. MR60BHA2 distance
|
||||
frames are unchanged; they use their own range-validity field in the decoder.
|
||||
This distinguishes absence from a distance measurement, without new physical
|
||||
range limits or an accuracy claim.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual Python serial pump and production process runner read complete
|
||||
LD2410 frames from an isolated serial module. Public readMmwave must report 90 cm
|
||||
for present 90/absent, null for all-absent, and 150 cm for moving 90/static 210.
|
||||
Presence fractions and raw last-frame zero controls remain intact. The baseline
|
||||
fails both absence regressions and passes the positive-distance control. No
|
||||
hardware port opens or radar transmissions occur.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-409: Align SDK device argument types with supported runtime options
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (typed SDK), ADR-373 (device readers), ADR-378 (spectrum)
|
||||
|
||||
## Context
|
||||
|
||||
The SDK forwards device arguments to the current tool registry, but its shipped
|
||||
TypeScript declarations still describe earlier options. A real TypeScript
|
||||
consumer cannot select ESPHome by source/host/API port, request ESP32 spectrum
|
||||
rows, or set an iPhone depth packet cap. Each workflow already works through the
|
||||
same public SDK at runtime. Explicit serial source is missing too.
|
||||
|
||||
## Decision
|
||||
|
||||
Describe mmWave arguments as serial (the existing default, requiring port) or
|
||||
ESPHome (requiring host). Add the existing optional spectrum parameters and
|
||||
iPhone max_frames. Keep numeric and enum constraints, and all existing serial
|
||||
call shapes. This changes declarations only; no reader or tool behavior changes.
|
||||
Group these omissions as one declaration-drift fix rather than separate fields.
|
||||
|
||||
## Verification
|
||||
|
||||
A TypeScript consumer compiled with strict NodeNext checks fails on the valid
|
||||
runtime options before the fix and succeeds after it. Its expected-error controls
|
||||
retain host/port requirements, numeric parameter types, and known backends. The
|
||||
same public SDK is exercised with real owned TCP ESPHome, UDP CSI, and WebSocket
|
||||
depth producers: numeric state, spectrum bins, and packet-cap termination all
|
||||
work before and after the declaration change. No hardware or external service
|
||||
is involved. The fixture can be replayed with an installed TypeScript compiler:
|
||||
|
||||
```
|
||||
tsc --strict --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext test/fixtures/device-types.mts
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-410: Require callback/frame markers for CSI monitor flow evidence
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (serial monitor), ADR-375 (starvation diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The monitor counted any line containing the ESP32 csi_collector log tag as a
|
||||
CSI callback. Current firmware uses that tag for early configuration, startup,
|
||||
self-ping failures and other metadata as well as actual callbacks. A startup
|
||||
log with no callback consequently returned ok=true and falsely certified flow.
|
||||
The source's actual callback line uses `CSI cb #`; Realtek uses
|
||||
`RUVIEW_CSI: frame #`.
|
||||
|
||||
## Decision
|
||||
|
||||
Count only those explicit callback/frame markers as flow evidence. Keep the
|
||||
existing startup-upgrade metadata and independent buffer-starvation detection.
|
||||
The existing count remains a count of observed rate-limited log markers, not a
|
||||
radio callback-rate measurement. No UART, reset or hardware control behavior
|
||||
changes. Older log formats with only configuration tags cannot certify flow.
|
||||
|
||||
## Verification
|
||||
|
||||
The public SDK monitor runs the actual Python monitor script in a real child
|
||||
process. An isolated serial module supplies current firmware startup/self-ping
|
||||
messages; the baseline falsely succeeds, and the fix returns no-flow diagnostics.
|
||||
Mixed startup plus ESP32/Realtek frame markers counts only two observations.
|
||||
A real frame plus a starvation message still fails as csi_buffer_starvation.
|
||||
Existing monitor rendering and Python fixture controls run too. No physical
|
||||
serial port opens or node resets occur.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-411: Preserve structured doctor diagnostics for package corruption
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-370 (doctor), ADR-394 (SDK strict diagnostic contract)
|
||||
- Promotion: extend existing doctor PR #2208; no separate publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The package-integrity diagnostic reports hash drift as a structured warning, but
|
||||
malformed manifest JSON or a missing outer checksum throws before the doctor
|
||||
report is assembled. The public SDK therefore cannot return the promised
|
||||
checks/remedies for those ordinary package-corruption cases. A copied-package
|
||||
fixture reproduces both exceptions without touching the installed manifest.
|
||||
|
||||
## Decision
|
||||
|
||||
Convert manifest read/parse and packaged-file read failures into integrity
|
||||
findings. Report a missing or unreadable checksum explicitly. Preserve existing
|
||||
hash comparison, file counts, diagnostic report shape and warning policy. SDK
|
||||
strict mode continues rejecting failed reports through the existing checked
|
||||
path; integrity warnings retain their current warning behavior. No additional
|
||||
manifest schema or trust policy is introduced.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual public SDK is imported from an owned temporary copy containing the
|
||||
package source and reviewed brain. Strict doctor with groups=harness must return
|
||||
an integrity warning and reinstall remedy for malformed JSON, a missing or
|
||||
unreadable checksum, and unreadable packaged data. The valid-copy control retains successful integrity status. Existing
|
||||
doctor and strict failed-report controls run alongside these tests. No canonical
|
||||
manifest changes, credentials, network requests or hardware operations occur.
|
||||
@@ -0,0 +1,30 @@
|
||||
# ADR-412: Quarantine incomplete recorded I/Q pairs during transcode
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-096 and ADR-097 (rvCSI capture bridge)
|
||||
|
||||
## Context
|
||||
|
||||
The public ESP32 JSONL converter is the documented capture fixture bridge. Its
|
||||
contract drops rejected frames. It currently floors odd byte lengths, silently
|
||||
accepting a truncated I/Q pair as a shorter valid frame; malformed hex instead
|
||||
aborts the entire capture and loses later valid frames. The existing rej_len
|
||||
counter is never used.
|
||||
|
||||
## Decision
|
||||
|
||||
Reject invalid hex and odd byte lengths before constructing numeric samples.
|
||||
Increment the existing rej_len counter once for each rejected record and
|
||||
continue processing later frames. Preserve complete int8 pair conversion,
|
||||
quality scoring, timestamps, and output format. Do not infer missing samples or
|
||||
change adapter calibration or hardware policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI processes owned temporary JSONL captures with valid
|
||||
frames before and after each damaged record. The baseline accepts the odd pair
|
||||
and aborts on invalid hex. Fixed runs retain the two complete frames with
|
||||
contiguous frame IDs and truthful counters. A signed-IQ control verifies
|
||||
amplitude and real/imaginary orientation. All eight harness gates run alongside
|
||||
these subprocess tests. This is offline recording evidence only.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-413: Preserve complete UDP datagrams in the CSI relay
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (ESP32 CSI packet layout)
|
||||
|
||||
## Context
|
||||
|
||||
The public Python relay promises byte-for-byte forwarding without parsing or
|
||||
mutation. Its 2048-byte receive buffer truncates larger datagrams; a valid
|
||||
ADR-018 shape with four antennas and 256 subcarriers occupies 2068 bytes. Its
|
||||
truthiness check also discards empty UDP datagrams instead of forwarding them.
|
||||
Both behaviors violate the transparent relay contract.
|
||||
|
||||
## Decision
|
||||
|
||||
Receive up to 65535 bytes, sufficient for a complete IPv4 UDP payload, and
|
||||
forward every received datagram, including an empty payload. Retain None as the
|
||||
socket-timeout sentinel. Preserve bind defaults, destination selection,
|
||||
reporting and cleanup. Introduce no packet interpretation or hardware policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI relays traffic between owned loopback sockets. The
|
||||
baseline truncates a complete 2068-byte ADR-018 packet and fails to forward an
|
||||
empty datagram; a small binary payload is unchanged. Fixed runs compare every
|
||||
byte and retain both controls. All eight harness gates run with these actual
|
||||
subprocess tests. This proves local datagram preservation, not physical CSI
|
||||
capture quality or network performance.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-414: Use ADR-018 decoding for overnight capture segments
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (wire layout), ADR-135 (empty-room corpus)
|
||||
|
||||
## Context
|
||||
|
||||
The overnight empty-room recorder claims the same ADR-018 layout as the ordinary
|
||||
UDP recorder, but still reads an obsolete eight-byte header. It records the
|
||||
subcarrier-count bytes as RSSI/channel, consumes the remaining header as I/Q,
|
||||
and accepts unrelated or truncated datagrams. This corrupts baseline corpus
|
||||
samples before downstream analysis.
|
||||
|
||||
## Decision
|
||||
|
||||
Reuse record-csi-udp.py's existing twenty-byte ADR-018 parser through a local
|
||||
module import. Preserve segment naming, duration and receive workflow. The
|
||||
shared parser retains receive timestamps, decodes metadata and declared
|
||||
antenna-major payload, ignores trailing bytes, and rejects unsupported magic or
|
||||
truncated payloads. Avoid a second copy of the packet layout and do not alter
|
||||
quality, calibration or physical capture policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public overnight CLI writes owned temporary segment files from loopback
|
||||
UDP traffic. Baseline reports the wrong RSSI, channel and payload and retains
|
||||
invalid packets. Fixed tests verify declared multi-antenna amplitudes and raw IQ,
|
||||
metadata, trailing-byte handling, and valid-frame continuity after invalid or
|
||||
truncated datagrams. All eight harness gates run alongside these tests.
|
||||
This is deterministic software packet evidence only; no hardware accuracy claim.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-415: Allocate distinct training capture files on session-name collisions
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (collector wire input)
|
||||
|
||||
## Context
|
||||
|
||||
The training collector names sessions to whole seconds and opens recordings in
|
||||
append mode. Two captures with the same label during that second append samples
|
||||
to one file and overwrite its metadata with only the later frame count. Dataset
|
||||
manifests then undercount the actual recording and the original session loses
|
||||
its identity. Fractional CLI durations and concurrent collection make the
|
||||
collision reachable.
|
||||
|
||||
## Decision
|
||||
|
||||
Allocate each recording with exclusive file creation. Retain existing names
|
||||
when available; on a data or metadata collision, try a numeric suffix until
|
||||
allocation succeeds. Update session ID, data path and metadata path together.
|
||||
Preserve prior recordings and metadata instead of implicitly resuming them.
|
||||
No CLI resume contract exists; ordinary collection creates a new session.
|
||||
|
||||
## Verification
|
||||
|
||||
Two actual public collector CLI processes receive distinct owned loopback
|
||||
packets under an isolated fixed datetime fixture that reproduces a same-second
|
||||
collision deterministically. Baseline leaves one combined data file and one
|
||||
miscounted metadata entry. Fixed captures retain distinct IDs, one sample per
|
||||
file, matching metadata counts and a two-session/two-frame manifest. All eight
|
||||
harness gates run. No hardware capture or physical accuracy is claimed.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR-416: Validate alignment CLI window parameters before processing captures
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-079 (ground-truth pairing)
|
||||
|
||||
## Context
|
||||
|
||||
The public ground-truth alignment CLI parses window and numeric options without
|
||||
validation. A zero window frame count makes the windowing loop advance by zero
|
||||
and grow indefinitely. Negative counts also cannot advance correctly, and
|
||||
fractional or partially numeric values are silently truncated. Invalid camera
|
||||
counts, confidence thresholds and time values can bypass matching checks or
|
||||
produce misleading paired output.
|
||||
|
||||
## Decision
|
||||
|
||||
Parse complete numeric values and validate before reading or writing captures.
|
||||
Require positive safe integer frame counts, finite nonnegative window duration,
|
||||
confidence in [0,1], and finite signed clock offsets. Retain zero expansion and
|
||||
zero confidence as supported controls. Report the offending option and exit2
|
||||
for invalid inputs. Preserve valid windowing and pairing calculations.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual CLI subprocesses process owned tiny feature/camera recordings. The
|
||||
baseline zero/negative count processes are bounded by a one-second timeout and
|
||||
32MiB heap cap; fractional/malformed/range cases lack the expected early error.
|
||||
The fixed CLI rejects each invalid option before output creation and retains
|
||||
complete valid windows, zero expansion/confidence and a signed finite offset.
|
||||
An initial fixture passed negative values as separate tokens, which Node's
|
||||
argument parser already rejects; that attempt is retained but invalidated.
|
||||
Final regressions use documented --option=value syntax. All eight harness gates
|
||||
run. No model training, camera, physical capture or hardware operation occurs.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-417: Preserve input recordings when converter output aliases the source
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-412 (capture converter input quarantine)
|
||||
- Promotion: extend existing converter PR #2227; no new publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The public JSONL converter opens output in write mode before reading input. If
|
||||
both arguments refer to one file, including hard-link or symbolic-link aliases,
|
||||
it truncates the recording and reports successful conversion with zero frames.
|
||||
Input validation must not destroy the original evidence it is meant to read.
|
||||
|
||||
## Decision
|
||||
|
||||
Check existing file identity before opening the output. Reject identical files
|
||||
with a clear CLI error and exit2, preserving source bytes and links. Keep normal
|
||||
distinct-path conversion and the existing incomplete-IQ quarantine unchanged.
|
||||
This does not add in-place conversion or a broader transactional output policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI receives owned input captures with identical, hard-link
|
||||
and symbolic-link output paths. Baseline overwrites each source; fixed runs
|
||||
leave the original bytes intact and reject the arguments. Existing signed IQ,
|
||||
quarantine and later-frame controls pass alongside these fixtures. All eight
|
||||
harness gates run. Original capture files outside temporary fixtures are never
|
||||
mutated. This extends the existing converter PR as one reviewed unit.
|
||||
@@ -0,0 +1,30 @@
|
||||
# ADR-418: Handle expected resets in owned ESPHome rejection fixtures
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-406 (ESPHome fixture-driven role validation)
|
||||
- Promotion: extend existing ESPHome PR #2221; no new publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The encrypted/password test fixtures write while the reader deliberately rejects
|
||||
and closes the connection. The server socket can then emit ECONNRESET after the
|
||||
test ends. With no socket error listener, that expected fixture event produces
|
||||
an uncaught exception and intermittent unrelated full/security gate failures.
|
||||
Converter and alignment failures retained locally show this exact test event.
|
||||
|
||||
## Decision
|
||||
|
||||
Handle ECONNRESET only in the owned encrypted/password fixture modes. Continue
|
||||
throwing any unexpected error code and any reset in ordinary fixture mode. Do
|
||||
not change the production reader, its failure reports or its network policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public reader connects to owned TCP fixtures and returns the expected
|
||||
rejection report. A controlled socket reset event reproduces the missing error
|
||||
lifecycle deterministically: baseline throws, fixed modes accept the reset.
|
||||
Negative controls retain failures for EIO in rejection mode and ECONNRESET in
|
||||
ordinary mode. Existing roles and public reader tests, plus all eight gates,
|
||||
run on this revision. This is an explicitly injected software fixture event,
|
||||
not physical device or hardware evidence.
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
Latest proposed decisions:
|
||||
|
||||
- [ADR-379: An OTA'd image confirms itself with a bounded health check, or rolls back](ADR-379-ota-first-boot-health-check.md)
|
||||
- [ADR-380: Loopback UDP tee for second consumers of the CSI stream](ADR-380-loopback-udp-tee.md)
|
||||
- [ADR-382: `spatial.evidence.v1` export of RF Gaussians and link observations](ADR-382-spatial-evidence-rf-export.md)
|
||||
- [ADR-378: `ruview-live` showcase views — CSI waterfall, radar fan, animated cells](ADR-378-ruview-live-showcase-views.md)
|
||||
- [ADR-377: `ruview-live` — a Claude Code mod shipped inside `@ruvnet/ruview`](ADR-377-ruview-live-claude-code-mod.md)
|
||||
- [ADR-376: `ruview` — one npm install for every RuView component](ADR-376-ruview-umbrella-npm-package.md)
|
||||
- [ADR-375: MCP Apps console, HTTP transport and terminal UI for `@ruvnet/ruview`](ADR-375-ruview-mcp-apps-console-http-transport-and-terminal-ui.md)
|
||||
- [ADR-374: Remote host access over SSH (read-only)](ADR-374-remote-host-access-over-ssh.md)
|
||||
- [ADR-373: Host device access layer — ESP32, mmWave, LiDAR](ADR-373-host-device-access-layer.md)
|
||||
- [ADR-372: Structured debugging doctor](ADR-372-structured-debugging-doctor.md)
|
||||
- [ADR-371: Harness training runner and mean-pose evidence gate](ADR-371-harness-training-runner-and-evidence-gate.md)
|
||||
- [ADR-370: Cross-platform, verified ESP32 firmware flashing from npm](ADR-370-cross-platform-firmware-flashing.md)
|
||||
- [ADR-369: npm RuView operator surface — CLI, MCP, and SDK](ADR-369-ruview-npm-operator-surface-and-sdk.md)
|
||||
- [ADR-368: RuView compute kernel as a WASM + napi-rs npm package](ADR-368-ruview-kernel-wasm-napi-npm.md)
|
||||
- [ADR-367: Bounded research swarm and compute ownership](ADR-367-bounded-research-swarm-and-compute-ownership.md)
|
||||
- [ADR-366: CSI controls and frozen evaluation](ADR-366-csi-controls-and-frozen-evaluation.md)
|
||||
- [ADR-365: Public BFI dataset and decoder contract](ADR-365-public-bfi-dataset-and-decoder-contract.md)
|
||||
- [ADR-364: BFI capture admission and continuity](ADR-364-bfi-capture-admission-and-continuity.md)
|
||||
- [ADR-187: archive/v1 deprecation + model-weights honest labeling](ADR-187-archive-v1-deprecation-honest-labeling.md) (refs #509, #1125)
|
||||
- [ADR-186: Training progress API — wire the orphaned in-server trainer to /ws/train/progress](ADR-186-training-progress-api.md) (refs #1233)
|
||||
- [ADR-185: Python P6 SOTA bindings — AETHER, MERIDIAN, MAT](ADR-185-python-p6-sota-bindings.md)
|
||||
@@ -9,7 +27,7 @@ Latest proposed decisions:
|
||||
- [ADR-264: Versioned wire protocol for RTL8720F CFR and Range-FFT reports](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- [ADR-263: Adopt RTL8720F 2.4 GHz FMCW radar as an optional RuView sensing platform](ADR-263-rtl8720f-2-4ghz-fmcw-radar-platform.md)
|
||||
|
||||
This folder contains 210 Architecture Decision Records (ADRs) that document every significant technical choice in the RuView / WiFi-DensePose project. (The index tables below list a curated subset per domain; see the directory listing for the full set.)
|
||||
This folder records significant architectural choices in the RuView / WiFi-DensePose project. The index tables below list a curated subset per domain; see the directory listing for the full set.
|
||||
|
||||
## Why ADRs?
|
||||
|
||||
@@ -59,6 +77,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-040](ADR-040-wasm-programmable-sensing.md) | WASM Programmable Sensing (Tier 3) | Accepted |
|
||||
| [ADR-041](ADR-041-wasm-module-collection.md) | WASM Module Collection (65 edge modules) | Accepted (hardware-validated) |
|
||||
| [ADR-044](ADR-044-provisioning-tool-enhancements.md) | Provisioning Tool Enhancements | Proposed |
|
||||
| [ADR-379](ADR-379-ota-first-boot-health-check.md) | OTA'd image confirms itself with a bounded health check (IP + CSI frame) or rolls back; OTA httpd stack sized from device data | Proposed (host-tested; hardware-verified on one S3 and one C6) |
|
||||
| [ADR-110](ADR-110-esp32-c6-firmware-extension.md) | ESP32-C6 firmware extension — Wi-Fi 6 / 802.15.4 / TWT / LP-core | Accepted, P1-P10 complete, firmware-side substrate closed at **[v0.7.0-esp32](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)**. Companion docs: [`WITNESS-LOG-110`](../WITNESS-LOG-110.md) (13 §A0.x entries · 99.56 % cross-board RX · **104.1 µs smoothed sync stdev** · ≤100 µs target met), [`ADR-110-REVIEW-GUIDE`](../ADR-110-REVIEW-GUIDE.md) (one-page reviewer tour), [`ADR-110-BRANCH-STATE`](../ADR-110-BRANCH-STATE.md) (coordination map vs `feat/adr-115-ha-mqtt-matter`). Host decoders + tests: Python `SyncPacketParser` (10) + Rust `wifi_densepose_hardware::SyncPacket` (15), cross-language hex pin gates drift. |
|
||||
|
||||
### Signal processing and sensing
|
||||
@@ -166,6 +185,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-294](ADR-294-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) |
|
||||
| [ADR-295](ADR-295-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) |
|
||||
| [ADR-296](ADR-296-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) |
|
||||
| [ADR-380](ADR-380-loopback-udp-tee.md) | Loopback UDP tee for second consumers of the CSI stream (`--udp-tee`) | Proposed |
|
||||
| [ADR-297](ADR-297-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) |
|
||||
| [ADR-298](ADR-298-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) |
|
||||
| [ADR-299](ADR-299-csi-data-incident-repo-controls.md) | Repository CSI data-incident controls — ignore rules and pre-commit/CI policy check | Accepted (controls implemented; tree remediation gated) |
|
||||
|
||||