首页
/ RuView ADR-117 实践指南:用 PyO3 + maturin 把 pip `wifi-densepose` 从纯 Python 服务重构为 Rust 核心绑定(PIP-PHOENIX)

RuView ADR-117 实践指南:用 PyO3 + maturin 把 pip `wifi-densepose` 从纯 Python 服务重构为 Rust 核心绑定(PIP-PHOENIX)

2026-09-07 10:47:53作者:牧宁李

本文基于 RuView 仓库中的架构决策记录 ADR-117 展开:项目曾把 wifi-densepose 以纯 Python 服务的形式发布到 PyPI,而生产能力早已迁移到 Rust 工作区(v2/),形成"PyPI 包误导新用户、生态两套分裂"的窘境。ADR-117 提出代号 PIP-PHOENIX 的方案——采用 PyO3 + maturin 绑定三个高价值 Rust crate,发布带稳定 ABI 的编译型 wheel,并配一层纯 Python 的 WS/MQTT 客户端。读完本文,你将掌握:该方案被选中而另外三种方案被否决的完整理由、首版 wheel 的绑定范围与包布局、abi3-py310 多平台构建矩阵、见证链(witness chain)如何重新挂到 Rust 管线之上,以及 P1→P6+ 的分阶段迁移清单和 1.x 老用户的兼容性处理策略。

1. 背景:这个 pip 包今天是什么,为什么必须改

wifi-densepose v1.1.0 于 2025-06-07 发布到 PyPI(同日先后发布 1.0.0 与 1.1.0 两个版本),wheel 标签是 py3-none-any——即没有任何编译扩展、没有任何平台相关代码的纯 Python 服务器应用,源码全部来自 archive/v1/

从当前仓库的 archive/v1/setup.py 可以核实关键事实:

  • 安装时会拉起一套约 40 个硬依赖的栈:FastAPI、PyTorch、SQLAlchemy、Redis、Celery、OpenCV、asyncpg、psycopg2、Scapy 等(见 setup.py 中默认 requirements 列表);
  • 声明的两个控制台入口均为 src.cli:cli
    wifi-densepose = src.cli:cli
    wdp             = src.cli:cli
    
  • python_requires=">=3.9",classifier 覆盖 Python 3.9/3.10/3.11/3.12,状态为 Beta;
  • find_packages(include=["src", "src.*"]) 说明 wheel 以 src.* 作为可导入命名空间;
  • archive/v1/src/init.py 导出的是 FastAPI appCSIProcessorPhaseSanitizerPoseEstimatorRouterInterfaceServiceOrchestratorHealthCheckServiceMetricsService 等服务器应用概念。

换句话说,v1 包是一个面向 postgres + redis 的 FastAPI 服务器,并不是可被脚本化调用的

1.1 为什么现在必须处理(现状证据表)

ADR-115(2026-05-23 合入)已从 Rust crate wifi-densepose-sensing-server 交付了 21 个 Home Assistant 实体、10 个语义原语、mTLS、隐私模式与完整见证 bundle;ADR-116 正在把它打包为 Cognitum Seed cog。但这两条能力面在 pip install wifi-densepose全都不可达——pip 包既不能 import 一个 CsiFrame、无法解码 edge-vitals 报文、无法调用任何 DSP 阶段、无法校验 witness bundle,也无法订阅 sensing-server 的 MQTT/WebSocket 端点。

ADR 给出的现状证据表:

Artifact 来源
最新 PyPI 版本 1.1.0 pypi.org/pypi/wifi-densepose/json
首次发布日期 2025-06-07T13:24:53Z PyPI JSON 元数据
最近一次发布日期 2025-06-07T17:02:40Z PyPI JSON 元数据
距最近发布已过去 约 11.5 个月(截至 2026-05-24) PyPI JSON 元数据
Wheel 标签 py3-none-any PyPI simple index
硬依赖数量 40(torch、fastapi、sqlalchemy、redis、celery…) setup.py
Python 要求 >=3.9 setup.py
当前 Rust 工作区版本 0.3.0 v2/Cargo.toml
Rust crate 数量 20+ v2/Cargo.toml members
ADR-115 交付时间 2026-05-23(PR #778)

三个具体的客户痛点被写进决策依据:

  1. Python 用户 pip install wifi-densepose 期望消费实时 pose/vitals 数据,得到的却是一个依赖 postgres + redis 的 FastAPI 服务器,不是可写脚本的库;
  2. 编写 HA 自动化或 Node-RED 流程的集成方,缺少面向 v0.7 遥测面(ADR-115 实体、语义原语)的惯用 Python API;
  3. ADR-028 见证链本是 Python 实现、经 archive/v1/data/proof/verify.py 验证,但它只 import v1 栈——无法见证如今作为生产实现的 Rust 管线

1.2 这个 ADR 不是什么

ADR 明确划清四条边界:

  • 不是删除 archive/v1/:v1 保留为研究归档,其 proof bundle 继续留在 archive/v1/data/proof/
  • 不是把 Rust crate 移植成 Python:Rust 工作区(v2/)是权威实现,本 ADR 不改动它;
  • 不是替换 wifi-densepose-sensing-server 二进制:pip 包只是包装或作为其客户端,不重新实现它;
  • 不与 ADR-116 重叠:ADR-116 交付 Seed 可安装产物;ADR-117 交付面向 Python 开发者、用于对 Rust 栈进行脚本化/自动化/原型验证的库。

1.3 Gap 分析:v1 与 Rust 栈的能力差

能力 Rust crate pip v1.1.0 状态 严重度
CsiFrame / CsiMetadata 核心类型 wifi-densepose-coretypes.rs 缺失——v1 用 CSIData Python 类 Critical
从 CSI buffer 提取 HR/BR wifi-densepose-vitals(4 阶段:preprocessor → breathing → heartrate → anomaly) Python stub 无 DSP Critical
相位净化/去噪 wifi-densepose-signalphase_sanitizercsi_processorhampel Python stub Critical
动作检测 + presence 打分 wifi-densepose-signalmotion.rsMotionDetector 缺失 Critical
RuvSense 多站感知(13 模块) wifi-densepose-signal/src/ruvsense/ 缺失(ADR-029 晚于 v1) Critical
17 关键点姿态估计 wifi-densepose-nnwifi-densepose-mat 需要模型权重的 stub PoseEstimator High
MQTT 发布(21 个 HA 实体) wifi-densepose-sensing-server/src/mqtt/ 缺失(ADR-115 晚于 v1) High
语义原语(10 种) wifi-densepose-sensing-server/src/semantic/ 缺失 High
Matter bridge wifi-densepose-sensing-server/src/matter/ 缺失 High
sensing-server 的 WS/REST 客户端 wifi-densepose-sensing-server(Axum) v1 只有独立 FastAPI 服务,无客户端 High
Witness bundle 验证 ADR-028 / scripts/generate-witness-bundle.sh verify.py 只证明 v1 管线 High
ESP32-C6 固件遥测(ADR-110) wifi-densepose-hardware + wifi-densepose-sensing-server 缺失 Medium
跨视角融合(RuVector) wifi-densepose-ruvector/src/viewpoint/ 缺失 Medium
语义原语 MQTT 载荷 wifi-densepose-sensing-server/src/semantic/bus.rs 缺失 Medium
PostgreSQL + Redis 服务器模式 archive/v1/ 存在(仅 v1) Low(非 SOTA)
FastAPI HTTP REST 服务器 archive/v1/src/app.py 存在(仅 v1) Low(非 SOTA)

2. 决策:PyO3 + maturin,方案代号 PIP-PHOENIX

ADR-117 采纳 PyO3 + maturin Python 扩展绑定作为主要现代化路径:把 pip 包发布为平台原生 wheelmanylinuxmacosxwin-amd64),内含编译好的 Rust 扩展模块,同时附一层与运行中的 wifi-densepose-sensing-server 通信的纯 Python WS/MQTT 客户端。

2.1 为什么赢过另外三种被否决的方案

判据 PyO3 + maturin(入选) Subprocess 包装 仅 REST/WS 客户端 纯 Python 重实现
DSP 性能 原生 Rust 速度、零拷贝 每次调用都有 IPC 开销 N/A——无本地 DSP Python 瓶颈
wheel 内二进制体积 仅 core + vitals + signal:约 2 MB(strip 后) 完整 sensing-server 二进制:约 15–30 MB 极小(约 50 kB) 极小(约 100 kB)
离线 / 无服务器可用 是(捆绑二进制) 否——必须有服务器 部分
Proof bundle 能否覆盖 Rust 管线 能——绑定与服务器调用同一份 Rust 代码 部分——服务器是黑盒
安装体验 pip install wifi-densepose——wheel 无系统依赖 pip install 下载 25 MB 二进制 pip install——纯 Python pip install——纯 Python
维护面 Python 绑定 + Rust 工作区 Python 薄垫片 Python 客户端 Python 重实现必须追着 Rust 跑
Async / tokio PyO3 0.28 用 pyo3-asynciopyo3-async-runtimes 导出 async;DSP 热路径用同步入口 N/A 客户端原生 asyncio N/A
GIL 关切 DSP 重调用经 py.allow_threads 释放 GIL;按模块管理 tokio runtime N/A N/A
契合现有架构 core + vitals + signal 已有干净 public API(lib.rs re-export) 要求 sensing-server 在跑 要求 sensing-server 分叉领域模型

三句话记住淘汰理由:

  • Subprocess 包装被否:把约 25 MB 的预编译服务器二进制塞进每个 pip wheel,安装过于沉重,且不启动服务器就无法离线写脚本;
  • REST/WS client only 被否:离线时没有任何 DSP 价值,且无法弥合 witness 缺口——proof bundle 必须执行同一份管线代码;
  • 纯 Python 重实现被否(明确拒绝):这正是当前 11 个月漂移的根源,任何 Python 重实现都会在 Rust 栈演进时立刻再次漂移。

方案起点刻意做小:只绑定对 Python 最有用处的三个 cratewifi-densepose-corewifi-densepose-vitalswifi-densepose-signal),把 py3-none-any 的纯 Python WS/MQTT 客户端作为独立子模块发布,再逐步扩展。

3. 详细设计

3.1 首版 wheel(v2.0)绑定的 Rust crate

三个 crate 均无重型系统依赖(不带 libtorch、不带 ONNX Runtime),在 lib.rs 有稳定 pub re-export,且直击三个最被要求的缺失能力:

Crate 导出的 Python 类型/函数 绑定理由
wifi-densepose-core CsiFrameCsiMetadataKeypointKeypointTypePersonPosePoseEstimateConfidenceBoundingBox 其余 crate 共享的基石类型;没有它们用户连一帧都描述不了
wifi-densepose-vitals CsiVitalPreprocessorBreathingExtractorHeartRateExtractorVitalAnomalyDetectorVitalSignStoreVitalReadingVitalEstimateAnomalyAlert 被问得最多的面:4 行 Python 从 CSI buffer 拿 HR/BR
wifi-densepose-signal CsiProcessorCsiProcessorConfigPhaseSanitizerMotionDetectorMotionScoreFeatureExtractorHardwareNormalizer 产出 vitals/pose 所需特征的那条 DSP 管线

推迟到 P6+wifi-densepose-nn(需要 libtorch 或 candle——wheel 体积风险)、wifi-densepose-mat(依赖 nn)、wifi-densepose-ruvector(RuVector GNN 类型价值高但引入 ruvector-gnn 2.0.5 链接依赖)、wifi-densepose-hardware(ESP32 HAL,不适合 Python 脚本)。

3.2 新的工作区成员与 crate 结构

ADR 设计了一个新的 cdylib crate(位于 v2/crates/wifi-densepose-py/),在单个 maturin 模块 wifi_densepose._core 后面统一 re-export 三个被绑定 crate。其 Cargo.toml 要点如下:

# Cargo.toml(sketch)
[package]
name = "wifi-densepose-py"
version.workspace = true
edition.workspace = true

[lib]
name = "_core"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py310"] }
wifi-densepose-core   = { path = "../wifi-densepose-core", features = ["serde"] }
wifi-densepose-vitals = { path = "../wifi-densepose-vitals" }
wifi-densepose-signal = { path = "../wifi-densepose-signal" }

其中 abi3-py310 锁定了 CPython 3.10+ 的稳定 ABI——同一个二进制 wheel 无需重编译即可覆盖 3.10、3.11、3.12、3.13。这就是后面 wheel 矩阵从 20 个缩到 5 个的关键。

PyO3 绑定模式以 CsiFrame 为例(属性逐一暴露为 getter):

// v2/crates/wifi-densepose-py/src/core_types.rs
use pyo3::prelude::*;
use wifi_densepose_core::CsiFrame as RustCsiFrame;

#[pyclass(name = "CsiFrame")]
#[derive(Clone)]
pub struct PyCsiFrame {
    inner: RustCsiFrame,
}

#[pymethods]
impl PyCsiFrame {
    #[new]
    fn new(amplitudes: Vec<f32>, phases: Vec<f32>, n_subcarriers: usize,
           sample_index: u64, sample_rate_hz: f32) -> Self {
        Self { inner: RustCsiFrame { amplitudes, phases, n_subcarriers,
                                     sample_index, sample_rate_hz } }
    }

    #[getter] fn amplitudes(&self) -> Vec<f32> { self.inner.amplitudes.clone() }
    #[getter] fn phases(&self) -> Vec<f32> { self.inner.phases.clone() }
    #[getter] fn n_subcarriers(&self) -> usize { self.inner.n_subcarriers }
}

执行时间超过 1 ms 的 DSP 调用要释放 GIL(否则 Python 线程会被阻塞在 Rust 侧):

#[pymethods]
impl PyCsiProcessor {
    fn process<'py>(&mut self, py: Python<'py>, frame: &PyCsiFrame)
        -> PyResult<Option<PyProcessedSignal>>
    {
        py.allow_threads(|| self.inner.process(&frame.inner))
            .map(|opt| opt.map(PyProcessedSignal::from))
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))
    }
}

3.3 pip 包布局

wifi-densepose/                  ← PyPI 包名(不变)
  wifi_densepose/                ← 可导入命名空间
    __init__.py                  ← re-export 核心类型 + 版本
    _core.pyd / _core.so         ← 编译的 PyO3 扩展(maturin 构建产物)
    vitals.py                    ← 覆盖 _core vitals 类型的薄 Python 包装 + docstring
    signal.py                    ← 覆盖 _core signal 类型的薄 Python 包装
    client/
      __init__.py
      ws.py                      ← sensing-server /ws/sensing 的 asyncio WebSocket 客户端
      mqtt.py                    ← 订阅 ruview/<node_id>/raw/* 主题的 paho-mqtt 包装
      ha.py                      ← HA-DISCO 载荷辅助(只读,对齐 ADR-115 §3.2)
    witness/
      __init__.py
      verify.py                  ← 可被 Python 调用的 witness 验证器(经 PyO3 绑定
                                     在 Rust 管线上重建 ADR-028 proof,而非 archive/v1/)
    compat/
      v1.py                      ← 抛出 MigrationError 的 import 垫片(见 §7)
    py.typed                     ← PEP 561 标记

导入路径刻意与 Rust crate 名一一对应:

from wifi_densepose import CsiFrame           # core types
from wifi_densepose.vitals import BreathingExtractor, HeartRateExtractor
from wifi_densepose.signal import CsiProcessor, MotionDetector
from wifi_densepose.client.ws import SensingClient
from wifi_densepose.witness import verify_bundle

3.4 PyPI 分发:wheel 矩阵

wifi-densepose==2.0.0 发布,由 cibuildwheel 驱动(建议放独立 workflow,不改动既有 Rust CI):

平台 架构 CPython Tag(稳定 ABI)
manylinux_2_28 x86_64 3.10+ cp310-abi3-manylinux_2_28_x86_64
manylinux_2_28 aarch64 3.10+ cp310-abi3-manylinux_2_28_aarch64
macosx_11_0 x86_64 3.10+ cp310-abi3-macosx_11_0_x86_64
macosx_11_0 arm64 3.10+ cp310-abi3-macosx_11_0_arm64
win amd64 3.10+ cp310-abi3-win_amd64
sdist 源码兜底

abi3-py310每个 OS/arch 一个二进制即可覆盖所有受支持 Python 版本——总共 5 个 wheel + 1 个 sdist,而不用稳定 ABI 则需要 20 个 wheel 的矩阵。

# .github/workflows/pip-release.yml(sketch)
- uses: pypa/cibuildwheel@v2
  with:
    package-dir: v2/crates/wifi-densepose-py
    output-dir: dist
  env:
    CIBW_BUILD: "cp310-*"
    CIBW_ARCHS_LINUX: "x86_64 aarch64"
    CIBW_ARCHS_MACOS: "x86_64 arm64"
    CIBW_ARCHS_WINDOWS: "AMD64"
    CIBW_BEFORE_BUILD: "pip install maturin"
    CIBW_BUILD_FRONTEND: "build[uv]"

3.5 CLI 对等(但不重实现)

wheel 安装同名 wifi-densepose console script,v2 中它是一个薄 Python 垫片:

  1. 检查 wifi-densepose-sensing-server 二进制是否在 PATH 上(由独立的平台二进制分发或 cargo install 安装);
  2. 找到则用 subprocess.runwifi-densepose servewifi-densepose stream 等转发给 Rust 二进制;
  3. 找不到则回退到 PyO3 模块执行离线 DSP 命令(如 wifi-densepose vitals --file recording.jsonl)。

不是对 CLI 的重实现——权威 CLI 仍是 Rust 二进制(wifi-densepose-cli,当前暴露 matversion 子命令),pip 垫片只是发现/便捷层。

3.6 WS/MQTT 客户端层

SensingClient 是包装 sensing-server WebSocket /ws/sensing 的纯 Python asyncio 客户端:

async with SensingClient("ws://localhost:8765/ws/sensing") as client:
    async for msg in client.stream():
        if msg.type == "edge_vitals":
            print(msg.breathing_rate_bpm, msg.heartrate_bpm)

RuViewMqttClient 包装 paho-mqtt,按 ADR-115 §3.2 订阅 ruview/<node_id>/raw/+

两个客户端都是纯 Python(无 PyO3),属于可选依赖:pip install wifi-densepose[client],分别依赖 websockets>=12paho-mqtt>=2

3.7 BFLD:向 v2.0 追加的绑定目标

2026-05-24 依维护者反馈在 P3 实现期间新增。 BFLD(Beamforming Feedback Loop Data,波束成形反馈环数据)是信道发射端 / AP-站点环路视角——802.11ac/ax/be 站点每个 sounding 周期发给 AP 的压缩波束成形反馈帧。从感知视角它与接收端 CSI 互补:

接收端 CSI(现状) BFLD(本次新增)
来源 无线电 RX 侧(如 Pi 5 的 Nexmon CSI、ESP32 promisc cb) 空气中嗅探的 BFR 帧或 mac80211 ACK trace
子载波(HE20) 52(HT-LTF)或 242(HE-LTF) 最高 996(HE160 压缩 BFR)——更密
硬件要求 需打补丁的 Broadcom/Cypress 或专用 ESP32 任意 802.11ac+ 站点-AP 对——无需补丁固件
隐私模型 捕获无线电范围内所有人 相同
仓库内成熟度 生产(ADR-014、ADR-018、ADR-039) 研究;尚无 Rust crate
合适用例 穿墙 pose + vitals 面向 AETHER 类生物特征(ADR-024)与 soul-signature 规范的稠密子载波反射剖面

由于 Rust 工作区还没有 wifi-densepose-bfld crate,P3 交付一个向前兼容的 Python trait 面,未来 Rust crate 可直接插入而无需改动 Python API:

from wifi_densepose import BfldFrame, BfldReport

# 今天(P3):从解析好的 BFR 反馈矩阵构造(自带 parser 路径)。
# Pi 5 + Wireshark BFR dissector 的用户可直接把帧灌进来。
frame = BfldFrame.from_compressed_feedback(
    timestamp_ms=…,
    sounding_index=…,
    sta_mac="aa:bb:cc:…",
    bandwidth_mhz=80,
    n_subcarriers=996,
    feedback_matrix=…,  # numpy ndarray complex64 [Nr × Nc × Nsc]
)

# 明天(post-v2.0):wifi-densepose-bfld Rust crate(TBD,独立 ADR-1xx)
# 提供 Nexmon nl80211 trace + 内核 mac80211 debugfs hook 的摄取,
# pip wheel 在不改 Python 面前提下透明绑定它。

为什么 BFLD 应进 v2.0 而不是等 Rust core

  1. 客户拉动——多位集成方读了 ADR-115 发布说明后就在问 WiFi-6 稠密子载波采集,答案就是 BFLD,API 应在他们搭管线前稳定下来;
  2. soul-signature 依赖——其研究规范把 "Subcarrier Reflection Profile" 列为七种生物特征信道之一,HE20/HE80 下稠密 BFR 子载波正是正确输入;先暴露 BfldFrame 让研究者不必等 Rust 摄取 crate 就能原型验证(见 docs/research/soul/);
  3. 跨厂商可移植——CSI 摄取需要补丁固件,而 BFR 摄取在原生 802.11ac/ax 硬件上即可工作(tcpdump/Wireshark + BFR dissector 捕获)。先发布 Python 数据结构,等于给社区一条从不受支持设备向 RuView 喂数据的路。

P3 的实现面——新增 bindings/bfld.rs(约 150 行、三个 #[pyclass] 类型):

  • BfldFrame(frozen)——一份压缩反馈矩阵快照。构造器:from_compressed_feedback(...)from_uncompressed_v(...)(802.11n V-matrix 形式)。属性:timestamp_mssounding_indexsta_macbandwidth_mhzn_subcarriersn_rows(Nr)、n_cols(Nc)、feedback_matrix(numpy complex64 ndarray);
  • BfldReport(frozen)——一个窗口内多帧 BfldFrame 的聚合器。属性:n_framestimestamp_firsttimestamp_lastmean_amplitude_per_subcarriercoherence_score,给用户一个"这 60 秒扫描里的所有 BFR 数据"的稳定句柄而不泄露存储表示;
  • BfldKind#[pyclass(eq, eq_int, hash, frozen)])——枚举支持的 BFR 变体:CompressedHE20CompressedHE40CompressedHE80CompressedHE160UncompressedHT20UncompressedHT40

在正式 Rust crate 出现之前,stub 实现放 python/src/bfld_stub.rs,刻意不进 v2/crates/;未来由独立的 ADR-1xx 拥有 Rust 摄取 crate。

3.8 Witness 链:重新挂到 Rust 管线

wifi_densepose.witness.verify_bundle(path) 取代 v1 proof 验证,改用经 PyO3 执行 Rust 管线的新链条:

from wifi_densepose.witness import verify_bundle

result = verify_bundle("dist/witness-bundle-ADR028-*/")
assert result.verdict == "PASS", result.detail

内部步骤:

  1. 从 bundle 加载 1,000 帧参考 JSON;
  2. 每帧喂给 PyCsiProcessor(Rust CsiProcessor 的 PyO3 绑定);
  3. 用与 v1 verify.py 相同的 SHA-256 方案对输出取哈希;
  4. expected_features.sha256 中发布的哈希比对。

关键点:v1 的 proof(archive/v1/data/proof/verify.py原样保留,继续证明 v1 管线;新的 witness.py 证明 v2/Rust 管线。两者共存,ADR-028 witness bundle 同时携带二者。

4. 分阶段迁移路径

P1  ──►  P2  ──►  P3  ──►  P4  ──►  P5  ──►  P6+
scaffold  core   vitals+   client   publish  deferred
          types  signal    layer    v2.0.0
  • P1 — Scaffold(1 周):新增 v2/crates/wifi-densepose-py/ 工作区成员;Cargo.tomlcrate-type = ["cdylib"]、pyo3 0.28 + abi3-py310(空模块可编译可导入);根目录 python/pyproject.toml[build-system] requires = ["maturin>=1.8"][tool.maturin] features = ["pyo3/extension-module"];CI 在 ubuntu-latest + Python 3.12 venv 跑 maturin develop 并验证 import wifi_densepose._core;把 wifi-densepose==1.99.0 作为墓碑版发布到 PyPI(见 §7)。
  • P2 — 核心类型绑定(1 周):绑定 core 的 CsiFrameCsiMetadataConfidenceKeypointKeypointTypeBoundingBoxPoseEstimatePersonPose;所有类型在有意义处实现 __repr__/__eq__/__hash__,经 pyo3-serde 或手写 to_dict()/from_dict() 完成 serde JSON round-trip;加 py.typed + 由 pyo3-stub-gen 生成的 .pyitests/test_core.py 覆盖每种类型的构造与 JSON round-trip。
  • P3 — Vitals + signal DSP 绑定(2 周):绑定完整 4 阶段 vitals 管线(CsiVitalPreprocessorBreathingExtractorHeartRateExtractorVitalAnomalyDetectorVitalSignStoreVitalReadingVitalEstimateAnomalyAlert);绑定 signal DSP 入口(CsiProcessorCsiProcessorConfigPhaseSanitizerMotionDetectorHardwareNormalizer);对 bench 中测得 >0.5 ms 的所有调用执行 GIL 释放(py.allow_threads);集成测试把 sample_csi_data.json 的 1,000 帧灌进 PyO3 vitals 管线并断言输出确定性;用 P3 绑定重实现 witness/verify.py 并与 v1 期望哈希比对——注意哈希必然不同(Rust 与 Python 处理器并不逐位一致),因此需生成并发布新的 expected_features_v2.sha256
  • P3.5 — BFLD 绑定面(与 P3 并行)bindings/bfld.rsBfldFrame/BfldReport/BfldKind#[pyclass] 包装(先以 stub Rust 实现背书,等 v3 wifi-densepose-bfld crate);bfld_stub.rs 提供进程内最小存储(压缩反馈矩阵 vec),让 Python API 在摄取 crate 落地前即可用;feedback_matrix(Complex64 ndarray)走与 P3 CsiFrame.amplitude 相同的 numpy bridge;测试覆盖各带宽构造路径(HE20/HE40/HE80/HE160 + HT20/HT40)、n_subcarriers 契约、coherence_score 合理性、BfldKind 可哈希 + 相等;前向兼容契约测试保证今天从 numpy ndarray 构造的 BfldFrame 在未来 Rust crate 存在后能经 (de)serialisation 一致 round-trip。
  • P4 — WS/MQTT 客户端层(1 周):实现 wifi_densepose.client.ws.SensingClient(asyncio,websockets>=12);实现 wifi_densepose.client.mqtt.RuViewMqttClient(paho-mqtt 2.x);加 client.ha 把 ADR-115 MQTT discovery 载荷解析为 Python dataclass 的辅助;集成测试用 Docker 以 --mock-frames 拉起 sensing-server,断言 SensingClient 能收到 edge_vitals 消息。
  • P5 — 首次 cibuildwheel 发布 v2.0.0(1 周):落地 .github/workflows/pip-release.yml 的 cibuildwheel 矩阵(5 wheel + sdist);python_requires = ">=3.10"pyproject.toml 写最小 install_requires(pyo3 是构建期依赖而非运行期;运行时 extras:[client]websockets>=12,paho-mqtt>=2);各 CI 平台 smoke 测试 pip install wifi-densepose==2.0.0;用 Trusted Publisher(OIDC,密钥不存 API token) 发布到 PyPI;公告 1.99.0 墓碑已在 PyPI、搜索结果的 v2.0.0 顶替它。
  • P6+ — 推迟项wifi-densepose-bfld Rust crate(Nexmon BFR pcap + mac80211 debugfs 摄取,替换 P3.5 stub 而不改 Python API,自持 ADR-1xx);wifi-densepose-nn 绑定(libtorch/candle wheel 体积待定);wifi-densepose-ruvector 绑定(RuVector attention 类型);MQTT/Matter 集成辅助(client.matter);对 wifi-densepose==1.x 的弃用公告(PyPI yank);经 pip extra 分发 sensing-server 二进制(pip install wifi-densepose[server]);在 pip 客户端层之上做 HACS Python 集成(ADR-115 后续)。

5. 兼容性与弃用策略

5.1 版本大跳

wifi-densepose==2.0.0硬性 major 破坏:1.x 的 src.* 导入命名空间与 2.x 的 wifi_densepose.* 不兼容,不存在透明桥接两者的垫片。

5.2 墓碑版 v1.99.0

发布 v2.0.0 之前,先发布唯一的职责就是把迁移错误抛给用户的 wifi-densepose==1.99.0(纯 Python sdist/wheel):

# wifi_densepose/__init__.py  (v1.99.0)
raise ImportError(
    "wifi-densepose 1.x has been superseded by v2.0.0 which wraps "
    "the Rust-based stack. Run:\n\n"
    "    pip install wifi-densepose==2.0.0\n\n"
    "Migration guide: 见仓库内 pip-migration 说明\n"
    "Legacy v1 source: archive/v1/ in the repository"
)

仓库中 python/tombstone/src/wifi_densepose/init.py 正是这份墓碑的实现(含指向仓库内 docs 的说明),确保任何把版本钉在 wifi-densepose>=1 的项目升级到 1.99.0 时得到清晰报错而非静默的坏导入。

5.3 PyPI yank 策略(v2.0.0 稳定后,90 天观察窗口)

  • Yank wifi-densepose==1.0.0——它从未有独立稳定期,发布 4 小时后即被取代;
  • wifi-densepose==1.1.0 不 yank 但在描述中标 deprecated;
  • wifi-densepose==1.99.0 作为 1.x 的权威落地页(抛错)。

被 yank 的版本仍可用 pip install wifi-densepose==1.1.0 --force 安装,保证钉住精确版本的可复现构建不被静默破坏。

5.4 Semver 一览

版本 内容
1.0.0 – 1.1.0 遗留 Python 服务器(archive/v1/)
1.99.0 墓碑:ImportError 迁移提示
2.0.0 PyO3 Rust 绑定 + WS/MQTT 客户端
2.x.y 增量绑定 + 客户端改进
3.0.0 若加入 nn 绑定(libtorch wheel 体积可能迫使拆分独立包)

6. 落地的仓库状态(源码佐证)

从当前仓库源码结构看,ADR-117 的骨架已在 python/ 目录落地成型,且实现细节与 ADR 原始 sketch 略有演进:

  • python/Cargo.toml 定义了 crate wifi-densepose-py2.0.0-alpha.1),并注释说明该 crate 刻意位于 v2/ 工作区之外,避免 cargo test --workspace 连带编译 pyo3;[lib] name = "wifi_densepose_native"crate-type = ["cdylib", "rlib"];pyo3 采用 0.22 + abi3-py310(而非设计稿中的 0.28);
  • python/src/lib.rs 是模块注册中心:编译模块名为 _nativemodule-name = "wifi_densepose._native",见 python/pyproject.toml),导出 __rust_version__/__rust_build_tag__/__build_features__hello() smoke 函数;绑定分模块注册——keypoint、pose(对应 P2 core 类型)、vitals(P3)、bfld(P3.5)、privacy_gate(ADR-118 语义),而 aether/meridian/mat 仅在对应 Cargo feature 打开时编译注册(ADR-185 扩展);
  • python/wifi_densepose/init.py 就是 §3.3 所画的 Python facade:__version__ = "2.0.0",从 _native re-export Keypoint/BoundingBox/PersonPose/PoseEstimate(P2)、VitalStatus/VitalEstimate/VitalReading/BreathingExtractor/HeartRateExtractor(P3)、BfldKind/BfldFrame/BfldReport(P3.5),并带上 PEP 561 的 py.typed.pyi 存根;
  • python/src/bindings/vitals.rs 展示了 P3 的 GIL 释放实践:BreathingExtractor(0.1–0.5 Hz 带通 → 呼吸率)与 HeartRateExtractor(0.8–2.0 Hz 带通 + 自相关 → 心率)的 extract 都包在 py.allow_threads(|| ...) 里执行纯同步 DSP,并提供 esp32_default()(56 子载波、100 Hz、30 s/15 s 窗口)等便捷静态构造器;
  • python/wifi_densepose/client/ 已包含 P4 的三个客户端模块(ws/mqtt/ha 以及 ADR-115 语义原语辅助 primitives);python/wifi_densepose/client/ws.py 把 sensing-server 的 connection_established/pose_data/edge_vitals 三类 JSON 消息解码为 frozen dataclass,支持 Authorization: Bearer(token 取 RUVIEW_API_TOKEN 环境变量或构造参数),并对 websockets>=12 内 header kwarg 的改名做了签名自检;
  • python/pyproject.tomlversion = "2.0.0"requires-python = ">=3.10"、maturin 构建后端、strip = true 控制 wheel 体积,并配置了 [client]websockets>=12.0paho-mqtt>=2.1)、[aether]/[meridian]/[mat]/[sota] 等 optional extras;
  • python/tombstone/ 存放 1.99.0 墓碑包源码与测试,对应 §5.2;
  • 测试侧,python/tests/test_smoke.pytest_vitals.pytest_bfld.pytest_client_ws.pytest_client_mqtt.pytest_client_ha.py 等对应 P2–P4 各阶段验收;Rust 侧还保留 golden 与 SHA-256 parity 测试来锁定确定性输出。

需要提醒:ADR 文档状态仍标 Proposed,上述目录呈现的是仓库内正在推进的实现快照;个别字段(pyo3 大版本、模块名 _native vs _core、crate 放置位置)与决策稿不完全一致,均以仓库源码为准。

7. 风险登记

风险 可能性 严重度 缓解
构建矩阵复杂度——5 个目标 triple × cibuildwheel 配置、CI 时间、aarch64 交叉编译需 QEMU abi3-py310(5 个 wheel 而非 20 个);GitHub Actions 自带 QEMU aarch64 模拟;maturin 自动处理 auditwheel
二进制体积——未来 nn/ONNX 绑定可能把 wheel 推到 50 MB 以上 nn 绑定放独立 wifi-densepose-nn PyPI 包;core+vitals+signal wheel 保持精简(约 2 MB stripped)
GIL / async 问题——用 PyO3 包 tokio crate 需要谨慎管理 runtime;所有阻塞 Rust 调用必须用 py.allow_threads 初始绑定只做同步 Rust API(vitals/signal/core 全为 sync);async 的 sensing-server 客户端留在纯 Python 的 client/ws.py
维护者开销——两种语言、两套构建系统、一个 PyPI 包 maturin 统一构建;CI 负责发布;只从 3 个被绑 crate 起步
1.x 用户破坏——钉在 wifi-densepose>=1,<2 的用户会撞上墓碑 1.99.0 墓碑给出清晰报错;v2 后 1.1.0 在 PyPI 上保持 90 天不 yank
Windows Rust 工具链——Windows 上链接 PyO3 需要 MSVC 或 mingw,额外 CI 复杂度 GitHub Actions windows-latest 自带 MSVC;maturin + cibuildwheel 原生处理
稳定 ABI 限制——abi3 会排除部分高级 PyO3 特性(如 Buffer 协议) core/vitals/signal 类型是标量/Vec,P2–P3 不需要 buffer 协议
PyPI 名字归属——项目拥有 wifi-densepose 这个名字(rUv author 字段已确认) 发布前以 pypi.org/user/ruvnet 复核

8. 验收标准(全部通过才视为 Accepted)

  • pip install wifi-densepose==2.0.0 在 Python 3.10/3.11/3.12/3.13 × linux/x86_64、macos/arm64、windows/amd64 的全新 venv 中无需额外构建工具即可成功;
  • python -c "import wifi_densepose; print(wifi_densepose.__version__)" 输出 2.0.0
  • python -c "from wifi_densepose import CsiFrame; f = CsiFrame([1.0]*56, [0.0]*56, 56, 0, 100.0); print(f)" 产生非报错的 repr;
  • 4 阶段 vitals 管线在参考机器(CPython 3.12、linux x86_64、无 GPU)上处理 1,000 帧 耗时低于 500 ms
  • wifi_densepose.witness.verify_bundle(path)scripts/generate-witness-bundle.sh 新生成的 bundle 返回 verdict="PASS"
  • SensingClient 能在 5 秒内从 sensing-server --mock-frames 收到至少一条 edge_vitals 消息;
  • pip install wifi-densepose==1.99.0 抛出带迁移指引的 ImportError
  • 编译出的 _core 扩展除 libc/msvcrt 外无未解析动态库依赖(Linux 用 auditwheel show、macOS 用 delocate-listdeps 验证);
  • 类型存根(wifi_densepose/*.pyi)存在,且 mypy --strict 通过示例代码;
  • core+vitals+signal 每平台 wheel 总大小 ≤ 5 MB

9. 悬而未决的问题(Open Questions)

  1. 稳定 ABI 基线版本abi3-py310 会丢掉 v1.1.0 声明支持的 Python 3.9。3.9 已于 2025-10-05 EOL,是否干净利落地弃用?暂定:是,弃 3.9,用 abi3-py310。
  2. nn 绑定包名:若 wifi-densepose-nn 需要约 30 MB libtorch wheel,应放独立 PyPI 包 wifi-densepose-nn,还是作 wifi-densepose[nn] 可选重 extra?暂定:独立包,避免污染精简 wheel。
  3. Witness 哈希连续性:Rust 管线对同样的输入帧会产出与 v1 Python 管线不同的 SHA-256,新的 expected_features_v2.sha256 必须在 v2.0.0 前生成并提交。谁来生成、生成过程如何被见证?暂定:CI 中生成、哈希提交进 proof 目录并纳入 ADR-028 矩阵。
  4. ruv-neural crate:工作区内已有 v2/crates/ruv-neural/,它是否适合提前做 Python 绑定(利于训练循环脚本化)?暂定:推迟——它依赖训练后端。
  5. Tokio runtime:sensing-server 基于 tokio,但 P2–P3 绑定的三个 crate 都是同步的。是否存在隐藏 tokio 依赖会逼扩展模块引入 runtime?暂定:P1 scaffold 前逐个检查各 crate Cargo.toml 的 tokio 依赖。
  6. pyo3-stub-gen 还是手写存根:对泛型与 newtype 模式自动生成有毛刺。暂定:首版用 pyo3-stub-gen 搭骨架、公开 API 手工精修。
  7. wifi_densepose vs wifi-densepose 命名空间:PyPI 名用连字符、Python 导入用下划线;v1 包走 src.* 而非 wifi_densepose.*,是否有工具硬编码了 src暂定:src.* 是 archive/v1 专属,干净丢弃。
  8. cibuildwheel 版本:现有 GitHub Actions 是 cargo build/build.py 模式,是否需为 maturin 构建更新?暂定:加独立 pip-release.yml,不改既有 Rust CI。
  9. RuVector 绑定时间线wifi-densepose-ruvector 依赖 ruvector-gnn = "2.0.5",它是否以预构建静态库分发、还是构建时链接?这直接影响 P6+ wheel 体积。暂定:承诺时间线前先查 ruvector-gnn 链接策略。
  10. client.ha 与 ADR-115/116 的冲突ha.py 不应在 Python 侧重复 ADR-115 的 MQTT discovery 逻辑。只读(解析 HA discovery JSON → dataclass)还是也写(发布 discovery JSON)?暂定:v2.0 只读;写路径推迟到 HACS 集成后续(ADR-115 §6.A)。
  11. BFLD Rust crate 归属(2026-05-24 新增):P3.5 绑定以 bfld_stub.rs 打底,正式 crate(Nexmon BFR pcap 解析 + mac80211 debugfs 摄取)应作新工作区成员 wifi-densepose-bfld,还是扩展 wifi-densepose-signal暂定:独立新 crate——BFR 解析代码量大(Wireshark dissector 约 2k 行)会撑爆 -signal;且 BFLD 摄取是可选项,独立 crate 让默认 -signal 保持精简。
  12. BFLD 各厂商压缩角变体(2026-05-24 新增):802.11 标准化了压缩反馈格式,但 Broadcom/Intel/Qualcomm/MediaTek 在 psi/phi 量化步长与矩阵条目顺序上有差异。多少归一化应放 Python 绑定、多少放未来 Rust crate?暂定:Python 绑定保持"笨"(numpy 进 numpy 出、不解码);未来 Rust crate 拥有按厂商归一化,经构造器上的 Vendor 枚举暴露。

10. 关联决策与其他参考

ADR-117 不是孤立的架构决策,它串联了 RuView 生态中一串相邻决策记录,理解这些上下文有助于把握其位置:

  • ADR-115 Home Assistant 集成——定义了 client/mqtt.py/client/ha.py 消费的 MQTT 主题结构与 discovery 语义;ADR-117 是让 Python 自动化能访问这套面的通道;
  • ADR-116 HA-COG Seed 打包——Seed 可安装产物,与 ADR-117 面向开发者的库平行而非重叠;
  • ADR-185 Python P6+ SOTA 绑定——AETHER/MERIDIAN/MAT 三个 SOTA 子系统的编译 feature 化绑定,正是 §3.2 中"落地演进"分支的直接后续;
  • ADR-021(ESP32 vitals)、ADR-110(ESP32-C6 固件扩展)——定义了 vitals 提取管线与固件遥测,是 P3 绑定的上游语义;
  • ADR-118 BFLD 检测层——python/src/lib.rsprivacy_gate 绑定(PrivacyClass、身份风险打分)来自 ADR-118
  • ADR-024 对比式 CSI embedding、docs/research/soul/ soul-signature 研究——是 §3.7 BFLD 稠密子载波剖面未来消费方的能力佐证;
  • v1 证据文件:archive/v1/setup.py(40 依赖、双入口、src.* 命名空间)、archive/v1/src/init.py(FastAPI/服务类导出);
  • 落地实现:python/ 目录(详见 §6 列表),其中 tombstone 位于 python/tombstone/

结语

ADR-117 用一句话概括:不再让"Python 生态入口"与"Rust 生产实现"长期分叉。通过 PyO3 + maturin 把 wifi-densepose 重塑为"编译型核心 + 纯 Python 客户端 + 独立 CLI 垫片"的三层结构,用 abi3-py310 把构建矩阵压到 5 个 wheel,用墓碑版 + yank 策略护住 1.x 老用户,并用重挂到 Rust 管线的 witness 链保证"证明的就是生产跑的"。对需要在自己的栈里做同类"纯 Python → Rust 绑定"跃迁的开发者而言,本文梳理的选型判据表、GIL 释放写法、稳定 ABI 取舍、灰度发布顺序与验收清单,都是一份可以直接对照执行的地图。仓库内 python/ 目录已经让这份设计有了可读的骨架实现,值得顺着 python/pyproject.tomlpython/src/lib.rs 继续深读。

登录后查看全文
热门项目推荐
相关项目推荐