Compare commits

..

12 Commits

Author SHA1 Message Date
ruv d3c58145a4 docs(changelog): add ADR-061/062 QEMU testing platform entries
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 13:38:14 -04:00
ruv b41681e079 fix(firmware): make QEMU mock CSI boot successfully on Windows
Tested on Windows with Espressif QEMU 9.0.0 — firmware boots,
generates mock CSI frames, runs edge processing. 14/16 validation
checks pass.

Fixes:
- Guard wifi_init_sta() with CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
  (QEMU has no RF PHY, WiFi init stalled at calibration)
- Guard stream_sender_init_with() (UDP needs network stack)
- Guard ota_update_init_ex() (HTTP server needs network)
- Guard display_task_start() with CONFIG_DISPLAY_ENABLE
  (no I2C hardware in QEMU)
- Add mock_csi_init() call in main.c when CONFIG_CSI_MOCK_ENABLED
- Add #include "sdkconfig.h" to mock_csi.c (ESP-IDF not auto-including)
- Suppress unused s_bad_mac warning

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 13:33:07 -04:00
ruv 0f13a55f52 fix(installer): add Windows/MINGW detection with WSL/Docker guidance
Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 13:03:56 -04:00
ruv 71f9597f58 feat(scripts): add QEMU installer and unified CLI
install-qemu.sh (328 lines):
- Auto-detects OS (Ubuntu, Fedora, Arch, macOS, WSL)
- Installs build deps, clones Espressif QEMU fork, builds with SLIRP
- Symlinks to ~/.local/bin, verifies esp32s3 machine support
- Installs Python deps (esptool, pyyaml, esp-idf-nvs-partition-gen)
- Flags: --check, --uninstall, --install-dir, --branch, --skip-deps

qemu-cli.sh (362 lines):
- Single entry point for all QEMU operations
- 11 commands: install, test, mesh, swarm, snapshot, chaos, fuzz,
  nvs, health, status, help
- Auto-detects QEMU in PATH / ~/.espressif/qemu/ / QEMU_PATH env
- Status command shows install state of all tools
- Delegates to existing scripts with args passthrough

User guide updated to reference installer and CLI.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 12:55:28 -04:00
ruv bfe5cbc83a docs(readme): add ADR-062 swarm section, update ADR count and links
- Add collapsed QEMU Swarm Configurator section (ADR-062) with
  usage examples, topology/role/preset/assertion summaries
- Update ADR count from 49 to 62 in docs table
- Add user guide and ADR-061/062 links to Documentation Links section

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 12:47:39 -04:00
ruv 1e0af686a0 docs(user-guide): add QEMU testing and swarm configurator guide
Plain-language guide for testing firmware without hardware:
- What QEMU does and when to use it
- Prerequisites and install steps
- First test run walkthrough
- Understanding test output (exit codes, check names)
- Multi-node swarm testing with presets
- Writing custom YAML swarm configs
- Scenario table (10 mock CSI types)
- Topology options (star/mesh/line/ring)
- GDB debugging walkthrough
- Full test suite commands
- 6 QEMU troubleshooting entries

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 12:45:22 -04:00
ruv 21ec163941 fix(swarm): resolve 16 bugs from deep review of ADR-062
CRITICAL:
- Delete stale nvs_provision.bin before provisioning each node
- Fix log filename mismatch: swarm_health.py now finds qemu_node{i}.log
  with node_{i}.log fallback
- CI swarm-test job builds firmware instead of downloading missing artifact
- Accept both qemu_flash.bin and qemu_flash_base.bin as base image

HIGH:
- Replace broad "heap" substring match with precise regex patterns
  (HEAP_ERROR, heap_caps_alloc.*failed, etc.) to avoid false positives
- Guard os.geteuid() with hasattr for Windows compatibility
- Offset SLIRP ports by +100 to avoid collision with aggregator on 5005
- Assertions now WARN (not vacuous PASS) when no parseable data found

MEDIUM:
- Mark network_partitioned_recovery as "(future)" in ADR-062
- Fix node_id prefix dedup bug (node_1 no longer matches node_10)
- Add duplication note in qemu_swarm.py pointing to swarm_health.py
- Document implicit TDM auto-assignment in ADR YAML schema
- swarm_health.py only checks sensor nodes for frame production
- Fix channel 0 treated as falsy

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 12:36:25 -04:00
ruv a8f5276d9b feat(qemu): ADR-062 QEMU swarm configurator for multi-ESP32 testing
YAML-driven orchestrator for testing multiple ESP32-S3 QEMU instances
with configurable topologies (star/mesh/line/ring), role-based nodes
(sensor/coordinator/gateway), and swarm-level health assertions.

New files:
- ADR-062: architecture decision record
- qemu_swarm.py: main orchestrator (1097 lines)
  - YAML config parsing with schema validation
  - 4 topology implementations with TAP/SLIRP fallback
  - Per-node NVS provisioning via provision.py --dry-run
  - Signal-safe cleanup, dry-run mode, JSON results output
- swarm_health.py: 9-assertion health oracle (653 lines)
- 7 preset configs: smoke (2n/15s), standard (3n/60s),
  large-mesh (6n/90s), line-relay (4n/60s), ring-fault (4n/75s),
  heterogeneous (5n/90s), ci-matrix (3n/30s)
- CI: swarm-test job in firmware-qemu.yml

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 12:24:06 -04:00
ruv e574cbe129 fix(qemu): resolve 23 bugs from deep code review
CRITICAL:
- inject_fault.py: make nvs_corrupt write actual bytes via --flash arg;
  heap_exhaust and corrupt_frame now pause VM with honest WARNING about
  GDB stub requirement for real memory writes
- firmware-qemu.yml: remove github.run_id from cache key (was causing
  100% cache miss rate, rebuilding QEMU every run)
- mock_csi.c: change scenario_elapsed_ms() to int64_t (uint32 wrapped
  at ~49 days)

HIGH:
- qemu-mesh-test.sh: pass --results flag to validate_mesh_test.py
  (was passing positional arg to named-only parameter)
- test/Makefile: separate corpus directories per fuzz target
  (corpus_serialize/, corpus_edge/, corpus_nvs/)
- qemu-snapshot-test.sh: replace log truncation with tail-based
  extraction (truncation created sparse file while QEMU held fd)

MEDIUM:
- mock_csi.c: reset s_mac_filter_initialized in mock_csi_init()
- mock_csi.c: fix LFSR polynomial comment (32,31,29,1 not 32,22,2,1)
- sdkconfig.coverage: add FreeRTOS timer stack 4096 and WDT tuning
- firmware-qemu.yml: replace continue-on-error with FUZZER_CRASH env
- qemu-chaos-test.sh: rename heap_pressure to heap_exhaust for consistency
- validate_qemu_output.py: fix docstring "14 checks" -> "16 checks"
- generate_nvs_matrix.py: deduplicate temp file cleanup paths

LOW:
- mock_csi.c: remove M_PI float suffix, fix overflow burst flag
- qemu-snapshot-test.sh: fix now_ms() for macOS date +%s%N
- ADR-061: fix scenario 8 RSSI range to -90...-10 dBm
- launch.json: remove contradictory compound debug config

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 11:28:57 -04:00
ruv 1dbea4e9fb fix(scripts): improve usability across all ADR-061 QEMU testing scripts
- Add --help/-h flags to all 4 shell scripts with usage, env vars, examples
- Add prerequisite checks with install hints (apt/brew/pip) for missing tools
- Standardize exit codes (0=PASS, 1=WARN, 2=FAIL, 3=FATAL) across all scripts
- Standardize MESH_TIMEOUT to QEMU_TIMEOUT with backward compatibility
- Add SKIP_BUILD precheck for missing flash image in qemu-esp32s3-test.sh
- Add argparse to validate_qemu_output.py (was using raw sys.argv)
- Improve error messages in generate_nvs_matrix.py with NVS tool install hints
- Add socket connection warnings in inject_fault.py connect_monitor()
- Add example output epilog to check_health.py --help
- Add glossary (14 terms) and quick-start section to ADR-061
- Add GDB debugging walkthrough to ADR-061 Layer 4
- Fix stat portability in CI workflow (stat -c%s -> portable file_size())
- Add -type f to find commands in CI workflow

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 11:19:39 -04:00
ruv fb2d1afb0c feat(firmware): complete ADR-061 QEMU testing platform (all 9 layers)
Fix 9 bugs (LFSR bias, MAC filter init, scenario loop, NVS boundary
values), add 7 new files completing Layers 3 (mesh), 4 (GDB), 5
(coverage), 8 (snapshots), 9 (chaos testing), expand CI with fuzz
and NVS validation jobs, update README with full platform overview.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-14 11:08:59 -04:00
ruv ffeaa46bc6 feat(firmware): QEMU ESP32-S3 testing platform (ADR-061)
Implement full QEMU emulation framework for firmware testing without
physical hardware:

Mock CSI Generator (mock_csi.c):
- 10 test scenarios: empty room, static/walking person, fall, multi-person,
  channel sweep, MAC filter, ring overflow, boundary RSSI, zero-length
- Physics-based signal model with breathing modulation and Doppler
- LFSR pseudo-random noise, CONFIG_CSI_MOCK_ENABLED Kconfig guard
- Scenario 255 runs all sequentially

QEMU Runner & CI:
- qemu-esp32s3-test.sh: build, merge flash image, run QEMU, validate
- validate_qemu_output.py: 14 automated checks (boot, NVS, edge, vitals,
  crash detection) with colored output and severity-based exit codes
- generate_nvs_matrix.py: 14 NVS provisioning configs for matrix testing
- firmware-qemu.yml: GitHub Actions CI with 4-scenario matrix

Fuzz Testing:
- 3 libFuzzer targets: CSI serialize, NVS config validation, ring buffer
- Host-compilable ESP-IDF stubs (no ESP-IDF dependency for fuzzing)
- 6 seed corpus files for guided fuzzing
- Makefile with ASAN + UBSAN sanitizers

Documentation:
- firmware/esp32-csi-node/README.md: comprehensive QEMU testing guide
- Root README.md: collapsed QEMU testing section

Build verified: normal firmware build (RC=0) with mock_csi excluded.

Closes #259

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-13 09:17:07 -04:00
1570 changed files with 257612 additions and 3891 deletions
+1
View File
@@ -0,0 +1 @@
31273
+7 -22
View File
@@ -38,7 +38,7 @@ jobs:
with:
path: /opt/qemu-esp32
# Include date component so cache refreshes monthly when branch updates
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v5
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v4
restore-keys: |
qemu-esp32s3-${{ env.QEMU_BRANCH }}-
@@ -49,7 +49,6 @@ jobs:
sudo apt-get install -y \
git build-essential ninja-build pkg-config \
libglib2.0-dev libpixman-1-dev libslirp-dev \
libgcrypt20-dev \
python3 python3-venv
- name: Clone and build Espressif QEMU
@@ -114,9 +113,7 @@ jobs:
run: /opt/qemu-esp32/bin/qemu-system-xtensa --version
- name: Install Python dependencies
run: |
. $IDF_PATH/export.sh
pip install esptool esp-idf-nvs-partition-gen
run: pip install esptool esp-idf-nvs-partition-gen
- name: Set target ESP32-S3
working-directory: firmware/esp32-csi-node
@@ -134,7 +131,6 @@ jobs:
- name: Generate NVS matrix
run: |
. $IDF_PATH/export.sh
python3 scripts/generate_nvs_matrix.py \
--output-dir firmware/esp32-csi-node/build/nvs_matrix \
--only ${{ matrix.nvs_config }}
@@ -153,7 +149,6 @@ jobs:
python3 -m esptool --chip esp32s3 merge_bin \
-o build/qemu_flash.bin \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
--fill-flash-size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
$OTA_ARGS \
@@ -322,15 +317,13 @@ jobs:
uses: actions/download-artifact@v4
with:
name: qemu-esp32
path: /opt/qemu-esp32
path: ${{ github.workspace }}/qemu-build
- name: Make QEMU executable
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
run: chmod +x ${{ github.workspace }}/qemu-build/bin/qemu-system-xtensa
- name: Install Python dependencies
run: |
. $IDF_PATH/export.sh
pip install pyyaml esptool esp-idf-nvs-partition-gen
run: pip install pyyaml esptool esp-idf-nvs-partition-gen
- name: Build firmware for swarm
working-directory: firmware/esp32-csi-node
@@ -341,23 +334,15 @@ jobs:
python3 -m esptool --chip esp32s3 merge_bin \
-o build/qemu_flash.bin \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
--fill-flash-size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x20000 build/esp32-csi-node.bin
- name: Run swarm smoke test
run: |
. $IDF_PATH/export.sh
EXIT_CODE=0
python3 scripts/qemu_swarm.py --preset ci_matrix \
--qemu-path /opt/qemu-esp32/bin/qemu-system-xtensa \
--output-dir build/swarm-results || EXIT_CODE=$?
# Exit 0=PASS, 1=WARN (acceptable in CI without real hardware)
if [ "$EXIT_CODE" -gt 1 ]; then
echo "Swarm test failed with exit code $EXIT_CODE"
exit "$EXIT_CODE"
fi
--qemu-path ${{ github.workspace }}/qemu-build/bin/qemu-system-xtensa \
--output-dir build/swarm-results
timeout-minutes: 10
- name: Upload swarm results
+1 -15
View File
@@ -226,18 +226,4 @@ v1/src/sensing/mac_wifi
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Claude Flow runtime artifacts (auto-generated, machine-specific)
**/daemon.pid
**/pending-insights.jsonl
**/vectors.db
**/memory.db
**/.claude-flow/sessions/session-*.json
**/.claude-flow/sessions/current.json
# Node modules (should use npm ci, not committed)
**/node_modules/
# Local build scripts
firmware/esp32-csi-node/build_firmware.bat
.cursorindexingignore
BIN
View File
Binary file not shown.
-23
View File
@@ -5,29 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.5.0-esp32] — 2026-03-15
### Added
- **60 GHz mmWave sensor fusion (ADR-063)** — Auto-detects Seeed MR60BHA2 (60 GHz, HR/BR/presence) and HLK-LD2410 (24 GHz, presence/distance) on UART at boot. Probes 115200 then 256000 baud, registers device capabilities, starts background parser.
- **48-byte fused vitals packet** (magic `0xC5110004`) — Kalman-style fusion: mmWave 80% + CSI 20% when both available. Automatic fallback to standard 32-byte CSI-only packet.
- **Server-side fusion bridge** (`scripts/mmwave_fusion_bridge.py`) — Reads two serial ports simultaneously for dual-sensor setups where mmWave runs on a separate ESP32.
- **Multimodal ambient intelligence roadmap (ADR-064)** — 25+ applications from fall detection to sleep monitoring to RF tomography.
### Verified
- Real hardware: ESP32-S3 (COM7) WiFi CSI + ESP32-C6/MR60BHA2 (COM4) 60 GHz mmWave running concurrently. HR=75 bpm, BR=25/min at 52 cm range. All 11 QEMU CI jobs green.
## [v0.4.3-esp32] — 2026-03-15
### Fixed
- **Fall detection false positives (#263)** — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
- **Kconfig default mismatch** — `CONFIG_EDGE_FALL_THRESH` Kconfig default was still 2000 (=2.0) while `nvs_config.c` fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
- **provision.py NVS generator API change** — `esp_idf_nvs_partition_gen` package changed its `generate()` signature; switched to subprocess-first invocation for cross-version compatibility.
- **QEMU CI pipeline (11 jobs)** — Fixed all failures: fuzz test `esp_timer` stubs, QEMU `libgcrypt` dependency, NVS matrix generator, IDF container `pip` path, flash image padding, validation WARN handling, swarm `ip`/`cargo` missing.
### Added
- **4MB flash support (#265)** — `partitions_4mb.csv` and `sdkconfig.defaults.4mb` for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
- **`--strict` flag** for `validate_qemu_output.py` — WARNs now pass by default in CI (no real WiFi in QEMU); use `--strict` to fail on warnings.
## [Unreleased]
### Added
+5 -41
View File
@@ -70,17 +70,6 @@ All 5 ruvector crates integrated in workspace:
- ADR-031: RuView sensing-first RF mode (Proposed)
- ADR-032: Multistatic mesh security hardening (Proposed)
### Supported Hardware
| Device | Port | Chip | Role | Cost |
|--------|------|------|------|------|
| ESP32-S3 (8MB flash) | COM7 | Xtensa dual-core | WiFi CSI sensing node | ~$9 |
| ESP32-S3 SuperMini (4MB) | — | Xtensa dual-core | WiFi CSI (compact) | ~$6 |
| ESP32-C6 + Seeed MR60BHA2 | COM4 | RISC-V + 60 GHz FMCW | mmWave HR/BR/presence | ~$15 |
| HLK-LD2410 | — | 24 GHz FMCW | Presence + distance | ~$3 |
**Not supported:** ESP32 (original), ESP32-C3 — single-core, can't run CSI DSP pipeline.
### Build & Test Commands (this repo)
```bash
# Rust — full workspace tests (1,031+ tests, ~2 min)
@@ -90,6 +79,11 @@ cargo test --workspace --no-default-features
# Rust — single crate check (no GPU needed)
cargo check -p wifi-densepose-train --no-default-features
# Rust — publish crates (dependency order)
cargo publish -p wifi-densepose-core --no-default-features
cargo publish -p wifi-densepose-signal --no-default-features
# ... see crate publishing order below
# Python — deterministic proof verification (SHA-256)
python v1/data/proof/verify.py
@@ -97,36 +91,6 @@ python v1/data/proof/verify.py
cd v1 && python -m pytest tests/ -x -q
```
### ESP32 Firmware Build (Windows — Python subprocess required)
```bash
# Build 8MB firmware (real WiFi CSI mode, no mocks)
# See CLAUDE.local.md for the full Python subprocess command
# Key: must strip MSYSTEM env vars for ESP-IDF v5.4 on Git Bash
# Build 4MB firmware
cp sdkconfig.defaults.4mb sdkconfig.defaults
# then same build process
# Flash to COM7
# [python, idf_py, '-p', 'COM7', 'flash']
# Provision WiFi
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
# Monitor serial
python -m serial.tools.miniterm COM7 115200
```
### Firmware Release Process
1. Build 8MB from `sdkconfig.defaults.template` (no mock)
2. Build 4MB from `sdkconfig.defaults.4mb` (no mock)
3. Save 6 binaries: `esp32-csi-node.bin`, `bootloader.bin`, `partition-table.bin`, `ota_data_initial.bin`, `esp32-csi-node-4mb.bin`, `partition-table-4mb.bin`
4. Tag: `git tag v0.X.Y-esp32 && git push origin v0.X.Y-esp32`
5. Release: `gh release create v0.X.Y-esp32 <binaries> --title "..." --notes-file ...`
6. Verify on real hardware (COM7) before publishing
7. **CRITICAL:** Always test with real WiFi CSI, not mock mode — mock missed the Kconfig threshold bug
### Crate Publishing Order
Crates must be published in dependency order:
1. `wifi-densepose-core` (no internal deps)
+5 -14
View File
@@ -78,7 +78,6 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
| [Architecture Decisions](docs/adr/README.md) | 62 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
| [Desktop App](rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
| [Medical Examples](examples/medical/README.md) | Contactless blood pressure, heart rate, breathing rate via 60 GHz mmWave radar — $15 hardware, no wearable |
---
@@ -1039,7 +1038,7 @@ ESP32-S3 node UDP/5005 Host server (optional)
| Subcarriers per frame | 64 / 128 / 192 (depends on WiFi mode) |
| UDP latency | < 1 ms on local network |
| Presence detection range | Reliable at 3 m through walls |
| Binary size | 990 KB (8MB flash) / 773 KB (4MB flash) |
| Binary size | 947 KB (fits in 1 MB flash partition) |
| Boot to ready | ~3.9 seconds |
### Flash and provision
@@ -1048,25 +1047,17 @@ Download a pre-built binary — no build toolchain needed:
| Release | What's included | Tag |
|---------|-----------------|-----|
| [v0.5.0](https://github.com/ruvnet/RuView/releases/tag/v0.5.0-esp32) | **Stable**mmWave sensor fusion ([ADR-063](docs/adr/ADR-063-mmwave-sensor-fusion.md)), auto-detect MR60BHA2/LD2410, 48-byte fused vitals, all v0.4.3.1 fixes | `v0.5.0-esp32` |
| [v0.4.3.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.3.1-esp32) | Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash ([#265](https://github.com/ruvnet/RuView/issues/265)), watchdog fix ([#266](https://github.com/ruvnet/RuView/issues/266)) | `v0.4.3.1-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable**CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence and WASM modules ([ADR-039](docs/adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](docs/adr/ADR-040-wasm-programmable-sensing.md)) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
```bash
# 1. Flash the firmware to your ESP32-S3 (8MB flash — most boards)
# 1. Flash the firmware to your ESP32-S3
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
# 1b. For 4MB flash boards (e.g. ESP32-S3 SuperMini 4MB) — use the 4MB binaries:
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
@@ -1113,9 +1104,9 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
--edge-tier 2
# Fine-tune detection thresholds (fall-thresh in milli-units: 15000 = 15.0 rad/s²)
# Fine-tune detection thresholds
python firmware/esp32-csi-node/provision.py --port COM7 \
--edge-tier 2 --vital-int 500 --fall-thresh 15000 --subk-count 16
--edge-tier 2 --vital-int 500 --fall-thresh 5000 --subk-count 16
```
When Tier 2 is active, the node sends a 32-byte vitals packet once per second containing: presence, motion level, breathing BPM, heart rate BPM, confidence scores, fall alert flag, and occupancy count.
-261
View File
@@ -1,261 +0,0 @@
# ADR-063: 60 GHz mmWave Sensor Fusion with WiFi CSI
**Status:** Proposed
**Date:** 2026-03-15
**Deciders:** @ruvnet
**Related:** ADR-014 (SOTA signal processing), ADR-021 (vital sign extraction), ADR-029 (RuvSense multistatic), ADR-039 (edge intelligence), ADR-042 (CHCI coherent sensing)
## Context
RuView currently senses the environment using WiFi CSI — a passive technique that analyzes how WiFi signals are disturbed by human presence and movement. While this works through walls and requires no line of sight, CSI-derived vital signs (breathing rate, heart rate) are inherently noisy because they rely on phase extraction from multipath-rich WiFi channels.
A complementary sensing modality exists: **60 GHz mmWave radar** modules (e.g., Seeed MR60BHA2) that use active FMCW radar at 60 GHz to measure breathing and heart rate with clinical-grade accuracy. These modules are inexpensive (~$15), run on ESP32-C6/C3, and output structured vital signs over UART.
**Live hardware capture (COM4, 2026-03-15)** from a Seeed MR60BHA2 on an ESP32-C6 running ESPHome:
```
[D][sensor:093]: 'Real-time respiratory rate': Sending state 22.00000
[D][sensor:093]: 'Real-time heart rate': Sending state 92.00000 bpm
[D][sensor:093]: 'Distance to detection object': Sending state 0.00000 cm
[D][sensor:093]: 'Target Number': Sending state 0.00000
[D][binary_sensor:036]: 'Person Information': Sending state OFF
[D][sensor:093]: 'Seeed MR60BHA2 Illuminance': Sending state 0.67913 lx
```
### The Opportunity
Fusing WiFi CSI with mmWave radar creates a sensor system that is greater than the sum of its parts:
| Capability | WiFi CSI Alone | mmWave Alone | Fused |
|-----------|---------------|-------------|-------|
| Through-wall sensing | Yes (5m+) | No (LoS only, ~3m) | Yes — CSI for room-scale, mmWave for precision |
| Heart rate accuracy | ±5-10 BPM | ±1-2 BPM | ±1-2 BPM (mmWave primary, CSI cross-validates) |
| Breathing accuracy | ±2-3 BPM | ±0.5 BPM | ±0.5 BPM |
| Presence detection | Good (adaptive threshold) | Excellent (range-gated) | Excellent + through-wall |
| Multi-person | Via subcarrier clustering | Via range-Doppler bins | Combined spatial + RF resolution |
| Fall detection | Phase acceleration | Range/velocity + micro-Doppler | Dual-confirm reduces false positives to near-zero |
| Pose estimation | Via trained model | Not available | CSI provides pose; mmWave provides ground-truth vitals for training |
| Coverage | Whole room (passive) | ~120° cone, 3m range | Full room + precision zone |
| Cost per node | ~$9 (ESP32-S3) | ~$15 (ESP32-C6 + MR60BHA2) | ~$24 combined |
### RuVector Integration Points
The RuVector v2.0.4 stack (already integrated per ADR-016) provides the signal processing backbone:
| RuVector Component | Role in mmWave Fusion |
|-------------------|----------------------|
| `ruvector-attention` (`bvp.rs`) | Blood Volume Pulse estimation — mmWave heart rate can calibrate the WiFi CSI BVP phase extraction |
| `ruvector-temporal-tensor` (`breathing.rs`) | Breathing rate estimation — mmWave provides ground-truth for adaptive filter tuning |
| `ruvector-solver` (`triangulation.rs`) | Multilateration — mmWave range-gated distance + CSI amplitude = 3D position |
| `ruvector-attn-mincut` (`spectrogram.rs`) | Time-frequency decomposition — mmWave Doppler complements CSI phase spectrogram |
| `ruvector-mincut` (`metrics.rs`, DynamicPersonMatcher) | Multi-person association — mmWave target IDs help disambiguate CSI subcarrier clusters |
### RuvSense Integration Points
The RuvSense multistatic sensing pipeline (ADR-029) gains new capabilities:
| RuvSense Module | mmWave Integration |
|----------------|-------------------|
| `pose_tracker.rs` (AETHER re-ID) | mmWave distance + velocity as additional re-ID features for Kalman tracker |
| `longitudinal.rs` (Welford stats) | mmWave vitals as reference signal for CSI drift detection |
| `intention.rs` (pre-movement) | mmWave micro-Doppler detects pre-movement 100-200ms earlier than CSI |
| `adversarial.rs` (consistency check) | mmWave provides independent signal to detect CSI spoofing/anomalies |
| `coherence_gate.rs` | mmWave presence as additional gate input — if mmWave says "no person", CSI coherence gate rejects |
### Cross-Viewpoint Fusion Integration
The viewpoint fusion pipeline (`ruvector/src/viewpoint/`) extends naturally:
| Viewpoint Module | mmWave Extension |
|-----------------|-----------------|
| `attention.rs` (CrossViewpointAttention) | mmWave range becomes a new "viewpoint" in the attention mechanism |
| `geometry.rs` (GeometricDiversityIndex) | mmWave cone geometry contributes to Fisher Information / Cramer-Rao bounds |
| `coherence.rs` (phase phasor) | mmWave phase coherence as validation for WiFi phasor coherence |
| `fusion.rs` (MultistaticArray) | mmWave node becomes a member of the multistatic array with its own domain events |
## Decision
Add 60 GHz mmWave radar sensor support to the RuView firmware and sensing pipeline with auto-detection and device-specific capabilities.
### Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Sensing Node │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ ESP32-S3 │ │ ESP32-C6 │ │ Combined │ │
│ │ WiFi CSI │ │ + MR60BHA2 │ │ S3 + UART │ │
│ │ (COM7) │ │ 60GHz mmWave │ │ mmWave │ │
│ │ │ │ (COM4) │ │ │ │
│ │ Passive │ │ Active radar │ │ Both modes │ │
│ │ Through-wall │ │ LoS, precise │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │ │
│ └────────┬───────────┘ │ │
│ ▼ │ │
│ ┌────────────────┐ │ │
│ │ Fusion Engine │◄──────────────────────┘ │
│ │ │ │
│ │ • Kalman fuse │ Vitals packet (extended): │
│ │ • Cross-validate│ magic 0xC5110004 │
│ │ • Ground-truth │ + mmwave_hr, mmwave_br │
│ │ calibration │ + mmwave_distance │
│ │ • Fall confirm │ + mmwave_target_count │
│ └────────────────┘ + confidence scores │
└─────────────────────────────────────────────────────────┘
```
### Three Deployment Modes
**Mode 1: Standalone CSI (existing)** — ESP32-S3 only, WiFi CSI sensing.
**Mode 2: Standalone mmWave** — ESP32-C6 + MR60BHA2, precise vitals in a single room.
**Mode 3: Fused (recommended)** — ESP32-S3 + mmWave module on UART, or two separate nodes with server-side fusion.
### Auto-Detection Protocol
The firmware will auto-detect connected mmWave modules at boot:
1. **UART probe** — On configured UART pins, send the MR60BHA2 identification command (`0x01 0x01 0x00 0x01 ...`) and check for valid response header
2. **Protocol detection** — Identify the sensor family:
- Seeed MR60BHA2 (breathing + heart rate)
- Seeed MR60FDA1 (fall detection)
- Seeed MR24HPC1 (presence + light sleep/deep sleep)
- HLK-LD2410 (presence + distance)
- HLK-LD2450 (multi-target tracking)
3. **Capability registration** — Register detected sensor capabilities in the edge config:
```c
typedef struct {
uint8_t mmwave_detected; /** 1 if mmWave module found on UART */
uint8_t mmwave_type; /** Sensor family (MR60BHA2, MR60FDA1, etc.) */
uint8_t mmwave_has_hr; /** Heart rate capability */
uint8_t mmwave_has_br; /** Breathing rate capability */
uint8_t mmwave_has_fall; /** Fall detection capability */
uint8_t mmwave_has_presence; /** Presence detection capability */
uint8_t mmwave_has_distance; /** Range measurement capability */
uint8_t mmwave_has_tracking; /** Multi-target tracking capability */
float mmwave_hr_bpm; /** Latest heart rate from mmWave */
float mmwave_br_bpm; /** Latest breathing rate from mmWave */
float mmwave_distance_cm; /** Distance to nearest target */
uint8_t mmwave_target_count; /** Number of detected targets */
bool mmwave_person_present;/** mmWave presence state */
} mmwave_state_t;
```
### Supported Sensors
| Sensor | Frequency | Capabilities | UART Protocol | Cost |
|--------|-----------|-------------|---------------|------|
| **Seeed MR60BHA2** | 60 GHz | HR, BR, presence, illuminance | Seeed proprietary frames | ~$15 |
| **Seeed MR60FDA1** | 60 GHz | Fall detection, presence | Seeed proprietary frames | ~$15 |
| **Seeed MR24HPC1** | 24 GHz | Presence, sleep stage, distance | Seeed proprietary frames | ~$10 |
| **HLK-LD2410** | 24 GHz | Presence, distance (motion + static) | HLK binary protocol | ~$3 |
| **HLK-LD2450** | 24 GHz | Multi-target tracking (x,y,speed) | HLK binary protocol | ~$5 |
### Fusion Algorithms
**1. Vital Sign Fusion (Kalman filter)**
```
mmWave HR (high confidence, 1 Hz) ─┐
├─► Kalman fuse → fused HR ± confidence
CSI-derived HR (lower confidence) ─┘
```
**2. Fall Detection (dual-confirm)**
```
CSI phase accel > thresh ──────┐
├─► AND gate → confirmed fall (near-zero false positives)
mmWave range-velocity pattern ─┘
```
**3. Presence Validation**
```
CSI adaptive threshold ────┐
├─► Weighted vote → robust presence
mmWave target count > 0 ──┘
```
**4. Training Calibration**
```
mmWave ground-truth vitals → train CSI BVP extraction model
mmWave distance → calibrate CSI triangulation
mmWave micro-Doppler → label CSI activity patterns
```
### Vitals Packet Extension
Extend the existing 32-byte vitals packet (magic `0xC5110002`) with a new 48-byte fused packet:
```c
typedef struct __attribute__((packed)) {
/* Existing 32-byte vitals fields */
uint32_t magic; /* 0xC5110004 (fused vitals) */
uint8_t node_id;
uint8_t flags; /* Bit0=presence, Bit1=fall, Bit2=motion, Bit3=mmwave_present */
uint16_t breathing_rate; /* Fused BPM * 100 */
uint32_t heartrate; /* Fused BPM * 10000 */
int8_t rssi;
uint8_t n_persons;
uint8_t mmwave_type; /* Sensor type enum */
uint8_t fusion_confidence;/* 0-100 fusion quality score */
float motion_energy;
float presence_score;
uint32_t timestamp_ms;
/* New mmWave fields (16 bytes) */
float mmwave_hr_bpm; /* Raw mmWave heart rate */
float mmwave_br_bpm; /* Raw mmWave breathing rate */
float mmwave_distance; /* Distance to nearest target (cm) */
uint8_t mmwave_targets; /* Target count */
uint8_t mmwave_confidence;/* mmWave signal quality 0-100 */
uint16_t reserved;
} edge_fused_vitals_pkt_t;
_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48, "fused vitals must be 48 bytes");
```
### NVS Configuration
New provisioning parameters:
```bash
python provision.py --port COM7 \
--mmwave-uart-tx 17 --mmwave-uart-rx 18 \ # UART pins for mmWave module
--mmwave-type auto \ # auto-detect, or: mr60bha2, ld2410, etc.
--fusion-mode kalman \ # kalman, vote, mmwave-primary, csi-primary
--fall-dual-confirm true # require both CSI + mmWave for fall alert
```
### Implementation Phases
| Phase | Scope | Effort |
|-------|-------|--------|
| **Phase 1** | UART driver + MR60BHA2 parser + auto-detection | 2 weeks |
| **Phase 2** | Fused vitals packet + Kalman vital sign fusion | 1 week |
| **Phase 3** | Dual-confirm fall detection + presence voting | 1 week |
| **Phase 4** | HLK-LD2410/LD2450 support + multi-target fusion | 2 weeks |
| **Phase 5** | RuVector calibration pipeline (mmWave as ground truth) | 3 weeks |
| **Phase 6** | Server-side fusion for separate CSI + mmWave nodes | 2 weeks |
## Consequences
### Positive
- Near-zero false positive fall detection (dual-confirm)
- Clinical-grade vital signs when mmWave is present, with CSI as fallback
- Self-calibrating CSI pipeline using mmWave ground truth
- Backward compatible — existing CSI-only nodes work unchanged
- Low incremental cost (~$3-15 per mmWave module)
- Auto-detection means zero configuration for supported sensors
- RuVector attention/solver/temporal-tensor modules gain a high-quality reference signal
### Negative
- Added firmware complexity (~2-3 KB RAM for mmWave state + UART buffer)
- mmWave modules require line-of-sight (complementary to CSI, not replacement)
- Multiple UART protocols to maintain (Seeed, HLK families)
- 48-byte fused packet requires server parser update
### Neutral
- ESP32-C6 cannot run the full CSI pipeline (single-core RISC-V) but can serve as a dedicated mmWave bridge node
- mmWave modules add ~15 mA power draw per node
@@ -1,327 +0,0 @@
# ADR-064: Multimodal Ambient Intelligence — WiFi CSI + mmWave + Environmental Sensors
**Status:** Proposed
**Date:** 2026-03-15
**Deciders:** @ruvnet
**Related:** ADR-063 (mmWave fusion), ADR-039 (edge intelligence), ADR-042 (CHCI), ADR-029 (RuvSense multistatic), ADR-024 (AETHER contrastive embeddings)
## Context
With ADR-063 we demonstrated real-time fusion of WiFi CSI (ESP32-S3, COM7) and 60 GHz mmWave radar (Seeed MR60BHA2 on ESP32-C6, COM4). The live capture showed:
- **mmWave**: HR 75 bpm, BR 25/min, presence at 52 cm, 1.4 Hz update
- **WiFi CSI**: Channel 5, RSSI -41, 20+ Hz frame rate, through-wall coverage
- **BH1750**: Ambient light 0.0-0.7 lux (room darkness level)
This ADR explores the full spectrum of what becomes possible when these modalities are combined — from immediately practical applications to speculative research directions.
---
## Tier 1: Practical (Build Now)
### 1.1 Intelligent Fall Detection with Zero False Positives
**Current state:** CSI-only fall detection with 15.0 rad/s² threshold (v0.4.3.1).
**With fusion:** mmWave confirms fall via range-velocity signature (sudden height drop + impact deceleration). CSI provides the alert; mmWave provides the confirmation.
```
CSI phase acceleration > 15 rad/s² ─┐
├─► AND gate + temporal correlation
mmWave: height drop > 50cm in <1s ──┘ → CONFIRMED FALL (call 911)
```
**Impact:** Elderly care facilities spend $34B/year on fall injuries. A $24 sensor node with zero false positives replaces $200/month medical alert wearables that residents forget to wear.
### 1.2 Sleep Quality Monitoring
**Sensors used:** mmWave (BR/HR), CSI (bed occupancy, movement), BH1750 (light)
| Metric | Source | Method |
|--------|--------|--------|
| Sleep onset | CSI motion → still transition | Phase variance drops below threshold |
| Sleep stages | mmWave BR variability | BR 12-20 = light sleep, 6-12 = deep sleep |
| REM detection | mmWave HR variability | HR variability increases during REM |
| Restlessness | CSI motion energy | Counts of motion episodes per hour |
| Room darkness | BH1750 | Correlate light exposure with sleep latency |
| Wake events | CSI + mmWave | Motion + HR spike = awakening |
**Output:** Sleep score (0-100), time in each stage, disturbance log.
**No wearable required.** Works through a mattress.
### 1.3 Occupancy-Aware HVAC and Lighting
**Sensors:** CSI (room-level presence through walls), mmWave (precise count + distance), BH1750 (ambient light)
- CSI detects which rooms are occupied (through walls, whole-floor sensing)
- mmWave counts exact number of people in the sensor's room
- BH1750 measures if lights are on/needed
- System sends MQTT/UDP commands to smart home controllers
**Energy savings:** 20-40% HVAC reduction by not heating/cooling empty rooms.
### 1.4 Bathroom Safety for Elderly
**Sensor placement:** One CSI node outside bathroom (through-wall), one mmWave inside.
- CSI detects person entered bathroom (through-wall)
- mmWave monitors vitals while showering (waterproof enclosure)
- If no movement for > N minutes AND HR drops: alert
- Fall detection in shower (slippery surface = high risk)
### 1.5 Baby/Infant Breathing Monitor
**mmWave at crib-side:** Contactless breathing monitoring at 0.5-1m range.
- BR < 10 or BR = 0 for > 20s: alarm (apnea detection)
- CSI provides room context (parent present? other motion?)
- BH1750 tracks night feeding times (light on/off events)
---
## Tier 2: Advanced (Research Prototype)
### 2.1 Gait Analysis and Fall Risk Prediction
**Method:** CSI tracks walking pattern across the room; mmWave measures stride length and velocity.
| Feature | Source | Clinical Use |
|---------|--------|-------------|
| Gait velocity | mmWave Doppler | < 0.8 m/s = fall risk indicator |
| Stride variability | CSI phase patterns | High variability = cognitive decline marker |
| Turning stability | CSI + mmWave | Difficulty turning = Parkinson's indicator |
| Get-up time | mmWave (sit→stand) | Timed Up and Go (TUG) test, contactless |
**Clinical value:** Gait velocity is called the "sixth vital sign" — it predicts hospitalization, cognitive decline, and mortality. Currently requires a $10,000 GAITRite mat. A $24 sensor node replaces it.
### 2.2 Emotion and Stress Detection via Micro-Vitals
**mmWave at desk:** Continuous HR variability (HRV) monitoring during work.
- **HRV time-domain:** SDNN, RMSSD from beat-to-beat intervals
- **HRV frequency-domain:** LF/HF ratio (sympathetic/parasympathetic balance)
- Low HF power = stress; high HF = relaxation
- CSI detects fidgeting, posture shifts (correlated with stress)
- BH1750 correlates lighting with mood/productivity
**Application:** Smart office that adjusts lighting, temperature, and notification frequency based on detected stress level.
### 2.3 Gesture Recognition as Room Control
**CSI:** Already has DTW template matching gesture classifier (`ruvsense/gesture.rs`).
**mmWave:** Adds range-Doppler micro-gesture detection (hand wave, swipe, circle).
- CSI recognizes gross gestures (wave arm, walk pattern)
- mmWave recognizes fine hand gestures (swipe left/right, push/pull)
- Fused: spatial context (CSI knows where you are) + precise gesture (mmWave knows what your hand did)
**Use case:** Wave at the sensor to turn off lights. Swipe to change music. No voice assistant, no camera, no wearable.
### 2.4 Respiratory Disease Screening
**mmWave BR patterns over days/weeks:**
| Pattern | Indicator |
|---------|-----------|
| BR > 20 at rest, trending up | Possible pneumonia/COVID |
| Periodic breathing (Cheyne-Stokes) | Heart failure |
| Obstructive apnea pattern | Sleep apnea (> 5 events/hour) |
| BR variability decrease | COPD exacerbation |
**CSI adds:** Cough detection (sudden phase disturbance pattern), movement reduction (malaise indicator).
**Longitudinal tracking** via `ruvsense/longitudinal.rs` (Welford stats, biomechanics drift detection) — the system learns your normal breathing pattern and alerts on deviations.
### 2.5 Multi-Room Activity Recognition
**3-6 CSI nodes (through walls) + 1-2 mmWave (key rooms):**
```
Kitchen (CSI): person detected, high motion → cooking
Living room (mmWave + CSI): 2 people, low motion, HR stable → watching TV
Bedroom (CSI): person detected, minimal motion → sleeping
Bathroom (CSI): person entered 3 min ago, still inside → OK
Front door (CSI): motion pattern = leaving/arriving
```
**Output:** Activity timeline, daily routine deviation alerts, loneliness detection (no visitors in N days).
---
## Tier 3: Speculative (Research Frontier)
### 3.1 Cardiac Arrhythmia Detection
**mmWave at < 1m range:** Beat-to-beat interval extraction from chest wall displacement.
- Atrial fibrillation: irregular R-R intervals (coefficient of variation > 0.1)
- Bradycardia/tachycardia: sustained HR < 60 or > 100
- Premature ventricular contractions: occasional short-long-short patterns
**Challenge:** Requires sub-millimeter displacement resolution. The MR60BHA2 may lack the SNR for single-beat extraction, but clinical-grade 60 GHz modules (Infineon BGT60TR13C) can achieve this.
**CSI role:** Validates that the person is stationary (motion corrupts beat-to-beat analysis).
### 3.2 Blood Pressure Estimation (Contactless)
**Theory:** Pulse Transit Time (PTT) between two body points correlates with blood pressure. With two mmWave sensors at different body positions, PTT can be estimated from the phase difference of reflected chest/wrist signals.
**Feasibility:** Academic papers demonstrate ±10 mmHg accuracy in controlled settings. Far from clinical grade but useful for trending.
### 3.3 RF Tomography — 3D Occupancy Imaging
**Method:** Multiple CSI nodes form a tomographic array. Each TX-RX pair measures signal attenuation. Inverse problem (ISTA L1 solver, already in `ruvsense/tomography.rs`) reconstructs a 3D voxel grid of where absorbers (people) are.
**mmWave adds:** Range-gated targets as sparse priors for the tomographic reconstruction, dramatically reducing the ill-posedness of the inverse problem.
```
CSI tomography (coarse 3D grid, 50cm resolution) ─┐
├─► Sparse fusion
mmWave targets (precise range, cm resolution) ─────┘ → 10cm 3D occupancy map
```
### 3.4 Sign Language Recognition
**CSI phase patterns (body/arm movement) + mmWave Doppler (hand micro-movements):**
- CSI captures the gross arm trajectory of each sign
- mmWave captures the finger configuration at the pause point
- AETHER contrastive embeddings (`ADR-024`) learn to map (CSI phase sequence, mmWave Doppler) → sign label
- No camera required — works in the dark, preserves privacy
**Training data:** Record CSI + mmWave while performing signs with a camera as ground truth, then deploy camera-free.
### 3.5 Cognitive Load Estimation
**Multimodal features:**
| Feature | Source | Cognitive Load Indicator |
|---------|--------|------------------------|
| HR increase | mmWave | Sympathetic activation |
| BR irregularity | mmWave | Cognitive interference |
| Posture stiffness | CSI motion variance | Reduced when concentrating |
| Fidgeting frequency | CSI high-freq motion | Increases with frustration |
| Micro-saccade proxy | mmWave head micro-movement | Correlated with attention |
**Application:** Adaptive learning systems that slow down when the student is overloaded. Smart meeting rooms that detect when participants are disengaged.
### 3.6 Drone/Robot Navigation via RF Sensing
**CSI mesh as indoor GPS:** A network of CSI nodes creates a spatial RF fingerprint map. A robot or drone with an ESP32 can localize itself by matching its observed CSI to the map.
**mmWave on the robot:** Obstacle avoidance + human detection (don't collide with people).
**CSI from the environment:** Tells the robot where people are in adjacent rooms (through walls) so it can plan routes that avoid occupied spaces.
### 3.7 Building Structural Health Monitoring
**CSI multipath signature over months/years:**
- The CSI channel response is a fingerprint of the room's geometry
- Subtle shifts in multipath (wall crack propagation, foundation settlement) change the CSI signature
- `ruvsense/cross_room.rs` (environment fingerprinting) tracks these long-term drifts
- mmWave detects surface vibrations (micro-displacement from traffic, wind, seismic)
**Application:** Early warning for structural degradation in bridges, tunnels, old buildings.
### 3.8 Swarm Sensing — Emergent Spatial Awareness
**50+ nodes across a building:**
Each node runs local edge intelligence (ADR-039). The `hive-mind` consensus system (ADR-062) aggregates across nodes. Emergent behaviors:
- **Flow detection:** Track how people move between rooms over time
- **Anomaly detection:** "This hallway usually has 5 people/hour but had 0 today"
- **Emergency routing:** During fire, track which exits are blocked (no movement) vs available
- **Crowd density:** Concert/stadium safety — detect dangerous compression zones through walls
---
## Tier 4: Exotic / Sci-Fi Adjacent
### 4.1 Emotion Contagion Mapping
If multiple people are in a room and the system can estimate individual HR/HRV (via multi-target mmWave + CSI subcarrier clustering), you can detect:
- Physiological synchrony (two people's HR converging = rapport/empathy)
- Stress propagation (one person's stress → others' HR rises)
- "Emotional temperature" of a room
### 4.2 Dream State Detection and Lucid Dream Induction
During REM sleep (detected via mmWave HR variability + CSI minimal body movement):
- Detect REM onset with high confidence
- Trigger a subtle environmental cue (gentle light via smart bulb, barely audible tone)
- The sleeper incorporates the cue into the dream, recognizing it as a dream trigger
- BH1750 confirms room is dark (not a natural awakening)
Based on published lucid dreaming induction research (e.g., LaBerge's MILD technique with external cues).
### 4.3 Plant Growth Monitoring
WiFi signals pass through plant tissue differently based on water content.
- CSI amplitude through a greenhouse changes as plants absorb/release water
- mmWave reflects off leaf surfaces — micro-displacement from growth
- Long-term CSI drift correlates with biomass increase
Academic proof-of-concept: "Sensing Plant Water Content Using WiFi Signals" (2023).
### 4.4 Pet Behavior Analysis
- CSI detects pet movement patterns (different phase signature than humans — lower, faster)
- mmWave detects breathing rate (pets have higher BR than humans)
- System learns pet's daily routine and alerts on deviations (lethargy, pacing, not eating)
### 4.5 Paranormal Investigation Tool
(For the entertainment/hobbyist market)
- CSI detects "unexplained" signal disturbances in empty rooms
- mmWave confirms no physical presence
- System logs "anomalous RF events" with timestamps
- Export as Ghost Hunting report
**Actual explanation:** Temperature changes, HVAC drafts, and EMI cause CSI fluctuations. But it would sell.
---
## Implementation Priority Matrix
| Application | Sensors Needed | Effort | Value | Priority |
|------------|---------------|--------|-------|----------|
| Fall detection (zero false positive) | CSI + mmWave | 1 week | Critical (healthcare) | **P0** |
| Sleep monitoring | mmWave + BH1750 | 2 weeks | High (wellness) | **P1** |
| Occupancy HVAC/lighting | CSI + mmWave | 1 week | High (energy) | **P1** |
| Baby breathing monitor | mmWave | 1 week | Critical (safety) | **P1** |
| Bathroom safety | CSI + mmWave | 1 week | Critical (elderly) | **P1** |
| Gait analysis | CSI + mmWave | 3 weeks | High (clinical) | **P2** |
| Gesture control | CSI + mmWave | 4 weeks | Medium (UX) | **P2** |
| Multi-room activity | CSI mesh + mmWave | 4 weeks | High (elder care) | **P2** |
| Respiratory screening | mmWave longitudinal | 6 weeks | High (health) | **P2** |
| Stress/emotion detection | mmWave HRV + CSI | 6 weeks | Medium (wellness) | **P3** |
| RF tomography | CSI mesh + mmWave | 8 weeks | Medium (research) | **P3** |
| Sign language | CSI + mmWave + ML | 12 weeks | Medium (accessibility) | **P3** |
| Cardiac arrhythmia | High-res mmWave | 12 weeks | High (clinical) | **P3** |
| Swarm sensing | 50+ nodes | 16 weeks | High (safety) | **P3** |
## Decision
Document these possibilities as the product roadmap for the RuView multimodal ambient intelligence platform. Prioritize P0-P1 items (fall detection, sleep, occupancy, baby monitor, bathroom safety) for immediate implementation using the existing hardware (ESP32-S3 + MR60BHA2 + BH1750).
## Consequences
### Positive
- Positions RuView as a platform, not just a WiFi sensing demo
- Each application can ship as a WASM edge module (ADR-040), deployable to existing hardware
- Healthcare applications have clear regulatory paths (fall detection is FDA Class I exempt)
- Most P0-P1 applications require no additional hardware beyond what's already deployed
### Negative
- Clinical applications (arrhythmia, blood pressure) require medical device validation
- Privacy concerns scale with capability — need clear data retention policies
- Some exotic applications may attract scrutiny (surveillance concerns)
### Risk Mitigation
- All processing happens on-device (edge) — no cloud, no recordings by default
- No cameras — signal-based sensing preserves visual privacy
- Open source — users can audit exactly what is sensed and transmitted
+5 -16
View File
@@ -819,31 +819,20 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
| Release | What It Includes | Tag |
|---------|-----------------|-----|
| [v0.5.0](https://github.com/ruvnet/RuView/releases/tag/v0.5.0-esp32) | **Stable (recommended)** — mmWave sensor fusion (MR60BHA2/LD2410 auto-detect), 48-byte fused vitals, all v0.4.3.1 fixes | `v0.5.0-esp32` |
| [v0.4.3.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.3.1-esp32) | Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash ([#265](https://github.com/ruvnet/RuView/issues/265)), watchdog fix ([#266](https://github.com/ruvnet/RuView/issues/266)) | `v0.4.3.1-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence (ADR-039) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
> **Important:** Always use **v0.4.3.1 or later**. Earlier versions have false fall detection alerts (v0.4.2 and below) and CSI disabled in the build config (pre-v0.4.1).
> **Important:** Firmware versions prior to v0.4.1 had CSI **disabled** in the build config, causing a runtime error (`E wifi:CSI not enabled in menuconfig!`). Always use v0.4.1 or later.
```bash
# Flash an ESP32-S3 with 8MB flash (most boards)
# Flash an ESP32-S3 (requires esptool: pip install esptool)
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
```
**4MB flash boards** (e.g. ESP32-S3 SuperMini 4MB): download the 4MB binaries from the [v0.4.3 release](https://github.com/ruvnet/RuView/releases/tag/v0.4.3-esp32) and use `--flash-size 4MB`:
```bash
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
```
**Provisioning:**
```bash
@@ -905,14 +894,14 @@ Key NVS settings for edge processing:
|---------|---------|-----------------|
| `edge_tier` | 0 | Processing tier (0=off, 1=stats, 2=vitals) |
| `pres_thresh` | 50 | Sensitivity for presence detection (lower = more sensitive) |
| `fall_thresh` | 15000 | Fall detection threshold in milli-units (15000 = 15.0 rad/s²). Normal walking is 2-5, real falls are 20+. Raise to reduce false positives. |
| `fall_thresh` | 500 | Fall detection threshold (variance spike trigger) |
| `vital_win` | 300 | How many frames of phase history to keep for breathing/HR extraction |
| `vital_int` | 1000 | How often to send a vitals packet, in milliseconds |
| `subk_count` | 32 | Number of best subcarriers to keep (out of 56) |
When Tier 2 is active, the node sends a 32-byte vitals packet at 1 Hz (configurable) containing presence state, motion score, breathing BPM, heart rate BPM, confidence values, fall flag, and occupancy estimate. The packet uses magic `0xC5110002` and is sent to the same aggregator IP and port as raw CSI frames.
Binary size: 990 KB (8MB flash, 52% free) or 773 KB (4MB flash). v0.5.0 adds mmWave sensor fusion (~12 KB larger).
Binary size: 777 KB (24% free in the 1 MB app partition).
> **Alpha notice**: Vital sign estimation uses heuristic BPM extraction. Accuracy is best with stationary subjects in controlled environments. Not for medical use.
-56
View File
@@ -1,56 +0,0 @@
# Examples
Real-time sensing applications built on the RuView platform.
## Unified Dashboard (start here)
```bash
pip install pyserial numpy
python examples/ruview_live.py --csi COM7 --mmwave COM4
```
The live dashboard auto-detects available sensors and displays fused vitals, environment data, and events in real-time. Works with any combination of sensors.
## Individual Examples
| Example | Sensors | What It Does |
|---------|---------|-------------|
| [**ruview_live.py**](ruview_live.py) | CSI + mmWave + Light | Unified dashboard: HR, BR, BP, stress, presence, light, RSSI |
| [Medical: Blood Pressure](medical/) | mmWave | Contactless BP estimation from HRV |
| [Medical: Vitals Suite](medical/vitals_suite.py) | mmWave | 10-in-1: HR, BR, BP, HRV, sleep stages, apnea, cough, snoring, activity, meditation |
| [Sleep: Apnea Screener](sleep/) | mmWave | Detects breathing cessation events, computes AHI |
| [Stress: HRV Monitor](stress/) | mmWave | Real-time stress level from heart rate variability |
| [Environment: Room Monitor](environment/) | CSI + mmWave | Occupancy, light, RF fingerprint, activity events |
## Hardware
| Port | Device | Cost | What It Provides |
|------|--------|------|-----------------|
| COM7 | ESP32-S3 (WiFi CSI) | ~$9 | Presence, motion, breathing, heart rate (through walls) |
| COM4 | ESP32-C6 + Seeed MR60BHA2 | ~$15 | Precise HR/BR, presence, distance, ambient light |
Either sensor works alone. Both together enable fusion (mmWave 80% + CSI 20%).
## Quick Start
```bash
pip install pyserial numpy
# Unified dashboard (recommended)
python examples/ruview_live.py --csi COM7 --mmwave COM4
# Blood pressure estimation
python examples/medical/bp_estimator.py --port COM4
# Sleep apnea screening (run overnight)
python examples/sleep/apnea_screener.py --port COM4 --duration 28800
# Stress monitoring (workday session)
python examples/stress/hrv_stress_monitor.py --port COM4 --duration 3600
# Room environment monitor
python examples/environment/room_monitor.py --csi-port COM7 --mmwave-port COM4
# CSI only (no mmWave)
python examples/ruview_live.py --csi COM7 --mmwave none
```
-190
View File
@@ -1,190 +0,0 @@
#!/usr/bin/env python3
"""
Room Environment Monitor WiFi CSI + mmWave + Light Sensor Fusion
Combines all available sensors to build a real-time room awareness picture:
- WiFi CSI (COM7): Presence, motion energy, room RF fingerprint
- mmWave (COM4): Occupancy count, distance, HR/BR of nearest person
- BH1750 (COM4): Ambient light level
Detects: occupancy changes, lighting anomalies, activity patterns,
room RF fingerprint drift (door/window state changes).
Usage:
python examples/environment/room_monitor.py --csi-port COM7 --mmwave-port COM4
"""
import argparse
import collections
import math
import re
import serial
import sys
import threading
import time
RE_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)\s*bpm", re.IGNORECASE)
RE_BR = re.compile(r"'Real-time respiratory rate'.*?(\d+\.?\d*)", re.IGNORECASE)
RE_PRES = re.compile(r"'Person Information'.*?state\s+(ON|OFF)", re.IGNORECASE)
RE_DIST = re.compile(r"'Distance to detection object'.*?(\d+\.?\d*)\s*cm", re.IGNORECASE)
RE_LUX = re.compile(r"'Seeed MR60BHA2 Illuminance'.*?(\d+\.?\d*)\s*lx", re.IGNORECASE)
RE_TARGETS = re.compile(r"'Target Number'.*?(\d+\.?\d*)", re.IGNORECASE)
RE_CSI_CB = re.compile(r"CSI cb #(\d+).*?len=(\d+).*?rssi=(-?\d+)")
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
# Light categories
def light_category(lux):
if lux < 1: return "Dark"
if lux < 10: return "Dim"
if lux < 50: return "Low"
if lux < 200: return "Normal"
if lux < 500: return "Bright"
return "Very Bright"
def main():
parser = argparse.ArgumentParser(description="Room Environment Monitor")
parser.add_argument("--csi-port", default="COM7")
parser.add_argument("--mmwave-port", default="COM4")
parser.add_argument("--duration", type=int, default=120)
args = parser.parse_args()
# Shared state
state = {
"hr": 0.0, "br": 0.0, "presence_mw": False, "distance": 0.0,
"lux": 0.0, "targets": 0, "rssi": 0, "csi_frames": 0,
"mw_frames": 0, "events": [],
}
rssi_history = collections.deque(maxlen=60)
lux_history = collections.deque(maxlen=60)
lock = threading.Lock()
stop = threading.Event()
def read_mmwave():
try:
ser = serial.Serial(args.mmwave_port, 115200, timeout=1)
except Exception:
return
while not stop.is_set():
line = ser.readline().decode("utf-8", errors="replace")
clean = RE_ANSI.sub("", line)
with lock:
m = RE_HR.search(clean)
if m: state["hr"] = float(m.group(1)); state["mw_frames"] += 1
m = RE_BR.search(clean)
if m: state["br"] = float(m.group(1))
m = RE_PRES.search(clean)
if m:
new_pres = m.group(1) == "ON"
if new_pres != state["presence_mw"]:
event = f"Person {'arrived' if new_pres else 'left'} (mmWave)"
state["events"].append((time.time(), event))
state["presence_mw"] = new_pres
m = RE_DIST.search(clean)
if m: state["distance"] = float(m.group(1))
m = RE_LUX.search(clean)
if m:
lux = float(m.group(1))
old_cat = light_category(state["lux"])
new_cat = light_category(lux)
if old_cat != new_cat and state["lux"] > 0:
state["events"].append((time.time(), f"Light: {old_cat} -> {new_cat} ({lux:.1f} lx)"))
state["lux"] = lux
lux_history.append(lux)
m = RE_TARGETS.search(clean)
if m: state["targets"] = int(float(m.group(1)))
ser.close()
def read_csi():
try:
ser = serial.Serial(args.csi_port, 115200, timeout=1)
except Exception:
return
while not stop.is_set():
line = ser.readline().decode("utf-8", errors="replace")
m = RE_CSI_CB.search(line)
if m:
with lock:
state["csi_frames"] = int(m.group(1))
state["rssi"] = int(m.group(3))
rssi_history.append(int(m.group(3)))
ser.close()
t1 = threading.Thread(target=read_mmwave, daemon=True)
t2 = threading.Thread(target=read_csi, daemon=True)
t1.start()
t2.start()
print()
print("=" * 70)
print(" Room Environment Monitor (WiFi CSI + mmWave + Light)")
print("=" * 70)
print()
start_time = time.time()
last_print = 0
try:
while time.time() - start_time < args.duration:
time.sleep(1)
elapsed = int(time.time() - start_time)
if elapsed <= last_print or elapsed % 5 != 0:
continue
last_print = elapsed
with lock:
s = dict(state)
events = list(state["events"][-3:])
# RSSI stability (RF fingerprint drift)
rssi_std = 0
if len(rssi_history) >= 5:
vals = list(rssi_history)
mean = sum(vals) / len(vals)
rssi_std = math.sqrt(sum((x - mean)**2 for x in vals) / len(vals))
rf_status = "Stable" if rssi_std < 3 else "Shifting" if rssi_std < 6 else "Volatile"
pres = "YES" if s["presence_mw"] else "no"
lcat = light_category(s["lux"])
print(f" {elapsed:>4}s | Pres:{pres:>3} Dist:{s['distance']:>4.0f}cm | "
f"HR:{s['hr']:>3.0f} BR:{s['br']:>2.0f} | "
f"Light:{s['lux']:>5.1f}lx ({lcat:<6}) | "
f"RSSI:{s['rssi']:>3}dBm RF:{rf_status:<8} | "
f"CSI:{s['csi_frames']} MW:{s['mw_frames']}")
for ts, event in events:
age = elapsed - int(ts - start_time)
if age < 10:
print(f" ** EVENT: {event}")
except KeyboardInterrupt:
pass
stop.set()
time.sleep(1)
print()
print("=" * 70)
print(" ROOM SUMMARY")
print("=" * 70)
with lock:
print(f" Duration: {time.time()-start_time:.0f}s")
print(f" CSI frames: {state['csi_frames']}")
print(f" mmWave data: {state['mw_frames']} readings")
print(f" Last HR: {state['hr']:.0f} bpm")
print(f" Last BR: {state['br']:.0f}/min")
print(f" Light: {state['lux']:.1f} lux ({light_category(state['lux'])})")
if lux_history:
print(f" Light range: {min(lux_history):.1f} - {max(lux_history):.1f} lux")
if rssi_history:
print(f" RSSI range: {min(rssi_history)} to {max(rssi_history)} dBm (std={rssi_std:.1f})")
print(f" Events: {len(state['events'])}")
for ts, event in state["events"]:
print(f" [{int(ts-start_time):>4}s] {event}")
print()
if __name__ == "__main__":
main()
-111
View File
@@ -1,111 +0,0 @@
# Medical Sensing Examples
Contactless vital sign monitoring using 60 GHz mmWave radar — no wearable, no camera, no physical contact.
## Blood Pressure Estimator
Estimates blood pressure in real-time from heart rate variability (HRV) captured by a Seeed MR60BHA2 60 GHz mmWave radar module connected to an ESP32-C6.
### How It Works
The radar detects **microscopic chest wall displacement** caused by:
- **Respiration**: 0.1-1.0 mm displacement at 12-25 breaths/min
- **Cardiac pulse**: 0.01-0.1 mm displacement at 60-100 bpm
Modern 60 GHz FMCW radar resolves displacement down to **fractions of a millimeter**. Once the signal is isolated and filtered, the heartbeat-by-heartbeat pattern is remarkably clear.
From there, the estimator:
1. **Extracts beat-to-beat intervals** from the HR time series
2. **Computes HRV metrics**: SDNN (overall variability), LF/HF ratio (sympathetic/parasympathetic balance)
3. **Estimates blood pressure** using the correlation between HR, HRV, and cardiovascular tone:
- Higher HR → higher BP (sympathetic activation)
- Lower HRV (SDNN) → higher BP (reduced parasympathetic)
- Higher LF/HF ratio → higher BP (sympathetic dominance)
### Hardware Required
| Component | Cost | Role |
|-----------|------|------|
| ESP32-C6 + Seeed MR60BHA2 | ~$15 | 60 GHz mmWave radar (HR, BR, presence) |
| USB cable | — | Power + serial data |
That's it. Total cost: **~$15**.
### Quick Start
```bash
pip install pyserial numpy
# Basic (uncalibrated — shows trends)
python examples/medical/bp_estimator.py --port COM4
# Calibrated (take a real BP reading first, then enter it)
python examples/medical/bp_estimator.py --port COM4 \
--cal-systolic 120 --cal-diastolic 80 --cal-hr 72
```
### Sample Output (Real Hardware, 2026-03-15)
```
Contactless Blood Pressure Estimation (mmWave 60 GHz)
Time HR SBP DBP Category Samples
-------------------------------------------------------
15s | 64 | 117/78 | Normal | SDNN 22ms | n=4
20s | 65 | 117/78 | Normal | SDNN 28ms | n=5
25s | 71 | 119/79 | Normal | SDNN 88ms | n=9
30s | 77 | 122/81 | Elevated | SDNN 108ms | n=14
35s | 80 | 123/82 | Elevated | SDNN 106ms | n=18
40s | 80 | 123/82 | Elevated | SDNN 98ms | n=22
45s | 82 | 124/83 | Elevated | SDNN 97ms | n=26
50s | 83 | 125/83 | Elevated | SDNN 95ms | n=29
55s | 83 | 125/83 | Elevated | SDNN 92ms | n=32
60s | 84 | 125/83 | Elevated | SDNN 91ms | n=35
RESULT: 125/83 mmHg | HR 84 bpm | SDNN 91ms | 35 samples
```
### Accuracy
| Condition | Accuracy |
|-----------|----------|
| Uncalibrated, stationary | ±15-20 mmHg (trend tracking) |
| Calibrated, stationary | ±8-12 mmHg |
| Moving subject | Not reliable — wait for subject to be still |
Accuracy improves with:
- Longer recording duration (60s minimum, 120s recommended)
- Calibration with a real cuff reading
- Stationary subject within 1m of sensor
- Minimal environmental RF interference
### AHA Blood Pressure Categories
| Category | Systolic | Diastolic |
|----------|----------|-----------|
| Normal | < 120 | < 80 |
| Elevated | 120-129 | < 80 |
| High BP Stage 1 | 130-139 | 80-89 |
| High BP Stage 2 | 140+ | 90+ |
### Disclaimer
**This is NOT a medical device.** Blood pressure estimates from heart rate variability are approximations based on population-level correlations. Individual variation is significant. Always use a validated cuff-based sphygmomanometer for clinical decisions.
This tool is intended for:
- Research into contactless vital sign monitoring
- Wellness trend tracking (is my BP going up or down over days?)
- Technology demonstration
- Educational purposes
### How This Connects to RuView
This example is part of the [RuView](https://github.com/ruvnet/RuView) ambient intelligence platform. When combined with WiFi CSI sensing:
- **WiFi CSI** provides through-wall presence detection and room-scale activity recognition
- **mmWave radar** provides clinical-grade heart rate, breathing rate, and BP estimation
- **Sensor fusion** (ADR-063) combines both for zero false-positive fall detection and comprehensive health monitoring
- **RuVector** dynamic min-cut analysis treats physiological signals as a coherence graph, automatically separating noise, motion artifacts, and environmental interference
The result: cheap sensors ($15-24 per node), local computation (no cloud), real physiological understanding.
-376
View File
@@ -1,376 +0,0 @@
#!/usr/bin/env python3
"""
Contactless Blood Pressure Estimation via mmWave Heart Rate Variability
Reads real-time heart rate from a Seeed MR60BHA2 (60 GHz mmWave) sensor
and estimates blood pressure trends using the Pulse Transit Time (PTT)
correlation method.
Theory:
Blood pressure correlates inversely with Pulse Transit Time the time
for a pulse wave to travel from the heart to the periphery. While we
can't measure PTT directly with a single sensor, heart rate variability
(HRV) features specifically the ratio of low-frequency to high-frequency
power (LF/HF ratio) correlate with sympathetic nervous system activity,
which drives blood pressure changes.
The model uses:
1. Mean HR over a window baseline systolic/diastolic estimate
2. HR variability (SDNN) adjustment for sympathetic tone
3. LF/HF ratio from HR intervals fine adjustment
Calibration: Provide a known BP reading to anchor the estimates.
Without calibration, the system shows relative trends only.
NOT A MEDICAL DEVICE. For research and wellness tracking only.
Accuracy is ±15-20 mmHg without calibration. With calibration and
a stationary subject, ±8-12 mmHg is achievable for trending.
Usage:
python examples/medical/bp_estimator.py --port COM4
# With calibration (take a real BP reading first):
python examples/medical/bp_estimator.py --port COM4 \
--cal-systolic 120 --cal-diastolic 80 --cal-hr 72
Requirements:
pip install pyserial numpy
"""
import argparse
import collections
import math
import re
import sys
import time
import serial
try:
import numpy as np
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
# ---- ESPHome MR60BHA2 log parsing ----
RE_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)\s*bpm", re.IGNORECASE)
RE_BR = re.compile(r"'Real-time respiratory rate'.*?(\d+\.?\d*)", re.IGNORECASE)
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
class BPEstimator:
"""
Estimates blood pressure from heart rate time series.
Uses a physiological model:
SBP = a * HR + b * SDNN + c * (LF/HF) + offset_sys
DBP = d * HR + e * SDNN + f * (LF/HF) + offset_dia
Coefficients derived from published PTT-BP correlation studies:
- Mukkamala et al., "Toward Ubiquitous Blood Pressure Monitoring
via Pulse Transit Time", IEEE TBME 2015
- Ding et al., "Continuous Cuffless Blood Pressure Estimation
Using Pulse Transit Time and Photoplethysmogram", EMBC 2016
"""
# Population-average model coefficients
# These assume resting adult, seated position
HR_COEFF_SYS = 0.5 # mmHg per bpm
HR_COEFF_DIA = 0.3
SDNN_COEFF_SYS = -0.8 # Higher HRV → lower BP (parasympathetic)
SDNN_COEFF_DIA = -0.5
LFHF_COEFF_SYS = 3.0 # Higher sympathetic → higher BP
LFHF_COEFF_DIA = 2.0
# Population baseline (average resting adult)
BASE_SYS = 120.0
BASE_DIA = 80.0
BASE_HR = 72.0
def __init__(self, window_sec=60, cal_sys=None, cal_dia=None, cal_hr=None):
self.hr_history = collections.deque(maxlen=300) # 5 min at 1 Hz
self.hr_timestamps = collections.deque(maxlen=300)
self.window_sec = window_sec
# Calibration offsets
self.cal_offset_sys = 0.0
self.cal_offset_dia = 0.0
if cal_sys is not None and cal_hr is not None:
# Compute what the model would predict at calibration HR
predicted_sys = self.BASE_SYS + self.HR_COEFF_SYS * (cal_hr - self.BASE_HR)
self.cal_offset_sys = cal_sys - predicted_sys
if cal_dia is not None and cal_hr is not None:
predicted_dia = self.BASE_DIA + self.HR_COEFF_DIA * (cal_hr - self.BASE_HR)
self.cal_offset_dia = cal_dia - predicted_dia
def add_hr(self, hr_bpm: float) -> None:
"""Add a heart rate measurement."""
if hr_bpm <= 0 or hr_bpm > 220:
return
self.hr_history.append(hr_bpm)
self.hr_timestamps.append(time.time())
def _get_recent(self, window_sec: float):
"""Get HR values within the last window_sec seconds."""
now = time.time()
cutoff = now - window_sec
values = []
for t, hr in zip(self.hr_timestamps, self.hr_history):
if t >= cutoff:
values.append(hr)
return values
def _compute_sdnn(self, hrs: list) -> float:
"""Standard deviation of beat-to-beat intervals (SDNN proxy).
We don't have R-R intervals, so we approximate from HR:
RR_i 60 / HR_i (seconds)
SDNN = std(RR_i) * 1000 (milliseconds)
"""
if len(hrs) < 5:
return 50.0 # Default: normal HRV
rr_intervals = [60.0 / hr * 1000.0 for hr in hrs if hr > 0]
if len(rr_intervals) < 5:
return 50.0
if HAS_NUMPY:
return float(np.std(rr_intervals))
else:
mean = sum(rr_intervals) / len(rr_intervals)
variance = sum((x - mean) ** 2 for x in rr_intervals) / len(rr_intervals)
return math.sqrt(variance)
def _compute_lf_hf_ratio(self, hrs: list) -> float:
"""Estimate LF/HF ratio from HR variability.
LF (0.04-0.15 Hz): sympathetic + parasympathetic
HF (0.15-0.4 Hz): parasympathetic only
LF/HF > 2: sympathetic dominant (stress, higher BP)
LF/HF < 1: parasympathetic dominant (relaxed, lower BP)
Without true spectral analysis, we approximate from the
ratio of slow (>10s period) to fast (<7s period) HR fluctuations.
"""
if len(hrs) < 20:
return 1.5 # Default: slight sympathetic
if not HAS_NUMPY:
return 1.5 # Need numpy for spectral estimate
arr = np.array(hrs, dtype=float)
detrended = arr - np.mean(arr)
# Simple spectral power estimate via autocorrelation
n = len(detrended)
fft = np.fft.rfft(detrended)
psd = np.abs(fft) ** 2 / n
# Frequency bins (assuming 1 Hz sampling from mmWave)
freqs = np.fft.rfftfreq(n, d=1.0)
# LF band: 0.04-0.15 Hz
lf_mask = (freqs >= 0.04) & (freqs < 0.15)
lf_power = np.sum(psd[lf_mask]) if np.any(lf_mask) else 0.0
# HF band: 0.15-0.4 Hz
hf_mask = (freqs >= 0.15) & (freqs < 0.4)
hf_power = np.sum(psd[hf_mask]) if np.any(hf_mask) else 0.001
ratio = lf_power / max(hf_power, 0.001)
return min(max(ratio, 0.1), 10.0) # Clamp to reasonable range
def estimate(self) -> dict:
"""Estimate current blood pressure.
Returns dict with: systolic, diastolic, mean_hr, sdnn, lf_hf,
confidence (0-100), n_samples.
"""
recent = self._get_recent(self.window_sec)
if len(recent) < 3:
return {
"systolic": 0, "diastolic": 0,
"mean_hr": 0, "sdnn": 0, "lf_hf": 0,
"confidence": 0, "n_samples": len(recent),
"status": "Collecting data..."
}
mean_hr = sum(recent) / len(recent)
sdnn = self._compute_sdnn(recent)
lf_hf = self._compute_lf_hf_ratio(recent)
# Model
hr_delta = mean_hr - self.BASE_HR
sys = (self.BASE_SYS
+ self.HR_COEFF_SYS * hr_delta
+ self.SDNN_COEFF_SYS * (sdnn - 50.0) / 50.0
+ self.LFHF_COEFF_SYS * (lf_hf - 1.5)
+ self.cal_offset_sys)
dia = (self.BASE_DIA
+ self.HR_COEFF_DIA * hr_delta
+ self.SDNN_COEFF_DIA * (sdnn - 50.0) / 50.0
+ self.LFHF_COEFF_DIA * (lf_hf - 1.5)
+ self.cal_offset_dia)
# Physiological clamps
sys = max(80, min(200, sys))
dia = max(50, min(130, dia))
if dia >= sys:
dia = sys - 20
# Confidence based on data quality
conf = min(100, len(recent) * 2)
if self.cal_offset_sys != 0:
conf = min(100, conf + 20) # Calibrated = higher confidence
status = "Estimating"
if len(recent) < 10:
status = "Warming up..."
elif conf >= 80:
status = "Stable estimate"
return {
"systolic": round(sys),
"diastolic": round(dia),
"mean_hr": round(mean_hr, 1),
"sdnn": round(sdnn, 1),
"lf_hf": round(lf_hf, 2),
"confidence": conf,
"n_samples": len(recent),
"status": status,
}
def bp_category(sys: int, dia: int) -> str:
"""AHA blood pressure category."""
if sys == 0:
return ""
if sys < 120 and dia < 80:
return "Normal"
elif sys < 130 and dia < 80:
return "Elevated"
elif sys < 140 or dia < 90:
return "High BP Stage 1"
elif sys >= 140 or dia >= 90:
return "High BP Stage 2"
elif sys > 180 or dia > 120:
return "Hypertensive Crisis"
return "Unknown"
def main():
parser = argparse.ArgumentParser(
description="Contactless BP estimation from mmWave heart rate",
epilog="NOT A MEDICAL DEVICE. For research/wellness tracking only.",
)
parser.add_argument("--port", default="COM4", help="mmWave sensor serial port")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--window", type=int, default=60, help="Analysis window in seconds")
parser.add_argument("--cal-systolic", type=int, help="Calibration: your actual systolic BP")
parser.add_argument("--cal-diastolic", type=int, help="Calibration: your actual diastolic BP")
parser.add_argument("--cal-hr", type=int, help="Calibration: your HR at time of BP reading")
parser.add_argument("--duration", type=int, default=120, help="Recording duration in seconds")
args = parser.parse_args()
estimator = BPEstimator(
window_sec=args.window,
cal_sys=args.cal_systolic,
cal_dia=args.cal_diastolic,
cal_hr=args.cal_hr,
)
try:
ser = serial.Serial(args.port, args.baud, timeout=1)
except Exception as e:
print(f"Error opening {args.port}: {e}")
sys.exit(1)
print()
print("=" * 66)
print(" Contactless Blood Pressure Estimation (mmWave 60 GHz)")
print(" ⚠️ NOT A MEDICAL DEVICE — research/wellness only")
print("=" * 66)
if args.cal_systolic:
print(f" Calibrated: {args.cal_systolic}/{args.cal_diastolic} mmHg at {args.cal_hr} bpm")
else:
print(" Uncalibrated — showing relative trends. Use --cal-* for accuracy.")
print()
header = f" {'Time':>5} {'HR':>5} {'SBP':>5} {'DBP':>5} {'Category':>20} {'SDNN':>6} {'LF/HF':>6} {'Conf':>4} {'Status'}"
print(header)
print(" " + "-" * (len(header) - 2))
# Print initial blank lines for live update area
for _ in range(3):
print()
start = time.time()
last_print = 0
try:
while time.time() - start < args.duration:
line = ser.readline().decode("utf-8", errors="replace")
clean = RE_ANSI.sub("", line)
m = RE_HR.search(clean)
if m:
hr = float(m.group(1))
estimator.add_hr(hr)
# Update display every 3 seconds
elapsed = int(time.time() - start)
if elapsed > last_print and elapsed % 3 == 0:
last_print = elapsed
est = estimator.estimate()
if est["systolic"] > 0:
cat = bp_category(est["systolic"], est["diastolic"])
sys.stdout.write(f"\r {elapsed:>4}s {est['mean_hr']:>4.0f} "
f"{est['systolic']:>4} {est['diastolic']:>4} "
f"{cat:>20} {est['sdnn']:>5.1f} {est['lf_hf']:>5.2f} "
f"{est['confidence']:>3}% {est['status']}")
sys.stdout.write(" \n")
else:
sys.stdout.write(f"\r {elapsed:>4}s {'':>4} {'':>4} {'':>4} "
f"{'':>20} {'':>5} {'':>5} "
f"{'':>3} {est['status']}")
sys.stdout.write(" \n")
sys.stdout.flush()
except KeyboardInterrupt:
pass
ser.close()
# Final summary
est = estimator.estimate()
print()
print()
print("=" * 66)
print(" BLOOD PRESSURE ESTIMATION SUMMARY")
print("=" * 66)
if est["systolic"] > 0:
cat = bp_category(est["systolic"], est["diastolic"])
print(f" Systolic: {est['systolic']} mmHg")
print(f" Diastolic: {est['diastolic']} mmHg")
print(f" Category: {cat}")
print(f" Mean HR: {est['mean_hr']} bpm")
print(f" HRV (SDNN): {est['sdnn']} ms")
print(f" LF/HF ratio: {est['lf_hf']}")
print(f" Confidence: {est['confidence']}%")
print(f" Samples: {est['n_samples']} readings over {args.window}s window")
else:
print(" Insufficient data. Ensure person is within sensor range.")
print()
print(" ⚠️ This is an ESTIMATE based on HR/HRV correlation models.")
print(" For actual BP measurement, use a validated cuff device.")
print()
if __name__ == "__main__":
main()
-391
View File
@@ -1,391 +0,0 @@
#!/usr/bin/env python3
"""
RuView Medical Vitals Suite 10 capabilities from a single mmWave sensor
Capabilities:
1. Heart rate monitoring (continuous)
2. Breathing rate monitoring (continuous)
3. Blood pressure estimation (HRV-based)
4. HRV stress analysis (SDNN, RMSSD, pNN50, LF/HF)
5. Sleep stage classification (awake/light/deep/REM)
6. Apnea event detection (BR=0 for >10s)
7. Cough detection (BR spike pattern)
8. Snoring detection (periodic high-amplitude BR)
9. Activity state (resting/active/exercising)
10. Meditation quality scorer (coherence of BR+HR)
Usage:
python examples/medical/vitals_suite.py --port COM4 --duration 120
"""
import argparse
import collections
import math
import re
import serial
import sys
import time
try:
import numpy as np
HAS_NP = True
except ImportError:
HAS_NP = False
RE_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)\s*bpm", re.I)
RE_BR = re.compile(r"'Real-time respiratory rate'.*?(\d+\.?\d*)", re.I)
RE_PRES = re.compile(r"'Person Information'.*?state\s+(ON|OFF)", re.I)
RE_DIST = re.compile(r"'Distance to detection object'.*?(\d+\.?\d*)\s*cm", re.I)
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
class WelfordStats:
def __init__(self):
self.count = 0
self.mean = 0.0
self.m2 = 0.0
def update(self, v):
self.count += 1
d = v - self.mean
self.mean += d / self.count
self.m2 += d * (v - self.mean)
def std(self):
return math.sqrt(self.m2 / self.count) if self.count > 1 else 0.0
def cv(self):
return self.std() / self.mean if self.mean > 0 else 0.0
class VitalsSuite:
def __init__(self):
# Raw buffers
self.hr_buf = collections.deque(maxlen=300)
self.br_buf = collections.deque(maxlen=300)
self.hr_ts = collections.deque(maxlen=300)
self.br_ts = collections.deque(maxlen=300)
self.distance = 0.0
self.presence = False
self.frames = 0
# Welford trackers
self.hr_stats = WelfordStats()
self.br_stats = WelfordStats()
# Apnea detection
self.last_br_time = time.time()
self.last_nonzero_br = 0.0
self.apnea_events = []
self.in_apnea = False
self.apnea_start = 0.0
# Cough detection
self.cough_events = []
self.prev_br = 0.0
# Snoring detection
self.snore_events = 0
self.br_amplitude_buf = collections.deque(maxlen=30)
# Sleep state
self.sleep_state = "Awake"
self.sleep_onset = 0.0
# Meditation
self.meditation_score = 0.0
# Events
self.events = collections.deque(maxlen=50)
def feed(self, hr=0.0, br=0.0, presence=False, distance=0.0):
now = time.time()
self.presence = presence
self.distance = distance
self.frames += 1
if hr > 0:
self.hr_buf.append(hr)
self.hr_ts.append(now)
self.hr_stats.update(hr)
if br > 0:
self.br_buf.append(br)
self.br_ts.append(now)
self.br_stats.update(br)
self.last_br_time = now
self.last_nonzero_br = br
# Cough: sudden BR spike > 2x baseline
if self.prev_br > 0 and br > self.prev_br * 2.5 and self.br_stats.count > 10:
self.cough_events.append(now)
self.events.append((now, "Cough detected"))
# Snoring: track BR amplitude variation
if len(self.br_buf) >= 2:
amp = abs(br - list(self.br_buf)[-2])
self.br_amplitude_buf.append(amp)
self.prev_br = br
# End apnea
if self.in_apnea:
duration = now - self.apnea_start
self.apnea_events.append(duration)
self.events.append((now, f"Apnea ended ({duration:.0f}s)"))
self.in_apnea = False
else:
# Apnea: BR=0 for >10s
gap = now - self.last_br_time
if gap >= 10 and not self.in_apnea and self.br_stats.count > 5:
self.in_apnea = True
self.apnea_start = self.last_br_time
self.events.append((now, f"APNEA started (no breath for {gap:.0f}s)"))
# Sleep stage classification
self._classify_sleep()
# Meditation score
self._compute_meditation()
# Snoring: periodic high-amplitude BR oscillation
if len(self.br_amplitude_buf) >= 10:
amps = list(self.br_amplitude_buf)
mean_amp = sum(amps) / len(amps)
if mean_amp > 3.0 and self.sleep_state != "Awake":
self.snore_events += 1
def _classify_sleep(self):
"""Sleep stage from BR variability + HR patterns."""
hrs = list(self.hr_buf)
brs = list(self.br_buf)
if len(hrs) < 10 or len(brs) < 10:
self.sleep_state = "Awake"
return
recent_hr = hrs[-10:]
recent_br = brs[-10:]
mean_hr = sum(recent_hr) / len(recent_hr)
mean_br = sum(recent_br) / len(recent_br)
# HR variability of last 10 readings
hr_std = math.sqrt(sum((h - mean_hr) ** 2 for h in recent_hr) / len(recent_hr))
br_std = math.sqrt(sum((b - mean_br) ** 2 for b in recent_br) / len(recent_br))
# Activity check
if mean_hr > 100 or mean_br > 25:
self.sleep_state = "Awake"
return
# Low HR + low BR + low variability = deep sleep
if mean_hr < 60 and mean_br < 14 and hr_std < 3 and br_std < 1:
if self.sleep_state != "Deep Sleep":
self.events.append((time.time(), "Entered deep sleep"))
self.sleep_state = "Deep Sleep"
# Moderate HR + high HR variability = REM
elif hr_std > 5 and br_std > 2 and mean_br < 20:
if self.sleep_state != "REM":
self.events.append((time.time(), "Entered REM sleep"))
self.sleep_state = "REM"
# Low-moderate HR + low motion = light sleep
elif mean_hr < 75 and mean_br < 20:
if self.sleep_state != "Light Sleep":
self.events.append((time.time(), "Entered light sleep"))
self.sleep_state = "Light Sleep"
else:
self.sleep_state = "Awake"
def _compute_meditation(self):
"""Meditation quality: BR regularity + HR deceleration + HRV increase."""
brs = list(self.br_buf)
hrs = list(self.hr_buf)
if len(brs) < 15 or len(hrs) < 15:
self.meditation_score = 0.0
return
# BR regularity (lower CV = more regular breathing)
br_recent = brs[-15:]
br_mean = sum(br_recent) / len(br_recent)
br_std = math.sqrt(sum((b - br_mean) ** 2 for b in br_recent) / len(br_recent))
br_cv = br_std / br_mean if br_mean > 0 else 1.0
br_score = max(0, min(1, 1.0 - br_cv * 5)) # CV < 0.05 = perfect
# HR deceleration (lower HR = better)
hr_recent = hrs[-15:]
mean_hr = sum(hr_recent) / len(hr_recent)
hr_score = max(0, min(1, (90 - mean_hr) / 30)) # 60bpm=1.0, 90bpm=0.0
# HRV increase (higher SDNN = better)
rr = [60000 / h for h in hr_recent if h > 0]
if len(rr) >= 5:
rr_mean = sum(rr) / len(rr)
sdnn = math.sqrt(sum((r - rr_mean) ** 2 for r in rr) / len(rr))
hrv_score = max(0, min(1, sdnn / 100)) # 100ms SDNN = perfect
else:
hrv_score = 0.0
self.meditation_score = (br_score * 0.4 + hr_score * 0.3 + hrv_score * 0.3) * 100
def activity_state(self):
if len(self.hr_buf) < 3:
return "Unknown"
recent = list(self.hr_buf)[-5:]
mean_hr = sum(recent) / len(recent)
if mean_hr > 120:
return "Exercising"
elif mean_hr > 90:
return "Active"
elif mean_hr > 60:
return "Resting"
else:
return "Deep Rest"
def hrv(self):
hrs = list(self.hr_buf)
if len(hrs) < 5:
return {"sdnn": 0, "rmssd": 0, "pnn50": 0}
rr = [60000 / h for h in hrs if h > 0]
if len(rr) < 5:
return {"sdnn": 0, "rmssd": 0, "pnn50": 0}
mean = sum(rr) / len(rr)
sdnn = math.sqrt(sum((r - mean) ** 2 for r in rr) / len(rr))
diffs = [abs(rr[i + 1] - rr[i]) for i in range(len(rr) - 1)]
rmssd = math.sqrt(sum(d ** 2 for d in diffs) / len(diffs)) if diffs else 0
pnn50 = sum(1 for d in diffs if d > 50) / len(diffs) * 100 if diffs else 0
return {"sdnn": sdnn, "rmssd": rmssd, "pnn50": pnn50}
def bp(self):
hrs = list(self.hr_buf)
if len(hrs) < 5:
return 0, 0
mean_hr = sum(hrs) / len(hrs)
hrv = self.hrv()
if hrv["sdnn"] <= 0:
return 0, 0
delta = mean_hr - 72
sbp = round(max(80, min(200, 120 + 0.5 * delta - 0.8 * (hrv["sdnn"] - 50) / 50)))
dbp = round(max(50, min(130, 80 + 0.3 * delta - 0.5 * (hrv["sdnn"] - 50) / 50)))
return sbp, dbp
def stress(self):
h = self.hrv()
s = h["sdnn"]
if s <= 0: return "---"
if s < 30: return "HIGH"
if s < 50: return "Moderate"
if s < 80: return "Mild"
if s < 100: return "Relaxed"
return "Calm"
def main():
parser = argparse.ArgumentParser(description="Medical Vitals Suite (10 capabilities)")
parser.add_argument("--port", default="COM4")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--duration", type=int, default=120)
args = parser.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=1)
suite = VitalsSuite()
start = time.time()
last_print = 0
print()
print("=" * 80)
print(" RuView Medical Vitals Suite (10 capabilities from 1 sensor)")
print(" Point MR60BHA2 at yourself within 1m. Sit still.")
print("=" * 80)
print()
print(f"{'s':>4} {'HR':>4} {'BR':>3} {'BP':>7} {'Stress':>8} {'SDNN':>5} "
f"{'Sleep':>11} {'Activity':>10} {'Medit':>5} "
f"{'Apnea':>5} {'Cough':>5} {'Snore':>5}")
print("-" * 80)
try:
while time.time() - start < args.duration:
line = ser.readline().decode("utf-8", errors="replace")
clean = RE_ANSI.sub("", line)
hr, br, pres, dist = 0.0, 0.0, suite.presence, suite.distance
m = RE_HR.search(clean)
if m: hr = float(m.group(1))
m = RE_BR.search(clean)
if m: br = float(m.group(1))
m = RE_PRES.search(clean)
if m: pres = m.group(1) == "ON"
m = RE_DIST.search(clean)
if m: dist = float(m.group(1))
if hr > 0 or br > 0:
suite.feed(hr=hr, br=br, presence=pres, distance=dist)
elapsed = int(time.time() - start)
if elapsed > last_print and elapsed % 5 == 0:
last_print = elapsed
hrv = suite.hrv()
sbp, dbp = suite.bp()
bp_s = f"{sbp:>3}/{dbp:<3}" if sbp > 0 else " --- "
sdnn_s = f"{hrv['sdnn']:>5.0f}" if hrv["sdnn"] > 0 else " ---"
hrs = list(suite.hr_buf)
mean_hr = sum(hrs) / len(hrs) if hrs else 0
brs = list(suite.br_buf)
mean_br = sum(brs) / len(brs) if brs else 0
print(f"{elapsed:>3}s {mean_hr:>4.0f} {mean_br:>3.0f} {bp_s} {suite.stress():>8} {sdnn_s} "
f"{suite.sleep_state:>11} {suite.activity_state():>10} {suite.meditation_score:>5.0f} "
f"{len(suite.apnea_events):>5} {len(suite.cough_events):>5} {suite.snore_events:>5}")
# Print recent events
for ts, msg in list(suite.events)[-3:]:
if time.time() - ts < 6:
print(f" >> {msg}")
except KeyboardInterrupt:
pass
ser.close()
elapsed = time.time() - start
print()
print("=" * 80)
print(" VITALS SUITE SUMMARY")
print("=" * 80)
hrv = suite.hrv()
sbp, dbp = suite.bp()
hrs = list(suite.hr_buf)
brs = list(suite.br_buf)
print(f" Duration: {elapsed:.0f}s")
print(f" Readings: {suite.frames}")
print()
if hrs:
print(f" 1. Heart Rate: {sum(hrs)/len(hrs):.0f} bpm (range {min(hrs):.0f}-{max(hrs):.0f})")
if brs:
print(f" 2. Breathing: {sum(brs)/len(brs):.0f}/min (range {min(brs):.0f}-{max(brs):.0f})")
if sbp:
print(f" 3. BP Estimate: {sbp}/{dbp} mmHg")
if hrv["sdnn"] > 0:
print(f" 4. HRV/Stress: SDNN={hrv['sdnn']:.0f}ms RMSSD={hrv['rmssd']:.0f}ms pNN50={hrv['pnn50']:.1f}% -> {suite.stress()}")
print(f" 5. Sleep State: {suite.sleep_state}")
print(f" 6. Apnea Events: {len(suite.apnea_events)} {'(AHI=' + str(round(len(suite.apnea_events)/(elapsed/3600),1)) + '/hr)' if suite.apnea_events else ''}")
print(f" 7. Cough Events: {len(suite.cough_events)}")
print(f" 8. Snore Events: {suite.snore_events}")
print(f" 9. Activity: {suite.activity_state()}")
print(f" 10. Meditation: {suite.meditation_score:.0f}/100")
if suite.events:
print(f"\n Events ({len(suite.events)}):")
for ts, msg in list(suite.events)[-15:]:
print(f" [{int(ts-start):>4}s] {msg}")
print()
print(" NOT A MEDICAL DEVICE. For research/wellness only.")
print()
if __name__ == "__main__":
main()
-545
View File
@@ -1,545 +0,0 @@
#!/usr/bin/env python3
"""
RuView Live Ambient Intelligence Dashboard with RuVector Signal Processing
Fuses WiFi CSI (ESP32-S3) + 60 GHz mmWave (MR60BHA2) with signal processing
algorithms ported from RuView's Rust crates:
- wifi-densepose-vitals: BreathingExtractor (bandpass + zero-crossing),
HeartRateExtractor, VitalAnomalyDetector (Welford z-score)
- ruvsense/longitudinal: Drift detection via Welford online statistics
- ruvsense/adversarial: Signal consistency checks
- ruvsense/coherence: Z-score coherence scoring with DriftProfile
Usage:
python examples/ruview_live.py --csi COM7 --mmwave COM4
"""
import argparse
import collections
import math
import re
import serial
import sys
import threading
import time
try:
import numpy as np
HAS_NP = True
except ImportError:
HAS_NP = False
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
RE_MW_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)\s*bpm", re.I)
RE_MW_BR = re.compile(r"'Real-time respiratory rate'.*?(\d+\.?\d*)", re.I)
RE_MW_PRES = re.compile(r"'Person Information'.*?state\s+(ON|OFF)", re.I)
RE_MW_DIST = re.compile(r"'Distance to detection object'.*?(\d+\.?\d*)\s*cm", re.I)
RE_MW_LUX = re.compile(r"illuminance=(\d+\.?\d*)", re.I)
RE_CSI_CB = re.compile(r"CSI cb #(\d+).*?rssi=(-?\d+)")
RE_CSI_VITALS = re.compile(r"Vitals:.*?br=(\d+\.?\d*).*?hr=(\d+\.?\d*).*?motion=(\d+\.?\d*).*?pres=(\w+)", re.I)
RE_CSI_FALL = re.compile(r"Fall detected.*?accel=(\d+\.?\d*)")
RE_CSI_CALIB = re.compile(r"Adaptive calibration.*?threshold=(\d+\.?\d*)")
# ====================================================================
# RuVector-inspired signal processing (ported from Rust crates)
# ====================================================================
class WelfordStats:
"""Welford online statistics — from ruvsense/field_model.rs and vitals/anomaly.rs"""
def __init__(self):
self.count = 0
self.mean = 0.0
self.m2 = 0.0
def update(self, value):
self.count += 1
delta = value - self.mean
self.mean += delta / self.count
delta2 = value - self.mean
self.m2 += delta * delta2
def variance(self):
return self.m2 / self.count if self.count > 1 else 0.0
def std(self):
return math.sqrt(self.variance())
def z_score(self, value):
s = self.std()
return abs(value - self.mean) / s if s > 0 else 0.0
class VitalAnomalyDetector:
"""Ported from wifi-densepose-vitals/anomaly.rs — Welford z-score detection."""
def __init__(self, z_threshold=2.5):
self.z_threshold = z_threshold
self.hr_stats = WelfordStats()
self.br_stats = WelfordStats()
self.rr_stats = WelfordStats() # R-R interval stats
self.alerts = []
def check(self, hr=0.0, br=0.0):
self.alerts.clear()
if hr > 0:
if self.hr_stats.count >= 10:
z = self.hr_stats.z_score(hr)
if z > self.z_threshold:
if hr > self.hr_stats.mean:
self.alerts.append(("cardiac", "tachycardia", z, f"HR {hr:.0f} ({z:.1f}sd above baseline {self.hr_stats.mean:.0f})"))
else:
self.alerts.append(("cardiac", "bradycardia", z, f"HR {hr:.0f} ({z:.1f}sd below baseline {self.hr_stats.mean:.0f})"))
self.hr_stats.update(hr)
rr = 60000.0 / hr
self.rr_stats.update(rr)
if br > 0:
if self.br_stats.count >= 10:
z = self.br_stats.z_score(br)
if z > self.z_threshold:
self.alerts.append(("respiratory", "abnormal_rate", z, f"BR {br:.0f} ({z:.1f}sd from baseline {self.br_stats.mean:.0f})"))
elif br == 0 and self.br_stats.count > 5 and self.br_stats.mean > 5:
self.alerts.append(("respiratory", "apnea", 5.0, "Breathing stopped"))
self.br_stats.update(br)
return self.alerts
class LongitudinalTracker:
"""Ported from ruvsense/longitudinal.rs — drift detection over time."""
def __init__(self, drift_sigma=2.0, min_observations=10):
self.drift_sigma = drift_sigma
self.min_obs = min_observations
self.metrics = {} # name -> WelfordStats
def observe(self, metric_name, value):
if metric_name not in self.metrics:
self.metrics[metric_name] = WelfordStats()
self.metrics[metric_name].update(value)
def check_drift(self, metric_name, value):
if metric_name not in self.metrics:
return None
stats = self.metrics[metric_name]
if stats.count < self.min_obs:
return None
z = stats.z_score(value)
if z > self.drift_sigma:
direction = "above" if value > stats.mean else "below"
return f"{metric_name} drifting {direction} baseline ({z:.1f}sd, mean={stats.mean:.1f})"
return None
def summary(self):
result = {}
for name, stats in self.metrics.items():
result[name] = {"mean": stats.mean, "std": stats.std(), "n": stats.count}
return result
class CoherenceScorer:
"""Ported from ruvsense/coherence.rs — signal quality scoring."""
def __init__(self, decay=0.95):
self.decay = decay
self.score = 0.5
self.stale_count = 0
self.last_update = 0.0
def update(self, signal_quality):
"""signal_quality: 0.0 (bad) to 1.0 (perfect)."""
self.score = self.decay * self.score + (1 - self.decay) * signal_quality
self.last_update = time.time()
if signal_quality < 0.1:
self.stale_count += 1
else:
self.stale_count = 0
def is_coherent(self):
return self.score > 0.3 and self.stale_count < 10
def age_ms(self):
return int((time.time() - self.last_update) * 1000) if self.last_update > 0 else -1
class HRVAnalyzer:
"""Advanced HRV analysis — ported from wifi-densepose-vitals/heartrate.rs concepts."""
def __init__(self, window=60):
self.rr_intervals = collections.deque(maxlen=window)
def add_hr(self, hr):
if 30 < hr < 200:
self.rr_intervals.append(60000.0 / hr)
def compute(self):
rr = list(self.rr_intervals)
if len(rr) < 5:
return {"sdnn": 0, "rmssd": 0, "pnn50": 0, "lf_hf": 1.5, "n": len(rr)}
mean = sum(rr) / len(rr)
sdnn = math.sqrt(sum((x - mean) ** 2 for x in rr) / len(rr))
diffs = [abs(rr[i + 1] - rr[i]) for i in range(len(rr) - 1)]
rmssd = math.sqrt(sum(d ** 2 for d in diffs) / len(diffs)) if diffs else 0
pnn50 = sum(1 for d in diffs if d > 50) / len(diffs) * 100 if diffs else 0
# Spectral LF/HF estimate
lf_hf = 1.5
if HAS_NP and len(rr) >= 20:
arr = np.array(rr) - np.mean(rr)
fft = np.fft.rfft(arr)
psd = np.abs(fft) ** 2 / len(arr)
freqs = np.fft.rfftfreq(len(arr), d=1.0)
lf = np.sum(psd[(freqs >= 0.04) & (freqs < 0.15)])
hf = np.sum(psd[(freqs >= 0.15) & (freqs < 0.4)])
lf_hf = float(lf / max(hf, 0.001))
lf_hf = min(max(lf_hf, 0.1), 10.0)
return {"sdnn": sdnn, "rmssd": rmssd, "pnn50": pnn50, "lf_hf": lf_hf, "n": len(rr)}
class BPEstimator:
"""Blood pressure from HRV — calibratable."""
def __init__(self, cal_sys=None, cal_dia=None, cal_hr=None):
self.offset_sys = 0.0
self.offset_dia = 0.0
if cal_sys and cal_hr:
self.offset_sys = cal_sys - (120 + 0.5 * (cal_hr - 72))
if cal_dia and cal_hr:
self.offset_dia = cal_dia - (80 + 0.3 * (cal_hr - 72))
def estimate(self, hr, sdnn, lf_hf=1.5):
if hr <= 0 or sdnn <= 0:
return 0, 0
delta = hr - 72
sbp = 120 + 0.5 * delta - 0.8 * (sdnn - 50) / 50 + 3.0 * (lf_hf - 1.5) + self.offset_sys
dbp = 80 + 0.3 * delta - 0.5 * (sdnn - 50) / 50 + 2.0 * (lf_hf - 1.5) + self.offset_dia
return round(max(80, min(200, sbp))), round(max(50, min(130, dbp)))
# ====================================================================
# Sensor Hub
# ====================================================================
class SensorHub:
def __init__(self):
self.lock = threading.Lock()
self.mw_hr = 0.0
self.mw_br = 0.0
self.mw_presence = False
self.mw_distance = 0.0
self.mw_lux = 0.0
self.mw_frames = 0
self.mw_ok = False
self.csi_hr = 0.0
self.csi_br = 0.0
self.csi_motion = 0.0
self.csi_presence = False
self.csi_rssi = 0
self.csi_frames = 0
self.csi_ok = False
self.csi_fall = False
self.events = collections.deque(maxlen=50)
# RuVector processors
self.hrv = HRVAnalyzer()
self.anomaly = VitalAnomalyDetector()
self.longitudinal = LongitudinalTracker()
self.coherence_mw = CoherenceScorer()
self.coherence_csi = CoherenceScorer()
self.bp = BPEstimator()
def update_mw(self, **kw):
with self.lock:
for k, v in kw.items():
setattr(self, f"mw_{k}", v)
self.mw_ok = True
hr = kw.get("hr", 0)
br = kw.get("br", 0)
if hr > 0:
self.hrv.add_hr(hr)
self.longitudinal.observe("hr", hr)
self.coherence_mw.update(1.0)
else:
self.coherence_mw.update(0.1)
if br > 0:
self.longitudinal.observe("br", br)
alerts = self.anomaly.check(hr=hr, br=br)
for a in alerts:
self.events.append((time.time(), f"ANOMALY: {a[3]}"))
def update_csi(self, **kw):
with self.lock:
for k, v in kw.items():
setattr(self, f"csi_{k}", v)
self.csi_ok = True
rssi = kw.get("rssi", 0)
if rssi != 0:
self.longitudinal.observe("rssi", rssi)
self.coherence_csi.update(min(1.0, max(0.0, (rssi + 90) / 50)))
def add_event(self, msg):
with self.lock:
self.events.append((time.time(), msg))
def compute(self):
with self.lock:
hrv = self.hrv.compute()
mw_hr = self.mw_hr
csi_hr = self.csi_hr
if mw_hr > 0 and csi_hr > 0:
fused_hr = mw_hr * 0.8 + csi_hr * 0.2
hr_src = "Fused"
elif mw_hr > 0:
fused_hr = mw_hr
hr_src = "mmWave"
elif csi_hr > 0:
fused_hr = csi_hr
hr_src = "CSI"
else:
fused_hr = 0
hr_src = ""
mw_br = self.mw_br
csi_br = self.csi_br
fused_br = mw_br * 0.8 + csi_br * 0.2 if mw_br > 0 and csi_br > 0 else mw_br or csi_br
sbp, dbp = self.bp.estimate(fused_hr, hrv["sdnn"], hrv["lf_hf"])
# Stress from SDNN
sdnn = hrv["sdnn"]
if sdnn <= 0:
stress = ""
elif sdnn < 30:
stress = "HIGH"
elif sdnn < 50:
stress = "Moderate"
elif sdnn < 80:
stress = "Mild"
elif sdnn < 100:
stress = "Relaxed"
else:
stress = "Calm"
# Drift checks
drifts = []
for metric in ["hr", "br", "rssi"]:
val = {"hr": fused_hr, "br": fused_br, "rssi": self.csi_rssi}.get(metric, 0)
if val:
d = self.longitudinal.check_drift(metric, val)
if d:
drifts.append(d)
return {
"hr": fused_hr, "hr_src": hr_src,
"br": fused_br, "sbp": sbp, "dbp": dbp,
"stress": stress, "sdnn": sdnn, "rmssd": hrv["rmssd"],
"pnn50": hrv["pnn50"], "lf_hf": hrv["lf_hf"],
"presence": self.mw_presence or self.csi_presence,
"distance": self.mw_distance, "lux": self.mw_lux,
"rssi": self.csi_rssi, "motion": self.csi_motion,
"csi_frames": self.csi_frames, "mw_frames": self.mw_frames,
"coh_mw": self.coherence_mw.score, "coh_csi": self.coherence_csi.score,
"fall": self.csi_fall, "drifts": drifts,
"events": list(self.events),
"longitudinal": self.longitudinal.summary(),
}
# ====================================================================
# Serial readers
# ====================================================================
def reader_mmwave(port, baud, hub, stop):
try:
ser = serial.Serial(port, baud, timeout=1)
hub.add_event(f"mmWave: {port}")
except Exception as e:
hub.add_event(f"mmWave FAIL: {e}")
return
prev_pres = None
while not stop.is_set():
try:
line = ser.readline().decode("utf-8", errors="replace")
except Exception:
continue
c = RE_ANSI.sub("", line)
m = RE_MW_HR.search(c)
if m:
hub.update_mw(hr=float(m.group(1)), frames=hub.mw_frames + 1)
m = RE_MW_BR.search(c)
if m:
hub.update_mw(br=float(m.group(1)))
m = RE_MW_PRES.search(c)
if m:
p = m.group(1) == "ON"
if prev_pres is not None and p != prev_pres:
hub.add_event(f"Person {'arrived' if p else 'left'}")
prev_pres = p
hub.update_mw(presence=p)
m = RE_MW_DIST.search(c)
if m:
hub.update_mw(distance=float(m.group(1)))
m = RE_MW_LUX.search(c)
if m:
hub.update_mw(lux=float(m.group(1)))
ser.close()
def reader_csi(port, baud, hub, stop):
try:
ser = serial.Serial(port, baud, timeout=1)
hub.add_event(f"CSI: {port}")
except Exception as e:
hub.add_event(f"CSI FAIL: {e}")
return
while not stop.is_set():
try:
line = ser.readline().decode("utf-8", errors="replace")
except Exception:
continue
m = RE_CSI_VITALS.search(line)
if m:
hub.update_csi(br=float(m.group(1)), hr=float(m.group(2)),
motion=float(m.group(3)), presence=m.group(4).upper() == "YES")
m = RE_CSI_CB.search(line)
if m:
hub.update_csi(frames=int(m.group(1)), rssi=int(m.group(2)))
m = RE_CSI_FALL.search(line)
if m:
hub.update_csi(fall=True)
hub.add_event(f"FALL (accel={m.group(1)})")
m = RE_CSI_CALIB.search(line)
if m:
hub.add_event(f"CSI calibrated (thresh={m.group(1)})")
ser.close()
# ====================================================================
# Display
# ====================================================================
def run_display(hub, duration, interval):
start = time.time()
last = 0
print()
print("=" * 80)
print(" RuView Live — Ambient Intelligence + RuVector Signal Processing")
print("=" * 80)
print()
hdr = (f"{'s':>4} {'HR':>4} {'BR':>3} {'BP':>7} {'Stress':>8} "
f"{'SDNN':>5} {'RMSSD':>5} {'LF/HF':>5} "
f"{'Pres':>4} {'Dist':>5} {'Lux':>5} {'RSSI':>5} "
f"{'Coh':>4} {'CSI#':>5}")
print(hdr)
print("-" * 80)
while time.time() - start < duration:
time.sleep(0.5)
elapsed = int(time.time() - start)
if elapsed <= last or elapsed % interval != 0:
continue
last = elapsed
d = hub.compute()
hr_s = f"{d['hr']:>4.0f}" if d["hr"] > 0 else ""
br_s = f"{d['br']:>3.0f}" if d["br"] > 0 else ""
bp_s = f"{d['sbp']:>3}/{d['dbp']:<3}" if d["sbp"] > 0 else " —/— "
sdnn_s = f"{d['sdnn']:>5.0f}" if d["sdnn"] > 0 else ""
rmssd_s = f"{d['rmssd']:>5.0f}" if d["rmssd"] > 0 else ""
lfhf_s = f"{d['lf_hf']:>5.2f}" if d["sdnn"] > 0 else ""
pres_s = "YES" if d["presence"] else " no"
dist_s = f"{d['distance']:>4.0f}cm" if d["distance"] > 0 else ""
lux_s = f"{d['lux']:>5.1f}" if d["lux"] > 0 else ""
rssi_s = f"{d['rssi']:>5}" if d["rssi"] != 0 else ""
coh = max(d["coh_mw"], d["coh_csi"])
coh_s = f"{coh:>.2f}"
print(f"{elapsed:>3}s {hr_s} {br_s} {bp_s} {d['stress']:>8} "
f"{sdnn_s} {rmssd_s} {lfhf_s} "
f"{pres_s:>4} {dist_s} {lux_s} {rssi_s} "
f"{coh_s:>4} {d['csi_frames']:>5}")
for drift in d["drifts"]:
print(f" DRIFT: {drift}")
for ts, msg in d["events"][-3:]:
if time.time() - ts < interval + 1:
print(f" >> {msg}")
# Final summary
d = hub.compute()
print()
print("=" * 80)
print(" SESSION SUMMARY (RuVector Analysis)")
print("=" * 80)
sensors = []
if hub.mw_ok:
sensors.append(f"mmWave ({d['mw_frames']})")
if hub.csi_ok:
sensors.append(f"CSI ({d['csi_frames']})")
print(f" Sensors: {', '.join(sensors)}")
if d["hr"] > 0:
print(f" Heart Rate: {d['hr']:.0f} bpm ({d['hr_src']})")
if d["br"] > 0:
print(f" Breathing: {d['br']:.0f}/min")
if d["sbp"] > 0:
print(f" BP Estimate: {d['sbp']}/{d['dbp']} mmHg")
if d["sdnn"] > 0:
print(f" HRV SDNN: {d['sdnn']:.0f} ms — {d['stress']}")
print(f" HRV RMSSD: {d['rmssd']:.0f} ms")
print(f" HRV pNN50: {d['pnn50']:.1f}%")
print(f" LF/HF ratio: {d['lf_hf']:.2f} {'(sympathetic dominant)' if d['lf_hf'] > 2 else '(balanced)' if d['lf_hf'] > 0.5 else '(parasympathetic)'}")
if d["lux"] > 0:
print(f" Ambient Light: {d['lux']:.1f} lux")
# Longitudinal baselines
longi = d["longitudinal"]
if longi:
print(f" Baselines ({len(longi)} metrics tracked):")
for name, stats in sorted(longi.items()):
print(f" {name}: mean={stats['mean']:.1f} std={stats['std']:.1f} n={stats['n']}")
# Signal coherence
print(f" Coherence: mmWave={d['coh_mw']:.2f} CSI={d['coh_csi']:.2f}")
events = d["events"]
if events:
print(f" Events ({len(events)}):")
for ts, msg in events[-10:]:
print(f" {msg}")
print()
def main():
parser = argparse.ArgumentParser(description="RuView Live + RuVector Analysis")
parser.add_argument("--csi", default="COM7", help="CSI port (or 'none')")
parser.add_argument("--mmwave", default="COM4", help="mmWave port (or 'none')")
parser.add_argument("--duration", type=int, default=120)
parser.add_argument("--interval", type=int, default=3)
args = parser.parse_args()
hub = SensorHub()
stop = threading.Event()
if args.mmwave.lower() != "none":
threading.Thread(target=reader_mmwave, args=(args.mmwave, 115200, hub, stop), daemon=True).start()
if args.csi.lower() != "none":
threading.Thread(target=reader_csi, args=(args.csi, 115200, hub, stop), daemon=True).start()
time.sleep(2)
try:
run_display(hub, args.duration, args.interval)
except KeyboardInterrupt:
print("\nStopping...")
stop.set()
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
#!/usr/bin/env python3
"""
Sleep Apnea Screener Contactless via 60 GHz mmWave
Monitors breathing rate from MR60BHA2 and detects apnea events
(breathing cessation > 10 seconds). Clinical threshold: > 5 events/hour
= Obstructive Sleep Apnea (mild), > 15 = moderate, > 30 = severe.
Usage:
python examples/sleep/apnea_screener.py --port COM4
python examples/sleep/apnea_screener.py --port COM4 --duration 3600 # 1 hour
"""
import argparse
import collections
import re
import serial
import sys
import time
RE_BR = re.compile(r"'Real-time respiratory rate'.*?(\d+\.?\d*)", re.IGNORECASE)
RE_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)", re.IGNORECASE)
RE_PRES = re.compile(r"'Person Information'.*?state\s+(ON|OFF)", re.IGNORECASE)
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
APNEA_THRESHOLD_SEC = 10 # Breathing absent for >10s = apnea event
HYPOPNEA_BR = 6.0 # BR < 6/min = hypopnea (shallow breathing)
def main():
parser = argparse.ArgumentParser(description="Sleep Apnea Screener (mmWave)")
parser.add_argument("--port", default="COM4")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--duration", type=int, default=120, help="Duration in seconds")
args = parser.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=1)
print()
print("=" * 60)
print(" Sleep Apnea Screener (60 GHz mmWave)")
print(" Lie still within 1m of sensor. Monitoring breathing.")
print("=" * 60)
print()
br_history = collections.deque(maxlen=600)
apnea_events = []
hypopnea_events = []
last_br_time = time.time()
last_br_value = 0.0
last_hr = 0.0
in_apnea = False
apnea_start = 0.0
start = time.time()
last_print = 0
try:
while time.time() - start < args.duration:
line = ser.readline().decode("utf-8", errors="replace")
clean = RE_ANSI.sub("", line)
m = RE_BR.search(clean)
if m:
br = float(m.group(1))
br_history.append((time.time(), br))
if br > 0:
last_br_time = time.time()
last_br_value = br
if in_apnea:
duration = time.time() - apnea_start
apnea_events.append(duration)
print(f" ** APNEA EVENT ENDED: {duration:.1f}s **")
in_apnea = False
if br < HYPOPNEA_BR and br > 0:
hypopnea_events.append(br)
elif br == 0 and not in_apnea:
gap = time.time() - last_br_time
if gap >= APNEA_THRESHOLD_SEC:
in_apnea = True
apnea_start = last_br_time
print(f" ** APNEA DETECTED at {int(time.time()-start)}s (no breath for {gap:.0f}s) **")
m = RE_HR.search(clean)
if m:
last_hr = float(m.group(1))
elapsed = int(time.time() - start)
if elapsed > last_print and elapsed % 10 == 0:
last_print = elapsed
gap = time.time() - last_br_time
status = "APNEA" if in_apnea else ("OK" if gap < 5 else f"gap {gap:.0f}s")
print(f" {elapsed:>4}s | BR {last_br_value:>4.0f}/min | HR {last_hr:>4.0f} | "
f"Apneas: {len(apnea_events)} | Hypopneas: {len(hypopnea_events)} | {status}")
except KeyboardInterrupt:
pass
ser.close()
duration_hr = (time.time() - start) / 3600.0
print()
print("=" * 60)
print(" APNEA SCREENING RESULTS")
print("=" * 60)
ahi = (len(apnea_events) + len(hypopnea_events)) / max(duration_hr, 0.01)
print(f" Duration: {time.time()-start:.0f}s ({duration_hr*60:.1f} min)")
print(f" Apnea events: {len(apnea_events)} (breathing absent > {APNEA_THRESHOLD_SEC}s)")
print(f" Hypopneas: {len(hypopnea_events)} (BR < {HYPOPNEA_BR}/min)")
print(f" AHI estimate: {ahi:.1f} events/hour")
print()
if ahi < 5:
print(" Classification: Normal (AHI < 5)")
elif ahi < 15:
print(" Classification: Mild OSA (AHI 5-14)")
elif ahi < 30:
print(" Classification: Moderate OSA (AHI 15-29)")
else:
print(" Classification: Severe OSA (AHI >= 30)")
print()
print(" NOT A MEDICAL DEVICE. Consult a sleep specialist for diagnosis.")
print()
if __name__ == "__main__":
main()
-149
View File
@@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
Real-Time Stress Monitor via Heart Rate Variability (HRV)
Reads heart rate from MR60BHA2 mmWave radar and computes HRV metrics
to estimate stress level continuously.
HRV Science:
- SDNN < 50ms = high stress / low parasympathetic tone
- SDNN 50-100ms = moderate
- SDNN > 100ms = relaxed / high vagal tone
- RMSSD: successive difference metric, more sensitive to acute stress
Usage:
python examples/stress/hrv_stress_monitor.py --port COM4
"""
import argparse
import collections
import math
import re
import serial
import sys
import time
RE_HR = re.compile(r"'Real-time heart rate'.*?(\d+\.?\d*)\s*bpm", re.IGNORECASE)
RE_ANSI = re.compile(r"\x1b\[[0-9;]*m")
def compute_hrv(hr_values):
"""Compute HRV metrics from HR time series."""
if len(hr_values) < 5:
return {"sdnn": 0, "rmssd": 0, "mean_hr": 0, "stress": ""}
rr = [60000.0 / h for h in hr_values if h > 0]
if len(rr) < 5:
return {"sdnn": 0, "rmssd": 0, "mean_hr": 0, "stress": ""}
mean_rr = sum(rr) / len(rr)
sdnn = math.sqrt(sum((x - mean_rr) ** 2 for x in rr) / len(rr))
# RMSSD: root mean square of successive differences
diffs = [(rr[i+1] - rr[i]) ** 2 for i in range(len(rr) - 1)]
rmssd = math.sqrt(sum(diffs) / len(diffs)) if diffs else 0
mean_hr = sum(hr_values) / len(hr_values)
if sdnn < 30:
stress = "HIGH STRESS"
elif sdnn < 50:
stress = "Moderate Stress"
elif sdnn < 80:
stress = "Mild Stress"
elif sdnn < 100:
stress = "Relaxed"
else:
stress = "Very Relaxed"
return {"sdnn": sdnn, "rmssd": rmssd, "mean_hr": mean_hr, "stress": stress}
def stress_bar(sdnn, width=30):
"""Visual stress bar: more filled = more stressed."""
level = max(0, min(1, 1.0 - sdnn / 120.0))
filled = int(level * width)
bar = "#" * filled + "." * (width - filled)
return f"[{bar}] {level*100:.0f}%"
def main():
parser = argparse.ArgumentParser(description="HRV Stress Monitor (mmWave)")
parser.add_argument("--port", default="COM4")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--duration", type=int, default=120)
parser.add_argument("--window", type=int, default=60, help="HRV window in seconds")
args = parser.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=1)
print()
print("=" * 60)
print(" Real-Time Stress Monitor (mmWave HRV)")
print(" Sit still within 1m. Lower stress = higher HRV.")
print("=" * 60)
print()
hr_buffer = collections.deque(maxlen=args.window)
start = time.time()
last_print = 0
min_stress = 999.0
max_stress = 0.0
readings = []
try:
while time.time() - start < args.duration:
line = ser.readline().decode("utf-8", errors="replace")
clean = RE_ANSI.sub("", line)
m = RE_HR.search(clean)
if m:
hr = float(m.group(1))
if 30 < hr < 200:
hr_buffer.append(hr)
elapsed = int(time.time() - start)
if elapsed > last_print and elapsed % 5 == 0 and len(hr_buffer) >= 3:
last_print = elapsed
hrv = compute_hrv(list(hr_buffer))
bar = stress_bar(hrv["sdnn"])
readings.append(hrv)
if hrv["sdnn"] > 0:
min_stress = min(min_stress, hrv["sdnn"])
max_stress = max(max_stress, hrv["sdnn"])
print(f" {elapsed:>4}s | HR {hrv['mean_hr']:>4.0f} | "
f"SDNN {hrv['sdnn']:>5.1f}ms | RMSSD {hrv['rmssd']:>5.1f}ms | "
f"{hrv['stress']:<16} | {bar}")
except KeyboardInterrupt:
pass
ser.close()
print()
print("=" * 60)
print(" STRESS SESSION SUMMARY")
print("=" * 60)
if readings:
avg_sdnn = sum(r["sdnn"] for r in readings) / len(readings)
avg_rmssd = sum(r["rmssd"] for r in readings) / len(readings)
avg_hr = sum(r["mean_hr"] for r in readings) / len(readings)
final_stress = readings[-1]["stress"]
print(f" Duration: {time.time()-start:.0f}s")
print(f" Avg HR: {avg_hr:.0f} bpm")
print(f" Avg SDNN: {avg_sdnn:.1f} ms {'(low — consider a break)' if avg_sdnn < 50 else '(healthy range)' if avg_sdnn > 70 else ''}")
print(f" Avg RMSSD: {avg_rmssd:.1f} ms")
print(f" SDNN range: {min_stress:.0f} - {max_stress:.0f} ms")
print(f" Assessment: {final_stress}")
print()
print(" SDNN Guide: <30=high stress, 30-50=moderate, 50-100=normal, >100=relaxed")
else:
print(" No data collected. Ensure person is in range.")
print()
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
{"type":"edit","file":"unknown","timestamp":1773152422749,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152444021,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152460956,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152493971,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152501432,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152510853,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152596890,"sessionId":null}
@@ -0,0 +1,12 @@
{
"id": "session-1773152560779",
"startedAt": "2026-03-10T14:22:40.779Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node",
"context": {},
"metrics": {
"edits": 1,
"commands": 0,
"tasks": 0,
"errors": 0
}
}
@@ -2,7 +2,6 @@ set(SRCS
"main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c"
"edge_processing.c" "ota_update.c" "power_mgmt.c"
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
"mmwave_sensor.c"
)
set(REQUIRES "")
@@ -68,13 +68,10 @@ menu "Edge Intelligence (ADR-039)"
config EDGE_FALL_THRESH
int "Fall detection threshold (x1000)"
default 15000
default 2000
range 100 50000
help
Phase acceleration threshold for fall detection.
Value is divided by 1000 to get rad/s². Default 15000 = 15.0 rad/s².
Raise to reduce false positives in high-traffic environments.
Normal walking produces accelerations of 2-5 rad/s².
Stored as integer; divided by 1000 at runtime.
Default 2000 = 2.0 rad/s^2.
+3 -3
View File
@@ -117,8 +117,8 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
uint32_t magic = CSI_MAGIC;
memcpy(&buf[0], &magic, 4);
/* Node ID (from NVS runtime config, not compile-time Kconfig) */
buf[4] = g_nvs_config.node_id;
/* Node ID */
buf[4] = (uint8_t)CONFIG_CSI_NODE_ID;
/* Number of antennas */
buf[5] = n_antennas;
@@ -273,7 +273,7 @@ void csi_collector_init(void)
}
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%u)",
g_nvs_config.node_id, (unsigned)csi_channel);
CONFIG_CSI_NODE_ID, (unsigned)csi_channel);
}
/* ---- ADR-029: Channel hopping ---- */
+5 -4
View File
@@ -7,11 +7,8 @@
*/
#include "display_ui.h"
#include "nvs_config.h"
#include "sdkconfig.h"
extern nvs_config_t g_nvs_config;
#if CONFIG_DISPLAY_ENABLE
#include <stdio.h>
@@ -350,7 +347,11 @@ void display_ui_update(void)
{
char buf[48];
snprintf(buf, sizeof(buf), "Node: %d", g_nvs_config.node_id);
#ifdef CONFIG_CSI_NODE_ID
snprintf(buf, sizeof(buf), "Node: %d", CONFIG_CSI_NODE_ID);
#else
snprintf(buf, sizeof(buf), "Node: --");
#endif
lv_label_set_text(s_sys_node, buf);
snprintf(buf, sizeof(buf), "Heap: %lu KB free",
+17 -92
View File
@@ -18,11 +18,6 @@
*/
#include "edge_processing.h"
#include "nvs_config.h"
#include "mmwave_sensor.h"
/* Runtime config — declared in main.c, loaded from NVS at boot. */
extern nvs_config_t g_nvs_config;
#include "wasm_runtime.h"
#include "stream_sender.h"
@@ -249,10 +244,6 @@ static uint32_t s_frame_count;
/** Previous phase velocity for fall detection (acceleration). */
static float s_prev_phase_velocity;
/** Fall detection debounce state (issue #263). */
static uint8_t s_fall_consec_count; /**< Consecutive frames above threshold. */
static int64_t s_fall_last_alert_us; /**< Timestamp of last fall alert (debounce). */
/** Adaptive calibration state. */
static bool s_calibrated;
static float s_calib_sum;
@@ -430,7 +421,11 @@ static void send_compressed_frame(const uint8_t *iq_data, uint16_t iq_len,
uint32_t magic = EDGE_COMPRESSED_MAGIC;
memcpy(&pkt[0], &magic, 4);
pkt[4] = g_nvs_config.node_id;
#ifdef CONFIG_CSI_NODE_ID
pkt[4] = (uint8_t)CONFIG_CSI_NODE_ID;
#else
pkt[4] = 0;
#endif
pkt[5] = channel;
memcpy(&pkt[6], &iq_len, 2);
memcpy(&pkt[8], &comp_len, 2);
@@ -548,7 +543,11 @@ static void send_vitals_packet(void)
memset(&pkt, 0, sizeof(pkt));
pkt.magic = EDGE_VITALS_MAGIC;
pkt.node_id = g_nvs_config.node_id;
#ifdef CONFIG_CSI_NODE_ID
pkt.node_id = (uint8_t)CONFIG_CSI_NODE_ID;
#else
pkt.node_id = 0;
#endif
pkt.flags = 0;
if (s_presence_detected) pkt.flags |= 0x01;
@@ -574,58 +573,8 @@ static void send_vitals_packet(void)
s_latest_pkt = pkt;
s_pkt_valid = true;
/* ADR-063: If mmWave is active, send fused 48-byte packet instead. */
mmwave_state_t mw;
if (mmwave_sensor_get_state(&mw) && mw.detected) {
edge_fused_vitals_pkt_t fpkt;
memset(&fpkt, 0, sizeof(fpkt));
fpkt.magic = EDGE_FUSED_MAGIC;
fpkt.node_id = pkt.node_id;
fpkt.flags = pkt.flags;
if (mw.person_present) fpkt.flags |= 0x08; /* Bit3 = mmwave_present */
fpkt.rssi = pkt.rssi;
fpkt.n_persons = pkt.n_persons;
fpkt.mmwave_type = (uint8_t)mw.type;
fpkt.motion_energy = pkt.motion_energy;
fpkt.presence_score = pkt.presence_score;
fpkt.timestamp_ms = pkt.timestamp_ms;
/* Kalman-style fusion: prefer mmWave when available, CSI as fallback. */
if (mw.heart_rate_bpm > 0.0f && s_heartrate_bpm > 0.0f) {
/* Weighted average: mmWave 80%, CSI 20% (mmWave is more accurate). */
float fused_hr = mw.heart_rate_bpm * 0.8f + s_heartrate_bpm * 0.2f;
fpkt.heartrate = (uint32_t)(fused_hr * 10000.0f);
fpkt.fusion_confidence = 90;
} else if (mw.heart_rate_bpm > 0.0f) {
fpkt.heartrate = (uint32_t)(mw.heart_rate_bpm * 10000.0f);
fpkt.fusion_confidence = 85;
} else {
fpkt.heartrate = pkt.heartrate;
fpkt.fusion_confidence = 50;
}
if (mw.breathing_rate > 0.0f && s_breathing_bpm > 0.0f) {
float fused_br = mw.breathing_rate * 0.8f + s_breathing_bpm * 0.2f;
fpkt.breathing_rate = (uint16_t)(fused_br * 100.0f);
} else if (mw.breathing_rate > 0.0f) {
fpkt.breathing_rate = (uint16_t)(mw.breathing_rate * 100.0f);
} else {
fpkt.breathing_rate = pkt.breathing_rate;
}
/* Raw mmWave values for server-side analysis. */
fpkt.mmwave_hr_bpm = mw.heart_rate_bpm;
fpkt.mmwave_br_bpm = mw.breathing_rate;
fpkt.mmwave_distance = mw.distance_cm;
fpkt.mmwave_targets = mw.target_count;
fpkt.mmwave_confidence = (mw.frame_count > 10) ? 80 : 40;
stream_sender_send((const uint8_t *)&fpkt, sizeof(fpkt));
} else {
/* No mmWave — send standard 32-byte packet. */
stream_sender_send((const uint8_t *)&pkt, sizeof(pkt));
}
/* Send over UDP. */
stream_sender_send((const uint8_t *)&pkt, sizeof(pkt));
}
/* ======================================================================
@@ -740,7 +689,7 @@ static void process_frame(const edge_ring_slot_t *slot)
}
s_presence_detected = (s_presence_score > threshold);
/* --- Step 10: Fall detection (phase acceleration + debounce, issue #263) --- */
/* --- Step 10: Fall detection (phase acceleration) --- */
if (s_history_len >= 3) {
uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN;
uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN;
@@ -748,26 +697,10 @@ static void process_frame(const edge_ring_slot_t *slot)
float accel = fabsf(velocity - s_prev_phase_velocity);
s_prev_phase_velocity = velocity;
if (accel > s_cfg.fall_thresh) {
s_fall_consec_count++;
} else {
s_fall_consec_count = 0;
}
/* Require EDGE_FALL_CONSEC_MIN consecutive frames above threshold,
* plus a cooldown period to prevent alert storms. */
int64_t now_us = esp_timer_get_time();
int64_t cooldown_us = (int64_t)EDGE_FALL_COOLDOWN_MS * 1000;
if (s_fall_consec_count >= EDGE_FALL_CONSEC_MIN
&& (now_us - s_fall_last_alert_us) >= cooldown_us)
{
s_fall_detected = true;
s_fall_last_alert_us = now_us;
s_fall_consec_count = 0;
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f (consec=%u)",
accel, s_cfg.fall_thresh, EDGE_FALL_CONSEC_MIN);
} else if (s_fall_consec_count == 0) {
s_fall_detected = false;
s_fall_detected = (accel > s_cfg.fall_thresh);
if (s_fall_detected) {
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f",
accel, s_cfg.fall_thresh);
}
}
@@ -834,12 +767,6 @@ static void edge_task(void *arg)
while (1) {
if (ring_pop(&slot)) {
process_frame(&slot);
/* Yield after every frame to feed the Core 1 watchdog.
* process_frame() is CPU-intensive (biquad filters, Welford stats,
* BPM estimation, multi-person vitals) and can take several ms.
* Without this yield, edge_dsp at priority 5 starves IDLE1 at
* priority 0, triggering the task watchdog. See issue #266. */
vTaskDelay(1);
} else {
/* No frames available — yield briefly. */
vTaskDelay(pdMS_TO_TICKS(1));
@@ -923,8 +850,6 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
s_latest_rssi = 0;
s_frame_count = 0;
s_prev_phase_velocity = 0.0f;
s_fall_consec_count = 0;
s_fall_last_alert_us = 0;
s_last_vitals_send_us = 0;
s_has_prev_iq = false;
s_prev_iq_len = 0;
@@ -42,10 +42,6 @@
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
#define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */
/* ---- Fall detection ---- */
#define EDGE_FALL_COOLDOWN_MS 5000 /**< Minimum ms between fall alerts (debounce). */
#define EDGE_FALL_CONSEC_MIN 3 /**< Consecutive frames above threshold to trigger. */
/* ---- SPSC ring buffer slot ---- */
typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
@@ -106,35 +102,6 @@ typedef struct __attribute__((packed)) {
_Static_assert(sizeof(edge_vitals_pkt_t) == 32, "vitals packet must be 32 bytes");
/* ---- ADR-063: Fused vitals packet (48 bytes, wire format) ---- */
#define EDGE_FUSED_MAGIC 0xC5110004 /**< Fused vitals packet magic. */
typedef struct __attribute__((packed)) {
/* First 32 bytes match edge_vitals_pkt_t layout */
uint32_t magic; /**< EDGE_FUSED_MAGIC = 0xC5110004. */
uint8_t node_id;
uint8_t flags; /**< Bit0=presence, Bit1=fall, Bit2=motion, Bit3=mmwave_present. */
uint16_t breathing_rate; /**< Fused BPM * 100 (CSI + mmWave Kalman). */
uint32_t heartrate; /**< Fused BPM * 10000. */
int8_t rssi;
uint8_t n_persons;
uint8_t mmwave_type; /**< mmwave_type_t enum. */
uint8_t fusion_confidence; /**< 0-100 fusion quality score. */
float motion_energy;
float presence_score;
uint32_t timestamp_ms;
/* mmWave extension (16 bytes) */
float mmwave_hr_bpm; /**< Raw mmWave heart rate. */
float mmwave_br_bpm; /**< Raw mmWave breathing rate. */
float mmwave_distance;/**< Distance to nearest target (cm). */
uint8_t mmwave_targets; /**< Target count from mmWave. */
uint8_t mmwave_confidence; /**< mmWave signal quality 0-100. */
uint16_t reserved3;
uint32_t reserved4; /**< Pad to 48 bytes for alignment. */
} edge_fused_vitals_pkt_t;
_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48, "fused vitals must be 48 bytes");
/* ---- Edge configuration (from NVS) ---- */
typedef struct {
uint8_t tier; /**< Processing tier: 0=raw, 1=basic, 2=full. */
+2 -16
View File
@@ -27,7 +27,6 @@
#include "wasm_runtime.h"
#include "wasm_upload.h"
#include "display_task.h"
#include "mmwave_sensor.h"
#ifdef CONFIG_CSI_MOCK_ENABLED
#include "mock_csi.h"
#endif
@@ -228,18 +227,6 @@ void app_main(void)
}
}
/* ADR-063: Initialize mmWave sensor (auto-detect on UART). */
esp_err_t mmwave_ret = mmwave_sensor_init(-1, -1); /* -1 = use default GPIO pins */
if (mmwave_ret == ESP_OK) {
mmwave_state_t mw;
if (mmwave_sensor_get_state(&mw)) {
ESP_LOGI(TAG, "mmWave sensor: %s (caps=0x%04x)",
mmwave_type_name(mw.type), mw.capabilities);
}
} else {
ESP_LOGI(TAG, "No mmWave sensor detected (CSI-only mode)");
}
/* Initialize power management. */
power_mgmt_init(g_nvs_config.power_duty);
@@ -251,12 +238,11 @@ void app_main(void)
}
#endif
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s, mmWave=%s)",
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
g_nvs_config.target_ip, g_nvs_config.target_port,
g_nvs_config.edge_tier,
(ota_ret == ESP_OK) ? "ready" : "off",
(wasm_ret == ESP_OK) ? "ready" : "off",
(mmwave_ret == ESP_OK) ? "active" : "off");
(wasm_ret == ESP_OK) ? "ready" : "off");
/* Main loop — keep alive */
while (1) {
@@ -1,571 +0,0 @@
/**
* @file mmwave_sensor.c
* @brief ADR-063: mmWave sensor UART driver with auto-detection.
*
* Supports Seeed MR60BHA2 (60 GHz) and HLK-LD2410 (24 GHz).
* Under QEMU (CONFIG_CSI_MOCK_ENABLED), uses a mock generator
* that produces synthetic vital signs for pipeline testing.
*
* MR60BHA2 frame format (Seeed mmWave protocol):
* [0] SOF = 0x01
* [1-2] Frame ID (uint16, big-endian)
* [3-4] Data Length (uint16, big-endian)
* [5-6] Frame Type (uint16, big-endian)
* [7] Header Checksum = ~XOR(bytes 0..6)
* [8..N] Payload (N = data_length)
* [N+1] Data Checksum = ~XOR(payload bytes)
*
* Frame types: 0x0A14=breathing, 0x0A15=heart rate,
* 0x0A16=distance, 0x0F09=presence
*
* LD2410 frame format (HLK binary, 256000 baud):
* Header: 0xF4 0xF3 0xF2 0xF1
* Length: uint16 LE
* Data: [type 0xAA] [target_state] [moving_dist LE] [energy] ...
* Footer: 0xF8 0xF7 0xF6 0xF5
*/
#include "mmwave_sensor.h"
#include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "sdkconfig.h"
#ifndef CONFIG_CSI_MOCK_ENABLED
#include "driver/uart.h"
#endif
static const char *TAG = "mmwave";
/* ---- Configuration ---- */
#define MMWAVE_UART_NUM UART_NUM_1
#define MMWAVE_MR60_BAUD 115200
#define MMWAVE_LD2410_BAUD 256000
#define MMWAVE_BUF_SIZE 256
#define MMWAVE_TASK_STACK 4096
#define MMWAVE_TASK_PRIORITY 3
#define MMWAVE_PROBE_TIMEOUT_MS 2000
#define MMWAVE_MR60_MAX_PAYLOAD 30 /* Sanity limit from Arduino lib */
/* ---- MR60BHA2 protocol constants (Seeed mmWave) ---- */
#define MR60_SOF 0x01
/* Frame types (big-endian uint16 at offset 5-6) */
#define MR60_TYPE_BREATHING 0x0A14
#define MR60_TYPE_HEARTRATE 0x0A15
#define MR60_TYPE_DISTANCE 0x0A16
#define MR60_TYPE_PRESENCE 0x0F09
#define MR60_TYPE_PHASE 0x0A13
#define MR60_TYPE_POINTCLOUD 0x0A04
/* ---- LD2410 protocol constants ---- */
#define LD2410_REPORT_HEAD 0xAA
#define LD2410_REPORT_TAIL 0x55
/* ---- Shared state ---- */
static mmwave_state_t s_state;
static volatile bool s_running;
/* ======================================================================
* MR60BHA2 Parser (corrected protocol from Seeed Arduino library)
* ====================================================================== */
static uint8_t mr60_calc_checksum(const uint8_t *data, uint16_t len)
{
uint8_t cksum = 0;
for (uint16_t i = 0; i < len; i++) {
cksum ^= data[i];
}
return ~cksum;
}
typedef enum {
MR60_WAIT_SOF,
MR60_READ_HEADER, /* Accumulate bytes 1..7 (frame_id, len, type, hdr_cksum) */
MR60_READ_DATA,
MR60_READ_DATA_CKSUM,
} mr60_parse_state_t;
typedef struct {
mr60_parse_state_t state;
uint8_t header[8]; /* Full header: SOF + frame_id(2) + len(2) + type(2) + hdr_cksum */
uint8_t hdr_idx;
uint16_t data_len;
uint16_t frame_type;
uint16_t data_idx;
uint8_t data[MMWAVE_BUF_SIZE];
} mr60_parser_t;
static mr60_parser_t s_mr60;
static void mr60_process_frame(uint16_t type, const uint8_t *data, uint16_t len)
{
s_state.frame_count++;
s_state.last_update_us = esp_timer_get_time();
switch (type) {
case MR60_TYPE_BREATHING:
if (len >= 4) {
/* Breathing rate as float32 (little-endian in payload). */
float br;
memcpy(&br, data, sizeof(float));
if (br >= 0.0f && br <= 60.0f) {
s_state.breathing_rate = br;
}
}
break;
case MR60_TYPE_HEARTRATE:
if (len >= 4) {
float hr;
memcpy(&hr, data, sizeof(float));
if (hr >= 0.0f && hr <= 250.0f) {
s_state.heart_rate_bpm = hr;
}
}
break;
case MR60_TYPE_DISTANCE:
if (len >= 8) {
/* Bytes 0-3: range flag (uint32 LE). 0 = no valid distance. */
uint32_t range_flag;
memcpy(&range_flag, data, sizeof(uint32_t));
if (range_flag != 0 && len >= 8) {
float dist;
memcpy(&dist, &data[4], sizeof(float));
s_state.distance_cm = dist;
}
}
break;
case MR60_TYPE_PRESENCE:
if (len >= 1) {
s_state.person_present = (data[0] != 0);
}
break;
default:
break;
}
}
static void mr60_feed_byte(uint8_t b)
{
switch (s_mr60.state) {
case MR60_WAIT_SOF:
if (b == MR60_SOF) {
s_mr60.header[0] = b;
s_mr60.hdr_idx = 1;
s_mr60.state = MR60_READ_HEADER;
}
break;
case MR60_READ_HEADER:
s_mr60.header[s_mr60.hdr_idx++] = b;
if (s_mr60.hdr_idx >= 8) {
/* Validate header checksum: ~XOR(bytes 0..6) == byte 7 */
uint8_t expected = mr60_calc_checksum(s_mr60.header, 7);
if (expected != s_mr60.header[7]) {
s_state.error_count++;
s_mr60.state = MR60_WAIT_SOF;
break;
}
/* Parse header fields (big-endian) */
s_mr60.data_len = ((uint16_t)s_mr60.header[3] << 8) | s_mr60.header[4];
s_mr60.frame_type = ((uint16_t)s_mr60.header[5] << 8) | s_mr60.header[6];
s_mr60.data_idx = 0;
if (s_mr60.data_len > MMWAVE_MR60_MAX_PAYLOAD) {
s_state.error_count++;
s_mr60.state = MR60_WAIT_SOF;
} else if (s_mr60.data_len == 0) {
s_mr60.state = MR60_READ_DATA_CKSUM;
} else {
s_mr60.state = MR60_READ_DATA;
}
}
break;
case MR60_READ_DATA:
s_mr60.data[s_mr60.data_idx++] = b;
if (s_mr60.data_idx >= s_mr60.data_len) {
s_mr60.state = MR60_READ_DATA_CKSUM;
}
break;
case MR60_READ_DATA_CKSUM:
/* Validate data checksum */
if (s_mr60.data_len > 0) {
uint8_t expected = mr60_calc_checksum(s_mr60.data, s_mr60.data_len);
if (expected == b) {
mr60_process_frame(s_mr60.frame_type, s_mr60.data, s_mr60.data_len);
} else {
s_state.error_count++;
}
} else {
/* Zero-length payload — checksum byte is for empty data */
mr60_process_frame(s_mr60.frame_type, s_mr60.data, 0);
}
s_mr60.state = MR60_WAIT_SOF;
break;
}
}
/* ======================================================================
* LD2410 Parser (HLK binary protocol, 256000 baud)
* ====================================================================== */
typedef enum {
LD_WAIT_F4, LD_WAIT_F3, LD_WAIT_F2, LD_WAIT_F1,
LD_READ_LEN_L, LD_READ_LEN_H,
LD_READ_DATA,
LD_WAIT_F8, LD_WAIT_F7, LD_WAIT_F6, LD_WAIT_F5,
} ld2410_parse_state_t;
typedef struct {
ld2410_parse_state_t state;
uint16_t data_len;
uint16_t data_idx;
uint8_t data[MMWAVE_BUF_SIZE];
} ld2410_parser_t;
static ld2410_parser_t s_ld;
static void ld2410_process_frame(const uint8_t *data, uint16_t len)
{
s_state.frame_count++;
s_state.last_update_us = esp_timer_get_time();
if (len < 12) return;
uint8_t data_type = data[0]; /* 0x02 = normal, 0x01 = engineering */
uint8_t head_marker = data[1]; /* Must be 0xAA */
if (head_marker != LD2410_REPORT_HEAD) return;
/* Normal mode target report (data_type 0x02 or 0x01) */
uint8_t target_state = data[2];
uint16_t moving_dist = data[3] | ((uint16_t)data[4] << 8);
uint8_t moving_energy = data[5];
uint16_t static_dist = data[6] | ((uint16_t)data[7] << 8);
uint8_t static_energy = data[8];
uint16_t detect_dist = data[9] | ((uint16_t)data[10] << 8);
(void)moving_energy;
(void)static_energy;
(void)detect_dist;
s_state.person_present = (target_state != 0);
s_state.target_count = (target_state != 0) ? 1 : 0;
if (target_state == 1 || target_state == 3) {
s_state.distance_cm = (float)moving_dist;
} else if (target_state == 2) {
s_state.distance_cm = (float)static_dist;
} else {
s_state.distance_cm = 0.0f;
}
}
static void ld2410_feed_byte(uint8_t b)
{
switch (s_ld.state) {
case LD_WAIT_F4: s_ld.state = (b == 0xF4) ? LD_WAIT_F3 : LD_WAIT_F4; break;
case LD_WAIT_F3: s_ld.state = (b == 0xF3) ? LD_WAIT_F2 : LD_WAIT_F4; break;
case LD_WAIT_F2: s_ld.state = (b == 0xF2) ? LD_WAIT_F1 : LD_WAIT_F4; break;
case LD_WAIT_F1: s_ld.state = (b == 0xF1) ? LD_READ_LEN_L : LD_WAIT_F4; break;
case LD_READ_LEN_L:
s_ld.data_len = b;
s_ld.state = LD_READ_LEN_H;
break;
case LD_READ_LEN_H:
s_ld.data_len |= ((uint16_t)b << 8);
s_ld.data_idx = 0;
if (s_ld.data_len == 0 || s_ld.data_len > MMWAVE_BUF_SIZE) {
s_ld.state = LD_WAIT_F4;
} else {
s_ld.state = LD_READ_DATA;
}
break;
case LD_READ_DATA:
s_ld.data[s_ld.data_idx++] = b;
if (s_ld.data_idx >= s_ld.data_len) s_ld.state = LD_WAIT_F8;
break;
case LD_WAIT_F8: s_ld.state = (b == 0xF8) ? LD_WAIT_F7 : LD_WAIT_F4; break;
case LD_WAIT_F7: s_ld.state = (b == 0xF7) ? LD_WAIT_F6 : LD_WAIT_F4; break;
case LD_WAIT_F6: s_ld.state = (b == 0xF6) ? LD_WAIT_F5 : LD_WAIT_F4; break;
case LD_WAIT_F5:
if (b == 0xF5) {
ld2410_process_frame(s_ld.data, s_ld.data_len);
}
s_ld.state = LD_WAIT_F4;
break;
}
}
/* ======================================================================
* Mock mmWave Generator (for QEMU testing)
* ====================================================================== */
#ifdef CONFIG_CSI_MOCK_ENABLED
static void mock_mmwave_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "Mock mmWave generator started (simulating MR60BHA2)");
s_state.type = MMWAVE_TYPE_MOCK;
s_state.detected = true;
s_state.capabilities = MMWAVE_CAP_HEART_RATE | MMWAVE_CAP_BREATHING
| MMWAVE_CAP_PRESENCE | MMWAVE_CAP_DISTANCE;
float hr_base = 72.0f;
float br_base = 16.0f;
uint32_t tick = 0;
while (s_running) {
tick++;
/* Simulate realistic vital sign variation. */
float hr_noise = 2.0f * sinf((float)tick * 0.1f) + 0.5f * sinf((float)tick * 0.37f);
float br_noise = 1.0f * sinf((float)tick * 0.07f) + 0.3f * sinf((float)tick * 0.23f);
s_state.heart_rate_bpm = hr_base + hr_noise;
s_state.breathing_rate = br_base + br_noise;
s_state.person_present = true;
s_state.distance_cm = 150.0f + 20.0f * sinf((float)tick * 0.05f);
s_state.target_count = 1;
s_state.frame_count++;
s_state.last_update_us = esp_timer_get_time();
/* Simulate person leaving at tick 200-250 (for scenario testing). */
if (tick >= 200 && tick <= 250) {
s_state.person_present = false;
s_state.heart_rate_bpm = 0.0f;
s_state.breathing_rate = 0.0f;
s_state.distance_cm = 0.0f;
s_state.target_count = 0;
}
/* ~1 Hz update rate (matches real MR60BHA2). */
vTaskDelay(pdMS_TO_TICKS(1000));
}
vTaskDelete(NULL);
}
#endif /* CONFIG_CSI_MOCK_ENABLED */
/* ======================================================================
* UART Auto-Detection and Task
* ====================================================================== */
#ifndef CONFIG_CSI_MOCK_ENABLED
/**
* Try to detect a sensor at the given baud rate.
* Returns the sensor type if detected, MMWAVE_TYPE_NONE otherwise.
*/
static mmwave_type_t probe_at_baud(uint32_t baud)
{
/* Reconfigure baud rate. */
uart_set_baudrate(MMWAVE_UART_NUM, baud);
uart_flush_input(MMWAVE_UART_NUM);
uint8_t buf[128];
int mr60_sof_seen = 0;
int ld2410_header_seen = 0;
int64_t deadline = esp_timer_get_time() + (int64_t)(MMWAVE_PROBE_TIMEOUT_MS / 2) * 1000;
while (esp_timer_get_time() < deadline) {
int len = uart_read_bytes(MMWAVE_UART_NUM, buf, sizeof(buf), pdMS_TO_TICKS(100));
if (len <= 0) continue;
for (int i = 0; i < len; i++) {
/* MR60BHA2: SOF = 0x01, followed by valid-looking frame_id bytes */
if (buf[i] == MR60_SOF && baud == MMWAVE_MR60_BAUD) {
mr60_sof_seen++;
}
/* LD2410: 4-byte header 0xF4F3F2F1 */
if (i + 3 < len && buf[i] == 0xF4 && buf[i+1] == 0xF3
&& buf[i+2] == 0xF2 && buf[i+3] == 0xF1
&& baud == MMWAVE_LD2410_BAUD) {
ld2410_header_seen++;
}
}
if (mr60_sof_seen >= 3) return MMWAVE_TYPE_MR60BHA2;
if (ld2410_header_seen >= 2) return MMWAVE_TYPE_LD2410;
}
if (mr60_sof_seen > 0) return MMWAVE_TYPE_MR60BHA2;
if (ld2410_header_seen > 0) return MMWAVE_TYPE_LD2410;
return MMWAVE_TYPE_NONE;
}
/**
* Auto-detect sensor by probing at both baud rates.
* MR60BHA2 uses 115200, LD2410 uses 256000.
*/
static mmwave_type_t probe_sensor(void)
{
ESP_LOGI(TAG, "Probing at %d baud (MR60BHA2)...", MMWAVE_MR60_BAUD);
mmwave_type_t result = probe_at_baud(MMWAVE_MR60_BAUD);
if (result != MMWAVE_TYPE_NONE) return result;
ESP_LOGI(TAG, "Probing at %d baud (LD2410)...", MMWAVE_LD2410_BAUD);
result = probe_at_baud(MMWAVE_LD2410_BAUD);
return result;
}
static void mmwave_uart_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "mmWave UART task started (type=%s)",
mmwave_type_name(s_state.type));
uint8_t buf[128];
while (s_running) {
int len = uart_read_bytes(MMWAVE_UART_NUM, buf, sizeof(buf), pdMS_TO_TICKS(100));
if (len <= 0) {
vTaskDelay(1);
continue;
}
for (int i = 0; i < len; i++) {
if (s_state.type == MMWAVE_TYPE_MR60BHA2) {
mr60_feed_byte(buf[i]);
} else if (s_state.type == MMWAVE_TYPE_LD2410) {
ld2410_feed_byte(buf[i]);
}
}
vTaskDelay(1);
}
vTaskDelete(NULL);
}
#endif /* !CONFIG_CSI_MOCK_ENABLED */
/* ======================================================================
* Public API
* ====================================================================== */
const char *mmwave_type_name(mmwave_type_t type)
{
switch (type) {
case MMWAVE_TYPE_MR60BHA2: return "MR60BHA2";
case MMWAVE_TYPE_LD2410: return "LD2410";
case MMWAVE_TYPE_MOCK: return "Mock";
case MMWAVE_TYPE_NONE:
default: return "None";
}
}
esp_err_t mmwave_sensor_init(int uart_tx_pin, int uart_rx_pin)
{
memset(&s_state, 0, sizeof(s_state));
memset(&s_mr60, 0, sizeof(s_mr60));
memset(&s_ld, 0, sizeof(s_ld));
s_running = true;
#ifdef CONFIG_CSI_MOCK_ENABLED
ESP_LOGI(TAG, "Mock mode: starting synthetic mmWave generator");
BaseType_t ret = xTaskCreatePinnedToCore(
mock_mmwave_task, "mmwave_mock", MMWAVE_TASK_STACK,
NULL, MMWAVE_TASK_PRIORITY, NULL, 0);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create mock mmWave task");
return ESP_ERR_NO_MEM;
}
return ESP_OK;
#else
if (uart_tx_pin < 0) uart_tx_pin = 17;
if (uart_rx_pin < 0) uart_rx_pin = 18;
/* Install UART driver at MR60 baud (will be changed during probe). */
uart_config_t uart_config = {
.baud_rate = MMWAVE_MR60_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_driver_install(MMWAVE_UART_NUM, MMWAVE_BUF_SIZE * 2, 0, 0, NULL, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "UART driver install failed: %s", esp_err_to_name(err));
return err;
}
uart_param_config(MMWAVE_UART_NUM, &uart_config);
uart_set_pin(MMWAVE_UART_NUM, uart_tx_pin, uart_rx_pin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
ESP_LOGI(TAG, "Probing UART%d (TX=%d, RX=%d) for mmWave sensor...",
MMWAVE_UART_NUM, uart_tx_pin, uart_rx_pin);
mmwave_type_t detected = probe_sensor();
if (detected == MMWAVE_TYPE_NONE) {
ESP_LOGI(TAG, "No mmWave sensor detected on UART%d", MMWAVE_UART_NUM);
uart_driver_delete(MMWAVE_UART_NUM);
return ESP_ERR_NOT_FOUND;
}
/* Set final baud rate for the detected sensor. */
uint32_t final_baud = (detected == MMWAVE_TYPE_LD2410)
? MMWAVE_LD2410_BAUD : MMWAVE_MR60_BAUD;
uart_set_baudrate(MMWAVE_UART_NUM, final_baud);
s_state.type = detected;
s_state.detected = true;
switch (detected) {
case MMWAVE_TYPE_MR60BHA2:
s_state.capabilities = MMWAVE_CAP_HEART_RATE | MMWAVE_CAP_BREATHING
| MMWAVE_CAP_PRESENCE | MMWAVE_CAP_DISTANCE;
break;
case MMWAVE_TYPE_LD2410:
s_state.capabilities = MMWAVE_CAP_PRESENCE | MMWAVE_CAP_DISTANCE;
break;
default:
break;
}
ESP_LOGI(TAG, "Detected %s at %lu baud (caps=0x%04x)",
mmwave_type_name(detected), (unsigned long)final_baud,
s_state.capabilities);
BaseType_t ret = xTaskCreatePinnedToCore(
mmwave_uart_task, "mmwave_uart", MMWAVE_TASK_STACK,
NULL, MMWAVE_TASK_PRIORITY, NULL, 0);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create mmWave UART task");
return ESP_ERR_NO_MEM;
}
return ESP_OK;
#endif
}
bool mmwave_sensor_get_state(mmwave_state_t *state)
{
if (!s_state.detected || state == NULL) return false;
memcpy(state, &s_state, sizeof(mmwave_state_t));
return true;
}
@@ -1,83 +0,0 @@
/**
* @file mmwave_sensor.h
* @brief ADR-063: 60 GHz mmWave sensor auto-detection and UART driver.
*
* Supports:
* - Seeed MR60BHA2 (60 GHz, heart rate + breathing + presence)
* - HLK-LD2410 (24 GHz, presence + distance)
*
* Auto-detects sensor type at boot by probing UART for known frame headers.
* Runs a background task that parses incoming frames and updates shared state.
*/
#ifndef MMWAVE_SENSOR_H
#define MMWAVE_SENSOR_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/* ---- Sensor type enumeration ---- */
typedef enum {
MMWAVE_TYPE_NONE = 0, /**< No sensor detected. */
MMWAVE_TYPE_MR60BHA2 = 1, /**< Seeed MR60BHA2 (60 GHz, HR + BR). */
MMWAVE_TYPE_LD2410 = 2, /**< HLK-LD2410 (24 GHz, presence + range). */
MMWAVE_TYPE_MOCK = 99, /**< Mock sensor for QEMU testing. */
} mmwave_type_t;
/* ---- Capability flags ---- */
#define MMWAVE_CAP_HEART_RATE (1 << 0)
#define MMWAVE_CAP_BREATHING (1 << 1)
#define MMWAVE_CAP_PRESENCE (1 << 2)
#define MMWAVE_CAP_DISTANCE (1 << 3)
#define MMWAVE_CAP_FALL (1 << 4)
#define MMWAVE_CAP_MULTI_TARGET (1 << 5)
/* ---- Shared mmWave state (updated by background task) ---- */
typedef struct {
/* Detection */
mmwave_type_t type; /**< Detected sensor type. */
uint16_t capabilities; /**< Bitmask of MMWAVE_CAP_* flags. */
bool detected; /**< True if sensor responded on UART. */
/* Vital signs (MR60BHA2) */
float heart_rate_bpm; /**< Heart rate in BPM (0 if unavailable). */
float breathing_rate; /**< Breathing rate in breaths/min. */
/* Presence and range (LD2410 / MR60BHA2) */
bool person_present; /**< True if person detected. */
float distance_cm; /**< Distance to nearest target in cm. */
uint8_t target_count; /**< Number of detected targets. */
/* Quality metrics */
uint32_t frame_count; /**< Total parsed frames since boot. */
uint32_t error_count; /**< Parse errors / CRC failures. */
int64_t last_update_us; /**< Timestamp of last valid frame. */
} mmwave_state_t;
/**
* Initialize the mmWave sensor subsystem.
*
* Probes the configured UART for known sensor types. If a sensor is
* detected, starts a background FreeRTOS task to parse incoming frames.
*
* @param uart_tx_pin GPIO pin for UART TX (to sensor RX). Use -1 for default.
* @param uart_rx_pin GPIO pin for UART RX (from sensor TX). Use -1 for default.
* @return ESP_OK if sensor detected, ESP_ERR_NOT_FOUND if no sensor.
*/
esp_err_t mmwave_sensor_init(int uart_tx_pin, int uart_rx_pin);
/**
* Get a snapshot of the current mmWave state (thread-safe copy).
*
* @param state Output state struct.
* @return true if valid data is available (sensor detected and running).
*/
bool mmwave_sensor_get_state(mmwave_state_t *state);
/**
* Get the detected sensor type name as a string.
*/
const char *mmwave_type_name(mmwave_type_t type);
#endif /* MMWAVE_SENSOR_H */
+1 -1
View File
@@ -61,7 +61,7 @@ void nvs_config_load(nvs_config_t *cfg)
#ifdef CONFIG_EDGE_FALL_THRESH
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
#else
cfg->fall_thresh = 15.0f; /* Default raised from 2.0 — see issue #263. */
cfg->fall_thresh = 2.0f;
#endif
cfg->vital_window = 256;
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS
+5 -4
View File
@@ -12,9 +12,6 @@
#include "sdkconfig.h"
#include "wasm_runtime.h"
#include "nvs_config.h"
extern nvs_config_t g_nvs_config;
#if defined(CONFIG_WASM_ENABLE) && defined(WASM3_AVAILABLE)
@@ -383,7 +380,11 @@ static void send_wasm_output(uint8_t slot_id)
memset(&pkt, 0, sizeof(pkt));
pkt.magic = WASM_OUTPUT_MAGIC;
pkt.node_id = g_nvs_config.node_id;
#ifdef CONFIG_CSI_NODE_ID
pkt.node_id = (uint8_t)CONFIG_CSI_NODE_ID;
#else
pkt.node_id = 0;
#endif
pkt.module_id = slot_id;
pkt.event_count = n_filtered;
@@ -1,15 +0,0 @@
# ESP32-S3 CSI Node — 4MB flash partition table (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
# Binary is ~978KB so each OTA slot is 1.875MB — plenty of room.
#
# Usage: copy to partitions_display.csv OR set in sdkconfig:
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x1D0000,
ota_1, app, ota_1, 0x1F0000, 0x1D0000,
Can't render this file because it contains an unexpected character in line 6 and column 44.
+26 -20
View File
@@ -83,20 +83,25 @@ def generate_nvs_binary(csv_content, size):
bin_path = csv_path.replace(".csv", ".bin")
try:
# Method 1: subprocess invocation (most reliable across package versions)
for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
try:
subprocess.check_call(
[sys.executable, "-m", module_name, "generate",
csv_path, bin_path, hex(size)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
with open(bin_path, "rb") as f:
return f.read()
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# Try the pip-installed version first (esp_idf_nvs_partition_gen package)
try:
from esp_idf_nvs_partition_gen import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Method 2: ESP-IDF bundled script
# Try legacy import name (older versions)
try:
import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Fall back to calling the ESP-IDF script directly
idf_path = os.environ.get("IDF_PATH", "")
gen_script = os.path.join(idf_path, "components", "nvs_flash",
"nvs_partition_generator", "nvs_partition_gen.py")
@@ -108,10 +113,13 @@ def generate_nvs_binary(csv_content, size):
with open(bin_path, "rb") as f:
return f.read()
raise RuntimeError(
"NVS partition generator not available. "
"Install: pip install esp-idf-nvs-partition-gen"
)
# Last resort: try as a module
subprocess.check_call([
sys.executable, "-m", "nvs_partition_gen", "generate",
csv_path, bin_path, hex(size)
])
with open(bin_path, "rb") as f:
return f.read()
finally:
for p in (csv_path, bin_path):
@@ -160,9 +168,7 @@ def main():
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
help="Edge processing tier: 0=off, 1=stats, 2=vitals")
parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold in milli-units "
"(value/1000 = rad/s²). Default: 15000 → 15.0 rad/s². "
"Raise to reduce false positives in high-traffic areas.")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)")
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")
@@ -1,29 +0,0 @@
# ESP32-S3 CSI Node — 4MB Flash SDK Configuration (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
#
# Build: cp sdkconfig.defaults.4mb sdkconfig.defaults && idf.py set-target esp32s3 && idf.py build
# Or: idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults.4mb" set-target esp32s3 && idf.py build
CONFIG_IDF_TARGET="esp32s3"
# 4MB flash partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# Compiler: optimize for size (critical for 4MB)
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CSI support
CONFIG_ESP_WIFI_CSI_ENABLED=y
# Disable display support to save flash (ADR-045 display requires 8MB)
# CONFIG_DISPLAY_ENABLE is not set
# Reduce logging to save flash
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LWIP_SO_RCVBUF=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
@@ -63,13 +63,3 @@ esp_err_t wasm_runtime_unload(uint8_t id) { (void)id; return ESP_OK; }
void wasm_runtime_on_timer(void) {}
void wasm_runtime_get_info(wasm_module_info_t *info, uint8_t *count) { (void)info; if(count) *count = 0; }
esp_err_t wasm_runtime_set_manifest(uint8_t id, const char *n, uint32_t c, uint32_t m) { (void)id; (void)n; (void)c; (void)m; return ESP_OK; }
/* ---- mmwave_sensor stubs (ADR-063) ---- */
#include "mmwave_sensor.h"
static mmwave_state_t s_stub_mmwave = {0};
esp_err_t mmwave_sensor_init(int tx, int rx) { (void)tx; (void)rx; return ESP_ERR_NOT_FOUND; }
bool mmwave_sensor_get_state(mmwave_state_t *s) { if (s) *s = s_stub_mmwave; return false; }
const char *mmwave_type_name(mmwave_type_t t) { (void)t; return "None"; }
+1 -22
View File
@@ -20,9 +20,8 @@
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_FAIL (-1)
#define ESP_ERR_NO_MEM 0x101
#define ESP_ERR_NO_MEM 0x101
#define ESP_ERR_INVALID_ARG 0x102
#define ESP_ERR_NOT_FOUND 0x105
/* ---- esp_log.h ---- */
#define ESP_LOGI(tag, fmt, ...) ((void)0)
@@ -34,32 +33,12 @@ typedef int esp_err_t;
/* ---- esp_timer.h ---- */
typedef void *esp_timer_handle_t;
/** Timer callback type (matches ESP-IDF signature). */
typedef void (*esp_timer_cb_t)(void *arg);
/** Timer creation arguments (matches ESP-IDF esp_timer_create_args_t). */
typedef struct {
esp_timer_cb_t callback;
void *arg;
const char *name;
} esp_timer_create_args_t;
/**
* Stub: returns a monotonically increasing microsecond counter.
* Declared here, defined in esp_stubs.c.
*/
int64_t esp_timer_get_time(void);
/** Stub: timer lifecycle (no-ops for fuzz testing). */
static inline esp_err_t esp_timer_create(const esp_timer_create_args_t *args, esp_timer_handle_t *h) {
(void)args; if (h) *h = (void *)1; return ESP_OK;
}
static inline esp_err_t esp_timer_start_periodic(esp_timer_handle_t h, uint64_t period) {
(void)h; (void)period; return ESP_OK;
}
static inline esp_err_t esp_timer_stop(esp_timer_handle_t h) { (void)h; return ESP_OK; }
static inline esp_err_t esp_timer_delete(esp_timer_handle_t h) { (void)h; return ESP_OK; }
/* ---- esp_wifi_types.h ---- */
/** Minimal rx_ctrl fields needed by csi_serialize_frame. */
@@ -0,0 +1,10 @@
{"type":"edit","file":"unknown","timestamp":1772820418129,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772820462588,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772820472219,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832571444,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832585997,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773099593107,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115162931,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115172336,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773147087836,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773149448951,"sessionId":null}
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
else
exec node "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.cjs" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
else
exec node "$basedir/../browserslist/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
else
exec node "$basedir/../jsesc/bin/jsesc" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jsesc\bin\jsesc" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
else
exec node "$basedir/../json5/lib/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\json5\lib\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../json5/lib/cli.js" $args
} else {
& "node$exe" "$basedir/../json5/lib/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@"
else
exec node "$basedir/../loose-envify/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\loose-envify\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../loose-envify/cli.js" $args
} else {
& "node$exe" "$basedir/../loose-envify/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
else
exec node "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@babel\parser\bin\babel-parser.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
} else {
& "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
} else {
& "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
else
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
else
exec node "$basedir/../semver/bin/semver.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@"
else
exec node "$basedir/../update-browserslist-db/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\update-browserslist-db\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../update-browserslist-db/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../update-browserslist-db/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../update-browserslist-db/cli.js" $args
} else {
& "node$exe" "$basedir/../update-browserslist-db/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
import {
Channel,
PluginListener,
Resource,
SERIALIZE_TO_IPC_FN,
addPluginListener,
checkPermissions,
convertFileSrc,
invoke,
isTauri,
requestPermissions,
transformCallback
} from "./chunk-G7S6KQDI.js";
import "./chunk-BUSYA2B4.js";
export {
Channel,
PluginListener,
Resource,
SERIALIZE_TO_IPC_FN,
addPluginListener,
checkPermissions,
convertFileSrc,
invoke,
isTauri,
requestPermissions,
transformCallback
};
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
@@ -0,0 +1,70 @@
import {
invoke,
transformCallback
} from "./chunk-G7S6KQDI.js";
import "./chunk-BUSYA2B4.js";
// node_modules/@tauri-apps/api/event.js
var TauriEvent;
(function(TauriEvent2) {
TauriEvent2["WINDOW_RESIZED"] = "tauri://resize";
TauriEvent2["WINDOW_MOVED"] = "tauri://move";
TauriEvent2["WINDOW_CLOSE_REQUESTED"] = "tauri://close-requested";
TauriEvent2["WINDOW_DESTROYED"] = "tauri://destroyed";
TauriEvent2["WINDOW_FOCUS"] = "tauri://focus";
TauriEvent2["WINDOW_BLUR"] = "tauri://blur";
TauriEvent2["WINDOW_SCALE_FACTOR_CHANGED"] = "tauri://scale-change";
TauriEvent2["WINDOW_THEME_CHANGED"] = "tauri://theme-changed";
TauriEvent2["WINDOW_CREATED"] = "tauri://window-created";
TauriEvent2["WEBVIEW_CREATED"] = "tauri://webview-created";
TauriEvent2["DRAG_ENTER"] = "tauri://drag-enter";
TauriEvent2["DRAG_OVER"] = "tauri://drag-over";
TauriEvent2["DRAG_DROP"] = "tauri://drag-drop";
TauriEvent2["DRAG_LEAVE"] = "tauri://drag-leave";
})(TauriEvent || (TauriEvent = {}));
async function _unlisten(event, eventId) {
window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId);
await invoke("plugin:event|unlisten", {
event,
eventId
});
}
async function listen(event, handler, options) {
var _a;
const target = typeof (options === null || options === void 0 ? void 0 : options.target) === "string" ? { kind: "AnyLabel", label: options.target } : (_a = options === null || options === void 0 ? void 0 : options.target) !== null && _a !== void 0 ? _a : { kind: "Any" };
return invoke("plugin:event|listen", {
event,
target,
handler: transformCallback(handler)
}).then((eventId) => {
return async () => _unlisten(event, eventId);
});
}
async function once(event, handler, options) {
return listen(event, (eventData) => {
void _unlisten(event, eventData.id);
handler(eventData);
}, options);
}
async function emit(event, payload) {
await invoke("plugin:event|emit", {
event,
payload
});
}
async function emitTo(target, event, payload) {
const eventTarget = typeof target === "string" ? { kind: "AnyLabel", label: target } : target;
await invoke("plugin:event|emit_to", {
target: eventTarget,
event,
payload
});
}
export {
TauriEvent,
emit,
emitTo,
listen,
once
};
//# sourceMappingURL=@tauri-apps_api_event.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
import {
invoke
} from "./chunk-G7S6KQDI.js";
import "./chunk-BUSYA2B4.js";
// node_modules/@tauri-apps/plugin-dialog/dist-js/index.js
function buttonsToRust(buttons) {
if (buttons === void 0) {
return void 0;
}
if (typeof buttons === "string") {
return buttons;
} else if ("ok" in buttons && "cancel" in buttons) {
return { OkCancelCustom: [buttons.ok, buttons.cancel] };
} else if ("yes" in buttons && "no" in buttons && "cancel" in buttons) {
return {
YesNoCancelCustom: [buttons.yes, buttons.no, buttons.cancel]
};
} else if ("ok" in buttons) {
return { OkCustom: buttons.ok };
}
return void 0;
}
async function open(options = {}) {
if (typeof options === "object") {
Object.freeze(options);
}
return await invoke("plugin:dialog|open", { options });
}
async function save(options = {}) {
if (typeof options === "object") {
Object.freeze(options);
}
return await invoke("plugin:dialog|save", { options });
}
async function message(message2, options) {
var _a, _b;
const opts = typeof options === "string" ? { title: options } : options;
return invoke("plugin:dialog|message", {
message: message2.toString(),
title: (_a = opts == null ? void 0 : opts.title) == null ? void 0 : _a.toString(),
kind: opts == null ? void 0 : opts.kind,
okButtonLabel: (_b = opts == null ? void 0 : opts.okLabel) == null ? void 0 : _b.toString(),
buttons: buttonsToRust(opts == null ? void 0 : opts.buttons)
});
}
async function ask(message2, options) {
var _a, _b, _c;
const opts = typeof options === "string" ? { title: options } : options;
return await invoke("plugin:dialog|ask", {
message: message2.toString(),
title: (_a = opts == null ? void 0 : opts.title) == null ? void 0 : _a.toString(),
kind: opts == null ? void 0 : opts.kind,
yesButtonLabel: (_b = opts == null ? void 0 : opts.okLabel) == null ? void 0 : _b.toString(),
noButtonLabel: (_c = opts == null ? void 0 : opts.cancelLabel) == null ? void 0 : _c.toString()
});
}
async function confirm(message2, options) {
var _a, _b, _c;
const opts = typeof options === "string" ? { title: options } : options;
return await invoke("plugin:dialog|confirm", {
message: message2.toString(),
title: (_a = opts == null ? void 0 : opts.title) == null ? void 0 : _a.toString(),
kind: opts == null ? void 0 : opts.kind,
okButtonLabel: (_b = opts == null ? void 0 : opts.okLabel) == null ? void 0 : _b.toString(),
cancelButtonLabel: (_c = opts == null ? void 0 : opts.cancelLabel) == null ? void 0 : _c.toString()
});
}
export {
ask,
confirm,
message,
open,
save
};
//# sourceMappingURL=@tauri-apps_plugin-dialog.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,70 @@
{
"hash": "13a93b42",
"configHash": "11f7da6e",
"lockfileHash": "c11f8b2c",
"browserHash": "7ba6c70b",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "719a8793",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "a01e6562",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "9805f9d7",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "887c0649",
"needsInterop": true
},
"@tauri-apps/api/core": {
"src": "../../@tauri-apps/api/core.js",
"file": "@tauri-apps_api_core.js",
"fileHash": "03cf5cf9",
"needsInterop": false
},
"@tauri-apps/api/event": {
"src": "../../@tauri-apps/api/event.js",
"file": "@tauri-apps_api_event.js",
"fileHash": "885e887d",
"needsInterop": false
},
"@tauri-apps/plugin-dialog": {
"src": "../../@tauri-apps/plugin-dialog/dist-js/index.js",
"file": "@tauri-apps_plugin-dialog.js",
"fileHash": "466fff97",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "4d2affb4",
"needsInterop": true
}
},
"chunks": {
"chunk-G7S6KQDI": {
"file": "chunk-G7S6KQDI.js"
},
"chunk-KVMAXHTM": {
"file": "chunk-KVMAXHTM.js"
},
"chunk-L57YJLEW": {
"file": "chunk-L57YJLEW.js"
},
"chunk-BUSYA2B4": {
"file": "chunk-BUSYA2B4.js"
}
}
}
@@ -0,0 +1,8 @@
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
export {
__commonJS
};
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
@@ -0,0 +1,150 @@
// node_modules/@tauri-apps/api/external/tslib/tslib.es6.js
function __classPrivateFieldGet(receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
}
function __classPrivateFieldSet(receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value;
}
// node_modules/@tauri-apps/api/core.js
var _Channel_onmessage;
var _Channel_nextMessageIndex;
var _Channel_pendingMessages;
var _Channel_messageEndIndex;
var _Resource_rid;
var SERIALIZE_TO_IPC_FN = "__TAURI_TO_IPC_KEY__";
function transformCallback(callback, once = false) {
return window.__TAURI_INTERNALS__.transformCallback(callback, once);
}
var Channel = class {
constructor(onmessage) {
_Channel_onmessage.set(this, void 0);
_Channel_nextMessageIndex.set(this, 0);
_Channel_pendingMessages.set(this, []);
_Channel_messageEndIndex.set(this, void 0);
__classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => {
}), "f");
this.id = transformCallback((rawMessage) => {
const index = rawMessage.index;
if ("end" in rawMessage) {
if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) {
this.cleanupCallback();
} else {
__classPrivateFieldSet(this, _Channel_messageEndIndex, index, "f");
}
return;
}
const message = rawMessage.message;
if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) {
__classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message);
__classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f");
while (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") in __classPrivateFieldGet(this, _Channel_pendingMessages, "f")) {
const message2 = __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")];
__classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message2);
delete __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")];
__classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f");
}
if (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") === __classPrivateFieldGet(this, _Channel_messageEndIndex, "f")) {
this.cleanupCallback();
}
} else {
__classPrivateFieldGet(this, _Channel_pendingMessages, "f")[index] = message;
}
});
}
cleanupCallback() {
window.__TAURI_INTERNALS__.unregisterCallback(this.id);
}
set onmessage(handler) {
__classPrivateFieldSet(this, _Channel_onmessage, handler, "f");
}
get onmessage() {
return __classPrivateFieldGet(this, _Channel_onmessage, "f");
}
[(_Channel_onmessage = /* @__PURE__ */ new WeakMap(), _Channel_nextMessageIndex = /* @__PURE__ */ new WeakMap(), _Channel_pendingMessages = /* @__PURE__ */ new WeakMap(), _Channel_messageEndIndex = /* @__PURE__ */ new WeakMap(), SERIALIZE_TO_IPC_FN)]() {
return `__CHANNEL__:${this.id}`;
}
toJSON() {
return this[SERIALIZE_TO_IPC_FN]();
}
};
var PluginListener = class {
constructor(plugin, event, channelId) {
this.plugin = plugin;
this.event = event;
this.channelId = channelId;
}
async unregister() {
return invoke(`plugin:${this.plugin}|remove_listener`, {
event: this.event,
channelId: this.channelId
});
}
};
async function addPluginListener(plugin, event, cb) {
const handler = new Channel(cb);
try {
await invoke(`plugin:${plugin}|register_listener`, {
event,
handler
});
return new PluginListener(plugin, event, handler.id);
} catch {
await invoke(`plugin:${plugin}|registerListener`, { event, handler });
return new PluginListener(plugin, event, handler.id);
}
}
async function checkPermissions(plugin) {
return invoke(`plugin:${plugin}|check_permissions`);
}
async function requestPermissions(plugin) {
return invoke(`plugin:${plugin}|request_permissions`);
}
async function invoke(cmd, args = {}, options) {
return window.__TAURI_INTERNALS__.invoke(cmd, args, options);
}
function convertFileSrc(filePath, protocol = "asset") {
return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol);
}
var Resource = class {
get rid() {
return __classPrivateFieldGet(this, _Resource_rid, "f");
}
constructor(rid) {
_Resource_rid.set(this, void 0);
__classPrivateFieldSet(this, _Resource_rid, rid, "f");
}
/**
* Destroys and cleans up this resource from memory.
* **You should not call any method on this object anymore and should drop any reference to it.**
*/
async close() {
return invoke("plugin:resources|close", {
rid: this.rid
});
}
};
_Resource_rid = /* @__PURE__ */ new WeakMap();
function isTauri() {
return !!(globalThis || window).isTauri;
}
export {
SERIALIZE_TO_IPC_FN,
transformCallback,
Channel,
PluginListener,
addPluginListener,
checkPermissions,
requestPermissions,
invoke,
convertFileSrc,
Resource,
isTauri
};
//# sourceMappingURL=chunk-G7S6KQDI.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -0,0 +1,6 @@
import {
require_react_dom
} from "./chunk-KVMAXHTM.js";
import "./chunk-L57YJLEW.js";
import "./chunk-BUSYA2B4.js";
export default require_react_dom();
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
@@ -0,0 +1,39 @@
import {
require_react_dom
} from "./chunk-KVMAXHTM.js";
import "./chunk-L57YJLEW.js";
import {
__commonJS
} from "./chunk-BUSYA2B4.js";
// node_modules/react-dom/client.js
var require_client = __commonJS({
"node_modules/react-dom/client.js"(exports) {
var m = require_react_dom();
if (false) {
exports.createRoot = m.createRoot;
exports.hydrateRoot = m.hydrateRoot;
} else {
i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
exports.createRoot = function(c, o) {
i.usingClientEntryPoint = true;
try {
return m.createRoot(c, o);
} finally {
i.usingClientEntryPoint = false;
}
};
exports.hydrateRoot = function(c, h, o) {
i.usingClientEntryPoint = true;
try {
return m.hydrateRoot(c, h, o);
} finally {
i.usingClientEntryPoint = false;
}
};
}
var i;
}
});
export default require_client();
//# sourceMappingURL=react-dom_client.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../react-dom/client.js"],
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
"mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
"names": []
}

Some files were not shown because too many files have changed in this diff Show More