Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6699a2fc44 | ||
|
|
964377765a | ||
|
|
5cfbb8d98d | ||
|
|
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 | ||
|
|
5ef001b4fe | ||
|
|
6524712900 | ||
|
|
dd02efe2fe | ||
|
|
7d15e9fb4a | ||
|
|
69106970dd | ||
|
|
686e4bad00 | ||
|
|
b713a23cac | ||
|
|
0c1efda425 | ||
|
|
b1be91478c | ||
|
|
6eb79cad26 | ||
|
|
45fc41ac4c | ||
|
|
901cf16eb3 | ||
|
|
af2d6981a3 | ||
|
|
a04fbc4694 | ||
|
|
dbaa3c7f52 | ||
|
|
6cf0356554 | ||
|
|
b2ad3a65dc | ||
|
|
815384dcc4 | ||
|
|
8cd8742d2b | ||
|
|
dbf4788628 | ||
|
|
cdad032296 | ||
|
|
0decc5c3fa | ||
|
|
d4a3c7f17f | ||
|
|
2c0abcc802 | ||
|
|
f1dbaac452 | ||
|
|
33a9e90896 | ||
|
|
bbbb14169d | ||
|
|
4fbe58f736 | ||
|
|
a64e9e98fb | ||
|
|
130afabae8 | ||
|
|
36e4df95b7 | ||
|
|
b370d4fcd4 | ||
|
|
500cfe4787 | ||
|
|
f7c2c6587e | ||
|
|
30391bfbd2 | ||
|
|
00a42d7cda | ||
|
|
bd6f38e2df | ||
|
|
203bd66daa | ||
|
|
2c3a924231 | ||
|
|
e5422d52e4 | ||
|
|
26e8ca3f39 | ||
|
|
c407e738a3 | ||
|
|
755e9bca05 | ||
|
|
73e15a7d29 | ||
|
|
f38beff441 | ||
|
|
830abd7e18 | ||
|
|
b296ea26a4 | ||
|
|
0b29902d8b | ||
|
|
b18b34e9f5 | ||
|
|
169597a7d2 | ||
|
|
0d1a247bfe | ||
|
|
a4d49f76b9 | ||
|
|
51102b3baf | ||
|
|
977fa26731 | ||
|
|
2841b22cc9 | ||
|
|
3fd3851633 | ||
|
|
d6b940184e | ||
|
|
60adac05b7 | ||
|
|
ec9da648b8 | ||
|
|
0fd1c63563 | ||
|
|
521b29ea21 | ||
|
|
e8e4e5cf67 | ||
|
|
9af1cc6644 | ||
|
|
56fcc49104 | ||
|
|
11c014485e | ||
|
|
86e45980fe | ||
|
|
d613a576ea | ||
|
|
97578ee401 | ||
|
|
e54b4171ca | ||
|
|
338c523217 | ||
|
|
224c9eb671 | ||
|
|
2deb6d2d64 | ||
|
|
82e05d0664 | ||
|
|
a2c662f9f7 | ||
|
|
48c6244d37 | ||
|
|
30404dd9cf | ||
|
|
c4021f9884 | ||
|
|
2227b42841 | ||
|
|
767c36ad12 | ||
|
|
2e9e87c65c | ||
|
|
8d1a67d31b | ||
|
|
c19b846394 | ||
|
|
cf6e955196 | ||
|
|
dddc214a25 | ||
|
|
5da64c6fc5 | ||
|
|
1c088e67c2 | ||
|
|
1231088427 | ||
|
|
4d857a908f | ||
|
|
d6347fe12e | ||
|
|
9b595b8333 | ||
|
|
944b7bea77 | ||
|
|
7f9df37380 | ||
|
|
f574450516 | ||
|
|
3621a211bd | ||
|
|
5eb8ddde12 | ||
|
|
fbecae4e02 | ||
|
|
de099c1f2d | ||
|
|
01460b98b0 | ||
|
|
26f444138e | ||
|
|
00737441c4 | ||
|
|
25d7be3c25 | ||
|
|
29b3968731 | ||
|
|
ed006abd58 | ||
|
|
5419bff2a8 | ||
|
|
7cdf5714e8 | ||
|
|
f1ea275d5d | ||
|
|
5c50fa8588 | ||
|
|
f19938a7b6 | ||
|
|
db5a898372 | ||
|
|
b11422459e | ||
|
|
e5bf0d4892 | ||
|
|
e04f269f1a | ||
|
|
12a61c16e8 | ||
|
|
a70803fe31 | ||
|
|
4b295b1b4d | ||
|
|
615e2d419b | ||
|
|
f85896cccb | ||
|
|
0a0b3411f8 | ||
|
|
08210b02c9 | ||
|
|
27f5540663 | ||
|
|
b742eae7d6 | ||
|
|
d42c5581f3 | ||
|
|
0df48df7b2 | ||
|
|
bd110e0eac | ||
|
|
f3c361efd1 | ||
|
|
a3b6e1d500 | ||
|
|
1d2ad6aa8e | ||
|
|
c929bbc8b3 | ||
|
|
d36f346bba | ||
|
|
2c249ec8cb | ||
|
|
7927839f4f | ||
|
|
a76adc3c2f | ||
|
|
aae2ed5345 | ||
|
|
3161af52da | ||
|
|
501f138360 | ||
|
|
4685618388 | ||
|
|
1d50518a70 | ||
|
|
0370d49e4a | ||
|
|
de27336fa1 | ||
|
|
73e82313ac |
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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"sessionId":"d80c93c2-51b7-42e8-a0fc-dc47cff1200f","pid":45748,"acquiredAt":1779668018388}
|
||||
{"sessionId":"905385c4-b13f-5091-96df-5752fb109cf5","pid":509,"procStart":"527","acquiredAt":1786922977672}
|
||||
@@ -0,0 +1,3 @@
|
||||
# The contributor harness hashes provenance inputs byte-for-byte. Keep text
|
||||
# files in this boundary on LF even when Windows enables core.autocrlf.
|
||||
harness/ruview/** text=auto eol=lf
|
||||
@@ -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
|
||||
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
|
||||
@@ -53,7 +64,9 @@ jobs:
|
||||
sdkconfig: sdkconfig.defaults
|
||||
partition_table_name: partitions_display.csv
|
||||
size_warn_kb: 1100
|
||||
size_limit_kb: 1152
|
||||
# Current 8 MB image is 1163 KiB after USB onboarding. The OTA app
|
||||
# partition is 2048 KiB; retain a 1200 KiB hard gate and warning.
|
||||
size_limit_kb: 1200
|
||||
artifact_app: esp32-csi-node.bin
|
||||
artifact_pt: partition-table.bin
|
||||
- variant: 4mb
|
||||
@@ -92,12 +105,11 @@ jobs:
|
||||
idf.py set-target ${{ matrix.target }}
|
||||
idf.py build
|
||||
|
||||
- name: Build and run host-side ADR-110 unit tests
|
||||
- name: Build and run host-side firmware unit tests
|
||||
if: matrix.variant == 'c6-4mb'
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: |
|
||||
make test_adr110
|
||||
./test_adr110
|
||||
make host_tests
|
||||
|
||||
- name: Verify binary size budget
|
||||
working-directory: firmware/esp32-csi-node
|
||||
@@ -118,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: |
|
||||
@@ -162,10 +178,14 @@ jobs:
|
||||
mkdir -p release-staging
|
||||
cp build/esp32-csi-node.bin release-staging/${{ matrix.artifact_app }}
|
||||
cp build/partition_table/partition-table.bin release-staging/${{ matrix.artifact_pt }}
|
||||
if [ "${{ matrix.variant }}" = "8mb" ]; then
|
||||
cp build/bootloader/bootloader.bin release-staging/bootloader.bin
|
||||
cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
|
||||
fi
|
||||
cp build/bootloader/bootloader.bin release-staging/bootloader.bin
|
||||
cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
|
||||
cp version.txt release-staging/version.txt
|
||||
(cd release-staging && sha256sum \
|
||||
"${{ matrix.artifact_app }}" \
|
||||
"${{ matrix.artifact_pt }}" \
|
||||
bootloader.bin ota_data_initial.bin version.txt \
|
||||
> SHA256SUMS.txt)
|
||||
ls -la release-staging/
|
||||
|
||||
- name: Check QEMU ESP32-S3 support status
|
||||
@@ -180,3 +200,59 @@ jobs:
|
||||
name: esp32-csi-node-firmware-${{ matrix.variant }}
|
||||
path: firmware/esp32-csi-node/release-staging/
|
||||
retention-days: 90
|
||||
|
||||
build-c6-idf6:
|
||||
name: Build XIAO C6 external antenna (ESP-IDF 6.0.2)
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v6.0.2
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build the IDF 6 canary image
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32c6;sdkconfig.defaults.xiao-external" set-target esp32c6
|
||||
idf.py build
|
||||
test "$(stat -c%s build/esp32-csi-node.bin)" -le $((1152 * 1024))
|
||||
strings build/esp32-csi-node.bin | grep -F "external U.FL"
|
||||
|
||||
publish-release-assets:
|
||||
name: Publish firmware release assets
|
||||
if: github.ref_type == 'tag'
|
||||
needs: [version-guard, provisioning-tests, build, build-c6-idf6]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download the exact-run firmware artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
with:
|
||||
pattern: esp32-csi-node-firmware-*
|
||||
path: release-downloads
|
||||
|
||||
- name: Package variants without filename collisions
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
for directory in release-downloads/esp32-csi-node-firmware-*; do
|
||||
variant="${directory##*/}"
|
||||
tar -C "$directory" -czf "release-assets/${variant}.tar.gz" .
|
||||
done
|
||||
(cd release-assets && sha256sum *.tar.gz > SHA256SUMS.txt)
|
||||
|
||||
- name: Publish the downloadable firmware bundles
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
|
||||
with:
|
||||
name: ESP32 firmware ${{ github.ref_name }}
|
||||
prerelease: true
|
||||
make_latest: false
|
||||
body: |
|
||||
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/*
|
||||
@@ -0,0 +1,70 @@
|
||||
name: iPhone LiDAR integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/iphone-lidar/**'
|
||||
- 'docs/adr/ADR-340-iphone-lidar-sensor-bridge.md'
|
||||
- '.github/workflows/iphone-lidar.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'integrations/iphone-lidar/**'
|
||||
- 'docs/adr/ADR-340-iphone-lidar-sensor-bridge.md'
|
||||
- '.github/workflows/iphone-lidar.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
web:
|
||||
name: Node relay and codec
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/iphone-lidar/web
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: integrations/iphone-lidar/web/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Audit runtime dependencies
|
||||
run: npm audit --omit=optional --audit-level=high
|
||||
|
||||
ios:
|
||||
name: iOS 17 compile
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
|
||||
- name: Compile native sources with strict concurrency
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sdk="$(xcrun --sdk iphoneos --show-sdk-path)"
|
||||
build_dir="$RUNNER_TEMP/ruview-lidar-build"
|
||||
mkdir -p "$build_dir"
|
||||
cd "$build_dir"
|
||||
xcrun swiftc \
|
||||
-parse-as-library \
|
||||
-target arm64-apple-ios17.0 \
|
||||
-sdk "$sdk" \
|
||||
-module-name RuViewLiDAR \
|
||||
-strict-concurrency=complete \
|
||||
-warnings-as-errors \
|
||||
-emit-module \
|
||||
-emit-module-path "$build_dir/RuViewLiDAR.swiftmodule" \
|
||||
-c "$GITHUB_WORKSPACE"/integrations/iphone-lidar/native/RuViewLiDAR/*.swift
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Model release gate (ADR-298)
|
||||
|
||||
# ADR-298 model-release sanity gates (issue #1521): structural checks that
|
||||
# block a degenerate/mislabeled classifier head (unreachable decision
|
||||
# boundary, near-constant output, degenerate class balance, a metric
|
||||
# surfaced under a task name it wasn't computed as) before it ships.
|
||||
#
|
||||
# Checker: v2/crates/wifi-densepose-train/src/model_gates.rs
|
||||
#
|
||||
# IMPORTANT — the honest scope of this job: it protects the *checker itself*
|
||||
# from regressing (the gate logic + its issue-1521 regression fixture are
|
||||
# exercised on every push/PR that touches this crate), and running it is
|
||||
# required before ADR-298 can be called "wired in" at all. It does NOT gate
|
||||
# an actual model publish — this repository does not automate uploading to
|
||||
# the HuggingFace model repo (`ruvnet/wifi-densepose-pretrained`); that
|
||||
# remains a manual, human-run step. Before publishing or replacing a model
|
||||
# artifact there, run this gate against the real head weights locally:
|
||||
#
|
||||
# cargo test -p wifi-densepose-train model_gates
|
||||
#
|
||||
# and, until a CLI entry point exists to run `evaluate_linear_head` against an
|
||||
# arbitrary `.safetensors`/`.rvf` file, load the head's `weight`/`bias` in a
|
||||
# short script and call `wifi_densepose_train::evaluate_linear_head` directly.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "v2/crates/wifi-densepose-train/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "v2/crates/wifi-densepose-train/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
model-release-gate:
|
||||
name: Model release gate check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: rustup toolchain install stable --profile minimal
|
||||
|
||||
- name: Run the model-release gate's own test suite
|
||||
working-directory: v2
|
||||
run: cargo test -p wifi-densepose-train --no-default-features model_gates -- --nocapture
|
||||
|
||||
- name: Summarize result
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo '### Model release gate (ADR-298)'
|
||||
echo ''
|
||||
echo 'This job protects `model_gates.rs` from regressing. It does not itself'
|
||||
echo 'gate a real HuggingFace model publish — that upload is a manual step'
|
||||
echo 'outside this repository; run `cargo test -p wifi-densepose-train model_gates`'
|
||||
echo 'against real head weights before publishing one.'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -40,8 +40,13 @@ jobs:
|
||||
- dir: harness/ruview
|
||||
build: false
|
||||
publishable: true
|
||||
# ADR-283: brain + local hosts + replay assets; still runtime-dependency-free.
|
||||
unpacked_budget: 131072
|
||||
# ADR-283/325: brain + local hosts + replay assets + guarded Spaces OAuth adapter;
|
||||
# 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
|
||||
@@ -130,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,627 @@
|
||||
name: RuForecast Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop, 'feat/*', 'feature/*']
|
||||
paths:
|
||||
- 'v2/crates/ruforecast'
|
||||
- 'v2/Cargo.toml'
|
||||
- 'v2/Cargo.lock'
|
||||
- 'v2/rust-toolchain.toml'
|
||||
- 'scripts/csi-data-policy-check.sh'
|
||||
- 'scripts/run-ruforecast-benchmarks.sh'
|
||||
- 'docs/adr/ADR-348-*.md'
|
||||
- 'docs/adr/ADR-349-*.md'
|
||||
- 'docs/adr/ADR-350-*.md'
|
||||
- 'docs/adr/README.md'
|
||||
- 'docs/benchmarks/ruforecast.md'
|
||||
- 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md'
|
||||
- 'docs/security/ruview-forecast-*'
|
||||
- 'docs/validation/ruforecast-requirements-evidence.md'
|
||||
- '.github/workflows/ruforecast-ci.yml'
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
paths:
|
||||
- 'v2/crates/ruforecast'
|
||||
- 'v2/Cargo.toml'
|
||||
- 'v2/Cargo.lock'
|
||||
- 'v2/rust-toolchain.toml'
|
||||
- 'scripts/csi-data-policy-check.sh'
|
||||
- 'scripts/run-ruforecast-benchmarks.sh'
|
||||
- 'docs/adr/ADR-348-*.md'
|
||||
- 'docs/adr/ADR-349-*.md'
|
||||
- 'docs/adr/ADR-350-*.md'
|
||||
- 'docs/adr/README.md'
|
||||
- 'docs/benchmarks/ruforecast.md'
|
||||
- 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md'
|
||||
- 'docs/security/ruview-forecast-*'
|
||||
- 'docs/validation/ruforecast-requirements-evidence.md'
|
||||
- '.github/workflows/ruforecast-ci.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ruforecast-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_PROFILE_DEV_DEBUG: '0'
|
||||
CARGO_PROFILE_TEST_DEBUG: '0'
|
||||
CARGO_PROFILE_BENCH_DEBUG: '0'
|
||||
CARGO_PROFILE_RELEASE_DEBUG: '0'
|
||||
|
||||
jobs:
|
||||
contract-rust-189:
|
||||
name: Forecast contract and feature-off boundary (Rust 1.89)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout recursively
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust 1.89 with lint components
|
||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
||||
with:
|
||||
toolchain: 1.89.0
|
||||
components: rustfmt,clippy
|
||||
|
||||
- name: Cache Cargo artifacts
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
||||
with:
|
||||
workspaces: v2/crates/ruforecast
|
||||
key: ruforecast-rust-189
|
||||
|
||||
- name: Validate benchmark runner and evidence contract
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash -n scripts/run-ruforecast-benchmarks.sh
|
||||
test -x scripts/run-ruforecast-benchmarks.sh
|
||||
test -f docs/benchmarks/ruforecast.md
|
||||
test -f docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md
|
||||
test -f docs/security/ruview-forecast-clean-room.md
|
||||
test -f docs/security/ruview-forecast-threat-model.md
|
||||
test -f docs/validation/ruforecast-requirements-evidence.md
|
||||
|
||||
for adr_number in 348 349 350; do
|
||||
mapfile -t adr_files < <(
|
||||
find docs/adr -maxdepth 1 -type f -name "ADR-${adr_number}-*.md" -print
|
||||
)
|
||||
if [[ "${#adr_files[@]}" -ne 1 ]]; then
|
||||
echo "Expected exactly one ADR-${adr_number} file, found ${#adr_files[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q "^# ADR-${adr_number}:" "${adr_files[0]}"
|
||||
grep -Eq '^[-*] \*\*Status\*\*: (Proposed|Accepted|Superseded|Deprecated)( |$|—)|^## Status' "${adr_files[0]}"
|
||||
done
|
||||
|
||||
grep -q '^## Requirements' docs/adr/ADR-348-*.md
|
||||
grep -q '^## Acceptance gates' docs/adr/ADR-348-*.md
|
||||
grep -q '^## Requirements and acceptance' docs/adr/ADR-349-*.md
|
||||
grep -q '^## Requirements and acceptance' docs/adr/ADR-350-*.md
|
||||
for adr_number in 348 349 350; do
|
||||
grep -q "| \[ADR-${adr_number}\](ADR-${adr_number}-" docs/adr/README.md \
|
||||
|| { echo "ADR-${adr_number} is missing from the ADR index" >&2; exit 1; }
|
||||
done
|
||||
|
||||
for number in $(seq 1 12); do
|
||||
requirement="$(printf 'RF-%03d' "$number")"
|
||||
grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \
|
||||
|| { echo "Missing evidence row for ${requirement}" >&2; exit 1; }
|
||||
done
|
||||
for number in $(seq 1 8); do
|
||||
requirement="$(printf 'FT-%03d' "$number")"
|
||||
grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \
|
||||
|| { echo "Missing evidence row for ${requirement}" >&2; exit 1; }
|
||||
done
|
||||
for number in $(seq 1 9); do
|
||||
requirement="$(printf 'PM-%03d' "$number")"
|
||||
grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \
|
||||
|| { echo "Missing evidence row for ${requirement}" >&2; exit 1; }
|
||||
done
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
root = Path("v2/crates/ruforecast/crates")
|
||||
core = tomllib.loads((root / "ruforecast-core/Cargo.toml").read_text())
|
||||
model = tomllib.loads((root / "ruforecast-model/Cargo.toml").read_text())
|
||||
train = tomllib.loads((root / "ruforecast-train/Cargo.toml").read_text())
|
||||
|
||||
assert core.get("features", {}).get("default", []) == []
|
||||
assert model["features"]["default"] == [], "model defaults must stay backend-free"
|
||||
assert train["features"]["default"] == [], "train defaults must stay backend-free"
|
||||
assert {"model", "cpu", "cuda", "wgpu", "ruvector"} <= model["features"].keys()
|
||||
assert {"cpu", "cuda", "training", "cli", "server", "fal-client"} <= train["features"].keys()
|
||||
assert {"model", "dep:burn-ndarray"} <= set(model["features"]["cpu"])
|
||||
assert {"model", "dep:burn-cuda"} <= set(model["features"]["cuda"])
|
||||
assert {"model", "dep:burn-wgpu"} <= set(model["features"]["wgpu"])
|
||||
assert "dep:ruvector-core" in model["features"]["ruvector"]
|
||||
assert {"ruforecast-model/cpu", "training"} <= set(train["features"]["cpu"])
|
||||
assert {"ruforecast-model/cuda", "training"} <= set(train["features"]["cuda"])
|
||||
|
||||
bins = {entry["name"]: entry for entry in train.get("bin", [])}
|
||||
assert bins["ruforecast"]["required-features"] == ["cli"]
|
||||
|
||||
model_benches = {entry["name"]: entry for entry in model.get("bench", [])}
|
||||
train_benches = {entry["name"]: entry for entry in train.get("bench", [])}
|
||||
assert model_benches["forecast_inference"]["harness"] is False
|
||||
assert model_benches["forecast_inference"]["required-features"] == ["cpu"]
|
||||
assert train_benches["data_pipeline"]["harness"] is False
|
||||
assert train_benches["data_pipeline"]["required-features"] == ["cpu"]
|
||||
PY
|
||||
|
||||
- name: Check formatting for forecast packages only
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 fmt
|
||||
-p ruforecast-core
|
||||
-p ruforecast-model
|
||||
-p ruforecast-train
|
||||
-- --check
|
||||
|
||||
- name: Prove feature-off graph excludes Burn and CubeCL
|
||||
working-directory: v2/crates/ruforecast
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: > forecast-feature-off-tree.txt
|
||||
for package in ruforecast-core ruforecast-model ruforecast-train; do
|
||||
cargo +1.89.0 tree --locked -e normal,build -p "$package" --no-default-features \
|
||||
>> forecast-feature-off-tree.txt
|
||||
done
|
||||
if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' forecast-feature-off-tree.txt; then
|
||||
echo 'Burn/CubeCL entered the Rust 1.89 feature-off dependency graph.' >&2
|
||||
cat forecast-feature-off-tree.txt >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check feature-off packages
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 check --locked
|
||||
-p ruforecast-core
|
||||
-p ruforecast-model
|
||||
-p ruforecast-train
|
||||
--no-default-features --all-targets
|
||||
|
||||
- name: Test model-neutral core
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 test --locked
|
||||
-p ruforecast-core
|
||||
--no-default-features --lib --tests
|
||||
|
||||
- name: Test model contracts without a model backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 test --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --lib --tests
|
||||
|
||||
- name: Test RuVector retrieval boundary without a model backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 test --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features ruvector --lib --tests
|
||||
|
||||
- name: Clippy RuVector retrieval boundary
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 clippy --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features ruvector --all-targets -- -D warnings
|
||||
|
||||
- name: Clippy forecast packages without backend features
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.89.0 clippy --locked
|
||||
-p ruforecast-core
|
||||
-p ruforecast-model
|
||||
-p ruforecast-train
|
||||
--no-default-features --all-targets -- -D warnings
|
||||
|
||||
- name: Upload feature-off dependency tree
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ruforecast-feature-off-tree
|
||||
path: v2/crates/ruforecast/forecast-feature-off-tree.txt
|
||||
if-no-files-found: error
|
||||
|
||||
hosted-boundaries-rust-189:
|
||||
name: Forecast hosted boundaries and privacy contract (Rust 1.89)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout recursively
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust 1.89 with Clippy
|
||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
||||
with:
|
||||
toolchain: 1.89.0
|
||||
components: clippy
|
||||
|
||||
- name: Cache Cargo artifacts
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
||||
with:
|
||||
workspaces: v2/crates/ruforecast
|
||||
key: ruforecast-hosted-rust-189
|
||||
|
||||
- name: Enforce clean-room source, secret, and hosted-wire boundary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
roots = [
|
||||
Path("v2/crates/ruforecast/crates/ruforecast-core"),
|
||||
Path("v2/crates/ruforecast/crates/ruforecast-model"),
|
||||
Path("v2/crates/ruforecast/crates/ruforecast-train"),
|
||||
]
|
||||
text_suffixes = {".json", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"}
|
||||
forbidden_source = re.compile(r"(?i)\b(?:times[ _-]?fm|google[ _/-]?research)\b")
|
||||
secret_patterns = {
|
||||
"private key": re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),
|
||||
"AWS access key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
||||
"JWT": re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b"),
|
||||
"presigned URL": re.compile(r"(?i)[?&](?:X-Amz-Signature|X-Goog-Signature)="),
|
||||
"literal FAL_KEY": re.compile(r"(?i)\bFAL_KEY\b\s*[:=]\s*['\"][^'$<{\"\n]{8,}['\"]"),
|
||||
}
|
||||
forbidden_blobs = {".ckpt", ".mpk", ".npy", ".npz", ".onnx", ".pt", ".pth", ".safetensors"}
|
||||
|
||||
errors = []
|
||||
for root in roots:
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file() or "target" in path.parts:
|
||||
continue
|
||||
if path.suffix.lower() in forbidden_blobs:
|
||||
errors.append(f"forbidden checked-in model/data blob: {path}")
|
||||
continue
|
||||
if path.suffix.lower() not in text_suffixes:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if forbidden_source.search(text):
|
||||
errors.append(f"forbidden clean-room source/import reference: {path}")
|
||||
for label, pattern in secret_patterns.items():
|
||||
if pattern.search(text):
|
||||
errors.append(f"possible {label} in {path}")
|
||||
|
||||
fal_source = Path("v2/crates/ruforecast/crates/ruforecast-train/src/fal.rs").read_text()
|
||||
for struct_name in ("HostedOptimizer", "HostedBudget", "HostedSyntheticPayload"):
|
||||
match = re.search(
|
||||
rf"pub struct {struct_name}\s*\{{(?P<body>.*?)^\}}",
|
||||
fal_source,
|
||||
flags=re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
if match is None:
|
||||
errors.append(f"missing hosted wire struct: {struct_name}")
|
||||
continue
|
||||
body = match.group("body")
|
||||
fields = re.findall(r"^\s*pub\s+([A-Za-z0-9_]+)\s*:", body, re.MULTILINE)
|
||||
forbidden_fields = (
|
||||
"account", "data_policy", "dataset", "device", "identity", "path",
|
||||
"person", "room", "session", "site", "split", "subject", "tenant",
|
||||
"workspace",
|
||||
)
|
||||
for field in fields:
|
||||
lowered = field.lower()
|
||||
if any(fragment in lowered for fragment in forbidden_fields):
|
||||
errors.append(f"forbidden hosted field {struct_name}.{field}")
|
||||
if "bytes" in lowered and field not in {
|
||||
"max_artifact_bytes",
|
||||
"max_memory_bytes",
|
||||
}:
|
||||
errors.append(f"unapproved hosted byte field {struct_name}.{field}")
|
||||
if "DataPolicy" in body:
|
||||
errors.append(f"DataPolicy entered hosted wire struct {struct_name}")
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
- name: Enforce tracked-data policy
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash scripts/csi-data-policy-check.sh --self-test
|
||||
bash scripts/csi-data-policy-check.sh --tracked
|
||||
|
||||
- name: Validate minimal fal deployment archive policy
|
||||
run: >-
|
||||
python3
|
||||
v2/crates/ruforecast/crates/ruforecast-train/deploy/fal/deploy.py
|
||||
self-test
|
||||
|
||||
- name: Prove hosted feature graph excludes Burn and CubeCL
|
||||
working-directory: v2/crates/ruforecast
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo +1.89.0 tree --locked -e normal,build \
|
||||
-p ruforecast-train \
|
||||
--no-default-features --features cli,server,fal-client \
|
||||
> forecast-hosted-feature-tree.txt
|
||||
if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' \
|
||||
forecast-hosted-feature-tree.txt; then
|
||||
echo 'Burn/CubeCL entered the Rust 1.89 hosted dependency graph.' >&2
|
||||
cat forecast-hosted-feature-tree.txt >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Require named hosted boundary regressions
|
||||
working-directory: v2/crates/ruforecast
|
||||
shell: bash
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo +1.89.0 test --locked \
|
||||
-p ruforecast-train \
|
||||
--no-default-features --features cli,server,fal-client \
|
||||
--lib --bins --tests -- --list \
|
||||
| tee hosted-boundary-tests.txt
|
||||
for regression in \
|
||||
fal_app_rejects_path_and_host_confusion \
|
||||
fal_urls_require_exact_origin_path_and_no_query \
|
||||
fal_submit_headers_disable_retry_and_omit_store_io \
|
||||
hosted_payload_dto_has_no_customer_fields \
|
||||
queue_status_discards_provider_error_body \
|
||||
result_url_accepts_exact_response_suffix \
|
||||
artifact_file_url_uses_expected_api_path \
|
||||
fal_key_debug_is_redacted \
|
||||
hosted_payload_rejects_app_field \
|
||||
hosted_reservation_cannot_exceed_source_retention \
|
||||
artifact_handoff_expiry_boundary_is_fail_closed \
|
||||
fal_result_enforces_cumulative_artifact_budget_boundary \
|
||||
fal_download_rejects_over_budget_without_writing \
|
||||
direct_server_train_requires_request_id_header \
|
||||
request_id_rejects_path_injection \
|
||||
unknown_cancel_is_not_success \
|
||||
privacy_external_dataset_payload_is_denied; do
|
||||
grep -q "$regression" hosted-boundary-tests.txt \
|
||||
|| { echo "Missing hosted boundary regression: $regression" >&2; exit 1; }
|
||||
done
|
||||
|
||||
- name: Exercise hosted external-data rejection
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.89.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cli,server,fal-client
|
||||
privacy_external_dataset_payload_is_denied
|
||||
|
||||
- name: Test server and fal client with mock integrations
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.89.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cli,server,fal-client
|
||||
--lib --bins --tests
|
||||
|
||||
- name: Clippy server and fal client boundaries
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.89.0 clippy --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cli,server,fal-client
|
||||
--all-targets -- -D warnings
|
||||
|
||||
- name: Clippy standalone CLI feature boundary
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.89.0 clippy --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cli
|
||||
--bin ruforecast -- -D warnings
|
||||
|
||||
- name: Upload hosted dependency tree
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ruforecast-hosted-feature-tree
|
||||
path: v2/crates/ruforecast/forecast-hosted-feature-tree.txt
|
||||
if-no-files-found: error
|
||||
|
||||
burn-cpu-rust-192:
|
||||
name: Forecast Burn CPU tests and bench compile (Rust 1.92)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Checkout recursively
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust 1.92 with Clippy
|
||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
||||
with:
|
||||
toolchain: 1.92.0
|
||||
components: clippy
|
||||
|
||||
- name: Cache Cargo artifacts
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
||||
with:
|
||||
workspaces: v2/crates/ruforecast
|
||||
key: ruforecast-rust-192-cpu
|
||||
|
||||
- name: Test model with Burn CPU backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 test --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features cpu --lib --tests
|
||||
|
||||
- name: Test trainer and CLI with Burn CPU backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu,cli --lib --bins
|
||||
|
||||
- name: Run one-step local JSONL training smoke
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu
|
||||
--test local_jsonl_smoke --
|
||||
--exact local_hash_addressed_jsonl_executes_one_real_optimizer_step
|
||||
|
||||
- name: Run idempotent synthetic CLI training smoke
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu,cli
|
||||
--test cli_smoke --
|
||||
--exact cli_smoke_trains_and_writes_the_complete_candidate_set
|
||||
|
||||
- name: Test combined CPU and hosted feature surface
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.92.0 test --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu,cli,server,fal-client
|
||||
--lib --bins
|
||||
|
||||
- name: Clippy model with Burn CPU backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 clippy --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features cpu --all-targets -- -D warnings
|
||||
|
||||
- name: Clippy trainer and CLI with Burn CPU backend
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.92.0 clippy --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu,cli,server,fal-client
|
||||
--all-targets -- -D warnings
|
||||
|
||||
- name: Compile forecast inference benchmark
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 bench --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features cpu
|
||||
--bench forecast_inference --no-run
|
||||
|
||||
- name: Compile forecast data-pipeline benchmark
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 bench --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cpu
|
||||
--bench data_pipeline --no-run
|
||||
|
||||
- name: Run quick inference benchmark for trend visibility
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: |
|
||||
set -o pipefail
|
||||
mkdir -p ../../bench-out/ruforecast
|
||||
cargo +1.92.0 bench --locked \
|
||||
-p ruforecast-model \
|
||||
--no-default-features --features cpu \
|
||||
--bench forecast_inference -- \
|
||||
--warm-up-time 1 --measurement-time 2 --sample-size 10 \
|
||||
| tee ../../bench-out/ruforecast/forecast-inference.txt
|
||||
|
||||
- name: Run quick data-pipeline benchmark for trend visibility
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: |
|
||||
set -o pipefail
|
||||
mkdir -p ../../bench-out/ruforecast
|
||||
cargo +1.92.0 bench --locked \
|
||||
-p ruforecast-train \
|
||||
--no-default-features --features cpu \
|
||||
--bench data_pipeline -- \
|
||||
--warm-up-time 1 --measurement-time 2 --sample-size 10 \
|
||||
| tee ../../bench-out/ruforecast/data-pipeline.txt
|
||||
|
||||
- name: Upload informational benchmark logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ruforecast-informational-cpu-benchmarks
|
||||
path: bench-out/ruforecast/
|
||||
if-no-files-found: warn
|
||||
|
||||
burn-cuda-compile-rust-192:
|
||||
name: Forecast Burn CUDA/WGPU compile checks only (Rust 1.92)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout recursively
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust 1.92
|
||||
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
|
||||
with:
|
||||
toolchain: 1.92.0
|
||||
|
||||
- name: Cache Cargo artifacts
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
|
||||
with:
|
||||
workspaces: v2/crates/ruforecast
|
||||
key: ruforecast-rust-192-cuda-check
|
||||
|
||||
- name: Check model CUDA feature
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 check --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features cuda --lib
|
||||
|
||||
- name: Check model WGPU feature
|
||||
working-directory: v2/crates/ruforecast
|
||||
run: >-
|
||||
cargo +1.92.0 check --locked
|
||||
-p ruforecast-model
|
||||
--no-default-features --features wgpu --lib
|
||||
|
||||
- name: Check trainer CUDA and hosted-server feature combination
|
||||
working-directory: v2/crates/ruforecast
|
||||
env:
|
||||
FAL_KEY: ''
|
||||
run: >-
|
||||
cargo +1.92.0 check --locked
|
||||
-p ruforecast-train
|
||||
--no-default-features --features cuda,cli,server
|
||||
--lib --bins
|
||||
@@ -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: brain + local hosts + replay assets; no runtime deps.
|
||||
harness/ruview) export UNPACKED_BUDGET=131072 ;;
|
||||
# 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);"
|
||||
|
||||
@@ -14,6 +14,33 @@ env:
|
||||
PYTHON_VERSION: '3.11'
|
||||
|
||||
jobs:
|
||||
# Rust dependency advisories are deterministic for the checked-in lockfile,
|
||||
# so this job gates the PR and retains the exact machine-readable report.
|
||||
rust-audit:
|
||||
name: Rust Dependency Audit
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit --locked --version 0.22.2
|
||||
|
||||
- name: Audit the checked-in Rust lockfile
|
||||
run: |
|
||||
set -o pipefail
|
||||
cargo audit --file v2/Cargo.lock --json | tee v2/cargo-audit.json
|
||||
|
||||
- name: Upload Rust advisory report
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: cargo-audit-report
|
||||
path: v2/cargo-audit.json
|
||||
if-no-files-found: error
|
||||
|
||||
# Static Application Security Testing (SAST)
|
||||
sast:
|
||||
name: Static Application Security Testing
|
||||
|
||||
@@ -28,6 +28,7 @@ on:
|
||||
- 'v2/crates/wifi-densepose-wifiscan/**'
|
||||
- 'v2/crates/wifi-densepose-bfld/**'
|
||||
- 'v2/crates/cog-ha-matter/**'
|
||||
- 'v2/crates/homecore*/**'
|
||||
- 'v2/Cargo.toml'
|
||||
- 'v2/Cargo.lock'
|
||||
- 'ui/**'
|
||||
|
||||
@@ -17,11 +17,16 @@ firmware/esp32-csi-node/sdkconfig.defaults.bak
|
||||
firmware/esp32-hello-world/sdkconfig.old
|
||||
|
||||
# Host-built firmware test binaries (compiled from test/*.c, not source)
|
||||
firmware/esp32-csi-node/test/test_adr110
|
||||
firmware/esp32-csi-node/test/test_vitals
|
||||
firmware/esp32-csi-node/test/fuzz_serialize
|
||||
firmware/esp32-csi-node/test/fuzz_edge
|
||||
firmware/esp32-csi-node/test/fuzz_nvs
|
||||
# Compiled host-test and fuzz binaries. The Makefile writes them beside their
|
||||
# sources, so they land in a tracked directory. Ignore them by SHAPE rather
|
||||
# than by name: every source here carries an extension and no binary does.
|
||||
#
|
||||
# This replaced a hand-maintained list of five targets, which had gone stale --
|
||||
# test_csi_sanitize, test_mmwave_detect and test_thermal were all built by
|
||||
# `make host_tests`, ignored by nothing, and got committed by accident.
|
||||
firmware/esp32-csi-node/test/test_*
|
||||
firmware/esp32-csi-node/test/fuzz_*
|
||||
!firmware/esp32-csi-node/test/*.*
|
||||
firmware/esp32-csi-node/test/*.exe
|
||||
firmware/esp32-csi-node/test/*.obj
|
||||
|
||||
@@ -303,4 +308,20 @@ ruvector.db
|
||||
# sensing-server runtime artifacts written by its test suite (trained model
|
||||
# snapshots + the generated session-secret) — never tracked
|
||||
v2/crates/wifi-densepose-sensing-server/data/
|
||||
# The server also writes this secret when launched from v2/. Keep the rule
|
||||
# file-specific so tracked datasets below v2/data remain visible.
|
||||
/v2/data/session-secret
|
||||
*.proptest-regressions
|
||||
|
||||
# ADR-324: wasm-bindgen output for ruview-offaxis is generated locally
|
||||
# (see the crate README); never commit generated artifacts.
|
||||
v2/crates/ruview-offaxis/pkg/
|
||||
|
||||
# fal.ai visual-teacher LoRA training data/weights -- regenerable, never
|
||||
# commit training images or model weights (ADR-353)
|
||||
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
|
||||
@@ -33,3 +33,7 @@
|
||||
path = vendor/metaharness
|
||||
url = https://github.com/ruvnet/metaharness
|
||||
branch = main
|
||||
[submodule "v2/crates/ruforecast"]
|
||||
path = v2/crates/ruforecast
|
||||
url = https://github.com/ruvnet/RuForecast.git
|
||||
branch = main
|
||||
@@ -47,17 +47,18 @@ from the current tree when needed.
|
||||
|
||||
## RuView contributor harness
|
||||
|
||||
`@ruvnet/ruview@0.3.1` is the runtime-dependency-free contributor interface
|
||||
`@ruvnet/ruview@0.5.0` is the runtime-dependency-free contributor interface
|
||||
defined by ADR-283.
|
||||
|
||||
```bash
|
||||
npx @ruvnet/ruview@0.3.1 doctor
|
||||
npx @ruvnet/ruview@0.3.1 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.3.1 agent run \
|
||||
npx @ruvnet/ruview@0.5.0 doctor
|
||||
npx @ruvnet/ruview@0.5.0 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.5.0 agent run \
|
||||
--host codex --repo . --prompt "Find the nearest tests and cite files"
|
||||
npx @ruvnet/ruview@0.3.1 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.3.1 mcp start
|
||||
npx @ruvnet/ruview@0.5.0 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.5.0 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.5.0 spaces
|
||||
npx @ruvnet/ruview@0.5.0 mcp start
|
||||
```
|
||||
|
||||
Start unfamiliar repository work with `ruview_guidance`. It returns reviewed
|
||||
|
||||
@@ -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.
|
||||
@@ -25,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`archive/v1` (the original pure-Python implementation) formally deprecated (ADR-187)** — commits `1fb5397dd`, `b1417fb6e`; refs #509, #1125. Added `archive/v1/DEPRECATED.md` (a loud tombstone) and a `> ⚠️ DEPRECATED` notice atop `archive/v1/README.md`, both pointing at the maintained `v2/` workspace and the `wifi-densepose 2.x` / `ruview` pip wheel (ADR-117). Records the honest fact behind #509: `archive/v1`'s `DensePoseHead` is **architecture-only** — random `kaiming_normal_` init with **zero committed checkpoints** under `archive/v1/` (MEASURED by Glob over `**/*.{pth,onnx,safetensors,pt,ckpt,bin}`). The ADR-028 deterministic proof `archive/v1/data/proof/verify.py` stays live and is explicitly out of scope. The same effort added a **"Model weights: what's real, what's not" three-tier table** to `README.md` + `docs/user-guide.md`, separating real-and-validated checkpoints (presence 82.3% held-out temporal-triplet, MM-Fi pose 82.69% torso-PCK@20, `count_v1`) from the real-but-weak on-device `pose_v1` (PCK@20 = 3.0%, runtime `confidence=0` stub, below the ADR-079 ≥35% target) from the architecture-only `archive/v1` head — and caveated every live single-ESP32 17-keypoint advertisement accordingly. Docs/labeling only; no code or model behavior changed.
|
||||
|
||||
### Fixed
|
||||
- **Pose-vitals, desktop, and repository-integrity issue remediation.** Breathing confidence now measures periodic autocorrelation at the estimated respiratory frequency instead of penalizing clean sinusoidal signals via crest factor (#1610). The desktop launcher resolves the Windows `.exe`, uses `where` for PATH lookup, and passes log filtering through `RUST_LOG`; its React versions, Vite type declarations, and Tauri UI hook working directories are aligned (#1516, #1517, #1518). Runtime session secrets written from `v2/` are ignored, and contributor-harness provenance inputs are pinned to LF across Windows checkouts (#1519, #1520). With explicit owner authorization, the six raw CSI/person-data capture and metadata files identified by ADR-299 were removed from the current tree; historical copies remain pending separately coordinated incident response.
|
||||
- **`docs/huggingface/MODEL_CARD.md` had drifted from the model card actually published on the Hub (issue #1481).** Every filename in its "Files in this repo" table (`pretrained-encoder.onnx`, `pretrained-heads.onnx`, `pretrained.rvf`, `room-profiles.json`) pointed at files never uploaded to `ruvnet/wifi-densepose-pretrained` — only `config.json` existed. Replaced the in-repo card with the content actually live on the Hub (`model.safetensors`, `model-q{2,4,8}.bin`, `node-{1,2}.json`, `presence-head.json`, `csi-embed-v2.*`, honest v1→v2 retraction of the single-class "100%" presence claim) and added a "Using with the Rust sensing server (RVF conversion)" section documenting the `--convert-model`/`--convert-out` and `--model` auto-convert paths that neither card previously mentioned.
|
||||
- **`--convert-model` failed on the published `model.safetensors`: NUL-padded safetensors header rejected by strict JSON parse (issue #1480, #894 follow-up).** The reference safetensors format pads its JSON header to an 8-byte boundary with trailing NUL bytes; `safetensors_to_rvf` (`wifi-densepose-sensing-server/src/model_format.rs`) fed the full declared-length header slice straight to `serde_json::from_slice`, which rejects the padding as "trailing characters." Since the only published full-precision weight file exercises this padding, `--convert-model` could not convert it at all. Fixed by trimming trailing NUL/whitespace bytes before parsing. Pinned by `safetensors_nul_padded_header_converts` (a header padded to the 8-byte boundary, matching the real HF file, converts and round-trips its weights through `ProgressiveLoader`).
|
||||
- **In-server training reconnected — "Start Training" no longer silently no-ops; `/ws/train/progress` streams real progress (ADR-186, issue #1233).** The dashboard's Start Training button POSTed a config, got `success:true`, and nothing happened: `/api/v1/train/start` was a stub that flipped a status string and logged one line, and `/ws/train/progress` 404'd. The full pure-Rust trainer in `training_api.rs` (loads recorded CSI, gradient-descent, exports a `.rvf`) already existed but was **orphaned** — never declared as a module (no `mod training_api;`), so it wasn't compiled at all. Fix (`wifi-densepose-sensing-server`): declared the module, reconciled `AppStateInner` (replaced the `training_status`/`training_config` stub fields with a shared `TrainingState` status handle + cooperative cancel flag + a `training_progress_tx` broadcast), deleted the stub handlers, and merged the real `training_api::routes()` (so `/api/v1/train/{start,stop,status,pretrain,lora}` and `/ws/train/progress` resolve under the existing `/api/v1/*` bearer gate). The training core was decoupled from the ~60-field server state so it is unit-testable. **P5 honesty guarantee:** with `RUVIEW_DISABLE_SERVER_TRAINING` set, start returns a structured `{enabled:false, cli:"wifi-densepose train-room"}` HTTP 409 — never a silent success — and the dashboard disables the Start buttons with a CLI tooltip (enablement is surfaced on `/api/v1/train/status`). Pinned by 8 new tests incl. a **live-socket** test that completes a genuine 101 WebSocket handshake and receives a real progress frame after a POST start, a full POST→poll-status→`.rvf`-exists round-trip, a path-traversal rejection, cancellation, and the disabled-409 path. `cargo test -p wifi-densepose-sensing-server -p wifi-densepose-train --no-default-features` — 0 failed.
|
||||
|
||||
@@ -45,7 +45,7 @@ retrieved memories, generated proposals, and old test counts are not.
|
||||
Do not hardcode crate, ADR, or test counts in instructions; derive them when a
|
||||
task needs them.
|
||||
|
||||
## Contributor metaharness (`@ruvnet/ruview@0.3.1`)
|
||||
## Contributor metaharness (`@ruvnet/ruview@0.4.0`)
|
||||
|
||||
ADR-283 defines the current community metaharness. It adds secure local
|
||||
Claude/Codex execution, a reviewed shared brain, default-deny MCP mutation
|
||||
@@ -54,21 +54,24 @@ free of runtime dependencies.
|
||||
|
||||
```bash
|
||||
# Diagnose the installed harness
|
||||
npx @ruvnet/ruview@0.3.1 doctor
|
||||
npx @ruvnet/ruview@0.4.0 doctor
|
||||
|
||||
# Get a source-cited capability map before unfamiliar work
|
||||
npx @ruvnet/ruview@0.3.1 guidance --topic homecore --query "restore and plugins"
|
||||
npx @ruvnet/ruview@0.4.0 guidance --topic homecore --query "restore and plugins"
|
||||
|
||||
# Explore this trusted checkout through Claude Code (stdin, plan/safe mode)
|
||||
npx @ruvnet/ruview@0.3.1 agent run \
|
||||
npx @ruvnet/ruview@0.4.0 agent run \
|
||||
--host claude-code --repo . --prompt "Map the relevant subsystem and cite files"
|
||||
|
||||
# Search reviewed, source-cited repository knowledge
|
||||
npx @ruvnet/ruview@0.3.1 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
npx @ruvnet/ruview@0.4.0 brain search --query "community memory"
|
||||
npx @ruvnet/ruview@0.4.0 brain verify --repo .
|
||||
|
||||
# Read the OAuth-bound Cognitum Spaces projection
|
||||
npx @ruvnet/ruview@0.4.0 spaces
|
||||
|
||||
# Run the dependency-free RuView MCP server
|
||||
npx @ruvnet/ruview@0.3.1 mcp start
|
||||
npx @ruvnet/ruview@0.4.0 mcp start
|
||||
```
|
||||
|
||||
`ruview_guidance` returns reviewed capability maturity, repository citations,
|
||||
|
||||
@@ -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.
|
||||
@@ -2,20 +2,32 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cognitum.one/seed">
|
||||
<img src="assets/ruview-seed.png" alt="RuView - WiFi DensePose" width="100%">
|
||||
<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>
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="33%"><a href="docs/user-guide.md"><img src="assets/readme/nav-start-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.svg" width="180" alt="Cognitum Seed" /></a><br/><a href="https://cognitum.one/seed">Cognitum Seed</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cognitum.one/marketplace">
|
||||
<img src="assets/musica-promo.png" alt="Cognitum Musica" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/ruvnet/RuCelium">
|
||||
<img src="assets/rucelium-hero.png" alt="RuCelium — environmental intelligence" width="100%">
|
||||
<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** ##
|
||||
|
||||
**Turn ordinary WiFi into a spatial intelligence / sensing system.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras or wearables. Just physics.
|
||||
@@ -30,6 +42,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
|
||||
@@ -46,31 +60,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-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.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-spatial-v3.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, and an honesty check for accuracy claims.
|
||||
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.3.1 doctor
|
||||
npx @ruvnet/ruview@0.3.1 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.3.1 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.3.1 brain search --query "calibration"
|
||||
npx @ruvnet/ruview@0.3.1 brain verify --repo .
|
||||
|
||||
# Check claims, replay the deterministic proof, or expose the MCP server
|
||||
npx @ruvnet/ruview@0.3.1 claim-check --file REPORT.md
|
||||
npx @ruvnet/ruview@0.3.1 verify
|
||||
npx @ruvnet/ruview@0.3.1 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>
|
||||
|
||||
@@ -108,8 +166,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).
|
||||
@@ -122,8 +180,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:
|
||||
@@ -223,7 +284,7 @@ huggingface-cli download ruvnet/wifi-densepose-pretrained --local-dir models/wif
|
||||
|
||||
| Consumer | Format used | Status |
|
||||
|----------|-------------|--------|
|
||||
| Python training / evaluation / embedding extraction | `model.safetensors` | ✅ Works — load with `safetensors.torch.load_file` |
|
||||
| Python training / evaluation / embedding extraction | `model.safetensors` | ⚠️ The published file's header is NUL-padded, which the reference `safetensors.torch.load_file` rejects (issue [#1522](https://github.com/ruvnet/RuView/issues/1522)) — pending a corrected re-upload. `csi-embed-v2.safetensors` in the same repo is unaffected and loads normally. |
|
||||
| Inspect / re-export the bundle | `model.rvf.jsonl` (line-by-line JSON) | ✅ Works — plain JSONL |
|
||||
| Sensing-server `--model <PATH>` flag | native RVF, `model.safetensors`, or `model.rvf.jsonl` | ✅ Native RVF loads directly; safetensors and JSONL auto-convert in memory |
|
||||
|
||||
@@ -235,6 +296,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>
|
||||
@@ -244,8 +307,8 @@ See the measured benchmarks, witness records, and one-command reproducibility ch
|
||||
|------|-------|---------|
|
||||
| **MM-Fi pose model (SOTA)** | [`ruvnet/wifi-densepose-mmfi-pose`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose) | 82.69% torso-PCK@20 (single) · 83.59% (ensemble+TTA) · 75K-param micro variant 74.30% |
|
||||
| **AetherArena benchmark Space** | [`ruvnet/aether-arena`](https://huggingface.co/spaces/ruvnet/aether-arena) | self-correcting, auditable MM-Fi leaderboard |
|
||||
| **Full MM-Fi study (honest picture)** | [`docs/benchmarks/mmfi-wifi-sensing-study.md`](docs/benchmarks/mmfi-wifi-sensing-study.md) | pose + action; zero-shot cross-subject ~64%, +~30 s in-room calibration → 72.2% |
|
||||
| **Efficiency frontier** | [`docs/benchmarks/wifi-pose-efficiency-frontier.md`](docs/benchmarks/wifi-pose-efficiency-frontier.md) | SOTA-beating WiFi pose in a 20 KB int4 edge model |
|
||||
| **Full MM-Fi study (honest picture)** | [`docs/benchmarks/mmfi-wifi-sensing-study.md`](docs/benchmarks/mmfi-wifi-sensing-study.md) | pose + action; zero-shot cross-subject ~64%, labeled in-room calibration → 72.2% |
|
||||
| **Efficiency frontier** | [`docs/benchmarks/wifi-pose-efficiency-frontier.md`](docs/benchmarks/wifi-pose-efficiency-frontier.md) | SOTA-beating MM-Fi pose in a ~37 KB int4 model; live ESP32 compatibility not established |
|
||||
| **Pretrained encoder** | [`ruvnet/wifi-densepose-pretrained`](https://huggingface.co/ruvnet/wifi-densepose-pretrained) | 82.3% held-out temporal-triplet, 8 KB int4 |
|
||||
| **Reproducible proof (Trust Kill Switch)** | [`archive/v1/data/proof/verify.py`](archive/v1/data/proof/verify.py) + [`expected_features.sha256`](archive/v1/data/proof/expected_features.sha256) | one-command deterministic pipeline replay (SHA-256 of output vs published hash) |
|
||||
| **Benchmark-proof ADR** | [ADR-168](docs/adr/ADR-168-benchmark-proof.md) | how the numbers are produced and verified |
|
||||
@@ -289,6 +352,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>
|
||||
@@ -463,6 +528,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:
|
||||
|
||||
```
|
||||
@@ -487,12 +554,20 @@ Neural Network: processed signals → 17 body keypoints + vital signs + room mod
|
||||
Output: real-time pose, breathing, heart rate, room fingerprint, drift alerts
|
||||
```
|
||||
|
||||
No training cameras required — the [Self-Learning system (ADR-024)](docs/adr/ADR-024-contrastive-csi-embedding-model.md) bootstraps from raw WiFi data alone. [MERIDIAN (ADR-027)](docs/adr/ADR-027-cross-environment-domain-generalization.md) ensures the model works in any room, not just the one it trained in.
|
||||
The [Self-Learning system (ADR-024)](docs/adr/ADR-024-contrastive-csi-embedding-model.md) provides
|
||||
camera-free representation-learning components. Cross-room pose remains a separate, data-gated
|
||||
problem: [MERIDIAN (ADR-027)](docs/adr/ADR-027-cross-environment-domain-generalization.md) is
|
||||
**Proposed**, while the measured calibration reference requires labeled CSI/keypoint pairs and
|
||||
model-specific adapters. See the [model compatibility boundary](docs/user-guide.md#model-and-capture-compatibility).
|
||||
|
||||
---
|
||||
|
||||
## 🏢 Use Cases & Applications
|
||||
|
||||
> **Safety boundary:** these are research and prototype applications, not medical devices,
|
||||
> emergency systems, or safety-certified controls. Vital-sign and pose outputs require independent
|
||||
> validation on the exact hardware, room, subjects, and failure conditions before operational use.
|
||||
|
||||
WiFi sensing works anywhere WiFi exists. No new hardware in most cases — just software on existing access points or a $8 ESP32 add-on. Because there are no cameras, deployments avoid privacy regulations (GDPR video, HIPAA imaging) by design.
|
||||
|
||||
**Scaling:** Each AP distinguishes ~3-5 people (56 subcarriers). Multi-AP multiplies linearly — a 4-AP retail mesh covers ~15-20 occupants. No hard software limit; the practical ceiling is signal physics.
|
||||
@@ -527,7 +602,7 @@ WiFi sensing works anywhere WiFi exists. No new hardware in most cases — just
|
||||
| Use Case | What It Does | Hardware | Key Metric | Edge Module |
|
||||
|----------|-------------|----------|------------|-------------|
|
||||
| **Smart home automation** | Room-level presence triggers (lights, HVAC, music) that work through walls — no dead zones, no motion-sensor timeouts | 2-3 ESP32-S3 nodes ($24) | Through-wall range ~5m | [HVAC Presence](docs/edge-modules/building.md), [Lighting Zones](docs/edge-modules/building.md) |
|
||||
| **Fitness & sports** | Rep counting, posture correction, breathing cadence during exercise — no wearable, no camera in locker rooms | 3+ ESP32-S3 mesh | Pose: 17 keypoints | [Breathing Sync](docs/edge-modules/exotic.md), [Gait Analysis](docs/edge-modules/medical.md) |
|
||||
| **Fitness & sports research** | Explore motion and breathing cadence without a wearable or camera; reliable posture correction requires a validated compatible pose model | 3+ ESP32-S3 mesh + edge host | Prototype; no live S3 pose accuracy claim | [Breathing Sync](docs/edge-modules/exotic.md), [Gait Analysis](docs/edge-modules/medical.md) |
|
||||
| **Childcare & schools** | Naptime breathing monitoring, playground headcount, restricted-area alerts — privacy-safe for minors | 2-4 ESP32-S3 per zone | Breathing: ±1 BPM | [Sleep Apnea](docs/edge-modules/medical.md), [Perimeter Breach](docs/edge-modules/security.md) |
|
||||
| **Event venues & concerts** | Crowd density mapping, crush-risk detection via breathing compression, emergency evacuation flow tracking | Multi-AP mesh (4-8 APs) | Density per m² | [Customer Flow](docs/edge-modules/retail.md), [Panic Motion](docs/edge-modules/security.md) |
|
||||
| **Stadiums & arenas** | Section-level occupancy for dynamic pricing, concession staffing, emergency egress flow modeling | Enterprise AP grid | 15-20 per AP mesh | [Dwell Heatmap](docs/edge-modules/retail.md), [Queue Length](docs/edge-modules/retail.md) |
|
||||
@@ -579,6 +654,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 +771,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.3.1`; 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 +785,8 @@ For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.3.1`; the quick c
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
Start with the user, build, and calibration guides; expand for the full reference map.
|
||||
|
||||
<details>
|
||||
@@ -719,6 +804,8 @@ Start with the user, build, and calibration guides; expand for the full referenc
|
||||
| [Semantic Primitives — Precision/Recall](docs/integrations/semantic-primitives-metrics.md) | Per-primitive F1 on the held-out paired-capture set: someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting, bathroom, fall-risk, bed-exit, no-movement, multi-room. |
|
||||
| [Claude Code / Codex Plugin](plugins/ruview/README.md) | The `ruview` plugin + marketplace — skills, `/ruview-*` commands, agents, and the Codex prompt mirror |
|
||||
| [Portable harness — `npx @ruvnet/ruview`](harness/ruview/README.md) | MetaHarness-minted, host-portable RuView operator harness — `ruview.*` MCP tools + the MEASURED-vs-CLAIMED honesty guardrail enforced in code ([ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md)). A lighter, multi-host companion to the in-repo plugin. |
|
||||
| [CSI Frame Selection — Measured](docs/csi-frame-selection-measured.md) | Which rate gate actually buys cross-node pairing, measured on a nine-node ESP32-C6 fleet as a 2x2 factorial in three Latin-square blocks. Selecting frames by 802.11 `rx_seq` roughly doubled pairing (+31.7 pp) while mesh-time alignment bought +0.5 pp, inside noise. Includes the trade it makes, a prediction that turned out wrong, the limits of the measurement, and `run_arms.py` so the comparison can be re-run rather than taken on trust. |
|
||||
| [ESP32-C6 Firmware Runbook](firmware/esp32-csi-node/RUNBOOK.md) | What to run, in what order, and what to check: the three-file build that keeps OTA rollback compiled in, a five-key pre-flash gate, which flash regions OTA can and cannot reach, a single-board diagnostic flash with a revert path, and the tunables whose right value depends on your house rather than on the chip. |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 205 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Domain Models](docs/ddd/README.md) | 8 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI, rvCSI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
||||
| [rvCSI — edge RF sensing runtime](https://github.com/ruvnet/rvcsi) | Rust-first / TypeScript-accessible / hardware-abstracted CSI runtime: multi-source ingestion (incl. real nexmon_csi `.pcap` from a **Raspberry Pi 5** / Pi 4 / Pi 3B+ — CYW43455 / BCM43455c0) → validation → DSP → typed events → RuVector RF memory ([ADR-095](docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md), [ADR-096](docs/adr/ADR-096-rvcsi-ffi-crate-layout.md), [domain model](docs/ddd/rvcsi-domain-model.md)). Now its own repo — [`ruvnet/rvcsi`](https://github.com/ruvnet/rvcsi) — vendored here under `vendor/rvcsi`; 9 `rvcsi-*` crates on crates.io, `@ruv/rvcsi` on npm, plus a Claude Code plugin. |
|
||||
@@ -741,6 +828,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.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# RuView Calibration Service (reference implementation)
|
||||
|
||||
Turn a **shared WiFi-CSI pose base model** into a room-specific one with a **30-second labeled
|
||||
calibration** and a **~11 KB per-room LoRA adapter**. This is the deployable resolution of the
|
||||
cross-subject / cross-environment generalization problem (full study: [ADR-150 §3.3–3.6](../../docs/adr/ADR-150-rf-foundation-encoder.md)).
|
||||
Fit a room-specific **~11 KB LoRA adapter** for a shared WiFi-CSI pose base from a short **labeled
|
||||
capture**. This is a measured MM-Fi reference path for cross-subject / cross-environment adaptation
|
||||
(full study: [ADR-150 §3.3–3.6](../../docs/adr/ADR-150-rf-foundation-encoder.md)); it is not proof of
|
||||
plug-and-play adaptation from a live ESP32 stream.
|
||||
|
||||
> **Not the proposed MERIDIAN fast path.** Both producers below require paired CSI and keypoint
|
||||
> labels, and their tensor shapes and adapter files are model-specific. ADR-027's automatic,
|
||||
> unlabeled 10-second MERIDIAN calibration remains **Proposed** and is not implemented as an
|
||||
> end-to-end deployment command.
|
||||
|
||||
## Why
|
||||
|
||||
@@ -66,8 +72,8 @@ Adapters are **model-specific**. There are two calibration producers here:
|
||||
| `cog_calibrate.py` | cog **conv+MLP** (`pose_v1.safetensors`, 56×20) | `[N,56,20]` | `.safetensors` (`fc1.a`/`fc1.b`/`fc2.a`/`fc2.b`) | Rust `cog-pose-estimation run --adapter` |
|
||||
|
||||
```bash
|
||||
# Produce a cog-format per-room adapter for the deployed Rust pose engine:
|
||||
python cog_calibrate.py --base pose_v1.safetensors --data calib.npz --out room.safetensors
|
||||
# Produce a cog-format per-room adapter from X:[N,56,20], Y:[N,17,2]:
|
||||
python cog_calibrate.py --base pose_v1.safetensors --data cog-calib.npz --out room.safetensors
|
||||
# then in the cog runtime:
|
||||
cog-pose-estimation run --config <cfg> --adapter room.safetensors
|
||||
```
|
||||
|
||||
@@ -10,6 +10,39 @@ func main() {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// One connected-link RSSI sample for the Rust adapter. Keep the legacy
|
||||
// no-argument 10 Hz stream below unchanged for Python callers.
|
||||
if CommandLine.arguments.contains("--scan-once") {
|
||||
guard interface.powerOn(),
|
||||
let channel = interface.wlanChannel(),
|
||||
interface.rssiValue() < 0 else {
|
||||
fputs("WiFi is not connected\n", stderr)
|
||||
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,
|
||||
"rssi": interface.rssiValue(),
|
||||
"noise": interface.noiseMeasurement(),
|
||||
"timestamp": Date().timeIntervalSince1970,
|
||||
"tx_rate": interface.transmitRate()
|
||||
]
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: sample, options: [.sortedKeys])
|
||||
FileHandle.standardOutput.write(data)
|
||||
FileHandle.standardOutput.write(Data([0x0a]))
|
||||
} catch {
|
||||
fputs("Could not encode WiFi sample: \(error)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Flush stdout automatically to prevent buffering issues with Python subprocess
|
||||
setbuf(stdout, nil)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endpoint": "fal-ai/flux-lora",
|
||||
"lora_url": "https://v3b.fal.media/files/b/0aa8c60d/OjaBs6Gi8pMhZqMo79iym_pytorch_lora_weights.safetensors",
|
||||
"prompt": "ruviewstyle room visualization, a living room with a glowing cyan pose skeleton overlay, wifi signal arcs radiating across the space, a vitals readout panel, dark tech aesthetic, synthetic sensor visualization",
|
||||
"scale": 1.0,
|
||||
"source_image_url": "https://v3b.fal.media/files/b/0aa8cf67/JH1qyC3GCKQ9ojkrAUXe9.png"
|
||||
}
|
||||
|
After Width: | Height: | Size: 585 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="303" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="44" y="42" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">COGNITUM ONE</text><text x="44" y="99" font-size="42" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Ambient Intelligence.</text><text x="44" y="147" font-size="25" fill="#78cfff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >At the edge of the physical world.</text><text x="44" y="190" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >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="#071124" text-anchor="start" font-weight="500" >Explore Cognitum</text><path d="M263 247H284M277 240L284 247L277 254" fill="none" stroke="#071124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="947" cy="151" r="173" fill="url(#halo)"/><g transform="translate(945 173) scale(1.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(945 145) scale(1.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#438fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g></g><ellipse cx="945" cy="150" rx="140" ry="30" transform="rotate(-25 945 150)" fill="none" stroke="#437cb2" stroke-dasharray="5 18" class="flow"/><ellipse cx="945" cy="150" rx="160" ry="43" transform="rotate(-25 945 150)" fill="none" stroke="#437cb2" stroke-dasharray="5 18" class="flow"/><ellipse cx="945" cy="150" rx="180" ry="56" transform="rotate(-25 945 150)" fill="none" stroke="#437cb2" stroke-dasharray="5 18" class="flow"/></svg>
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">COGNITUM ONE</text><text x="44" y="99" font-size="42" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Ambient Intelligence.</text><text x="44" y="147" font-size="25" fill="#72eee0" text-anchor="start" font-weight="650" letter-spacing="-0.6" >At the edge of the physical world.</text><text x="44" y="190" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >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" font-weight="500" >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 173) scale(1.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(945 145) scale(1.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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: 4.7 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 06</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >A guided AI operator</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Source-cited setup and verification.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >CLI, MCP and agent workflows.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 0L0 -33" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#78cfff" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">TOOLKIT GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 06</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >A guided AI operator</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Source-cited setup and verification.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >CLI, MCP and agent workflows.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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" font-weight="500" 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: 5.0 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 05</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Local automation</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Connect rooms to your smart home.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Home Assistant, MQTT and Matter.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-9" r="5" stroke="#438fff" fill="none" class="breathe"/></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">INTEGRATION GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 05</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Local automation</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Connect rooms to your smart home.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Home Assistant, MQTT and Matter.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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" font-weight="500" 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: 4.4 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 04</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Edge sensor mesh</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Provision, capture and calibrate.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Explore ESP32 firmware and runbooks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0c2242" stroke="#78cfff"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#438fff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#78cfff" fill="#78cfff" class="breathe"/></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">HARDWARE GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 04</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Edge sensor mesh</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Provision, capture and calibrate.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Explore ESP32 firmware and runbooks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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" font-weight="500" 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: 4.9 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 03</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Pose research</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Train and compare body keypoints.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Live accuracy differs from benchmarks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><circle cx="0" cy="-28" r="8" stroke="#b8f36c" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-14" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="0" cy="12" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="-24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="-18" cy="38" r="3" stroke="#ffb45e" fill="#071124" class="breathe"/><circle cx="18" cy="38" r="3" stroke="#ffb45e" fill="#071124" class="breathe"/></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">READ THE EVIDENCE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 03</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Pose research</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Train and compare body keypoints.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Live accuracy differs from benchmarks.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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" font-weight="500" 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: 4.9 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 01</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Presence & motion</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Explore changes in room occupancy.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Inspect motion features from CSI.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><circle cx="0" cy="-28" r="8" stroke="#b8f36c" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-14" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="0" cy="12" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="-24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><ellipse cx="0" cy="14" rx="40" ry="13" fill="none" stroke="#438fff" stroke-dasharray="3 8" class="flow"/></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">SENSING GUIDE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 01</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Presence & motion</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Explore changes in room occupancy.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Inspect motion features from CSI.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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><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" font-weight="500" 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: 4.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="579" height="275" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="30" y="39" font-size="11" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / 02</text><text x="30" y="94" font-size="29" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Breathing & vital signals</text><text x="30" y="136" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Inspect phase-derived signal trends.</text><text x="30" y="164" font-size="16" fill="#b0c2db" text-anchor="start" font-weight="500" >Validate estimates on your hardware.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#233f63" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g></g><path d="M30 204H550" fill="none" stroke="#2d4e77" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M30 204H550" fill="none" stroke="#78cfff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow" opacity=".5"/><text x="30" y="245" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">SIGNAL PIPELINE</text><path d="M510 239H540M532 231L540 239L532 247" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / 02</text><text x="30" y="94" font-size="29" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Breathing & vital signals</text><text x="30" y="136" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Inspect phase-derived signal trends.</text><text x="30" y="164" font-size="16" fill="#9abac5" text-anchor="start" font-weight="500" >Validate estimates on your hardware.</text><circle cx="465" cy="170" r="110" fill="url(#halo)"/><g transform="translate(480 161) scale(0.53)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(474 155) scale(0.68)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></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" font-weight="500" 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: 4.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(76 75) scale(0.75)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 0L0 -33" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#78cfff" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="140" y="63" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Choose your next step</text><text x="142" y="99" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">GUIDES / HARDWARE / INTEGRATIONS</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Choose your next step</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" font-weight="500" class="mono">GUIDES / HARDWARE / INTEGRATIONS</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><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: 4.3 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(76 75) scale(0.75)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0c2242" stroke="#78cfff"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#438fff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#78cfff" fill="#78cfff" class="breathe"/></g></g><text x="140" y="63" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Intelligence at the edge</text><text x="142" y="99" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">SENSORS / FIRMWARE / MODULES</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Intelligence at the edge</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" font-weight="500" class="mono">SENSORS / FIRMWARE / MODULES</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><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: 4.1 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(76 75) scale(0.75)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#438fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g></g><text x="140" y="63" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Evidence before accuracy claims</text><text x="142" y="99" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">BENCHMARKS / REPRODUCERS / LIMITS</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Evidence before accuracy claims</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" font-weight="500" class="mono">BENCHMARKS / REPRODUCERS / LIMITS</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><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.8 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(76 75) scale(0.75)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><circle cx="0" cy="-28" r="8" stroke="#b8f36c" fill="none" /><path d="M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-14" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="0" cy="12" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="-24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><circle cx="24" cy="-6" r="2.3" stroke="#ffb45e" fill="#ffb45e" class="spark"/><ellipse cx="0" cy="14" rx="40" ry="13" fill="none" stroke="#438fff" stroke-dasharray="3 8" class="flow"/></g></g><text x="140" y="63" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Explore the capabilities</text><text x="142" y="99" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">SENSING / MODELS / AUTOMATION</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Explore the capabilities</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" font-weight="500" class="mono">SENSING / MODELS / AUTOMATION</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><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.8 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="147" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(76 75) scale(0.75)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#233f63" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="140" y="63" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >From recordings to models</text><text x="142" y="99" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">CAPTURE / TRAIN / EVALUATE</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><path d="M1140 70H1160M1152 62L1160 70L1152 78" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="140" y="63" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >From recordings to models</text><text x="142" y="99" font-size="12" fill="#72eee0" text-anchor="start" font-weight="500" class="mono">CAPTURE / TRAIN / EVALUATE</text><g transform="translate(1012 84) scale(0.55)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><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.7 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="449" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="48" y="44" font-size="13" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">RUVIEW / COGNITUM ECOSYSTEM</text><text x="48" y="121" font-size="76" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >RuView</text><text x="48" y="184" font-size="38" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Spaces become signals.</text><text x="48" y="234" font-size="32" fill="#78cfff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Signals become understanding.</text><text x="50" y="279" font-size="18" fill="#b0c2db" text-anchor="start" font-weight="500" >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="#071124" text-anchor="start" font-weight="500" >Explore the guide</text><path d="M272 352H292M285 345L292 352L285 359" fill="none" stroke="#071124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><text x="49" y="419" font-size="11" fill="#b0c2db" text-anchor="start" font-weight="500" 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="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M790 282L690 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M830 282L770 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M870 282L850 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M910 282L930 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M950 282L1010 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M990 282L1090 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M1030 282L1170 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M1070 282L1250 443" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 306H1168" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 336H1168" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 372H1168" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><path d="M680 418H1168" fill="none" stroke="#162f50" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M803 157L1008 106L1137 177L930 236ZM803 157V314L930 391L1137 324V177M930 236V391M1008 106V268L803 314M1008 268L1137 324" fill="none" stroke="#5490c5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g><g transform="translate(0 80)"><g class="scan"><path d="M803 157L1008 106L1137 177L930 236Z" fill="#78cfff" fill-opacity=".1" stroke="#78cfff" stroke-opacity=".6"/></g></g><g transform="translate(974 282)"><g class="orbit"><ellipse rx="135" ry="36" fill="none" stroke="#438fff" stroke-dasharray="3 11" opacity=".4"/><circle cx="135" cy="0" r="4" fill="#9ee7ff"/></g></g><g transform="translate(974 312)"><ellipse rx="140" ry="44" fill="none" stroke="#438fff" class="wave" style="animation-delay:-0.0s"/></g><g transform="translate(974 312)"><ellipse rx="140" ry="44" fill="none" stroke="#438fff" class="wave" style="animation-delay:-1.3s"/></g><g transform="translate(974 312)"><ellipse rx="140" ry="44" fill="none" stroke="#438fff" class="wave" style="animation-delay:-2.6s"/></g><circle cx="849" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-0.0s"/><circle cx="893" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-0.5s"/><circle cx="937" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-1.0s"/><circle cx="981" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-1.5s"/><circle cx="1025" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-2.0s"/><circle cx="1069" cy="198" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-2.5s"/><circle cx="849" cy="233" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-3.0s"/><circle cx="893" cy="233" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-0.0s"/><circle cx="937" cy="233" r="1.7" stroke="#438fff" fill="#438fff" class="spark" style="animation-delay:-0.5s"/><circle cx="981" cy="233" r="1.7" stroke="#43Line truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">RUVIEW / COGNITUM ECOSYSTEM</text><text x="48" y="121" font-size="76" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >RuView</text><text x="48" y="184" font-size="38" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Spaces become signals.</text><text x="48" y="234" font-size="32" fill="#72eee0" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Signals become understanding.</text><text x="50" y="279" font-size="18" fill="#9abac5" text-anchor="start" font-weight="500" >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" font-weight="500" >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" font-weight="500" 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" /><g class="float"><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><g transform="translate(0 80)"><g class="scan"><path d="M803 157L1008 106L1137 177L930 236Z" fill="#72eee0" fill-opacity=".1" stroke="#72eee0" stroke-opacity=".6"/></g></g><g transform="translate(974 282)"><g class="orbit"><ellipse rx="135" ry="36" fill="none" stroke="#70b6ff" stroke-dasharray="3 11" opacity=".4"/><circle cx="135" cy="0" r="4" fill="#91f6df"/></g></g><circle cx="849" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-0.0s"/><circle cx="893" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-0.5s"/><circle cx="937" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-1.0s"/><circle cx="981" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-1.5s"/><circle cx="1025" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-2.0s"/><circle cx="1069" cy="198" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-2.5s"/><circle cx="849" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-3.0s"/><circle cx="893" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-0.0s"/><circle cx="937" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-0.5s"/><circle cx="981" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-1.0s"/><circle cx="1025" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-1.5s"/><circle cx="1069" cy="233" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-2.0s"/><circle cx="849" cy="268" r="1.7" stroke="#70b6ff" fill="#70b6ff" class="spark" style="animation-delay:-2.5s"/><circlLine truncated
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"style": "RuView navy, electric blue, lime and amber",
|
||||
"motion": "Decorative CSS; prefers-reduced-motion supported; no script or remote dependencies",
|
||||
"assets": [
|
||||
{
|
||||
"file": "assets/readme/hero-spatial-v3.svg",
|
||||
"title": "RuView: spaces become signals",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 450
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-start-spatial-v3.svg",
|
||||
"title": "Start here",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-hardware-spatial-v3.svg",
|
||||
"title": "Hardware",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-models-spatial-v3.svg",
|
||||
"title": "Models",
|
||||
"target": "https://huggingface.co/ruvnet/wifi-densepose-pretrained",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-agents-spatial-v3.svg",
|
||||
"title": "AI toolkit",
|
||||
"target": "harness/ruview/README.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-home-spatial-v3.svg",
|
||||
"title": "Smart home",
|
||||
"target": "docs/integrations/home-assistant.md",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/nav-seed-spatial-v3.svg",
|
||||
"title": "Cognitum",
|
||||
"target": "https://cognitum.one/seed",
|
||||
"width": 180,
|
||||
"height": 116
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-presence-spatial-v3.svg",
|
||||
"title": "Presence & motion",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-vitals-spatial-v3.svg",
|
||||
"title": "Breathing & vital signals",
|
||||
"target": "v2/crates/wifi-densepose-signal/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-pose-spatial-v3.svg",
|
||||
"title": "Pose research",
|
||||
"target": "docs/benchmarks/pose-estimation-cog.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-edge-spatial-v3.svg",
|
||||
"title": "Edge sensor mesh",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-automation-spatial-v3.svg",
|
||||
"title": "Local automation",
|
||||
"target": "docs/integrations/home-assistant.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/feature-agents-spatial-v3.svg",
|
||||
"title": "A guided AI operator",
|
||||
"target": "harness/ruview/README.md",
|
||||
"width": 580,
|
||||
"height": 276
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/overview-spatial-v3.svg",
|
||||
"title": "RuView sensing overview",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 396
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-features-spatial-v3.svg",
|
||||
"title": "Explore the capabilities",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-edge-spatial-v3.svg",
|
||||
"title": "Intelligence at the edge",
|
||||
"target": "firmware/esp32-csi-node/README.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-learning-spatial-v3.svg",
|
||||
"title": "From recordings to models",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-evidence-spatial-v3.svg",
|
||||
"title": "Evidence before accuracy claims",
|
||||
"target": "docs/benchmarks/pose-estimation-cog.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/header-docs-spatial-v3.svg",
|
||||
"title": "Choose your next step",
|
||||
"target": "docs/user-guide.md",
|
||||
"width": 1200,
|
||||
"height": 148
|
||||
},
|
||||
{
|
||||
"file": "assets/readme/cognitum-spatial-v3.svg",
|
||||
"title": "Explore Cognitum One",
|
||||
"target": "https://cognitum.one",
|
||||
"width": 1200,
|
||||
"height": 304
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 0L0 -33" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="0" cy="-33" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L-35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="-35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><path d="M0 0L35 23" fill="none" stroke="#438fff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="flow"/><circle cx="35" cy="23" r="7" stroke="#78cfff" fill="none" class="breathe"/><circle cx="0" cy="0" r="15" stroke="#78cfff" fill="none" /><path d="M-6 0L-1 5L8 -6" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >AI toolkit</text></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >AI toolkit</text></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><rect x="-25" y="-25" width="50" height="50" rx="7" fill="#0c2242" stroke="#78cfff"/><rect x="-13" y="-13" width="26" height="26" rx="3" fill="none" stroke="#438fff"/><path d="M-16 -36V-25M-16 25V36M-36 -16H-25M25 -16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M0 -36V-25M0 25V36M-36 0H-25M25 0H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><path d="M16 -36V-25M16 25V36M-36 16H-25M25 16H36" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="0" r="4" stroke="#78cfff" fill="#78cfff" class="breathe"/></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >Hardware</text></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >Hardware</text></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="0" cy="-9" r="5" stroke="#438fff" fill="none" class="breathe"/></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >Smart home</text></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >Smart home</text></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43" fill="none" stroke="#b8f36c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/><path d="M-42 31H43" fill="none" stroke="#233f63" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >Models</text></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >Models</text></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><path d="M-15 1L-5 11L17 -12" fill="none" stroke="#438fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trace"/></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >Cognitum</text></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >Cognitum</text></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="179" height="115" rx="18" fill="url(#wash)" stroke="#2d4e77"/><g transform="translate(90 45) scale(0.65)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-10.4 -5.2Q0 -18.2 10.4 -5.2" fill="none" stroke="#78cfff" 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="#78cfff" 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="#78cfff" 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="#78cfff" fill="#78cfff"/><path d="M0 12V-1" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="90" y="98" font-size="16" fill="#f3f7ff" text-anchor="middle" font-weight="500" >Start here</text></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="90" y="98" font-size="16" fill="#ecfafb" text-anchor="middle" font-weight="500" >Start here</text></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,21 @@
|
||||
<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="#102749"/><stop offset=".55" stop-color="#071429"/><stop offset="1" stop-color="#03091a"/></linearGradient><linearGradient id="accent"><stop stop-color="#9ee7ff"/><stop offset="1" stop-color="#348bff"/></linearGradient><radialGradient id="halo"><stop stop-color="#348bff" stop-opacity=".17"/><stop offset="1" stop-color="#348bff" stop-opacity="0"/></radialGradient></defs><style>
|
||||
text{font-family:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
.wave{transform-box:fill-box;transform-origin:center;animation:wave 4s ease-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@keyframes wave{0%{transform:scale(.5);opacity:.7}100%{transform:scale(1.3);opacity:0}}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important}}
|
||||
</style><rect x=".5" y=".5" width="1199" height="395" rx="18" fill="url(#wash)" stroke="#2d4e77"/><text x="36" y="44" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" class="mono">SIGNAL TO CONTEXT</text><text x="36" y="89" font-size="34" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >A sensing loop you can inspect.</text><rect x="36" y="120" width="336" height="220" rx="14" fill="#091b35" stroke="#315685"/><text x="60" y="153" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" >01</text><text x="60" y="188" font-size="26" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Capture RF</text><text x="60" y="216" font-size="15" fill="#b0c2db" text-anchor="start" font-weight="500" >CSI from supported hardware</text><g transform="translate(305 278) scale(0.35)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(305 270) scale(0.5)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><path d="M-10.4 -5.2Q0 -18.2 10.4 -5.2" fill="none" stroke="#78cfff" 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="#78cfff" 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="#78cfff" 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="#78cfff" fill="#78cfff"/><path d="M0 12V-1" fill="none" stroke="#78cfff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></g></g><text x="60" y="311" font-size="13" fill="#b0c2db" text-anchor="start" font-weight="500" >Provision and calibrate</text><path d="M380 229H420M412 221L420 229L412 237" fill="none" stroke="#78cfff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flow"/><rect x="432" y="120" width="336" height="220" rx="14" fill="#091b35" stroke="#315685"/><text x="456" y="153" font-size="12" fill="#78cfff" text-anchor="start" font-weight="500" >02</text><text x="456" y="188" font-size="26" fill="#f3f7ff" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Extract context</text><text x="456" y="216" font-size="15" fill="#b0c2db" text-anchor="start" font-weight="500" >Signal processing and models</text><g transform="translate(701 278) scale(0.35)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0b1b35" stroke="#2e5b8e"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#4473a5"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#438fff" fill-opacity=".12" stroke="#78cfff" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#438fff" fill="#438fff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#438fff" fill="#438fff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#438fff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#9ee7ff"/></g><ellipse cx="0" cy="36" rx="85" ry="28" fill="none" stroke="#438fff" class="wave"/></g><g transform="translate(701 270) scale(0.5)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#142c50" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#2e527d" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g Line truncated
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -0,0 +1,19 @@
|
||||
<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:"Avenir Next","Segoe UI",Helvetica,Arial,sans-serif}
|
||||
.mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;letter-spacing:1.6px}
|
||||
.float{animation:float 5s ease-in-out infinite}
|
||||
.flow{stroke-dasharray:5 12;animation:flow 5s linear infinite}
|
||||
.breathe{animation:breathe 4s ease-in-out infinite}
|
||||
.trace{stroke-dasharray:400;animation:trace 6s ease-in-out infinite}
|
||||
.scan{animation:scan 7s ease-in-out infinite}
|
||||
.orbit{transform-box:fill-box;transform-origin:center;animation:orbit 18s linear infinite}
|
||||
.spark{animation:spark 3.5s ease-in-out infinite}
|
||||
@keyframes float{0%,100%{transform:translate(0,0)}50%{transform:translate(3px,-10px)}}
|
||||
@keyframes scan{0%,100%{transform:translateY(0)}50%{transform:translateY(-76px)}}
|
||||
@keyframes orbit{to{transform:rotate(360deg)}}
|
||||
@keyframes spark{50%{opacity:.25}}
|
||||
@keyframes flow{to{stroke-dashoffset:-136}}
|
||||
@keyframes breathe{50%{opacity:.4}}
|
||||
@keyframes trace{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:90}}
|
||||
@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" font-weight="500" class="mono">SIGNAL TO CONTEXT</text><text x="36" y="89" font-size="34" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >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" font-weight="500" >01</text><text x="60" y="188" font-size="26" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Capture RF</text><text x="60" y="216" font-size="15" fill="#9abac5" text-anchor="start" font-weight="500" >CSI from supported hardware</text><g transform="translate(305 278) scale(0.35)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(305 270) scale(0.5)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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></g><text x="60" y="311" font-size="13" fill="#9abac5" text-anchor="start" font-weight="500" >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" font-weight="500" >02</text><text x="456" y="188" font-size="26" fill="#ecfafb" text-anchor="start" font-weight="650" letter-spacing="-0.6" >Extract context</text><text x="456" y="216" font-size="15" fill="#9abac5" text-anchor="start" font-weight="500" >Signal processing and models</text><g transform="translate(701 278) scale(0.35)"><path d="M-82 36L0 -6L82 36L0 78Z" fill="#0a2733" stroke="#286171"/><path d="M-82 36V-52L0 -94L82 -52V36M-82 -52L0 -10L82 -52M0 -10V78" fill="none" stroke="#366d80"/><g class="scan"><path d="M-81 32L0 -10L81 32L0 74Z" fill="#5fe3d8" fill-opacity=".12" stroke="#72eee0" stroke-opacity=".7"/></g><circle cx="-40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="-15" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="40" cy="6" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="-20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="20" cy="30" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><circle cx="0" cy="49" r="2" stroke="#70b6ff" fill="#70b6ff" class="spark"/><g class="orbit"><ellipse cx="0" cy="4" rx="102" ry="30" fill="none" stroke="#70b6ff" stroke-opacity=".4" stroke-dasharray="2 8"/><circle cx="102" cy="4" r="4" fill="#91f6df"/></g></g><g transform="translate(701 270) scale(0.5)"><ellipse cx="0" cy="48" rx="43" ry="12" fill="#133a45" opacity=".5"/><path d="M-43 48L0 27L43 48L0 69Z" fill="none" stroke="#285c68" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" /><g class="float"><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="roundLine truncated
|
||||
|
After Width: | Height: | Size: 9.3 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 |
|
After Width: | Height: | Size: 5.5 MiB |
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "pretrain-1775182186",
|
||||
"name": "pretrain-1775182186",
|
||||
"label": "mixed-activity",
|
||||
"started_at": "2026-04-03T02:09:46Z",
|
||||
"ended_at": "2026-04-03T02:11:46Z",
|
||||
"duration_secs": 120,
|
||||
"frame_count": 5783,
|
||||
"file_size_bytes": 2580539,
|
||||
"file_path": "data/recordings\\pretrain-1775182186.csi.jsonl",
|
||||
"nodes": {
|
||||
"2": 2886,
|
||||
"1": 2897
|
||||
}
|
||||
}
|
||||
@@ -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.*
|
||||
@@ -139,6 +139,32 @@ Implement the §3.3 mapping: `effective_class → PrivacyClass`, `cog-ha-matter`
|
||||
Add an opt-in `/ws/field` endpoint (or a `field_events` array on `SensingUpdate` behind a flag) carrying the signed `FieldEvent` + a privacy badge. Add an ingest route to `rufield-viewer` (it has none today — `server.rs:63-72`) so it can replay RuView's live feed instead of only `SyntheticSim`. **Gate:** a WS integration test asserting a connected client receives a privacy-badged, signature-verifiable `FieldEvent`; a viewer test asserting the new ingest route renders a live event. The `cognitum` appliance can speak RuField by consuming this endpoint (it already runs `ruview-vitals-worker`); deferred to its own ADR.
|
||||
|
||||
**P4 — fusion composition + multi-modality (ARCHITECTURE, optional).**
|
||||
|
||||
> **Update — second modality landed as a library.** Open question 5 below asked
|
||||
> whether the second modality should be `rvcsi`. It is **ultrasonic**, because
|
||||
> the cost collapsed: `rufield-adapters` now ships `UltrasonicReplayAdapter`,
|
||||
> the first adapter for `Modality::Ultrasonic` (registry code 7, empty since
|
||||
> v0.1), which parses, validates and signs [BatVu](https://github.com/ruvnet/batvu)
|
||||
> range profiles upstream. RuView only has to decide what it will put on a wire.
|
||||
>
|
||||
> `wifi-densepose-rufield::ultrasonic` is that decision, and it is expressed
|
||||
> structurally: the adapter is configured for its 32-bin coarse output (`P1`,
|
||||
> egress-safe) rather than its full per-bin frame (`P0`, edge-local), because a
|
||||
> consumer cannot un-coarsen a coarse profile whereas a runtime check can be
|
||||
> reordered. The `network_egress_allowed` gate still runs and is asserted to
|
||||
> drop nothing.
|
||||
>
|
||||
> Gates: `tests/ultrasonic_gates.rs`, 12 tests — round-trip, signature-verify,
|
||||
> fusion ingest, P1 on **both** tensor and observation, structural unreachability
|
||||
> of P4/P5, trust-tier refusal in both directions, determinism, whole-file
|
||||
> rejection of a malformed recording. Plus one asserting the honest negative
|
||||
> result: **an ultrasonic scan produces no fused inferences at all**, because the
|
||||
> adapter declines to populate `presence` (one transducer pair cannot tell a
|
||||
> person from a coat on a chair) and the engine's feature vocabulary is entirely
|
||||
> statements about a body. RuField v0.1 has no predicate for static geometry.
|
||||
>
|
||||
> Not wired into the running server. P1 shipped as a library before P3 wired it
|
||||
> in; this follows the same staging.
|
||||
Wire a second modality (cheapest: an `rvcsi`-sourced event, or recorded mmWave) into `RuFieldFusion` alongside the WiFi event, proving cross-modality fusion above ruvsense. **Gate:** a fusion test with two modalities producing ≥1 cross-modal inference, with provenance coverage 100%.
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted — **implemented** (O1–O9 in `@ruvnet/ruview@0.2.0`; security/community extension in `0.3.0`, ADR-283; source-cited guidance in `0.3.1`): fail-closed schemas and MCP policy, async dispatch, zero runtime dependencies, bounded/redacted local Claude/Codex adapters, reviewed shared brain, source-checked capability guidance, and replay-verified Darwin/Flywheel gate. CI gate: `ruview-harness-flywheel.yml` |
|
||||
| **Status** | Accepted — **implemented** (O1–O9 in `@ruvnet/ruview@0.2.0`; security/community extension in `0.3.0`, ADR-283; source-cited guidance in `0.3.1`; guarded Cognitum Spaces OAuth read in `0.4.0`, ADR-325): fail-closed schemas and MCP policy, async dispatch, zero runtime dependencies, bounded/redacted local Claude/Codex adapters, reviewed shared brain, source-checked capability guidance, credential-gated external reads, and replay-verified Darwin/Flywheel gate. CI gate: `ruview-harness-flywheel.yml` |
|
||||
| **Date** | 2026-07-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RUVIEW-NPM-REVIEW-1** |
|
||||
|
||||
@@ -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`.
|
||||
@@ -31,6 +31,26 @@ bounded output/time, secret redaction and realpath-based RuView checkout
|
||||
validation. Write mode requires two explicit flags and never uses permission or
|
||||
sandbox bypasses.
|
||||
|
||||
## Credentialed external reads
|
||||
|
||||
Read-only cloud access is not equivalent to an uncredentialed local read. The
|
||||
Cognitum Spaces adapter therefore delegates OAuth and response validation to
|
||||
the Rust `wifi-densepose` client, never accepts bearer tokens or API keys, and
|
||||
removes the API-key compatibility environment from the child process. Its MCP
|
||||
tool is denied unless the server operator grants `credential-use`; MCP callers
|
||||
cannot select a credential path or API origin. The adapter uses only an
|
||||
installed `wifi-densepose` binary; it never executes Cargo build scripts from
|
||||
an auto-detected checkout while holding credential authority. The tool is
|
||||
marked open-world and independently rechecks response size, structure, privacy
|
||||
class, and prohibited raw fields.
|
||||
|
||||
An expiring access token may rotate the stored refresh credential. The MCP
|
||||
annotation is therefore non-read-only and non-idempotent even though the cloud
|
||||
data operation is read-only. That bounded authentication side effect is
|
||||
disclosed in the schema and result. It does not change the cloud operation from
|
||||
read-only and confers no write or action authority. ADR-325 remains authoritative
|
||||
for the Spaces data and policy boundary.
|
||||
|
||||
## Shared brain
|
||||
|
||||
The public brain is committed JSONL, not a shared mutable database. Canonical
|
||||
@@ -67,5 +87,6 @@ autonomously promotes or publishes an evolved candidate.
|
||||
Contributors can explore RuView with either major local CLI and share durable
|
||||
findings without sharing secrets. Improvements become reproducible proposals
|
||||
with frozen evaluation evidence. The cost is a larger development-only npm
|
||||
lockfile, a 128 KiB unpacked-package budget (the current tarball is below that
|
||||
bound), and explicit maintenance of the corpus, genome and gate.
|
||||
lockfile, a 160 KiB unpacked-package budget after adding the duplicated host
|
||||
playbook and bounded OAuth adapter (the package remains runtime-dependency-free),
|
||||
and explicit maintenance of the corpus, genome and gate.
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-299: Repository CSI data-incident controls — ignore rules and a pre-commit/CI policy check
|
||||
|
||||
- **Status**: Accepted — controls implemented; tree remediation gated on owner sign-off
|
||||
- **Status**: Accepted — controls and current-tree remediation implemented; history coordination pending
|
||||
- **Date**: 2026-08-11
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: privacy, data-governance, ci, security, incident
|
||||
@@ -27,20 +27,20 @@ formatting nit.
|
||||
or present as tracked files, with a message pointing here. Tests may use
|
||||
only synthetic or expressly-consented minimal fixtures.
|
||||
|
||||
**Explicitly gated on data-owner sign-off (NOT done autonomously):**
|
||||
**Owner-authorized current-tree remediation (2026-08-15):**
|
||||
|
||||
- Removing the existing recordings from the tree, and any history rewrite, are
|
||||
outward-facing/destructive and require the data owner to first establish
|
||||
provenance, consent, purpose, retention authority, and redistribution
|
||||
rights. The review is correct that rewriting `origin` does not erase forks
|
||||
and clones; coordination is required. This ADR records the controls and the
|
||||
required follow-up; it does not delete the data.
|
||||
- The data owner authorized removal of the six known CSI capture and metadata
|
||||
files from the current tree. The removal is recoverable from Git history and
|
||||
does not claim to erase existing clones, forks, caches, or release artifacts.
|
||||
- Any history rewrite remains a separate coordinated incident-response action.
|
||||
It requires an inventory of affected refs and releases, downstream notice,
|
||||
credential and artifact review, and an explicit execution plan.
|
||||
|
||||
## Consequences
|
||||
|
||||
- No new CSI captures can be committed (ignore + policy check).
|
||||
- The existing tracked recordings remain until the owner decides; the incident
|
||||
is documented and the guard prevents worsening it.
|
||||
- The six known tracked recordings are absent from the current tree. Historical
|
||||
copies remain until a separately authorized and coordinated history rewrite.
|
||||
- CI gains one fast policy job; contributors get a local pre-commit check.
|
||||
|
||||
## Validation
|
||||
@@ -48,3 +48,5 @@ formatting nit.
|
||||
- Policy-check unit tests: a staged `*.csi.jsonl` fails; a synthetic fixture
|
||||
under an allowed test path passes; the check is deterministic and offline.
|
||||
- Manual confirmation that the new ignore globs cover both active directories.
|
||||
- `bash scripts/csi-data-policy-check.sh --tracked` passes after the authorized
|
||||
current-tree removal.
|
||||
@@ -0,0 +1,364 @@
|
||||
# ADR-323: Native Rust physics-constrained pose refinement
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-08-15
|
||||
- **Deciders**: ruv
|
||||
- **Owners**: RuView perception and edge runtime maintainers
|
||||
- **Tags**: pose, physics, rust, uncertainty, provenance, abstention, edge
|
||||
- **Numbering note**: ADR-323 is the next free number in the authoring checkout. Re-run the ADR index/collision check immediately before merge and rename if needed.
|
||||
- **Extends**: ADR-020, ADR-027, ADR-079, ADR-101, ADR-135, ADR-145, ADR-150, ADR-273, ADR-279, ADR-282, ADR-295, ADR-296, ADR-297, ADR-298, ADR-302, ADR-303, ADR-304, ADR-305, ADR-306
|
||||
- **Supersedes**: None
|
||||
|
||||
## Executive decision
|
||||
|
||||
RuView will add a clean-room native Rust boundary between RF pose inference and
|
||||
semantic publication. It will preserve the immutable RF observation, publish a
|
||||
physics assessment, optionally produce a bounded corrected candidate, and
|
||||
abstain when required evidence is absent. It must never increase observational
|
||||
confidence merely because a pose is physically plausible.
|
||||
|
||||
Three independently gated layers are adopted:
|
||||
|
||||
1. A deterministic kinematic auditor and bounded covariance-weighted projector
|
||||
using Rust and `nalgebra`.
|
||||
2. An optional articulated-body dynamics auditor using `rapier3d`.
|
||||
3. A later optional supervised residual model using Burn.
|
||||
|
||||
The first production milestone is deterministic audit. It is not a GRIP port,
|
||||
not PPO, and not evidence that the current pose observer is production-ready.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-101's committed Cog emits 17 COCO keypoints as normalized 2D coordinates.
|
||||
Its model has no per-joint uncertainty head and publishes a constant confidence.
|
||||
The sensing server also contains renderer-oriented EMA and bone clamping. These
|
||||
surfaces cannot establish metric 3D physics and can make weak evidence look
|
||||
more convincing.
|
||||
|
||||
Pose output can violate bone length, floor, velocity, acceleration, and temporal
|
||||
continuity constraints. Downstream consumers also cannot reliably distinguish
|
||||
observed coordinates from derived correction. The rejected premise is:
|
||||
"physically plausible means more likely correct." Plausibility is only a prior;
|
||||
many incorrect poses are plausible.
|
||||
|
||||
GRIP is architectural inspiration for an observer/controller split, but it
|
||||
observes four wearable IMUs and pressure insoles and drives a simulator. RuView
|
||||
observes RF, so GRIP weights are not input-compatible. External code, weights,
|
||||
simulators, and datasets require independent license review and never enter the
|
||||
runtime dependency graph by implication.
|
||||
|
||||
## Outcome and actors
|
||||
|
||||
For every accepted person track/timestamp, the engine returns exactly one
|
||||
`PoseRefinementV1`, including off, timeout, rejection, and abstention paths:
|
||||
|
||||
- immutable `PoseObservationV2` content hash;
|
||||
- constraint residuals and quality disposition;
|
||||
- an optional bounded candidate and an explicit `selected` bit;
|
||||
- a typed reason when correction is unavailable;
|
||||
- model, calibration, configuration, and optional learned-artifact provenance.
|
||||
|
||||
The RF observer owns observations and calibrated uncertainty; tracking owns
|
||||
identity stability; physics owns assessment/correction only; the sensing server
|
||||
owns deadlines, modes, publication, and rollback; the evidence engine owns
|
||||
release evaluation; clients choose raw/both/refined without silent fallback.
|
||||
|
||||
## Input and coordinate contract
|
||||
|
||||
Metric correction requires a monotonic nanosecond timestamp, session-scoped
|
||||
track ID, sequence and sensor epoch, 17 ordered COCO joints in metric X/Y/Z,
|
||||
per-joint positive-semidefinite covariance calibrated on held-out data, a
|
||||
versioned right-handed Z-up room frame, a normalized upward floor plane, model
|
||||
and calibration hashes, ADR-302 trust state, and authenticated/replay-protected
|
||||
source provenance.
|
||||
|
||||
`Image2d` observations may be audited for image-plane ratios and continuity but
|
||||
must never enter 3D projection/dynamics or be called physically corrected.
|
||||
Unknown trust, missing calibration, missing uncertainty, stale/non-monotonic
|
||||
input, non-finite values, invalid covariance, excessive tracks, and room-bound
|
||||
violations fail to raw output with a typed reason.
|
||||
|
||||
## Public contracts
|
||||
|
||||
`wifi-densepose-core` owns `PoseObservationV2` and `PoseRefinementV1`; no
|
||||
duplicate server/Cog contract is permitted. Public output remains COCO17. The
|
||||
engine derives pelvis and thorax virtually and never labels them observed.
|
||||
|
||||
The raw content hash is deterministic and excludes its own hash field. The
|
||||
idempotency key is `(sensor_epoch, sequence, track_id, raw_hash, config_hash)`.
|
||||
An exact duplicate returns the cached result; same sequence with different
|
||||
content is a replay rejection.
|
||||
|
||||
Contact is `hypothesis` unless a measured sensor and its provenance say
|
||||
otherwise. Raw, derived, hypothesis, and unknown labels must survive every
|
||||
projection.
|
||||
|
||||
## Confidence invariant
|
||||
|
||||
For upstream calibrated confidence `c_obs`, normalized residual `r`, and
|
||||
normalized intervention `i`:
|
||||
|
||||
```text
|
||||
c_physics = exp(-(beta_r * r + beta_i * i))
|
||||
c_effective = min(c_obs, c_obs * c_physics)
|
||||
0 <= c_effective <= c_obs <= 1
|
||||
```
|
||||
|
||||
Only a separately witnessed multimodal fusion contract may increase fused
|
||||
confidence.
|
||||
|
||||
## Deterministic projector
|
||||
|
||||
The default `kinematic` feature has no Rapier, Burn, ONNX, libtorch, Python,
|
||||
CUDA, or network dependency. Per bounded iteration it:
|
||||
|
||||
1. projects observed parent/child distances toward anonymous track-scoped
|
||||
bone-length posteriors;
|
||||
2. applies broad joint/trunk validity checks without an upright prior;
|
||||
3. bounds temporal motion and resets derivatives after gaps;
|
||||
4. resolves floor penetration only, allowing seated, kneeling, prone, child-
|
||||
scale, mobility-aid, and genuine-fall poses;
|
||||
5. recomputes residuals and stops below epsilon.
|
||||
|
||||
Initial operator-owned caps are four iterations (hard maximum eight), 0.20 m
|
||||
single-joint correction, 0.10 m root correction, 250 ms derivative gap, 500 ms
|
||||
track reset, ten known joints, a 100 m metric room bound, a separate 16,384
|
||||
image-coordinate audit bound, and a 5 ms one-track Pi 5 p95 gate. Keeping image
|
||||
and metric bounds separate prevents legitimate pixel observations from
|
||||
weakening the physical room bound. A candidate over either correction cap is
|
||||
discarded in full.
|
||||
|
||||
Bone posteriors are initialized only from high-confidence frames, anonymous,
|
||||
memory-only, track-scoped, and deleted on expiry. Persistent personalization is
|
||||
outside this ADR and requires consent/retention/deletion governance.
|
||||
|
||||
## Optional dynamics and learned layers
|
||||
|
||||
`dynamics` adds a process-owned Rapier humanoid and begins audit-only. Network
|
||||
input may never provide Rapier snapshots, bodies, constraints, solver limits,
|
||||
or arbitrary geometry. Dynamics approval is independent of kinematic approval.
|
||||
|
||||
`learned` uses first-party Burn 0.21 core/NN components without `burn-tch`
|
||||
because this workspace already has a different native libtorch link.
|
||||
`learned-cpu` adds the ndarray backend. The implemented two-layer GRU uses a
|
||||
20-frame history and width 128 to predict bounded residuals, uncertainty,
|
||||
foot-contact hypotheses, and abstention. Verified model records can be loaded
|
||||
from bytes and executed natively; no trained artifact is shipped or approved.
|
||||
The resolved Burn/CubeCL graph declares Rust 1.92, while the workspace file
|
||||
pins Rust 1.89 and the authoring host provides Rust 1.91.1.
|
||||
`--ignore-rust-version` is diagnostic evidence only: learned activation remains
|
||||
blocked until an approved Rust 1.92 release-toolchain change builds it without
|
||||
that override. Residuals are hard-clipped to deterministic caps and cannot
|
||||
bypass validation or confidence monotonicity. PPO is deferred until measured
|
||||
evidence identifies a failure supervised residual learning cannot address.
|
||||
|
||||
## Feature boundary
|
||||
|
||||
```text
|
||||
default = kinematic
|
||||
dynamics = rapier3d
|
||||
learned = burn-core + burn-nn
|
||||
learned-cpu = learned + burn-ndarray
|
||||
learned-train = learned + burn-train
|
||||
learned-wgpu = learned-train + burn-wgpu
|
||||
learned-cuda = learned-train + burn-cuda
|
||||
deterministic = rapier3d?/enhanced-determinism
|
||||
```
|
||||
|
||||
The lockfile is release authority. The learned feature currently requires the
|
||||
toolchain supported by Burn/CubeCL's resolved graph; this does not change the
|
||||
default edge build.
|
||||
|
||||
## Runtime modes and API
|
||||
|
||||
Rollout is `OFF -> AUDIT -> SHADOW_CORRECT -> OPT_IN_CORRECT -> DEFAULT_CORRECT`.
|
||||
Evidence permits forward transitions; any regression returns immediately to
|
||||
audit/off. Correct selection additionally requires authenticated sensor
|
||||
identity and replay protection from ADR-305. High model confidence cannot
|
||||
override missing source authentication.
|
||||
|
||||
Existing pose fields stay unchanged and raw remains the migration default:
|
||||
|
||||
```text
|
||||
GET /api/v1/pose/current?view=raw
|
||||
GET /api/v1/pose/current?view=both
|
||||
GET /api/v1/pose/current?view=refined
|
||||
```
|
||||
|
||||
Refined-only returns HTTP 409 with `pose_refined_unavailable` when no selected
|
||||
candidate exists. It never silently returns raw labeled refined.
|
||||
|
||||
## Security, privacy, and availability
|
||||
|
||||
All frames, model output, geometry, and pre-verification artifacts are
|
||||
untrusted. Calibration/config/model artifacts become trusted only after signed,
|
||||
hash-addressed verification and atomic activation. Runtime inference performs
|
||||
no model retrieval or other network access.
|
||||
|
||||
Fixed arrays/caps, bounded iterations, a maximum track count, room geometry
|
||||
limits, deadlines, and track expiry constrain denial of service. Timeout drops
|
||||
partial refinement, never raw publication. Backpressure retains the newest raw
|
||||
frame per track, drops intermediate refinement work, resets derivatives after
|
||||
250 ms, and never extrapolates beyond 500 ms.
|
||||
|
||||
Metrics contain only allowlisted aggregate scalars: mode/disposition/reason,
|
||||
stage latency, iterations, maximum correction, residuals, confidence delta,
|
||||
track resets, invalid input, timeout, and raw/refined divergence. They exclude
|
||||
joint arrays, body dimensions, room coordinates, CSI, and persistent person
|
||||
identifiers. Bone/gait state is memory-only and excluded from logs.
|
||||
|
||||
Refined output is not a sole medical, emergency, industrial-safety, or
|
||||
autonomous-control source. A real fall is valid state and must never be made
|
||||
upright to stabilize a simulator.
|
||||
|
||||
## Threat model summary
|
||||
|
||||
| Threat | Primary control | Residual risk |
|
||||
|---|---|---|
|
||||
| Spoofed/replayed sensor | ADR-305 identity, MAC, sequence and replay window; correction gate | Compromised legitimate sensor |
|
||||
| Altered model/floor/config | Signed hashes, authenticated configuration, atomic activation | Authorized unsafe configuration |
|
||||
| Poisoned data/splits | Immutable manifests, strict split validator, witnessed benchmarks | Subtle label poisoning |
|
||||
| Operator repudiation | Append-only witnessed transition with actor/old/new hash/reason | Compromised signer |
|
||||
| Biometric/log leakage | Track-local retention and fixed metric allowlist | Aggregate inference |
|
||||
| Track/geometry CPU flood | Authentication, cardinality/geometry/allocation/deadline caps | Valid dense-scene overload |
|
||||
| Remote mode escalation | Capability-scoped local control plane, deny by default | Compromised operator capability |
|
||||
| Derived output relabeled observed | Required schema/provenance and signed event envelope | Malicious downstream stripping |
|
||||
|
||||
The implementation review records commit, lockfile hash, Rust toolchain,
|
||||
scanner versions, and advisory-feed timestamp.
|
||||
|
||||
## Evidence protocol
|
||||
|
||||
Evidence levels are L0 deterministic synthetic, L1 public measured replay, L2
|
||||
controlled RuView RF plus optical truth, L3 subject/room/hardware/session-
|
||||
disjoint RuView, L4 privacy-safe shadow fleet aggregates, and L5 independent
|
||||
vertical validation outside this ADR.
|
||||
|
||||
No sequence, contiguous take, subject, room, or calibration session may cross
|
||||
train/test for the generalization gate. Preprocessing, body priors, and
|
||||
uncertainty calibration fit training data only. Reports include raw observer,
|
||||
renderer smoothing, audit, deterministic correction, dynamics audit, and
|
||||
learned residual on identical observations, plus empty-room, prone/fall,
|
||||
missing-joint, and OOD subsets.
|
||||
|
||||
Primary metrics are 3D MPJPE, declared-threshold PCK, per-joint error, foot
|
||||
slide, floor penetration, jerk, uncertainty calibration, abstention coverage,
|
||||
and selective risk. Learned runs use at least five fixed seeds and report mean,
|
||||
median, standard deviation, and 95% bootstrap intervals. All frames count;
|
||||
selective metrics report risk and coverage.
|
||||
|
||||
## Acceptance gates
|
||||
|
||||
- **G0 contract**: real metric 3D/covariance output, round-trip raw hash,
|
||||
versioned frame/floor, 2D compatibility, non-stub observer, ADR-298 artifact
|
||||
sanity, and the ADR-079 PCK@20 >=35% gate or adopted successor. The current
|
||||
committed Cog does not pass G0, so correction remains unavailable.
|
||||
- **G1 deterministic audit**: property/fuzz tests, deterministic hashes per
|
||||
platform class, 24-hour accelerated replay without panic/growth, Pi 5 p95
|
||||
<=5 ms, and universal confidence monotonicity.
|
||||
- **G2 shadow correction**: strict-disjoint measured median MPJPE improvement
|
||||
>=10% with positive 95% CI lower bound; foot slide >=30% and jerk >=25%
|
||||
better; no joint median >5 mm worse; fall/prone sensitivity change <=2 pp;
|
||||
>=95% corrections below 0.10 m; every correction above 0.20 m abstains.
|
||||
- **G3 opt-in**: >=30 subjects, 10 rooms, 3 hardware configurations, and 3
|
||||
independent sessions/room; UNKNOWN never selected; confidence monotonic;
|
||||
live disable; REST/WebSocket/MQTT/Home Assistant/replay compatibility.
|
||||
- **G4 default visualization only**: 30 shadow days under 0.1% timeout/internal
|
||||
error, no open severity 1/2 incidents, and gates still valid for current
|
||||
model/calibration.
|
||||
|
||||
Dynamics and learned engines each repeat G2-G4; approval is not inherited.
|
||||
|
||||
## Testing and completion evidence
|
||||
|
||||
Unit/property/fuzz/integration/security coverage maps to requirements R1-R13:
|
||||
raw hash, confidence, modes, malformed/stale/frame/covariance input, caps and
|
||||
deadlines, provenance, dependency graph, pose diversity/fall preservation,
|
||||
strict splits, fail-to-raw faults, no network capability, and authenticated
|
||||
source/replay selection.
|
||||
|
||||
Release commands include focused core/physics tests, default/dynamics/learned
|
||||
feature checks, format/clippy, benches, `cargo deny`, `cargo audit`, strict split
|
||||
verification, and golden replay verification. Completion also requires JSON
|
||||
schemas, measured Pi 5/x86 rows, strict manifest hashes, raw/refined metrics,
|
||||
SBOM/license report, rollback drill, and residual-risk owners. Missing measured
|
||||
or operational evidence leaves status Proposed and runtime in audit.
|
||||
|
||||
## Rollback
|
||||
|
||||
Rollback is an authenticated mode transition to audit/off, not a binary
|
||||
downgrade. Stop selection immediately, keep raw publication and disposition
|
||||
records, discard track state, and retain only aggregate incident metrics plus
|
||||
signed configuration history. Failed artifact activation leaves the previous
|
||||
engine atomically active. Additive schemas remain; refined-only callers receive
|
||||
the typed unavailable response.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Explicit anti-hallucination and provenance boundary after RF inference.
|
||||
- Reusable native Rust consistency primitive with measurable abstention.
|
||||
- Python/CUDA remain absent from the production default.
|
||||
- Cross-modal teacher data remains possible without wearable runtime inputs.
|
||||
|
||||
### Negative
|
||||
|
||||
- Full value requires a real metric 3D observer and calibrated uncertainty.
|
||||
- Stateful tracks add latency/memory; optional backends add supply-chain surface.
|
||||
- A constrained but wrong pose can look more credible.
|
||||
- Strict data collection costs more than the software implementation.
|
||||
|
||||
### Neutral
|
||||
|
||||
- This ADR does not improve RF observability or current weight evidence.
|
||||
- Existing 2D consumers continue to function.
|
||||
|
||||
## Implementation phases
|
||||
|
||||
P0 contracts/schemas; P1 deterministic audit; P2 bounded shadow correction; P3
|
||||
server/Cog publication and evidence ledger; P4 Rapier audit; P5 Burn residual
|
||||
training/inference. Code may land ahead of evidence, but runtime authority
|
||||
advances only through the gates above.
|
||||
|
||||
## Implementation status at proposal
|
||||
|
||||
- P0-P3 are implemented on this branch: canonical contracts, strict schemas,
|
||||
deterministic audit/projection, authenticated correction receipts,
|
||||
idempotency, bounded track state, latest-frame backpressure, additive HTTP
|
||||
and WebSocket publication, live legacy-2D audit, privacy-safe metrics, golden
|
||||
replay, and strict-split checks.
|
||||
- P4 is implemented as an optional persistent per-track Rapier dynamics auditor
|
||||
and remains audit-only pending independent G2-G4 evidence.
|
||||
- P5 inference architecture, artifact verification, serialization, and native
|
||||
CPU execution are implemented. Training data, a signed trained artifact, and
|
||||
G2-G4 accuracy/calibration evidence do not exist, so the layer has no runtime
|
||||
selection authority. Its resolved Rust 1.92 requirement is also an explicit
|
||||
activation blocker on the current Rust 1.91.1 release host.
|
||||
- The live Cog honestly emits `Image2d`, degraded trust, and uncalibrated
|
||||
uncertainty. It can be audited but cannot be selected for 3D correction.
|
||||
G0 therefore remains open until an independently released metric-3D observer
|
||||
with calibrated covariance is integrated.
|
||||
- Local x86 latency and synthetic contract checks are recorded in the append-
|
||||
only evidence ledger. Pi 5 measurements, 24-hour replay, 100-million-case
|
||||
fuzzing, held-out RF/optical accuracy, fleet shadowing, and vertical safety
|
||||
validation remain release evidence gates rather than software claims.
|
||||
|
||||
## References
|
||||
|
||||
- [GRIP project](https://ryosukehori.github.io/grip-project/)
|
||||
- [GRIP paper (arXiv:2603.16233)](https://arxiv.org/abs/2603.16233)
|
||||
- [Rapier documentation](https://docs.rs/rapier3d/)
|
||||
- [Burn documentation](https://docs.rs/burn/0.21.0/burn/)
|
||||
- [ADR-020](./ADR-020-rust-ruvector-ai-model-migration.md)
|
||||
- [ADR-079](./ADR-079-camera-ground-truth-training.md)
|
||||
- [ADR-101](./ADR-101-pose-estimation-cog.md)
|
||||
- [ADR-150](./ADR-150-rf-foundation-encoder.md)
|
||||
- [ADR-273](./ADR-273-unified-rf-spatial-world-model.md)
|
||||
- [ADR-279](./ADR-279-native-rf-frame-contract.md)
|
||||
- [ADR-298](./ADR-298-model-release-sanity-gates.md)
|
||||
- [ADR-302](./ADR-302-out-of-distribution-detection.md)
|
||||
- [ADR-303](./ADR-303-ground-truth-synchronization.md)
|
||||
- [ADR-304](./ADR-304-evidence-engine.md)
|
||||
- [ADR-305](./ADR-305-authenticated-sensor-identity.md)
|
||||
- [ADR-306](./ADR-306-canonical-spatial-ontology.md)
|
||||
@@ -0,0 +1,218 @@
|
||||
# ADR-323: RTL8721Dx (Realtek AmebaDplus) CSI Wire Protocol
|
||||
|
||||
- **Status**: accepted
|
||||
- **Date**: 2026-09-15
|
||||
- **Deciders**: RuView maintainers
|
||||
- **Tags**: realtek, ameba, amebadplus, rtl8721dx, csi, protocol, rust, udp
|
||||
|
||||
## Context
|
||||
|
||||
Unlike the MediaTek/Qualcomm CSI paths (ADR-266..269), which are simulator-first
|
||||
because no supported public vendor interface exports complex CSI, Realtek's
|
||||
official Ameba RTOS SDK (`Ameba-AIoT/ameba-rtos`, Apache-2.0) documents and
|
||||
exposes a real, callback-driven CSI API for the RTL8721Dx ("AmebaDplus")
|
||||
family: `wifi_csi_config()` plus the `RTW_EVENT_CSI_DONE` event delivering a
|
||||
`struct rtw_event_csi_report_info` (per-tone signed I/Q, 8-bit or 16-bit
|
||||
precision, selectable tone decimation, passive or active/triggered capture).
|
||||
This has been confirmed directly against SDK source
|
||||
(`component/wifi/api/wifi_api_event.h`) and against the official
|
||||
`example/wifi/wifi_csi` example, and validated on real PKM8721DAF-C13-F10
|
||||
hardware (build+flash+boot confirmed on branch `feat/rtl8721dx-csi-bringup`).
|
||||
|
||||
The RTL8721Dx is a 1x1 WiFi 4 part — single TX/RX chain, not MIMO — so the
|
||||
MediaTek/Qualcomm wire format (Tx/Rx dimensioned matrices, chipset chain
|
||||
counts) does not fit. The vendor SDK's raw struct is also not a safe host
|
||||
wire format as-is: it has no CRC, a C flexible-array-member payload, and
|
||||
fields sized for firmware convenience (e.g. a wrapping `u32` microsecond
|
||||
timestamp) rather than network robustness.
|
||||
|
||||
Per the existing repo convention (ESP32/ADR-018, MediaTek/ADR-266-267,
|
||||
Qualcomm/ADR-268-269, RTL8720F radar/ADR-263-264), each vendor CSI/radar
|
||||
source gets its own self-delimiting envelope and its own Rust module pair
|
||||
(`wifi-densepose-hardware` for framing+codec+simulator,
|
||||
`wifi-densepose-sensing-server` for the bounded JSON-safe snapshot), never a
|
||||
shared "generic vendor CSI" struct that would either lose vendor-specific
|
||||
fields or invite magic-space collisions. `SourceKind::Realtek` (value 2) is
|
||||
already assigned to the RTL8720F FMCW radar path (ADR-263/264) and must not
|
||||
be reused for CSI.
|
||||
|
||||
## Decision
|
||||
|
||||
Define `RAC1` ("Realtek Ameba CSI", version 1) as a little-endian,
|
||||
self-delimiting envelope, magic `0x3143_4152` (distinct from `MTC1`
|
||||
`0x3143544d`, `QCS1` 0x31535143, `RTR1` 0x31525452, and all ADR-018
|
||||
`0xC511000x` siblings):
|
||||
|
||||
Fixed 49-byte header (explicit field-by-field serialization, no `#[repr(C)]`
|
||||
padding, matching the MediaTek/Qualcomm modules' style):
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| magic | u32 LE | `0x3143_4152` |
|
||||
| version | u8 | 1 |
|
||||
| header_len | u16 LE | 49 |
|
||||
| frame_len | u32 LE | header + payload + 4-byte CRC |
|
||||
| node_id | u8 | disambiguates multi-board illuminator/receiver deployments; 0 = unset |
|
||||
| csi_mode | u8 | 0=passive, 1=active, 2=board-to-board (`CsiMode`) |
|
||||
| sequence | u32 LE | from firmware `csi_sequence` |
|
||||
| timestamp_us | u32 LE | from firmware `hw_assigned_timestamp` (hardware-clock microseconds, wraps ~71 min — consumers must not assume monotonic across a capture longer than that) |
|
||||
| peer_mac | [u8; 6] | firmware `mac_addr` |
|
||||
| trig_mac | [u8; 6] | firmware `trig_addr` |
|
||||
| channel | u8 | operating channel |
|
||||
| bandwidth | u8 | 0=20MHz, 1=40MHz |
|
||||
| rx_rate | u8 | firmware `rx_rate` |
|
||||
| protocol_mode | u8 | 0=OFDM,1=HT,2=VHT,3=HE (`ProtocolMode`) |
|
||||
| num_sub_carrier | u16 LE | tone count in payload |
|
||||
| num_bit_per_tone | u8 | 16 (8-bit I + 8-bit Q) or 32 (16-bit I + 16-bit Q) |
|
||||
| decimation | u8 | tone group (1/2/4/8 — every Nth tone) |
|
||||
| rssi_dbm | i8 | single RF chain (1x1 hardware) |
|
||||
| rxsc | u8 | sub-20 MHz channel used |
|
||||
| csi_valid | u8 | 0/1 from firmware |
|
||||
| flags | u8 | bit 0 = SYNTHETIC (simulator provenance, never clearable downstream) |
|
||||
| payload_len | u32 LE | bytes, == `num_sub_carrier * (num_bit_per_tone / 8)` |
|
||||
|
||||
Payload: `num_sub_carrier` consecutive (I, Q) tone pairs, each
|
||||
`num_bit_per_tone / 16` bytes per component (1 byte/component when
|
||||
`num_bit_per_tone == 16`, 2 bytes/component little-endian signed when `== 32`)
|
||||
— i.e. exactly the byte layout the firmware already produces in
|
||||
`csi_data[]`, just framed. CRC-32/IEEE over header+payload, final 4
|
||||
little-endian bytes. One envelope per UDP datagram.
|
||||
|
||||
Parsers reject: unknown version, wrong `header_len`, `frame_len` exceeding
|
||||
the IPv4 UDP payload cap (65,507 bytes), `num_bit_per_tone` outside
|
||||
`{16, 32}`, `payload_len` inconsistent with `num_sub_carrier *
|
||||
(num_bit_per_tone/8)`, trailing bytes, and bad CRC — mirroring the
|
||||
MediaTek/Qualcomm parser contract exactly.
|
||||
|
||||
Host side: add `realtek_csi.rs` to both
|
||||
`wifi-densepose-hardware` (frame + codec + simulator, mirroring
|
||||
`mediatek_csi.rs`/`qualcomm_csi.rs`) and `wifi-densepose-sensing-server`
|
||||
(bounded `RealtekCsiSnapshot`, mirroring `QualcommCsiSnapshot`), wired into
|
||||
the same UDP listener loop and `/api/v1/csi/realtek/latest` route as the
|
||||
existing vendor sources. The initial Mac compatibility port keeps Realtek CSI
|
||||
as a separate bounded diagnostic stream. Fresh ESP32 frames remain the primary
|
||||
sensing source. This change does not add `SourceKind::RealtekCsi` to a signed
|
||||
ingest contract; that requires a separate reviewed identity-bound hardware
|
||||
proof. RAC1 CRC checks corruption, not origin.
|
||||
|
||||
Firmware side (`example/wifi/wifi_csi` derivative, built against the
|
||||
official Ameba RTOS SDK, tracked outside this repository per the existing
|
||||
`CLAUDE.local.md` convention — the vendor SDK itself is never vendored into
|
||||
this repo): join the board to the deployment's WiFi AP in STA mode
|
||||
(passive CSI against the real AP as peer, since only one physical board is
|
||||
available in this bring-up), read the AP's BSSID at runtime after
|
||||
association (never hardcode a MAC into firmware source), pack each
|
||||
`RTW_EVENT_CSI_DONE` report into one `RAC1` datagram, and `sendto()` it over
|
||||
UDP to the host sensing-server's existing CSI port.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- CSI is real, MEASURED hardware output from day one — no simulator-first
|
||||
bridge phase is needed the way MediaTek/Qualcomm required, because the
|
||||
vendor SDK genuinely exposes the capability and we have the board in hand.
|
||||
- One board is sufficient for a first useful signal (passive CSI against the
|
||||
home AP); active and board-to-board modes are additive later without a
|
||||
wire-format break (`csi_mode` already reserves the values).
|
||||
- Reuses the bounded UDP parser and status-route pattern without changing
|
||||
existing ESP32 sensing. Unsigned RAC1 diagnostics are not authorized as
|
||||
calibrated presence, person count, pose, or vital evidence.
|
||||
|
||||
### Negative
|
||||
|
||||
- 1x1 hardware means no spatial (multi-antenna) diversity from a single
|
||||
board; multi-board deployment is required for the differential/localization
|
||||
use cases described in the original RTL8721Dx capability analysis.
|
||||
- The 71-minute microsecond timestamp wraparound (inherited from the
|
||||
firmware's `u32` field) means long unattended captures need host-side
|
||||
sequence-based reordering, not raw timestamp arithmetic, across a wrap.
|
||||
|
||||
### Neutral
|
||||
|
||||
- The RAC1 envelope intentionally does not carry the SDK's raw
|
||||
`csi_signature` (0xABCD) marker — our own `magic`/CRC supersede it as the
|
||||
framing integrity check.
|
||||
|
||||
## Hardware gates
|
||||
|
||||
- [x] Confirm SDK source exposes `RTW_EVENT_CSI_DONE` / `rtw_event_csi_report_info` (done — `component/wifi/api/wifi_api_event.h`).
|
||||
- [x] Build and flash a baseline image on real PKM8721DAF-C13-F10 hardware, confirm boot over serial (done — `feat/rtl8721dx-csi-bringup`).
|
||||
- [x] Flash the CSI-streaming firmware and confirm `RAC1` datagrams parse on the host from the real board (done, 2026-09-15 — see below).
|
||||
- [x] Record MEASURED throughput/latency/jitter from the real running firmware (done, 2026-09-15 — see below).
|
||||
- [ ] A human walk-test across the link (amplitude perturbation vs. static baseline) is deferred — requires the operator physically present; not claimed until performed.
|
||||
|
||||
### MEASURED: real RAC1 capture and sustained throughput (2026-09-15)
|
||||
|
||||
Passive CSI (`RTW_CSI_MODE_NORMAL`, `RTW_CSI_TRIG_BEACON | RTW_CSI_TRIG_DATA | RTW_CSI_TRIG_QOS_DATA`)
|
||||
against the deployment AP. A single `RAC1` datagram was first
|
||||
cross-checked field-by-field against the firmware's own serial log of the
|
||||
same report — every byte matched: magic `0x31434152`, RSSI byte exact match,
|
||||
channel/bandwidth/protocol/subcarrier-count exact match, 157-byte total frame
|
||||
(49-byte header + 104-byte payload + 4-byte CRC, exactly as specified above).
|
||||
|
||||
`RTW_CSI_MODE_RX_RESP` (estimate CSI from ACKs to the STA's own transmissions)
|
||||
never fired `RTW_EVENT_CSI_DONE` across two full test runs with confirmed
|
||||
real traffic (not a hang) — this mode does not work for this hardware/AP
|
||||
combination and `RTW_CSI_MODE_NORMAL` with beacon+data triggers is the
|
||||
working configuration.
|
||||
|
||||
**An earlier note here claimed `NORMAL` mode fires only once per CSI enable
|
||||
— that was wrong**, caused by a debug log line gated to print only every
|
||||
50th frame (`frame_count % 50 == 1`), which was misread as "only one report
|
||||
total." A follow-up capture with a UDP listener running concurrently with a
|
||||
board reset measured **277 real RAC1 datagrams in 40 seconds (43,489 bytes)**,
|
||||
with `csi_sequence` climbing continuously and monotonically in the firmware's
|
||||
own serial log across six separate 50-frame log checkpoints in one 45s
|
||||
session — CSI reporting is continuous, roughly 7-12 fps, not sparse.
|
||||
|
||||
### MEASURED: throughput optimization pass (2026-09-15)
|
||||
|
||||
Formalized as a repeatable tool: `scripts/benchmark-rtl8721dx-csi.py` (listens
|
||||
on the sensing-server's CSI UDP port, reports fps/throughput/RSSI/inter-frame
|
||||
interval/sequence-gap stats from real received `RAC1` datagrams).
|
||||
|
||||
**Baseline** (`trig_period=200` [64ms], `trig_frame_ctrl=0`,
|
||||
`CSI_REPORT_BUF_NUM=1`), 30s capture:
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| RAC1 fps | 14.67 |
|
||||
| Throughput | 2,302.7 bytes/s |
|
||||
| Mean inter-frame interval | 68.3 ms |
|
||||
| RSSI | mean -34.4 dBm |
|
||||
|
||||
`trig_period` (units of 320µs) was hypothesized as the rate-limiting
|
||||
throttle — the observed 68.3ms mean interval tracks the 64ms configured
|
||||
period almost exactly. Confirmed by tuning:
|
||||
|
||||
1. `trig_period`: 200 → 15 (the SDK doc's stated minimum recommended value,
|
||||
~4.8ms)
|
||||
2. `trig_frame_ctrl`: 0 → `RTW_CSI_TRIG_ACK` (second trigger source: every
|
||||
link-layer ACK the AP sends for the heartbeat's own outgoing traffic, on
|
||||
top of the existing beacon trigger)
|
||||
3. `CSI_REPORT_BUF_NUM`: 1 → 4 (a "lack of csi buf!" drop was observed even
|
||||
at the slower baseline rate; more headroom needed at a higher rate)
|
||||
|
||||
**Tuned**, 30s capture:
|
||||
|
||||
| Metric | Value | vs. baseline |
|
||||
|---|---|---|
|
||||
| RAC1 fps | 66.40 | **4.5x** |
|
||||
| Throughput | 10,424.8 bytes/s | **4.5x** |
|
||||
| Mean inter-frame interval | 14.9 ms | 4.6x lower |
|
||||
| Sequence gaps | 78 / ~2036 (~3.8%) | new (0 observed at the slower baseline rate) |
|
||||
| RSSI | mean -31.1 dBm | (different session/position, not a controlled variable here) |
|
||||
|
||||
The `trig_period` hypothesis is confirmed as the primary lever. The ~3.8%
|
||||
frame-loss rate at 66 fps is a reasonable place to stop for now — it is
|
||||
UDP-over-WiFi loss, not a firmware defect, and is small enough not to block
|
||||
downstream sensing use; revisit only if a use case needs stronger delivery
|
||||
guarantees (sequence-gap-triggered re-request, TCP fallback, etc.).
|
||||
|
||||
## Links
|
||||
|
||||
- [ADR-266](ADR-266-mediatek-filogic-csi-platform.md) / [ADR-267](ADR-267-mediatek-mimo-csi-wire-protocol.md) — sibling vendor CSI ADR pair (simulator-first precedent)
|
||||
- [ADR-268](ADR-268-qualcomm-atheros-csi-platform.md) / [ADR-269](ADR-269-qualcomm-csi-wire-protocol.md) — sibling vendor CSI ADR pair
|
||||
- [ADR-263](ADR-263-rtl8720f-2-4ghz-fmcw-radar-platform.md) / [ADR-264](ADR-264-rtl8720f-radar-wire-protocol.md) — existing Realtek (RTL8720F radar) path; `SourceKind::Realtek` owner
|
||||
- `CLAUDE.local.md` — RTL8721Dx toolchain setup, board/COM port, first bring-up log
|
||||
@@ -0,0 +1,276 @@
|
||||
# ADR-324: off-axis-mode — RF-assisted head-coupled perspective for the three.js realtime demo
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed (core implemented — see §2.5) |
|
||||
| **Date** | 2026-08-16 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **off-axis-mode** |
|
||||
| **Scope** | New `examples/three.js/demos/07-off-axis-window.html` (client-side only); no server changes |
|
||||
| **Relates to** | ADR-019 (sensing-only UI), ADR-035 (live sensing UI accuracy), ADR-169 (adam-mode), ADR-170 (yoga-mode), ADR-282 (L0–L5 evidence ladder), ADR-295 (source provenance), ADR-306 (spatial ontology), ADR-307 (persistent tracking), ADR-323 (pose refinement) |
|
||||
| **Prior art** | [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker) (reference only — see §2.1 licensing) |
|
||||
| **Numbering note** | ADR-324 is the next free number in the authoring checkout (322 is unused, 323 is the latest on disk). Re-run the ADR index/collision check immediately before merge and rename if needed. |
|
||||
| **Tracking issue** | none yet |
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
### 1.1 The question this ADR answers
|
||||
|
||||
"Can we use [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker)
|
||||
with RuView?" The answer is: **yes for the technique, no for the code, and
|
||||
only honestly for the RF part.** This ADR records the research behind each of
|
||||
those three clauses and defines the integration that is actually defensible.
|
||||
|
||||
### 1.2 What off-axis-sneaker is
|
||||
|
||||
`off-axis-sneaker` is a React + TypeScript + Vite web app that renders a GLB
|
||||
model (a sneaker) in three.js and creates a *head-coupled perspective*
|
||||
("fish-tank VR" / "window into the screen") illusion:
|
||||
|
||||
- **Tracking input**: MediaPipe Face Mesh (468 facial landmarks) from a
|
||||
webcam. Head (x, y) comes from the eye midpoint; depth (z) is proxied by
|
||||
inter-ocular distance. An exponential moving average (default factor 0.3)
|
||||
smooths jitter; sensitivity multipliers are `strengthX: 4`, `strengthY: 3`,
|
||||
`strengthZ: 2`.
|
||||
- **Projection**: `src/utils/offAxisCamera.ts` builds a **true asymmetric
|
||||
(off-axis) frustum** — `makePerspective(left, right, top, bottom, near, far)`
|
||||
with `left/right/top/bottom = (screenBound − eyePosition) · (near /
|
||||
viewerToScreenDistance)` — i.e. Kooima's generalized perspective projection,
|
||||
plus a matching camera translation. Constants: `nearPlane 0.05`,
|
||||
`farPlane 1000`, `worldScale 0.01` (cm → world units), `movementScale 1.5`.
|
||||
- **Calibration**: a wizard captures physical screen width/height (cm),
|
||||
typical viewing distance, and pixel density, stored locally, so eye position
|
||||
is computed relative to the *physical* display.
|
||||
|
||||
The technique descends from Johnny Chung Lee's 2007 Wii-remote desktop VR
|
||||
demo and the fish-tank VR literature (Ware, Arthur & Booth, CHI '93). The
|
||||
projection math is Robert Kooima's "Generalized Perspective Projection"
|
||||
(2008). Both are public, well-documented techniques independent of any one
|
||||
implementation.
|
||||
|
||||
### 1.3 What the illusion physically requires
|
||||
|
||||
The head-coupled illusion is only convincing when the tracked eye position is
|
||||
**accurate to roughly centimeters** and **low-latency**. The VR literature
|
||||
puts comfortable motion-to-photon latency below ~20 ms for head-mounted
|
||||
displays; desktop fish-tank VR tolerates more, but visible lag between head
|
||||
motion and parallax response is exactly what breaks the "window" illusion.
|
||||
`CLAIMED` (literature values; no RuView measurement exists for this demo yet).
|
||||
|
||||
### 1.4 What RuView RF sensing can actually supply today
|
||||
|
||||
This is where honesty is mandatory (repo rule: never present WiFi sensing as
|
||||
camera-grade).
|
||||
|
||||
- **Field-peak position, not metric localization.**
|
||||
`wifi-densepose-sensing-server/src/field_localize.rs` derives a position
|
||||
from the strongest peak of the 20×20 `signal_field` carried on
|
||||
`/ws/sensing` `sensing_update` frames. Its own module doc states the
|
||||
caveat: the subcarrier→angle mapping is a *representation*; "a single ESP32
|
||||
link cannot resolve a true (x, z) room position." The emitted position is
|
||||
"strongest field peak in the room model," mapped with `X_SCALE 0.6`,
|
||||
`Z_SCALE 0.5`, gated by `PEAK_THRESHOLD 0.35` — real, live, motion-tracking,
|
||||
but **not a calibrated person fix** and nowhere near eye-position precision.
|
||||
- **RF pose is 2-D, normalized, constant-confidence.** The committed Cog
|
||||
(ADR-101, restated by ADR-323) emits 17 COCO keypoints as normalized 2-D
|
||||
coordinates with a constant confidence and no per-joint uncertainty. A
|
||||
"nose" keypoint exists (COCO index 0), but it is not a metric 3-D head fix.
|
||||
- **Tracks are coarse and pseudonymous by design.** `ruview-track` (ADR-307)
|
||||
maintains `person_N` tracks with container-level ("kitchen → hallway")
|
||||
continuity, coarse non-reversible features, and asserts **no accuracy
|
||||
number** — outputs default to evidence level `L1`.
|
||||
- **Update cadence and latency are unmeasured for this purpose.** The demo
|
||||
pipeline runs at ~30 Hz on the MediaPipe side (ADR-170), but no end-to-end
|
||||
RF motion-to-photon latency has been measured. Any figure quoted for the RF
|
||||
path must be tagged `MEASURED` with a reproducer before it appears in docs
|
||||
or UI.
|
||||
|
||||
Conclusion of the capability match: **RF cannot drive a convincing fish-tank
|
||||
illusion by itself today**, and this ADR does not claim it can. RF *can*
|
||||
supply things a webcam cannot: camera-free presence, zone-level position,
|
||||
person count, approach direction, and pseudonymous continuity — including
|
||||
when the camera is off.
|
||||
|
||||
### 1.5 What this ADR is *not*
|
||||
|
||||
- Not a vendoring of `off-axis-sneaker` (see §2.1 — the repo has no license).
|
||||
- Not a claim of camera-grade RF head tracking, at any tier.
|
||||
- Not a backend change: no new server endpoints, no new auth surface, no
|
||||
schema changes. Purely additive client-side HTML/JS, like ADR-169/170.
|
||||
- Not a React/Vite/Tailwind adoption. The `examples/three.js/demos/*` are
|
||||
dependency-light single-file HTML demos and stay that way.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
### 2.1 Licensing: adopt the technique, not the code
|
||||
|
||||
`off-axis-sneaker` publishes **no license**. Under default copyright, its
|
||||
source cannot be copied, vendored, or translated into this repository.
|
||||
Decision:
|
||||
|
||||
1. **No code, assets, or models from `off-axis-sneaker` enter this repo.**
|
||||
The GLB sneaker model is likewise unlicensed for reuse; demos use assets
|
||||
already present in `examples/`.
|
||||
2. The off-axis projection is implemented **clean-room from the public
|
||||
sources**: Kooima's "Generalized Perspective Projection" (2008) — the
|
||||
`pa/pb/pc` screen-corner formulation — and three.js's documented
|
||||
`PerspectiveCamera.projectionMatrix` override path. The repository is cited
|
||||
as prior art in this ADR only.
|
||||
3. If upstream later adds a permissive license, revisiting reuse requires a
|
||||
new ADR note, not silent copying.
|
||||
|
||||
### 2.2 Tiered integration — each tier labeled by what it really is
|
||||
|
||||
**Tier A (ships first): webcam-fine + RF-context hybrid.**
|
||||
`07-off-axis-window.html` uses MediaPipe Face Landmarker (already the pattern
|
||||
in demo 05) for fine head tracking and the Kooima frustum for rendering —
|
||||
functionally what off-axis-sneaker does, reimplemented. RuView RF adds the
|
||||
camera-free layer around it:
|
||||
|
||||
- **Presence-gated camera**: the webcam pipeline starts only when the RF
|
||||
presence signal (`/ws/sensing` `sensing_update`) says someone is in the
|
||||
zone, and stops after a configurable RF-vacancy timeout. The privacy
|
||||
posture improves: the camera is *off* until physics says there is someone
|
||||
to track.
|
||||
- **Multi-person arbitration**: when RF reports more than one person, the HUD
|
||||
says so and the demo holds the last stable perspective instead of jumping
|
||||
between faces.
|
||||
- **Pre-warm**: RF approach direction (field-peak trajectory) warms up
|
||||
MediaPipe and the scene before the person sits down.
|
||||
|
||||
**Tier B (demo mode, prominently labeled): RF-only coarse parallax.**
|
||||
A toggle drives the off-axis eye position from RF alone — field peak (x, z)
|
||||
plus the pose nose keypoint when present — through a one-euro filter, a
|
||||
deadband, and a hard gain clamp. The HUD labels it **"RF coarse body
|
||||
parallax — not head tracking"** and shows the live evidence level (`L1`
|
||||
heuristic unless a certificate says otherwise, per ADR-282/ADR-318). The
|
||||
expected experience is a slow, body-scale parallax sway — a demonstrative
|
||||
"the room model moves because *you* moved, with no camera" — not a stable
|
||||
fish-tank illusion. The demo must never present Tier B as equivalent to
|
||||
Tier A.
|
||||
|
||||
**Tier C (future, explicitly gated, not promised): metric RF head position.**
|
||||
Only a calibrated multistatic deployment (ADR-297 multi-node semantics,
|
||||
ADR-311 fusion, ADR-303 ground-truth sync) with an evidence-engine ledger
|
||||
entry (ADR-304) and a capability certificate (ADR-318) could justify feeding
|
||||
RF positions into the fine path. No current data supports this; Tier C exists
|
||||
in this ADR solely so nobody ships it informally without those gates.
|
||||
|
||||
### 2.3 Implementation surface
|
||||
|
||||
- New file `examples/three.js/demos/07-off-axis-window.html` (07, not 06 —
|
||||
ADR-170 reserves `06-yoga-mode.html`). Single-file demo following the 01–05
|
||||
conventions: same CSS custom properties, same HUD/helper-panel pattern,
|
||||
served from the existing static demo server
|
||||
(`http://127.0.0.1:8765/examples/three.js/demos/…`).
|
||||
- A small clean-room module (inline `<script type="module">` or
|
||||
`examples/three.js/lib/off-axis-camera.js` if shared later) that, given
|
||||
screen corners `pa, pb, pc` (from calibration) and eye point `pe`, sets
|
||||
`camera.projectionMatrix` via the Kooima formulation each frame.
|
||||
- Data inputs are the **existing** streams only: `/ws/sensing`
|
||||
(`sensing_update` → `signal_field` → field peak, using the same
|
||||
`X_SCALE`/`Z_SCALE`/`PEAK_THRESHOLD` mapping as `field_localize.rs`) and,
|
||||
when available, `/api/v1/stream/pose` for the nose keypoint. WebSocket
|
||||
access uses the existing ticket flow (`ws_ticket.rs` / `bearer_auth.rs`);
|
||||
no endpoint is exempted or added.
|
||||
- Calibration mirrors the sneaker app's concept without its code: screen
|
||||
width/height in cm, viewing distance, persisted in `localStorage` under a
|
||||
demo-scoped key. No calibration data leaves the browser.
|
||||
- Provenance discipline: if the demo is pointed at a synthetic or replayed
|
||||
source, the ADR-295 provenance state must surface in the HUD exactly as the
|
||||
Observatory does — synthetic can never present as live.
|
||||
|
||||
### 2.4 Honesty and evidence rules binding this feature
|
||||
|
||||
1. Every user-visible latency, accuracy, or precision statement in the demo,
|
||||
README, or docs carries a `MEASURED` (with reproducer), `CLAIMED`, or
|
||||
`SYNTHETIC` tag. This ADR itself contains no `MEASURED` claims.
|
||||
2. Tier B is labeled coarse body parallax in the HUD at all times; there is
|
||||
no configuration that hides the label while RF drives the camera.
|
||||
3. No PCK or pose-accuracy number may be quoted for the RF path without the
|
||||
mean-pose baseline and a leakage-free held-out split (repo rule).
|
||||
4. The webcam feed never leaves the browser; no frames, landmarks, or
|
||||
embeddings are sent to the server. RF data continues to obey ADR-307's
|
||||
privacy invariants (pseudonymous, coarse, rotatable).
|
||||
|
||||
### 2.5 Implementation status (2026-08-16 amendment)
|
||||
|
||||
The projection core shipped as a **Rust crate compiled to WASM** rather than
|
||||
the inline JS module §2.3 anticipated — a strict upgrade with the same
|
||||
surface: `v2/crates/ruview-offaxis` (dependency-free native core; wasm-bindgen
|
||||
only on wasm32) implements the Kooima projection, the one-euro filter, the
|
||||
field-peak mapping (constants mirroring `field_localize.rs`), and the Tier B
|
||||
coarse-parallax stage with its deadband/gain/clamp bounds enforced in Rust.
|
||||
`examples/three.js/demos/07-off-axis-window.html` consumes the wasm-bindgen
|
||||
output (built locally per the crate README; generated artifacts are not
|
||||
committed). Validation and `MEASURED` benchmarks live in the crate README.
|
||||
The demo ships with a `SYNTHETIC`-labeled mouse simulator and the labeled
|
||||
Tier B RF mode; a Tier A fine tracker connects through
|
||||
`OffAxisCamera.update_normalized` and remains host-provided.
|
||||
|
||||
## 3. Options considered
|
||||
|
||||
| Option | Verdict | Why |
|
||||
|---|---|---|
|
||||
| Vendor `off-axis-sneaker` (or fork + point at RuView) | **Rejected** | No license ⇒ no redistribution rights. Also React/Vite stack conflicts with the repo's single-file demo convention. |
|
||||
| Clean-room Kooima off-axis demo, webcam-fine + RF-context (Tier A/B) | **Chosen** | Legally clean, matches demo conventions, uses RF for what it is actually good at, and demonstrates camera-free presence value honestly. |
|
||||
| RF-only head-coupled perspective as the headline | **Rejected** | Over-claim. Single-link field peaks are a representation, not metric localization (`field_localize.rs` caveat); shipping this as "head tracking" violates the camera-grade rule. Survives only as the labeled Tier B toggle. |
|
||||
| Wait for multistatic metric localization (Tier C) before any demo | **Rejected** | Blocks a useful, honest demo on a phase-2/3 program (ADR-303/311/318) with no delivery date. The gates are recorded instead. |
|
||||
| Add a dedicated server endpoint for head position | **Rejected** | Unnecessary — existing `/ws/sensing` + `/api/v1/stream/pose` suffice; a new endpoint would expand the auth surface for no capability gain. |
|
||||
|
||||
## 4. Consequences
|
||||
|
||||
**Improves**
|
||||
|
||||
- A publicly legible demo of RF sensing's actual differentiator: the scene
|
||||
knows you are there, where you roughly are, and how many of you there are —
|
||||
before and without any camera.
|
||||
- Privacy posture of the head-tracking demo class: camera duty-cycle is
|
||||
bounded by RF presence instead of always-on.
|
||||
- Canonical, licensed off-axis projection code the Observatory or future UI
|
||||
can reuse.
|
||||
|
||||
**Costs / risks**
|
||||
|
||||
- Tier B can underwhelm viewers primed by webcam demos; the mitigation is the
|
||||
labeling and the side-by-side toggle, not inflated gain.
|
||||
- MediaPipe CDN dependency (same as demo 05) remains a network-availability
|
||||
risk for Tier A; the demo must degrade to Tier B with a visible notice.
|
||||
- Screen-calibration friction (cm measurements) may deter casual users; a
|
||||
"skip calibration (approximate)" path with degraded-accuracy labeling is
|
||||
acceptable.
|
||||
- Upstream `off-axis-sneaker` may change or add a license; tracking that is
|
||||
manual.
|
||||
|
||||
**Follow-ups (not in this ADR's scope)**
|
||||
|
||||
- Measure end-to-end RF motion-to-parallax latency with a reproducer and
|
||||
publish it `MEASURED`.
|
||||
- If/when ADR-303/311 land, evaluate Tier C against the ADR-318 certificate
|
||||
gate.
|
||||
- Consider promoting the off-axis camera module into the Observatory 3D view.
|
||||
|
||||
## 5. Validation
|
||||
|
||||
- Demo checklist (manual, per ADR-169/170 practice): loads from the static
|
||||
server; Tier A activates only on RF presence; Tier B label visible whenever
|
||||
RF drives the camera; provenance badge correct against a synthetic source;
|
||||
no network requests carrying webcam-derived data (verified in devtools).
|
||||
- `rg` gate before merge: no file under `examples/` contains code originating
|
||||
from `icurtis1/off-axis-sneaker`.
|
||||
- No workspace, harness, or firmware validation rows are triggered — the
|
||||
change is a static HTML demo plus this document.
|
||||
|
||||
## 6. References
|
||||
|
||||
- [`icurtis1/off-axis-sneaker`](https://github.com/icurtis1/off-axis-sneaker) — prior-art reference (unlicensed; technique only)
|
||||
- Robert Kooima, *Generalized Perspective Projection*, 2008 — off-axis frustum math
|
||||
- Johnny Chung Lee, *Head Tracking for Desktop VR Displays using the Wii Remote*, 2007
|
||||
- Ware, Arthur & Booth, *Fish Tank Virtual Reality*, CHI '93 — head coupling vs. stereo
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/field_localize.rs` — field-peak honesty caveat and coordinate mapping
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/ws_ticket.rs`, `bearer_auth.rs` — WebSocket auth pattern
|
||||
- `v2/crates/ruview-track/src/lib.rs` — ADR-307 privacy invariants and evidence discipline
|
||||
- ADR-169, ADR-170 — demo-scoped ADR pattern for `examples/three.js/demos/`
|
||||
- ADR-282 — L0–L5 evidence ladder; ADR-295 — provenance state machine
|
||||
@@ -0,0 +1,543 @@
|
||||
# ADR-325: Cognitum Spaces activation and governed spatial exchange
|
||||
|
||||
- **Status**: Accepted — legacy and versioned reads, OAuth activation, local spatial memory, governed-action policy, metaharness support, and npm distribution are implemented; HTTPS production evidence is complete
|
||||
- **Date**: 2026-08-17
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: cognitum-spaces, oauth, spatial-state, privacy, ruvector, policy, autogenous
|
||||
- **Relates to**: ADR-271, ADR-277, ADR-304, ADR-306, ADR-312, ADR-318, ADR-319, ADR-321; Cognitum API ADR-094; Autogenous ADR-402
|
||||
|
||||
## Context
|
||||
|
||||
RuView produces camera-free RF perception locally. Cognitum Spaces provides a
|
||||
tenant-scoped cloud projection of physical places. Autogenous ADR-402 proposes
|
||||
using that projection as a spatial-intelligence input for agent coordination.
|
||||
The useful product is not another sensor dashboard: it is a governed chain from
|
||||
local perception to spatial state, persistent memory, explanation, and action.
|
||||
|
||||
Four product pillars define the requested integration:
|
||||
|
||||
1. **Spatial state** — sites, buildings, floors, rooms/spaces, zones, entities,
|
||||
semantic events, and alerts.
|
||||
2. **RuView perception** — camera-free sensing is normalized locally before any
|
||||
permitted P2/P3 semantic event synchronizes.
|
||||
3. **Persistent memory** — RuVector grounds anomaly explanations in
|
||||
tenant-scoped spatial history.
|
||||
4. **Governed action** — agents observe or recommend by default; consequential
|
||||
execution requires explicit policy authorization.
|
||||
|
||||
The live API audit on 2026-08-17 established the current production boundary:
|
||||
|
||||
- `GET https://api.cognitum.one/v1/spaces` exists and returns a bounded list;
|
||||
- an unauthenticated request is rejected;
|
||||
- the current account has no paired sites, so the authenticated result is an
|
||||
empty list rather than fabricated sample state;
|
||||
- the projection declares HomeCore Edge authoritative and excludes raw CSI,
|
||||
CIR, RF tensors, recordings, pose frames, vital waveforms, and identity
|
||||
observations;
|
||||
- the first deployed Function revision accepted only legacy `cog_` API keys;
|
||||
- the gateway was configured to authenticate private Function hops, but the
|
||||
direct Function endpoint was still publicly invokable; that bypass has now
|
||||
been closed and the exact gateway runtime service account is the only
|
||||
invoker;
|
||||
- OAuth protected-resource metadata and a RuView-scoped OAuth accept path were
|
||||
absent.
|
||||
|
||||
The Autogenous review at commit
|
||||
`f7fa308b261bac89a8909edae8a3fdbbfb8ce66c` found additional integration risks:
|
||||
|
||||
- its Spaces client only listed spaces; no governed ingest contract existed;
|
||||
- it trusted a loose TypeScript cast, with no response-size, timeout, redirect,
|
||||
or strict semantic-boundary validation;
|
||||
- its observation conversion dropped tenant/message/sequence identity;
|
||||
- missing confidence became zero but could still enter fusion;
|
||||
- provenance could be substituted for calibration identity;
|
||||
- a Spaces-derived belief could be converted back into an observation and
|
||||
counted as independent corroboration, laundering one source into two;
|
||||
- its API-key exchange returns a `cognitum-cli` OAuth token, but the live Spaces
|
||||
endpoint accepted only a `cog_` key. Calling this “OAuth Spaces access” was a
|
||||
contract mismatch.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a one-way-by-default, typed spatial exchange with separate activation,
|
||||
data, memory, and action authorities.
|
||||
|
||||
```text
|
||||
RuView RF capture (P0/P1, local)
|
||||
-> calibrated/OOD-gated semantic observation
|
||||
-> ontology + evidence + witness envelope (P2/P3)
|
||||
-> HomeCore authoritative edge state
|
||||
-> Cognitum Spaces tenant/workspace projection
|
||||
-> RuView bounded read client / Autogenous spatial context
|
||||
-> RuVector tenant-scoped memory and explanation
|
||||
-> recommendation
|
||||
-> ruvview-policy authorization + approval + receipt
|
||||
-> optional consequential action
|
||||
```
|
||||
|
||||
Cloud state is a projection of edge state, not a second sensor and not an
|
||||
independent corroborating modality.
|
||||
|
||||
### 1. Activation and data-plane credentials are distinct
|
||||
|
||||
RuView uses Cognitum's existing Authorization Code + PKCE flow with the public
|
||||
`ruview` client. A user explicitly requests `spaces:read` with
|
||||
`wifi-densepose login --spaces`. The authorization-server registration is a
|
||||
ceiling; ordinary sensing login does not silently gain cloud access.
|
||||
|
||||
The Spaces resource server accepts either:
|
||||
|
||||
- a legacy API key carrying `spaces:read` (or the migration-compatible
|
||||
predecessor `devices:manage`); or
|
||||
- a Cognitum OAuth access token that passes every condition below.
|
||||
|
||||
OAuth acceptance is conjunctive:
|
||||
|
||||
| Check | Required value |
|
||||
|---|---|
|
||||
| Signature | ES256 against `https://auth.cognitum.one/.well-known/jwks.json` |
|
||||
| Issuer | exact `https://auth.cognitum.one` |
|
||||
| Audience | exact `ruview` |
|
||||
| Client claim | exact `ruview` |
|
||||
| Token type | ordinary `access`; setup/workload tokens denied |
|
||||
| Lifetime | current `exp`/`nbf`, five-second clock tolerance only |
|
||||
| Scope | exact token `spaces:read` member |
|
||||
| Tenant binding | valid non-empty UUID `org_id` and `workspace_id` |
|
||||
|
||||
An API key is not called OAuth. An OAuth token is not stored in
|
||||
`COGNITUM_SPACES_API`. The compatibility environment variable contains an API
|
||||
key only and is never printed, logged, or committed.
|
||||
|
||||
OAuth consent grants identity-bound read access. It does **not** grant device
|
||||
pairing, data publication, deployment, billing, spending, leases, learning
|
||||
promotion, automation installation, commands, or actuator authority.
|
||||
|
||||
The contributor metaharness exposes this as CLI verb `spaces` and MCP tool
|
||||
`ruview_spaces_list`. It delegates to the same Rust client rather than parsing
|
||||
or refreshing OAuth independently. The tool never accepts a bearer token or API
|
||||
key. MCP use requires an operator-provided `credential-use` grant, and MCP calls
|
||||
cannot select the credential path or API origin. The adapter requires an
|
||||
installed `wifi-densepose` binary rather than executing Cargo build scripts
|
||||
from an auto-detected checkout while holding credential authority. Because
|
||||
refresh tokens rotate, a read may atomically update the local OAuth credential
|
||||
before contacting Spaces; this authentication side effect is disclosed and
|
||||
does not add cloud write authority.
|
||||
|
||||
### 2. The gateway owns the private credential relay
|
||||
|
||||
The public gateway strips inbound `X-Cognitum-User-Authorization` and
|
||||
`X-Serverless-Authorization`. For a locked Function upstream it then:
|
||||
|
||||
1. retains a legacy `cog_` credential in `X-API-Key`, or, for the exact Spaces
|
||||
route only, retains a non-key bearer in a gateway-owned internal header;
|
||||
2. replaces `Authorization` with the gateway's Google invoker ID token;
|
||||
3. fails closed with `503` if it cannot mint that hop identity;
|
||||
4. forwards only to the configured Function origin.
|
||||
|
||||
The Function's Cloud Run invoker check is enabled. `allUsers` has no invoker
|
||||
binding; only the exact `apigateway-sa` service account may invoke it. This is
|
||||
required because otherwise a caller could bypass Cloud Armor and spoof an
|
||||
internal relay header.
|
||||
|
||||
The API publishes RFC 9728 protected-resource metadata naming the authorization
|
||||
server and `spaces:read` scope. Discovery describes capability; it does not
|
||||
grant it.
|
||||
|
||||
### 3. Tenant isolation is part of authentication
|
||||
|
||||
Legacy API-key documents are queried by their existing owner-bound `tenantId`.
|
||||
OAuth requests are conjunctively queried by both signed `org_id` and
|
||||
`workspace_id` using stored `tenantId` and `workspaceId` fields. The public
|
||||
tenant identifier is projected from signed `org_id`. A request cannot supply
|
||||
either selector in a query string.
|
||||
|
||||
No cross-tenant aggregation exists on this path. Pagination, search, memory,
|
||||
and event endpoints added later must carry the same authoritative principal;
|
||||
client-provided tenant filters may only narrow within it, never replace it.
|
||||
|
||||
### 4. Spatial model and ownership
|
||||
|
||||
The canonical RuView vocabulary remains ADR-306:
|
||||
|
||||
```text
|
||||
Site -> Building -> Floor -> Space -> Zone
|
||||
-> Sensor / Person / Object / Track
|
||||
-> Observation -> Event -> Alert
|
||||
```
|
||||
|
||||
Cognitum may call a bounded room a “space”; RuView does not create a second
|
||||
room type. Stable external IDs are namespaced and validated before entering the
|
||||
ontology. HomeCore remains authoritative for local registry state and local
|
||||
automation. Cognitum owns tenant/workspace projection and activation. RuVector
|
||||
owns indexed spatial history, not tenancy or authorization.
|
||||
|
||||
The current live endpoint exposes the first `Space` slice only. Sites, floors,
|
||||
zones, entities, events, and alerts are contract milestones, not inferred from
|
||||
missing fields. A client must represent absence as unknown/unavailable and must
|
||||
not fabricate parents, coordinates, people, alerts, or provenance.
|
||||
|
||||
### 5. Privacy boundary and synchronization eligibility
|
||||
|
||||
Only allow-listed P2/P3 semantic projections may cross the cloud boundary.
|
||||
|
||||
| Class | Examples | Cloud default |
|
||||
|---|---|---|
|
||||
| P0 | raw CSI, CIR, RF tensors, packet captures | prohibited |
|
||||
| P1 | pose frames, vital waveforms, identity observations, recordings | prohibited |
|
||||
| P2 | occupancy count, bounded activity/fall possibility, anomaly score | permitted when policy allows |
|
||||
| P3 | versions, connection health, signed capability metadata | permitted |
|
||||
|
||||
The client independently rejects forbidden raw-field names anywhere in the
|
||||
response. This is defense in depth, not a substitute for server-side
|
||||
projection. It also enforces HTTPS except for loopback tests, refuses redirects,
|
||||
uses bounded connect/total timeouts, caps responses at 1 MiB, caps the list at
|
||||
100 spaces, bounds nesting/arrays/strings, validates confidence, and rejects
|
||||
non-P2/P3 space records.
|
||||
|
||||
Cloud-bound envelopes must preserve, when available:
|
||||
|
||||
- tenant/workspace/site/space/device identity;
|
||||
- `messageId` and monotonic `eventSequence`;
|
||||
- `observedAt`, `expiresAt`, freshness, and connection state;
|
||||
- privacy class and semantic schema version;
|
||||
- calibrated confidence and explicit uncertainty/abstention;
|
||||
- model, HomeCore, hardware-manifest, calibration, evidence, and witness
|
||||
provenance.
|
||||
|
||||
Provenance is never used as a calibration identifier. Missing confidence,
|
||||
calibration, timestamp, or tenant identity stays missing and cannot satisfy an
|
||||
admission rule.
|
||||
|
||||
### 6. No feedback laundering or false corroboration
|
||||
|
||||
A Spaces record derived from RuView evidence carries derivation lineage. If it
|
||||
returns to RuView or Autogenous, it is a **projection/recollection** of that
|
||||
lineage, not a new observation. It cannot:
|
||||
|
||||
- increment corroborating-sensor count;
|
||||
- raise evidence level;
|
||||
- be fused as an independent modality;
|
||||
- reset freshness to retrieval time;
|
||||
- erase abstention, contradiction, or uncertainty;
|
||||
- generate a second belief that cites the first as support.
|
||||
|
||||
Deduplication keys include tenant, source/witness identity, message ID, and
|
||||
sequence. Cycles are detected and rejected. Independent corroboration requires
|
||||
a distinct authenticated source and evidence chain.
|
||||
|
||||
### 7. Persistent memory is tenant-scoped and explanation-oriented
|
||||
|
||||
RuVector indexes accepted semantic state under at least:
|
||||
|
||||
```text
|
||||
(tenant_id, workspace_id, site_id, space_id, schema_version, time_bucket)
|
||||
```
|
||||
|
||||
It stores bounded semantic features, uncertainty, evidence references, and
|
||||
witness digests. It does not store OAuth/API credentials or prohibited raw
|
||||
payloads. Retrieval always applies the authenticated tenant/workspace filter
|
||||
before similarity ranking.
|
||||
|
||||
An anomaly explanation names:
|
||||
|
||||
- the current semantic state and its uncertainty;
|
||||
- the relevant learned baseline/window from ADR-312;
|
||||
- comparable tenant-local history;
|
||||
- the measured deviation and contradictory evidence;
|
||||
- the provenance/witness chain;
|
||||
- the evidence label (`MEASURED`, `SYNTHETIC`, or `CLAIMED`).
|
||||
|
||||
Memory supplies context, not permission. A historically common action is not
|
||||
automatically authorized.
|
||||
|
||||
### 8. Agents observe and recommend; policy authorizes action
|
||||
|
||||
Autogenous and other agents receive read-only spatial context by default. Their
|
||||
normal outputs are observations, explanations, proposals, and recommendations.
|
||||
|
||||
Any consequential action must cross the ADR-321 `ruview-policy` gate with:
|
||||
|
||||
- an exact action class and target;
|
||||
- a fresh capability certificate;
|
||||
- KNOWN/DEGRADED/UNKNOWN domain state;
|
||||
- bounded uncertainty and sufficient evidence;
|
||||
- tenant/workspace authorization;
|
||||
- expiry, nonce, idempotency key, and replay protection;
|
||||
- required human/policy approval;
|
||||
- a terminal witness receipt for allow or deny.
|
||||
|
||||
Missing policy, unknown action class, stale state, incomplete provenance, or an
|
||||
unavailable approval service denies. OAuth `spaces:read` can never authorize an
|
||||
action. This ADR adds no actuator method to the Spaces client.
|
||||
|
||||
## Implementation
|
||||
|
||||
### RuView
|
||||
|
||||
- `ruview-cognitum-spaces` is a reusable, read-only client with typed/redacted
|
||||
credentials and a bounded response decoder.
|
||||
- `wifi-densepose login --spaces` explicitly requests `spaces:read` through the
|
||||
existing PKCE flow and credential store.
|
||||
- `wifi-densepose spaces` refreshes OAuth through the existing single-flight,
|
||||
persist-before-return mechanism, verifies that the stored grant contains
|
||||
`spaces:read`, and lists validated state. `COGNITUM_SPACES_API` remains an
|
||||
explicit compatibility path.
|
||||
- the dependency-free contributor metaharness adds `spaces` /
|
||||
`ruview_spaces_list`, invokes only the OAuth branch, bounds and revalidates
|
||||
child output, fixes the production API origin, strips the API-key compatibility
|
||||
environment, requires an installed binary, and default-denies MCP access
|
||||
without `credential-use`.
|
||||
|
||||
### Cognitum Identity
|
||||
|
||||
- the `ruview` public client allow-list includes `spaces:read`;
|
||||
- RFC 8414 metadata advertises it;
|
||||
- refresh preserves the originally granted scope;
|
||||
- no new client secret or password grant is introduced.
|
||||
|
||||
### Cognitum API
|
||||
|
||||
- the gateway preserves caller OAuth through an internal, spoof-resistant
|
||||
relay while authenticating the private Function hop;
|
||||
- Spaces verifies the signed OAuth principal and queries by tenant + workspace;
|
||||
- legacy API-key behavior remains available;
|
||||
- bounded semantic-state `PUT` is available only to an explicitly scoped API-key
|
||||
publisher and is not exposed by the RuView OAuth client;
|
||||
- OpenAPI documents both alternatives and RFC 9728 metadata supports discovery;
|
||||
- the Function remains gateway-only at Cloud Run IAM.
|
||||
|
||||
### Autogenous
|
||||
|
||||
Autogenous must consume an explicitly typed credential. It must not imply that
|
||||
`/v1/cli/session/exchange` produces a RuView-audience token: that exchange
|
||||
currently produces `client_id=cognitum-cli` and cannot pass the Spaces policy.
|
||||
An external RuView PKCE token may be supplied after activation, or a scoped API
|
||||
key may be used as the compatibility path. Response validation and lineage
|
||||
rules in this ADR apply before agent belief formation.
|
||||
|
||||
## Threat model
|
||||
|
||||
| Threat | Required control |
|
||||
|---|---|
|
||||
| Direct Function bypass | invoker IAM check; gateway SA only; no `allUsers` |
|
||||
| Forged internal OAuth header | strip inbound relay headers; gateway writes after route classification |
|
||||
| Token substitution | ES256/JWKS plus exact issuer, audience, client, type, scope, and tenant claims |
|
||||
| Cross-tenant enumeration | principal-derived Firestore selector; bounded non-enumerating errors |
|
||||
| Redirect/token exfiltration | redirects disabled; HTTPS required; fixed path |
|
||||
| Oversized/malformed response | byte/depth/count/string bounds before use |
|
||||
| Raw-data regression | server allow-list plus client forbidden-field rejection |
|
||||
| Secret disclosure | redacting types; no token logs/URLs; `.env` untracked |
|
||||
| Feedback amplification | lineage preservation, dedupe, cycle rejection, no independent corroboration |
|
||||
| Memory leakage | tenant filter before vector search; no global nearest-neighbor pass |
|
||||
| Agent overreach | observe/recommend default; ADR-321 fail-closed action gate |
|
||||
| Stale/replayed state | expiry, sequence, message ID, freshness, witness receipt |
|
||||
| JWKS outage/rotation | bounded cache; fail closed; refresh after unknown `kid`; no algorithm fallback |
|
||||
|
||||
## Deployment and rollback
|
||||
|
||||
Rollout order is dependency-safe:
|
||||
|
||||
1. merge and deploy Identity scope/metadata;
|
||||
2. deploy the Spaces Function with OAuth verification while API-key behavior
|
||||
remains unchanged;
|
||||
3. deploy the gateway relay and protected-resource metadata;
|
||||
4. verify gateway API-key access, OAuth denial matrices, direct-origin platform
|
||||
denial (`401` or `403` before application code), and tenant isolation;
|
||||
5. merge/release the RuView client and CLI activation;
|
||||
6. enable Autogenous consumption only after its strict validation/lineage gates
|
||||
pass.
|
||||
|
||||
Rollback disables OAuth advertisement/relay and returns clients to scoped API
|
||||
keys. It must not restore public Function invocation. Revoking an OAuth session
|
||||
or API key must not alter paired-site state.
|
||||
|
||||
## Validation and acceptance
|
||||
|
||||
Required automated gates:
|
||||
|
||||
- Identity: metadata test, migration application, PKCE authorize/token/refresh
|
||||
scope preservation, cross-client scope denial;
|
||||
- API Function: valid claim matrix and rejection for wrong issuer/audience/
|
||||
client/type/scope/tenant, API-key regression, tenant query assertion, bounded
|
||||
projection tests, build and dependency audit;
|
||||
- gateway: spoofed relay stripped, caller OAuth preserved, Google hop identity
|
||||
substituted, OpenAPI security alternatives, RFC 9728 metadata, build and
|
||||
dependency audit;
|
||||
- RuView: semantic decoder bounds/privacy tests, redaction tests, login scope
|
||||
tests, CLI compile, and live empty/non-empty response tests without fixtures
|
||||
masquerading as production;
|
||||
- policy: no Spaces read can invoke an actuator; denial receipts are witnessed.
|
||||
|
||||
Production readback must prove:
|
||||
|
||||
- unauthenticated gateway request returns `401`;
|
||||
- legacy scoped API key returns the authenticated tenant list;
|
||||
- valid RuView OAuth returns only its workspace;
|
||||
- wrong client, missing `spaces:read`, setup/workload token, and second-tenant
|
||||
token are denied;
|
||||
- the direct Function origin is rejected by the Google platform with `401` or
|
||||
`403` before application code, even with a valid application credential;
|
||||
- response remains `no-store` and excludes P0/P1;
|
||||
- no secret appears in logs, diffs, artifacts, or issue/PR text.
|
||||
|
||||
Performance, detection quality, and action-safety numbers are not claimed by
|
||||
this decision. Any such number requires a named reproducer and the repository's
|
||||
evidence labels. An empty production tenant is a successful isolation/read-path
|
||||
test, not sensing-quality evidence.
|
||||
|
||||
## Production evidence (2026-08-18)
|
||||
|
||||
The bounded Spaces read slice and RuView activation path are deployed. The exact
|
||||
production release chain is:
|
||||
|
||||
- Spaces run `32148530629`, revision `spacesapi-00003-xij`, source
|
||||
`fc333e634cd918b9d6fdde4eecbe7beac1043ab8`, Node 22, runtime service account
|
||||
`spacesapi-runtime@cognitum-20260110.iam.gserviceaccount.com`, with
|
||||
`apigateway-sa@cognitum-20260110.iam.gserviceaccount.com` as sole invoker;
|
||||
- gateway run `32151485401`, revision `apigateway-00180-peh`, source
|
||||
`c4e99ebb4ce0d4e1407f435f905621476c1f0166`, image digest
|
||||
`sha256:bacb81281a54256ff6fdaac253175e76ce6fc225f399163ca0a807a2839bd6a3`;
|
||||
- Identity run `32163542502`, revision `identity-00052-fid`, source
|
||||
`fb6320827b879e481cad6caf184d3cbccd8279c4`, image digest
|
||||
`sha256:0cd5896518bd8ecf042d2f3e9aea58a32e65a68dbddaab1e54f8ae6da2bfab06`,
|
||||
and runtime service account
|
||||
`identity-runtime-prod@cognitum-20260110.iam.gserviceaccount.com`.
|
||||
|
||||
The live API-key matrix returned `200` with an empty bounded list,
|
||||
`Cache-Control: private, no-store`, and no prohibited P0/P1 projection fields.
|
||||
No credential returned `401`. A direct-origin request received a Google
|
||||
Frontend Bearer challenge (`401`) before application code.
|
||||
|
||||
Two independent RuView Authorization Code + PKCE principals also passed the
|
||||
live matrix. Each token used ES256, exact issuer/audience/client checks,
|
||||
`sensing:read spaces:read`, signed UUID organization/workspace claims, refresh
|
||||
rotation, and revocation. Each gateway read returned `200`, an empty bounded
|
||||
list, and `private, no-store`; a corrupted signature returned `401`; and the
|
||||
principals had distinct pseudonymous tenant/workspace fingerprints. This proves
|
||||
the production empty-tenant behavior and independent claim binding. Non-empty
|
||||
cross-tenant isolation remains emulator/staging evidence because production was
|
||||
not mutated to manufacture a fixture.
|
||||
|
||||
Identity metadata deliberately advertises `spaces:read` for RuView but not
|
||||
`spaces:write`. The deployed semantic-state `PUT` remains an API-key-only
|
||||
publisher surface. RuView therefore has no OAuth write, command, policy-approval,
|
||||
or actuator capability.
|
||||
|
||||
That receipt was for the initial flat Space slice. The following production
|
||||
expansion supersedes only its hierarchy/event/alert deferral. MQTT, commands,
|
||||
actuators, real-hardware accuracy, and the long-duration operational trial
|
||||
remain outside the completed claim.
|
||||
|
||||
## Completed implementation and production expansion (2026-08-19)
|
||||
|
||||
- Cognitum API PRs #211 and #212 shipped the eight `/v1/spatial` collections,
|
||||
transactional hierarchy integrity, stable pagination, event/alert retention,
|
||||
strict P2/P3 admission, API-key-only writes, OAuth/API-key reads, and the
|
||||
additive-only Firestore release authority. Function run `32279092861`
|
||||
promoted active Node 22 revision `spacesapi-00005-kaf`.
|
||||
- Edge PRs #214, #215, and #216 preserved canonical UUID routing, kept SQLi
|
||||
denial, and removed secret-valued API-key rate selection. Gateway run
|
||||
`32284410107` promoted the reviewed immutable digest to 100% production
|
||||
traffic. Every versioned collection returned HTTP 200 through the public
|
||||
edge; the hierarchy composite index is `READY` and both retention TTL fields
|
||||
are `ACTIVE`.
|
||||
- The dedicated RuView service credential was rotated to exactly
|
||||
`spaces:read` and `spaces:write`; its predecessor returns 401. A non-mutating
|
||||
invalid-body probe reached write validation without persisting customer data.
|
||||
Other potentially affected owner keys and residual log retention remain
|
||||
tracked in Cognitum API #217.
|
||||
- A live RuView Authorization Code + S256 PKCE consent requested exactly
|
||||
`sensing:read spaces:read`. Its in-memory token read versioned `sites` with
|
||||
HTTP 200 and schema `1.0`; the verifier then revoked the temporary refresh
|
||||
credential and persisted no token.
|
||||
- RuView PR #1650 merged `ruview-cognitum-spaces`,
|
||||
`ruview-spatial-memory`, the ADR-327 policy extension, CLI paging, and the
|
||||
guarded `ruview_spaces_list` metaharness surface. PR #1651 removed stale
|
||||
feature-branch guidance and refreshed the signed package manifest.
|
||||
- The contributor metaharness fixes the API origin, accepts bounded resource,
|
||||
limit, and opaque-cursor inputs, strips API-key compatibility authority over
|
||||
MCP, invokes only the hardened OAuth CLI, and rejects raw sensing or malformed
|
||||
hierarchy/event/alert output. Its test, security, reviewed-brain, flywheel,
|
||||
manifest, audit, exact-tarball, and claim-check gates pass.
|
||||
- Release run `32286297277` rebuilt and smoke-tested the exact package and
|
||||
provenance-published `@ruvnet/ruview` 0.5.0. The public npm registry resolves
|
||||
0.5.0 as `latest`; no workstation publish was used.
|
||||
- `ruview-spatial-memory` keeps one RuVector HNSW index per authenticated
|
||||
tenant/workspace with replay, derivation, retention, cascading-erasure,
|
||||
bounded-explanation, encrypted-snapshot, and reload-verified rotation gates.
|
||||
This is local `SYNTHETIC` evidence, not a production sensing claim.
|
||||
- `ruview-policy` keeps observe/recommend/execute intents distinct, requires
|
||||
exact host grants plus signed approval for consequence, rejects nonce replay,
|
||||
and emits signed hash-chained receipts. `spaces:read` is explicitly denied as
|
||||
execution authority.
|
||||
- Focused Rust gates and the Linux workspace/CLI/security lanes pass. Earlier
|
||||
Windows whole-workspace attempts ended in host compiler failure or timeout;
|
||||
those attempts are not reclassified as green evidence.
|
||||
- No OAuth write/action scope, actuator callback, MQTT deployment claim, sensing
|
||||
accuracy claim, or real-hardware claim is introduced.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- One Cognitum identity can explicitly activate RuView's cloud spatial read
|
||||
capability without sharing a long-lived static bearer.
|
||||
- Tenant and workspace become cryptographically bound inputs to the data query.
|
||||
- RuView and Autogenous gain useful spatial context without importing raw RF or
|
||||
inventing independent evidence.
|
||||
- The design keeps a path for RuVector-grounded explanations and separately
|
||||
governed action without treating either as part of the deployed read slice.
|
||||
- The direct-origin bypass is closed permanently, independent of OAuth rollout.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Two credential types coexist during migration and must stay visibly distinct.
|
||||
- OAuth depends on Identity JWKS availability and correct key rotation.
|
||||
- Production exposes both the legacy Space twins and the versioned hierarchy,
|
||||
anonymous entities, semantic events, and alerts over HTTPS. MQTT remains a
|
||||
design contract without deployment evidence.
|
||||
- OAuth workspace IDs will return only documents populated with `workspaceId`;
|
||||
legacy owner-only documents require an explicit migration, never a broad query.
|
||||
- The RuView client exposes no write, command, or agent execution surface. The
|
||||
separate API-key semantic-state ingress is neither OAuth activation nor
|
||||
actuator authority.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep API keys only.** Rejected as the target: keys are useful for service
|
||||
compatibility but do not provide user activation, consent, short lifetime, or
|
||||
refresh/revocation semantics.
|
||||
|
||||
**Treat the CLI API-key exchange token as a Spaces OAuth token.** Rejected: it
|
||||
is minted for `cognitum-cli`, not `ruview`, and accepting it would remove the
|
||||
audience/client boundary.
|
||||
|
||||
**Trust the gateway without verifying OAuth in Spaces.** Rejected: hop identity
|
||||
and user authorization are distinct, and authorization must remain valid if the
|
||||
route topology changes.
|
||||
|
||||
**Make Spaces state independent corroboration.** Rejected: it is derived from
|
||||
the same RuView/HomeCore lineage and would double-count evidence.
|
||||
|
||||
**Allow agents to execute from `spaces:read`.** Rejected: read consent is not
|
||||
action authority, and perception confidence alone cannot authorize consequence.
|
||||
|
||||
**Synchronize raw RF for better cloud models.** Rejected by default: it violates
|
||||
the edge privacy boundary and is unnecessary for the semantic product.
|
||||
|
||||
## References
|
||||
|
||||
- Autogenous ADR-402, `docs/adr/ADR-402-ruview-cognitum-spaces-spatial-intelligence.md`
|
||||
- Cognitum API ADR-094, `docs/adr/ADR-094-cognitum-spaces-homecore-edge-boundary.md`
|
||||
- Cognitum API hierarchy/events/alerts follow-up,
|
||||
`https://github.com/cognitum-one/api/issues/206`
|
||||
- RuView metaharness OAuth surface,
|
||||
`https://github.com/ruvnet/RuView/issues/1643`
|
||||
- RuVector spatial-history follow-up,
|
||||
`https://github.com/ruvnet/RuView/issues/1640`
|
||||
- governed-action and witness-receipt follow-up,
|
||||
`https://github.com/ruvnet/RuView/issues/1641`
|
||||
- RFC 7636, Proof Key for Code Exchange
|
||||
- RFC 8414, OAuth 2.0 Authorization Server Metadata
|
||||
- RFC 9700, OAuth 2.0 Security Best Current Practice
|
||||
- RFC 9728, OAuth 2.0 Protected Resource Metadata
|
||||
@@ -0,0 +1,137 @@
|
||||
# ADR-326: Tenant-scoped RuVector spatial memory and anomaly explanations
|
||||
|
||||
- **Status**: Accepted — implementation complete; repository-wide and deployment gates pending
|
||||
- **Date**: 2026-08-19
|
||||
- **Decision owners**: RuView maintainers
|
||||
- **Extends**: ADR-312, ADR-319, ADR-325
|
||||
- **Implements**: ruvnet/RuView#1640
|
||||
- **Tags**: cognitum-spaces, ruvector, memory, tenant-isolation, explanation, privacy
|
||||
|
||||
## Context
|
||||
|
||||
ADR-325 requires anomaly explanations grounded in tenant-local spatial history,
|
||||
but the deployed client only returns a current list. A global vector index would
|
||||
be unsafe: filtering nearest-neighbor results after the search can reveal that a
|
||||
different tenant has a close match, even when identifiers are removed. A memory
|
||||
record can also launder returned RuView-derived state into a second independent
|
||||
observation, reset freshness, or form circular evidence.
|
||||
|
||||
Spatial memory must be useful without storing OAuth/API credentials, raw CSI/CIR,
|
||||
RF tensors, pose frames, vital waveforms, recordings, identity observations, or
|
||||
unbounded agent transcripts. Persistence also needs explicit retention,
|
||||
deletion, provenance, and key-rotation behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Partition before similarity
|
||||
|
||||
`ruview-spatial-memory` owns a `SpatialMemory` map keyed by the exact authenticated
|
||||
`(tenant_id, workspace_id)` pair. Each partition owns its own RuVector HNSW index.
|
||||
Ingest and search resolve the partition first; no global ANN query exists. Site,
|
||||
space, schema version, and time-window constraints narrow within the selected
|
||||
partition before results are returned.
|
||||
|
||||
### 2. Bounded semantic records
|
||||
|
||||
An accepted record contains:
|
||||
|
||||
- tenant/workspace/site/space and stable record identity;
|
||||
- source ID, message ID, record ID, monotonic event sequence, schema version;
|
||||
- original `observed_at`/`expires_at` and a retention deadline;
|
||||
- a bounded finite semantic feature vector, uncertainty, and evidence label;
|
||||
- provenance and witness digests, plus bounded derivation references;
|
||||
- explicit observation/inference classification.
|
||||
|
||||
Credentials and P0/P1 fields have no representation in the type. Strings,
|
||||
features, references, record counts, and query `k` are bounded. Non-finite
|
||||
features and uncertainty fail closed.
|
||||
|
||||
### 3. Lineage and replay
|
||||
|
||||
The partition rejects:
|
||||
|
||||
- changed reuse of `(source_id, message_id)`;
|
||||
- a non-increasing sequence for the same source;
|
||||
- duplicate derivation references;
|
||||
- self-reference, missing/forward parents, and therefore every cycle;
|
||||
- expired input or a provenance/witness substitution.
|
||||
|
||||
A recollection keeps its original lineage, timestamp, uncertainty, and evidence
|
||||
label. It cannot increment corroborating-source count or become independent
|
||||
support for its own ancestor.
|
||||
|
||||
### 4. Persistent encrypted storage
|
||||
|
||||
Snapshots are encrypted with XChaCha20-Poly1305 under a caller-supplied 256-bit
|
||||
key and a non-secret key ID. The authenticated associated data binds the storage
|
||||
format and key ID. The envelope is bounded and versioned; plaintext spatial
|
||||
records are never written to disk. Loading requires a keyring containing the
|
||||
named key. Rotation decrypts with the old key, atomically creates a new
|
||||
generation under the new key ID, reload-verifies that generation, and leaves
|
||||
the source intact. Snapshots never overwrite an existing path implicitly.
|
||||
|
||||
Deletion supports a tenant/workspace partition, a record, and retention cutoff.
|
||||
Every deletion rebuilds that partition's HNSW index so removed records cannot be
|
||||
returned from stale graph nodes.
|
||||
|
||||
### 5. Explanations
|
||||
|
||||
`explain` compares a bounded query vector with nearest tenant-local history and
|
||||
returns the exact authenticated partition, generation time, ordered record IDs,
|
||||
RuVector distances, original uncertainty/evidence labels, and provenance/witness
|
||||
digests. Its basis explicitly says that similarity is not causation. The API
|
||||
does not expose the vectors or invent a causal explanation.
|
||||
|
||||
History provides context, not authority. An explanation cannot authorize an
|
||||
action, increase certificate class, or replace a policy decision.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Cross-tenant ANN leakage is structurally unavailable.
|
||||
- Explanations cite the exact tenant-local records used.
|
||||
- Replay/cycle/provenance substitution are rejected before indexing.
|
||||
- Encrypted persistence has explicit key IDs and rotation behavior.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Partition-local HNSW uses more indexes than a global graph.
|
||||
- Deletes and key rotation rebuild indexes.
|
||||
- No detection-quality or latency claim is made; tests are `SYNTHETIC` unless a
|
||||
reproducer explicitly marks a measurement.
|
||||
- Cloud Cognitum does not receive the local encrypted memory file.
|
||||
|
||||
## Validation
|
||||
|
||||
- cross-tenant and cross-workspace nearest-neighbor denial;
|
||||
- duplicate record/message, stale-sequence, self/duplicate/missing-parent, and
|
||||
provenance-substitution tests;
|
||||
- expiry, retention deletion, whole-partition deletion, sealed round-trip,
|
||||
tamper rejection, wrong-key rejection, and key-rotation tests;
|
||||
- explanation citations and retained evidence/provenance labels;
|
||||
- no forbidden raw-field or credential representation;
|
||||
- the focused `ruview-spatial-memory` crate suite passes with `SYNTHETIC`
|
||||
evidence on 2026-08-19;
|
||||
- the whole-workspace Windows gate was non-terminal (compiler crash in parallel,
|
||||
timeout when serialized), so Linux CI, a RustSec advisory scan, and package
|
||||
review remain release gates.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One global HNSW followed by filtering.** Rejected: ranking itself crosses the
|
||||
tenant boundary.
|
||||
|
||||
**Cloud vector memory.** Rejected as the default: it expands the privacy and
|
||||
credential boundary without being needed for local explanations.
|
||||
|
||||
**Plain JSONL persistence.** Rejected because tenant spatial history is sensitive
|
||||
even when raw sensing is excluded.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-312: Long-term spatial memory
|
||||
- ADR-319: Witness chain
|
||||
- ADR-325: Cognitum Spaces activation and governed exchange
|
||||
- Cognitum API ADR-101
|
||||
- ruvnet/RuView#1640
|
||||
@@ -0,0 +1,133 @@
|
||||
# ADR-327: Governed action intents, approvals, replay protection, and witness receipts
|
||||
|
||||
- **Status**: Accepted — implementation complete; repository-wide and deployment gates pending
|
||||
- **Date**: 2026-08-19
|
||||
- **Decision owners**: RuView maintainers
|
||||
- **Extends**: ADR-318, ADR-319, ADR-321, ADR-325
|
||||
- **Implements**: ruvnet/RuView#1641
|
||||
- **Tags**: policy, governed-action, approval, idempotency, witness, cognitum-spaces
|
||||
|
||||
## Context
|
||||
|
||||
The current `ruview-policy` crate evaluates assurance for an action class, but it
|
||||
does not define a complete action intent, tenant/workspace binding, policy
|
||||
version, approval, nonce/idempotency replay behavior, or signed terminal receipt.
|
||||
An agent recommendation can therefore be mistaken for execution authority, and
|
||||
`spaces:read` could be accidentally treated as a general capability.
|
||||
|
||||
The system needs a framework that can prove why an action was allowed or denied
|
||||
without adding any actuator. Real actuation remains a separate integration and
|
||||
requires its own threat model and device evidence.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Typed intent and registered policy
|
||||
|
||||
A governed `ActionIntent` binds:
|
||||
|
||||
- intent ID, tenant, workspace, action name/class, and exact target;
|
||||
- requested policy version and parameter/evidence digests;
|
||||
- creation/expiry, replay nonce, and requesting principal;
|
||||
- the recommendation/explanation that motivated review, never a hidden command.
|
||||
|
||||
The gate accepts only a registered action policy. Unknown action, action-class
|
||||
mismatch, policy-version mismatch, target mismatch, invalid timestamps, and
|
||||
missing exact host authority deny before assurance is evaluated. Tenant and
|
||||
workspace are part of the signed intent/receipt and nonce key. `spaces:read` is
|
||||
explicitly tested as insufficient for an `alerts:execute` rule.
|
||||
|
||||
### 2. Assurance and approval
|
||||
|
||||
The existing ADR-321 certificate/domain/uncertainty/evidence gate remains the
|
||||
assurance authority. The registered policy declares a bounded minimum of
|
||||
distinct enrolled approvers. An absent, rejected, duplicated, expired,
|
||||
wrong-intent, wrong-policy-version, or unverifiable approval denies. Approval
|
||||
resolution fails closed.
|
||||
|
||||
Agents observe, explain, or recommend by default. `evaluate` returns a decision
|
||||
receipt; it does not call an actuator. An executor may consume an `allow` receipt
|
||||
only if a separate adapter verifies the receipt, target, expiry, and its own
|
||||
device-specific authority.
|
||||
|
||||
### 3. Replay and idempotency
|
||||
|
||||
The bounded in-memory gate stores terminal receipts by intent ID and tracks
|
||||
nonces by `(tenant, workspace, nonce)`.
|
||||
|
||||
- exact intent replay returns the original terminal receipt;
|
||||
- changed reuse of an intent ID returns a fail-closed idempotency error;
|
||||
- reuse of a nonce by another intent returns a fail-closed replay error;
|
||||
- expired intents and approvals deny;
|
||||
- failed or denied attempts are terminal and auditable.
|
||||
|
||||
The current state store is bounded and in-memory, intended for local/runtime use
|
||||
rather than cross-process replay protection. A production executor must place
|
||||
the same intent/nonce/receipt invariants behind a transactional durable store;
|
||||
this ADR does not claim that adapter exists.
|
||||
|
||||
### 4. Witnessed terminal receipt
|
||||
|
||||
Every evaluated observe/recommend/execute request produces a canonical receipt
|
||||
containing the intent digest, decision/reason, policy version, tenant/workspace,
|
||||
decision/expiry time, intent ID and nonce, approval count, and previous receipt
|
||||
digest. The receipt is signed through the `ruview-attest` signer interface and
|
||||
can be independently verified. Hash chaining makes removal/reordering visible.
|
||||
Malformed input, ID conflict, nonce replay, capacity exhaustion, and sequence
|
||||
exhaustion are errors before receipt creation and must be audited by the host.
|
||||
|
||||
The reference keyed-BLAKE3 signer remains `SYNTHETIC` evidence only, as documented
|
||||
by ADR-319. Production asymmetric signing and key custody must be supplied by the
|
||||
deployment adapter; no symmetric test MAC is represented as hardware identity.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Recommendation, authorization, and execution are distinct typed stages.
|
||||
- Default-deny covers missing policy, stale evidence, unavailable approval, and replay.
|
||||
- Every decision has a terminal, verifiable explanation.
|
||||
- `spaces:read` cannot silently expand into consequence.
|
||||
|
||||
### Costs and limitations
|
||||
|
||||
- Executors must implement a separate receipt-verifying adapter.
|
||||
- Distributed replay protection needs a transactional durable store.
|
||||
- This ADR implements no actuator, command transport, pairing mutation, or device control.
|
||||
- Simulator tests are not hardware validation.
|
||||
|
||||
## Validation
|
||||
|
||||
- unknown/missing policy, stale intent, policy-version/target mismatch,
|
||||
insufficient authority, and `spaces:read`-only denial;
|
||||
- certificate/domain/uncertainty/evidence denial matrix from ADR-321;
|
||||
- missing/rejected/expired/duplicate/wrong-intent approval tests;
|
||||
- exact idempotent replay, changed reuse, nonce replay, and bounded-store tests;
|
||||
- receipt signature, canonical digest, chain linkage, and tamper rejection;
|
||||
- tests proving evaluation exposes no actuator callback or network/file side effect.
|
||||
|
||||
The focused `ruview-policy` suite passes on 2026-08-19. The reference signer
|
||||
tests are `SYNTHETIC`; they are not hardware-identity evidence. The non-terminal
|
||||
whole-workspace Windows gate still requires authoritative Linux CI evidence.
|
||||
|
||||
Any future actuator adds a separate ADR, credential boundary, failure/rollback
|
||||
plan, allow/deny integration tests, and captured target-device evidence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Let agents call actuators after a recommendation.** Rejected: recommendation
|
||||
quality is not authorization.
|
||||
|
||||
**Treat OAuth scopes as action policy.** Rejected: `spaces:read` expresses read
|
||||
consent only and carries no target-specific assurance or approval.
|
||||
|
||||
**Emit receipts only for successful actions.** Rejected: denial and unavailable
|
||||
approval are security-relevant terminal facts.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-318: Capability certificates
|
||||
- ADR-319: Witness chain
|
||||
- ADR-321: Decision policy action authorization
|
||||
- ADR-325: Cognitum Spaces activation and governed exchange
|
||||
- ADR-326: Tenant-scoped RuVector spatial memory
|
||||
- ruvnet/RuView#1641
|
||||
@@ -0,0 +1,59 @@
|
||||
# ADR 340: iPhone LiDAR Sensor Bridge
|
||||
|
||||
Status: Proposed
|
||||
|
||||
## Context
|
||||
|
||||
RuView needs a low cost mobile geometry sensor that can contribute calibrated spatial observations without coupling the perception substrate to Apple frameworks.
|
||||
|
||||
ARKit exposes rear LiDAR scene depth through `ARFrame.sceneDepth` and `smoothedSceneDepth` on supported devices. Ordinary mobile web pages do not receive this ARKit depth surface directly, so native capture and web visualization must be separated.
|
||||
|
||||
## Decision
|
||||
|
||||
Use a two layer architecture.
|
||||
|
||||
1. Native Swift and ARKit perform acquisition.
|
||||
2. A modality neutral wire frame transports geometry into browser tools and, next, the RuView HAL.
|
||||
|
||||
The native client will capture depth, confidence, camera intrinsics, and world tracking pose. RGB imagery is excluded from the default transport.
|
||||
|
||||
The protocol identifier is `ruview.lidar.depth.v1`.
|
||||
|
||||
Depth samples are quantized to UInt16 millimeters for transport. Confidence remains UInt8. The default sender downsamples by two spatially and caps transmission at 15 FPS. Full fidelity depth remains available locally for future on device inference.
|
||||
|
||||
## RuView integration boundary
|
||||
|
||||
The transport must not become a second world model. The production receiver converts each packet into the canonical `ruview-hal::Observation`, then passes it through authenticated sensor identity, provenance, OOD gating, uncertainty aware fusion, spatial memory, and WorldGraph adapters.
|
||||
|
||||
Rules:
|
||||
|
||||
1. `source=live` is valid only for frames produced by an active ARKit session.
|
||||
2. Sequence numbers are monotonic per sensor session.
|
||||
3. Wall clock timestamp is separate from ARKit monotonic frame timing.
|
||||
4. RGB is off by default and requires an explicit higher privacy capability.
|
||||
5. Browser clients consume geometry but are not treated as authoritative sensors.
|
||||
6. Unsupported devices fail closed rather than substituting simulated depth.
|
||||
|
||||
## Performance target
|
||||
|
||||
`[SYNTHETIC]` A 256 x 192 Float32 depth map is about 196 KB before confidence and metadata. Downsampling to 128 x 96 and encoding each sample as two byte depth plus one byte confidence yields about 36.9 KB raw. At 15 FPS the raw sensor payload is about 553 KB/s. Base64 raises this to roughly 737 KB/s before JSON metadata. These values are arithmetic sizing estimates, not device measurements.
|
||||
|
||||
The `[CLAIMED target]` for local network latency is below 150 ms p95. A later binary WebSocket or QUIC transport can remove base64 overhead; the exact end-to-end reduction must be measured before it is claimed.
|
||||
|
||||
## Security
|
||||
|
||||
The development relay is LAN-facing, requires a random per-run bearer token, bounds message size, and restricts the files it serves. Its default `ws://` transport is not encrypted, so it is not a production trust boundary.
|
||||
|
||||
Production requires WSS, authenticated sensor identity, replay protection, message size limits, per tenant authorization, provenance receipts, and explicit retention policy before persistence.
|
||||
|
||||
## Consequences
|
||||
|
||||
Benefits include commodity hardware, metric depth, tracked camera pose, rapid room scanning, calibration support for RF sensing, and a practical ground truth source for RuView experiments.
|
||||
|
||||
The main limitation is that Apple provides processed scene depth rather than the underlying raw transient LiDAR waveform. Therefore this implementation supports direct geometry and sensor fusion now, but does not reproduce research systems that require raw multipath time of flight transients for non line of sight reconstruction.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
A physical LiDAR capable iPhone must stream live geometry to the browser viewer with monotonically increasing sequence numbers, no RGB payload, valid confidence maps, and below 150 ms p95 local network latency over a 60 second run.
|
||||
|
||||
CI type-checking and simulator runs do not satisfy this criterion. Until a captured physical-device run records the environment and results, the hardware behavior and latency remain unverified.
|
||||
@@ -0,0 +1,93 @@
|
||||
# ADR-344: Adaptive local installation discovery
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — local advertisement and mobile discovery implemented; physical
|
||||
peer-link and hosted-relay qualification pending.
|
||||
|
||||
## Context
|
||||
|
||||
RuView installations previously depended on a manually stored IP address.
|
||||
DHCP changes, access-point changes, and client isolation could therefore leave
|
||||
a healthy sensing installation unreachable from the mobile app. Repeated
|
||||
authentication-policy log entries did not prove that the selected endpoint was
|
||||
reachable.
|
||||
|
||||
Discovery must make commissioning recoverable without turning a service name
|
||||
into authentication, leaking sensor data, scanning arbitrary subnets, or
|
||||
silently moving private spatial data to a hosted service.
|
||||
|
||||
## Decision
|
||||
|
||||
The sensing server advertises one bounded `_ruview._tcp.local.` service when it
|
||||
is bound to a routable interface. Loopback-only instances do not advertise,
|
||||
and operators can disable advertisement with `--no-mdns`.
|
||||
|
||||
The TXT contract is deliberately small:
|
||||
|
||||
| Key | Required | Meaning |
|
||||
|---|---:|---|
|
||||
| `schema=ruview.installation.v1` | yes | Fail-closed protocol discriminator |
|
||||
| `tls=0|1` | yes | HTTP or HTTPS origin construction |
|
||||
| `installation=<opaque id>` | no | Bounded routing hint, never authentication |
|
||||
|
||||
The advertisement contains no node inventory, room identifier, SSID,
|
||||
credentials, CSI, vital estimates, pose, identity, or learning data. The
|
||||
hostname is bounded and sanitized before registration. Advertisement failure
|
||||
is recoverable and does not stop sensing.
|
||||
|
||||
RuView Mobile resolves only the matching service and schema, validates the
|
||||
resulting origin under its private-LAN/HTTPS policy, and requires an application
|
||||
health probe before selection. Its adaptive broker retains the configured
|
||||
origin preference and requires two failed configured probes plus two healthy
|
||||
fallback probes before switching. Credentials remain scoped to their saved
|
||||
origin.
|
||||
|
||||
The recovery ladder is:
|
||||
|
||||
1. configured private-LAN or HTTPS origin;
|
||||
2. verified Bonjour local origin;
|
||||
3. physically qualified Apple peer-to-peer local path;
|
||||
4. explicit, authenticated HTTPS relay for bounded derived frames only;
|
||||
5. optional administrator-managed WireGuard/Tailscale access.
|
||||
|
||||
Only levels 1 and 2 are implemented and software-validated by this decision.
|
||||
Peer-to-peer browsing is enabled on Apple platforms, but it is not a qualified
|
||||
peer-link data plane. This repository does not provide a hosted relay and must
|
||||
fail closed when no local endpoint is healthy.
|
||||
|
||||
## Security and privacy consequences
|
||||
|
||||
- Service discovery is routing evidence, not installation authentication.
|
||||
- Public HTTP origins, credentials in URLs, malformed records, and records with
|
||||
the wrong schema are rejected before use.
|
||||
- Raw CSI, RSSI streams, camera/LiDAR frames, room geometry, pose labels,
|
||||
identity data, and training examples remain local.
|
||||
- Future relay work requires a separate consent, authentication, revocation,
|
||||
minimization, and physical evidence review.
|
||||
- ESP32-S3/C6 nodes remain provisioned to the sensing installation. This ADR
|
||||
does not claim direct phone-to-node discovery or invent a firmware protocol.
|
||||
|
||||
## Validation
|
||||
|
||||
Software acceptance requires:
|
||||
|
||||
- unit tests for bounded advertisement construction and hostname sanitation;
|
||||
- mobile parser rejection, route scoring, anti-flapping, and Settings UI tests;
|
||||
- Rust, TypeScript, lint, security, metaharness, Expo, and native compile gates;
|
||||
- a real Bonjour resolve of the TXT contract followed by a successful
|
||||
`/api/v1/status` probe.
|
||||
|
||||
Physical qualification additionally requires installation discovery on an
|
||||
iPhone, live frame and node-inventory receipt, DHCP-change recovery without
|
||||
flapping, and a five-to-ten-minute zero-fusion-error burn-in. Simulator and
|
||||
host-only results remain `MEASURED_SOFTWARE`; peer-link, relay, and physical
|
||||
reconnection claims remain `NOT_MEASURED` until those captures exist.
|
||||
|
||||
## Implementation references
|
||||
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/discovery.rs`
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/main.rs`
|
||||
- Mobile companion decision: `cognitum-one/ruview-mobile`,
|
||||
`docs/adr/ADR-026-adaptive-local-installation-discovery-and-transport-recovery.md`
|
||||
- Related decisions: ADR-034, ADR-054, ADR-296
|
||||
@@ -0,0 +1,193 @@
|
||||
# ADR-345: Per-link CSI attribution and node-to-node ranging
|
||||
|
||||
- **Status**: Proposed — measurement layer implemented and validated on hardware; localization not built
|
||||
- **Date**: 2026-08-28
|
||||
- **Deciders**: Joe
|
||||
- **Tags**: csi, wire-format, esp32-c6, localization, adr-018, adr-060, adr-110
|
||||
|
||||
## Context
|
||||
|
||||
Every attempt at device-free position estimation in this repository has failed
|
||||
against the same wall, and the wall has now been measured twice from different
|
||||
directions.
|
||||
|
||||
**Phase is unusable.** The bistatic tier needed a signed per-link radial
|
||||
velocity, which requires the common-mode component of CSI phase. Measured
|
||||
2026-08-28 across all three ESP32-C6 nodes over ~40,000 frames, the
|
||||
frame-to-frame change in common-mode phase is statistically indistinguishable
|
||||
from uniform random — resultant length `R` = 0.011–0.032 against a 0.000
|
||||
reference, standard deviation 1.78–1.81 against `pi/sqrt(3)` = 1.814, and no
|
||||
lag-1 autocorrelation. A second, unwrap-free estimator agreed. The mechanism is
|
||||
packet-detection timing quantization: common-mode phase is `-2*pi*f_c*tau`, and
|
||||
one ADC sample of jitter at 20 MHz is `2*pi * 2.412e9 * 50e-9` ~= 758 rad, about
|
||||
120 full wraps, re-randomized every packet. Not calibratable. The bistatic tier
|
||||
is default-off as a result (`bistatic_tier_enabled`).
|
||||
|
||||
**Amplitude works but had no parallax.** The three AP -> node links leave a
|
||||
single distant transmitter within a 36.5 degree fan and converge on the node
|
||||
cluster. Bearings computed from `room_config.json`: 198.5, 221.2, 235.0 degrees.
|
||||
Scalar per-node energy weighting was closed with a clean negative result on
|
||||
2026-08-27 — magnitude responds to proximity, direction does not.
|
||||
|
||||
**The receivers were hearing far more than we recorded.** A node in promiscuous
|
||||
MGMT+DATA mode with no `filter_mac` produces CSI for every transmitter on the
|
||||
channel. Measured on node 2: the AP accounted for only **34%** of frames; the
|
||||
two peer nodes supplied 39% and 27%. Those peer frames are valid measurements
|
||||
of *node-to-node* channels — but ADR-018's wire format carries no transmitter
|
||||
address, so the sink could not attribute them. They interleaved into one
|
||||
`frame_history` with mixed geometry, and the only available remedy
|
||||
(`filter_mac`, ADR-060) fixes the mixing by discarding ~75% of the data.
|
||||
|
||||
The node-to-node links between the same three boards cross the room from three
|
||||
sides at **108.7 degrees** of angular spread (bearings 0.0, 73.2, 108.7) versus
|
||||
36.5 for the AP links — roughly 3x the parallax, with both endpoints known,
|
||||
fixed, and under our control rather than wherever the router happens to live.
|
||||
Link count also grows as `N*(N-1)/2`: 3 nodes give 3 links, 6 give 15, 13 give
|
||||
78.
|
||||
|
||||
## Decision
|
||||
|
||||
Carry the transmitter MAC on the CSI wire and model CSI per **link**
|
||||
(receiver, transmitter) rather than per **node**, as a layer alongside the
|
||||
existing per-node model.
|
||||
|
||||
### 1. Wire format v2
|
||||
|
||||
`CSI_MAGIC_V2 = 0xC5110008`, header 26 bytes: the v1 header unchanged through
|
||||
byte 19, then the transmitter address (`info->mac`, 802.11 addr2) at bytes
|
||||
20..25, with I/Q from byte 26.
|
||||
|
||||
`0xC5110002`..`0xC5110007` were already claimed by the vitals, feature and other
|
||||
edge packets, so v2 takes the next free value rather than an adjacent one — see
|
||||
`issue_928_magic_collision_tests` for why that matters. The parser accepts both
|
||||
magics, so a mixed fleet keeps working and v1 nodes simply report no
|
||||
transmitter.
|
||||
|
||||
### 2. Per-link state (`links.rs`)
|
||||
|
||||
`LinkTable` keyed by `LinkId { rx_node, tx_mac }`, each link holding its own
|
||||
amplitude history, RSSI EMA, learned resting baseline and motion metric.
|
||||
|
||||
The metric is the mean over subcarriers of the temporal standard deviation of
|
||||
**AGC-normalized** amplitude. Removing each frame's own mean across subcarriers
|
||||
is essential rather than cosmetic: the ESP32 applies automatic gain control per
|
||||
packet, so absolute amplitude jumps between packets for reasons unrelated to the
|
||||
channel, and any statistic built on the raw level measures the radio's gain
|
||||
control instead of the room.
|
||||
|
||||
Amplitude only, deliberately — nothing here can depend on phase coherence.
|
||||
|
||||
### 3. Beacon rate
|
||||
|
||||
`BEACON_PERIOD_MS` 100 -> 20. At 10 Hz a peer samples a node-to-node link with a
|
||||
5 Hz Nyquist while motion Doppler lives at 1–20 Hz; it aliases before it starts.
|
||||
20 ms matches both the receiver's own `CSI_MIN_PROCESS_INTERVAL_US` ceiling and
|
||||
the gateway self-ping already used for the AP link.
|
||||
|
||||
### 4. `GET /api/v1/links` and a live UI panel
|
||||
|
||||
The link table was initially reachable only through a 30-second log line, which
|
||||
is the wrong medium for a quantity whose value is watching it respond while you
|
||||
walk across the room. `GET /api/v1/links` returns one row per link — `motion`,
|
||||
`raw_motion`, `rssi_dbm`, `frames`, `kind` — and `LinkMeshPanel` renders them in
|
||||
the Sensing tab at 1 Hz. `raw_motion` is shown beside `motion` deliberately: a
|
||||
link whose baseline has not settled reports a healthy raw value and a near-zero
|
||||
motion, and with only the one number that is indistinguishable from a dead link.
|
||||
|
||||
Node identity is **inferred, not measured**. Nothing in the system records a
|
||||
node's own MAC — a node reports the addresses it hears, never its own. But a
|
||||
radio does not receive its own transmissions, so given the set of receivers
|
||||
currently reporting, a transmitter heard by all of them except exactly one is
|
||||
that one receiver's own board; a transmitter heard by every receiver is external
|
||||
infrastructure. `links::infer_transmitting_node` declines (returns `None`) with
|
||||
fewer than three receivers, where "heard by all but one" and "heard by exactly
|
||||
one" are the same observation, and whenever more than one receiver is missing,
|
||||
which a peer out of range produces indistinguishably. The field is named
|
||||
`tx_node_inferred` and the UI draws the peer id with a trailing `?`.
|
||||
|
||||
Checked against the known MAC map on 2026-08-28 the inference labelled all four
|
||||
transmitters correctly — nodes 0, 1 and 2 by address, and the AP as external —
|
||||
with no access to that map. A wrong answer here mislabels a display row; it
|
||||
never reaches a metric.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Validated on hardware, 2026-08-28
|
||||
|
||||
All three nodes on wire v2 at 20 ms produced the full mesh: **9 links** (3 AP +
|
||||
6 directional node-to-node).
|
||||
|
||||
Reciprocity — each physical channel measured independently from both ends:
|
||||
|
||||
| channel | direction A | direction B | ratio |
|
||||
|---|---|---|---|
|
||||
| 0 <-> 1 | 2.229 | 1.930 | 1.15 |
|
||||
| 0 <-> 2 | 1.353 | 1.000 | 1.35 |
|
||||
| 1 <-> 2 | 1.398 | 1.486 | 1.06 |
|
||||
|
||||
Nothing in the implementation enforces this. Three independent radio pairs
|
||||
agreeing within 6–35% is the strongest available evidence that the per-link
|
||||
separation is real rather than an artifact of frame bucketing.
|
||||
|
||||
Link quality is also far more uniform than the AP links: node-to-node raw motion
|
||||
spans 1.0–2.2 across all six, versus 3.8–17.8 for the three AP links (which
|
||||
track RSSI: -67, -72, -80). Uniform link quality is desirable for tomographic
|
||||
localization — no single link dominates the solution.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Peer links are ~10x weaker.** A 16-byte ESP-NOW beacon offers far less
|
||||
energy to estimate a channel response from than a full data frame. Whether
|
||||
1.0–2.2 perturbation is sufficient to localize with is **unknown and untested**.
|
||||
- **Three links is below threshold.** Tomographic localization needs many
|
||||
crossing links; 3 will not localize regardless of quality. This ADR delivers
|
||||
the measurement layer, not localization.
|
||||
- **Localization is not implemented.** No position tier consumes `LinkTable`.
|
||||
The live position estimate remains `doppler_centroid`.
|
||||
- **Per-node path regression.** Removing `filter_mac` to admit peer frames
|
||||
returned the *per-node* `frame_history` to a mixed-transmitter stream, since
|
||||
it still consumes every frame regardless of source. Node 2's classification
|
||||
signal is noisier as a result. The fix is to select a link to feed the
|
||||
per-node path — software filtering that keeps the other links — and it is not
|
||||
yet done.
|
||||
|
||||
### Scaling limits to settle before more nodes
|
||||
|
||||
- **Airtime.** Every node broadcasts, so N nodes put N/period frames per second
|
||||
on the channel being sensed. 3 nodes at 20 ms is 150 pps; 13 would be 650 pps.
|
||||
`BEACON_PERIOD_MS` should become an NVS setting scaled to fleet size.
|
||||
- **`MAX_LINKS` is 64.** 13 nodes give 78 receiver-transmitter pairs plus the AP
|
||||
plus neighbours, which overflows. Raise deliberately, with the memory implied.
|
||||
- **Placement.** Links must *cross* the target area. Nodes spread through a
|
||||
house give roughly one link per room, each attenuated by walls and perturbed
|
||||
by anyone anywhere along it — whole-house coarse presence, not position.
|
||||
Perimeter placement around a single room is the geometry that works.
|
||||
|
||||
## Related bugs found and fixed while implementing
|
||||
|
||||
- **ADR-060 MAC filtering cost ~75% of capture rate.** The 50 Hz rate gate ran
|
||||
*before* the MAC filter and stamped `s_last_process_us` before the check, so a
|
||||
frame from any other transmitter consumed the slot and was then discarded.
|
||||
Enabling `filter_mac` dropped yield from ~42 pps to 6–13. Filtering first
|
||||
restores it (measured 31–37 pps). A 6-byte `memcmp` is negligible ISR work next
|
||||
to the CSI processing the gate guards.
|
||||
- **The entire edge pipeline was dead on ESP32-C6.** `EDGE_MAX_SUBCARRIERS` was
|
||||
128, an S3-era assumption; a C6 on an HE-capable AP delivers HE20 frames with
|
||||
256 bins, and `process_frame` rejected every one. ADR-110 onboarded the C6 to
|
||||
the capture path but not this one. Vitals, presence, fall detection and
|
||||
per-slot counting had never run on these boards. Now target-conditional on
|
||||
`CONFIG_SOC_WIFI_HE_SUPPORT`, with the four subcarrier-sized arrays hoisted to
|
||||
static so peak stack is *lower* than it was at 128.
|
||||
- **A target-conditional constant that failed silently.** `edge_processing.h`
|
||||
had no `#include` at all, so `CONFIG_SOC_WIFI_HE_SUPPORT` was undefined and
|
||||
`#if` evaluated it as 0 — selecting the wrong size with no warning. An
|
||||
undefined identifier in `#if` is 0 in C. This is the same failure mode as the
|
||||
bug it was fixing, one layer up.
|
||||
|
||||
## References
|
||||
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/links.rs` — per-link state, 8 tests
|
||||
- `v2/crates/wifi-densepose-sensing-server/src/phase_diag.rs` — the phase measurement
|
||||
- `firmware/esp32-csi-node/main/csi_collector.c` — wire v2 serialization, filter ordering
|
||||
- `firmware/esp32-csi-node/main/edge_processing.h` — `EDGE_MAX_SUBCARRIERS`
|
||||
- ADR-018 (wire format), ADR-060 (MAC filtering), ADR-110 (C6 onboarding)
|
||||
@@ -0,0 +1,52 @@
|
||||
# ADR 346: Fail closed ESP32 occupancy evidence
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented. Physical qualification is required after each firmware build.
|
||||
|
||||
## Date
|
||||
|
||||
2026 08 31
|
||||
|
||||
## Context
|
||||
|
||||
The ESP32 Tier 2 pipeline produces two different signals. Presence is a debounced room level decision. Person count is a bounded subcarrier diversity heuristic. A live four node installation emitted packets with `presence=false` and `n_persons=3` or `4`. The server eventually gated the aggregate room count, but raw WebSocket consumers and diagnostics could still treat the contradictory count as occupancy evidence.
|
||||
|
||||
That contradiction is more dangerous than a missed optional count. It can contaminate empty room calibration, train a room model on false labels, and encourage a product claim that the firmware cannot support. The count is not identity, pose, or a validated multi person estimator.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Firmware person slots are subordinate to the debounced presence gate.
|
||||
2. When presence is false, the firmware clears slot activity, slot history, candidate count, persistence streak, and stable count.
|
||||
3. The serialized person count is always zero when presence is false and is clamped to `EDGE_MAX_PERSONS` when presence is true.
|
||||
4. The sensing server repeats the invariant for older firmware. A contradictory or out of range count becomes zero and carries `person_count_valid=false`.
|
||||
5. Fused CSI plus mmWave packets use either CSI presence or mmWave presence as the supporting presence condition.
|
||||
6. The node inventory and WebSocket diagnostics expose person count validity. Consumers must not infer a person from an invalid count.
|
||||
7. No count accuracy claim is created by this change. The firmware output remains a heuristic until a leakage free, held out physical dataset demonstrates otherwise.
|
||||
8. OTA admission uses the selected update partition size rather than a stale fixed 900 KB ceiling. The status endpoint reports that same hardware bound, while image validation and authenticated OTA remain mandatory.
|
||||
|
||||
## Security and privacy
|
||||
|
||||
The change retains no raw CSI or personal data. It reduces authority by preventing a secondary heuristic from asserting occupancy after the primary gate has closed. The host validates packet length, magic, range, and logical consistency before using count evidence.
|
||||
|
||||
## Consequences
|
||||
|
||||
Older firmware remains wire compatible. Invalid count evidence becomes visibly unavailable instead of silently affecting calibration. A true multi person event can still be undercounted when the presence gate is false, which is the intended fail closed behavior. Current C6 images larger than 900 KB can use the installed 1,900,544 byte OTA slots after one serial upgrade, without weakening the OTA authentication gate.
|
||||
|
||||
The largest uncertainty is whether the current presence gate itself generalizes across the installed rooms. The fix path is a room bound empty baseline plus the fixed room selective held out protocol, not a global threshold reduction.
|
||||
|
||||
## Evidence and acceptance
|
||||
|
||||
MEASURED before implementation on 2026 08 31: four live nodes streamed for 86 seconds with zero transport errors, while edge packets repeatedly contradicted `presence=false` with counts of three or four.
|
||||
|
||||
Software acceptance requires:
|
||||
|
||||
1. Host firmware tests prove absent plus four active slots serializes zero.
|
||||
2. Rust parser tests prove contradictory and out of range counts fail closed.
|
||||
3. The node API exposes count validity without breaking older firmware.
|
||||
|
||||
Physical acceptance requires the updated firmware on a confirmed board, a captured boot log, five minutes of live packets, zero logical count contradictions, and no increase in transport errors. Accuracy remains unmeasured until labelled held out sequences are recorded.
|
||||
|
||||
Physical occupancy qualification completed for ESP32 C6 node 4 on 2026 08 31. The five minute run observed 242 edge packets, including 61 absent packets, with zero logical count contradictions and zero parse errors. See `docs/validation/2026-08-31-esp32-c6-occupancy-integrity.md`.
|
||||
|
||||
ESP32 C6 node 7 was subsequently identified, upgraded to firmware 0.8.8, and transport qualified for five minutes with zero fused presence count contradictions and zero steady state transport errors. Its controlled empty room sequence remains required before occupancy qualification. See `docs/validation/2026-08-31-esp32-c6-node7-rate-aware-sensing.md`. Other nodes remain unqualified until separately identified and upgraded.
|
||||
@@ -0,0 +1,85 @@
|
||||
# ADR 347: Rate aware ESP32 temporal sensing
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Implemented in firmware 0.8.8. The timing and transport path is
|
||||
physically qualified on ESP32 C6. The independent raw transport path is also
|
||||
physically qualified on an ESP32 S3 running Tier 0. Held out inference accuracy
|
||||
remains required.
|
||||
|
||||
## Context
|
||||
|
||||
The ESP32 firmware creates CSI opportunities by sending one byte ICMP probes to
|
||||
the connected access point. The traffic source is configured for 50 Hz, but the
|
||||
delivered CSI cadence varies with channel contention and callback safety gates.
|
||||
A physical ESP32 C6 produced 28 to 37 callbacks per second during the baseline
|
||||
capture. Firmware 0.8.5 then exposed that the old per-interval estimator saw
|
||||
only 12 to 16 Hz because WiFi replies arrived in short bursts separated by
|
||||
longer gaps. The filters still consumed those burst frames, so excluding them
|
||||
from the clock estimate was incorrect.
|
||||
|
||||
The edge DSP estimates its sample rate from timestamps so that breathing,
|
||||
heartbeat, motion, and future Doppler features stay in physical Hertz. That
|
||||
estimator was capped at 30 Hz. Once the actual cadence exceeded the cap, every
|
||||
temporal feature was scaled against the wrong clock.
|
||||
|
||||
Physical firmware 0.8.5 validation corrected that initial diagnosis. Although
|
||||
the callback path received 26 to 40 frames per second, Tier 2 on the unicore C6
|
||||
processed an irregular subset that converged toward the 8 Hz estimator floor.
|
||||
The right design is not to force the edge DSP to match raw capture. The paths
|
||||
need independent, explicit cadence contracts.
|
||||
|
||||
The device free gesture preprint at
|
||||
`https://www.preprints.org/manuscript/202602.0018` reinforces the importance of
|
||||
timestamp correct Doppler features, but its 100 Hz controlled link is not a
|
||||
safe firmware default for RuView. Existing S3 and C6 evidence records WiFi ISR
|
||||
and packet buffer failures under sustained callback pressure above 50 Hz.
|
||||
|
||||
## Decision
|
||||
|
||||
1. Make the connected STA probe rate a build time setting from 10 through 50
|
||||
Hz, with a default and hard ceiling of 50 Hz.
|
||||
|
||||
2. Track the delivered DSP cadence by counting every processed frame interval
|
||||
over one second timestamp windows, then smooth successive windows in an 8
|
||||
through 60 Hz estimator range. The 60 Hz estimator ceiling accommodates
|
||||
timestamp jitter; it does not authorize more than 50 Hz callback processing.
|
||||
|
||||
3. Reject incomplete windows below one second and stalled windows above three
|
||||
seconds. Do not discard valid burst frames from the estimated clock.
|
||||
|
||||
4. Surface the DSP rate in the one second controller diagnostic so hardware
|
||||
validation can compare callback yield with the clock used by temporal
|
||||
filters.
|
||||
|
||||
5. Keep raw CSI on the wire at the independent network cadence. Rate-limit the
|
||||
C6 on-device Tier 1 and Tier 2 DSP input to a uniform 8 Hz. Physical 0.8.7
|
||||
evidence showed that a requested 10 Hz input still converged to 8.0 through
|
||||
8.4 Hz under Tier 2 load, while raw delivery remained 30 through 40 pps.
|
||||
Eight hertz retains a 4 Hz Nyquist limit for the 0.1 through 2.0 Hz vital
|
||||
bands without creating a backlog. The S3 default remains 20 Hz.
|
||||
|
||||
6. STFT, spectrogram gating, and learned temporal
|
||||
classification remain host or iPhone responsibilities where memory,
|
||||
rollback, and held out evaluation are stronger.
|
||||
|
||||
## Consequences
|
||||
|
||||
Heartbeat, respiration, and motion features receive a stable timestamped clock
|
||||
instead of an accidental subset determined by C6 backlog. Operators can lower
|
||||
the probe or DSP load for constrained networks without editing source. The host
|
||||
still receives the higher-rate raw stream for richer Doppler processing.
|
||||
|
||||
This does not prove vital sign accuracy or gesture recognition. Higher temporal
|
||||
fidelity only improves the representation available to a separately validated
|
||||
model. The 50 Hz ceiling also means the paper's 100 Hz results are not directly
|
||||
transferable.
|
||||
|
||||
## Acceptance test
|
||||
|
||||
On a physical C6, run at least five minutes after flashing. Pass when the boot
|
||||
log reports the configured probe and DSP rates, the controller converges within
|
||||
one hertz of the configured DSP cadence, raw callback yield remains at least 20
|
||||
pps, no steady-state ENOMEM, watchdog, panic, or reboot occurs, and the fail
|
||||
closed occupancy invariant remains zero contradictions for at least 30 absent
|
||||
packets.
|
||||
@@ -0,0 +1,336 @@
|
||||
# ADR-348: Independent Rust multivariate forecasting for RuView
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-09-01
|
||||
- **Deciders**: ruv
|
||||
- **Owners**: RuView perception, model, security, and data-governance maintainers
|
||||
- **Tags**: forecasting, rust, ruvector, temporal, uncertainty, clean-room, provenance
|
||||
- **Extends**: ADR-016, ADR-020, ADR-145, ADR-273, ADR-282, ADR-295,
|
||||
ADR-298, ADR-302, ADR-304, ADR-317, ADR-318, ADR-319
|
||||
- **Supersedes**: None
|
||||
|
||||
## Executive decision
|
||||
|
||||
RuView will develop an independently specified and independently trained Rust
|
||||
multivariate forecasting subsystem. It will forecast bounded, versioned
|
||||
temporal feature streams, publish calibrated quantiles and an explicit
|
||||
abstention state, optionally use a split-safe RuVector analogue index, and
|
||||
remain advisory until all release gates in this ADR pass.
|
||||
|
||||
This is not a port, compatibility layer, distillation, or behavioural clone of
|
||||
Google TimesFM. The implementation API is derived from RuView requirements.
|
||||
Its weights must descend from an approved random initialization and approved
|
||||
training data only.
|
||||
|
||||
**Evidence status at proposal:** the forecasting accuracy, false-alert
|
||||
reduction, calibration, CPU latency, memory use, cross-building generalization,
|
||||
and operational value described below are all **UNMEASURED targets**. This ADR
|
||||
authorizes implementation and evaluation; it makes no `MEASURED` capability
|
||||
claim and approves no production model.
|
||||
|
||||
## Context
|
||||
|
||||
RuView currently observes present and recent RF state. Forecasting could add a
|
||||
separate answer to questions such as whether a feature trajectory is consistent
|
||||
with real occupancy, whether motion is likely to transition between zones, or
|
||||
whether a radio link is degrading. A forecast is not a sensor observation. It
|
||||
is derived evidence with uncertainty and must never overwrite the immutable
|
||||
observation that produced it.
|
||||
|
||||
TimesFM 3 is useful research context because its public description discusses
|
||||
multivariate targets, past covariates, known-future covariates, probabilistic
|
||||
outputs, temporal patching, and cross-series attention. Its licensing creates
|
||||
two distinct surfaces:
|
||||
|
||||
- the Google TimesFM source repository states that source code is Apache-2.0;
|
||||
- the TimesFM 3 pretrained weights are distributed under a separate
|
||||
non-commercial, non-production license that also restricts commercial
|
||||
training, fine-tuning, and distillation from the model.
|
||||
|
||||
An Apache-compliant Rust port would be a permissible but attributed derivative
|
||||
source path, subject to its exact license obligations. It would not support the
|
||||
strongest independent-development claim. Using the restricted TimesFM 3 model
|
||||
or its outputs to create a RuView commercial model is outside this ADR. The
|
||||
project therefore chooses the stricter clean-room protocol in
|
||||
[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md).
|
||||
|
||||
Clean-room controls reduce copyright, contract, and provenance risk. They do
|
||||
not establish freedom to operate against patents, clear trademarks, approve a
|
||||
dataset, or replace legal review in a launch jurisdiction.
|
||||
|
||||
## Options considered
|
||||
|
||||
1. **Use the TimesFM 3 checkpoint in RuView.** Rejected for commercial and
|
||||
production use under the current checkpoint license.
|
||||
2. **Translate the Apache-licensed implementation into Rust.** Not selected.
|
||||
This can be evaluated under a separate ADR, with attribution and license
|
||||
compliance, but must not be mixed with the independent-development path.
|
||||
3. **Build an independent Rust forecasting specialist for RuView.** Chosen.
|
||||
This narrows model scope, keeps inference native, controls provenance, and
|
||||
makes RF-specific evaluation the source of authority.
|
||||
4. **Do not add forecasting.** Retained as the deployment baseline. Forecasting
|
||||
must beat simpler deterministic and statistical baselines before acquiring
|
||||
runtime authority.
|
||||
|
||||
## Requirements
|
||||
|
||||
| ID | Requirement | Verification authority |
|
||||
|---|---|---|
|
||||
| RF-001 | No TimesFM 3 code, configuration, weights, parameters, outputs, activations, or derivatives enter specification, implementation, training, evaluation feedback, or release artifacts. | Clean-room manifest, contributor attestations, repository and artifact scans |
|
||||
| RF-002 | Every contributor, reference, dependency, dataset, transform, training job, and checkpoint has immutable provenance. | Signed provenance manifests and review receipts |
|
||||
| RF-003 | Every training byte has documented rights for commercial machine-learning use and applicable privacy approval. | Dataset licensing gate and data-steward approval |
|
||||
| RF-004 | The default runtime is native Rust, bounded, offline, deterministic for a fixed artifact/input within the declared platform class, and free of runtime model download. | Unit, property, fuzz, replay, and dependency tests |
|
||||
| RF-005 | Inputs and outputs are versioned, finite, bounded, timestamped, mask-aware, and provenance-labelled. Invalid, stale, insufficient, or OOD inputs abstain. | Contract tests and malformed-input corpus |
|
||||
| RF-006 | Forecasts remain derived evidence. They cannot rewrite observations, silently increase observation confidence, or be relabelled `MEASURED`. | Schema invariants and evidence-engine tests |
|
||||
| RF-007 | Quantile ordering, interval calibration, missing-data behaviour, and abstention are evaluated on leakage-free site/session/device holdouts. | Frozen evaluation manifest and reproducible report |
|
||||
| RF-008 | RuVector retrieval is optional, split-scoped, provenance-recorded, and evaluated against the same model without retrieval. | Retrieval isolation tests and paired ablation |
|
||||
| RF-009 | RuVLLM may explain or summarize a signed forecast but cannot alter numeric forecasts, bypass policy, spend money, or trigger medical, emergency, access-control, or life-safety action. | Capability tests and downstream policy review |
|
||||
| RF-010 | Training on local Linux or a hosted accelerator uses identical signed code, container, data, and configuration identities; the provider is an untrusted processor. | Training receipts and digest comparison |
|
||||
| RF-011 | Accuracy, latency, memory, power, and operational claims remain `UNMEASURED` or `CLAIMED` until a named reproducer satisfies the repository evidence contract. | Documentation and release review |
|
||||
| RF-012 | Rollout is reversible and cannot advance from offline evaluation to shadow or advisory operation without the mapped gates below. | Signed mode transition and rollback drill |
|
||||
|
||||
## Architecture boundary
|
||||
|
||||
The initial implementation is split across three crates:
|
||||
|
||||
| Crate | Owned responsibility | Dependency/runtime rule |
|
||||
|---|---|---|
|
||||
| `ruview-forecast-core` | Backend-neutral schemas, invariants, metrics, forecast receipts, and the `Forecaster` trait | No Burn, CUDA, WGPU, provider SDK, sensing-server, or network dependency |
|
||||
| `ruview-forecast-model` | Independent Burn 0.21 patch-mixer architecture and artifact execution | CPU, CUDA, and WGPU are explicit optional features; every backend feature is off by default |
|
||||
| `ruview-forecast-train` | Dataset manifests/splits, trainer, evaluator, the `ruforecast` CLI, and Linux/fal.ai training assets | Training-only authority; no production activation or sensing-server mutation |
|
||||
|
||||
This PR does not connect the forecaster to the sensing server. A shadow bridge
|
||||
requires a follow-up change after the core contract and evidence receipt have
|
||||
stabilized. Landing crates or passing synthetic tests therefore creates no
|
||||
live RuView forecasting capability.
|
||||
|
||||
Version one activates only the exact reviewed `tiny_ci` and `large_linux`
|
||||
architecture profiles. The model and training boundaries enforce checked
|
||||
dimension, parameter, activation-cell, input-cell, and forward multiply-add
|
||||
limits per batch. Adding or changing a profile is therefore a reviewed code and
|
||||
artifact-schema decision, not untrusted request configuration.
|
||||
|
||||
The proposed logical pipeline is:
|
||||
|
||||
```text
|
||||
authenticated RF observations
|
||||
|
|
||||
v
|
||||
versioned one-second feature windows
|
||||
|
|
||||
+----> split-scoped RuVector analogue retrieval (optional)
|
||||
|
|
||||
v
|
||||
independent Rust temporal model
|
||||
|
|
||||
v
|
||||
point forecast + ordered quantiles + validity mask + abstention
|
||||
|
|
||||
v
|
||||
signed derived-evidence record
|
||||
|
|
||||
+----> RuView policy/evidence engine
|
||||
+----> RuVLLM explanation with no numeric mutation authority
|
||||
```
|
||||
|
||||
The initial feature contract should favour compact one-second summaries rather
|
||||
than feeding an unbounded raw CSI stream. Candidate fields include motion
|
||||
energy, Doppler summary, amplitude/phase dispersion, coherence, RSSI, packet
|
||||
loss, channel utilization, current detector confidence, device temperature,
|
||||
and source-validity masks. Each field needs a schema version, physical unit,
|
||||
aggregation rule, validity semantics, and provenance. Adding a field is a
|
||||
schema change, not an implicit positional extension.
|
||||
|
||||
The model may independently combine normalization, temporal patch encoding,
|
||||
temporal mixing, cross-stream fusion, and quantile heads. This is a functional
|
||||
design space, not permission to reproduce protected source expression,
|
||||
checkpoint dimensions, constants, tests, diagrams, or API choices. Exact
|
||||
architecture and parameter count remain implementation decisions recorded in
|
||||
the model card and training receipt.
|
||||
|
||||
## Forecast contract
|
||||
|
||||
For each request the runtime returns one typed result, including abstention and
|
||||
error paths. The result must identify:
|
||||
|
||||
- input schema, feature-window, model, configuration, and calibration digests;
|
||||
- source time range, requested horizon, cadence, target streams, and masks;
|
||||
- point estimate and declared quantiles for each target/horizon cell;
|
||||
- whether quantiles were corrected for crossing and which method was used;
|
||||
- OOD, insufficient-context, stale-input, and non-finite dispositions;
|
||||
- optional RuVector index version, neighbour identifiers, distances, and
|
||||
leakage-scope receipt;
|
||||
- runtime platform class, duration, and peak-memory measurement when enabled;
|
||||
- evidence label `DERIVED_FORECAST`, never `MEASURED_OBSERVATION`;
|
||||
- deterministic content hash and optional RVF signature.
|
||||
|
||||
The runtime rejects non-finite values, duplicate or non-monotonic timestamps,
|
||||
unsupported schema versions, unbounded dimensions, invalid quantile requests,
|
||||
future covariates without an allowed source, and payloads above configured
|
||||
limits. It abstains rather than imputing an authoritative state when history or
|
||||
validity coverage is below the model-card threshold.
|
||||
|
||||
## RuVector integration
|
||||
|
||||
RuVector supplies temporal memory, not ground truth. During training and
|
||||
evaluation, each split has a separate index built only from records permitted
|
||||
for that split. A test query may retrieve training analogues, but it may never
|
||||
retrieve a window from the same subject/session/site holdout, an overlapping
|
||||
target horizon, or any test record. Fitted normalizers and retrieval thresholds
|
||||
come from training data only.
|
||||
|
||||
Every release reports three comparable rows on identical examples:
|
||||
|
||||
1. deterministic/statistical baseline;
|
||||
2. forecasting model without retrieval;
|
||||
3. forecasting model with RuVector retrieval.
|
||||
|
||||
This prevents retrieval from hiding a weak model or leaking future state.
|
||||
|
||||
## RuVLLM and action authority
|
||||
|
||||
RuVLLM consumes an immutable, signed forecast record and may produce an
|
||||
explanation referencing its uncertainty and provenance. Its prose is not the
|
||||
forecast and receives no higher evidence grade. Numeric values exposed to APIs
|
||||
come from the forecasting record, not regenerated text.
|
||||
|
||||
No forecast or LLM explanation independently triggers a health alert, emergency
|
||||
response, access decision, actuator, firmware change, model promotion, or
|
||||
commercial spend. Such actions require a separate governed policy, explicit
|
||||
capability, and appropriate observed evidence.
|
||||
|
||||
## Training and artifact boundary
|
||||
|
||||
Training jobs run from a pinned container or reproducible local environment.
|
||||
The receipt binds the source commit, lockfile, compiler, container, datasets,
|
||||
transforms, architecture, hyperparameters, seeds, hardware, provider job ID,
|
||||
start/end time, and every emitted checkpoint. Hosted workers receive only the
|
||||
least data and credentials necessary. Provider caches, logs, retention, and
|
||||
reuse rights must pass security and data review before customer-derived data is
|
||||
uploaded.
|
||||
|
||||
A release artifact is immutable and hash addressed. It is loaded locally after
|
||||
signature, schema, size, and compatibility verification. The inference process
|
||||
does not fetch models, execute embedded code, accept arbitrary operators, or
|
||||
contact the training provider.
|
||||
|
||||
## Leakage and evaluation protocol
|
||||
|
||||
- Split by deployment/site first, then subject, session, device, and contiguous
|
||||
time block as applicable. No overlapping raw sequence or derived window may
|
||||
cross a split.
|
||||
- Freeze test manifests before tuning. A failed test result does not become a
|
||||
new training target; material architecture changes require a new untouched
|
||||
holdout.
|
||||
- Fit normalization, calibration, feature selection, thresholds, and RuVector
|
||||
index parameters on training/validation data only.
|
||||
- Report missingness, abstention coverage, selective risk, per-horizon errors,
|
||||
interval coverage, quantile loss, and results by site/device/interference
|
||||
regime. Pooled performance cannot hide a failed domain.
|
||||
- Compare against last value, seasonal naive, and at least one small classical
|
||||
or recurrent baseline. TimesFM 3 output is not an acceptance oracle.
|
||||
- Forecast evaluation does not prove presence, pose, fall, respiration, or
|
||||
medical accuracy. Any downstream claim requires its own labelled protocol.
|
||||
|
||||
## Acceptance gates and requirement mapping
|
||||
|
||||
Targets below are release criteria, not current results.
|
||||
|
||||
| Gate | Requirements | Pass condition | Current state |
|
||||
|---|---|---|---|
|
||||
| G0 independent-authoring boundary | RF-001, RF-002 | Approved source allowlist; 100% current contributor attestations; zero restricted artifacts or outputs; all similarity findings adjudicated by the clean-room custodian. | **OPEN / UNMEASURED** |
|
||||
| G1 data and lineage | RF-002, RF-003, RF-010 | 100% of training/evaluation bytes resolve to approved manifests; zero unknown, noncommercial, research-only, no-ML, or no-derivatives sources; checkpoint lineage reaches approved random initialization; local/hosted receipts bind identical governed inputs. | **OPEN / UNMEASURED** |
|
||||
| G2 bounded Rust contract | RF-004, RF-005, RF-006 | Unit/property/fuzz tests cover dimensions, non-finite values, timestamp order, masks, quantile order, stale/OOD/insufficient context, deterministic hashes, offline loading, evidence labels, and resource caps; 24-hour accelerated replay has no panic or unbounded growth. | **OPEN / UNMEASURED** |
|
||||
| G3 leakage-free model evidence | RF-007, RF-008, RF-011 | Frozen site/session/device-disjoint report; all baseline and ablation rows present; nominal 80% interval coverage target is 75%-85%; weighted quantile loss target is at least 10% better than seasonal naive; every metric has a reproducer. | **OPEN / TARGETS UNMEASURED** |
|
||||
| G4 RuView shadow value | RF-006, RF-007, RF-008, RF-009 | At least 14 days shadow-only; empty-room false-alert target is at least 50% relative reduction without more than 2 percentage points occupied-room recall loss; no safety-critical action authority; drift and abstention reported by deployment. | **OPEN / TARGETS UNMEASURED** |
|
||||
| G5 deployment fitness | RF-004, RF-009, RF-010, RF-011, RF-012 | CPU p95 target is at most 1 second for 32 declared streams with peak process memory at most 4 GiB; signed model card/SBOM/provenance; privacy, security, trademark, dataset, and patent reviews; rollback drill; no open severity 1/2 issue. | **OPEN / TARGETS UNMEASURED** |
|
||||
|
||||
Failure at any gate keeps the model offline or shadow-only. Passing a software
|
||||
gate is not real-hardware evidence and does not convert an accuracy target into
|
||||
a measurement.
|
||||
|
||||
## Rollout and rollback
|
||||
|
||||
The only permitted progression is:
|
||||
|
||||
```text
|
||||
OFF -> OFFLINE_EVAL -> SHADOW -> ADVISORY
|
||||
```
|
||||
|
||||
This ADR does not authorize autonomous action. Each transition records actor,
|
||||
old/new mode, artifact and configuration digests, evidence report, and reason.
|
||||
Regression, provenance failure, calibration drift, security incident, or
|
||||
licensing uncertainty returns immediately to `OFF` or `SHADOW` while observed
|
||||
RuView sensing continues unchanged.
|
||||
|
||||
Rollback deactivates the forecast artifact atomically, clears ephemeral model
|
||||
and retrieval state, preserves signed aggregate evidence and incident records,
|
||||
and never downgrades or rewrites the raw observation stream.
|
||||
|
||||
## Security and privacy
|
||||
|
||||
Forecasting can infer routines from occupancy and movement even without raw
|
||||
CSI. Feature windows, neighbour identifiers, forecasts, and explanations are
|
||||
therefore deployment data subject to purpose limitation, tenant isolation,
|
||||
retention, deletion, access control, and audit.
|
||||
|
||||
Model artifacts, feature schemas, calibration, indexes, and manifests are
|
||||
untrusted until their signature and digest verify. Cardinality, dimensions,
|
||||
horizon, context, allocations, execution time, and concurrent requests are
|
||||
bounded. Metrics use allowlisted aggregate values and exclude raw CSI, precise
|
||||
room coordinates, persistent person identifiers, and unrestricted feature
|
||||
payloads.
|
||||
|
||||
The detailed authoring, data, AI-tool, incident, trademark, and patent controls
|
||||
are normative in
|
||||
[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- RuView gains a provider-neutral predictive evidence primitive with explicit
|
||||
uncertainty and abstention.
|
||||
- Rust-native inference can be evaluated on CPU without requiring a Python or
|
||||
cloud runtime in production.
|
||||
- RuVector retrieval becomes a measurable ablation rather than an implicit
|
||||
memory claim.
|
||||
- Signed provenance supports reproducible Linux and hosted training.
|
||||
|
||||
### Negative
|
||||
|
||||
- Independent training needs substantial diverse, correctly licensed temporal
|
||||
data and untouched deployment holdouts.
|
||||
- Strict source separation and manifests increase contributor and review cost.
|
||||
- A plausible forecast can make weak sensing look more authoritative unless
|
||||
downstream evidence labels remain intact.
|
||||
- Clean-room development does not remove patent, trademark, privacy, or dataset
|
||||
risk.
|
||||
|
||||
### Neutral
|
||||
|
||||
- This ADR does not repair an inaccurate upstream presence or pose estimator.
|
||||
- It does not approve TimesFM 3 for RuView use.
|
||||
- It does not select a final model size, training budget, or hosted provider.
|
||||
- No production checkpoint or measured benchmark is created by this decision.
|
||||
|
||||
## References
|
||||
|
||||
- [TimesFM 3 public research article](https://research.google/blog/timesfm-3-a-zero-shot-foundation-model-for-multivariate-forecasting/)
|
||||
- [Google TimesFM repository and source-license notice](https://github.com/google-research/timesfm)
|
||||
- [TimesFM 3 checkpoint license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE)
|
||||
- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [17 U.S.C. section 102](https://www.copyright.gov/title17/92chap1.html)
|
||||
- [USPTO Patent Public Search](https://www.uspto.gov/patents/search/patent-public-search)
|
||||
- [USPTO comprehensive trademark clearance](https://www.uspto.gov/trademarks/search/comprehensive-clearance-search-similar-trademarks)
|
||||
- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md)
|
||||
- [RuView Forecast model-card template](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md)
|
||||
- [ADR-145](./ADR-145-ablation-eval-harness-privacy-leakage.md)
|
||||
- [ADR-282](./ADR-282-ruview-ecosystem-positioning.md)
|
||||
- [ADR-295](./ADR-295-source-provenance-state-machine.md)
|
||||
- [ADR-298](./ADR-298-model-release-sanity-gates.md)
|
||||
- [ADR-302](./ADR-302-out-of-distribution-detection.md)
|
||||
- [ADR-304](./ADR-304-evidence-engine.md)
|
||||
- [ADR-317](./ADR-317-benchmark-multi-domain-scorecard.md)
|
||||
- [ADR-318](./ADR-318-capability-certificates.md)
|
||||
- [ADR-319](./ADR-319-witness-chain.md)
|
||||
@@ -0,0 +1,231 @@
|
||||
# ADR-349: Governed local and fal.ai forecast training
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-09-01
|
||||
- **Deciders**: ruv
|
||||
- **Owners**: RuView forecast training, security, and release maintainers
|
||||
- **Tags**: forecasting, training, fal, rust, receipts, idempotency, cost-control
|
||||
- **Parent**: ADR-348
|
||||
- **Extends**: ADR-010, ADR-145, ADR-298, ADR-319, ADR-348
|
||||
- **Supersedes**: None
|
||||
|
||||
## Decision
|
||||
|
||||
RuView Forecast will use one Rust training engine behind two deliberately
|
||||
different request boundaries. The local Linux request may bind a governed,
|
||||
hash-addressed RuView JSONL shard. Hosted v1 accepts only a bounded,
|
||||
deterministic synthetic generator recipe. It cannot serialize a local
|
||||
`TrainSpec`, `DataPolicy`, path, dataset bytes, tenant/account/workspace/site/
|
||||
device/session identity, split receipt, or RuVector namespace.
|
||||
|
||||
The fal.ai path is a bounded execution adapter, not a remote shell. It may
|
||||
train the fixed synthetic profile and export fixed artifact kinds, but it may
|
||||
not choose an arbitrary command, executable, container, URL, environment
|
||||
variable, output path, or release action. A later real-data hosted path needs a
|
||||
new accepted ADR, provider/privacy review, and an operator-signed export grant;
|
||||
it is intentionally not implemented here.
|
||||
|
||||
Every hosted result is untrusted and quarantined until it is exported with a
|
||||
complete receipt, downloaded through the authenticated provider channel,
|
||||
re-hashed locally, scanned, and bound to the original request. Only the local
|
||||
release boundary may sign or activate it. Signing keys never enter fal.ai.
|
||||
|
||||
This ADR delegates dataset rights, clean-room evidence, model-card, patent,
|
||||
trademark, and production gates to [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md)
|
||||
and the [clean-room protocol](../security/ruview-forecast-clean-room.md).
|
||||
|
||||
**Evidence status:** typed local and hosted configuration, root-confined file
|
||||
verification, atomic fixed-kind artifact publication, and cooperative
|
||||
cancellation source surfaces exist. End-to-end local training and mock hosted
|
||||
transport are software-testable in this change. Real fal.ai execution,
|
||||
provider cancellation, cost reconciliation, quarantine promotion, and local
|
||||
signature verification remain **UNMEASURED and unapproved** until their named
|
||||
acceptance evidence exists.
|
||||
|
||||
## Context
|
||||
|
||||
The same training engine needs to run on a 128 GiB Linux workstation and on an
|
||||
optional hosted accelerator without creating two architecture lineages. A
|
||||
generic remote-command interface would allow request data to become executable
|
||||
authority, make cost difficult to bound, and weaken cancellation. Sending a
|
||||
general local request to fal would also expose governance and customer
|
||||
identifiers that the hosted smoke path does not need.
|
||||
|
||||
The initial `ruview-forecast-train` crate already separates three relevant
|
||||
modules:
|
||||
|
||||
| Module | Current contract |
|
||||
|---|---|
|
||||
| [`config.rs`](../../v2/crates/ruview-forecast-train/src/config.rs) | `TrainingRequest`, `ValidatedTrainingRequest`, bounded `JobId`, root-relative dataset path, exact size/SHA-256, named model profiles, typed CPU/CUDA choice, and bounded optimizer values; unknown fields are denied |
|
||||
| [`artifact.rs`](../../v2/crates/ruview-forecast-train/src/artifact.rs) | verified open dataset handle, root confinement, fixed `Model`/`Manifest`/`Receipt`/`Checkpoint` kinds, atomic writes, content descriptors, and conflict-on-different-bytes idempotency |
|
||||
| [`cancel.rs`](../../v2/crates/ruview-forecast-train/src/cancel.rs) | `Cancellation`, `CancelToken`, and cooperative cancellation checks for batch/checkpoint boundaries |
|
||||
|
||||
The training crate features keep `cli`, `server`, `fal-client`, `cpu`, and
|
||||
`cuda` explicit, with default features empty. WGPU remains a model-crate
|
||||
inference backend and is not advertised as a training runner. The `ruforecast`
|
||||
binary requires the `cli` feature. Source presence is not execution evidence.
|
||||
|
||||
## Typed execution contracts
|
||||
|
||||
The non-serializable trusted `TrainingRequest` is built locally from a
|
||||
size-bounded `LocalTrainingRequestWire`. It contains:
|
||||
|
||||
- stable job identity;
|
||||
- a constructor-validated local split/horizon/normalization `TrainSpec`;
|
||||
- either a root-relative dataset identity with exact byte count and SHA-256,
|
||||
or a deterministic synthetic recipe;
|
||||
- one allowlisted model capacity profile and compiled-in local backend;
|
||||
- bounded optimizer, checkpoint cadence, seed, memory, step, time, checkpoint,
|
||||
and artifact budgets.
|
||||
|
||||
Local JSON and TOML deny unknown fields. Dataset paths resolve below an
|
||||
operator-configured root, and the exact regular file remains open after
|
||||
size/hash verification to avoid replacement between validation and use. JSONL
|
||||
windows are decoded incrementally with per-line and per-window caps rather
|
||||
than loading the full shard into memory.
|
||||
|
||||
The distinct `HostedSyntheticRequestV1` contains only a schema version,
|
||||
idempotency/request digests, a fixed model profile, deterministic generator
|
||||
parameters and seed, bounded optimizer/resource caps, and public build
|
||||
identities. Its constructor needs a non-serializable core governance authority
|
||||
proving that the synthetic recipe is the approved hosted source. Hosted v1 has
|
||||
no external dataset field.
|
||||
|
||||
Neither request contains a shell string, argument vector, executable path,
|
||||
container/image selector, package installer, arbitrary URL, callback URL,
|
||||
environment map, secret, source fragment, or unrestricted output name.
|
||||
|
||||
The local client maps `HostedSyntheticRequestV1` to one allowlisted,
|
||||
process-configured fal app and pre-deployed content-addressed worker image. The
|
||||
caller cannot override the app, machine, worker entry point, or artifact
|
||||
destination. The endpoint returns typed status or artifact descriptors only.
|
||||
Provider paths are derived from validated job identity and fixed artifact
|
||||
kinds, then downloaded through the authenticated fal Platform Files API; they
|
||||
are not accepted as remote URLs or local paths.
|
||||
|
||||
The deploy wrapper builds from an allowlisted `git archive`, selects the exact
|
||||
`fal_app.py::run_server` symbol, and passes `--auth private` for both `fal run`
|
||||
and `fal deploy`; ephemeral Fal runs otherwise default to public. Hosted v1
|
||||
retains only bounded synthetic request/result metadata so its typed queue result
|
||||
can be reconciled. It sets `X-Fal-No-Retry: 1` and a bounded timeout rather than
|
||||
claiming that `X-Fal-Store-IO: 0` or an unverified lifecycle header protects
|
||||
the result. A live unauthenticated 401/403 probe and provider retention/deletion
|
||||
reconciliation remain blocking operational evidence.
|
||||
|
||||
## Idempotency and cost authority
|
||||
|
||||
The local effective idempotency identity is:
|
||||
|
||||
```text
|
||||
job_id + canonical request digest + source commit + lockfile digest
|
||||
+ container digest + dataset/split digest + initial-weight digest
|
||||
```
|
||||
|
||||
Hosted v1 replaces `dataset/split digest` with the canonical synthetic-recipe
|
||||
digest. No local governance identifier is part of, or derivable from, the
|
||||
hosted payload.
|
||||
|
||||
Repeating an identical completed request returns the existing verified receipt
|
||||
and descriptors. Reusing `job_id` with any different governed input fails as a
|
||||
conflict. A retry after an ambiguous network failure must first query the same
|
||||
provider job; it must not silently create a second billable run.
|
||||
|
||||
Before hosted submission, the caller sets explicit maximum steps, wall-clock
|
||||
time, memory, checkpoint count, export bytes, and billable units. Submission
|
||||
also requires an explicit local spend-approval record bound to the request
|
||||
digest and maximum units. The adapter rejects a request when its configured
|
||||
cap exceeds that approval. The final receipt records estimated and actual
|
||||
provider units/cost when the provider supplies them, the price source/time,
|
||||
and whether the amount is final or provider-reported. A provider estimate is
|
||||
`CLAIMED`, never `MEASURED`, until reconciled against a bill.
|
||||
|
||||
No automatic retry may increase the approved budget. Budget changes create a
|
||||
new signed authorization record. The initial wire carries operator ceilings
|
||||
for wall time, billable seconds, and micro-USD, but it does not yet bind a
|
||||
provider price quote or reconcile a final provider ledger. Monetary enforcement
|
||||
therefore remains an operator/Fal-account control and the production hosted
|
||||
spend gate stays open.
|
||||
|
||||
## Cancellation and checkpoints
|
||||
|
||||
Cancellation is cooperative and idempotent:
|
||||
|
||||
1. Local signal or authenticated hosted cancel marks one `job_id` cancelled.
|
||||
2. Training checks `Cancellation` at every batch boundary and before/after each
|
||||
checkpoint/export boundary.
|
||||
3. A cancellation checkpoint is committed atomically when safe and within the
|
||||
artifact budget; a partial file never becomes a valid artifact.
|
||||
4. The terminal receipt records `cancelled`, the last completed epoch/step,
|
||||
checkpoint digest, provider state, elapsed resource units, and final known
|
||||
cost.
|
||||
5. Repeating cancel returns the same terminal state.
|
||||
|
||||
Cancellation does not promise immediate GPU termination. Cancellation latency,
|
||||
checkpoint durability, and residual provider billing are **UNMEASURED** until a
|
||||
real fal.ai run supplies a receipt. A timeout or lost cancellation response
|
||||
keeps the job and any output quarantined.
|
||||
|
||||
## Export, quarantine, and local signing
|
||||
|
||||
The worker may emit only the fixed artifact set from `ArtifactKind`:
|
||||
|
||||
| Kind | Required purpose |
|
||||
|---|---|
|
||||
| `Model` | Burn model record bytes |
|
||||
| `Manifest` | architecture, data/split, seed, build, and clean-room identity |
|
||||
| `Receipt` | request, environment, metrics, cost, status, and export lineage |
|
||||
| `Checkpoint` | final or cancellation-time model weights; v1 does not claim optimizer/cursor resume |
|
||||
|
||||
Every descriptor includes kind, size, and SHA-256. Export acceptance requires
|
||||
all mandatory descriptors, bounded lengths, exact hashes, the expected job and
|
||||
request digest, no duplicate kind, and a terminal status consistent with the
|
||||
artifact set.
|
||||
|
||||
Downloaded files enter a non-executable quarantine outside the model search
|
||||
path. Local verification repeats envelope/schema/size/hash checks, SBOM and
|
||||
malware/policy scans, clean-room declarations, dataset/split lineage, metric
|
||||
labels, and parent-checkpoint validation. No provider output may update a
|
||||
symlink, current-model pointer, RuVector index, sensing server, or release tag.
|
||||
|
||||
After quarantine passes, an authorized local signer binds the exact export
|
||||
receipt and artifact digests to an `ArtifactReceipt` and release manifest. The
|
||||
signature records algorithm, public key ID, signer capability, and time. Fal.ai
|
||||
receives no private signing material and cannot promote its own output. The
|
||||
current core supplies canonical digests and receipts, not a complete release
|
||||
signature implementation; signature-backed activation remains open.
|
||||
|
||||
## Requirements and acceptance
|
||||
|
||||
| ID | Requirement | ADR-348 gate | Acceptance evidence | Current state |
|
||||
|---|---|---|---|---|
|
||||
| FT-001 | Local and hosted adapters invoke the same model/trainer implementation while using intentionally separate local-data and hosted-synthetic request schemas. | G1 | Shared-engine tests plus hosted DTO exclusion and recipe-digest binding tests | **OPEN / UNMEASURED** |
|
||||
| FT-002 | No request field or endpoint can select arbitrary code, command, image, URL, environment, or output path. | G2 | Schema negatives, endpoint capability test, dependency review | **OPEN / UNMEASURED** |
|
||||
| FT-003 | Job/request idempotency prevents duplicate execution and conflicts on changed governed input. | G1, G5 | Concurrent/retry/lost-response tests with provider job query | **OPEN / UNMEASURED** |
|
||||
| FT-004 | Hosted execution has explicit non-escalating resource and monetary caps with estimated/actual cost receipts. | G5 | Over-budget rejection and reconciled provider-bill fixture plus real-run receipt | **OPEN / UNMEASURED** |
|
||||
| FT-005 | Cancellation is authenticated, cooperative, idempotent, checkpoint-safe, and terminally receipted. | G2, G5 | Local property tests and a real hosted cancellation drill | **OPEN / UNMEASURED** |
|
||||
| FT-006 | Export is fixed-kind, bounded, complete, hash-verified, and bound to request/job/environment. | G1, G2 | Missing/duplicate/truncated/tampered export tests | **OPEN / UNMEASURED** |
|
||||
| FT-007 | Hosted outputs remain quarantined until local verification and local-only signing succeed. | G1, G5 | Promotion-denial tests, signing-key absence check, rollback drill | **OPEN / UNMEASURED** |
|
||||
| FT-008 | Hosted v1 receives synthetic inputs only; provider credentials, logs, retention, region, output reuse, and deletion satisfy ADR-348 privacy/security approval before any real run. | G1, G5 | Hosted DTO exclusion tests, provider review, and deletion/export receipts | **OPEN / UNMEASURED** |
|
||||
|
||||
No successful unit test closes a hosted gate. G5 requires one bounded real
|
||||
fal.ai run, one cancellation drill, one ambiguous-retry/idempotency drill, cost
|
||||
reconciliation, quarantine rejection of a tampered export, and local signature
|
||||
verification over the accepted artifact. Until then fal.ai support is a typed
|
||||
software surface, not an operational capability claim.
|
||||
|
||||
## Consequences
|
||||
|
||||
The design gives local and hosted training one auditable lineage and keeps
|
||||
provider output below local release authority. It adds receipt, budget,
|
||||
quarantine, and signing work, and cooperative cancellation may still incur
|
||||
provider cost. If a hosted provider cannot support bounded idempotent status,
|
||||
export, cancellation, and deletion, the Linux path remains the only approved
|
||||
training environment.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md)
|
||||
- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md)
|
||||
- [`ruview-forecast-train` manifest](../../v2/crates/ruview-forecast-train/Cargo.toml)
|
||||
- [`ruview-forecast-core` receipts](../../v2/crates/ruview-forecast-core/src/receipt.rs)
|
||||
- [`ruview-forecast-model` public artifact boundary](../../v2/crates/ruview-forecast-model/src/lib.rs)
|
||||