mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
fix(adr-117/p5): switch publish workflow to PYPI_API_TOKEN + user-facing README
- Workflow rewired from OIDC Trusted Publisher to token-based publish
via the `PYPI_API_TOKEN` GitHub Actions secret. Both publish jobs
(v2 wheels + tombstone) pass `password: ${{ secrets.PYPI_API_TOKEN }}`
to `pypa/gh-action-pypi-publish@release/v1`. Workflow comments now
document the GCP → GH secret-refresh command.
- Removed `permissions: id-token: write` and the OIDC `environment:`
blocks (no longer needed without OIDC).
- Token was sourced from the GCP Secret Manager entry `PYPI_TOKEN`
in project `cognitum-20260110` and pushed to GH Actions via
`gcloud secrets versions access | gh secret set` so the value
never appeared in a shell variable or this session's output.
- Rewrote `python/README.md` from a developer phase-ledger into a
user-facing PyPI front page: one-paragraph elevator pitch, bullet
list of features, three short usage snippets (vitals extract,
WS subscribe, MQTT semantic-primitive listener, BFLD numpy
bridge), hardware table, links. The README is the FIRST thing
pip users see at https://pypi.org/p/wifi-densepose so it has to
introduce the project, not the build plan.
Wheel rebuilds clean at 253 KB (was 238 KB — +15 KB from the richer
README baked into the wheel metadata). Test suite unchanged at 183/183.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -13,15 +13,17 @@
|
||||
# 1. cut tag `v1.99.0-pip` → publishes the tombstone wheel first
|
||||
# 2. cut tag `v2.0.0-pip` → publishes the PyO3 v2 wheel matrix
|
||||
#
|
||||
# Both publish via PyPI Trusted Publisher (OIDC). No API tokens in
|
||||
# secrets — see https://docs.pypi.org/trusted-publishers/ for how to
|
||||
# register this workflow with PyPI before the first publish.
|
||||
# Publishes via PyPI API token stored in the `PYPI_API_TOKEN`
|
||||
# GitHub Actions secret. The token value comes from the GCP Secret
|
||||
# Manager entry `PYPI_TOKEN` in project `cognitum-20260110`; refresh
|
||||
# with:
|
||||
# gcloud secrets versions access latest --secret=PYPI_TOKEN \
|
||||
# --project=cognitum-20260110 \
|
||||
# | gh secret set PYPI_API_TOKEN --repo ruvnet/RuView
|
||||
#
|
||||
# Q3 (witness hash v2 — open in ADR-117 §11.3) MUST be resolved before
|
||||
# the first v2.0.0 publish. The `verify-witness` job below currently
|
||||
# only checks the v1 hash for backwards-compatibility against the
|
||||
# legacy archive/v1 sample. When v2 lands, add a parallel step that
|
||||
# verifies the v2 hash against the Rust pipeline.
|
||||
# Q3 (witness hash v2 — open in ADR-117 §11.3) MUST be resolved
|
||||
# before the first v2.0.0 publish. When v2 lands, add a parallel
|
||||
# step that verifies the v2 hash against the Rust pipeline.
|
||||
|
||||
name: pip-release
|
||||
|
||||
@@ -47,9 +49,7 @@ on:
|
||||
tags:
|
||||
- "v*-pip"
|
||||
|
||||
# Required for PyPI Trusted Publisher (OIDC).
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
@@ -193,12 +193,6 @@ jobs:
|
||||
startsWith(github.ref, 'refs/tags/v2.')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
# PyPI Trusted Publisher (OIDC) — register the workflow + repo
|
||||
# under https://pypi.org/manage/account/publishing/ before the
|
||||
# first publish. No API tokens in GH secrets.
|
||||
environment:
|
||||
name: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_to || 'pypi' }}
|
||||
url: https://pypi.org/p/wifi-densepose
|
||||
steps:
|
||||
- name: Gather all artifacts into dist/
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -214,9 +208,8 @@ jobs:
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist
|
||||
# Don't fail on existing — useful when re-running a dispatch
|
||||
# after fixing the workflow but before bumping the version.
|
||||
skip-existing: true
|
||||
- name: Publish to PyPI
|
||||
if: |
|
||||
@@ -224,6 +217,7 @@ jobs:
|
||||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist
|
||||
|
||||
publish-tombstone:
|
||||
@@ -237,9 +231,6 @@ jobs:
|
||||
startsWith(github.ref, 'refs/tags/v1.99')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_to || 'pypi' }}
|
||||
url: https://pypi.org/p/wifi-densepose
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -250,6 +241,7 @@ jobs:
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist
|
||||
skip-existing: true
|
||||
- name: Publish to PyPI
|
||||
@@ -258,4 +250,5 @@ jobs:
|
||||
(github.event_name == 'workflow_dispatch' && inputs.publish_to == 'pypi')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages-dir: dist
|
||||
|
||||
+125
-79
@@ -1,97 +1,143 @@
|
||||
# `wifi-densepose` v2.x — PyO3 bindings for the Rust core
|
||||
# wifi-densepose
|
||||
|
||||
This directory contains the source for the `wifi-densepose` PyPI wheel
|
||||
(v2.0+). It's a PyO3 + maturin build that wraps the Rust crates in
|
||||
[`v2/crates/`](../v2/crates/) and replaces the legacy pure-Python
|
||||
`wifi-densepose==1.1.0` (released 2025-06-07).
|
||||
[](https://pypi.org/project/wifi-densepose/)
|
||||
[](https://pypi.org/project/wifi-densepose/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
See [ADR-117](../docs/adr/ADR-117-pip-wifi-densepose-modernization.md)
|
||||
for the full modernization plan.
|
||||
**Detect human presence, count people, read breathing and heart rate, and
|
||||
estimate skeletal pose — using only the WiFi signal already in your home.**
|
||||
|
||||
## Build locally
|
||||
No cameras. No wearables. Works through walls and in the dark.
|
||||
|
||||
`wifi-densepose` is the Python binding for the [RuView](https://github.com/ruvnet/RuView)
|
||||
sensing stack: a Rust core that turns the Channel State Information (CSI)
|
||||
emitted by ordinary WiFi chips into ambient-intelligence signals. The wheel
|
||||
ships compiled DSP for fast offline analysis, plus an opt-in Python client
|
||||
for talking to a live RuView sensing-server over WebSocket or MQTT.
|
||||
|
||||
## Features
|
||||
|
||||
- **17-keypoint pose** — full-body skeletal estimate from WiFi CSI, no camera
|
||||
- **Vital signs** — respiratory rate (6–30 BPM) and heart rate (40–120 BPM)
|
||||
with a confidence score and clinical-grade / degraded / unreliable status
|
||||
- **Presence, person count, fall detection, motion** — fused outputs from
|
||||
the same CSI stream
|
||||
- **10 semantic primitives** (HA-MIND) — someone-sleeping, possible-distress,
|
||||
room-active, bathroom-occupied, fall-risk-elevated, bed-exit, … — ready
|
||||
to wire into Home Assistant or Apple Home automations
|
||||
- **Beamforming Feedback (BFLD) support** — 802.11ac/ax/be compressed feedback
|
||||
matrices on top of the receiver-side CSI path
|
||||
- **GIL-releasing DSP** — extract loops run with the GIL released, so a
|
||||
tokio-backed web server can call into the pipeline without stalling its
|
||||
event loop
|
||||
- **Tiny wheel** — ~240 KB compiled (one binary per OS/arch covers Python
|
||||
3.10+ via the stable ABI)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Install maturin + dev deps
|
||||
pip install maturin pytest
|
||||
|
||||
# Develop-install — builds the Rust extension in-place
|
||||
cd python
|
||||
maturin develop
|
||||
|
||||
# Run the smoke tests
|
||||
pytest tests/
|
||||
pip install wifi-densepose # core DSP only
|
||||
pip install "wifi-densepose[client]" # + WebSocket/MQTT clients
|
||||
```
|
||||
|
||||
The `maturin develop` command produces a debug-build wheel installed
|
||||
into your current Python environment. For release builds:
|
||||
Wheels are published for Linux (x86_64, aarch64), macOS (x86_64, arm64), and
|
||||
Windows (amd64).
|
||||
|
||||
```bash
|
||||
maturin build --release --strip
|
||||
## Usage
|
||||
|
||||
### Extract breathing rate from a CSI stream
|
||||
|
||||
```python
|
||||
from wifi_densepose import BreathingExtractor
|
||||
|
||||
br = BreathingExtractor.esp32_default() # 56 subcarriers @ 100 Hz, 30s window
|
||||
|
||||
for residuals, weights in your_csi_source: # one frame at a time
|
||||
est = br.extract(residuals=residuals, weights=weights)
|
||||
if est is not None:
|
||||
print(f"{est.value_bpm:.1f} BPM (confidence={est.confidence:.2f})")
|
||||
```
|
||||
|
||||
The wheel lands under `python/target/wheels/`.
|
||||
Heart rate is the same shape — `HeartRateExtractor.esp32_default()` with a
|
||||
0.8–2.0 Hz band-pass and a 15-second window.
|
||||
|
||||
## Layout
|
||||
### Subscribe to a live sensing-server
|
||||
|
||||
```
|
||||
python/
|
||||
├── Cargo.toml # PyO3 + abi3-py310 + Rust deps
|
||||
├── pyproject.toml # maturin backend + Python metadata
|
||||
├── README.md # this file
|
||||
├── src/
|
||||
│ └── lib.rs # #[pymodule] — Rust binding glue
|
||||
├── wifi_densepose/ # pure-Python facade (the user-facing API)
|
||||
│ ├── __init__.py # re-exports compiled module symbols
|
||||
│ └── py.typed # PEP 561 typed-package marker
|
||||
└── tests/
|
||||
└── test_smoke.py # P1 acceptance tests
|
||||
```python
|
||||
import asyncio
|
||||
from wifi_densepose.client import SensingClient, EdgeVitalsMessage
|
||||
|
||||
async def main():
|
||||
async with SensingClient("ws://your-ruview-node:8765/ws/sensing") as c:
|
||||
async for msg in c.stream():
|
||||
if isinstance(msg, EdgeVitalsMessage):
|
||||
print(msg.presence, msg.breathing_rate_bpm, msg.heartrate_bpm)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Phase status (per ADR-117 §6)
|
||||
### React to Home Assistant semantic primitives
|
||||
|
||||
- ✅ **P1 — Scaffold**: module loads, version constant exposed,
|
||||
6 smoke tests pass via `maturin develop`.
|
||||
- ✅ **P2 — Core type bindings**: `Keypoint`, `KeypointType`,
|
||||
`BoundingBox`, `PersonPose`, `PoseEstimate`. 51 additional tests.
|
||||
- ✅ **P3 — Vitals + signal DSP**: `VitalStatus`, `VitalEstimate`,
|
||||
`VitalReading`, `BreathingExtractor`, `HeartRateExtractor` with
|
||||
`py.allow_threads` GIL release on hot loops (Q5 tokio audit on
|
||||
2026-05-24 confirmed core/vitals/signal are pure-sync). 17 tests.
|
||||
- ✅ **P3.5 — BFLD bindings (stub Rust)**: `BfldKind`, `BfldFrame`,
|
||||
`BfldReport` — forward-compatible Python surface for 802.11ac/ax/be
|
||||
Beamforming Feedback Loop Data. numpy Complex64 bridge. 19 tests.
|
||||
Real Rust ingestion lands post-v2.0 in a `wifi-densepose-bfld`
|
||||
crate (see ADR-117 §11.11/12); the Python API does not change.
|
||||
- ✅ **P4 — WS/MQTT client**: pure-Python `wifi_densepose.client` extra
|
||||
(no Rust). `SensingClient` (asyncio websockets), `RuViewMqttClient`
|
||||
(paho-mqtt v2 with VERSION2 callbacks), `HABlueprintHelper` (HA
|
||||
discovery payload parser), `SemanticPrimitiveListener` (typed router
|
||||
for the 10 HA-MIND primitives from ADR-115 §3.12). 63 tests including
|
||||
end-to-end against an in-process `websockets.serve` fixture.
|
||||
- ⏳ **P5 — cibuildwheel + PyPI publish (workflow shipped)**: GH Actions
|
||||
workflow `.github/workflows/pip-release.yml` ships the 5-wheel
|
||||
matrix (manylinux x86_64+aarch64, macosx x86_64+arm64, win amd64)
|
||||
plus sdist via `cibuildwheel@2.21`. Publish via PyPI Trusted
|
||||
Publisher (OIDC) on `v2.X.Y-pip` tags or manual dispatch.
|
||||
**One-time PyPI Trusted Publisher registration required before the
|
||||
first publish can fire.** Q3 (witness hash v2 — ADR-117 §11.3)
|
||||
remains the hard gate before tagging.
|
||||
- ✅ **P-tomb — v1.99.0 tombstone wheel**: pure-Python wheel
|
||||
(`python/tombstone/`) whose `wifi_densepose/__init__.py` raises
|
||||
ImportError with the migration URL on import. Verified locally
|
||||
(2.7 KB wheel) — `pip install wifi_densepose-1.99.0-py3-none-any.whl`
|
||||
then `python -c "import wifi_densepose"` raises ImportError as
|
||||
expected. Same `pip-release.yml` workflow publishes the tombstone
|
||||
on `v1.99.0-pip` tag. Per ADR-117 §7.3, publish the tombstone
|
||||
BEFORE the first v2.0.0 publish to claim the "current" slot in
|
||||
pip's resolver.
|
||||
```python
|
||||
from wifi_densepose.client import (
|
||||
RuViewMqttClient, SemanticPrimitive, SemanticPrimitiveListener,
|
||||
)
|
||||
|
||||
Each phase ends with a checkbox PR. Tests are additive — every phase's
|
||||
smoke tests must still pass after later phases land.
|
||||
listener = SemanticPrimitiveListener()
|
||||
listener.on(SemanticPrimitive.BedExit, lambda e: print("bed exit:", e.node_id))
|
||||
listener.on(SemanticPrimitive.PossibleDistress, lambda e: alert(e))
|
||||
|
||||
## Migrating from v1.x
|
||||
client = RuViewMqttClient(broker_host="homeassistant.local")
|
||||
client.on_message(
|
||||
"homeassistant/+/wifi_densepose_+/+/state",
|
||||
listener.handle_mqtt_message,
|
||||
)
|
||||
client.start()
|
||||
client.wait_connected()
|
||||
```
|
||||
|
||||
The v1 line was a separate pure-Python implementation. v2 is a hard
|
||||
break (semver-justified by 11.5 months of stack drift). Migration
|
||||
guide ships in [docs/migrations/wifi-densepose-1-to-2.md](../docs/migrations/wifi-densepose-1-to-2.md)
|
||||
(landing in P5).
|
||||
### Decode 802.11ax beamforming feedback
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
from wifi_densepose import BfldFrame, BfldKind
|
||||
|
||||
# Parse compressed BFR from a Wireshark capture into a Complex64 ndarray ...
|
||||
fb = np.zeros((2, 1, 996), dtype=np.complex64) # Nr=2 Nc=1 Nsc=996 for HE80
|
||||
|
||||
frame = BfldFrame.from_compressed_feedback(
|
||||
timestamp_ms=ts,
|
||||
sounding_index=seq,
|
||||
sta_mac="aa:bb:cc:dd:ee:ff",
|
||||
kind=BfldKind.CompressedHE80,
|
||||
feedback_matrix=fb,
|
||||
)
|
||||
print(frame.n_subcarriers, frame.mean_amplitude)
|
||||
```
|
||||
|
||||
## Hardware
|
||||
|
||||
Works with any WiFi chip that exposes CSI. Reference setups (ESP-IDF firmware,
|
||||
build scripts, witness-verified test bundles) are in the
|
||||
[RuView repo](https://github.com/ruvnet/RuView):
|
||||
|
||||
| Device | Cost | Role |
|
||||
|---|---|---|
|
||||
| ESP32-S3 (8MB flash) | ~$9 | WiFi CSI sensing node |
|
||||
| ESP32-S3 SuperMini (4MB) | ~$6 | WiFi CSI (compact) |
|
||||
| ESP32-C6 + Seeed MR60BHA2 | ~$15 | mmWave HR/BR/presence add-on |
|
||||
|
||||
The legacy v1 line (Wi-Pose-style FastAPI server) is end-of-life;
|
||||
`wifi-densepose==1.99.0` is a tombstone that raises `ImportError` pointing
|
||||
to v2 with a migration URL.
|
||||
|
||||
## Links
|
||||
|
||||
- **Repository** — https://github.com/ruvnet/RuView
|
||||
- **Modernization plan** — [ADR-117](https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-117-pip-wifi-densepose-modernization.md)
|
||||
- **Home Assistant integration** — [ADR-115](https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-115-home-assistant-integration.md)
|
||||
- **Issues** — https://github.com/ruvnet/RuView/issues
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
Reference in New Issue
Block a user