diff --git a/.gitignore b/.gitignore index dcb8cd1..93c41db 100644 --- a/.gitignore +++ b/.gitignore @@ -193,6 +193,9 @@ cython_debug/ # PyPI configuration file .pypirc +# Compiled Swift helper binaries (macOS WiFi sensing) +v1/src/sensing/mac_wifi + # Cursor # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data diff --git a/CHANGELOG.md b/CHANGELOG.md index 37209fa..6ad2b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,68 +5,238 @@ 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). +## [Unreleased] + +### Added +- **Cross-platform RSSI adapters** — macOS CoreWLAN (`MacosCoreWlanScanner`) and Linux `iw` (`LinuxIwScanner`) Rust adapters with `#[cfg(target_os)]` gating +- macOS CoreWLAN Python sensing adapter with Swift helper (`mac_wifi.swift`) +- macOS synthetic BSSID generation (FNV-1a hash) for Sonoma 14.4+ BSSID redaction +- Linux `iw dev scan` parser with freq-to-channel conversion and `scan dump` (no-root) mode +- ADR-025: macOS CoreWLAN WiFi Sensing (ORCA) + +### Fixed +- Removed synthetic byte counters from Python `MacosWifiCollector` — now reports `tx_bytes=0, rx_bytes=0` instead of fake incrementing values + +--- + +## [3.0.0] - 2026-03-01 + +Major release: AETHER contrastive embedding model, Docker Hub images, and comprehensive UI overhaul. + +### Added — AETHER Contrastive Embedding Model (ADR-024) +- **Project AETHER** — self-supervised contrastive learning for WiFi CSI fingerprinting, similarity search, and anomaly detection (`9bbe956`) +- `embedding.rs` module: `ProjectionHead`, `InfoNceLoss`, `CsiAugmenter`, `FingerprintIndex`, `PoseEncoder`, `EmbeddingExtractor` (909 lines, zero external ML dependencies) +- SimCLR-style pretraining with 5 physically-motivated augmentations (temporal jitter, subcarrier masking, Gaussian noise, phase rotation, amplitude scaling) +- CLI flags: `--pretrain`, `--pretrain-epochs`, `--embed`, `--build-index ` +- Four HNSW-compatible fingerprint index types: `env_fingerprint`, `activity_pattern`, `temporal_baseline`, `person_track` +- Cross-modal `PoseEncoder` for WiFi-to-camera embedding alignment +- VICReg regularization for embedding collapse prevention +- 53K total parameters (55 KB at INT8) — fits on ESP32 + +### Added — Docker & Deployment +- Published Docker Hub images: `ruvnet/wifi-densepose:latest` (132 MB Rust) and `ruvnet/wifi-densepose:python` (569 MB) (`add9f19`) +- Multi-stage Dockerfile for Rust sensing server with RuVector crates +- `docker-compose.yml` orchestrating both Rust and Python services +- RVF model export via `--export-rvf` and load via `--load-rvf` CLI flags + +### Added — Documentation +- 33 use cases across 4 vertical tiers: Everyday, Specialized, Robotics & Industrial, Extreme (`0afd9c5`) +- "Why WiFi Wins" comparison table (WiFi vs camera vs LIDAR vs wearable vs PIR) +- Mermaid architecture diagrams: end-to-end pipeline, signal processing detail, deployment topology (`50f0fc9`) +- Models & Training section with RuVector crate links (GitHub + crates.io), SONA component table (`965a1cc`) +- RVF container section with deployment targets table (ESP32 0.7 MB to server 50+ MB) +- Collapsible README sections for improved navigation (`478d964`, `99ec980`, `0ebd6be`) +- Installation and Quick Start moved above Table of Contents (`50acbf7`) +- CSI hardware requirement notice (`528b394`) + +### Fixed +- **UI auto-detects server port from page origin** — no more hardcoded `localhost:8080`; works on any port (Docker :3000, native :8080, custom) (`3b72f35`, closes #55) +- **Docker port mismatch** — server now binds 3000/3001 inside container as documented (`44b9c30`) +- Added `/ws/sensing` WebSocket route to the HTTP server so UI only needs one port +- Fixed README API endpoint references: `/api/v1/health` → `/health`, `/api/v1/sensing` → `/api/v1/sensing/latest` +- Multi-person tracking limit corrected: configurable default 10, no hard software cap (`e2ce250`) + +--- + +## [2.0.0] - 2026-02-28 + +Major release: complete Rust sensing server, full DensePose training pipeline, RuVector v2.0.4 integration, ESP32-S3 firmware, and 6 security hardening patches. + +### Added — Rust Sensing Server +- **Full DensePose-compatible REST API** served by Axum (`d956c30`) + - `GET /health` — server health + - `GET /api/v1/sensing/latest` — live CSI sensing data + - `GET /api/v1/vital-signs` — breathing rate (6-30 BPM) and heartbeat (40-120 BPM) + - `GET /api/v1/pose/current` — 17 COCO keypoints derived from WiFi signal field + - `GET /api/v1/info` — server build and feature info + - `GET /api/v1/model/info` — RVF model container metadata + - `ws://host/ws/sensing` — real-time WebSocket stream +- Three data sources: `--source esp32` (UDP CSI), `--source windows` (netsh RSSI), `--source simulated` (deterministic reference) +- Auto-detection: server probes ESP32 UDP and Windows WiFi, falls back to simulated +- Three.js visualization UI with 3D body skeleton, signal heatmap, phase plot, Doppler bars, vital signs panel +- Static UI serving via `--ui-path` flag +- Throughput: 9,520–11,665 frames/sec (release build) + +### Added — ADR-021: Vital Sign Detection +- `VitalSignDetector` with breathing (6-30 BPM) and heartbeat (40-120 BPM) extraction from CSI fluctuations (`1192de9`) +- FFT-based spectral analysis with configurable band-pass filters +- Confidence scoring based on spectral peak prominence +- REST endpoint `/api/v1/vital-signs` with real-time JSON output + +### Added — ADR-023: DensePose Training Pipeline (Phases 1-8) +- `wifi-densepose-train` crate with complete 8-phase pipeline (`fc409df`, `ec98e40`, `fce1271`) + - Phase 1: `DataPipeline` with MM-Fi and Wi-Pose dataset loaders + - Phase 2: `CsiToPoseTransformer` — 4-head cross-attention + 2-layer GCN on COCO skeleton + - Phase 3: 6-term composite loss (MSE, bone length, symmetry, joint angle, temporal, confidence) + - Phase 4: `DynamicPersonMatcher` via ruvector-mincut (O(n^1.5 log n) Hungarian assignment) + - Phase 5: `SonaAdapter` — MicroLoRA rank-4 with EWC++ memory preservation + - Phase 6: `SparseInference` — progressive 3-layer model loading (A: essential, B: refinement, C: full) + - Phase 7: `RvfContainer` — single-file model packaging with segment-based binary format + - Phase 8: End-to-end training with cosine-annealing LR, early stopping, checkpoint saving +- CLI: `--train`, `--dataset`, `--epochs`, `--save-rvf`, `--load-rvf`, `--export-rvf` +- Benchmark: ~11,665 fps inference, 229 tests passing + +### Added — ADR-016: RuVector Training Integration (all 5 crates) +- `ruvector-mincut` → `DynamicPersonMatcher` in `metrics.rs` + subcarrier selection (`81ad09d`, `a7dd31c`) +- `ruvector-attn-mincut` → antenna attention in `model.rs` + noise-gated spectrogram +- `ruvector-temporal-tensor` → `CompressedCsiBuffer` in `dataset.rs` + compressed breathing/heartbeat +- `ruvector-solver` → sparse subcarrier interpolation (114→56) + Fresnel triangulation +- `ruvector-attention` → spatial attention in `model.rs` + attention-weighted BVP +- Vendored all 11 RuVector crates under `vendor/ruvector/` (`d803bfe`) + +### Added — ADR-017: RuVector Signal & MAT Integration (7 integration points) +- `gate_spectrogram()` — attention-gated noise suppression (`18170d7`) +- `attention_weighted_bvp()` — sensitivity-weighted velocity profiles +- `mincut_subcarrier_partition()` — dynamic sensitive/insensitive subcarrier split +- `solve_fresnel_geometry()` — TX-body-RX distance estimation +- `CompressedBreathingBuffer` + `CompressedHeartbeatSpectrogram` +- `BreathingDetector` + `HeartbeatDetector` (MAT crate, real FFT + micro-Doppler) +- Feature-gated behind `cfg(feature = "ruvector")` (`ab2453e`) + +### Added — ADR-018: ESP32-S3 Firmware & Live CSI Pipeline +- ESP32-S3 firmware with FreeRTOS CSI extraction (`92a5182`) +- ADR-018 binary frame format: `[0xAD, 0x18, len_hi, len_lo, payload]` +- Rust `Esp32Aggregator` receiving UDP frames on port 5005 +- `bridge.rs` converting I/Q pairs to amplitude/phase vectors +- NVS provisioning for WiFi credentials +- Pre-built binary quick start documentation (`696a726`) + +### Added — ADR-014: SOTA Signal Processing +- 6 algorithms, 83 tests (`fcb93cc`) + - Hampel filter (median + MAD, resistant to 50% contamination) + - Conjugate multiplication (reference-antenna ratio, cancels common-mode noise) + - Phase sanitization (unwrap + linear detrend, removes CFO/SFO) + - Fresnel zone geometry (TX-body-RX distance from first-principles physics) + - Body Velocity Profile (micro-Doppler extraction, 5.7x speedup) + - Attention-gated spectrogram (learned noise suppression) + +### Added — ADR-015: Public Dataset Training Strategy +- MM-Fi and Wi-Pose dataset specifications with download links (`4babb32`, `5dc2f66`) +- Verified dataset dimensions, sampling rates, and annotation formats +- Cross-dataset evaluation protocol + +### Added — WiFi-Mat Disaster Detection Module +- Multi-AP triangulation for through-wall survivor detection (`a17b630`, `6b20ff0`) +- Triage classification (breathing, heartbeat, motion) +- Domain events: `survivor_detected`, `survivor_updated`, `alert_created` +- WebSocket broadcast at `/ws/mat/stream` + +### Added — Infrastructure +- Guided 7-step interactive installer with 8 hardware profiles (`8583f3e`) +- Comprehensive build guide for Linux, macOS, Windows, Docker, ESP32 (`45f8a0d`) +- 12 Architecture Decision Records (ADR-001 through ADR-012) (`337dd96`) + +### Added — UI & Visualization +- Sensing-only UI mode with Gaussian splat visualization (`b7e0f07`) +- Three.js 3D body model (17 joints, 16 limbs) with signal-viz components +- Tabs: Dashboard, Hardware, Live Demo, Sensing, Architecture, Performance, Applications +- WebSocket client with automatic reconnection and exponential backoff + +### Added — Rust Signal Processing Crate +- Complete Rust port of WiFi-DensePose with modular workspace (`6ed69a3`) + - `wifi-densepose-signal` — CSI processing, phase sanitization, feature extraction + - `wifi-densepose-core` — shared types and configuration + - `wifi-densepose-nn` — neural network inference (DensePose head, RCNN) + - `wifi-densepose-hardware` — ESP32 aggregator, hardware interfaces + - `wifi-densepose-config` — configuration management +- Comprehensive benchmarks and validation tests (`3ccb301`) + +### Added — Python Sensing Pipeline +- `WindowsWifiCollector` — RSSI collection via `netsh wlan show networks` +- `RssiFeatureExtractor` — variance, spectral bands (motion 0.5-4 Hz, breathing 0.1-0.5 Hz), change points +- `PresenceClassifier` — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE) +- Cross-receiver agreement scoring for multi-AP confidence boosting +- WebSocket sensing server (`ws_server.py`) broadcasting JSON at 2 Hz +- Deterministic CSI proof bundles for reproducible verification (`v1/data/proof/`) +- Commodity sensing unit tests (`b391638`) + +### Changed +- Rust hardware adapters now return explicit errors instead of silent empty data (`6e0e539`) + +### Fixed +- Review fixes for end-to-end training pipeline (`45f0304`) +- Dockerfile paths updated from `src/` to `v1/src/` (`7872987`) +- IoT profile installer instructions updated for aggregator CLI (`f460097`) +- `process.env` reference removed from browser ES module (`e320bc9`) + +### Performance +- 5.7x Doppler extraction speedup via optimized FFT windowing (`32c75c8`) +- Single 2.1 MB static binary, zero Python dependencies for Rust server + +### Security +- Fix SQL injection in status command and migrations (`f9d125d`) +- Fix XSS vulnerabilities in UI components (`5db55fd`) +- Fix command injection in statusline.cjs (`4cb01fd`) +- Fix path traversal vulnerabilities (`896c4fc`) +- Fix insecure WebSocket connections — enforce wss:// on non-localhost (`ac094d4`) +- Fix GitHub Actions shell injection (`ab2e7b4`) +- Fix 10 additional vulnerabilities, remove 12 dead code instances (`7afdad0`) + +--- + ## [1.1.0] - 2025-06-07 ### Added -- Multi-column table of contents in README.md for improved navigation -- Enhanced documentation structure with better organization -- Improved visual layout for better user experience +- Complete Python WiFi-DensePose system with CSI data extraction and router interface +- CSI processing and phase sanitization modules +- Batch processing for CSI data in `CSIProcessor` and `PhaseSanitizer` +- Hardware, pose, and stream services for WiFi-DensePose API +- Comprehensive CSS styles for UI components and dark mode support +- API and Deployment documentation -### Changed -- Updated README.md table of contents to use a two-column layout -- Reorganized documentation sections for better logical flow -- Enhanced readability of navigation structure +### Fixed +- Badge links for PyPI and Docker in README +- Async engine creation poolclass specification -### Documentation -- Restructured table of contents for better accessibility -- Improved visual hierarchy in documentation -- Enhanced user experience for documentation navigation +--- ## [1.0.0] - 2024-12-01 ### Added -- Initial release of WiFi DensePose -- Real-time WiFi-based human pose estimation using CSI data -- DensePose neural network integration -- RESTful API with comprehensive endpoints -- WebSocket streaming for real-time data -- Multi-person tracking capabilities +- Initial release of WiFi-DensePose +- Real-time WiFi-based human pose estimation using Channel State Information (CSI) +- DensePose neural network integration for body surface mapping +- RESTful API with comprehensive endpoint coverage +- WebSocket streaming for real-time pose data +- Multi-person tracking with configurable capacity (default 10, up to 50+) - Fall detection and activity recognition -- Healthcare, fitness, smart home, and security domain configurations -- Comprehensive CLI interface -- Docker and Kubernetes deployment support -- 100% test coverage -- Production-ready monitoring and logging -- Hardware abstraction layer for multiple WiFi devices -- Phase sanitization and signal processing +- Domain configurations: healthcare, fitness, smart home, security +- CLI interface for server management and configuration +- Hardware abstraction layer for multiple WiFi chipsets +- Phase sanitization and signal processing pipeline - Authentication and rate limiting - Background task management -- Database integration with PostgreSQL and Redis -- Prometheus metrics and Grafana dashboards -- Comprehensive documentation and examples - -### Features -- Privacy-preserving pose detection without cameras -- Sub-50ms latency with 30 FPS processing -- Support for up to 10 simultaneous person tracking -- Enterprise-grade security and scalability -- Cross-platform compatibility (Linux, macOS, Windows) -- GPU acceleration support -- Real-time analytics and alerting -- Configurable confidence thresholds -- Zone-based occupancy monitoring -- Historical data analysis -- Performance optimization tools -- Load testing capabilities -- Infrastructure as Code (Terraform, Ansible) -- CI/CD pipeline integration -- Comprehensive error handling and logging +- Cross-platform support (Linux, macOS, Windows) ### Documentation -- Complete user guide and API reference +- User guide and API reference - Deployment and troubleshooting guides - Hardware setup and calibration instructions -- Performance benchmarks and optimization tips -- Contributing guidelines and code standards -- Security best practices -- Example configurations and use cases \ No newline at end of file +- Performance benchmarks +- Contributing guidelines + +[Unreleased]: https://github.com/ruvnet/wifi-densepose/compare/v3.0.0...HEAD +[3.0.0]: https://github.com/ruvnet/wifi-densepose/compare/v2.0.0...v3.0.0 +[2.0.0]: https://github.com/ruvnet/wifi-densepose/compare/v1.1.0...v2.0.0 +[1.1.0]: https://github.com/ruvnet/wifi-densepose/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/ruvnet/wifi-densepose/releases/tag/v1.0.0 diff --git a/README.md b/README.md index e9d6d10..924d647 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest > |--------|----------|------|----------|-------------| > | **ESP32 Mesh** (recommended) | 3-6x ESP32-S3 + WiFi router | ~$54 | Yes | Pose, breathing, heartbeat, motion, presence | > | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO | -> | **Any WiFi** | Windows/Linux laptop | $0 | No | RSSI-only: coarse presence and motion | +> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion | > > No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python v1/data/proof/verify.py` @@ -356,7 +356,7 @@ cargo add wifi-densepose-ruvector # RuVector v2.0.4 integration layer (ADR-017 | [`wifi-densepose-ruvector`](https://crates.io/crates/wifi-densepose-ruvector) | RuVector v2.0.4 integration layer — 7 signal+MAT integration points (ADR-017) | **All 5** | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-ruvector.svg)](https://crates.io/crates/wifi-densepose-ruvector) | | [`wifi-densepose-vitals`](https://crates.io/crates/wifi-densepose-vitals) | Vital signs: breathing (6-30 BPM), heart rate (40-120 BPM) | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-vitals.svg)](https://crates.io/crates/wifi-densepose-vitals) | | [`wifi-densepose-hardware`](https://crates.io/crates/wifi-densepose-hardware) | ESP32, Intel 5300, Atheros CSI sensor interfaces | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-hardware.svg)](https://crates.io/crates/wifi-densepose-hardware) | -| [`wifi-densepose-wifiscan`](https://crates.io/crates/wifi-densepose-wifiscan) | Multi-BSSID WiFi scanning (Windows-enhanced) | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-wifiscan.svg)](https://crates.io/crates/wifi-densepose-wifiscan) | +| [`wifi-densepose-wifiscan`](https://crates.io/crates/wifi-densepose-wifiscan) | Multi-BSSID WiFi scanning (Windows, macOS, Linux) | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-wifiscan.svg)](https://crates.io/crates/wifi-densepose-wifiscan) | | [`wifi-densepose-wasm`](https://crates.io/crates/wifi-densepose-wasm) | WebAssembly bindings for browser deployment | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-wasm.svg)](https://crates.io/crates/wifi-densepose-wasm) | | [`wifi-densepose-sensing-server`](https://crates.io/crates/wifi-densepose-sensing-server) | Axum server: UDP ingestion, WebSocket broadcast | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-sensing-server.svg)](https://crates.io/crates/wifi-densepose-sensing-server) | | [`wifi-densepose-cli`](https://crates.io/crates/wifi-densepose-cli) | Command-line tool for MAT disaster scanning | -- | [![crates.io](https://img.shields.io/crates/v/wifi-densepose-cli.svg)](https://crates.io/crates/wifi-densepose-cli) | @@ -622,7 +622,7 @@ See [ADR-021](docs/adr/ADR-021-vital-sign-detection-rvdna-pipeline.md).
-📡 WiFi Scan Domain Layer (ADR-022) — 8-stage RSSI pipeline for Windows WiFi +📡 WiFi Scan Domain Layer (ADR-022/025) — 8-stage RSSI pipeline for Windows, macOS, and Linux WiFi | Stage | Purpose | |-------|---------| @@ -1106,6 +1106,8 @@ WebSocket: `ws://localhost:3001/ws/sensing` (real-time sensing + vital signs) | Intel 5300 | Firmware mod | ~$15 | Linux `iwl-csi` | | Atheros AR9580 | ath9k patch | ~$20 | Linux only | | Any Windows WiFi | RSSI only | $0 | [Tutorial #36](https://github.com/ruvnet/wifi-densepose/issues/36) | +| Any macOS WiFi | RSSI only (CoreWLAN) | $0 | [ADR-025](docs/adr/ADR-025-macos-corewlan-wifi-sensing.md) | +| Any Linux WiFi | RSSI only (`iw`) | $0 | Requires `iw` + `CAP_NET_ADMIN` |
@@ -1279,7 +1281,7 @@ The largest release to date — delivers the complete end-to-end training pipeli - **`--export-rvf` CLI flag** — Standalone RVF model container generation with vital config, training proof, and SONA profiles - **`--train` CLI flag** — Full training mode with best-epoch snapshotting and checkpoint saving - **Vital sign detection (ADR-021)** — FFT-based breathing (6-30 BPM) and heartbeat (40-120 BPM) extraction, 11,665 fps benchmark -- **WiFi scan domain layer (ADR-022)** — 8-stage pure-Rust signal intelligence pipeline for Windows WiFi RSSI +- **WiFi scan domain layer (ADR-022/025)** — 8-stage pure-Rust signal intelligence pipeline for Windows, macOS, and Linux WiFi RSSI - **New crates** — `wifi-densepose-vitals` (1,863 lines) and `wifi-densepose-wifiscan` (4,829 lines) - **542+ Rust tests** — All passing, zero mocks diff --git a/claude.md b/claude.md index ad4b2fd..9e545b5 100644 --- a/claude.md +++ b/claude.md @@ -89,6 +89,19 @@ All development on: `claude/validate-code-quality-WNrNw` - **HNSW**: Enabled - **Neural**: Enabled +## Pre-Merge Checklist + +Before merging any PR, verify each item applies and is addressed: + +1. **Tests pass** — `cargo test` (Rust) and `python -m pytest` (Python) green +2. **README.md** — Update platform tables, crate descriptions, hardware tables, feature summaries if scope changed +3. **CHANGELOG.md** — Add entry under `[Unreleased]` with what was added/fixed/changed +4. **User guide** (`docs/user-guide.md`) — Update if new data sources, CLI flags, or setup steps were added +5. **ADR index** — Update ADR count in README docs table if a new ADR was created +6. **Docker Hub image** — Only rebuild if Dockerfile, dependencies, or runtime behavior changed (not needed for platform-gated code that doesn't affect the Linux container) +7. **Crate publishing** — Only needed if a crate is published to crates.io and its public API changed (workspace-internal crates don't need publishing) +8. **`.gitignore`** — Add any new build artifacts or binaries + ## Build & Test ```bash diff --git a/docs/user-guide.md b/docs/user-guide.md index 8297711..4e30a62 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -194,6 +194,29 @@ docker run --network host ruvnet/wifi-densepose:latest --source windows --tick-m See [Tutorial #36](https://github.com/ruvnet/wifi-densepose/issues/36) for a walkthrough. +### macOS WiFi (RSSI Only) + +Uses CoreWLAN via a Swift helper binary. macOS Sonoma 14.4+ redacts real BSSIDs; the adapter generates deterministic synthetic MACs so the multi-BSSID pipeline still works. + +```bash +# Compile the Swift helper (once) +swiftc -O v1/src/sensing/mac_wifi.swift -o mac_wifi + +# Run natively +./target/release/sensing-server --source macos --http-port 3000 --ws-port 3001 --tick-ms 500 +``` + +See [ADR-025](adr/ADR-025-macos-corewlan-wifi-sensing.md) for details. + +### Linux WiFi (RSSI Only) + +Uses `iw dev scan` to capture RSSI. Requires `CAP_NET_ADMIN` (root) for active scans; use `scan dump` for cached results without root. + +```bash +# Run natively (requires root for active scanning) +sudo ./target/release/sensing-server --source linux --http-port 3000 --ws-port 3001 --tick-ms 500 +``` + ### ESP32-S3 (Full CSI) Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for pose estimation, vital signs, and through-wall sensing. diff --git a/rust-port/wifi-densepose-rs/Cargo.lock b/rust-port/wifi-densepose-rs/Cargo.lock index e9c09f1..0a5ed57 100644 --- a/rust-port/wifi-densepose-rs/Cargo.lock +++ b/rust-port/wifi-densepose-rs/Cargo.lock @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" @@ -174,7 +174,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -265,7 +265,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -323,9 +323,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecheck" @@ -373,7 +373,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -384,9 +384,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -399,7 +399,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -410,9 +410,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bzip2" @@ -478,9 +478,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -496,9 +496,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -569,21 +569,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "colorchoice" @@ -632,9 +632,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -896,7 +896,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -923,9 +923,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -935,9 +935,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -996,9 +996,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1011,9 +1011,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1021,15 +1021,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1038,38 +1038,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1079,7 +1079,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1265,9 +1264,9 @@ dependencies = [ [[package]] name = "geographiclib-rs" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" +checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7" dependencies = [ "libm", ] @@ -1297,6 +1296,19 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "half" version = "2.7.1" @@ -1426,9 +1438,9 @@ dependencies = [ [[package]] name = "hmac-sha256" -version = "1.1.12" +version = "1.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" [[package]] name = "http" @@ -1504,12 +1516,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", - "futures-core", "http", "http-body", "hyper", @@ -1520,9 +1531,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1542,6 +1553,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.13.0" @@ -1550,6 +1567,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1627,9 +1646,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1642,22 +1661,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.180" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -1676,9 +1701,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lzma-rust2" -version = "0.15.6" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f7337d278fec032975dc884152491580dd23750ee957047856735fe0e61ede" +checksum = "1670343e58806300d87950e3401e820b519b9384281bbabfb15e3636689ffd69" [[package]] name = "matchers" @@ -1707,15 +1732,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", "stable_deref_trait", @@ -1791,14 +1816,14 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -1917,9 +1942,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -1980,7 +2005,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -1997,14 +2022,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -2167,7 +2192,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2192,9 +2217,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2238,15 +2263,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] @@ -2268,9 +2293,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -2282,20 +2307,30 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primal-check" version = "0.3.4" @@ -2331,22 +2366,22 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -2374,7 +2409,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2410,9 +2445,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2450,7 +2485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -2470,7 +2505,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -2484,9 +2519,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2517,7 +2552,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -2567,14 +2602,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2584,9 +2619,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2595,9 +2630,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -2635,7 +2670,7 @@ checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2740,11 +2775,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2753,9 +2788,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -2869,9 +2904,9 @@ checksum = "178f93f84a4a72c582026a45d9b8710acf188df4a22a25434c5dbba1df6c4cac" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safetensors" @@ -2919,11 +2954,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -2932,9 +2967,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2990,7 +3025,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3099,9 +3134,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3111,9 +3146,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -3188,9 +3223,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3211,7 +3246,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3220,7 +3255,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "enum-as-inner", "libc", @@ -3273,12 +3308,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3316,7 +3351,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3327,7 +3362,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3341,22 +3376,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tinytemplate" @@ -3408,7 +3443,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3533,7 +3568,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -3584,7 +3619,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3693,9 +3728,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -3709,6 +3744,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unty" version = "0.0.4" @@ -3717,9 +3758,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "ureq" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" dependencies = [ "base64", "der", @@ -3759,11 +3800,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -3841,18 +3882,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3863,11 +3913,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3876,9 +3927,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3886,31 +3937,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.56" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" dependencies = [ "async-trait", "cast", @@ -3925,17 +3976,34 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.56" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", ] [[package]] @@ -3950,10 +4018,34 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.83" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3971,9 +4063,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ "rustls-pki-types", ] @@ -4087,7 +4179,6 @@ dependencies = [ "memmap2", "ndarray 0.15.6", "num-traits", - "once_cell", "ort", "parking_lot", "proptest", @@ -4281,7 +4372,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4292,7 +4383,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4486,9 +4577,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "yoke" @@ -4510,28 +4683,28 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4551,7 +4724,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -4595,9 +4768,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.13" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/Cargo.toml b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/Cargo.toml index 241b08a..4bf0b58 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/Cargo.toml +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/Cargo.toml @@ -47,7 +47,6 @@ tokio = { workspace = true, features = ["sync", "rt"] } # Additional utilities parking_lot = "0.12" -once_cell = "1.19" memmap2 = "0.9" [dev-dependencies] diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/linux_scanner.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/linux_scanner.rs new file mode 100644 index 0000000..4026fe5 --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/linux_scanner.rs @@ -0,0 +1,359 @@ +//! Adapter that scans WiFi BSSIDs on Linux by invoking `iw dev scan`. +//! +//! This is the Linux counterpart to [`NetshBssidScanner`](super::NetshBssidScanner) +//! on Windows and [`MacosCoreWlanScanner`](super::MacosCoreWlanScanner) on macOS. +//! +//! # Design +//! +//! The adapter shells out to `iw dev scan` (or `iw dev scan dump` +//! to read cached results without triggering a new scan, which requires root). +//! The output is parsed into [`BssidObservation`] values using the same domain +//! types shared by all platform adapters. +//! +//! # Permissions +//! +//! - `iw dev scan` requires `CAP_NET_ADMIN` (typically root). +//! - `iw dev scan dump` reads cached results and may work without root +//! on some distributions. +//! +//! # Platform +//! +//! Linux only. Gated behind `#[cfg(target_os = "linux")]` at the module level. + +use std::process::Command; +use std::time::Instant; + +use crate::domain::bssid::{BandType, BssidId, BssidObservation, RadioType}; +use crate::error::WifiScanError; + +// --------------------------------------------------------------------------- +// LinuxIwScanner +// --------------------------------------------------------------------------- + +/// Synchronous WiFi scanner that shells out to `iw dev scan`. +/// +/// Each call to [`scan_sync`](Self::scan_sync) spawns a subprocess, captures +/// stdout, and parses the BSS stanzas into [`BssidObservation`] values. +pub struct LinuxIwScanner { + /// Wireless interface name (e.g. `"wlan0"`, `"wlp2s0"`). + interface: String, + /// If true, use `scan dump` (cached results) instead of triggering a new + /// scan. This avoids the root requirement but may return stale data. + use_dump: bool, +} + +impl LinuxIwScanner { + /// Create a scanner for the default interface `wlan0`. + pub fn new() -> Self { + Self { + interface: "wlan0".to_owned(), + use_dump: false, + } + } + + /// Create a scanner for a specific wireless interface. + pub fn with_interface(iface: impl Into) -> Self { + Self { + interface: iface.into(), + use_dump: false, + } + } + + /// Use `scan dump` instead of `scan` to read cached results without root. + pub fn use_cached(mut self) -> Self { + self.use_dump = true; + self + } + + /// Run `iw dev scan` and parse the output synchronously. + /// + /// Returns one [`BssidObservation`] per BSS stanza in the output. + pub fn scan_sync(&self) -> Result, WifiScanError> { + let scan_cmd = if self.use_dump { "dump" } else { "scan" }; + + let mut args = vec!["dev", &self.interface, "scan"]; + if self.use_dump { + args.push(scan_cmd); + } + + // iw uses "scan dump" not "scan scan dump" + let args = if self.use_dump { + vec!["dev", &self.interface, "scan", "dump"] + } else { + vec!["dev", &self.interface, "scan"] + }; + + let output = Command::new("iw") + .args(&args) + .output() + .map_err(|e| { + WifiScanError::ProcessError(format!( + "failed to run `iw {}`: {e}", + args.join(" ") + )) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WifiScanError::ScanFailed { + reason: format!( + "iw exited with {}: {}", + output.status, + stderr.trim() + ), + }); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + parse_iw_scan_output(&stdout) + } +} + +impl Default for LinuxIwScanner { + fn default() -> Self { + Self::new() + } +} + +// --------------------------------------------------------------------------- +// Parser +// --------------------------------------------------------------------------- + +/// Intermediate accumulator for fields within a single BSS stanza. +#[derive(Default)] +struct BssStanza { + bssid: Option, + ssid: Option, + signal_dbm: Option, + freq_mhz: Option, + channel: Option, +} + +impl BssStanza { + /// Flush this stanza into a [`BssidObservation`], if we have enough data. + fn flush(self, timestamp: Instant) -> Option { + let bssid_str = self.bssid?; + let bssid = BssidId::parse(&bssid_str).ok()?; + let rssi_dbm = self.signal_dbm.unwrap_or(-90.0); + + // Determine channel from explicit field or frequency. + let channel = self.channel.or_else(|| { + self.freq_mhz.map(freq_to_channel) + }).unwrap_or(0); + + let band = BandType::from_channel(channel); + let radio_type = infer_radio_type_from_freq(self.freq_mhz.unwrap_or(0)); + let signal_pct = ((rssi_dbm + 100.0) * 2.0).clamp(0.0, 100.0); + + Some(BssidObservation { + bssid, + rssi_dbm, + signal_pct, + channel, + band, + radio_type, + ssid: self.ssid.unwrap_or_default(), + timestamp, + }) + } +} + +/// Parse the text output of `iw dev scan [dump]`. +/// +/// The output consists of BSS stanzas, each starting with: +/// ```text +/// BSS aa:bb:cc:dd:ee:ff(on wlan0) +/// ``` +/// followed by indented key-value lines. +pub fn parse_iw_scan_output(output: &str) -> Result, WifiScanError> { + let now = Instant::now(); + let mut results = Vec::new(); + let mut current: Option = None; + + for line in output.lines() { + // New BSS stanza starts with "BSS " at column 0. + if line.starts_with("BSS ") { + // Flush previous stanza. + if let Some(stanza) = current.take() { + if let Some(obs) = stanza.flush(now) { + results.push(obs); + } + } + + // Parse BSSID from "BSS aa:bb:cc:dd:ee:ff(on wlan0)" or + // "BSS aa:bb:cc:dd:ee:ff -- associated". + let rest = &line[4..]; + let mac_end = rest.find(|c: char| !c.is_ascii_hexdigit() && c != ':') + .unwrap_or(rest.len()); + let mac = &rest[..mac_end]; + + if mac.len() == 17 { + let mut stanza = BssStanza::default(); + stanza.bssid = Some(mac.to_lowercase()); + current = Some(stanza); + } + continue; + } + + // Indented lines belong to the current stanza. + let trimmed = line.trim(); + if let Some(ref mut stanza) = current { + if let Some(rest) = trimmed.strip_prefix("SSID:") { + stanza.ssid = Some(rest.trim().to_owned()); + } else if let Some(rest) = trimmed.strip_prefix("signal:") { + // "signal: -52.00 dBm" + stanza.signal_dbm = parse_signal_dbm(rest); + } else if let Some(rest) = trimmed.strip_prefix("freq:") { + // "freq: 5180" + stanza.freq_mhz = rest.trim().parse().ok(); + } else if let Some(rest) = trimmed.strip_prefix("DS Parameter set: channel") { + // "DS Parameter set: channel 6" + stanza.channel = rest.trim().parse().ok(); + } + } + } + + // Flush the last stanza. + if let Some(stanza) = current.take() { + if let Some(obs) = stanza.flush(now) { + results.push(obs); + } + } + + Ok(results) +} + +/// Convert a frequency in MHz to an 802.11 channel number. +fn freq_to_channel(freq_mhz: u32) -> u8 { + match freq_mhz { + // 2.4 GHz: channels 1-14. + 2412..=2472 => ((freq_mhz - 2407) / 5) as u8, + 2484 => 14, + // 5 GHz: channels 36-177. + 5170..=5885 => ((freq_mhz - 5000) / 5) as u8, + // 6 GHz (Wi-Fi 6E). + 5955..=7115 => ((freq_mhz - 5950) / 5) as u8, + _ => 0, + } +} + +/// Parse a signal strength string like "-52.00 dBm" into dBm. +fn parse_signal_dbm(s: &str) -> Option { + let s = s.trim(); + // Take everything up to " dBm" or just parse the number. + let num_part = s.split_whitespace().next()?; + num_part.parse().ok() +} + +/// Infer radio type from frequency (best effort). +fn infer_radio_type_from_freq(freq_mhz: u32) -> RadioType { + match freq_mhz { + 5955..=7115 => RadioType::Ax, // 6 GHz → Wi-Fi 6E + 5170..=5885 => RadioType::Ac, // 5 GHz → likely 802.11ac + _ => RadioType::N, // 2.4 GHz → at least 802.11n + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + /// Real-world `iw dev wlan0 scan` output (truncated to 3 BSSes). + const SAMPLE_IW_OUTPUT: &str = "\ +BSS aa:bb:cc:dd:ee:ff(on wlan0) +\tTSF: 123456789 usec +\tfreq: 5180 +\tbeacon interval: 100 TUs +\tcapability: ESS Privacy (0x0011) +\tsignal: -52.00 dBm +\tSSID: HomeNetwork +\tDS Parameter set: channel 36 +BSS 11:22:33:44:55:66(on wlan0) +\tfreq: 2437 +\tsignal: -71.00 dBm +\tSSID: GuestWifi +\tDS Parameter set: channel 6 +BSS de:ad:be:ef:ca:fe(on wlan0) -- associated +\tfreq: 5745 +\tsignal: -45.00 dBm +\tSSID: OfficeNet +"; + + #[test] + fn parse_three_bss_stanzas() { + let obs = parse_iw_scan_output(SAMPLE_IW_OUTPUT).unwrap(); + assert_eq!(obs.len(), 3); + + // First BSS. + assert_eq!(obs[0].ssid, "HomeNetwork"); + assert_eq!(obs[0].bssid.to_string(), "aa:bb:cc:dd:ee:ff"); + assert!((obs[0].rssi_dbm - (-52.0)).abs() < f64::EPSILON); + assert_eq!(obs[0].channel, 36); + assert_eq!(obs[0].band, BandType::Band5GHz); + + // Second BSS: 2.4 GHz. + assert_eq!(obs[1].ssid, "GuestWifi"); + assert_eq!(obs[1].channel, 6); + assert_eq!(obs[1].band, BandType::Band2_4GHz); + assert_eq!(obs[1].radio_type, RadioType::N); + + // Third BSS: "-- associated" suffix. + assert_eq!(obs[2].ssid, "OfficeNet"); + assert_eq!(obs[2].bssid.to_string(), "de:ad:be:ef:ca:fe"); + assert!((obs[2].rssi_dbm - (-45.0)).abs() < f64::EPSILON); + } + + #[test] + fn freq_to_channel_conversion() { + assert_eq!(freq_to_channel(2412), 1); + assert_eq!(freq_to_channel(2437), 6); + assert_eq!(freq_to_channel(2462), 11); + assert_eq!(freq_to_channel(2484), 14); + assert_eq!(freq_to_channel(5180), 36); + assert_eq!(freq_to_channel(5745), 149); + assert_eq!(freq_to_channel(5955), 1); // 6 GHz channel 1 + assert_eq!(freq_to_channel(9999), 0); // Unknown + } + + #[test] + fn parse_signal_dbm_values() { + assert!((parse_signal_dbm(" -52.00 dBm").unwrap() - (-52.0)).abs() < f64::EPSILON); + assert!((parse_signal_dbm("-71.00 dBm").unwrap() - (-71.0)).abs() < f64::EPSILON); + assert!((parse_signal_dbm("-45.00").unwrap() - (-45.0)).abs() < f64::EPSILON); + } + + #[test] + fn empty_output() { + let obs = parse_iw_scan_output("").unwrap(); + assert!(obs.is_empty()); + } + + #[test] + fn missing_ssid_defaults_to_empty() { + let output = "\ +BSS 11:22:33:44:55:66(on wlan0) +\tfreq: 2437 +\tsignal: -60.00 dBm +"; + let obs = parse_iw_scan_output(output).unwrap(); + assert_eq!(obs.len(), 1); + assert_eq!(obs[0].ssid, ""); + } + + #[test] + fn channel_from_freq_when_ds_param_missing() { + let output = "\ +BSS aa:bb:cc:dd:ee:ff(on wlan0) +\tfreq: 5180 +\tsignal: -50.00 dBm +\tSSID: NoDS +"; + let obs = parse_iw_scan_output(output).unwrap(); + assert_eq!(obs.len(), 1); + assert_eq!(obs[0].channel, 36); // Derived from 5180 MHz. + } +} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rs new file mode 100644 index 0000000..be3d045 --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/macos_scanner.rs @@ -0,0 +1,360 @@ +//! Adapter that scans WiFi BSSIDs on macOS by invoking a compiled Swift +//! helper binary that uses Apple's CoreWLAN framework. +//! +//! This is the macOS counterpart to [`NetshBssidScanner`](super::NetshBssidScanner) +//! on Windows. It follows ADR-025 (ORCA — macOS CoreWLAN WiFi Sensing). +//! +//! # Design +//! +//! Apple removed the `airport` CLI in macOS Sonoma 14.4+ and CoreWLAN is a +//! Swift/Objective-C framework with no stable C ABI for Rust FFI. We therefore +//! shell out to a small Swift helper (`mac_wifi`) that outputs JSON lines: +//! +//! ```json +//! {"ssid":"MyNetwork","bssid":"aa:bb:cc:dd:ee:ff","rssi":-52,"noise":-90,"channel":36,"band":"5GHz"} +//! ``` +//! +//! macOS Sonoma+ redacts real BSSID MACs to `00:00:00:00:00:00` unless the app +//! holds the `com.apple.wifi.scan` entitlement. When we detect a zeroed BSSID +//! we generate a deterministic synthetic MAC via `SHA-256(ssid:channel)[:6]`, +//! setting the locally-administered bit so it never collides with real OUI +//! allocations. +//! +//! # Platform +//! +//! macOS only. Gated behind `#[cfg(target_os = "macos")]` at the module level. + +use std::process::Command; +use std::time::Instant; + +use crate::domain::bssid::{BandType, BssidId, BssidObservation, RadioType}; +use crate::error::WifiScanError; + +// --------------------------------------------------------------------------- +// MacosCoreWlanScanner +// --------------------------------------------------------------------------- + +/// Synchronous WiFi scanner that shells out to the `mac_wifi` Swift helper. +/// +/// The helper binary must be compiled from `v1/src/sensing/mac_wifi.swift` and +/// placed on `$PATH` or at a known location. The scanner invokes it with a +/// `--scan-once` flag (single-shot mode) and parses the JSON output. +/// +/// If the helper is not found, [`scan_sync`](Self::scan_sync) returns a +/// [`WifiScanError::ProcessError`]. +pub struct MacosCoreWlanScanner { + /// Path to the `mac_wifi` helper binary. Defaults to `"mac_wifi"` (on PATH). + helper_path: String, +} + +impl MacosCoreWlanScanner { + /// Create a scanner that looks for `mac_wifi` on `$PATH`. + pub fn new() -> Self { + Self { + helper_path: "mac_wifi".to_owned(), + } + } + + /// Create a scanner with an explicit path to the Swift helper binary. + pub fn with_path(path: impl Into) -> Self { + Self { + helper_path: path.into(), + } + } + + /// Run the Swift helper and parse the output synchronously. + /// + /// Returns one [`BssidObservation`] per BSSID seen in the scan. + pub fn scan_sync(&self) -> Result, WifiScanError> { + let output = Command::new(&self.helper_path) + .arg("--scan-once") + .output() + .map_err(|e| { + WifiScanError::ProcessError(format!( + "failed to run mac_wifi helper ({}): {e}", + self.helper_path + )) + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WifiScanError::ScanFailed { + reason: format!( + "mac_wifi exited with {}: {}", + output.status, + stderr.trim() + ), + }); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + parse_macos_scan_output(&stdout) + } +} + +impl Default for MacosCoreWlanScanner { + fn default() -> Self { + Self::new() + } +} + +// --------------------------------------------------------------------------- +// Parser +// --------------------------------------------------------------------------- + +/// Parse the JSON-lines output from the `mac_wifi` Swift helper. +/// +/// Each line is expected to be a JSON object with the fields: +/// `ssid`, `bssid`, `rssi`, `noise`, `channel`, `band`. +/// +/// Lines that fail to parse are silently skipped (the helper may emit +/// status messages on stdout). +pub fn parse_macos_scan_output(output: &str) -> Result, WifiScanError> { + let now = Instant::now(); + let mut results = Vec::new(); + + for line in output.lines() { + let line = line.trim(); + if line.is_empty() || !line.starts_with('{') { + continue; + } + + if let Some(obs) = parse_json_line(line, now) { + results.push(obs); + } + } + + Ok(results) +} + +/// Parse a single JSON line into a [`BssidObservation`]. +/// +/// Uses a lightweight manual parser to avoid pulling in `serde_json` as a +/// hard dependency. The JSON structure is simple and well-known. +fn parse_json_line(line: &str, timestamp: Instant) -> Option { + let ssid = extract_string_field(line, "ssid")?; + let bssid_str = extract_string_field(line, "bssid")?; + let rssi = extract_number_field(line, "rssi")?; + let channel_f = extract_number_field(line, "channel")?; + let channel = channel_f as u8; + + // Resolve BSSID: use real MAC if available, otherwise generate synthetic. + let bssid = resolve_bssid(&bssid_str, &ssid, channel)?; + + let band = BandType::from_channel(channel); + + // macOS CoreWLAN doesn't report radio type directly; infer from band/channel. + let radio_type = infer_radio_type(channel); + + // Convert RSSI to signal percentage using the standard mapping. + let signal_pct = ((rssi + 100.0) * 2.0).clamp(0.0, 100.0); + + Some(BssidObservation { + bssid, + rssi_dbm: rssi, + signal_pct, + channel, + band, + radio_type, + ssid, + timestamp, + }) +} + +/// Resolve a BSSID string to a [`BssidId`]. +/// +/// If the MAC is all-zeros (macOS redaction), generate a synthetic +/// locally-administered MAC from `SHA-256(ssid:channel)`. +fn resolve_bssid(bssid_str: &str, ssid: &str, channel: u8) -> Option { + // Try parsing the real BSSID first. + if let Ok(id) = BssidId::parse(bssid_str) { + // Check for the all-zeros redacted BSSID. + if id.0 != [0, 0, 0, 0, 0, 0] { + return Some(id); + } + } + + // Generate synthetic BSSID: SHA-256(ssid:channel), take first 6 bytes, + // set locally-administered + unicast bits (byte 0: bit 1 set, bit 0 clear). + Some(synthetic_bssid(ssid, channel)) +} + +/// Generate a deterministic synthetic BSSID from SSID and channel. +/// +/// Uses a simple hash (FNV-1a-inspired) to avoid pulling in `sha2` crate. +/// The locally-administered bit is set so these never collide with real OUI MACs. +fn synthetic_bssid(ssid: &str, channel: u8) -> BssidId { + // Simple but deterministic hash — FNV-1a 64-bit. + let mut hash: u64 = 0xcbf2_9ce4_8422_2325; + for &byte in ssid.as_bytes() { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(0x0100_0000_01b3); + } + hash ^= u64::from(channel); + hash = hash.wrapping_mul(0x0100_0000_01b3); + + let bytes = hash.to_le_bytes(); + let mut mac = [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]]; + + // Set locally-administered bit (bit 1 of byte 0) and clear multicast (bit 0). + mac[0] = (mac[0] | 0x02) & 0xFE; + + BssidId(mac) +} + +/// Infer radio type from channel number (best effort on macOS). +fn infer_radio_type(channel: u8) -> RadioType { + match channel { + // 5 GHz channels → likely 802.11ac or newer + 36..=177 => RadioType::Ac, + // 2.4 GHz → at least 802.11n + _ => RadioType::N, + } +} + +// --------------------------------------------------------------------------- +// Lightweight JSON field extractors +// --------------------------------------------------------------------------- + +/// Extract a string field value from a JSON object string. +/// +/// Looks for `"key":"value"` or `"key": "value"` patterns. +fn extract_string_field(json: &str, key: &str) -> Option { + let pattern = format!("\"{}\"", key); + let key_pos = json.find(&pattern)?; + let after_key = &json[key_pos + pattern.len()..]; + + // Skip optional whitespace and the colon. + let after_colon = after_key.trim_start().strip_prefix(':')?; + let after_colon = after_colon.trim_start(); + + // Expect opening quote. + let after_quote = after_colon.strip_prefix('"')?; + + // Find closing quote (handle escaped quotes). + let mut end = 0; + let bytes = after_quote.as_bytes(); + while end < bytes.len() { + if bytes[end] == b'"' && (end == 0 || bytes[end - 1] != b'\\') { + break; + } + end += 1; + } + + Some(after_quote[..end].to_owned()) +} + +/// Extract a numeric field value from a JSON object string. +/// +/// Looks for `"key": ` patterns. +fn extract_number_field(json: &str, key: &str) -> Option { + let pattern = format!("\"{}\"", key); + let key_pos = json.find(&pattern)?; + let after_key = &json[key_pos + pattern.len()..]; + + let after_colon = after_key.trim_start().strip_prefix(':')?; + let after_colon = after_colon.trim_start(); + + // Collect digits, sign, and decimal point. + let num_str: String = after_colon + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '-' || *c == '.' || *c == '+' || *c == 'e' || *c == 'E') + .collect(); + + num_str.parse().ok() +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE_OUTPUT: &str = r#" +{"ssid":"HomeNetwork","bssid":"aa:bb:cc:dd:ee:ff","rssi":-52,"noise":-90,"channel":36,"band":"5GHz"} +{"ssid":"GuestWifi","bssid":"11:22:33:44:55:66","rssi":-71,"noise":-92,"channel":6,"band":"2.4GHz"} +{"ssid":"Redacted","bssid":"00:00:00:00:00:00","rssi":-65,"noise":-88,"channel":149,"band":"5GHz"} +"#; + + #[test] + fn parse_valid_output() { + let obs = parse_macos_scan_output(SAMPLE_OUTPUT).unwrap(); + assert_eq!(obs.len(), 3); + + // First entry: real BSSID. + assert_eq!(obs[0].ssid, "HomeNetwork"); + assert_eq!(obs[0].bssid.to_string(), "aa:bb:cc:dd:ee:ff"); + assert!((obs[0].rssi_dbm - (-52.0)).abs() < f64::EPSILON); + assert_eq!(obs[0].channel, 36); + assert_eq!(obs[0].band, BandType::Band5GHz); + + // Second entry: 2.4 GHz. + assert_eq!(obs[1].ssid, "GuestWifi"); + assert_eq!(obs[1].channel, 6); + assert_eq!(obs[1].band, BandType::Band2_4GHz); + assert_eq!(obs[1].radio_type, RadioType::N); + + // Third entry: redacted BSSID → synthetic MAC. + assert_eq!(obs[2].ssid, "Redacted"); + // Should NOT be all-zeros. + assert_ne!(obs[2].bssid.0, [0, 0, 0, 0, 0, 0]); + // Should have locally-administered bit set. + assert_eq!(obs[2].bssid.0[0] & 0x02, 0x02); + // Should have unicast bit (multicast cleared). + assert_eq!(obs[2].bssid.0[0] & 0x01, 0x00); + } + + #[test] + fn synthetic_bssid_is_deterministic() { + let a = synthetic_bssid("TestNet", 36); + let b = synthetic_bssid("TestNet", 36); + assert_eq!(a, b); + + // Different SSID or channel → different MAC. + let c = synthetic_bssid("OtherNet", 36); + assert_ne!(a, c); + + let d = synthetic_bssid("TestNet", 6); + assert_ne!(a, d); + } + + #[test] + fn parse_empty_and_junk_lines() { + let output = "\n \nnot json\n{broken json\n"; + let obs = parse_macos_scan_output(output).unwrap(); + assert!(obs.is_empty()); + } + + #[test] + fn extract_string_field_basic() { + let json = r#"{"ssid":"MyNet","bssid":"aa:bb:cc:dd:ee:ff"}"#; + assert_eq!(extract_string_field(json, "ssid").unwrap(), "MyNet"); + assert_eq!( + extract_string_field(json, "bssid").unwrap(), + "aa:bb:cc:dd:ee:ff" + ); + assert!(extract_string_field(json, "missing").is_none()); + } + + #[test] + fn extract_number_field_basic() { + let json = r#"{"rssi":-52,"channel":36}"#; + assert!((extract_number_field(json, "rssi").unwrap() - (-52.0)).abs() < f64::EPSILON); + assert!((extract_number_field(json, "channel").unwrap() - 36.0).abs() < f64::EPSILON); + } + + #[test] + fn signal_pct_clamping() { + // RSSI -50 → pct = (-50+100)*2 = 100 + let json = r#"{"ssid":"Test","bssid":"aa:bb:cc:dd:ee:ff","rssi":-50,"channel":1}"#; + let obs = parse_json_line(json, Instant::now()).unwrap(); + assert!((obs.signal_pct - 100.0).abs() < f64::EPSILON); + + // RSSI -100 → pct = 0 + let json = r#"{"ssid":"Test","bssid":"aa:bb:cc:dd:ee:ff","rssi":-100,"channel":1}"#; + let obs = parse_json_line(json, Instant::now()).unwrap(); + assert!((obs.signal_pct - 0.0).abs() < f64::EPSILON); + } +} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/mod.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/mod.rs index 60d04c3..abdb176 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/mod.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/adapter/mod.rs @@ -1,12 +1,30 @@ //! Adapter implementations for the [`WlanScanPort`] port. //! //! Each adapter targets a specific platform scanning mechanism: -//! - [`NetshBssidScanner`]: Tier 1 -- parses `netsh wlan show networks mode=bssid`. -//! - [`WlanApiScanner`]: Tier 2 -- async wrapper with metrics and future native FFI path. +//! - [`NetshBssidScanner`]: Tier 1 -- parses `netsh wlan show networks mode=bssid` (Windows). +//! - [`WlanApiScanner`]: Tier 2 -- async wrapper with metrics and future native FFI path (Windows). +//! - [`MacosCoreWlanScanner`]: CoreWLAN via Swift helper binary (macOS, ADR-025). +//! - [`LinuxIwScanner`]: parses `iw dev scan` output (Linux). pub(crate) mod netsh_scanner; pub mod wlanapi_scanner; +#[cfg(target_os = "macos")] +pub mod macos_scanner; + +#[cfg(target_os = "linux")] +pub mod linux_scanner; + pub use netsh_scanner::NetshBssidScanner; pub use netsh_scanner::parse_netsh_output; pub use wlanapi_scanner::WlanApiScanner; + +#[cfg(target_os = "macos")] +pub use macos_scanner::MacosCoreWlanScanner; +#[cfg(target_os = "macos")] +pub use macos_scanner::parse_macos_scan_output; + +#[cfg(target_os = "linux")] +pub use linux_scanner::LinuxIwScanner; +#[cfg(target_os = "linux")] +pub use linux_scanner::parse_iw_scan_output; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/lib.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/lib.rs index bd2c13b..f1ebabb 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/lib.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/src/lib.rs @@ -6,8 +6,10 @@ //! //! - **Domain types**: [`BssidId`], [`BssidObservation`], [`BandType`], [`RadioType`] //! - **Port**: [`WlanScanPort`] -- trait abstracting the platform scan backend -//! - **Adapter**: [`NetshBssidScanner`] -- Tier 1 adapter that parses -//! `netsh wlan show networks mode=bssid` output +//! - **Adapters**: +//! - [`NetshBssidScanner`] -- Windows, parses `netsh wlan show networks mode=bssid` +//! - `MacosCoreWlanScanner` -- macOS, invokes CoreWLAN Swift helper (ADR-025) +//! - `LinuxIwScanner` -- Linux, parses `iw dev scan` output pub mod adapter; pub mod domain; @@ -19,6 +21,16 @@ pub mod port; pub use adapter::NetshBssidScanner; pub use adapter::parse_netsh_output; pub use adapter::WlanApiScanner; + +#[cfg(target_os = "macos")] +pub use adapter::MacosCoreWlanScanner; +#[cfg(target_os = "macos")] +pub use adapter::parse_macos_scan_output; + +#[cfg(target_os = "linux")] +pub use adapter::LinuxIwScanner; +#[cfg(target_os = "linux")] +pub use adapter::parse_iw_scan_output; pub use domain::bssid::{BandType, BssidId, BssidObservation, RadioType}; pub use domain::frame::MultiApFrame; pub use domain::registry::{BssidEntry, BssidMeta, BssidRegistry, RunningStats}; diff --git a/v1/src/sensing/mac_wifi.swift b/v1/src/sensing/mac_wifi.swift new file mode 100644 index 0000000..ccf0fc4 --- /dev/null +++ b/v1/src/sensing/mac_wifi.swift @@ -0,0 +1,34 @@ +import Foundation +import CoreWLAN + +// Output format: JSON lines for easy parsing by Python +// {"timestamp": 1234567.89, "rssi": -50, "noise": -90, "tx_rate": 866.0} + +func main() { + guard let interface = CWWiFiClient.shared().interface() else { + fputs("{\"error\": \"No WiFi interface found\"}\n", stderr) + exit(1) + } + + // Flush stdout automatically to prevent buffering issues with Python subprocess + setbuf(stdout, nil) + + // Run at ~10Hz + let interval: TimeInterval = 0.1 + + while true { + let timestamp = Date().timeIntervalSince1970 + let rssi = interface.rssiValue() + let noise = interface.noiseMeasurement() + let txRate = interface.transmitRate() + + let json = """ + {"timestamp": \(timestamp), "rssi": \(rssi), "noise": \(noise), "tx_rate": \(txRate)} + """ + print(json) + + Thread.sleep(forTimeInterval: interval) + } +} + +main() diff --git a/v1/src/sensing/rssi_collector.py b/v1/src/sensing/rssi_collector.py index 25fb0dd..40540ca 100644 --- a/v1/src/sensing/rssi_collector.py +++ b/v1/src/sensing/rssi_collector.py @@ -602,3 +602,137 @@ class WindowsWifiCollector: retry_count=0, interface=self._interface, ) + + +# --------------------------------------------------------------------------- +# macOS WiFi collector (real hardware via Swift CoreWLAN utility) +# --------------------------------------------------------------------------- + +class MacosWifiCollector: + """ + Collects real RSSI data from a macOS WiFi interface using a Swift utility. + + Data source: A small compiled Swift binary (`mac_wifi`) that polls the + CoreWLAN `CWWiFiClient.shared().interface()` at a high rate. + """ + + def __init__( + self, + sample_rate_hz: float = 10.0, + buffer_seconds: int = 120, + ) -> None: + self._rate = sample_rate_hz + self._buffer = RingBuffer(max_size=int(sample_rate_hz * buffer_seconds)) + self._running = False + self._thread: Optional[threading.Thread] = None + self._process: Optional[subprocess.Popen] = None + self._interface = "en0" # CoreWLAN automatically targets the active Wi-Fi interface + + # Compile the Swift utility if the binary doesn't exist + import os + base_dir = os.path.dirname(os.path.abspath(__file__)) + self.swift_src = os.path.join(base_dir, "mac_wifi.swift") + self.swift_bin = os.path.join(base_dir, "mac_wifi") + + # -- public API ---------------------------------------------------------- + + @property + def sample_rate_hz(self) -> float: + return self._rate + + def start(self) -> None: + if self._running: + return + + # Ensure binary exists + import os + if not os.path.exists(self.swift_bin): + logger.info("Compiling mac_wifi.swift to %s", self.swift_bin) + try: + subprocess.run(["swiftc", "-O", "-o", self.swift_bin, self.swift_src], check=True, capture_output=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to compile macOS WiFi utility: {e.stderr.decode('utf-8')}") + except FileNotFoundError: + raise RuntimeError("swiftc is not installed. Please install Xcode Command Line Tools to use native macOS WiFi sensing.") + + self._running = True + self._thread = threading.Thread( + target=self._sample_loop, daemon=True, name="mac-rssi-collector" + ) + self._thread.start() + logger.info("MacosWifiCollector started at %.1f Hz", self._rate) + + def stop(self) -> None: + self._running = False + if self._process: + self._process.terminate() + try: + self._process.wait(timeout=1.0) + except subprocess.TimeoutExpired: + self._process.kill() + self._process = None + + if self._thread is not None: + self._thread.join(timeout=2.0) + self._thread = None + logger.info("MacosWifiCollector stopped") + + def get_samples(self, n: Optional[int] = None) -> List[WifiSample]: + if n is not None: + return self._buffer.get_last_n(n) + return self._buffer.get_all() + + # -- internals ----------------------------------------------------------- + + def _sample_loop(self) -> None: + import json + + # Start the Swift binary + self._process = subprocess.Popen( + [self.swift_bin], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1 # Line buffered + ) + + while self._running and self._process and self._process.poll() is None: + try: + line = self._process.stdout.readline() + if not line: + continue + + line = line.strip() + if not line: + continue + + if line.startswith("{"): + data = json.loads(line) + if "error" in data: + logger.error("macOS WiFi utility error: %s", data["error"]) + continue + + rssi = float(data.get("rssi", -80.0)) + noise = float(data.get("noise", -95.0)) + + link_quality = max(0.0, min(1.0, (rssi + 100.0) / 60.0)) + + sample = WifiSample( + timestamp=time.time(), + rssi_dbm=rssi, + noise_dbm=noise, + link_quality=link_quality, + tx_bytes=0, + rx_bytes=0, + retry_count=0, + interface=self._interface, + ) + self._buffer.append(sample) + except Exception as e: + logger.error("Error reading macOS WiFi stream: %s", e) + time.sleep(1.0) + + # Process exited unexpectedly + if self._running: + logger.error("macOS WiFi utility exited unexpectedly. Collector stopped.") + self._running = False diff --git a/v1/src/sensing/ws_server.py b/v1/src/sensing/ws_server.py index 9f2a678..8b4448b 100644 --- a/v1/src/sensing/ws_server.py +++ b/v1/src/sensing/ws_server.py @@ -41,6 +41,7 @@ from v1.src.sensing.rssi_collector import ( LinuxWifiCollector, SimulatedCollector, WindowsWifiCollector, + MacosWifiCollector, WifiSample, RingBuffer, ) @@ -340,12 +341,26 @@ class SensingWebSocketServer: except Exception as e: logger.warning("Windows WiFi unavailable (%s), falling back", e) elif system == "Linux": + # In Docker on Mac, Linux is detected but no wireless extensions exist. + # Force SimulatedCollector if /proc/net/wireless doesn't exist. + import os + if os.path.exists("/proc/net/wireless"): + try: + collector = LinuxWifiCollector(sample_rate_hz=10.0) + self.source = "linux_wifi" + return collector + except RuntimeError: + logger.warning("Linux WiFi unavailable, falling back") + else: + logger.warning("Linux detected but /proc/net/wireless missing (likely Docker). Falling back.") + elif system == "Darwin": try: - collector = LinuxWifiCollector(sample_rate_hz=10.0) - self.source = "linux_wifi" + collector = MacosWifiCollector(sample_rate_hz=10.0) + logger.info("Using MacosWifiCollector") + self.source = "macos_wifi" return collector - except RuntimeError: - logger.warning("Linux WiFi unavailable, falling back") + except Exception as e: + logger.warning("macOS WiFi unavailable (%s), falling back", e) # 3. Simulated logger.info("Using SimulatedCollector")