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 | ||
|
|
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 |
No files matched your search
@@ -204,7 +204,10 @@ jobs:
|
||||
node-version: '22'
|
||||
|
||||
- name: Run UI unit tests
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
|
||||
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs ui/services/sensing.service.test.mjs ui/components/DashboardTab.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
|
||||
|
||||
- name: Run virtual-view adapter boundary tests
|
||||
run: node --test integrations/virtual-view/adapter.test.mjs
|
||||
|
||||
# Unit and Integration Tests
|
||||
# Python pytest matrix — runs against the archived v1 Python tree.
|
||||
@@ -501,7 +504,7 @@ jobs:
|
||||
notify:
|
||||
name: Notify
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, test, rust-tests, performance-test, docker-build, docs]
|
||||
needs: [code-quality, test, rust-tests, ui-tests, performance-test, docker-build, docs]
|
||||
if: always()
|
||||
permissions:
|
||||
contents: write # required by softprops/action-gh-release
|
||||
@@ -528,18 +531,22 @@ jobs:
|
||||
text: '❌ CI pipeline failed for ${{ github.ref }}'
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'
|
||||
if: github.ref == 'refs/heads/main' && needs.rust-tests.result == 'success' && needs.ui-tests.result == 'success' && needs.docker-build.result == 'success'
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
|
||||
with:
|
||||
tag_name: v${{ github.run_number }}
|
||||
name: Release v${{ github.run_number }}
|
||||
name: CI snapshot v${{ github.run_number }}
|
||||
# CI snapshots must not replace a curated product release as Latest.
|
||||
make_latest: false
|
||||
body: |
|
||||
Automated release from CI pipeline
|
||||
Source snapshot validated by the CI pipeline. This entry does not contain firmware bundles.
|
||||
|
||||
**Changes:**
|
||||
${{ github.event.head_commit.message }}
|
||||
|
||||
**Docker Image:**
|
||||
`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}`
|
||||
**Source commit:** `${{ github.sha }}`
|
||||
|
||||
Container images publish separately through the [sensing-server release workflow](https://github.com/ruvnet/RuView/actions/workflows/sensing-server-docker.yml).
|
||||
Firmware downloads are attached to the dedicated `vX.Y.Z-esp32` releases.
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -16,6 +16,17 @@ on:
|
||||
- '.github/workflows/firmware-ci.yml'
|
||||
|
||||
jobs:
|
||||
provisioning-tests:
|
||||
name: Provisioning tooling tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Verify provisioning and secret handling without hardware
|
||||
run: python -m unittest discover -s firmware/esp32-csi-node/tests -p 'test_provision*.py' -v
|
||||
|
||||
version-guard:
|
||||
name: Verify version.txt matches release tag
|
||||
runs-on: ubuntu-latest
|
||||
@@ -119,6 +130,10 @@ jobs:
|
||||
fi
|
||||
echo "Binary size OK: $SIZE <= $MAX"
|
||||
|
||||
- name: Verify built application version before packaging
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: python test/test_delivery_contract.py --built-app build/esp32-csi-node.bin
|
||||
|
||||
- name: Verify flash image integrity
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
@@ -208,7 +223,7 @@ jobs:
|
||||
publish-release-assets:
|
||||
name: Publish firmware release assets
|
||||
if: github.ref_type == 'tag'
|
||||
needs: [version-guard, build, build-c6-idf6]
|
||||
needs: [version-guard, provisioning-tests, build, build-c6-idf6]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -232,7 +247,12 @@ jobs:
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
|
||||
with:
|
||||
name: ESP32 firmware ${{ github.ref_name }}
|
||||
prerelease: true
|
||||
make_latest: false
|
||||
body: |
|
||||
Exact-run ESP32-S3 8 MB, ESP32-S3 4 MB, and ESP32-C6 4 MB firmware bundles.
|
||||
Exact-run ESP32-S3 display 8 MB, ESP32-S3 headless 4 MB, and ESP32-C6 4 MB firmware bundles.
|
||||
Verify each archive against `SHA256SUMS.txt` before flashing.
|
||||
CI builds and host tests verify software behavior, not target hardware.
|
||||
This prerelease has no new physical-board qualification or sensing-accuracy claim.
|
||||
Select the matching board configuration using firmware/esp32-csi-node/README.md.
|
||||
files: release-assets/*
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
|
||||
@@ -322,3 +322,6 @@ v2/crates/ruview-offaxis/pkg/
|
||||
scripts/fal-visual-teacher/training-data/
|
||||
scripts/fal-visual-teacher/training-data.zip
|
||||
scripts/fal-visual-teacher/output/
|
||||
|
||||
# Trained adaptive classifier (auto-created at runtime; #2135)
|
||||
/v2/data/adaptive_model.json
|
||||
@@ -7,6 +7,19 @@ 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`:
|
||||
|
||||
@@ -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.
|
||||
@@ -6,6 +6,27 @@
|
||||
</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://ruos.cognitum.one">
|
||||
<img src="assets/ruos-animated.svg" alt="RuView — WiFi becomes spatial awareness, with Ruflo coordination and a ruOS sensing workspace" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## **See through walls with WiFi** ##
|
||||
|
||||
@@ -21,6 +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
|
||||
@@ -37,6 +60,22 @@ 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>
|
||||
|
||||
@@ -127,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).
|
||||
@@ -141,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:
|
||||
@@ -254,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>
|
||||
@@ -308,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>
|
||||
@@ -482,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:
|
||||
|
||||
```
|
||||
@@ -606,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>
|
||||
@@ -735,6 +785,8 @@ For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.9.1`. The quick c
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
[](docs/user-guide.md)
|
||||
|
||||
Start with the user, build, and calibration guides; expand for the full reference map.
|
||||
|
||||
<details>
|
||||
@@ -776,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.
|
||||
|
||||
@@ -20,6 +20,10 @@ func main() {
|
||||
exit(1)
|
||||
}
|
||||
let sample: [String: Any] = [
|
||||
// Always the connected link. macOS redacts ssid/bssid without
|
||||
// Location Services; rssi/noise/channel stay real, so mark the
|
||||
// sample as the connected link for the Rust adapter.
|
||||
"connected": true,
|
||||
"ssid": interface.ssid() ?? "",
|
||||
"bssid": interface.bssid() ?? "00:00:00:00:00:00",
|
||||
"channel": channel.channelNumber,
|
||||
|
||||
@@ -0,0 +1,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 |
@@ -37,6 +37,31 @@ services:
|
||||
# volumes: ["/path/to/models:/app/models"]
|
||||
# MODELS_DIR=/app/models
|
||||
- MODELS_DIR=${MODELS_DIR:-data/models}
|
||||
# Auth (issue #864). The entrypoint refuses to start (exit 64) unless
|
||||
# RUVIEW_API_TOKEN is set. Export it before `docker compose up`:
|
||||
# export RUVIEW_API_TOKEN=$(openssl rand -hex 32)
|
||||
# Bare names pass the host value through only when it is set.
|
||||
- RUVIEW_API_TOKEN
|
||||
# ESP32 UDP ingest (ADR-296, issue #2089). The UDP receiver defaults to
|
||||
# 127.0.0.1, which cannot receive from the published 5005/udp port.
|
||||
# Set ONE source guard and the entrypoint binds 0.0.0.0:
|
||||
# RUVIEW_UDP_ALLOW=<node-ip-or-cidr> restrict sources (preferred)
|
||||
# RUVIEW_UDP_INSECURE_LAN=true accept the spoofing risk
|
||||
# Docker Desktop and OrbStack on macOS rewrite the source address to the
|
||||
# bridge gateway, so a LAN-subnet allowlist drops every frame. There,
|
||||
# allow the gateway instead:
|
||||
# docker network inspect <project>_default \
|
||||
# --format '{{(index .IPAM.Config 0).Gateway}}'
|
||||
# RUVIEW_UDP_ALLOW=<gateway>/32
|
||||
# The gateway admits anything that reaches the published port, so keep
|
||||
# 5005/udp off untrusted networks.
|
||||
- RUVIEW_UDP_ALLOW
|
||||
- RUVIEW_UDP_INSECURE_LAN
|
||||
- RUVIEW_UDP_BIND
|
||||
# Browsing the UI from another machine also needs the host name or IP
|
||||
# in SENSING_ALLOWED_HOSTS (otherwise 421), and the TCP ports above
|
||||
# published beyond 127.0.0.1.
|
||||
- SENSING_ALLOWED_HOSTS
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
# Default is `auto`. Set CSI_SOURCE=simulated when you want
|
||||
# fake data tagged as such; never set it implicitly.
|
||||
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
|
||||
# RUVIEW_UDP_ALLOW / RUVIEW_UDP_INSECURE_LAN — ADR-296 UDP source guard.
|
||||
# Setting either makes the entrypoint bind UDP on 0.0.0.0 so
|
||||
# ESP32 frames on the published 5005/udp port can arrive.
|
||||
# RUVIEW_UDP_BIND — explicit UDP bind; overrides the choice above.
|
||||
set -e
|
||||
|
||||
# ── Issue #864: fail-closed on default posture ───────────────────────────────
|
||||
@@ -34,8 +38,8 @@ set -e
|
||||
#
|
||||
# Escape hatches (operator must opt in explicitly):
|
||||
# * Set RUVIEW_API_TOKEN to a strong secret → auth enabled on /api/v1/*.
|
||||
# * Set RUVIEW_ALLOW_UNAUTHENTICATED=1 → preserves the pre-fix behaviour;
|
||||
# only safe on an isolated trust boundary.
|
||||
# * Set RUVIEW_ALLOW_UNAUTHENTICATED=1 (or true/yes/on, any case) →
|
||||
# preserves the pre-fix behaviour; only safe on an isolated trust boundary.
|
||||
# * Set RUVIEW_BIND_ADDR to a loopback / private interface → unauth is fine
|
||||
# when the socket isn't reachable. The auto-bind nudges toward 127.0.0.1.
|
||||
#
|
||||
@@ -45,7 +49,12 @@ set -e
|
||||
case "${1:-}" in
|
||||
cog-ha-matter|ha-matter|homecore|homecore-server) ;;
|
||||
*)
|
||||
if [ -z "${RUVIEW_API_TOKEN:-}" ] && [ "${RUVIEW_ALLOW_UNAUTHENTICATED:-}" != "1" ]; then
|
||||
# Same spellings the sensing-server accepts for its boolean env flags.
|
||||
case "${RUVIEW_ALLOW_UNAUTHENTICATED:-}" in
|
||||
1|[Tt][Rr][Uu][Ee]|[Yy][Ee][Ss]|[Oo][Nn]) __allow_unauth=1 ;;
|
||||
*) __allow_unauth=0 ;;
|
||||
esac
|
||||
if [ -z "${RUVIEW_API_TOKEN:-}" ] && [ "$__allow_unauth" != "1" ]; then
|
||||
# If the operator hasn't overridden the bind, refuse outright on
|
||||
# the default 0.0.0.0. If they've nailed it to loopback (or a
|
||||
# specific private address they trust), let it run.
|
||||
@@ -97,6 +106,50 @@ esac
|
||||
# server binary so users can just pass flags:
|
||||
# docker run <image> --source esp32 --tick-ms 500
|
||||
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
|
||||
# ── Issue #2089: UDP CSI ingest posture inside a container ───────────────
|
||||
# ADR-296 defaults the UDP receiver to 127.0.0.1. Inside a container that
|
||||
# bind can never receive from a published port, so no node ever appears.
|
||||
# We bind 0.0.0.0 only when the operator has configured an ADR-296 guard
|
||||
# (RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN); without one the server
|
||||
# would refuse a routable bind, so we keep loopback and say why instead.
|
||||
# An explicit RUVIEW_UDP_BIND or --udp-bind always wins.
|
||||
# The binary parses RUVIEW_UDP_INSECURE_LAN itself (1/yes/on/true, any
|
||||
# case); here we only need to know whether it is truthy to pick the bind.
|
||||
__insecure_lan=0
|
||||
case "$(printf '%s' "${RUVIEW_UDP_INSECURE_LAN:-}" | tr 'A-Z' 'a-z')" in
|
||||
1|true|yes|on) __insecure_lan=1 ;;
|
||||
esac
|
||||
__udp_bind_arg=0
|
||||
__udp_guard_arg=0
|
||||
for __arg in "$@"; do
|
||||
case "$__arg" in
|
||||
--udp-bind|--udp-bind=*) __udp_bind_arg=1 ;;
|
||||
--udp-allow|--udp-allow=*|--udp-insecure-lan) __udp_guard_arg=1 ;;
|
||||
esac
|
||||
done
|
||||
if [ -z "${RUVIEW_UDP_BIND:-}" ] && [ "$__udp_bind_arg" = 0 ]; then
|
||||
if [ -n "${RUVIEW_UDP_ALLOW:-}" ] || [ "$__insecure_lan" = 1 ] \
|
||||
|| [ "$__udp_guard_arg" = 1 ]; then
|
||||
export RUVIEW_UDP_BIND=0.0.0.0
|
||||
echo "[entrypoint] UDP CSI receiver: binding 0.0.0.0 inside the container" >&2
|
||||
echo "[entrypoint] (a source guard is set: RUVIEW_UDP_ALLOW or RUVIEW_UDP_INSECURE_LAN)." >&2
|
||||
else
|
||||
case "${CSI_SOURCE:-auto}" in
|
||||
simulated|simulate) ;;
|
||||
*)
|
||||
echo "[entrypoint] NOTE: UDP CSI receiver stays on 127.0.0.1 (ADR-296 default)." >&2
|
||||
echo "[entrypoint] Inside a container that bind cannot receive ESP32 frames from" >&2
|
||||
echo "[entrypoint] a published 5005/udp port, so no node will appear. To ingest:" >&2
|
||||
echo "[entrypoint] -e RUVIEW_UDP_ALLOW=<node-ip-or-cidr> (restrict sources), or" >&2
|
||||
echo "[entrypoint] -e RUVIEW_UDP_INSECURE_LAN=true (accept spoofing risk)" >&2
|
||||
echo "[entrypoint] Docker Desktop/OrbStack on macOS rewrite the source to the bridge" >&2
|
||||
echo "[entrypoint] gateway, so the allowlist must name <gateway>/32, not the LAN subnet." >&2
|
||||
echo "[entrypoint] See https://github.com/ruvnet/RuView/issues/2089" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
set -- /app/sensing-server \
|
||||
--source "${CSI_SOURCE:-auto}" \
|
||||
--tick-ms 100 \
|
||||
|
||||
@@ -6,7 +6,9 @@ Known issues and fixes from the rebase-to-upstream branch (upstream #301).
|
||||
|
||||
## 1. Node not appearing in /api/v1/nodes
|
||||
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/spatial/nodes`.
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/nodes`.
|
||||
|
||||
**Check the server first:** the UDP receiver listens on `127.0.0.1` by default (ADR-296), so a natively run `sensing-server` never hears nodes on your network unless it was started with `--udp-bind 0.0.0.0 --udp-allow <node-ip-or-cidr>` (or `--udp-insecure-lan`). The startup log line for the UDP receiver says `loopback-only` when this is the problem. For Docker, see "Receiving ESP32 frames in Docker" in the [user guide](user-guide.md).
|
||||
|
||||
**Root cause:** After USB flash, the node enters a limping state where WiFi associates but the UDP CSI sender silently fails. The SoftAP + mDNS stack initializes but the CSI callback never fires.
|
||||
|
||||
@@ -145,18 +147,18 @@ ESP32 nodes still target the host on `--target-ip <host>:5005` — no firmware r
|
||||
|
||||
**Symptom:** `sensing-server` starts cleanly, logs `HTTP server listening on http://localhost:3000`, but loading `http://localhost:3000/` (or `/ui/index.html`) returns `404 Not Found`. Reported in #188.
|
||||
|
||||
**Root cause:** The default `--ui-path ../../ui` is resolved relative to the binary's *current working directory*, not the binary location. When the binary is launched from anywhere other than `crates/wifi-densepose-sensing-server/`, the relative path doesn't reach the UI assets and Axum's static file handler returns 404.
|
||||
**Root cause:** The default `--ui-path ../ui` is resolved relative to the *current working directory*, not the binary location. If it doesn't exist the server also tries `./ui` and `../../ui`. Launching from `v2/` works; from anywhere else (including `v2/crates/wifi-densepose-sensing-server/`) those paths miss the repo's UI, or hit some other `ui` directory, and Axum's static file handler returns 404.
|
||||
|
||||
**Fix:** Pass an absolute UI path, run the binary from the crate directory, or use the Docker image (which bundles the UI under `/app/ui`).
|
||||
**Fix:** Pass an absolute UI path, run the binary from `v2/`, or use the Docker image (which bundles the UI under `/app/ui`).
|
||||
|
||||
```bash
|
||||
# Option A — absolute path (recommended for production)
|
||||
sensing-server --source esp32 --udp-port 5005 --http-port 3000 \
|
||||
--ws-port 3001 --ui-path /absolute/path/to/ui
|
||||
|
||||
# Option B — run from the crate dir (works for local dev / cargo run)
|
||||
cd v2/crates/wifi-densepose-sensing-server
|
||||
cargo run -- --source esp32
|
||||
# Option B — run from v2/ (works for local dev / cargo run)
|
||||
cd v2
|
||||
cargo run -p wifi-densepose-sensing-server -- --source esp32
|
||||
|
||||
# Option C — Docker (no path config needed)
|
||||
docker compose -f docker/docker-compose.yml up sensing-server
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-03-01 |
|
||||
| **Updated** | 2026-10-01: macOS 27 also redacts SSID without Location Services; the connected link is kept with a synthetic id, and `auto`+wifi keeps UDP bound (see Amendment 1) |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **ORCA** — OS-native Radio Channel Acquisition |
|
||||
| **Relates to** | ADR-013 (Feature-Level Sensing Commodity Gear), ADR-022 (Windows WiFi Enhanced Fidelity), ADR-014 (SOTA Signal Processing), ADR-018 (ESP32 Dev Implementation) |
|
||||
@@ -313,3 +314,22 @@ All verification on Mac Mini (M2 Pro, macOS 26.3).
|
||||
- ADR-022: Windows WiFi Enhanced Fidelity (analogous platform adapter)
|
||||
- ADR-013: Feature-Level Sensing from Commodity Gear
|
||||
- Issue [#56](https://github.com/ruvnet/wifi-densepose/issues/56): macOS support request
|
||||
|
||||
---
|
||||
|
||||
## Amendment 1 (2026-10-01): SSID is also redacted; keep the connected link
|
||||
|
||||
**Observed on macOS 27 (M-series Mac, CoreWLAN, unsigned CLI helper).** Without Location Services, `mac_wifi --scan-once` returns `"ssid":""` and `"bssid":"00:00:00:00:00:00"`, while `rssi`, `noise`, `channel` and `tx_rate` are real. The §1 assumption that "SSID + RSSI + channel" survive no longer holds. An unsigned CLI helper never calls `CLLocationManager`, so it never appears in Location Services and cannot be granted access.
|
||||
|
||||
Before this amendment, `resolve_bssid` abstained when both identities were blank (to avoid merging unrelated networks), so every macOS scan was dropped and `--source wifi` produced nothing, with no error.
|
||||
|
||||
**Change.**
|
||||
1. `--scan-once` marks its single sample `"connected": true`. It is always the one connected link, so it cannot collide with another network.
|
||||
2. A redacted sample marked `connected` gets a stable, locally administered synthetic BSSID derived from a reserved key plus the channel. The SSID stays empty; none is invented. Unmarked redacted lines still abstain.
|
||||
3. `--source auto` that resolves to host WiFi now also binds the UDP receiver, and `wifi_task` yields while the source is a live `esp32`. ESP32 nodes that start after the 2 s boot probe still promote the source, matching the #1004 rule for the simulator.
|
||||
4. A missing helper logs one clear error with the build command instead of a warning on every tick.
|
||||
|
||||
**Measured (2026-10-01, this change):** `--source wifi` → source `wifi:unnamed`, sensing ticks advance. `--source auto` on a quiet port → `wifi` with `udp_receiver=true`; relaying real ADR-018 frames promoted the source to `esp32`, with all 5 nodes listed.
|
||||
|
||||
**Still open:** real BSSID/SSID would need a signed `.app` helper with `NSLocationWhenInUseUsageDescription` that requests authorization. The helper still lives under `archive/v1` with no build step.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-03-02 |
|
||||
| **Updated** | 2026-10-04 — multistatic fusion combines phase across nodes only when every frame declares the same shared phase reference; independent ESP32 nodes and the amplitude-only server bridge fuse amplitude only and report the reason (`phase_fusion` in `/health/ready`, issue #1752). |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RuvSense** -- RuVector-Enhanced Sensing for Multistatic Fidelity |
|
||||
| **Relates to** | ADR-012 (ESP32 Mesh), ADR-014 (SOTA Signal Processing), ADR-016 (RuVector Training), ADR-017 (RuVector Signal+MAT), ADR-018 (ESP32 Implementation), ADR-024 (AETHER Embeddings), ADR-026 (Survivor Track Lifecycle), ADR-027 (MERIDIAN Generalization) |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | **Accepted** (MQTT track P1–P7 + P8a + P9 + P10 shipped 2026-05-23 in PR #778, 410 lib tests, witness bundle VERIFIED) / **Proposed** (Matter SDK wiring P8b deferred to v0.7.1 per §9.10) |
|
||||
| **Date** | 2026-05-23 |
|
||||
| **Date** | 2026-05-23 (amended 2026-10-02: P4.5 status note, §11) |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **HA-DISCO** (MQTT) + **HA-FABRIC** (Matter) + **HA-MIND** (semantic primitives) |
|
||||
| **Relates to** | ADR-018 (CSI binary frame format), ADR-021 (ESP32 vitals), ADR-031 (RuView sensing-first), ADR-039 (edge vitals packet 0xC511_0002), ADR-079 (camera ground-truth), ADR-103 (cog-person-count), ADR-110 (ESP32-C6 firmware), ADR-114 (cog-quantum-vitals) |
|
||||
@@ -440,7 +440,7 @@ This means: **adding a new primitive is one file change**. No MQTT schema rev, n
|
||||
| **P2** | HA discovery message emitter. New module `mqtt_discovery.rs`. Emits all entity `config` topics on connect + every `--mqtt-refresh-secs`. Schema-validated against HA's published JSON schema. | pending |
|
||||
| **P3** | State publication. Subscribe to internal `tokio::broadcast` channel (the one `tx.send(json)` writes to on line 3983 of `main.rs`). Translate `edge_vitals` / `sensing_update` / `pose_data` messages into per-entity state payloads. Apply rate-limit + privacy-mode filters. | pending |
|
||||
| **P4** | Integration tests: dockerised mosquitto in CI (extend `.github/workflows/firmware-qemu.yml` pattern), schema-validate every emitted config against HA's `homeassistant/components/mqtt` JSON schemas (pin to a tested HA version). Add a smoke test that brings up sensing-server in `--source mock --mqtt`, subscribes with `paho-mqtt` test client, asserts on entity creation. | pending |
|
||||
| **P4.5** | **Semantic inference layer (HA-MIND).** New module `semantic_inference.rs` implementing the 10 v1 primitives from §3.12. Output broadcast channel consumed by both MQTT publisher (P3) and Matter bridge (P8). Per-primitive precision/recall baselines published to `docs/integrations/semantic-primitives-metrics.md`. Unit tests per FSM + integration tests via replay of ADR-079 paired captures. | pending |
|
||||
| **P4.5** | **Semantic inference layer (HA-MIND).** New module `semantic_inference.rs` implementing the 10 v1 primitives from §3.12. Output broadcast channel consumed by both MQTT publisher (P3) and Matter bridge (P8). Per-primitive precision/recall baselines published to `docs/integrations/semantic-primitives-metrics.md`. Unit tests per FSM + integration tests via replay of ADR-079 paired captures. | partial — 6 of 10 primitives on the live MQTT publisher; see §11 |
|
||||
| **P5** | Docs: new `docs/integrations/home-assistant.md` with screenshots of the HA UI after auto-discovery completes, example HA dashboard YAML (Lovelace card configs), 8 starter blueprints from §3.12.2 (distress notify, wake routine, hallway dim, elderly anomaly alert, meeting lights, bathroom fan, fall-risk escalate, auto-arm security), and the raw-channel example automations: "turn on hall light when presence ON", "send notification on fall_detected event", "log HR/BR to InfluxDB". | pending |
|
||||
| **P6** | Ship `--mqtt` in the next sensing-server release (target: v0.7.0). Demo end-to-end on `cognitum-v0` against a Mosquitto add-on running on a Home Assistant OS install. Update README hardware-options table with "Works with Home Assistant" badge. | pending |
|
||||
| **P7** | Matter Bridge spike: build a throwaway prototype with `matter-rs` exposing one `OccupancySensor` endpoint + one `GenericSwitch` for fall. Pair against Apple Home, Google Home, and HA's Matter integration. Decision gate: if pairing works on all three, proceed to P8; if blocked, switch to chip-tool FFI and re-spike. | pending |
|
||||
@@ -667,4 +667,57 @@ Empty as of 2026-05-23. New questions discovered during implementation will be f
|
||||
|
||||
---
|
||||
|
||||
## 11. Status note: P4.5 semantic states on the live publisher (2026-10-02)
|
||||
|
||||
Issues [#2085](https://github.com/ruvnet/RuView/issues/2085) and [#2093](https://github.com/ruvnet/RuView/issues/2093) found that discovery announced 20 entities per device while only 6 ever received state, that `presence/state` was published on every CSI frame, and that HA device ids changed on every restart. The FSMs in `crate::semantic` existed but had no caller outside their tests. This note records what is now wired and what is not.
|
||||
|
||||
### 11.1 What changed
|
||||
|
||||
- **Semantic primitives run on the live path.** `mqtt::planner` holds one `SemanticBus` per node, sampled at 1 Hz (the rate the FSM constants assume). The bus feeds only the MQTT publisher. Matter does not consume it yet, so the "output broadcast channel shared with Matter" in P4.5 is not built.
|
||||
- **Announce only what has a source.** Six primitives are announced: `room_active`, `no_movement`, `elderly_inactivity_anomaly`, `someone_sleeping`, `possible_distress` and `fall_risk_elevated`. Four are not: `meeting_in_progress` needs a 1–20 % motion level, but per-node motion is a three-level classification; `bathroom_occupied`, `bed_exit` and `multi_room_transition` need zones, and the broadcast carries none (the §3.12.5 zone file is not implemented). For the same reason `zone_occupancy` (§3.1) and `pose` are not announced: `pose_keypoints` is never populated on the broadcast, and `persons[].keypoints` are synthesized.
|
||||
- **Per-entity availability (§3.6).** An announced entity is `online` only while its input exists: vitals while the calibration-gated estimate is published, `fall` while the node sends `edge_vitals`, `motion_energy` and `rssi` while present, and semantic primitives after the 60 s warmup (sleeping and distress also need breathing or heart rate, fall risk needs the fall detector). Otherwise it is `offline` and no state is sent. Transitions publish at once; all availability topics are re-sent every 30 s.
|
||||
- **No default values.** `motion_energy` was always `0.0` (MEASURED: 176 of 176 samples) because the bridge never set it. It now carries the node's `motion_band_power`, or the room value, or nothing. `fall` is driven by the rising edge of `edge_vitals.fall_detected`. Only `sensing_update` produces snapshots, which removes the phantom aggregate device that `edge_vitals` frames created. `present_still` now maps to 0 % motion instead of 100 %.
|
||||
- **Rates (§3.7).** Binary states publish on change, plus a 60 s retained heartbeat, as §3.5 already specified. Sensors keep their configured rates.
|
||||
- **Stable device identity (§3.8).** The default client id is now `wifi-densepose-<8 hex>`, persisted in `<data-dir>/mqtt_client_id`. §3.8 specified `wifi-densepose-<hostname>`; a persisted random id avoids a hostname dependency and survives hostname changes. `--mqtt-client-id` still overrides it.
|
||||
|
||||
### 11.2 Evidence
|
||||
|
||||
Unit tests (CODE-DERIVED, run with `cargo test -p wifi-densepose-sensing-server --features mqtt --lib -- mqtt:: semantic::`): `mqtt::planner::tests` covers the entity → state mapping, unavailable-without-source, warmup, on-change with heartbeat at a 73 frame/s input, sensor rates, reset and privacy mode. `mqtt::bridge::tests` covers message-type filtering, fall edges and motion energy. `mqtt::config::tests` covers client-id persistence across two "restarts" in a temp dir, override and malformed-file recovery.
|
||||
|
||||
Live measurement (MEASURED, 2026-10-02): one ESP32 node streaming CSI and `edge_vitals` over UDP to a macOS host; `sensing-server --source esp32 --mqtt` against `eclipse-mosquitto:2` in Docker on a non-default port; every topic captured with a `paho-mqtt` subscriber on `#`. "Before" is `03a89793` (main), "after" is this change, built `--release --features mqtt`, same command and window.
|
||||
|
||||
```bash
|
||||
# Broker: mosquitto.conf = "listener 1883 0.0.0.0 / allow_anonymous true / persistence false"
|
||||
docker run -d --rm -p 127.0.0.1:18830:1883 \
|
||||
-v "$PWD/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" eclipse-mosquitto:2
|
||||
# Run A (180 s, then SIGTERM) and run B (restart, 45 s) share one --data-dir.
|
||||
# A paho-mqtt subscriber on '#' starts 2 s before each run.
|
||||
RUVIEW_API_TOKEN=<random> sensing-server --source esp32 --udp-port 5006 \
|
||||
--udp-bind 0.0.0.0 --udp-allow <node-subnet> --http-port 3000 --ws-port 3001 \
|
||||
--no-mdns --data-dir <dir> --mqtt --mqtt-host 127.0.0.1 --mqtt-port 18830
|
||||
```
|
||||
|
||||
| Measure (180 s window unless noted) | Before (`03a89793`) | After |
|
||||
|---|---|---|
|
||||
| HA devices announced in run A | 2 (one phantom aggregate + `-node1`) | 1 (`-node1`) |
|
||||
| Entities announced per device | 20 | 15 |
|
||||
| Entities that received state (node device) | 6 | 10 |
|
||||
| Announced entities without state that still reported `online` | 14 | 1 (`fall`: an event, and no fall occurred) |
|
||||
| `presence/state` messages (node device) | 6,814 (37.9 / s) | 4 (one per change, plus 60 s heartbeat) |
|
||||
| `motion_energy` values | `0.0` in 176 of 176 | node band power, 177 samples |
|
||||
| Device id, run A → run B (restart, same `--data-dir`) | `wifi-densepose-<pid A>` → `<pid B>` (4 devices on the broker after B) | same persisted id in both runs (1 device) |
|
||||
| `offline` published on SIGTERM | no | no (unchanged; see 11.3) |
|
||||
| Same, plus `--privacy-mode` (75 s, fresh `--data-dir`) | — | 13 entities announced; no `heart_rate`, `breathing_rate` or `pose` topic for that device |
|
||||
|
||||
After the change, five announced entities had no state: `heart_rate` and `breathing_rate`, because no calibration had been run; `someone_sleeping` and `possible_distress`, which depend on them; and `fall`, which is an event and no fall occurred. The first four were `offline`. `fall` was `online`, because the node sent `edge_vitals`. Before the change all 14 entities without state still reported `online`. Only one node was streaming during this measurement. #2085 used five nodes and measured about 73 `presence` messages per second per node.
|
||||
|
||||
### 11.3 Not done
|
||||
|
||||
- Semantic precision/recall on real captures. The figures in `semantic-primitives-metrics.md` have no reproducer in the repository and stay CLAIMED.
|
||||
- Zone and threshold files, `--semantic*` / `--no-semantic` flags on `sensing-server`, the `reason` attribute (§3.12.4), and `semantic_events.jsonl`.
|
||||
- MQTT last-will and offline-on-shutdown (§3.6). The server registers no LWT and SIGTERM does not publish `offline`, so availability stays as last published after the server stops.
|
||||
- Matter consumption of the semantic states.
|
||||
|
||||
---
|
||||
|
||||
*ADR-115 is the integration story that turns RuView from "another sensing platform" into "drop-in upgrade for any HA install **and** any Matter-controller home." MQTT carries the rich, differentiated telemetry; Matter carries the standardised subset across every controller ecosystem. Numbers 111 and 112 remain reserved per the project ADR-numbering policy.*
|
||||
@@ -66,10 +66,64 @@ hardware claims.
|
||||
timestamps, sequence behavior, calibration, maximum size, and report rate.
|
||||
- Validate OpenWrt One first, then BPI-R3 4x4, before considering MT7996/MLO.
|
||||
|
||||
## Amendment 1 — vendor CSI on MT7981, still unvalidated
|
||||
|
||||
- **Date**: 2026-09-19, recorded here 2026-10-01
|
||||
- **Status**: proposed. One board model (two units). The hardware gates below are still open.
|
||||
|
||||
The 2026-07-18 decision said the public `mt76` tree has no supported CSI export,
|
||||
and that a firmware change might be required before physical CSI exists. Both
|
||||
statements need a narrower reading.
|
||||
|
||||
Mainline `openwrt/mt76` still has no CSI export. MediaTek's public OpenWrt feed
|
||||
does: vendor id `0x0ce7`, MCU event `0xc2`, patch
|
||||
`1001-wifi-mt76-mt7915-csi-implement-csi-support`. On a Wavlink WL-WN586X3
|
||||
Rev A (MT7981BA + MT7976CN + MT7531AE) that patch, rebased onto the `mt76`
|
||||
revision pinned by OpenWrt 24.10.8, produced complex samples without a firmware
|
||||
blob swap. The `0xc2` hook is already in the MT7981 firmware that the OpenWrt
|
||||
24.10.8 image carries. The
|
||||
rebased patch and the two userspace fixes are in `firmware/openwrt-wn586x3/`.
|
||||
|
||||
What the first capture was: one record, 2.4 GHz, 20 MHz, legacy OFDM, 64
|
||||
subcarriers, a single chain. CSI arrived only while a client sent frames.
|
||||
The rate is client traffic, not a sensor sample clock. The capture is not in
|
||||
this repository.
|
||||
|
||||
What it was not:
|
||||
|
||||
- Not a calibration, a sequence check, a timestamp check, or a repeatability run.
|
||||
The label on the wire is `mediatek:physical-unvalidated`. Plain `mediatek` stays
|
||||
reserved for a frame that has passed those gates.
|
||||
- Not a 4x4 array. Each radio is two chains. The four antennas are a 2.4 GHz pair
|
||||
and a 5 GHz pair. Two routers are two receivers. Cross-box phase is not a
|
||||
shared local oscillator, so it is not a beam.
|
||||
- Not the OpenWrt One or the BPI-R3. Those remain the upstream-friendly targets
|
||||
in the original decision. The WN586X3 is the board this path was actually run on.
|
||||
|
||||
`wifi-densepose-mtk-bridge` translates `mt76-vendor` JSON, or an MtkCSIdump
|
||||
datagram, into ADR-267 MTC1 and sends it to the sensing server. The server keeps
|
||||
one snapshot and a bounded ring of per-subcarrier amplitude and phase per
|
||||
`device_id`. It runs `mediatek-amplitude-heuristic-v0` on mean and peak amplitude,
|
||||
and `activity-index-v0` on per-subcarrier fluctuation. Both are `CLAIMED`. Neither
|
||||
is the ESP32 vital-sign path.
|
||||
|
||||
One measured property supports further work, and nothing more. Offline, on six
|
||||
local dump captures from two units (2.4 GHz, HT20, chains rx0 and rx1 of transmit
|
||||
stream 0), the cross-chain phase `arg(H0·conj(H1))`, summed over subcarriers,
|
||||
moved 0.014-0.039 rad (p50) between adjacent frames for well-sampled
|
||||
transmitters. Raw per-chain phase moved about 1.56 rad (p50), which is what
|
||||
uniformly random phase gives (MEASURED; reproducer
|
||||
`scripts/mediatek-csi-phase-stability.py`). That shows a stable observable inside
|
||||
one radio. It does not show that the observable tracks people.
|
||||
|
||||
The operator guide is [`docs/mediatek-router-csi.md`](../mediatek-router-csi.md).
|
||||
|
||||
## Links
|
||||
|
||||
- [ADR-123: BFLD capture path](ADR-123-bfld-capture-path-nexmon-and-esp32.md)
|
||||
- [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- [ADR-267: MediaTek MIMO CSI wire protocol](ADR-267-mediatek-mimo-csi-wire-protocol.md)
|
||||
- [MediaTek router CSI guide](../mediatek-router-csi.md)
|
||||
- [upstream mt76](https://github.com/openwrt/mt76)
|
||||
- [OpenWrt One](https://openwrt.org/toh/openwrt/one)
|
||||
- [MediaTek OpenWrt feed](https://git01.mediatek.com/openwrt/feeds/mtk-openwrt-feeds/)
|
||||
@@ -34,6 +34,81 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
|
||||
- Flags distinguish calibrated, saturated, time-synchronized, dropped-predecessor,
|
||||
and synthetic frames. Synthetic provenance cannot be cleared by downstream code.
|
||||
|
||||
## Amendment 1 — PPDU types `Legacy` and `Unknown`
|
||||
|
||||
- **Date**: 2026-09-19
|
||||
- **Status**: proposed. Demonstrated on one WL-WN586X3 capture. Not a maintainer acceptance.
|
||||
- **Scope**: `PpduType` discriminants only. The 72-byte header layout, CRC,
|
||||
payload ordering, flags, and caps are unchanged.
|
||||
|
||||
### What changed
|
||||
|
||||
`PpduType` gains two variants:
|
||||
|
||||
| Value | Name | Meaning |
|
||||
|---|---|---|
|
||||
| 6 | `Legacy` | Pre-HT: 802.11b CCK or 802.11a/g OFDM |
|
||||
| 7 | `Unknown` | A PPDU format the producer could not classify |
|
||||
|
||||
Values 1-5 (`Ht`, `Vht`, `HeSu`, `HeMu`, `Eht`) are untouched.
|
||||
|
||||
### Why
|
||||
|
||||
The original set assumed every CSI report would be HT or later. The first real
|
||||
MT7981 capture disproved that. A Wavlink WL-WN586X3 (MT7981B + MT7976C) running
|
||||
an OpenWrt 24.10.8 image built with the patches in `firmware/openwrt-wn586x3/`
|
||||
reported `rx_mode = 1` (`MT_PHY_TYPE_OFDM`) for a client associated on 2.4 GHz
|
||||
channel 6 at 20 MHz. The bridge had no way to express it and dropped the frame.
|
||||
|
||||
That was a real data loss, not a corner case. MediaTek's CSI driver treats legacy
|
||||
OFDM as a first-class mode: `mt7915_vendor_csi_tone_mask` skips only
|
||||
`MT_PHY_TYPE_CCK`, and its `mode_map` is keyed by `MT_PHY_TYPE_OFDM`, `_HT`,
|
||||
`_VHT` and `_HE_SU`, giving OFDM its own tone-mask group
|
||||
(`1001-wifi-mt76-mt7915-csi-implement-csi-support.patch:1070-1088`). Association
|
||||
and management exchanges are legacy-rate by design, so a sensing deployment will
|
||||
see these continuously.
|
||||
|
||||
`Unknown` exists so the enum's gaps (5-7 and 12 in `mt76_phy_type`) and future
|
||||
modes are carried explicitly rather than guessed at or dropped. It also covers
|
||||
transports that omit the field: the MtkCSIdump UDP datagram carries no `rx_mode`,
|
||||
and reporting `Unknown` there is honest where defaulting to a concrete format was
|
||||
not.
|
||||
|
||||
### Compatibility
|
||||
|
||||
- **Forward, not backward.** A decoder built before this amendment rejects
|
||||
discriminants 6 and 7 with `UnknownPpduType`, because the original parser
|
||||
rejects unknown types by design. `TryFrom<u8>` returns `Err` rather than
|
||||
panicking, so an old decoder fails safely — it just drops legacy frames, and
|
||||
does so silently. **Rebuild MTC1 consumers after this codec change.**
|
||||
- **Scope is narrower than "the workspace".** `wifi-densepose-hardware` defines
|
||||
three independent `PpduType` enums — `csi_frame.rs` (ESP32),
|
||||
`qualcomm_csi.rs`, and `mediatek_csi.rs` — and this amendment touches only the
|
||||
MediaTek one. Neither of the others changes, so no other vendor bridge is
|
||||
affected. The complete set of `mediatek_csi::PpduType` consumers is:
|
||||
`wifi-densepose-hardware`'s own `mediatek-csi-sim` binary,
|
||||
`wifi-densepose-mtk-bridge`, and `wifi-densepose-sensing-server`, which renders the
|
||||
value with `{:?}` and so needed no code change. In particular the
|
||||
`ppdu_type > 3` bound in `bootstrap_baseline.rs` is fed from the **ESP32**
|
||||
enum via `CsiGridKey::from_frame(&Esp32Frame)`, not from MediaTek frames, so
|
||||
`Legacy` and `Unknown` cannot reach it.
|
||||
- Because the variants were added rather than renumbered, and Rust requires
|
||||
exhaustive matches, the compiler locates any consumer that must be updated.
|
||||
Nothing outside the files above needed one.
|
||||
- Producers must emit 6 only for frames that genuinely are pre-HT, and 7 only
|
||||
where the format is truly unclassified. Neither is a dumping ground for a
|
||||
mapping the producer has not done.
|
||||
- The mapping from `rx_mode` to `PpduType` is now infallible, so no MTC1 producer
|
||||
may drop a frame on PPDU grounds.
|
||||
- Unchanged: synthetic provenance still cannot be cleared by downstream code.
|
||||
|
||||
### Observed dimensions
|
||||
|
||||
The same capture assembled to 1x1x64. That is the client's behaviour, not a
|
||||
protocol limit: the handset sent single-stream frames to a 2.4 GHz 20 MHz AP.
|
||||
Expect 1x2 or 2x2 on 5 GHz, and size consumers for the chipset profile's
|
||||
`max_chains` rather than for what one capture happened to show.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
@@ -59,3 +134,8 @@ Define `MTC1` version 1 as a little-endian, self-delimiting envelope:
|
||||
- [ADR-266: MediaTek Filogic CSI platform](ADR-266-mediatek-filogic-csi-platform.md)
|
||||
- [ADR-018: ESP32 binary CSI framing](ADR-018-esp32-csi-frame-protocol.md)
|
||||
- [ADR-264: RTL8720F radar wire protocol](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- Amendment 1 implementation: `v2/crates/wifi-densepose-hardware/src/mediatek_csi.rs`
|
||||
(codec) and `v2/crates/wifi-densepose-mtk-bridge` (`ppdu_from_rx_mode`). The
|
||||
motivating capture is a lab record and is not in this tree: it contains a
|
||||
client transmitter address and raw CSI. The in-repo check is a fabricated
|
||||
legacy-OFDM record in `wifi-densepose-mtk-bridge/tests/hardware_capture.rs`.
|
||||
@@ -0,0 +1,199 @@
|
||||
# ADR-379: An OTA'd image confirms itself with a bounded health check, or rolls back
|
||||
|
||||
**Status:** Proposed. Hardware-verified on 2026-09-29 on an ESP32-S3 (node 4) and an ESP32-C6 (node 5).
|
||||
**Date:** 2026-09-29
|
||||
**Numbering:** On 2026-10-02 `main` had up to ADR-378 and open PRs claimed
|
||||
nothing higher. If another branch also takes 379, renumber whichever merges
|
||||
second.
|
||||
|
||||
## Context
|
||||
|
||||
Every 16 MB node was flashed with `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`
|
||||
(`sdkconfig.defaults.16mb`). With that option the bootloader starts a freshly
|
||||
OTA'd image in `ESP_OTA_IMG_PENDING_VERIFY`. The app must then call
|
||||
`esp_ota_mark_app_valid_cancel_rollback()`. If it doesn't, the bootloader marks
|
||||
the image `ABORTED` at the next reset and boots the previous slot.
|
||||
|
||||
Nothing in `firmware/esp32-csi-node/main` made that call, and `nm` on the ELF
|
||||
confirmed the symbol was not linked. RUNBOOK.md said an
|
||||
`ota_rollback_boot_check()` "sits in the app", but no such function existed
|
||||
anywhere in the tree. The consequences:
|
||||
|
||||
- An image delivered by `POST /ota` ran until the next reset, then reverted
|
||||
silently. OTA updates were not durable.
|
||||
- While the image was pending, `esp_ota_begin()` refused a second OTA with
|
||||
`ESP_ERR_OTA_ROLLBACK_INVALID_STATE`.
|
||||
- Nothing checked health, so a bad image was never rolled back on purpose. It
|
||||
only reverted if it happened to crash or be power-cycled.
|
||||
|
||||
A second defect meant none of this was ever reached: **OTA never succeeded
|
||||
on this firmware before this change.** MEASURED on 2026-09-29, on node 4 (an
|
||||
ESP32-S3 running the v0.8.12 16 MB build, with the OTA key provisioned):
|
||||
|
||||
- A `POST /ota` of a valid 1,228,672 B image logged `OTA update started`, then
|
||||
`stack overflow in task httpd`, then `rst:0xc RTC_SW_CPU_RST`. The node came
|
||||
back on its old slot. Both attempts failed the same way.
|
||||
- The cause: the server used `HTTPD_DEFAULT_CONFIG()`, which gives a 4096 B
|
||||
task stack. The handler held a 1 KB receive buffer on that stack, while
|
||||
calling `esp_ota_*` and formatting a float progress log.
|
||||
|
||||
## Decision
|
||||
|
||||
At boot, `ota_health_start()` (in `main/ota_health.c`, called early in
|
||||
`app_main`) reads the running partition's state:
|
||||
|
||||
| state | meaning here | action |
|
||||
|---|---|---|
|
||||
| `PENDING_VERIFY` | just OTA'd, rollback armed | run the health check |
|
||||
| `VALID` | USB-flashed with `ota_data_initial.bin` (the bootloader writes VALID), or already confirmed | nothing |
|
||||
| `UNDEFINED` | selected by a build without rollback; boots unconditionally | nothing |
|
||||
| `NEW` | bootloader lacks rollback, so it never promoted the image | nothing |
|
||||
| error (`NOT_FOUND`/`NOT_SUPPORTED`) | no otadata entry, or factory partition | nothing |
|
||||
|
||||
For this table I read the ESP-IDF v5.5 source: `bootloader_utility.c`
|
||||
(`set_actual_ota_seq`, the NEW to PENDING_VERIFY to ABORTED transitions) and
|
||||
`esp_ota_ops.c`.
|
||||
|
||||
**The health check.** A low-priority task polls once a second and feeds
|
||||
`ota_health_step()`, which is a pure function in `ota_health.h`:
|
||||
|
||||
- **Signals, both latched:** the STA got an IP (`IP_EVENT_STA_GOT_IP`, via
|
||||
`ota_health_note_got_ip()` in `main.c`'s handler), and the stream sender
|
||||
accepted at least one CSI frame (`csi_collector_get_send_ok_count() > 0`).
|
||||
The second signal covers driver callback, serialization and `sendto()`
|
||||
together.
|
||||
- **Minimum uptime** `CONFIG_OTA_HEALTH_MIN_UPTIME_S` (default 30 s): the image
|
||||
is not confirmed before this point even with both signals present. An image
|
||||
that crashes soon after starting its pipelines still reverts, because a crash
|
||||
while pending is a rollback.
|
||||
- **Deadline** `CONFIG_OTA_HEALTH_TIMEOUT_S` (default 120 s, measured from
|
||||
boot). Both signals are required by then. The minimum uptime is clamped to
|
||||
the deadline.
|
||||
- **Pass:** `esp_ota_mark_app_valid_cancel_rollback()`, which logs
|
||||
`marked valid after <reason> in <ms> ms`.
|
||||
- **Fail:** logs `health check failed: <reason>, rolling back`, then calls
|
||||
`esp_ota_mark_app_invalid_rollback_and_reboot()`. If no other bootable slot
|
||||
exists, that call returns an error. The node then logs and stays up rather
|
||||
than reboot-looping.
|
||||
- **Terminal verdict:** once the check has confirmed or rolled back, it never
|
||||
changes.
|
||||
|
||||
**Negative-test hook.** `CONFIG_OTA_HEALTH_FORCE_FAIL` (default n, test only)
|
||||
makes the check roll back at the point where it would have confirmed. Use it to
|
||||
prove rollback end to end.
|
||||
|
||||
**Remote visibility.** `GET /ota/status` now includes `ota_state` (`valid`,
|
||||
`pending_verify`, `new`, `undefined`, `other`, or `none`). An operator can
|
||||
confirm durability without a serial console.
|
||||
|
||||
**The httpd stack.** The OTA/WASM HTTP server now sets `config.stack_size =
|
||||
CONFIG_OTA_HTTPD_STACK_SIZE`, which defaults to 8192. Three smaller changes go
|
||||
with it:
|
||||
|
||||
- The OTA receive buffer is static rather than on the stack. Handlers run one
|
||||
at a time on the single httpd task, so one buffer is enough.
|
||||
- The progress log prints an integer percent instead of a float.
|
||||
- `GET /wasm/list` keeps its 2 KB JSON buffer on the heap, not the stack.
|
||||
|
||||
`recv_wait_timeout` stays at 30 s. Each `POST /ota` and `POST /wasm/upload` now
|
||||
logs `httpd stack after <handler>: N of M bytes never used`, so the size can
|
||||
be re-tuned from device measurements.
|
||||
|
||||
Sizing: this is an estimate from static data, not a device measurement. It
|
||||
comes from a `-fstack-usage` build of the S3 16 MB configuration with IDF v5.5.
|
||||
|
||||
- **Frame sizes along the OTA path** (bytes):
|
||||
|
||||
| function | bytes |
|
||||
|---|---|
|
||||
| `httpd_thread` | 144 |
|
||||
| `httpd_sess_process` | 32 |
|
||||
| `httpd_req_new` | 176 |
|
||||
| `httpd_uri` | 64 |
|
||||
| `ota_upload_handler` | 192 (1,216 with the old 1 KB buffer) |
|
||||
| `esp_ota_end` | 48 |
|
||||
| `image_validate` | 304 |
|
||||
| `esp_image_verify` | 32 |
|
||||
| `image_load` | 80 |
|
||||
| `process_segments` | 112 |
|
||||
| SHA-256 | about 110 |
|
||||
| mmap/flash read | about 190 |
|
||||
|
||||
- That chain comes to about 1.5 KB now, and about 2.5 KB with the old buffer.
|
||||
- Two things are not in the static data:
|
||||
- libc's `vfprintf` behind every `ESP_LOG`. This build uses full newlib,
|
||||
not nano, and the old progress line formatted a float.
|
||||
- The flash-chip driver, which is reached through function pointers.
|
||||
|
||||
Together they plausibly add 1 to 1.5 KB. That explains an overflow at 4096
|
||||
with the old handler.
|
||||
- The WASM upload path adds `wasm_upload_process` (336) and
|
||||
`rvf_verify_signature` (352), plus a wasm3 module load that wasn't measured.
|
||||
- 8192 leaves roughly 4 to 5 KB above the estimated worst case, and costs
|
||||
4 KB of internal RAM. The high-water-mark log is the check on that estimate.
|
||||
|
||||
When the build has no `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE` (the 8 MB S3 and
|
||||
4 MB C6 CI lanes), both entry points compile to no-ops.
|
||||
|
||||
## Consequences
|
||||
|
||||
- OTA updates become durable on rollback-enabled nodes once the image passes
|
||||
the check. A second OTA is accepted after confirmation, which can come as
|
||||
early as `MIN_UPTIME_S`.
|
||||
- A bad image now reverts instead of limping, but "bad" only means it failed
|
||||
to reach the network or deliver CSI. An image that passes and then
|
||||
misbehaves later is not caught.
|
||||
- If the AP is down when a node boots an OTA'd image, a good image rolls back.
|
||||
That is the safe direction: push it again later.
|
||||
- Mock/QEMU builds that skip Wi-Fi can never pass. They must not enable
|
||||
bootloader rollback, and none do today.
|
||||
- Bootloader capability is still invisible remotely (RUNBOOK §2). A node
|
||||
whose bootloader lacks rollback reports `ota_state: "new"` after an OTA. That
|
||||
is the first remote hint of which bootloader a board has.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Host test `firmware/esp32-csi-node/test/test_ota_health.c` runs in
|
||||
`make host_tests`, which CI runs. It covers pass, each timeout reason, the
|
||||
terminal verdict, force-fail, the clamp, and zero soak. SYNTHETIC: it
|
||||
exercises the decision function, not a device.
|
||||
- ESP-IDF v5.5 S3 and C6 builds with rollback enabled link
|
||||
`esp_ota_mark_app_valid_cancel_rollback` (verified with `nm`). A build is not
|
||||
hardware evidence.
|
||||
- The httpd overflow is MEASURED on hardware (node 4, above).
|
||||
- Hardware verification, MEASURED on 2026-09-29 on node 4 (ESP32-S3,
|
||||
16 MB, rollback bootloader, OTA key provisioned). The
|
||||
build from this branch was first written over USB. The pushes were made from
|
||||
a Pi 5 with the `sensor-ota-push` cog.
|
||||
1. Positive (MEASURED, S3). An image built from this branch was pushed to `ota_1`: HTTP 200,
|
||||
1,230,848 B in 22 s.
|
||||
- The node logged "httpd stack after POST /ota: 4900 of 8192 bytes never
|
||||
used", so the upload's peak stack use is about 3.3 KB. With the old 1 KB
|
||||
on-stack buffer that is over 4 KB, which is consistent with the
|
||||
overflow.
|
||||
- It rebooted and logged "OTA image pending verify on ota_1".
|
||||
- `/ota/status` then read `ota_state: valid`.
|
||||
- After a hard reset it came back on the same build in `ota_1`, still
|
||||
`valid`, so the update is durable.
|
||||
2. Negative (MEASURED, S3 forced-fail rollback). A `CONFIG_OTA_HEALTH_FORCE_FAIL=y` build was pushed to `ota_0`:
|
||||
HTTP 200.
|
||||
- `/ota/status` showed it running as `pending_verify` for about 30 s.
|
||||
- The node then rebooted and came back on the previous build in `ota_1`,
|
||||
`valid`, so rollback works.
|
||||
3. One push attempt failed mid-upload when a serial capture was opened on the
|
||||
node's USB-Serial-JTAG port at the same moment. The node reset and was
|
||||
unharmed. Don't hold the USB console open while pushing OTA to a
|
||||
USB-attached node.
|
||||
- C6 positive, MEASURED on 2026-09-29 on node 5 (ESP32-C6, 8 MB
|
||||
flash, 4 MB layout, rollback bootloader).
|
||||
- The branch build was first written over USB and the OTA key provisioned.
|
||||
- A second build was pushed from the Pi 5 cog to `ota_1`: HTTP 200 in 11 s.
|
||||
- `/ota/status` showed `pending_verify`, then `valid` about 28 s later.
|
||||
- After a hard reset it stayed on the new build, `valid`.
|
||||
- Not yet verified, so CLAIMED (no log supports these):
|
||||
- the 120 s no-IP timeout path (CLAIMED);
|
||||
- a true power cycle (CLAIMED; a hard reset via RTS was used instead);
|
||||
- the C6 negative/rollback test (CLAIMED; run on the S3 only).
|
||||
- A pass of this check means only that the node reached the network and sent
|
||||
CSI. It does not validate sensing quality, calibration, or anything else the
|
||||
image does.
|
||||
@@ -0,0 +1,58 @@
|
||||
# ADR 380: Loopback UDP tee for second consumers of the CSI stream
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. Unit tested, and smoke tested on loopback: a datagram sent to the
|
||||
server arrived byte for byte at the tee target. Not yet exercised against a
|
||||
live multi node fleet.
|
||||
|
||||
Date: 2026-09-28
|
||||
|
||||
Numbering: On 2026-10-02 `main` had up to ADR 378 and open PRs claimed nothing
|
||||
higher; ADR 379 is the OTA first-boot health check. If another branch also
|
||||
takes 380, renumber whichever merges second.
|
||||
|
||||
## Context
|
||||
|
||||
A five node deployment (four ESP32 S3, one ESP32 C6) streams ADR 018 frames to
|
||||
one sensing server. A second consumer cannot see the raw stream.
|
||||
|
||||
Each node sends to a single `target_ip` (`firmware/esp32-csi-node/main/stream_sender.c`).
|
||||
The server's WebSocket and REST outputs carry per node amplitude only: no
|
||||
phase, no I/Q, no sequence number, and no `0xC511A110` sync packets. A fusion
|
||||
host that aligns CSI with camera, radar, or lidar needs all of those. Binding a
|
||||
second listener to the CSI port is not an option, and an external relay in
|
||||
front of the port would make every source loopback, which the ADR 296
|
||||
allowlist always admits.
|
||||
|
||||
## Decision
|
||||
|
||||
`--udp-tee` (env `RUVIEW_UDP_TEE`) copies every datagram the source allowlist
|
||||
admits, byte for byte, to up to four `ip:port` targets.
|
||||
|
||||
- Targets must be loopback, so raw CSI never leaves the host through the tee.
|
||||
- A target may not use port 0 or the listener's own port, which would loop.
|
||||
- Sends are non blocking. A copy that cannot be queued is dropped and counted,
|
||||
so a slow consumer never stalls the receive loop.
|
||||
- The copy happens after the ADR 296 allowlist check and before any parsing,
|
||||
so consumers see exactly what the server admitted, in every wire format.
|
||||
- The tee is off unless configured.
|
||||
|
||||
## Consequences
|
||||
|
||||
A fusion host or Cognitum cog can subscribe to the full data plane by reading
|
||||
a loopback port. A consumer that today binds the CSI port itself can keep its
|
||||
code and move to the tee port instead.
|
||||
|
||||
The tee is byte transparent, so it needs no change when the ADR 018 header or
|
||||
a later wire version changes.
|
||||
|
||||
Each target costs one `send_to` per admitted datagram. Four targets is the
|
||||
limit.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-sensing-server --no-default-features --lib udp_tee
|
||||
```
|
||||
@@ -0,0 +1,272 @@
|
||||
# ADR-381: Node self-survey from pairwise ranging (check mode first)
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-10-04
|
||||
- **Deciders**: RuView maintainers; acceptance pending
|
||||
- **Tags**: node-positions, ranging, ftm, uwb, multistatic, least-authority
|
||||
- **Related**: ADR-018 (ESP32 wire format), ADR-110 (ESP-NOW time sync),
|
||||
ADR-141 (privacy control plane), ADR-144 (UWB range-constraint fusion),
|
||||
ADR-281 (BLE Channel Sounding ranging evidence), ADR-308 (placement
|
||||
optimizer), rufield ADR-261 (BLE evidence and Channel Sounding adapter
|
||||
boundary, `vendor/rufield/docs/ADR-261-ble-evidence-and-channel-sounding.md`)
|
||||
- **Issues and PRs**: #1804, #1866, #2109 (show which nodes use the default
|
||||
position), #2133 (give the governed fuser the configured node positions)
|
||||
|
||||
## Context
|
||||
|
||||
Node positions reach the sensing server only through `--node-positions`. The
|
||||
operator types it by hand, keyed by node id (`id:x,y,z;...`) or by list index
|
||||
when the prefix is omitted. Three kinds of mistake pass silently today:
|
||||
|
||||
- **Wrong ids.** A config keyed by ids that do not match the live nodes looks
|
||||
applied and does nothing (#1804, #1866). #2109 makes the fallback to the
|
||||
default position visible, and #2133 makes sure both fusers receive the
|
||||
map. Neither can tell whether a correctly-keyed position is *right*.
|
||||
- **Wrong units.** Positions measured in feet and typed as metres are 3.28×
|
||||
too large. Nothing checks the scale.
|
||||
- **Stale positions.** A node moved after setup keeps its old coordinates.
|
||||
|
||||
Each one degrades multistatic fusion without an error. The repository has no
|
||||
node-to-node ranging and no anchor-free solver. ADR-144 models
|
||||
anchor-to-*tag* UWB ranges for people tracking
|
||||
(`wifi-densepose-mat/src/localization/range_constraint.rs`), not ranges
|
||||
between the fixed nodes themselves. ADR-281 computes BLE Channel Sounding
|
||||
ranging evidence, but there is no CS-capable radio in the fleet: rufield
|
||||
ADR-261 notes that the ESP32-S3 does not implement CS.
|
||||
|
||||
The ESP32-S3 and C6 nodes do support Wi-Fi Fine Timing Measurement (802.11mc
|
||||
RTT, station initiator ↔ SoftAP responder) in mainline ESP-IDF. So a ranging
|
||||
source exists on hardware we already have. The question is how good it is.
|
||||
|
||||
### What FTM measured
|
||||
|
||||
Bench, 2026-10-04 (UTC): two ESP32-S3 boards (rev v0.2), ESP-IDF v5.5
|
||||
`examples/wifi/ftm`, SoftAP responder on channel 1 at 20 MHz, 32 FTM frames
|
||||
per session, line of sight, tape ground truth. All rows MEASURED; the
|
||||
reproducer is in Appendix A.
|
||||
|
||||
| Tape (m) | Result |
|
||||
|---|---|
|
||||
| 0.00 | median 0.00 m |
|
||||
| 0.76 | median 0.00 m. Readings clamp to 0 below about 0.8 m. The first three sessions after a responder start read 1.8 to 3.0 m |
|
||||
| 1.14 | five responder starts: medians 0.60, 1.50, 2.25 and 3.08 m, plus a 7.7 m run at −68 dBm with a 1.92 m session spread (placement outlier) |
|
||||
| 3.71 | median 7.80 m (one start) |
|
||||
|
||||
- **Precision within one responder start:** ±0.2 to 0.4 m (standard
|
||||
deviation 0.19 to 0.31 m). A 40-session run over about two minutes showed
|
||||
no drift beyond that.
|
||||
- **Bias between responder starts:** −0.5 to +1.9 m at 1.14 m, with the
|
||||
boards untouched. A plain-versus-auto-start responder A/B test showed the
|
||||
responder build is not the cause.
|
||||
- **Quantisation:** every distance is a multiple of 0.15 m (1 ns of RTT).
|
||||
- **Not tested:** 40 MHz, other channels, `ftm -R -o` offset calibration, a
|
||||
second pair, non-line-of-sight, CSI coexistence, raw t1..t4 timestamps.
|
||||
|
||||
FTM is precise inside one start and inaccurate across starts. It can catch
|
||||
mistakes worth metres. It cannot place a node to decimetres. UWB (Qorvo
|
||||
DW3000 family) is the expected accuracy path: about 0.1 m line of sight is
|
||||
CLAIMED by the vendor, and we have no measurement of our own yet.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. A source-agnostic `RangeObservation` record
|
||||
|
||||
```text
|
||||
RangeObservation {
|
||||
initiator: u8, // node_id, as in --node-positions
|
||||
responder: u8,
|
||||
range_m: f64, // session median
|
||||
sigma_m: f64, // session spread, 0 if unknown
|
||||
method: Ftm | Uwb | BleCs | Manual,
|
||||
samples: u16, // valid frames / exchanges
|
||||
rssi_dbm: Option<i16>,
|
||||
bandwidth_mhz: Option<u16>,
|
||||
at_us: u64, // host receive time
|
||||
responder_boot: Option<u32>, // boot counter or nonce of the responder
|
||||
calibrated: bool, // set only by the host calibration step
|
||||
}
|
||||
```
|
||||
|
||||
`responder_boot` is required for calibration. The FTM bias moved with every
|
||||
responder start, so an offset is only valid within the boot it was measured
|
||||
in.
|
||||
|
||||
On the wire, nodes report ranges either in a new ADR-018-style packet with
|
||||
its own magic, or as an extension of the existing sync packet. This ADR does
|
||||
not choose between them. The host-side record and solver come first. The
|
||||
wire format follows the first firmware that emits ranges.
|
||||
|
||||
### 2. Host-side check mode (warn-only)
|
||||
|
||||
Given the configured positions and a batch of observations, check mode
|
||||
reports:
|
||||
|
||||
- **Per-link residuals.** `measured − ‖p_a − p_b‖`, with a tolerance of
|
||||
`floor + k·σ`. σ is the larger of the median session sigma and the spread
|
||||
across sessions. Each link is `ok`, `outlier`, `too_short` (configured
|
||||
length below the source's minimum), `weak` (every session below the RSSI
|
||||
gate), or `noisy` (σ above the source's maximum).
|
||||
- **Unit mismatch.** A least-squares scale `s` in `measured ≈ s·expected`.
|
||||
If `s` sits within a band of 0.3048 (feet), 0.0254 (inches), 0.01 (cm) or
|
||||
0.001 (mm), and rescaling removes outliers, the check reports the unit.
|
||||
- **Id swap.** For up to 8 ranged nodes, it scores every assignment of the
|
||||
configured positions to ids. The cost is a truncated quadratic in residual
|
||||
over tolerance, so one bad link cannot dominate. If another assignment
|
||||
cuts the cost to at most half and the configured cost is material, the
|
||||
check suggests that assignment, reported as `(node, position_of)` pairs.
|
||||
- **Moved node.** If most of a node's links are outliers (at least 2, and a
|
||||
strict majority), the node was probably moved. Outlier links left over are
|
||||
reported one at a time as possible bias, a body or wall in the path, or a
|
||||
bad session, and not as a configuration error.
|
||||
- **Unknown and unranged nodes.** Observations naming unconfigured ids, and
|
||||
configured nodes that no scored link reaches.
|
||||
|
||||
Distances do not change under rotation, reflection or translation, so check
|
||||
mode compares distances and never coordinates. Where the layout is
|
||||
symmetric, it says so instead of guessing:
|
||||
|
||||
- A relabelling that is a symmetry of the layout is **invisible**. For
|
||||
example, ids rotated 180° in a rectangle of corner nodes. Check mode
|
||||
reports such a layout as consistent.
|
||||
- When several assignments fit equally well, check mode prefers the one that
|
||||
changes the fewest ids, since a typo moves few. It reports how many
|
||||
assignments tied. If the fewest-changes assignments are themselves tied,
|
||||
as with a swap along one wall of a rectangle (its mirror image is the
|
||||
opposite wall's swap), it reports `id_swap_ambiguous` and names nothing.
|
||||
|
||||
Corner-mounted nodes in a rectangular room are the common case and are
|
||||
nearly symmetric. Operators should expect ambiguity reports there, and a
|
||||
fifth node off the axes removes most of them.
|
||||
|
||||
**Authority.** Check mode never writes positions, never edits config, and
|
||||
never changes fusion. Its output is a report for the startup log and a
|
||||
read-only status field. The report shape is JSON-serialisable; the field
|
||||
name follows #2109's `node_positions` block once that lands.
|
||||
|
||||
**Tolerances come from measured data:**
|
||||
|
||||
| Source | `floor_m` | `calibrated_floor_m` | `max_sigma_m` | `min_expected_m` | Basis |
|
||||
|---|---|---|---|---|---|
|
||||
| FTM, ESP32-S3, 20 MHz | 2.0 | 0.6 | 1.0 | 1.0 | MEASURED, bench above: bias −0.5 to +1.9 m between starts; spread ≤ 0.31 m within a start (the 1.92 m outlier excepted); clamp below about 0.8 m |
|
||||
| UWB | 0.3 | 0.3 | 0.3 | 0.0 | ESTIMATE until our own UWB bench. Vendor accuracy is CLAIMED |
|
||||
|
||||
With `floor_m = 2.0`, the one 3.71 m start (+4.1 m) would be flagged as a
|
||||
single-link outlier. That is uncalibrated FTM as it stands. Single-link
|
||||
outliers stay low-severity on purpose, and the strong findings (unit, swap,
|
||||
moved node) need several links to agree.
|
||||
|
||||
### 3. Per-boot calibration
|
||||
|
||||
An offset is recorded per `(initiator, responder, responder_boot)` from
|
||||
sessions at a known reference distance of at least 1.0 m:
|
||||
`offset = median(range) − reference`. It applies only to observations with
|
||||
the same key. After a responder reboot, observations pass through
|
||||
uncalibrated and get the wider floor.
|
||||
|
||||
Zero-separation calibration (ESP-IDF's `cm0` / `ftm -R -o`) is not used,
|
||||
because short ranges clamp to 0 on this hardware. In practice, the reference
|
||||
is one tape-measured link per responder per boot, or a UWB link once UWB
|
||||
exists. That the bias is stable over a whole boot, and not only for two
|
||||
minutes, is untested (ESTIMATE). Measuring it is the first FTM follow-up.
|
||||
|
||||
### 4. Fill mode, gated off
|
||||
|
||||
Fill mode (solve positions with classical-MDS initialisation, weighted
|
||||
robust SMACOF and gauge fixing, then write a file the operator passes to
|
||||
`--node-positions`) is **not implemented** and stays disabled for a source
|
||||
until all of these hold:
|
||||
|
||||
- the source has **MEASURED** link-error evidence with a reproducer;
|
||||
- the evidence covers at least 6 ground-truth links;
|
||||
- the p90 link error is **≤ 0.3 m**.
|
||||
|
||||
FTM's measured errors are metres, so it fails this gate. UWB is expected to
|
||||
pass it, but that is unproven. Even when the gate opens, fill mode writes a
|
||||
proposal file. The server never rewrites its own positions.
|
||||
|
||||
### 5. Pluggable sources
|
||||
|
||||
Every source produces `RangeObservation`, and check mode does not care which
|
||||
one did. Only the tolerance profile changes.
|
||||
|
||||
| Source | Status | How it plugs in |
|
||||
|---|---|---|
|
||||
| **FTM** (ESP32-S3/C6) | Now, for check mode | APSTA node runs a short scheduled survey window. One node is the SoftAP responder and the others initiate a directed session. `rtt_ns`, RSSI and session spread map to `range_m`, `rssi_dbm`, `sigma_m`. Airtime cost on the CSI channel is unmeasured |
|
||||
| **UWB** (DW3000 family, e.g. DWM3001CDK over USB) | After its own bench | Two-way ranging between nodes. Shares hardware with ADR-144's anchor-tag model, but node↔node ranges feed this ADR and anchor↔tag ranges keep feeding ADR-144 |
|
||||
| **BLE CS** | When a CS-capable radio exists | Through the rufield ADR-261 adapter boundary, using ADR-281's cross-validated phase/RTT ranging. A `Divergent` anomaly drops the observation. Single-mechanism evidence gets the wider floor |
|
||||
| **Manual** | Now | A taped distance entered by the operator. Mainly useful as the per-boot calibration reference |
|
||||
|
||||
### 6. Prototype
|
||||
|
||||
`v2/crates/ruview-survey` implements sections 1 to 4 on the host, with no
|
||||
I/O, no clock and no randomness, and bounded inputs: 64 nodes, 10 000
|
||||
observations, 100 m range, and the swap search capped at 8 nodes.
|
||||
Integration with the sensing server is a follow-up and depends on #2109 for
|
||||
where the report surfaces. The crate's tests are SYNTHETIC: generated layouts
|
||||
with FTM-like bias and noise from the envelope above. They cover consistent
|
||||
configs, feet and centimetre mistakes, a two-id swap, a three-id rotation,
|
||||
symmetric invisibility and ambiguity, a moved node, a bench-style single-link
|
||||
outlier, a noisy link, per-boot calibration, and the fill gate.
|
||||
`examples/check_ranges.rs` runs check mode on a JSONL file.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An operator gets a warning when ranges and `--node-positions` disagree
|
||||
grossly, with no new authority: check mode is read-only.
|
||||
- FTM on nodes needs APSTA and a SoftAP beacon on the sensing channel. Its
|
||||
cost to CSI yield must be measured before the firmware side ships.
|
||||
- The FTM tolerances rest on one evening with one pair. They are parameters,
|
||||
not constants, and must be revised from more data (40 MHz, more pairs,
|
||||
non-line-of-sight).
|
||||
- Ranges between fixed nodes are not person data. Ranges to phones or tags
|
||||
are, and remain under ADR-141 and ADR-144.
|
||||
- A symmetric layout can hide a relabelling entirely. Check mode documents
|
||||
this and does not claim to catch it.
|
||||
|
||||
## Evidence rule
|
||||
|
||||
No self-survey accuracy claim without a MEASURED reproducer: tape or laser
|
||||
ground truth, at least 4 nodes, line of sight plus at least one
|
||||
non-line-of-sight case, every number tagged. The prototype's tests are
|
||||
SYNTHETIC and show logic, not radio performance.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Treat FTM as accurate enough to fill positions.** Rejected: the measured
|
||||
bias between starts (−0.5 to +1.9 m at 1.14 m) and the +4.1 m at 3.71 m
|
||||
rule it out.
|
||||
- **Drop FTM.** Rejected for now. It costs nothing on existing hardware, it
|
||||
catches metre-scale mistakes, and it may suit coarse gating, calibrated
|
||||
pairs within a boot, and time sync. It is parked and revisited after UWB.
|
||||
- **Zero-separation `cm0` calibration.** Rejected: readings clamp to 0 below
|
||||
about 0.8 m.
|
||||
- **Server auto-corrects positions.** Rejected under least authority. Fill
|
||||
mode, when enabled, writes a proposal file only.
|
||||
- **LoRa SX1280 ranging, router (MT7981) FTM, raw I/Q phase ranging,
|
||||
CSI-amplitude ranging.** Rejected or deferred: wrong distance regime, no
|
||||
driver support, unproven, or too inaccurate respectively.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does one calibration offset hold for a whole responder boot? For both
|
||||
roles? For other initiators against the same responder?
|
||||
- Is the post-start transient (1.8 to 3.0 m in the first three sessions at
|
||||
0.76 m) real, and how long does it last?
|
||||
- Does 40 MHz shrink the bias between starts?
|
||||
- What does an FTM survey window cost in CSI frames on the same channel?
|
||||
- Can FTM t1..t4 timestamps be tied to the CSI timestamp clock for sync?
|
||||
- The wire format: new magic, or an extension of the sync packet?
|
||||
|
||||
## Appendix A: FTM bench reproducer
|
||||
|
||||
1. Flash ESP-IDF v5.5 `examples/wifi/ftm` on two ESP32-S3 boards
|
||||
(`CONFIG_ESP_WIFI_FTM_ENABLE=y`, the example's default).
|
||||
2. Responder console: `ap <ssid> <password>` (channel 1, 20 MHz defaults).
|
||||
Use throwaway credentials and do not commit them.
|
||||
3. Initiator console, at a taped separation: `ftm -I -s <ssid>`, 32 frames,
|
||||
10 to 40 sessions per distance. Record `Avg raw RTT`, `Avg RSSI`,
|
||||
`Estimated Distance` and the valid-frame count for each session.
|
||||
4. Reset the responder and repeat at the same distance to see the bias move
|
||||
between starts.
|
||||
5. Report the median, spread and error per run, tagged MEASURED, with the
|
||||
raw per-session data alongside.
|
||||
@@ -0,0 +1,137 @@
|
||||
# ADR-382: `spatial.evidence.v1` export of RF Gaussians and link observations
|
||||
|
||||
- **Status**: proposed
|
||||
- **Date**: 2026-10-03
|
||||
- **Deciders**: RuView maintainers
|
||||
- **Tags**: rf, gaussian, spatial, evidence, wire-format, interop
|
||||
- **Related**: ADR-273 (unified RF world model), ADR-275 (RF-aware Gaussian
|
||||
memory), ADR-142 (temporal voxel evidence). Origin of the wire format:
|
||||
WeftOS ADR-107 §7 ("Spatial evidence engine and workspace"), amended
|
||||
2026-10-03.
|
||||
- **Crate**: `v2/crates/ruview-spatial-evidence`
|
||||
|
||||
Numbering: on 2026-10-03 `main` had up to ADR-378, open PRs claimed 379
|
||||
and 380, and 381 was taken by another branch.
|
||||
|
||||
## Context
|
||||
|
||||
`ruview-unified` keeps a map of RF Gaussians (ADR-275) and a Friis channel
|
||||
model that turns a TX/RX link into an expected amplitude. A spatial evidence
|
||||
engine fuses many sensor types (tape measurements, radar, ToF, UWB, RF) into
|
||||
an occupancy octree and promotes stable geometry (walls, furniture) into room
|
||||
models. That engine reads a versioned JSONL format, `spatial.evidence.v1`,
|
||||
specified in WeftOS ADR-107 §7.
|
||||
|
||||
RuView should be able to feed it without depending on it, and without a
|
||||
reader of this repository having to go to another project to learn what the
|
||||
export writes. This ADR records the part of the contract RuView emits.
|
||||
|
||||
## Decision
|
||||
|
||||
1. A leaf crate, `ruview-spatial-evidence`, converts `ruview-unified` output
|
||||
to `spatial.evidence.v1` lines. Its only internal dependency is
|
||||
`ruview-unified`. The wire shapes are local serde structs; the contract is
|
||||
the JSON, not a shared crate.
|
||||
2. It emits two record types, `rf_gaussian` and `rf_link_observation`. It
|
||||
does not mirror the other v1 types and rejects them as unknown when
|
||||
parsing.
|
||||
3. Every record is validated against the v1 rules below before it is
|
||||
serialised, so a bad value fails in RuView rather than in the consumer.
|
||||
4. If WeftOS ADR-107 §7 changes the envelope or either RF body, this ADR is
|
||||
amended in the same change that updates the crate.
|
||||
|
||||
### Envelope (every record)
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `schema` | exactly `spatial.evidence.v1` |
|
||||
| `type` | `rf_gaussian` or `rf_link_observation` here |
|
||||
| `t_ns` | u64, ns since the Unix epoch |
|
||||
| `frame` | `room_enu` |
|
||||
| `region` | region id, e.g. `region/urth/meso/<room>` |
|
||||
| `source_id` | device or operator id, never a person |
|
||||
| `uncertainty_m` | 1σ position uncertainty, metres, in (0, 100] |
|
||||
| `provenance` | `{receipt, producer, proof}`; `proof` is `MEASURED`, `CODE` or `SYNTHETIC` |
|
||||
|
||||
Ids (`region`, `source_id`, `receipt`, `producer`) match
|
||||
`[A-Za-z0-9._:/-@]{1,128}`. A line is at most 16 KiB.
|
||||
|
||||
**Frame.** `room_enu` is room-local, origin at the room's SW floor corner,
|
||||
x east, y north, z up, metres. Coordinates are within ±1000 m.
|
||||
|
||||
**Angles.** Every yaw in v1 is degrees counter-clockwise from room +x
|
||||
(east): 0 faces +x, 90 faces +y. The two RF types have no yaw field. The
|
||||
`rf_gaussian` orientation is a body-to-room quaternion, and a positive
|
||||
rotation about +z turns the Gaussian's local x axis counter-clockwise from
|
||||
east, the same sense.
|
||||
|
||||
### `rf_gaussian`
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `position` | [x, y, z] m |
|
||||
| `scale` | per-axis σ, m, each in [1e-6, 1e4] |
|
||||
| `orientation` | quaternion `[w, x, y, z]`, finite, norm > 1e-6 |
|
||||
| `occupancy` | peak extinction, nepers/m, in [0, 1e6] |
|
||||
| `confidence` | [0, 1] |
|
||||
| `motion` | `static`, `slow` or `fast` |
|
||||
| `role`? | `absorber` or `reflector`; set only on priors sent back to RuView |
|
||||
|
||||
The exporter maps `RfGaussian` field for field. `uncertainty_m` is the
|
||||
geometric mean of the three σ, clamped to the envelope range, because
|
||||
`RfGaussian` has no separate position covariance. The receipt is
|
||||
`gauss:<device>:<t_ns>:<index>`. A Gaussian whose provenance is `synthetic`
|
||||
is always tagged `SYNTHETIC`; a caller can lower the proof tag, never raise
|
||||
it.
|
||||
|
||||
### `rf_link_observation`
|
||||
|
||||
| Field | Rule |
|
||||
|---|---|
|
||||
| `tx`, `rx` | antenna positions, m |
|
||||
| `freq_hz` | [1e8, 3e11] |
|
||||
| `excess_loss_db` | `20·log10(free / measured)`, in [−60, 200]; negative is constructive multipath |
|
||||
|
||||
The free-space reference comes from `ruview-unified`'s channel model with an
|
||||
empty map, so it is the same Friis amplitude the ADR-275 inverse update uses.
|
||||
|
||||
### Examples
|
||||
|
||||
These two lines are the WeftOS ADR-107 §7 examples. The crate's tests parse
|
||||
them and check that its own output has the same shape.
|
||||
|
||||
```jsonl
|
||||
{"schema":"spatial.evidence.v1","type":"rf_link_observation","t_ns":1759500001300000000,"frame":"room_enu","region":"region/urth/meso/test-room","source_id":"node-2","uncertainty_m":1.0,"provenance":{"receipt":"csi:node1-node2:000881","producer":"ruview-adapter@0.1","proof":"MEASURED"},"tx":[6.20,3.50,1.10],"rx":[0.20,0.20,1.10],"freq_hz":2437000000.0,"excess_loss_db":4.2}
|
||||
{"schema":"spatial.evidence.v1","type":"rf_gaussian","t_ns":1759500002000000000,"frame":"room_enu","region":"region/urth/meso/test-room","source_id":"ruview-unified","uncertainty_m":0.5,"provenance":{"receipt":"gauss:7f3a","producer":"ruview-unified@0.3","proof":"CODE"},"position":[3.2,0.0,1.2],"scale":[0.05,1.6,1.2],"orientation":[1,0,0,0],"occupancy":0.7,"confidence":0.8,"motion":"static","role":"absorber"}
|
||||
```
|
||||
|
||||
### Versioning
|
||||
|
||||
Additive optional fields may appear inside v1 and older readers ignore them.
|
||||
New record types may also be added inside v1 (the 2026-10-03 amendment added
|
||||
`tof_depth` and `radar_range`, and an optional `sensor` block on
|
||||
`radar_track_point`); a reader that does not know a type rejects it as
|
||||
unknown rather than misreading it. Removing, renaming or changing the
|
||||
meaning of a field requires `spatial.evidence.v2`, and the parser reports a
|
||||
v2 line as a version mismatch.
|
||||
|
||||
## Consequences
|
||||
|
||||
- RuView output can feed a spatial evidence engine with no new runtime
|
||||
dependency, and the contract is readable from this repository.
|
||||
- The engine can send promoted walls and objects back as `rf_gaussian`
|
||||
priors with a `role`. That gives ADR-275's inverse update a geometric
|
||||
starting point instead of an empty map.
|
||||
- The struct definitions are duplicated between the two projects. The
|
||||
golden-line tests catch drift in the RF types; a change on the WeftOS side
|
||||
still needs someone to amend this ADR and the crate.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Nothing calls the crate yet. The sensing server does not export records,
|
||||
and there is no adapter that turns an inbound `rf_gaussian` prior into an
|
||||
`RfGaussian` in the live map.
|
||||
- The tests are CODE-level (deterministic conversions and validation). No
|
||||
MEASURED claim is made about what the exported evidence does to a room
|
||||
model.
|
||||
- `uncertainty_m` for a Gaussian is a stand-in derived from its extent.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-383: Reject malformed HTTP request targets without stopping the transport
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
ADR-375 introduces the authenticated Streamable HTTP transport. Node accepts an HTTP request target that the WHATWG URL parser rejects. The current request listener lets that parser exception escape, terminating the process before route selection. A bounded loopback fixture using the public `startMcpHttp` API reproduces this with `http://[`; the original code resets the connection and exits.
|
||||
|
||||
## Decision
|
||||
|
||||
Catch URL construction failures at the request boundary and return HTTP 400 with the generic `invalid_request_target` error. Do not include the request target or parser exception in the response, since the target can carry a secret path. Keep route selection, token checks, origin policy, grants, protocol negotiation and JSON handling unchanged.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Relying on Node's HTTP parser does not suffice: a syntactically accepted request target can still fail URL construction. A process-level exception handler would obscure unrelated faults and would not provide a request-specific response.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED software fixture: the actual child-process HTTP transport fails the new regression on the original source (nine existing controls pass); the patched transport passes all ten focused tests. The new test also verifies subsequent health, rejected unauthenticated calls and successful authenticated initialization. Broader package checks are recorded separately against the final implementation revision. No external service, device, hardware, throughput or production deployment claim is made.
|
||||
|
||||
## Consequences
|
||||
|
||||
Malformed URL targets receive a stable client error and do not interrupt later requests. This change does not add SSE or sessions, change authentication, or authorize additional operations.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-384: Preserve explicitly supplied SDK search limits
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The public `memorySearch(query, limit)` helper uses a truthiness check before forwarding its optional limit. Explicit zero and NaN therefore disappear, even though the shared registry rejects them. The helper silently returns success where the equivalent raw SDK call returns `invalid_arguments`; strict SDK users also miss the expected `RuViewError`.
|
||||
|
||||
## Decision
|
||||
|
||||
Omit the optional limit only when it is undefined. Forward every explicitly supplied value to the existing shared validator. Do not duplicate validation or change the registry range, search algorithm, result count, strict error policy or defaults.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Validating separately in the helper would duplicate schema rules and risk drift. Retaining truthiness makes invalid caller inputs indistinguishable from an omitted option.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED local software regression exercises the public SDK and actual registry. Original code fails for zero; the corrected helper rejects zero, NaN, negative and excessive limits, preserves strict errors, and matches raw registry results for omitted and valid limits. Package gate receipts are recorded separately against the final revision. No external retrieval, training, hardware or production service claim is made.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-385: Stop watch mode on permanent input errors in every output mode
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The CLI's `esp32 --watch` repeats short capture windows. Permanent bind/port errors stop the terminal view, but the piped/JSON branch continues before reaching that check. Schema-level invalid arguments also never stop either branch. Actual CLI children with a fractional UDP port or rejected bind value repeatedly emit errors despite having no usable capture configuration. These fixtures fail before opening a socket or accessing a device.
|
||||
|
||||
## Decision
|
||||
|
||||
Classify existing permanent port/bind errors and schema-invalid arguments before selecting output mode. Emit the error once and return exit status 1 for both terminal and JSON output. Keep repeated capture behavior for nonterminal results, including windows with no CSI packets; retain the normal watch output contract.
|
||||
|
||||
## Alternatives
|
||||
|
||||
Adding a delay would still repeatedly retry an unchanged invalid configuration. Stopping every unsuccessful capture would incorrectly terminate legitimate empty capture windows.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED bounded actual CLI regression checks piped `--watch --json` with invalid inputs, one structured failure and nonzero exit. No network socket, hardware, external provider or live capture is exercised. Whole-package validation receipts accompany the final revision.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-386: Recover CSI packet loss by missing sequence identity
|
||||
|
||||
Status: Proposed; maintainer acceptance pending
|
||||
|
||||
## Context
|
||||
|
||||
The RF capture diagnostics report CSI packet loss and account for UDP reordering. The existing tracker decrements its gap count for any late sequence while gaps exist. A duplicated recovery or previously received packet therefore erases a different missing packet. For example, sequences 100, 103, 101, 101 leave 102 missing but currently report zero loss. Accurate loss diagnostics matter when distinguishing transport problems from healthy sensing input.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain the identities of counted missing sequences within the existing 1024-sequence reorder window. Recover a gap only when deleting its identity succeeds, so a duplicate cannot recover it twice. Prune identities outside the window without erasing their historical gap counts. A confirmed counter reset clears recovery identities but preserves accumulated loss. Keep the existing frame count/rate, loss denominator, stray/reset classification and packet format unchanged.
|
||||
|
||||
## Alternatives
|
||||
|
||||
A duplicate-only set cannot distinguish packets observed before the capture baseline from genuinely missing ones. Keeping all missing identities indefinitely is unnecessary and could grow without bound.
|
||||
|
||||
## Verification
|
||||
|
||||
MEASURED software fixtures use byte-accurate RAC1 envelopes, the public capture API and real loopback UDP. Regressions cover duplicated recoveries, already-received packets, pre-capture packets and distinct valid recoveries. A direct public parser/NodeStats control checks bounded bookkeeping and reset semantics. Existing sequence/stray/reset tests remain applicable. This is a software diagnostic correction, not physical RF, device throughput or physiological accuracy validation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Loss no longer disappears when unrelated late packets arrive. Recovery remains limited to the established reorder window; older gaps stay counted. Stored identities are bounded per node.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-389: Measure CSI rates over the observed capture interval
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (receive-only device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The public `captureEsp32` producer may stop at `max_packets` before its configured
|
||||
`seconds` deadline. Its node CSI rates still divided received packets by that
|
||||
requested deadline. Two packets received in approximately 150 milliseconds with a
|
||||
30-second deadline therefore appeared as 0.07 Hz, misleading operators evaluating
|
||||
whether a node streams normally.
|
||||
|
||||
## Decision
|
||||
|
||||
Start a monotonic clock when the UDP socket successfully binds. At completion,
|
||||
calculate `elapsedSeconds` and use it as the denominator for node CSI rates.
|
||||
Retain `seconds` as the configured deadline for compatibility and expose the
|
||||
observed interval separately. Close and analysis processing time are excluded.
|
||||
A socket error before binding has a zero observed interval and no received data.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
A deterministic-format ESP32 fixture is transmitted through an owned loopback UDP
|
||||
socket into the public capture function. The baseline fails because the observed
|
||||
interval is absent and the rate is 0.07 Hz. The fixed capture checks early
|
||||
termination, preserves the deadline, and checks the rate against observed elapsed
|
||||
time. Existing CSI decoding and capture tests remain controls. This is software
|
||||
transport verification; it makes no radio, hardware, sensing-accuracy, or firmware
|
||||
throughput claim. Normal deadline captures may report a slightly longer interval
|
||||
because timer callbacks are scheduled by the host event loop.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-390: Reduce LiDAR scan extrema without variadic argument limits
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
`readRplidar` accepts up to 4 MiB of serial scan bytes. Its summary passed every
|
||||
valid distance as an argument to `Math.min` and `Math.max`. A valid 1 MiB scan
|
||||
containing 200,000 measurement nodes exceeds the JavaScript argument-stack limit
|
||||
and throws `RangeError` instead of returning diagnostics.
|
||||
|
||||
## Decision
|
||||
|
||||
Reduce extrema with a loop and constant auxiliary state. Retain current median,
|
||||
zero-distance filtering, coverage, units, and empty-scan behavior. This avoids
|
||||
runtime-specific argument limits across the full supported capture size.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
An offline serial-pump dependency supplies protocol-valid bytes to the public
|
||||
`readRplidar` producer. The 3-point control passes on the baseline; the 200,000
|
||||
point fixture fails with `RangeError`. Both pass after this change with exact
|
||||
minimum, median, maximum, and coverage checks. Existing device tests provide
|
||||
empty and mixed-input controls. The fixture uses no serial device or motor and
|
||||
proves software decoding/aggregation only, not real hardware performance.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-391: Retain synthetic CSI provenance in analysis input
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-323 (RAC1 synthetic flag), ADR-373 (CSI analysis)
|
||||
|
||||
## Context
|
||||
|
||||
The RAC1 decoder exposes the firmware's synthetic flag and the waterfall retains
|
||||
it, but the kernel-analysis frame collector discarded it. Analysis of synthetic
|
||||
input consequently returned the same live-CSI explanatory note as unmarked input,
|
||||
preventing callers from distinguishing synthetic-inclusive estimates.
|
||||
|
||||
## Decision
|
||||
|
||||
Track whether any retained analysis frame is firmware-marked synthetic, expose
|
||||
that boolean in `analysis.input.synthetic`, and explicitly describe synthetic
|
||||
input in the estimate note. Frames rejected by the analysis buffer cap do not
|
||||
change the provenance of the retained computation. Keep arrival-rate measurements
|
||||
and raw packet diagnostics unchanged. An unmarked packet is not proof of physical
|
||||
radio origin; this records an explicit upstream flag rather than certifying truth.
|
||||
|
||||
## Verification
|
||||
|
||||
The public capture producer receives protocol-valid RAC1 datagrams through owned
|
||||
loopback UDP and invokes an injected deterministic analyzer. A mixed marked and
|
||||
unmarked window fails the baseline provenance assertion. The fixed path retains
|
||||
true for the mixed window and false for an unmarked control. This validates
|
||||
software provenance propagation only, not sensing accuracy or hardware behavior.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-392: Return single valid frames for explicitly selected radar models
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (host device diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The serial radar reader requires two valid frames before auto-detecting a model.
|
||||
That confidence threshold was also applied to an explicitly selected model,
|
||||
rejecting a valid short capture with one decoded presence or vital report as
|
||||
`no_valid_frames`. The caller already supplied the model and baud selection.
|
||||
|
||||
## Decision
|
||||
|
||||
Require two frames for auto detection and one for an explicitly selected model.
|
||||
Retain the empty-capture failure, checksums, parsing, capture permissions, and
|
||||
all reported values. A single decoded frame is a successful software read and
|
||||
is not evidence of clinical accuracy or sustained radar performance.
|
||||
|
||||
## Verification
|
||||
|
||||
An offline serial-pump fixture supplies one checksum-valid MR60BHA2 presence
|
||||
report through the public reader. Explicit model selection fails on the baseline
|
||||
and returns one frame with presentFraction=1 after the change. The same input in
|
||||
auto mode remains refused. Existing multi-frame and invalid-input device tests
|
||||
remain controls. No serial port or hardware was opened by this verification.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-393: Use the arithmetic median for even LiDAR samples
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (range and depth diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The shared LiDAR median helper selected the upper middle sample for even-sized
|
||||
sets. The public depth reader reported 4 metres for confident depths of 1 and
|
||||
4 metres, and the serial scan reader likewise returned 4000 mm. Fields named
|
||||
median should report the arithmetic median, 2.5 metres or 2500 mm respectively.
|
||||
|
||||
## Decision
|
||||
|
||||
Average the two central sorted samples for even counts and preserve the central
|
||||
sample for odd counts. Empty sets remain null. Existing distance/confidence
|
||||
filtering and metre/millimetre conversion remain in the public producers.
|
||||
Update existing even-sample expectations to reflect the corrected numerical
|
||||
contract; this intentionally changes these statistics for even counts.
|
||||
|
||||
## Verification
|
||||
|
||||
Offline protocol fixtures enter both public readers through their existing
|
||||
transport injection boundaries. The baseline fails two even-sample cases and
|
||||
passes an unsorted odd-sample control. The correction verifies both units and
|
||||
zero-depth exclusion. No real LiDAR, serial device, or radar was used, and this
|
||||
software calculation does not prove measurement accuracy.
|
||||
@@ -0,0 +1,26 @@
|
||||
# ADR-394: Apply strict SDK errors to diagnostic reports
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (SDK operator interface)
|
||||
|
||||
## Context
|
||||
|
||||
A strict SDK instance throws `RuViewError` when an operation returns `ok:false`.
|
||||
The doctor helper directly invokes the diagnostic producer to retain injectable
|
||||
operator dependencies, bypassing that shared result check. Failed diagnostics
|
||||
therefore silently resolved even when the caller selected strict mode.
|
||||
|
||||
## Decision
|
||||
|
||||
Extract the existing result check and apply it to both registry calls and doctor
|
||||
reports. Preserve injected dependencies and retain the original structured report
|
||||
on the thrown error. Non-strict calls and successful diagnostics continue to
|
||||
resolve reports; the existing dry-run exception remains unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
The public SDK doctor runs a deterministic offline invalid-protocol diagnostic.
|
||||
The baseline fails the promised strict rejection. The correction checks the exact
|
||||
error class, tool identifier, and failed report; controls verify non-strict failure
|
||||
reports and successful strict reports. No external service or device is accessed.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-395: Preserve missing numeric CLI values for schema rejection
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (operator CLI)
|
||||
|
||||
## Context
|
||||
|
||||
The common device/training CLI flag adapter coerces values with `Number`. A bare
|
||||
`--node-id` is parsed as boolean true and becomes node 1; `--node-id=` becomes
|
||||
node 0. Both silently select a real node rather than reporting the missing
|
||||
numeric argument. The same adapter handles capture deadlines and sample limits.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain boolean and empty-string values in the common schema argument adapter so
|
||||
existing numeric schemas reject them. Convert explicit non-empty numeric values
|
||||
as before, including zero. Keep presentation flags and tool schemas unchanged.
|
||||
This change is limited to verbs using the common snake-case argument adapter.
|
||||
|
||||
## Verification
|
||||
|
||||
Execute the actual CLI process with both missing-value forms and explicit zero.
|
||||
An intentionally invalid UDP port ensures argument validation completes before
|
||||
opening a socket. Baseline errors omit node_id for both missing forms; the fixed
|
||||
CLI reports those numeric type errors while preserving the explicit zero control.
|
||||
This is offline CLI/schema verification, not a radio capture.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-396: Convert ESPHome millimetre distances to reported centimetres
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (ESPHome radar reader)
|
||||
|
||||
## Context
|
||||
|
||||
ESPHome sensor discovery advertises each entity's unit. The reader maps entities
|
||||
named distance into `distanceCmMean`, converting metres to centimetres but leaving
|
||||
millimetres unchanged. An advertised 500 mm measurement therefore became 500 cm
|
||||
instead of 50 cm, a factor-of-ten numerical error.
|
||||
|
||||
## Decision
|
||||
|
||||
Apply unit scales of 100 for metres and 0.1 for millimetres. Preserve centimetre
|
||||
and existing unspecified/other-unit behavior, positive-distance filtering, and
|
||||
entity metadata. Unit matching remains case insensitive. This is a correction
|
||||
to reported units, not a change to sensor discovery or access permissions.
|
||||
|
||||
## Verification
|
||||
|
||||
The public reader connects to an owned loopback TCP fixture speaking the native
|
||||
ESPHome protocol. Discovery and state updates carry 500 mm, 0.5 m, and 50 cm.
|
||||
The millimetre baseline fails with 500 cm; metre and centimetre controls pass.
|
||||
All three report 50 cm after the correction. This proves software protocol/unit
|
||||
handling only; no real radar or external network service was accessed.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-397: Distinguish ESPHome received updates from retained samples
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (bounded ESPHome sessions)
|
||||
|
||||
## Context
|
||||
|
||||
ESPHome sessions count every incoming state update, but each entity retains only
|
||||
its first 10,000 samples. The entity report used the retained length as `updates`,
|
||||
understating actual arrivals once this cap was reached. Mean values also came from
|
||||
that bounded subset without exposing how many updates were omitted.
|
||||
|
||||
## Decision
|
||||
|
||||
Maintain a separate arrival counter per entity. Report that count as `updates`,
|
||||
retain `sampledUpdates` for the aggregation subset, and disclose `droppedSamples`.
|
||||
Preserve the existing buffer bound and latest-value behavior. Existing hand-built
|
||||
summary sessions without the new counter fall back to their retained sample count.
|
||||
|
||||
## Verification
|
||||
|
||||
An owned loopback native-protocol fixture emits 10,001 distance state updates
|
||||
through the public reader. The baseline reports session=10,001 but entity=10,000.
|
||||
The correction reports received=10,001, retained=10,000, omitted=1, with unchanged
|
||||
50 cm mean. An existing small multi-entity fixture verifies zero omitted samples
|
||||
and correct ordinary counts. This is software aggregation verification only.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-398: Treat nonfinite ESPHome sensor states as unavailable
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (native sensor-state semantics)
|
||||
|
||||
## Context
|
||||
|
||||
Native protobuf float states can encode NaN and infinities without setting the
|
||||
missing-state flag. Positive infinity passed the distance filter and poisoned an
|
||||
otherwise finite distance mean; last entity states also exposed nonfinite values
|
||||
that become null only after JSON serialization, producing inconsistent reports.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat a nonfinite sensor float as missing when ingesting a state. Preserve state
|
||||
arrival counts, normal finite values, proto3 absent-float zero, explicit missing
|
||||
flags, and binary/text state behavior. Existing summary rules then exclude the
|
||||
invalid sample from means and report unavailable last state as null.
|
||||
|
||||
## Verification
|
||||
|
||||
Owned loopback native-protocol fixtures emit a 50 cm state followed by explicitly
|
||||
encoded NaN, positive infinity, or negative infinity. Corrected baseline fixtures
|
||||
fail on the nonfinite mean or last-state result; all fixed cases retain the 50 cm
|
||||
mean, null latest state, and two arrival counts. The existing fixture controls
|
||||
finite vitals, explicit missing states, and omitted proto3 zero values. No real
|
||||
sensor or external service is accessed; this proves software numeric handling.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-399: Select the busiest CSI stream using total arrivals
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded live CSI analysis)
|
||||
|
||||
## Context
|
||||
|
||||
The analysis collector advertises selection of the busiest node's dominant shape.
|
||||
It ranks buffers by their retained frame length. Once multiple streams reach the
|
||||
analysis buffer cap, their retained lengths tie and insertion order wins even
|
||||
when a later stream received substantially more frames. A 70-frame first node
|
||||
therefore beats a 100-frame second node under a 64-frame cap.
|
||||
|
||||
## Decision
|
||||
|
||||
Rank candidate streams by retained frames plus their existing dropped-over-cap
|
||||
counter. Preserve the frame cap, first-window sample rate, explicit node filter,
|
||||
and deterministic insertion order for actual arrival-count ties. The analyzer
|
||||
still consumes only the retained bounded samples; ranking does not invent data.
|
||||
|
||||
## Verification
|
||||
|
||||
The public capture receives two protocol-valid node streams through owned
|
||||
loopback UDP. The baseline selects the 70-frame node despite the second node's
|
||||
100 arrivals. The correction selects node 2 and reports 64 retained/36 omitted
|
||||
frames. An explicit node-1 control still selects node 1 and reports 64/6.
|
||||
Existing uncapped analysis and decoding tests remain controls. This is software
|
||||
stream-selection verification, not hardware throughput or sensing accuracy.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-400: Decode fused vitals into node diagnostics
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-063 (fusion), ADR-373 (node diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The capture parser recognizes firmware magic `0xC5110004` as fused vitals but
|
||||
returns only a byte count. Node aggregation then omits its RSSI and physiological
|
||||
values. A valid fused-only stream therefore has null `lastVitals` even though the
|
||||
firmware supplies the current fused heart and breathing estimates.
|
||||
|
||||
## Decision
|
||||
|
||||
Decode the exact 48-byte packed `edge_fused_vitals_pkt_t` from
|
||||
`firmware/esp32-csi-node/main/edge_processing.h`. Share the common fields and fixed
|
||||
point unit conversions with ordinary vitals, then expose the mmWave presence,
|
||||
type, confidence, raw heart/breathing values, distance in centimetres, and target
|
||||
count from their actual offsets. Aggregate fused reports into RSSI and latest
|
||||
vitals while retaining their distinct packet kind counter. Reject truncated
|
||||
fused reports before they can count as decoded node data.
|
||||
|
||||
## Verification and limits
|
||||
|
||||
A firmware-layout fixture enters the public capture over owned loopback UDP.
|
||||
The baseline counts the packet but returns null RSSI and latest vitals. Fixed
|
||||
assertions check 72 bpm fused heart, 15 bpm breathing, 120 cm distance, confidence
|
||||
and target values, and timestamp units. Truncated-prefix tests and existing
|
||||
ordinary-vitals tests remain controls. These are decoded device-reported values;
|
||||
no clinical accuracy, real radio, or sensor-fusion quality is established.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-401: Validate auxiliary node datagrams before counting decoded reports
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-069 (features), ADR-039 (compression), feature-state and WASM firmware formats
|
||||
|
||||
## Context
|
||||
|
||||
Known auxiliary magics were sufficient to count a datagram as decoded. A
|
||||
four-byte feature, compressed, feature-state, or WASM prefix created a phantom
|
||||
node and successful diagnostic counts. Corrupt feature-state CRCs and truncated
|
||||
variable payloads were also accepted, hiding actual data corruption.
|
||||
|
||||
## Decision
|
||||
|
||||
Validate firmware-defined payload envelopes before retaining their existing
|
||||
metadata-only report shape: features require their 48-byte record, compressed
|
||||
reports require a 10-byte header and declared non-empty payload, feature-state
|
||||
requires the fixed 60-byte record and its IEEE CRC, and WASM output requires
|
||||
an 8-byte header plus five bytes for each of at most 16 declared events. Keep
|
||||
raw auxiliary payloads out of the response and retain existing kind counters.
|
||||
|
||||
Source contracts are `edge_processing.h`/`send_compressed_frame` in
|
||||
`edge_processing.c`, `rv_feature_state.h`, and the variable-length producer in
|
||||
`wasm_runtime.c`. This validates envelope integrity without claiming to decompress
|
||||
CSI or interpret feature/event semantics.
|
||||
|
||||
## Verification
|
||||
|
||||
Owned loopback UDP drives the public capture with four complete envelope controls,
|
||||
four magic-only prefixes, one corrupt CRC, and three truncated payloads. The
|
||||
baseline counts all twelve as decoded and hides corruption. The correction counts
|
||||
four decoded and eight malformed, with one valid node and exact kind counters.
|
||||
Existing CSI, vitals, and other packet tests remain controls. No firmware or real
|
||||
sensor was exercised; the claim is software format and diagnostic correctness.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-402: Preserve synthetic provenance for the whole CSI shape window
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-323 (RAC1 flags), ADR-373 (per-shape node diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
A normal capture's shape summary averages all frames but reports the synthetic
|
||||
flag from only its latest packet. A marked frame followed by an unmarked frame
|
||||
therefore contributes to the mean while removing the synthetic label shown by
|
||||
normal CLI diagnostics. The waterfall already preserves a window-wide flag;
|
||||
kernel-analysis input has a separate collector and provenance boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
Track whether any frame in each statistical shape window is firmware-marked
|
||||
synthetic and report that flag alongside the window mean/count. Preserve the
|
||||
latest packet's other descriptor fields and keep provenance independent for each
|
||||
shape. An unmarked window still reports false; that is not certification of its
|
||||
physical origin. No capture or hardware authority changes.
|
||||
|
||||
## Verification
|
||||
|
||||
The public ordinary capture receives a marked RAC1 frame then an unmarked frame
|
||||
through owned loopback UDP. The baseline reports two aggregated frames and false
|
||||
synthetic provenance. The correction reports true; an entirely unmarked control
|
||||
remains false. Existing shape and source statistics remain controls. This verifies
|
||||
software window provenance, not sensing accuracy or authentic radio origin.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-403: Normalize chronological report timestamps without truthiness
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-371 (offline training evidence gate)
|
||||
|
||||
## Context
|
||||
|
||||
The gate used `Date.parse(value) || numericFallback`. Valid epoch-zero ISO times
|
||||
parse to zero and were then treated as missing. Documented numeric millisecond
|
||||
boundaries were first interpreted as calendar strings by Date.parse, so 5 ms and
|
||||
30 ms could be misordered and falsely refused as temporal leakage.
|
||||
|
||||
## Decision
|
||||
|
||||
Retain finite numeric boundaries as milliseconds and parse string boundaries
|
||||
once. Test finiteness explicitly rather than using truthiness. Preserve refusal
|
||||
of invalid/missing dates and non-increasing train/test chronology. The tool schema
|
||||
continues accepting its existing ISO string input; the public training export
|
||||
retains its documented numeric input support.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual SDK evaluates an epoch-zero ISO boundary with a later test timestamp.
|
||||
The public training export evaluates numeric 5 ms and 30 ms boundaries. Both
|
||||
baseline cases are falsely refused and pass after normalization. Equal-time and
|
||||
invalid-date controls remain refused. Fixtures are explicitly synthetic; no
|
||||
training, model calls, dataset claims, or publication is performed by these tests.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-404: Preserve missing capture windows in CLI watch trends
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-375 (per-node watch rate sparklines)
|
||||
|
||||
## Context
|
||||
|
||||
Watch draws repeated timed capture windows, but appends rate history only for
|
||||
nodes present in a window. A present–absent–present node is therefore shown as
|
||||
two adjacent observations, hiding the intervening outage. The sparkline helper
|
||||
also discards missing positions, so merely adding null history entries would
|
||||
still compress time. An actual terminal-mode CLI and owned loopback UDP replay
|
||||
reproduces the false continuous two-bar trend.
|
||||
|
||||
## Decision
|
||||
|
||||
Append a missing entry for each previously observed node absent from a capture
|
||||
window. Keep the existing 30-window bound. Render each missing position as `·`,
|
||||
while scaling finite rates using only finite observations. Missing means no
|
||||
observation in that window; it is not a measured zero rate. Initial history
|
||||
still starts when a node is first observed. JSON capture output is unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
The public CLI receives one valid packet, an empty one-second capture, and one
|
||||
returning packet. Its terminal row must show `▄·▄` instead of `▄▄`. A real child
|
||||
process executes the CLI; only stdout's TTY capability is supplied by the test.
|
||||
The test owns and cleans up its loopback sockets and child. Finite, zero,
|
||||
all-missing and empty sparkline controls preserve scale and positions. This is
|
||||
software replay, not a claim about radio hardware availability.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-405: Complete iPhone LiDAR captures when the relay stream closes
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded device readers)
|
||||
|
||||
## Context
|
||||
|
||||
The iPhone reader completes at its packet limit, timeout, or pre-open failure.
|
||||
After an opened WebSocket closes normally, however, it keeps the capture timer
|
||||
running despite having no remaining source of frames. A one-frame stream that
|
||||
closes immediately waits the entire requested window; an empty normal stream
|
||||
also delays its no_frames result. This wastes the user's capture time and uses
|
||||
the idle post-close interval in its reported receive rate.
|
||||
|
||||
## Decision
|
||||
|
||||
Finish on every close event. Retain accepted depth statistics. Preserve the
|
||||
existing relay_closed refusal for abnormal empty streams, pre-open connection
|
||||
failure, and max_frames termination. A normal empty stream still returns
|
||||
no_frames; a stream with valid depth frames remains a successful capture. The
|
||||
requested seconds field retains its existing meaning.
|
||||
|
||||
## Verification
|
||||
|
||||
An owned loopback HTTP server performs a real WebSocket upgrade and sends a
|
||||
valid one-pixel depth frame followed by a normal close handshake. The public
|
||||
reader must return promptly instead of waiting its two-second capture timeout.
|
||||
A normal empty stream and abnormal empty code1008 stream check refusal behavior;
|
||||
a max_frames=1 control checks existing early termination. These are transport
|
||||
fixtures, not iPhone hardware or depth-accuracy evidence.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-406: Assign numeric radar roles only to ESPHome sensor entities
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 Amendment 3 (ESPHome native API reader)
|
||||
|
||||
## Context
|
||||
|
||||
The reader infers heart, breathing and distance roles from entity names without
|
||||
checking their protocol kind. A binary Heart alarm or text Distance status can
|
||||
therefore be selected before the actual numeric sensor. Boolean true becomes
|
||||
1 bpm through arithmetic coercion; a numeric-looking text status becomes a
|
||||
fabricated distance. The actual numeric sensor is then ignored. A loopback
|
||||
native API producer reproduces this with valid entity metadata and states.
|
||||
|
||||
## Decision
|
||||
|
||||
Require sensor kind for heartBpm, breathingBpm and distanceCm roles, matching
|
||||
the existing targets rule. Keep binary presence inference. Preserve binary and
|
||||
text entities and their raw states in the entities result, without assigning
|
||||
them numeric radar roles. Numeric scalar filtering and unit handling remain
|
||||
unchanged. No medical accuracy or sensor-name taxonomy is introduced.
|
||||
|
||||
## Verification
|
||||
|
||||
A real TCP producer lists binary alarm and text status entities before valid
|
||||
numeric heart, distance and breathing sensors. The public reader must select
|
||||
72 bpm,120 cm and15 bpm rather than1,50 and1. A producer with only nonnumeric
|
||||
entities must return null numeric summaries while retaining its3 raw updates.
|
||||
Existing ESPHome presence, missing-state and numeric sensor controls run too.
|
||||
These are deterministic native API fixtures, not radio or clinical evidence.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-407: Preserve interrupted serial-capture state in device summaries
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (bounded serial pump and device readers)
|
||||
|
||||
## Context
|
||||
|
||||
The pump can emit valid HEX chunks before its process fails. readSerial retains
|
||||
those useful bytes, but discards the failure whenever bytes exist. Radar and
|
||||
LiDAR callers consequently present an ordinary successful capture without any
|
||||
indication that the requested window was interrupted. Auto-detected radar also
|
||||
silently retains only its probe if the remainder capture fails.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep valid partial bytes and measurements. Mark the capture as incomplete and
|
||||
include a warning when the pump process fails after producing data. Propagate
|
||||
that state through both serial device summaries, including an auto-detection
|
||||
remainder failure. Do not expose subprocess stderr in the warning. Existing
|
||||
failures without bytes and normal complete capture behavior remain unchanged.
|
||||
The flag describes transport completeness, not measurement accuracy.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual Python PUMP_SCRIPT runs through the production execTool process
|
||||
runner, with a temporary fixture serial module that emits valid radar or scan
|
||||
bytes then raises. The public serial, radar and LiDAR readers must retain data
|
||||
and report captureIncomplete. A real normally completing pump is the control.
|
||||
Auto-detection also receives a normal probe followed by a failing empty pump.
|
||||
All serial port operations stay inside the fixture module; no hardware opens,
|
||||
physical motor commands, radio transmissions or model calls occur.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-408: Exclude absent LD2410 targets from distance means
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (shared mmWave summary)
|
||||
|
||||
## Context
|
||||
|
||||
The LD2410 decoder explicitly emits present=false and distanceCm=0 for target
|
||||
state none. The aggregate treats that no-target sentinel as a measured range.
|
||||
A 90 cm detection followed by an absent target consequently reports 45 cm; an
|
||||
all-absent stream reports 0 cm instead of having no distance observation. The
|
||||
presence fraction already identifies these absent frames correctly.
|
||||
|
||||
## Decision
|
||||
|
||||
Exclude explicitly absent target frames only from the distance mean. Keep their
|
||||
raw distance sentinel, frame counts, rates, and presence contribution. Continue
|
||||
averaging moving and stationary target measurements normally. MR60BHA2 distance
|
||||
frames are unchanged; they use their own range-validity field in the decoder.
|
||||
This distinguishes absence from a distance measurement, without new physical
|
||||
range limits or an accuracy claim.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual Python serial pump and production process runner read complete
|
||||
LD2410 frames from an isolated serial module. Public readMmwave must report 90 cm
|
||||
for present 90/absent, null for all-absent, and 150 cm for moving 90/static 210.
|
||||
Presence fractions and raw last-frame zero controls remain intact. The baseline
|
||||
fails both absence regressions and passes the positive-distance control. No
|
||||
hardware port opens or radar transmissions occur.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-409: Align SDK device argument types with supported runtime options
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-369 (typed SDK), ADR-373 (device readers), ADR-378 (spectrum)
|
||||
|
||||
## Context
|
||||
|
||||
The SDK forwards device arguments to the current tool registry, but its shipped
|
||||
TypeScript declarations still describe earlier options. A real TypeScript
|
||||
consumer cannot select ESPHome by source/host/API port, request ESP32 spectrum
|
||||
rows, or set an iPhone depth packet cap. Each workflow already works through the
|
||||
same public SDK at runtime. Explicit serial source is missing too.
|
||||
|
||||
## Decision
|
||||
|
||||
Describe mmWave arguments as serial (the existing default, requiring port) or
|
||||
ESPHome (requiring host). Add the existing optional spectrum parameters and
|
||||
iPhone max_frames. Keep numeric and enum constraints, and all existing serial
|
||||
call shapes. This changes declarations only; no reader or tool behavior changes.
|
||||
Group these omissions as one declaration-drift fix rather than separate fields.
|
||||
|
||||
## Verification
|
||||
|
||||
A TypeScript consumer compiled with strict NodeNext checks fails on the valid
|
||||
runtime options before the fix and succeeds after it. Its expected-error controls
|
||||
retain host/port requirements, numeric parameter types, and known backends. The
|
||||
same public SDK is exercised with real owned TCP ESPHome, UDP CSI, and WebSocket
|
||||
depth producers: numeric state, spectrum bins, and packet-cap termination all
|
||||
work before and after the declaration change. No hardware or external service
|
||||
is involved. The fixture can be replayed with an installed TypeScript compiler:
|
||||
|
||||
```
|
||||
tsc --strict --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext test/fixtures/device-types.mts
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-410: Require callback/frame markers for CSI monitor flow evidence
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-373 (serial monitor), ADR-375 (starvation diagnostics)
|
||||
|
||||
## Context
|
||||
|
||||
The monitor counted any line containing the ESP32 csi_collector log tag as a
|
||||
CSI callback. Current firmware uses that tag for early configuration, startup,
|
||||
self-ping failures and other metadata as well as actual callbacks. A startup
|
||||
log with no callback consequently returned ok=true and falsely certified flow.
|
||||
The source's actual callback line uses `CSI cb #`; Realtek uses
|
||||
`RUVIEW_CSI: frame #`.
|
||||
|
||||
## Decision
|
||||
|
||||
Count only those explicit callback/frame markers as flow evidence. Keep the
|
||||
existing startup-upgrade metadata and independent buffer-starvation detection.
|
||||
The existing count remains a count of observed rate-limited log markers, not a
|
||||
radio callback-rate measurement. No UART, reset or hardware control behavior
|
||||
changes. Older log formats with only configuration tags cannot certify flow.
|
||||
|
||||
## Verification
|
||||
|
||||
The public SDK monitor runs the actual Python monitor script in a real child
|
||||
process. An isolated serial module supplies current firmware startup/self-ping
|
||||
messages; the baseline falsely succeeds, and the fix returns no-flow diagnostics.
|
||||
Mixed startup plus ESP32/Realtek frame markers counts only two observations.
|
||||
A real frame plus a starvation message still fails as csi_buffer_starvation.
|
||||
Existing monitor rendering and Python fixture controls run too. No physical
|
||||
serial port opens or node resets occur.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-411: Preserve structured doctor diagnostics for package corruption
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-370 (doctor), ADR-394 (SDK strict diagnostic contract)
|
||||
- Promotion: extend existing doctor PR #2208; no separate publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The package-integrity diagnostic reports hash drift as a structured warning, but
|
||||
malformed manifest JSON or a missing outer checksum throws before the doctor
|
||||
report is assembled. The public SDK therefore cannot return the promised
|
||||
checks/remedies for those ordinary package-corruption cases. A copied-package
|
||||
fixture reproduces both exceptions without touching the installed manifest.
|
||||
|
||||
## Decision
|
||||
|
||||
Convert manifest read/parse and packaged-file read failures into integrity
|
||||
findings. Report a missing or unreadable checksum explicitly. Preserve existing
|
||||
hash comparison, file counts, diagnostic report shape and warning policy. SDK
|
||||
strict mode continues rejecting failed reports through the existing checked
|
||||
path; integrity warnings retain their current warning behavior. No additional
|
||||
manifest schema or trust policy is introduced.
|
||||
|
||||
## Verification
|
||||
|
||||
The actual public SDK is imported from an owned temporary copy containing the
|
||||
package source and reviewed brain. Strict doctor with groups=harness must return
|
||||
an integrity warning and reinstall remedy for malformed JSON, a missing or
|
||||
unreadable checksum, and unreadable packaged data. The valid-copy control retains successful integrity status. Existing
|
||||
doctor and strict failed-report controls run alongside these tests. No canonical
|
||||
manifest changes, credentials, network requests or hardware operations occur.
|
||||
@@ -0,0 +1,30 @@
|
||||
# ADR-412: Quarantine incomplete recorded I/Q pairs during transcode
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-096 and ADR-097 (rvCSI capture bridge)
|
||||
|
||||
## Context
|
||||
|
||||
The public ESP32 JSONL converter is the documented capture fixture bridge. Its
|
||||
contract drops rejected frames. It currently floors odd byte lengths, silently
|
||||
accepting a truncated I/Q pair as a shorter valid frame; malformed hex instead
|
||||
aborts the entire capture and loses later valid frames. The existing rej_len
|
||||
counter is never used.
|
||||
|
||||
## Decision
|
||||
|
||||
Reject invalid hex and odd byte lengths before constructing numeric samples.
|
||||
Increment the existing rej_len counter once for each rejected record and
|
||||
continue processing later frames. Preserve complete int8 pair conversion,
|
||||
quality scoring, timestamps, and output format. Do not infer missing samples or
|
||||
change adapter calibration or hardware policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI processes owned temporary JSONL captures with valid
|
||||
frames before and after each damaged record. The baseline accepts the odd pair
|
||||
and aborts on invalid hex. Fixed runs retain the two complete frames with
|
||||
contiguous frame IDs and truthful counters. A signed-IQ control verifies
|
||||
amplitude and real/imaginary orientation. All eight harness gates run alongside
|
||||
these subprocess tests. This is offline recording evidence only.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-413: Preserve complete UDP datagrams in the CSI relay
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (ESP32 CSI packet layout)
|
||||
|
||||
## Context
|
||||
|
||||
The public Python relay promises byte-for-byte forwarding without parsing or
|
||||
mutation. Its 2048-byte receive buffer truncates larger datagrams; a valid
|
||||
ADR-018 shape with four antennas and 256 subcarriers occupies 2068 bytes. Its
|
||||
truthiness check also discards empty UDP datagrams instead of forwarding them.
|
||||
Both behaviors violate the transparent relay contract.
|
||||
|
||||
## Decision
|
||||
|
||||
Receive up to 65535 bytes, sufficient for a complete IPv4 UDP payload, and
|
||||
forward every received datagram, including an empty payload. Retain None as the
|
||||
socket-timeout sentinel. Preserve bind defaults, destination selection,
|
||||
reporting and cleanup. Introduce no packet interpretation or hardware policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI relays traffic between owned loopback sockets. The
|
||||
baseline truncates a complete 2068-byte ADR-018 packet and fails to forward an
|
||||
empty datagram; a small binary payload is unchanged. Fixed runs compare every
|
||||
byte and retain both controls. All eight harness gates run with these actual
|
||||
subprocess tests. This proves local datagram preservation, not physical CSI
|
||||
capture quality or network performance.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-414: Use ADR-018 decoding for overnight capture segments
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (wire layout), ADR-135 (empty-room corpus)
|
||||
|
||||
## Context
|
||||
|
||||
The overnight empty-room recorder claims the same ADR-018 layout as the ordinary
|
||||
UDP recorder, but still reads an obsolete eight-byte header. It records the
|
||||
subcarrier-count bytes as RSSI/channel, consumes the remaining header as I/Q,
|
||||
and accepts unrelated or truncated datagrams. This corrupts baseline corpus
|
||||
samples before downstream analysis.
|
||||
|
||||
## Decision
|
||||
|
||||
Reuse record-csi-udp.py's existing twenty-byte ADR-018 parser through a local
|
||||
module import. Preserve segment naming, duration and receive workflow. The
|
||||
shared parser retains receive timestamps, decodes metadata and declared
|
||||
antenna-major payload, ignores trailing bytes, and rejects unsupported magic or
|
||||
truncated payloads. Avoid a second copy of the packet layout and do not alter
|
||||
quality, calibration or physical capture policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public overnight CLI writes owned temporary segment files from loopback
|
||||
UDP traffic. Baseline reports the wrong RSSI, channel and payload and retains
|
||||
invalid packets. Fixed tests verify declared multi-antenna amplitudes and raw IQ,
|
||||
metadata, trailing-byte handling, and valid-frame continuity after invalid or
|
||||
truncated datagrams. All eight harness gates run alongside these tests.
|
||||
This is deterministic software packet evidence only; no hardware accuracy claim.
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-415: Allocate distinct training capture files on session-name collisions
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-018 (collector wire input)
|
||||
|
||||
## Context
|
||||
|
||||
The training collector names sessions to whole seconds and opens recordings in
|
||||
append mode. Two captures with the same label during that second append samples
|
||||
to one file and overwrite its metadata with only the later frame count. Dataset
|
||||
manifests then undercount the actual recording and the original session loses
|
||||
its identity. Fractional CLI durations and concurrent collection make the
|
||||
collision reachable.
|
||||
|
||||
## Decision
|
||||
|
||||
Allocate each recording with exclusive file creation. Retain existing names
|
||||
when available; on a data or metadata collision, try a numeric suffix until
|
||||
allocation succeeds. Update session ID, data path and metadata path together.
|
||||
Preserve prior recordings and metadata instead of implicitly resuming them.
|
||||
No CLI resume contract exists; ordinary collection creates a new session.
|
||||
|
||||
## Verification
|
||||
|
||||
Two actual public collector CLI processes receive distinct owned loopback
|
||||
packets under an isolated fixed datetime fixture that reproduces a same-second
|
||||
collision deterministically. Baseline leaves one combined data file and one
|
||||
miscounted metadata entry. Fixed captures retain distinct IDs, one sample per
|
||||
file, matching metadata counts and a two-session/two-frame manifest. All eight
|
||||
harness gates run. No hardware capture or physical accuracy is claimed.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR-416: Validate alignment CLI window parameters before processing captures
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-079 (ground-truth pairing)
|
||||
|
||||
## Context
|
||||
|
||||
The public ground-truth alignment CLI parses window and numeric options without
|
||||
validation. A zero window frame count makes the windowing loop advance by zero
|
||||
and grow indefinitely. Negative counts also cannot advance correctly, and
|
||||
fractional or partially numeric values are silently truncated. Invalid camera
|
||||
counts, confidence thresholds and time values can bypass matching checks or
|
||||
produce misleading paired output.
|
||||
|
||||
## Decision
|
||||
|
||||
Parse complete numeric values and validate before reading or writing captures.
|
||||
Require positive safe integer frame counts, finite nonnegative window duration,
|
||||
confidence in [0,1], and finite signed clock offsets. Retain zero expansion and
|
||||
zero confidence as supported controls. Report the offending option and exit2
|
||||
for invalid inputs. Preserve valid windowing and pairing calculations.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual CLI subprocesses process owned tiny feature/camera recordings. The
|
||||
baseline zero/negative count processes are bounded by a one-second timeout and
|
||||
32MiB heap cap; fractional/malformed/range cases lack the expected early error.
|
||||
The fixed CLI rejects each invalid option before output creation and retains
|
||||
complete valid windows, zero expansion/confidence and a signed finite offset.
|
||||
An initial fixture passed negative values as separate tokens, which Node's
|
||||
argument parser already rejects; that attempt is retained but invalidated.
|
||||
Final regressions use documented --option=value syntax. All eight harness gates
|
||||
run. No model training, camera, physical capture or hardware operation occurs.
|
||||
@@ -0,0 +1,29 @@
|
||||
# ADR-417: Preserve input recordings when converter output aliases the source
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-412 (capture converter input quarantine)
|
||||
- Promotion: extend existing converter PR #2227; no new publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The public JSONL converter opens output in write mode before reading input. If
|
||||
both arguments refer to one file, including hard-link or symbolic-link aliases,
|
||||
it truncates the recording and reports successful conversion with zero frames.
|
||||
Input validation must not destroy the original evidence it is meant to read.
|
||||
|
||||
## Decision
|
||||
|
||||
Check existing file identity before opening the output. Reject identical files
|
||||
with a clear CLI error and exit2, preserving source bytes and links. Keep normal
|
||||
distinct-path conversion and the existing incomplete-IQ quarantine unchanged.
|
||||
This does not add in-place conversion or a broader transactional output policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public Python CLI receives owned input captures with identical, hard-link
|
||||
and symbolic-link output paths. Baseline overwrites each source; fixed runs
|
||||
leave the original bytes intact and reject the arguments. Existing signed IQ,
|
||||
quarantine and later-frame controls pass alongside these fixtures. All eight
|
||||
harness gates run. Original capture files outside temporary fixtures are never
|
||||
mutated. This extends the existing converter PR as one reviewed unit.
|
||||
@@ -0,0 +1,30 @@
|
||||
# ADR-418: Handle expected resets in owned ESPHome rejection fixtures
|
||||
|
||||
- Status: proposed
|
||||
- Date: 2026-10-11
|
||||
- Related: ADR-406 (ESPHome fixture-driven role validation)
|
||||
- Promotion: extend existing ESPHome PR #2221; no new publication unit
|
||||
|
||||
## Context
|
||||
|
||||
The encrypted/password test fixtures write while the reader deliberately rejects
|
||||
and closes the connection. The server socket can then emit ECONNRESET after the
|
||||
test ends. With no socket error listener, that expected fixture event produces
|
||||
an uncaught exception and intermittent unrelated full/security gate failures.
|
||||
Converter and alignment failures retained locally show this exact test event.
|
||||
|
||||
## Decision
|
||||
|
||||
Handle ECONNRESET only in the owned encrypted/password fixture modes. Continue
|
||||
throwing any unexpected error code and any reset in ordinary fixture mode. Do
|
||||
not change the production reader, its failure reports or its network policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Actual public reader connects to owned TCP fixtures and returns the expected
|
||||
rejection report. A controlled socket reset event reproduces the missing error
|
||||
lifecycle deterministically: baseline throws, fixed modes accept the reset.
|
||||
Negative controls retain failures for EIO in rejection mode and ECONNRESET in
|
||||
ordinary mode. Existing roles and public reader tests, plus all eight gates,
|
||||
run on this revision. This is an explicitly injected software fixture event,
|
||||
not physical device or hardware evidence.
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
Latest proposed decisions:
|
||||
|
||||
- [ADR-379: An OTA'd image confirms itself with a bounded health check, or rolls back](ADR-379-ota-first-boot-health-check.md)
|
||||
- [ADR-380: Loopback UDP tee for second consumers of the CSI stream](ADR-380-loopback-udp-tee.md)
|
||||
- [ADR-382: `spatial.evidence.v1` export of RF Gaussians and link observations](ADR-382-spatial-evidence-rf-export.md)
|
||||
- [ADR-378: `ruview-live` showcase views — CSI waterfall, radar fan, animated cells](ADR-378-ruview-live-showcase-views.md)
|
||||
- [ADR-377: `ruview-live` — a Claude Code mod shipped inside `@ruvnet/ruview`](ADR-377-ruview-live-claude-code-mod.md)
|
||||
- [ADR-376: `ruview` — one npm install for every RuView component](ADR-376-ruview-umbrella-npm-package.md)
|
||||
@@ -74,6 +77,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-040](ADR-040-wasm-programmable-sensing.md) | WASM Programmable Sensing (Tier 3) | Accepted |
|
||||
| [ADR-041](ADR-041-wasm-module-collection.md) | WASM Module Collection (65 edge modules) | Accepted (hardware-validated) |
|
||||
| [ADR-044](ADR-044-provisioning-tool-enhancements.md) | Provisioning Tool Enhancements | Proposed |
|
||||
| [ADR-379](ADR-379-ota-first-boot-health-check.md) | OTA'd image confirms itself with a bounded health check (IP + CSI frame) or rolls back; OTA httpd stack sized from device data | Proposed (host-tested; hardware-verified on one S3 and one C6) |
|
||||
| [ADR-110](ADR-110-esp32-c6-firmware-extension.md) | ESP32-C6 firmware extension — Wi-Fi 6 / 802.15.4 / TWT / LP-core | Accepted, P1-P10 complete, firmware-side substrate closed at **[v0.7.0-esp32](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)**. Companion docs: [`WITNESS-LOG-110`](../WITNESS-LOG-110.md) (13 §A0.x entries · 99.56 % cross-board RX · **104.1 µs smoothed sync stdev** · ≤100 µs target met), [`ADR-110-REVIEW-GUIDE`](../ADR-110-REVIEW-GUIDE.md) (one-page reviewer tour), [`ADR-110-BRANCH-STATE`](../ADR-110-BRANCH-STATE.md) (coordination map vs `feat/adr-115-ha-mqtt-matter`). Host decoders + tests: Python `SyncPacketParser` (10) + Rust `wifi_densepose_hardware::SyncPacket` (15), cross-language hex pin gates drift. |
|
||||
|
||||
### Signal processing and sensing
|
||||
@@ -181,6 +185,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-294](ADR-294-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) |
|
||||
| [ADR-295](ADR-295-source-provenance-state-machine.md) | Source provenance state machine — synthetic can never present as live | Accepted (initial implementation) |
|
||||
| [ADR-296](ADR-296-sensor-data-plane-bind-hardening.md) | Sensor data-plane hardening — UDP bind control and source allowlist (step one) | Accepted (initial implementation) |
|
||||
| [ADR-380](ADR-380-loopback-udp-tee.md) | Loopback UDP tee for second consumers of the CSI stream (`--udp-tee`) | Proposed |
|
||||
| [ADR-297](ADR-297-multi-node-semantic-correctness.md) | Multi-node semantic correctness — per-node inference, node-keyed rate limiting, stale state | Accepted (initial implementation) |
|
||||
| [ADR-298](ADR-298-model-release-sanity-gates.md) | Model release sanity gates — block degenerate and mislabeled model artifacts | Accepted (initial implementation) |
|
||||
| [ADR-299](ADR-299-csi-data-incident-repo-controls.md) | Repository CSI data-incident controls — ignore rules and pre-commit/CI policy check | Accepted (controls implemented; tree remediation gated) |
|
||||
|
||||
@@ -33,59 +33,82 @@ cargo run --release -p wifi-densepose-sensing-server \
|
||||
--mqtt-username homeassistant
|
||||
```
|
||||
|
||||
Within ~5 seconds of starting, Home Assistant should auto-create:
|
||||
Within ~5 seconds of the first CSI frame, Home Assistant should auto-create:
|
||||
|
||||
- One **device** per RuView node (named after the MAC or the `friendly_name` from your zones config)
|
||||
- 17+ **entities** per device (presence, person count, heart rate, breathing rate, motion, fall events, signal strength, zones, and the 10 semantic primitives)
|
||||
- One **device** per RuView node, named `RuView node <client-id>-node<N>` (see [Device identity](#device-identity))
|
||||
- **15 entities** per device (13 with `--privacy-mode`): the raw signals and six semantic primitives listed in the [Entity reference](#entity-reference)
|
||||
|
||||
If nothing appears in HA's Settings → Devices, see [Troubleshooting](#troubleshooting).
|
||||
|
||||
### 3. Stop the publisher cleanly
|
||||
|
||||
Ctrl-C — the publisher pushes `offline` to every availability topic before disconnect so HA marks all entities unavailable instantly. A `kill -9` triggers MQTT LWT, which has the same effect within ~30 s.
|
||||
Ctrl-C (SIGINT/SIGTERM) stops the server. It does **not** publish `offline` first, and no MQTT last-will is registered, so every availability topic keeps its last retained value until the server starts again (MEASURED: no availability message in the last 6 s of a SIGTERM'd run, see [ADR-115 §11](../adr/ADR-115-home-assistant-integration.md#11-status-note-p45-semantic-states-on-the-live-publisher-2026-10-02)). While the server runs, a node that stops sending frames goes `offline` at the next 30 s availability heartbeat.
|
||||
|
||||
---
|
||||
|
||||
## Entity reference
|
||||
|
||||
RuView publishes three classes of entity. Names below are the `unique_id` slugs — Home Assistant assigns friendly names automatically.
|
||||
Names below are the `unique_id` slugs; Home Assistant assigns friendly names automatically. Every entity has its own availability topic (`…/<slug>/availability`, retained). An entity is `online` only while the server has a real source for it. Otherwise it is `offline`, no state is published, and Home Assistant shows **unavailable** rather than a made-up value.
|
||||
|
||||
### Raw signals (11 entities)
|
||||
### Raw signals (9 entities)
|
||||
|
||||
| HA entity | Slug | HA component | Unit | Source field |
|
||||
| HA entity | Slug | HA component | Unit | Source | Available when |
|
||||
|---|---|---|---|---|---|
|
||||
| Presence | `presence` | `binary_sensor` | — | `nodes[].node_inference.classification != "absent"` | always |
|
||||
| Person count | `person_count` | `sensor` | persons | `persons[]` length, else `estimated_persons` (room-level, same on every node) | always |
|
||||
| Motion level | `motion_level` | `sensor` | % | node classification: `present_moving` = 100, `present_still`/`absent` = 0 | always |
|
||||
| Presence score | `presence_score` | `sensor` | % | node classification confidence × 100 while present, else 0 | always |
|
||||
| Motion energy | `motion_energy` | `sensor` | (dimensionless) | `node_features[].features.motion_band_power`, else the room `features.motion_band_power` | a band-power value was seen in the last 30 s |
|
||||
| Signal strength | `rssi` | `sensor` | dBm | `nodes[].rssi_dbm` | an RSSI was seen in the last 30 s |
|
||||
| Heart rate | `heart_rate` | `sensor` | bpm | `vital_signs.heart_rate_bpm` (room-level) | a published estimate in the last 30 s |
|
||||
| Breathing rate | `breathing_rate` | `sensor` | bpm | `vital_signs.breathing_rate_bpm` (room-level) | a published estimate in the last 30 s |
|
||||
| Fall detected | `fall` | `event` | — | rising edge of the node's `edge_vitals.fall_detected` | the node sent `edge_vitals` in the last 30 s |
|
||||
|
||||
The server publishes heart and breathing rate only after explicit calibration, with exactly one occupant and a passing quality gate. Until then both entities stay unavailable. Heart rate and breathing rate are **biometric**: `--privacy-mode` removes them from discovery and state (see [Privacy](#privacy)).
|
||||
|
||||
### Semantic automation primitives (6 entities)
|
||||
|
||||
Small finite-state machines run inside the server, one set per node, sampled once per second ([ADR-115 §3.12](../adr/ADR-115-home-assistant-integration.md#312-semantic-automation-primitives-ha-mind), P4.5). All of them are unavailable for the first 60 s after start (warmup). After that each publishes its current state, then on every change.
|
||||
|
||||
| HA entity | Slug | HA component | What it fires on | Available when |
|
||||
|---|---|---|---|---|
|
||||
| Presence | `presence` | `binary_sensor` | — | `edge_vitals.presence` |
|
||||
| Person count | `person_count` | `sensor` | persons | `edge_vitals.n_persons` |
|
||||
| Heart rate | `heart_rate` | `sensor` | bpm | `edge_vitals.heartrate_bpm` |
|
||||
| Breathing rate | `breathing_rate` | `sensor` | bpm | `edge_vitals.breathing_rate_bpm` |
|
||||
| Motion level | `motion_level` | `sensor` | % | `edge_vitals.motion` × 100 |
|
||||
| Motion energy | `motion_energy` | `sensor` | (dimensionless) | `edge_vitals.motion_energy` |
|
||||
| Fall detected | `fall` | `event` | — | `edge_vitals.fall_detected` |
|
||||
| Presence score | `presence_score` | `sensor` | % | `edge_vitals.presence_score` × 100 |
|
||||
| Signal strength | `rssi` | `sensor` | dBm | `edge_vitals.rssi` |
|
||||
| Zone occupancy | `zone_occupancy` | `binary_sensor` | — | `sensing_update.zones` |
|
||||
| Pose keypoints | `pose` | `sensor` (attrs) | — | `pose_data.keypoints` (opt-in via `--mqtt-publish-pose`) |
|
||||
| Room active | `room_active` | `binary_sensor` | presence + motion >10% within a 30 s window; off after 10 min idle | after warmup |
|
||||
| No movement (safety) | `no_movement` | `binary_sensor` | presence + motion <1% for 30 min | after warmup |
|
||||
| Elderly inactivity anomaly | `elderly_inactivity_anomaly` | `binary_sensor` | stillness > 2× the longest stillness seen since start (floor 30 min) | after warmup |
|
||||
| Someone sleeping | `someone_sleeping` | `binary_sensor` | presence + motion <5% + BR ∈ [8,20] bpm for 5 min | after warmup, while breathing rate is available |
|
||||
| Possible distress | `possible_distress` | `binary_sensor` | HR ≥ 1.5× resting baseline + motion >20% + no fall, for 60 s | after warmup, while heart rate is available |
|
||||
| Fall risk elevated | `fall_risk_elevated` | `sensor` | 0–100 score: 10 per fall in 24 h + 50 × motion variance over 60 s | after warmup, while the fall detector is available |
|
||||
|
||||
Heart rate, breathing rate, and pose are **biometric** entities — they are stripped from MQTT (and never published over Matter) when `--privacy-mode` is set. See [Privacy](#privacy) below.
|
||||
Per-node motion is a three-level classification, so the motion thresholds above effectively mean "moving" or "not moving". The precision and recall figures in [`semantic-primitives-metrics.md`](./semantic-primitives-metrics.md) are CLAIMED: the replay tooling they cite is not in the repository. No `reason` attribute is published yet; the explanation tags stay server-side.
|
||||
|
||||
### Semantic automation primitives (10 entities)
|
||||
### Not announced
|
||||
|
||||
These are the inferred high-level states that customer automations actually use. Each one is a small finite-state machine running server-side with explicit warmup, hysteresis, and refractory windows. Per-primitive precision/recall is published in [`semantic-primitives-metrics.md`](./semantic-primitives-metrics.md).
|
||||
These entities from ADR-115 §3.1 and §3.12 are not announced, because the sensing broadcast has nothing to drive them. Announcing them only produced entities stuck at "unknown" ([#2085](https://github.com/ruvnet/RuView/issues/2085)).
|
||||
|
||||
| HA entity | Slug | HA component | What it fires on |
|
||||
|---|---|---|---|
|
||||
| Someone sleeping | `someone_sleeping` | `binary_sensor` | presence + motion<5% + BR ∈ [8,20] bpm sustained for 5 min |
|
||||
| Possible distress | `possible_distress` | `binary_sensor` | HR > 1.5× baseline + motion >20% + no fall, sustained 60 s |
|
||||
| Room active | `room_active` | `binary_sensor` | motion >10% in a 30-s rolling window |
|
||||
| Elderly inactivity anomaly | `elderly_inactivity_anomaly` | `binary_sensor` | idle > 2× observed-max-idle baseline |
|
||||
| Meeting in progress | `meeting_in_progress` | `binary_sensor` | ≥2 persons + low-amplitude motion for 10 min |
|
||||
| Bathroom occupied | `bathroom_occupied` | `binary_sensor` | presence + active zone tagged `bathroom` |
|
||||
| Fall risk elevated | `fall_risk_elevated` | `sensor` | 0–100 score; event fires on ≥70 crossing |
|
||||
| Bed exit (overnight) | `bed_exit` | `event` | sleeping → presence leaves bed zone between 22:00–06:00 |
|
||||
| No movement (safety) | `no_movement` | `binary_sensor` | presence + motion <1% for 30 min |
|
||||
| Multi-room transition | `multi_room_transition` | `event` | zone X exit + zone Y enter within 10 s |
|
||||
| Slug | Why not |
|
||||
|---|---|
|
||||
| `zone_occupancy`, `bathroom_occupied`, `bed_exit`, `multi_room_transition` | the broadcast carries no zones, and the server loads no zone-tag map |
|
||||
| `meeting_in_progress` | needs a 1–20 % motion level; per-node motion is three-level |
|
||||
| `pose` | `pose_keypoints` is never populated on the broadcast, and the `persons[].keypoints` skeletons are synthesized. `--mqtt-publish-pose` is accepted but logs a warning and announces nothing |
|
||||
|
||||
Every state change carries a `reason` attribute (e.g. `["motion<5%", "br=12bpm", "presence=true"]`) so you can template against it in HA automations to understand why an automation triggered.
|
||||
### Publish rates and retention
|
||||
|
||||
| Entity kind | When it publishes | QoS / retain |
|
||||
|---|---|---|
|
||||
| `presence` and semantic `binary_sensor`s | on change, plus a heartbeat every 60 s if unchanged | 1 / retained |
|
||||
| `person_count`, `motion_level`, `motion_energy`, `presence_score`, `fall_risk_elevated` | at most `--mqtt-rate-motion` / `--mqtt-rate-count` (default 1 Hz) | 0 / not retained |
|
||||
| `heart_rate`, `breathing_rate` | at most `--mqtt-rate-vitals` (default 0.2 Hz) | 0 / not retained |
|
||||
| `rssi` | at most `--mqtt-rate-rssi` (default 0.1 Hz) | 0 / not retained |
|
||||
| `fall` | once per detected fall | 1 / not retained |
|
||||
| availability | on every transition, and all topics every 30 s | 1 / retained |
|
||||
|
||||
Before [#2093](https://github.com/ruvnet/RuView/issues/2093), `presence` was published on every CSI frame (MEASURED 37.9 msg/s with one node, 1 msg per ~45 s after the fix; see [ADR-115 §11](../adr/ADR-115-home-assistant-integration.md#11-status-note-p45-semantic-states-on-the-live-publisher-2026-10-02)).
|
||||
|
||||
### Device identity
|
||||
|
||||
Each node's HA device id is `wifi_densepose_<client-id>-node<N>`, where `<N>` is the node id from the CSI frames. Without `--mqtt-client-id`, the server creates `wifi-densepose-<8 hex digits>` on first start and keeps it in `<data-dir>/mqtt_client_id`, so device ids survive restarts. Keep `--data-dir` on persistent storage (a Docker volume, for example). If the file cannot be written, the server logs a warning and falls back to a per-process id for that run. `--mqtt-client-id` overrides the persisted id. Use it to pin ids across reinstalls or to run two servers against one broker.
|
||||
|
||||
Releases before this change used `wifi-densepose-<pid>`, so every restart created a new set of devices. To remove the leftovers, publish an empty retained message to each stale `homeassistant/+/wifi_densepose_<old-id>…/+/config` topic, or delete the devices in Home Assistant.
|
||||
|
||||
### Matter device-type mapping
|
||||
|
||||
@@ -115,7 +138,7 @@ Per ADR-115 §3.11.1, the Matter Bridge exposes a subset on standard clusters so
|
||||
| `--mqtt-port <PORT>` | 1883 (8883 with TLS) | Broker port |
|
||||
| `--mqtt-username <U>` | — | Username for broker auth |
|
||||
| `--mqtt-password-env <VAR>` | `MQTT_PASSWORD` | Env var holding the password |
|
||||
| `--mqtt-client-id <ID>` | `wifi-densepose-<hostname>` | MQTT client ID |
|
||||
| `--mqtt-client-id <ID>` | persisted `wifi-densepose-<hex>` in `<data-dir>/mqtt_client_id` | MQTT client ID; prefixes every HA device id |
|
||||
| `--mqtt-prefix <PREFIX>` | `homeassistant` | Discovery topic prefix |
|
||||
| `--mqtt-tls` | off | Encrypt connection |
|
||||
| `--mqtt-ca-file <PATH>` | — | Pinned CA for TLS / mTLS |
|
||||
@@ -126,43 +149,18 @@ Per ADR-115 §3.11.1, the Matter Bridge exposes a subset on standard clusters so
|
||||
| `--mqtt-rate-motion <HZ>` | 1.0 | Motion publish rate (Hz) |
|
||||
| `--mqtt-rate-count <HZ>` | 1.0 | Person-count publish rate (Hz) |
|
||||
| `--mqtt-rate-rssi <HZ>` | 0.1 | RSSI publish rate (Hz) |
|
||||
| `--mqtt-publish-pose` | off | Enable pose-keypoint publication |
|
||||
| `--mqtt-rate-pose <HZ>` | 1.0 | Pose publish rate when enabled |
|
||||
| `--privacy-mode` | off | Strip HR/BR/pose from MQTT and Matter |
|
||||
| `--matter` | off | Enable the HA-FABRIC Matter Bridge |
|
||||
| `--matter-setup-file <PATH>` | — | Where to write the QR + manual code |
|
||||
| `--matter-reset` | off | Wipe fabric credentials and re-commission |
|
||||
| `--matter-vendor-id <VID>` | `0xFFF1` (dev) | CSA-assigned vendor ID |
|
||||
| `--matter-product-id <PID>` | `0x8001` | Product ID |
|
||||
| `--semantic` | on | Enable inference layer |
|
||||
| `--semantic-thresholds-file <PATH>` | — | Per-primitive threshold overrides |
|
||||
| `--semantic-zones-file <PATH>` | — | Zone-tag map (`bathroom`, `bedroom`, …) |
|
||||
| `--no-semantic <PRIMITIVE>` | — | Disable a specific primitive (repeatable) |
|
||||
| `--mqtt-publish-pose` | off | Accepted; no pose source exists, so it only logs a warning |
|
||||
| `--mqtt-rate-pose <HZ>` | 1.0 | Unused while no pose entity is announced |
|
||||
| `--privacy-mode` | off | Strip HR/BR/pose from MQTT, Matter, REST, WebSocket and recordings |
|
||||
| `--data-dir <DIR>` | `data` | Holds `mqtt_client_id` (and other server state) |
|
||||
|
||||
### Zone tag file format
|
||||
The `--semantic`, `--semantic-thresholds-file`, `--semantic-zones-file` and `--no-semantic` flags from ADR-115 §3.12.5 are **not accepted by `sensing-server` yet**. The primitives always run with the defaults in `PrimitiveConfig`, and no zone map is loaded.
|
||||
|
||||
```yaml
|
||||
# semantic-zones.yaml — passed to --semantic-zones-file
|
||||
zones:
|
||||
bathroom: ["zone_3", "zone_7"]
|
||||
bedroom: ["zone_1"]
|
||||
kitchen: ["zone_2"]
|
||||
living: ["zone_5"]
|
||||
bed_zones: ["zone_1"]
|
||||
```
|
||||
The same goes for the Matter Bridge flags (`--matter`, `--matter-setup-file`, `--matter-reset`, `--matter-vendor-id`, `--matter-product-id`). They're defined in the library's `cli::Args` but the binary doesn't parse them, so the Matter Bridge can't be started yet.
|
||||
|
||||
### Threshold overrides
|
||||
### Zone tags and threshold overrides (not implemented)
|
||||
|
||||
```yaml
|
||||
# semantic-thresholds.yaml — passed to --semantic-thresholds-file
|
||||
sleep_dwell_secs: 300
|
||||
distress_hr_multiple: 1.5
|
||||
room_active_motion_threshold: 0.10
|
||||
elderly_anomaly_multiple: 2.0
|
||||
meeting_min_persons: 2
|
||||
no_movement_dwell_secs: 1800
|
||||
fall_risk_event_threshold: 70.0
|
||||
```
|
||||
ADR-115 §3.12.5 specifies a zone-tag file and a threshold-override file. Neither is read by `sensing-server` today, which is why the zone-based primitives are not announced.
|
||||
|
||||
---
|
||||
|
||||
@@ -170,9 +168,11 @@ fall_risk_event_threshold: 70.0
|
||||
|
||||
When deploying in **healthcare**, **AAL (aging-in-place)**, or **commercial** settings, set `--privacy-mode`. This:
|
||||
|
||||
- **Strips** heart rate, breathing rate, and pose keypoints from every outbound MQTT publication.
|
||||
- **Suppresses discovery** for those entities entirely — HA never even sees they exist.
|
||||
- **Keeps every semantic primitive enabled.** Sleeping / distress / room-active / etc are *inferred* states. The inference happens server-side and only the boolean or score crosses the wire. This is the architectural win that makes the platform deployable in regulated contexts.
|
||||
- **Strips** heart rate and breathing rate from every outbound MQTT publication and, in the same way, from REST responses, WebSocket frames and recordings (pose is never announced over MQTT; see [Not announced](#not-announced)).
|
||||
- **Suppresses discovery** for those entities entirely: Home Assistant never sees them, and no availability or state topic is published for them.
|
||||
- **Keeps the semantic primitives enabled.** `someone_sleeping` and `possible_distress` still use breathing and heart rate *inside the server*; only the ON/OFF state crosses the wire (ADR-115 §3.12.3). If that is too much for your deployment, drop those two entities in Home Assistant.
|
||||
|
||||
With `--privacy-mode`, 13 entities are announced per node. Privacy mode reduces what leaves the server. It does not by itself make a deployment compliant with any regulation.
|
||||
|
||||
Always pair `--privacy-mode` with `--mqtt-tls` on non-localhost brokers.
|
||||
|
||||
@@ -210,7 +210,6 @@ action:
|
||||
title: "Possible distress detected"
|
||||
message: >
|
||||
RuView flagged sustained elevated heart rate + agitated motion.
|
||||
Reason: {{ state_attr(trigger.entity_id, 'reason') }}.
|
||||
```
|
||||
|
||||
### 2. Dim hallway when someone is sleeping
|
||||
@@ -256,6 +255,8 @@ action:
|
||||
|
||||
### 3. Wake-up routine on bed exit
|
||||
|
||||
> `bed_exit` is not published by `sensing-server` today (it needs a bed-zone map; see [Not announced](#not-announced)). This blueprint will not fire until that lands.
|
||||
|
||||
```yaml
|
||||
blueprint:
|
||||
name: RuView — wake-up routine on bed exit
|
||||
@@ -335,8 +336,8 @@ cards:
|
||||
entity: binary_sensor.ruview_kitchen_presence
|
||||
name: Kitchen
|
||||
- type: tile
|
||||
entity: binary_sensor.ruview_bathroom_occupied
|
||||
name: Bathroom
|
||||
entity: binary_sensor.ruview_bedroom_room_active
|
||||
name: Bedroom active
|
||||
```
|
||||
|
||||
---
|
||||
@@ -373,6 +374,15 @@ All three accept the same HA discovery topics RuView publishes. Performance and
|
||||
wscat -c ws://localhost:8765/ws/sensing
|
||||
```
|
||||
3. Confirm rate limits aren't dropping everything: `--mqtt-rate-vitals 1.0` for diagnosis (default 0.2 Hz = every 5 s).
|
||||
4. Unchanged binary states (presence, the semantic primitives) publish once per change and then every 60 s. A flat line in HA history is expected while nothing changes.
|
||||
|
||||
### Entity shows "unavailable"
|
||||
|
||||
The server has no source for it right now: heart and breathing rate need explicit calibration and a single occupant, `fall` needs a node that sends `edge_vitals`, and the semantic primitives wait 60 s after start (`someone_sleeping` and `possible_distress` also need breathing or heart rate). Check the availability topic with `mosquitto_sub -t 'homeassistant/+/+/<slug>/availability' -v`.
|
||||
|
||||
### Duplicate devices after a restart
|
||||
|
||||
Older releases derived device ids from the process id. See [Device identity](#device-identity) for how to remove the stale devices; the current default id is persisted in `--data-dir`.
|
||||
|
||||
### "Plaintext MQTT on non-localhost broker" WARN
|
||||
|
||||
@@ -383,6 +393,8 @@ Per [ADR-115 §3.9](../adr/ADR-115-home-assistant-integration.md#39-tls--auth),
|
||||
|
||||
### Matter pairing fails
|
||||
|
||||
The Matter Bridge can't be started from `sensing-server` yet (see the note under the CLI flags), so this is for when it is.
|
||||
|
||||
1. Check the setup code in your `--matter-setup-file` log (defaults to printing on startup).
|
||||
2. Make sure the host running `sensing-server` is on the same WiFi subnet as the controller.
|
||||
3. If Apple Home complains about an unknown vendor, that's expected — RuView uses dev VID `0xFFF1` until P10 (see [ADR §9.9](../adr/ADR-115-home-assistant-integration.md#9b-matter-path-p7p10)). Tap "Add anyway".
|
||||
@@ -391,7 +403,7 @@ Per [ADR-115 §3.9](../adr/ADR-115-home-assistant-integration.md#39-tls--auth),
|
||||
|
||||
## Applications — what people actually do with this
|
||||
|
||||
The 21 entities per node — 11 raw signals (presence, person count, breathing, heart rate, motion, RSSI, etc.) and 10 inferred semantic states (someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting-in-progress, bathroom-occupied, fall-risk-elevated, bed-exit, no-movement, multi-room-transition) — slot into Home Assistant like any other sensor. The list below groups real-world uses so you can pick the ones that match your space.
|
||||
The 15 entities per node (9 raw signals and 6 semantic states, see the [Entity reference](#entity-reference)) slot into Home Assistant like any other sensor. Some uses below rely on entities that are not announced yet (zones, bathroom, bed exit, meeting); those wait on a zone source or a finer motion level. The list below groups real-world uses so you can pick the ones that match your space.
|
||||
|
||||
### Personal & home
|
||||
|
||||
@@ -495,13 +507,9 @@ A few patterns appear over and over; if you understand these you can build most
|
||||
|
||||
### What about regulated environments?
|
||||
|
||||
Run RuView with `--privacy-mode` and only the 10 inferred semantic states reach Home Assistant — heart rate, breathing rate, and pose values are stripped at the MQTT wire. Per ADR-115 §6, this passes:
|
||||
With `--privacy-mode`, heart rate and breathing rate never reach the broker; their entities are not even announced. The semantic states still reach Home Assistant, and two of them (`someone_sleeping`, `possible_distress`) are derived from those vitals inside the server, so their ON/OFF values are themselves health-related information.
|
||||
|
||||
- **HIPAA-style minimum-necessary** (no biometric numbers leave the device)
|
||||
- **GDPR purpose-limitation** (the inferred states are the smallest dataset that supports the automation)
|
||||
- **CCPA "sensitive personal information"** (no health data crosses the wire)
|
||||
|
||||
The fall-risk-elevated / possible-distress / someone-sleeping flags still work — they're computed *inside* the sensor pipeline and only the boolean outputs are published. That's the architectural win that makes RuView deployable in care homes, hospitals, schools, and shared-housing scenarios where raw biometrics would be a non-starter.
|
||||
Privacy mode narrows what crosses the MQTT wire. It is not a compliance certification, and RuView makes no HIPAA, GDPR or CCPA compliance claim. Whether a deployment meets a regulation depends on the whole system: broker access control and TLS, Home Assistant's recorder retention, who can see the dashboards, consent, and local law. Assess that with whoever is responsible for compliance in your setting.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# MediaTek Router CSI (MT7981, experimental)
|
||||
|
||||
RuView can take channel state information (CSI) from a MediaTek Filogic
|
||||
router, such as an MT7981B with an MT7976C radio running OpenWrt with
|
||||
MediaTek's vendor CSI patch. The router reports CSI. A host-side bridge
|
||||
converts it to RuView's MTC1 wire format (ADR-267). The sensing server ingests
|
||||
it like any other UDP source.
|
||||
|
||||
**Status: experimental.** This page documents a working data path, measured
|
||||
on one board model. It makes no sensing-quality claim. Frames from this path
|
||||
carry the provenance label `mediatek:physical-unvalidated`. The ADR-266
|
||||
hardware gates are still open: calibration, sequence, timestamp, and
|
||||
repeatability.
|
||||
|
||||
Developed in whitsentry, a RuView-based project.
|
||||
|
||||
Evidence tags used below: **MEASURED** (with a reproducer), **CODE-DERIVED**
|
||||
(read from source or build scripts, not run here), **CLAIMED** (stated, not
|
||||
independently verified), **SYNTHETIC** (simulated or fabricated data).
|
||||
|
||||
## Contents
|
||||
|
||||
- [Supported hardware](#supported-hardware)
|
||||
- [How the path works](#how-the-path-works)
|
||||
- [Build the OpenWrt image](#build-the-openwrt-image)
|
||||
- [Arm CSI on the router](#arm-csi-on-the-router)
|
||||
- [Run the bridge](#run-the-bridge)
|
||||
- [Start the sensing server](#start-the-sensing-server)
|
||||
- [What the data is, and is not](#what-the-data-is-and-is-not)
|
||||
- [Evidence](#evidence)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Reference](#reference)
|
||||
|
||||
## Supported hardware
|
||||
|
||||
| Board | SoC + radio | Status |
|
||||
|---|---|---|
|
||||
| Wavlink WL-WN586X3 **Rev A** | MT7981BA + MT7976CN (switch MT7531AE) | Run on two units. CSI captured from both is analysed offline below (MEASURED). Live delivery to the sensing server is CLAIMED from local run logs that are not in the tree. |
|
||||
| OpenWrt One | MT7981B + MT7976C | CLAIMED: same silicon per the OpenWrt hardware table and ADR-266. Not run with this patch set. |
|
||||
| Xiaomi AX3000T | MT7981B + MT7976C | CLAIMED: same silicon per the OpenWrt hardware table. Not run with this patch set. |
|
||||
| Other MT7981/MT7986 boards | — | Untested. The CSI patch targets the `mt7915` driver family, so other Filogic radios may work, but nothing here shows that. |
|
||||
|
||||
The board patch in `firmware/openwrt-wn586x3/` is specific to the WN586X3
|
||||
**Rev A**. The CSI driver patch and the two userspace fixes do not depend on
|
||||
the board.
|
||||
|
||||
Each MT7976C radio has **two receive chains**. The WN586X3's four external
|
||||
antennas are a 2.4 GHz pair and a 5 GHz pair. It is not a 4x4 array, and two
|
||||
routers are two independent receivers, not one phased array.
|
||||
|
||||
## How the path works
|
||||
|
||||
```
|
||||
router (OpenWrt 24.10.8) host
|
||||
┌────────────────────────────────┐ ┌──────────────────────────────┐
|
||||
│ mt76 + MediaTek CSI patch │ │ wifi-densepose-mtk-bridge │
|
||||
│ firmware MCU event 0xc2 │ JSON dump │ groups chains into frames, │
|
||||
│ mt76-vendor dump csi ─────────┼──────────────►│ applies provenance gate, │
|
||||
│ CSIdump (UDP, optional) ───────┼──── UDP ─────►│ emits ADR-267 MTC1 │
|
||||
└────────────────────────────────┘ └──────────────┬───────────────┘
|
||||
│ UDP :5005
|
||||
┌──────────────▼───────────────┐
|
||||
│ sensing-server │
|
||||
│ --source mediatek │
|
||||
│ /api/v1/csi/mediatek/* │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
Two transports leave the router:
|
||||
|
||||
- **`mt76-vendor <iface> dump csi <n> <file>`** writes a JSON array of
|
||||
per-chain records: timestamp, transmitter address, RSSI, SNR, bandwidth,
|
||||
PPDU mode, tx/rx chain index, chain marker, and I/Q. This is the richer
|
||||
path, and the one that carried sustained use. You copy the file to the host
|
||||
(for example, a loop over SSH) and replay it through the bridge.
|
||||
- **`CSIdump`** (MtkCSIdump) streams one antenna per UDP datagram and drops
|
||||
RSSI, PPDU mode, and transmitter identity. The bridge emits one 1x1 frame per
|
||||
datagram. This path was verified end to end once, as a recording. It has not
|
||||
run as a standing service.
|
||||
|
||||
## Build the OpenWrt image
|
||||
|
||||
The patch set lives in [`firmware/openwrt-wn586x3/`](../firmware/openwrt-wn586x3/README.md).
|
||||
It is not a firmware image. Build and hash your own.
|
||||
|
||||
| Patch | Applies to |
|
||||
|---|---|
|
||||
| `1001-mtk-mt76-mt7915-csi-implement-csi-support-REBASED.patch` | `mt76` package (`package/kernel/mt76/patches/`) |
|
||||
| `mt76-vendor-csi-dump-bounds.patch` | `mt76-vendor` source (`feed/app/mt76-vendor/src/csi.c` in `mediatek/mtk-openwrt-feeds`) |
|
||||
| `csidump-nl80211-attr-enum.patch` | MtkCSIdump `src/wifi_drv_api/mt76_api.cpp` (only if you use CSIdump) |
|
||||
| `0001-wn586x3-reva-single-image-sysupgrade-and-generic-spi-nor.patch` | `target/linux/mediatek` (WN586X3 Rev A only) |
|
||||
|
||||
The route that produced the tested image (CODE-DERIVED from its build scripts)
|
||||
used the stock OpenWrt **24.10.8** `mediatek/filogic` SDK and ImageBuilder in
|
||||
an x86_64 Linux container:
|
||||
|
||||
1. **SDK.** Unpack `openwrt-sdk-24.10.8-mediatek-filogic`, then run
|
||||
`./scripts/feeds update -a && ./scripts/feeds install -a`.
|
||||
2. **CSI driver.** Copy the rebased `1001-...` patch into
|
||||
`feeds/base/package/kernel/mt76/patches/`. Raise `PKG_RELEASE` in that
|
||||
package's `Makefile` (the tested build used `99`) so the ImageBuilder
|
||||
prefers your build over the CDN package of the same version.
|
||||
3. **mt76-vendor.** Add MediaTek's `mt76-vendor` package from
|
||||
`mtk-openwrt-feeds` as `package/mt76-vendor`, with the bounds patch applied
|
||||
to `src/csi.c`.
|
||||
4. **Configure and compile.** Select `CONFIG_TARGET_mediatek_filogic_DEVICE_wavlink_wl-wn586x3`,
|
||||
`kmod-mt7915e`, `mt76-vendor`, and `libnl-tiny`. Then run
|
||||
`make package/feeds/base/mt76/compile package/mt76-vendor/compile`.
|
||||
5. **Image.** Copy the built `.ipk` files into the ImageBuilder's `packages/`
|
||||
and run:
|
||||
```bash
|
||||
make image PROFILE=wavlink_wl-wn586x3 \
|
||||
PACKAGES="kmod-mt7915e kmod-mt7981-firmware mt7981-wo-firmware mt76-vendor libnl-tiny"
|
||||
```
|
||||
6. **Check the manifest.** The image's `.manifest` must list `kmod-mt7915e`
|
||||
at your raised release (for example `-r99`) and `mt76-vendor`. If it lists
|
||||
the CDN release, the image has no CSI.
|
||||
|
||||
**Rev A board patch.** An ImageBuilder cannot compile a device tree, and its
|
||||
`make image` reuses the prebuilt DTB and FIT kernel. The tested Rev A build
|
||||
compiled the patched DTB itself, rebuilt the FIT kernel around it, added the
|
||||
patched `lib/upgrade/platform.sh` as a rootfs overlay, then checked that the
|
||||
final sysupgrade image embedded that DTB. A full OpenWrt buildroot applies
|
||||
`target/` patches directly and avoids those steps. That route was not tested
|
||||
here.
|
||||
|
||||
Flash only a Rev A unit, keep the stock firmware backup, and follow the
|
||||
OpenWrt wiki and forum thread for this board. Neither this guide nor the patch
|
||||
set names a LAN, an account, or a password.
|
||||
|
||||
**Firmware blobs.** The CSI report is MCU event `0xc2`. The MT7981 firmware in
|
||||
the tested image (`kmod-mt7981-firmware`, `mt7981-wo-firmware` from OpenWrt
|
||||
24.10.8) already emits it, so no firmware blob swap was needed on the tested
|
||||
board. Other firmware versions are untested.
|
||||
|
||||
## Arm CSI on the router
|
||||
|
||||
CSI follows traffic from **associated clients**. With nothing associated and
|
||||
transmitting, there is no CSI. Run these on the router, as root, against the AP
|
||||
interface (`phy0-ap0` was the 2.4 GHz AP on the tested board). The sequence
|
||||
below is CODE-DERIVED from the capture scripts used on the tested board:
|
||||
|
||||
```sh
|
||||
mt76-vendor phy0-ap0 set csi ctrl=0,0,0,0 # disarm and reset
|
||||
mt76-vendor phy0-ap0 set csi interval=1000
|
||||
mt76-vendor phy0-ap0 set csi ctrl=2,3,0,34 # frame-type filter, as used on
|
||||
mt76-vendor phy0-ap0 set csi ctrl=2,9,1,0 # the tested board (data frames)
|
||||
mt76-vendor phy0-ap0 set csi ctrl=1,0,0,0 # arm
|
||||
```
|
||||
|
||||
Check that the driver is collecting:
|
||||
|
||||
```sh
|
||||
cat /sys/kernel/debug/ieee80211/phy0/mt76/csi_stats # data_cnt should climb
|
||||
iw dev phy0-ap0 station dump | grep -E "Station|signal"
|
||||
```
|
||||
|
||||
Pull a batch:
|
||||
|
||||
```sh
|
||||
rm -f /tmp/csi.json # the tool APPENDS; remove first
|
||||
mt76-vendor phy0-ap0 dump csi 64 /tmp/csi.json
|
||||
```
|
||||
|
||||
`dump csi` needs the filename argument. Without it, the tool returns nothing.
|
||||
|
||||
## Run the bridge
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cargo build --release -p wifi-densepose-mtk-bridge
|
||||
B=./target/release/wifi-densepose-mtk-bridge
|
||||
```
|
||||
|
||||
**Dump path (recommended).** Copy `/tmp/csi.json` to the host, then replay it.
|
||||
A file cannot prove it came off a radio, so `--replay` refuses to start until
|
||||
you either attest the hardware or declare the file synthetic:
|
||||
|
||||
```bash
|
||||
$B --replay csi.json --captured-on <model>/<firmware> \
|
||||
--node <node-name> --center-freq-khz <khz> --sink 127.0.0.1:5005
|
||||
```
|
||||
|
||||
- `--node` is hashed into the 64-bit `device_id`. Give each router its own name.
|
||||
- `--center-freq-khz` is the AP channel's centre frequency, for example
|
||||
`2437000` for 2.4 GHz channel 6.
|
||||
- `--captured-on` takes `<model>/<firmware>`, for example
|
||||
`WN586X3-RevA/openwrt-24.10.8-csi`. It is logged at startup.
|
||||
|
||||
For a continuous feed, repeat dump, copy, and replay in a loop, one loop per
|
||||
router. Keep a copy of each dump only if you need it, and never commit one
|
||||
(see below).
|
||||
|
||||
**Streaming path (CSIdump).** Start `CSIdump phy0-ap0 <rate> 8888` on the
|
||||
router. Then, on the host:
|
||||
|
||||
```bash
|
||||
$B --listen 0.0.0.0:8888 --csidump <router-ip>:8888 --listen-allow <router-ip> \
|
||||
--node <node-name> --center-freq-khz <khz> --sink 127.0.0.1:5005 \
|
||||
--record captures/<node-name>.mtkcap
|
||||
```
|
||||
|
||||
`--listen-allow` is mandatory. Datagrams from any other source are dropped and
|
||||
counted. CSIdump disarms CSI when it receives SIGTERM. The dump path and
|
||||
CSIdump cannot share the driver's ring, so run one or the other.
|
||||
|
||||
**No router.** The fabricated fixture exercises the whole host path. It must
|
||||
be replayed with `--synthetic`, and the server labels it `mediatek:simulated`:
|
||||
|
||||
```bash
|
||||
$B --replay crates/wifi-densepose-mtk-bridge/fixtures/FABRICATED-mt76-vendor-dump-mt7981-bw80.synthetic.json \
|
||||
--synthetic --node demo --sink 127.0.0.1:5005
|
||||
```
|
||||
|
||||
The ADR-266 simulator is another synthetic source:
|
||||
|
||||
```bash
|
||||
cargo run -p wifi-densepose-hardware --bin mediatek-csi-sim -- \
|
||||
--profile mt7981 --udp 127.0.0.1:5005 --frames 1000 --realtime
|
||||
```
|
||||
|
||||
Full flag reference: [`v2/crates/wifi-densepose-mtk-bridge/README.md`](../v2/crates/wifi-densepose-mtk-bridge/README.md).
|
||||
|
||||
## Start the sensing server
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cargo run --release -p wifi-densepose-sensing-server -- --source mediatek \
|
||||
--activity-state <state-dir>/activity-floor.json
|
||||
```
|
||||
|
||||
- `--source mediatek` binds the UDP receiver (loopback by default, ADR-296) and
|
||||
does **not** start the ESP32-shaped simulator. Before this source existed,
|
||||
`--source mediatek` bound nothing.
|
||||
- If the bridge runs on another host, bind a routable address with
|
||||
`--udp-bind` and name the bridge host in `--udp-allow`.
|
||||
- `--activity-state` is optional. It persists each device's learned
|
||||
quiet-room activity floor so that a restart does not relearn it.
|
||||
- `--csi-ring <n>` sets frames retained per device for inspection (default
|
||||
256, about 0.5 MiB per 2x2 device at 64 subcarriers).
|
||||
|
||||
Endpoints:
|
||||
|
||||
| Route | Returns |
|
||||
|---|---|
|
||||
| `GET /api/v1/csi/mediatek/latest` | Last MTC1 snapshot, any device |
|
||||
| `GET /api/v1/csi/mediatek/devices` | One snapshot per `device_id`, with `age_ms`. Devices silent for over 30 s are omitted. |
|
||||
| `GET /api/v1/csi/mediatek/devices/:device_id/frames?n=&fields=amp,phase` | Newest `n` frames of per-chain, per-subcarrier amplitude and phase |
|
||||
| `GET /api/v1/csi/mediatek/devices/:device_id/summary` | Per-chain amplitude mean/std per subcarrier, RSSI per Rx chain, frame rate, heuristic window stats |
|
||||
| `GET /api/v1/csi/mediatek/activity?seconds=300` | About 1 Hz history of the relative activity index |
|
||||
| `GET /api/v1/sensing/latest` | Room update. On this source, `classifier` is `mediatek-amplitude-heuristic-v0`. |
|
||||
|
||||
Check the label on every snapshot:
|
||||
|
||||
| `source` | Meaning |
|
||||
|---|---|
|
||||
| `mediatek:simulated` | `SYNTHETIC` flag set. Nothing downstream can clear it. |
|
||||
| `mediatek:physical-unvalidated` | Real silicon, not calibrated. Everything this path produces today. |
|
||||
| `mediatek` | Real and calibrated. Reserved. Nothing emits it yet. |
|
||||
|
||||
## What the data is, and is not
|
||||
|
||||
**It is** complex channel estimates per receive chain from a real MT7976C
|
||||
radio, carried end to end with provenance intact.
|
||||
|
||||
**It is not:**
|
||||
|
||||
- **A sensor with its own clock.** The report rate is the clients' traffic
|
||||
rate. An idle phone gives a few frames per second, and a busy one gives
|
||||
hundreds. With no associated, transmitting client, there is no data. A
|
||||
stationary device that keeps transmitting to the AP (an "illuminator", for
|
||||
example an ESP32 sending a steady packet stream) gives a regular rate
|
||||
(CLAIMED: the 09-20 and 09-24 captures below include an illuminator's
|
||||
frames; transmitter identity was not re-checked in this analysis).
|
||||
- **Calibrated.** `scale` is 1.0 in raw firmware units. Nothing has been
|
||||
checked against a reference. The ADR-266 gates are open.
|
||||
- **Validated for presence, motion, or vital signs.** The room output on this
|
||||
source comes from `mediatek-amplitude-heuristic-v0` (presence and motion from
|
||||
mean amplitude) and `activity-index-v0` (fluctuation against a learned
|
||||
floor). Both are **CLAIMED**. Neither has been scored against labelled ground
|
||||
truth. `activity` blocks carry `is_occupancy_estimate: false`. These outputs
|
||||
do not feed the validated ESP32 vital-sign pipeline.
|
||||
- **Coherent across routers.** Each radio has its own oscillator. Cross-chain
|
||||
phase is meaningful within one radio, not between two boxes.
|
||||
- **Free of personal data.** A raw dump holds client transmitter addresses
|
||||
(MACs) and channel samples that encode movement. RuView policy (ADR-299)
|
||||
forbids committing captures. Keep them out of the repository, and run
|
||||
`scripts/csi-data-policy-check.sh --staged` before committing near capture
|
||||
directories.
|
||||
|
||||
## Evidence
|
||||
|
||||
| Claim | Tag | Basis |
|
||||
|---|---|---|
|
||||
| A real WL-WN586X3 frame decodes and reaches the server as `mediatek:physical-unvalidated` | MEASURED (path) | First record, 2026-09-19: legacy OFDM, 20 MHz, 64 subcarriers, one chain. Its shape is pinned by `wifi-densepose-mtk-bridge/tests/hardware_capture.rs`, which uses a fabricated record. The capture itself is not in the tree. |
|
||||
| Cross-chain phase is steady frame to frame on one MT7976C radio, and raw per-chain phase is not | MEASURED (offline) | See below |
|
||||
| `mediatek-amplitude-heuristic-v0`, `activity-index-v0` | CLAIMED | No labelled evaluation |
|
||||
| ADR-266 simulator and bridge fixtures | SYNTHETIC | Labelled on the wire |
|
||||
|
||||
**Cross-chain phase stability.** Six local dump captures from two WN586X3 units,
|
||||
2.4 GHz, 20 MHz HT frames carrying chains (tx0, rx0) and (tx0, rx1), adjacent
|
||||
frames under 100 ms apart, 56 subcarriers (DC and guard bins excluded). The
|
||||
two 2026-09-19 captures were analysed whole; the four 09-20 and 09-24 captures
|
||||
by their first 20 MB each.
|
||||
|
||||
| Quantity, per adjacent-frame step | Well-sampled transmitters (6, at 825 to 14,104 frames each) |
|
||||
|---|---|
|
||||
| Raw per-chain phase `arg(H0[k])`, per bin, p50 | 1.557 to 1.601 rad, with 49.6 to 50.9 % of steps over π/2 |
|
||||
| Cross-chain phase `arg(H0[k]·conj(H1[k]))`, per bin, p50 | 0.023 to 0.056 rad |
|
||||
| Cross-chain phase summed coherently over bins, p50 / p90 / p99 | 0.014–0.039 / 0.205–0.358 / 0.437–0.820 rad |
|
||||
|
||||
Uniformly random phase gives a step p50 of π/2 = 1.571 rad, and half of all
|
||||
steps exceed π/2. Raw per-chain phase is indistinguishable from that, as
|
||||
expected with per-packet CFO and phase offset. In the cross-chain term those
|
||||
offsets cancel, and the step drops to hundredths of a radian. Steps are
|
||||
wrapped, so p99 can never exceed π, and is not evidence by itself.
|
||||
|
||||
Transmitters with few, irregular frames (229 to 595 frames) were much worse:
|
||||
coherent-sum p50 0.177 to 0.456 rad, p99 1.857 to 2.849 rad. So the result
|
||||
holds only with a steady frame rate.
|
||||
|
||||
This shows a **stable phase observable exists** within one radio. It does
|
||||
**not** show that the observable tracks people, breathing, or anything else.
|
||||
|
||||
Reproduce it on your own capture (one `dump csi` array per line):
|
||||
|
||||
```bash
|
||||
python3 scripts/mediatek-csi-phase-stability.py <capture.jsonl> [max_MB]
|
||||
```
|
||||
|
||||
The script prints transmitters as `TX-A`, `TX-B`, and so on, never addresses.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `csi_stats` `data_cnt` stays at 0 | No associated client transmitting, or CSI not armed | Associate a client and generate traffic, then re-run the arm sequence |
|
||||
| `data_cnt` stopped after a client roamed or the filter changed | Changing the CSI filter stops capture | Re-run the arm sequence ending in `ctrl=1,0,0,0`. Re-arming periodically is a reasonable guard. |
|
||||
| `mt76-vendor dump csi` prints nothing | No filename argument | Use `dump csi <n> <file>` |
|
||||
| `mt76-vendor dump csi` segfaults | Unpatched tool: the driver can return n+1 records | Rebuild with `mt76-vendor-csi-dump-bounds.patch` |
|
||||
| Bridge warns about repeated `(ta, ts, tx_idx, rx_idx)` chains | The dump file was not removed between dumps, and the tool appends | `rm -f` the file before every dump |
|
||||
| `--replay` refuses to start | Provenance not declared | Add `--captured-on <model>/<firmware>`, or `--synthetic` for fabricated input |
|
||||
| `--captured-on` is rejected | The input declares itself synthetic (sidecar, `.synthetic.` name, or capture header) | Expected. Synthetic is sticky. Use `--synthetic`. |
|
||||
| `--listen` refuses to start | No `--listen-allow` | Name the router's address or subnet |
|
||||
| `rejected_source` counts climb on `--listen` | The router's address is not in `--listen-allow` | Fix the allowlist |
|
||||
| CSIdump runs but nothing arrives | Unpatched CSIdump uses an off-by-one netlink attribute enum against this driver | Rebuild with `csidump-nl80211-attr-enum.patch` |
|
||||
| CSI stopped after CSIdump exited | CSIdump disarms CSI on SIGTERM | Re-run the arm sequence |
|
||||
| `/api/v1/csi/mediatek/devices` is empty | Server not on `--source mediatek` (or `auto` without UDP), wrong `--sink` port, or a non-loopback bridge not in `--udp-allow` | Check `--source`, port 5005, `--udp-bind`, and `--udp-allow` |
|
||||
| Two routers show as one device | Same `--node` on both bridges | Give each router its own `--node` (or `--device-id`) |
|
||||
| A frame shows `ppdu_type: legacy` | Normal. Association and management traffic is pre-HT. | None. ADR-267 Amendment 1 carries these frames rather than dropping them. |
|
||||
| Room status reads `unknown` for a device | The heuristic abstains below its confidence threshold | Expected with sparse frames. It is not an "absent" call. |
|
||||
| Room reads `absent` with `confidence: 0.0` | Every device abstained, so there is no evidence either way | Treat confidence 0 as "no information", not as an empty room |
|
||||
| Image flashes, but there is no `mt76-vendor` or CSI | The ImageBuilder used the CDN `kmod-mt7915e` | Check the manifest for your raised `PKG_RELEASE` |
|
||||
|
||||
## Reference
|
||||
|
||||
- [ADR-266: MediaTek Filogic CSI platform](adr/ADR-266-mediatek-filogic-csi-platform.md) (Amendment 1: vendor CSI on MT7981)
|
||||
- [ADR-267: MediaTek MIMO CSI wire protocol](adr/ADR-267-mediatek-mimo-csi-wire-protocol.md) (Amendment 1: `Legacy` and `Unknown` PPDU types)
|
||||
- [`firmware/openwrt-wn586x3/`](../firmware/openwrt-wn586x3/README.md): patches, licences, origins
|
||||
- [`v2/crates/wifi-densepose-mtk-bridge/`](../v2/crates/wifi-densepose-mtk-bridge/README.md): bridge transports, provenance rules, frame mapping
|
||||
- [MediaTek OpenWrt feeds](https://git01.mediatek.com/openwrt/feeds/mtk-openwrt-feeds/), [MtkCSIdump](https://github.com/MtkWifiRev/MtkCSIdump), [OpenWrt One](https://openwrt.org/toh/openwrt/one)
|
||||