RuView(WiFi-DensePose)领域驱动设计聚合根深度解析:CsiFrame、ProcessedSignal、PoseEstimate、Session 与 Device 的 Rust 实现
发布时间:2026/9/10 11:21:28来源:尧图网络
RuViewWiFi-DensePose领域驱动设计聚合根深度解析CsiFrame、ProcessedSignal、PoseEstimate、Session 与 Device 的 Rust 实现【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView本文是 RuView 项目 v2 Rust 重写版WiFi-DensePose领域驱动设计文档 中聚合Aggregates规格说明的技术解读。文章以 v2/docs/ddd/aggregates.md 为骨架逐一剖析系统定义的核心聚合根——CsiFrame、ProcessedSignal、PoseEstimate、Session、Device——并对照 wifi-densepose-core 与 domain-events.md 中的真实实现说明不变式invariant如何在 Rust 类型系统中落地。读完本文你将掌握如何在 RuView 中运用「聚合根 值对象 领域事件 仓储」的战术建模方式把 WiFi CSI 原始信号到人体姿态推断的完整链路组织成事务一致、可持久化、可流式分发的高内聚模块。一、为什么 WiFi-DensePose 需要聚合建模RuView 的核心能力是把普通 WiFi 的Channel State InformationCSI信道状态信息转化为实时空间智能、生命体征监测与人员存在检测。整条链路从 ESP32 / 路由器固件采集原始 CSI 帧开始经过信号清洗、特征提取、神经网络姿态推断最终通过 WebSocket 流式推送给客户端并触发跌倒告警等业务动作。链路长、数据量大、状态多样如果不加约束地让各模块互相直接引用对象很快会产生难以维护的耦合与一致性漏洞。聚合Aggregate是 DDD 中解决这一问题的战术设计单元聚合是一组以单一实体聚合根为入口、作为一个整体保证数据变更一致性的领域对象簇。RuView 的 v2 Rust 实现正是围绕 5 个聚合根组织整个领域模型详见 v2/docs/ddd/domain-model.md 中的上下文划分CsiFrame与ProcessedSignal属于 Signal DomainPoseEstimate属于 Pose Domain核心域Session属于 Streaming DomainDevice属于 Hardware Domain。二、聚合设计原则与 Rust 实现模式2.1 四条核心原则aggregates.md开篇即定义了聚合建模必须遵守的四条不变式原则这也是阅读后面所有聚合代码的总纲事务一致性Transactional Consistency聚合内部的所有变更要么全部成功、要么全部失败是原子操作的最小边界身份Identity每个聚合根必须有唯一标识符跨聚合引用一律使用 ID封装Encapsulation内部实体只能通过聚合根暴露的方法访问外部无法直接触碰内部状态最终一致性Eventual Consistency聚合之间不持有对方对象引用只通过事件或 ID 协作跨聚合的数据同步允许最终一致。2.2 Rust 落地模板aggregates.md给出了一个贯穿全文的 Rust 实现模式它把上述原则翻译成了三个可操作的编码约束// Aggregate root with private constructor enforcing invariants pub struct AggregateRoot { id: AggregateId, // ... fields } impl AggregateRoot { // Factory method enforcing invariants pub fn create(params: CreateParams) - ResultSelf, DomainError { // Validate invariants Self::validate(params)?; Ok(Self { id: AggregateId::generate(), // ... initialize fields }) } // Commands return domain events pub fn handle_command(mut self, cmd: Command) - ResultVecDomainEvent, DomainError { // Validate command against current state // Apply state changes // Return events } }这套模板的关键点在于字段私有struct的字段不暴露pub外部只能通过方法操作状态封装原则由此在编译器层面得到保证工厂方法create构造路径唯一所有不变式校验集中在工厂内杜绝直接struct字面量绕过校验命令返回事件状态变更方法返回ResultVecDomainEvent, DomainError既做了状态迁移合法性校验又顺带产出可审计、可持久化、可广播的领域事件。仓库中的落地实现虽然出于序列化需要把核心数据类型的字段设为pub见 types.rs但不变式校验仍然集中在构造方法中例如Confidence::new对[0.0, 1.0]区间做校验、Frequency要求必须为正数这与文档模板的设计意图完全一致。2.3 类型驱动的领域标识符聚合根的身份字段全部采用newtype 模式包裹Uuid这是 RuView 里贯穿始终的 Rust 惯例。aggregates.md中定义了FrameId、SignalId、EstimateId、SessionId、ClientId、DeviceId、PersonId、ZoneId、CalibrationId等新类型仓库实现与之对应/// Unique identifier for a CSI frame. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FrameId(Uuid); impl FrameId { pub fn new() - Self { Self(Uuid::new_v4()) } pub fn from_uuid(uuid: Uuid) - Self { Self(uuid) } pub fn as_uuid(self) - Uuid { self.0 } }源码见 v2/crates/wifi-densepose-core/src/types.rsnewtype 的价值在于DeviceId与SessionId在类型层面就是不同的编译器会阻止你把设备 ID 误当作会话 ID 传入比字符串裸用安全得多。wifi-densepose-core在 lib.rs 中还定义了领域级常量MAX_KEYPOINTS 17COCO 格式关键点数、MAX_SUBCARRIERS 256、DEFAULT_CONFIDENCE_THRESHOLD 0.5供全生态共享。三、聚合一CsiFrame——原始 CSI 数据3.1 职责CsiFrame代表从 WiFi 硬件捕获的单帧信道状态信息是贯穿信号处理管线的基础数据结构。它承载了人体感知所需的全部原始物理量多天线 × 多子载波的幅度与相位矩阵、中心频率、带宽、SNR、RSSI、噪声基底等并携带一个处理状态机ProcessingStatus。3.2 聚合根结构aggregates.md给出的规格pub struct CsiFrame { id: FrameId, // 身份 device_id: DeviceId, // 关联设备仅 ID session_id: OptionSessionId, // 关联会话仅 ID timestamp: DateTimeUtc, // 时间戳 sequence_number: u64, // 会话内单调递增序号 amplitude: Array2f32, // [antennas, subcarriers] phase: Array2f32, // [antennas, subcarriers] frequency: Frequency, bandwidth: Bandwidth, num_subcarriers: u16, num_antennas: u8, snr: SignalToNoise, rssi: OptionRssi, noise_floor: OptionNoiseFloor, status: ProcessingStatus, processed_at: OptionDateTimeUtc, metadata: FrameMetadata, }仓库中的 CsiFrame 采用了「元数据对象 复杂矩阵」的组织方式CsiFrame { id, metadata: CsiMetadata, data: Array2Complex64, amplitude: Array2f64, phase: Array2f64 }其中amplitude/phase由复数的模与辐角投影而来data.mapv(Complex::norm)与data.mapv(Complex::arg)。CsiMetadata则聚合了时间戳、设备 ID、频段、信道、带宽、天线配置、RSSI、噪声基底、序列号以及 ADR-136 规定的溯源字段calibration_idADR-135 空房间基线、model_id、model_version见 types.rs。这与文档规格一一对应只是把复数原始数据也保留下来方便 ndarray 原生数值计算。3.3 值对象围绕CsiFrameaggregates.md定义了三个关键值对象它们的共同特征是构造即校验、不可变值对象不变式关键行为Frequency(f64)必须为正hz 0.0拒绝as_hz()、as_ghz()、wifi_2_4ghz()、wifi_5ghz()Bandwidth枚举仅限 20/40/80/160 MHzas_hz()expected_subcarriers()分别返回 56/114/242/484SignalToNoise(f64)必须在 [-50, 50] dBis_good()≥20dB、is_acceptable()≥10dB其中Bandwidth::expected_subcarriers()给出的是 IEEE 802.11 OFDM 的典型子载波数这一数值在仓库 FrequencyBand::typical_subcarriers() 中有对应2.4GHz→56、5GHz→114、6GHz→234。值得注意SNR 的上下界 [-50, 50] dB 是文档规格实际捕获中如果出现越界值应在信号清洗阶段标记异常帧而非直接丢弃这正是ProcessingStatus::Failed存在的意义。ProcessingStatus枚举刻画了帧在管线中的生命周期pub enum ProcessingStatus { Pending, Preprocessing, FeatureExtraction, Completed, Failed { reason: String }, }3.4 不变式清单CsiFrame聚合根强制五条不变式全部在工厂方法中校验幅度矩阵与相位矩阵维度必须一致矩阵维度必须等于num_subcarriers × num_antennas频率必须为正数SNR 必须在合理范围内-50 到 50 dB同一会话内序列号单调递增。工厂方法create的维度校验逻辑如下来自aggregates.mdpub fn create(params: CreateCsiFrameParams) - ResultSelf, DomainError { let (rows, cols) params.amplitude.dim(); if rows ! params.num_antennas as usize || cols ! params.num_subcarriers as usize { return Err(DomainError::DimensionMismatch { /* ... */ }); } if params.amplitude.dim() ! params.phase.dim() { return Err(DomainError::PhaseDimensionMismatch); } // ... 初始化字段 }这里体现了一个重要设计维度错误不是 panic 而是领域错误DomainError错误类型携带期望值与实际值expected_antennas、expected_subcarriers、actual_rows、actual_cols便于上层在采集与传输层面对齐。从源码结构看仓库的CoreError体系error.rs同样区分SignalError、InferenceError、StorageError与文档领域错误与基础设施错误分离的指导思想一致。3.5 命令与状态机聚合根还定义了reconstitute从持久化重建绕过校验与三个状态迁移命令start_preprocessing()仅允许从Pending→Preprocessing产出CsiFramePreprocessingStartedcomplete_feature_extraction()仅允许从Preprocessing/FeatureExtraction→Completed写入processed_at产出CsiFrameProcessedfail(reason)任意状态 →Failed { reason }产出CsiFrameProcessingFailed。非法迁移一律返回DomainError::InvalidStateTransition { from, to }。这种把状态机编码进命令方法的做法让帧处理顺序错误这类 bug 在编译运行期第一时间暴露而不是等数据流到推理阶段才出错。3.6 与 ADR-136 帧契约的衔接仓库实现比文档更进一步CsiFrame实现了CanonicalFrametrait提供架构无关的确定性字节编码to_canonical_bytes()/from_canonical_bytes()types.rs。编码布局为「16 字节帧 UUID ‖ 元数据字段按声明顺序 ‖ 形状u32×u32 LE‖ 复数载荷每样本 16 字节 LE」解码端严格 fail-closed截断、非法判别字节、非 UTF-8 设备 ID、保留区非零、载荷长度不匹配、尾随字节全部报错。这套契约对应 ADR-136保证了 x86_64、aarch64 与 XtensaESP32-S3上产出逐字节一致的 witness 哈希——CsiFrame由此同时是领域模型与可验证的传输/审计单元。四、聚合二ProcessedSignal——供推理的特征信号4.1 职责与边界ProcessedSignal是信号域的产出、姿态域的输入它把一帧或多帧CsiFrame清洗、特征提取后的结果聚合成一个独立聚合。从 domain-model.md 的上下文图看Signal Domain 通过CsiProcessor、PhaseSanitizer、FeatureExtractor三个领域服务完成原始帧到特征信号的转换。4.2 聚合根结构aggregates.md规格pub struct ProcessedSignal { id: SignalId, source_frames: VecFrameId, // 关联原始帧仅 ID 列表 device_id: DeviceId, session_id: OptionSessionId, timestamp: DateTimeUtc, window_start: DateTimeUtc, window_end: DateTimeUtc, features: SignalFeatures, human_presence: HumanPresenceResult, quality_score: QualityScore, processing_config: ProcessingConfig, extraction_time: Duration, }仓库实现 ProcessedSignal 保留了source_frame_ids、timestamp、device_id并把文档中二维的幅度/相位特征升级为三维张量amplitude_tensor/phase_tensor形状为[time_steps, spatial_streams, subcarriers]——这是为神经网络推理NeuralInferencetrait准备的批式输入num_time_steps()返回时间窗内的帧数。SignalFeatures在仓库中沉淀为多普勒速度、飞行时间ToF、到达角AoA、运动置信度、存在置信度与身体计数的结构化输出types.rs。文档规格的SignalFeatures则从统计特征角度定义了完整的特征面pub struct SignalFeatures { amplitude_mean: Array1f32, amplitude_variance: Array1f32, amplitude_skewness: Array1f32, amplitude_kurtosis: Array1f32, phase_difference: Array1f32, phase_unwrapped: Array2f32, antenna_correlation: Array2f32, subcarrier_correlation: Array2f32, doppler_shift: Array1f32, power_spectral_density: Array1f32, dominant_frequencies: Vecf32, temporal_variance: OptionArray1f32, motion_indicators: OptionMotionIndicators, }可以看到特征被分为五类幅度统计均值/方差/偏度/峰度、相位特征相位差/解缠绕相位、相关性特征天线间/子载波间、频域特征多普勒频移/功率谱密度/主频、时域特征时间方差/运动指标。phase_unwrapped对应PhaseSanitizer服务的输出——修正 2π 相位不连续是 CSI 感知的关键预处理步骤见 domain-model.md 通用语言表。4.3 质量评分与存在检测ProcessedSignal引入了两个复合值对象HumanPresenceResult { detected, confidence, motion_score, estimated_count }——人体存在检测结果estimated_count: Optionu8支持多人场景QualityScore(f32)——信号质量评分必须落在 [0, 1]is_usable()≥0.3与is_good()≥0.7两个阈值分别决定信号能否进入推理、是否属于高质量样本。工厂方法中的质量计算逻辑calculate_quality是一个可运行的参考实现let amplitude_quality if features.amplitude_variance.iter().any(|v| v 0.0) { 1.0 } else { 0.5 }; let phase_quality if !features.phase_difference.is_empty() { 1.0 } else { 0.3 }; let score 0.6 * amplitude_quality 0.4 * phase_quality; QualityScore::new(score)其设计含义是幅度方差为零说明信号近乎静止/无效相位差为空说明相位清洗失败两路加权合成一个 [0,1] 评分。若source_frames为空工厂直接返回DomainError::NoSourceFrames——聚合不能凭空产生。4.4 事件的桥梁角色ProcessedSignal的创建对应领域事件SignalProcessed该事件携带FeatureSummary幅度均值/标准差、相位方差、主频、运动指标而非完整数据确保跨上下文只传摘要、不传大对象见 domain-events.md。事件流CsiFrameReceived → SignalProcessed → PoseEstimated → (MotionDetected | FallDetected)正是聚合间协作的最终一致性通道。五、聚合三PoseEstimate——姿态推断结果5.1 职责PoseEstimate是姿态推断的输出聚合包含检测到的人员及其身体配置、关键点、活动分类。它是 RuView 核心域Pose Domain的产物直接驱动流式分发与告警。5.2 聚合根结构pub struct PoseEstimate { id: EstimateId, signal_id: SignalId, // 源信号仅 ID session_id: SessionId, zone_id: OptionZoneId, timestamp: DateTimeUtc, frame_number: u64, persons: VecPersonDetection, // 多人检测 person_count: u8, processing_time: Duration, model_version: ModelVersion, algorithm: InferenceAlgorithm, // 如 DensePose overall_confidence: Confidence, is_valid: bool, detected_events: VecPoseEvent, }PersonDetection是完整的单人体检测描述person_id、bounding_box、keypoints: KeypointSet、body_parts: OptionBodyPartSegmentation身体部件分割、uv_coordinates: OptionUvMapDensePose 的 UV 表面映射、confidence、activity、velocity: OptionVelocity2D。body_parts与uv_coordinates的 Option 化意味着基础姿态推断可以只输出关键点DensePose 的稠密表面映射按需开启兼顾精度与算力成本。5.3 关键点集与 COCO 17 点KeypointSet内部用HashMapKeypointName, Keypoint组织关键点提供add/get/iter/visible_count()。KeypointName严格遵循COCO 17 关键点约定pub enum KeypointName { Nose, LeftEye, RightEye, LeftEar, RightEar, LeftShoulder, RightShoulder, LeftElbow, RightElbow, LeftWrist, RightWrist, LeftHip, RightHip, LeftKnee, RightKnee, LeftAnkle, RightAnkle, }仓库 KeypointType 以#[repr(u8)]精确对应 COCO 索引Nose0 … RightAnkle16并实现了TryFromu8越界返回CoreError还提供了is_face()、is_upper_body()、is_lower_body()分组判断。Keypoint支持 2D/3Dz: Optionf32坐标与置信度is_visible()的判定为「未遮挡且置信度 0.5」——与 lib.rs 的DEFAULT_CONFIDENCE_THRESHOLD 0.5遥相呼应。BoundingBox在仓库中实现了 IoU 计算、contains点包含、从关键点集推导边界框PersonPose::compute_bounding_box等实用方法types.rs。5.4 值对象Confidence(f32)范围 [0,1]is_high()≥0.8、is_medium()[0.5, 0.8)、is_low()0.5三档分级。仓库 Confidence 用is_high()对照全局阈值并支持自定义exceeds(threshold)Position2Dx/y 均用NormalizedCoordinate(f32)包裹强制归一化到 [0,1]——这保证了姿态数据与摄像头画面尺寸、屏幕分辨率解耦Activity枚举Standing / Sitting / Walking / Running / Lying / Falling / Unknown并带两个领域判定is_alert_worthy()仅Falling与is_mobile()Walking | Running。Falling是唯一需要立即告警的活动这与FallDetected作为关键告警事件的地位一致domain-events.md。5.5 命令、事件与告警生成PoseEstimate::create是文档中最完整的命令示例——它一次性完成检测结果聚合、置信度计算与事件生成pub fn create(...) - Result(Self, VecDomainEvent), DomainError { // 1. 统计人数与总体置信度多人置信度均值 let person_count persons.len() as u8; let overall_confidence Self::calculate_overall_confidence(persons); // 2. 显著速度 → MotionDetected聚合内事件 领域事件 if persons.iter().any(|p| p.velocity.map(|v| v.is_significant()).unwrap_or(false)) { events.push(DomainEvent::MotionDetected(MotionDetectedEvent { zone_id, person_count, timestamp })); } // 3. 高置信度 Falling → FallDetected逐人检查 for person in persons { if person.activity Activity::Falling person.confidence.is_high() { events.push(DomainEvent::FallDetected(FallDetectedEvent { person_id, zone_id, confidence, timestamp })); } } // 4. 始终产出 PoseEstimated 主事件 events.push(DomainEvent::PoseEstimated(PoseEstimatedEvent { estimate_id, signal_id, person_count, overall_confidence, timestamp })); Ok((estimate, events)) }注意这里命令返回的是(Self, VecDomainEvent)元组聚合状态变更与领域事件在同一事务边界内完成事件随后交给事件总线EventPublisher广播。MotionDetected事件在 domain-events.md 中细化了MotionTypeGeneral/Walking/Running/Sudden/Repetitive与速度、方向估计FallDetected则带FallTypeForward/Backward/Lateral/Gradual/Unknown、ImpactSeverityLow/Medium/High/Critical与requires_verification、auto_alert_sent等告警控制字段——这些字段正是物联网跌倒监测场景中避免误报、分级响应的关键设计。六、聚合四Session——实时流式会话6.1 职责Session代表一次客户端实时流式连接管理连接生命周期、订阅与投递指标。它处于 Streaming Domain是姿态数据到达浏览器的最后一公里。6.2 聚合根与订阅过滤pub struct Session { id: SessionId, client_id: ClientId, connected_at: DateTimeUtc, last_activity: DateTimeUtc, remote_addr: OptionIpAddr, user_agent: OptionString, stream_type: StreamType, zone_subscriptions: HashSetZoneId, // 多区域订阅 filters: SubscriptionFilters, status: SessionStatus, // 状态机 messages_sent: u64, messages_failed: u64, bytes_sent: u64, latency_samples: VecDuration, // 最近 100 条延迟样本 }SubscriptionFilters是客户端可动态调整的投递策略直接对应 WebSocket 协议层的参数协商字段类型语义min_confidenceOptionConfidence低于该置信度的姿态结果不推送max_personsOptionu8只关心不超过 N 人的场景include_keypointsbool是否携带关键点include_segmentationbool是否携带身体部件分割include_uv_coordinatesbool是否携带 DensePose UV 映射throttle_intervalOptionDuration节流间隔控制推送频率activity_filterOptionVecActivity只推送指定活动类别这一设计让同一个推理后端能服务不同客户端健康监测 App 可以只订阅Falling/Lying活动并关闭分割数据而可视化调试面板则全量订阅。domain-events.md中的SubscriptionFiltersDto与SessionStarted/SubscriptionUpdated事件为这一机制提供了协议层对应。6.3 会话状态机SessionStatus完整刻画了实时连接的七种状态pub enum SessionStatus { Connecting, // 初始连接尚未订阅 Active, // 正常接收数据 Paused, // 客户端主动暂停 Reconnecting { attempts: u8, last_attempt: DateTimeUtc }, Completed { ended_at: DateTimeUtc }, // 优雅关闭 Failed { reason: String, failed_at: DateTimeUtc }, Cancelled { cancelled_at: DateTimeUtc }, }状态迁移由命令方法强制约束非法迁移返回明确的DomainErroractivate()仅允许Connecting | Reconnecting→Activepause()/resume()仅Active ⇄ Pausedconnection_lost()仅Active | Paused→Reconnecting { attempts: 0, ... }complete()仅Active | Paused→Completed并产出带duration、messages_sent、reason的SessionEndedEventupdate_filters()/subscribe_to_zone()要求会话必须处于Activeis_active()守卫。投递指标方面record_message_sent(bytes, latency)维护滑动窗口——最多保留最近 100 条延迟样本超出即从队头移除average_latency()据此计算均值record_message_failed()累计失败数。订阅查询is_subscribed_to_zone(zone_id)遵循「订阅集为空 订阅全部区域」的语义简化了全量广播场景。6.4 会话生命周期事件流domain-events.md给出了完整的会话时序WebSocket Connect → SessionStarted → Subscribe to zones → SubscriptionUpdated → PoseEstimated 推送 → Disconnect → SessionEnded。聚合根的SessionStarted/SessionEnded事件带SessionEndReasonClientDisconnect/ServerShutdown/Timeout/Error/Evicted为监控与运维提供了终止原因归类。七、聚合五Device——硬件设备生命周期7.1 职责Device抽象了所有能提取 CSI 的物理 WiFi 设备从 ESP32、ESP32-S3 到 Atheros 路由器、Intel NIC 5300/5500、Nexmon、PicoScenes乃至自定义硬件DeviceType::Custom(String)。它管理设备生命周期、配置校验与健康状态。仓库中与之对应的硬件适配层包括 wifi-densepose-hardware 的 ESP32/联发科/高通 CSI 解析器与传输层。7.2 聚合根与能力模型pub struct Device { id: DeviceId, name: DeviceName, device_type: DeviceType, mac_address: MacAddress, ip_address: OptionIpAddress, firmware_version: OptionFirmwareVersion, hardware_version: OptionHardwareVersion, capabilities: DeviceCapabilities, location: OptionLocation, zone_id: OptionZoneId, status: DeviceStatus, // 状态机 last_seen: OptionDateTimeUtc, health_checks: VecDequeHealthCheckResult, // 最近 10 次 consecutive_failures: u8, config: DeviceConfig, calibration: OptionCalibrationData, tags: HashSetString, custom_properties: HashMapString, serde_json::Value, created_at: DateTimeUtc, updated_at: DateTimeUtc, }DeviceCapabilities是配置校验的依据pub struct DeviceCapabilities { pub max_subcarriers: u16, pub max_antennas: u8, pub supported_bandwidths: VecBandwidth, pub supported_frequencies: VecFrequencyBand, pub max_sampling_rate_hz: u32, pub supports_mimo: bool, pub supports_beamforming: bool, }DeviceConfig则描述当前采集参数sampling_rate_hz、subcarriers、antennas、bandwidth、channel: WifiChannel、tx_power、gain。能力与配置分离是这里的核心设计能力是硬件出厂属性配置是运行期可调参数配置必须接受能力校验。7.3 值对象MAC 与信道MacAddress([u8; 6])parse()接受冒号分隔的 6 段十六进制如aa:bb:cc:dd:ee:ff格式错误返回DomainError::InvalidMacFormatto_string()提供规范小写输出WifiChannel { number: u8, band: FrequencyBand }frequency()方法按频段换算中心频率——2.4GHz 波段2412 (channel - 1) × 5 MHz信道 1-145GHz 波段5000 channel × 5 MHz6GHz 波段5950 channel × 5 MHz。这为信道规划与频段扫描提供了纯函数换算对应仓库中FrequencyBand::center_frequency_mhz()types.rs。7.4 设备状态机pub enum DeviceStatus { Disconnected, Connecting { started_at: DateTimeUtc }, Connected { connected_at: DateTimeUtc }, Streaming { stream_started_at: DateTimeUtc, frames_sent: u64 }, Calibrating { calibration_id: CalibrationId, progress: u8 }, Maintenance { reason: String }, Error { error: DeviceError, occurred_at: DateTimeUtc }, }命令方法严格执行迁移规则register()工厂方法产出DeviceRegisteredEvent初始状态Disconnectedconnect()仅Disconnected | Error→Connectingconnection_established()仅Connecting→Connected重置consecutive_failuresstart_streaming()仅Connected→Streaming产出携带当前配置的DeviceStreamingStartedEventstop_streaming()则回到Connected并汇报累计frames_sentconfigure(config)能力校验三连——subcarriers ≤ max_subcarriers、antennas ≤ max_antennas、带宽必须在supported_bandwidths内否则分别返回ConfigExceedsCapabilities、UnsupportedBandwidthrecord_health_check(result)维护最近 10 次健康检查队列健康则清零连续失败计数否则累加。查询方法is_healthy()的定义值得注意consecutive_failures 3且当前不在Error状态——连续 3 次失败即判定设备不健康uptime()则基于connected_at/stream_started_at计算在线时长。这些阈值与DeviceHealthChanged、DeviceError事件domain-events.md共同构成设备自愈监控闭环。八、跨聚合引用只允许 ID禁止对象引用聚合之间永远不能直接持有对方对象。aggregates.md用正反两个例子划清边界// 正确按 ID 引用 pub struct CsiFrame { device_id: DeviceId, // ID only session_id: OptionSessionId, // ID only } // 错误直接引用对象永远不要这样做 pub struct CsiFrame { device: Device, // WRONG: 制造耦合 session: OptionSession, // WRONG: 破坏边界 }这样做的原因在文中已经明示直接引用会让聚合失去事务边界改动Device可能意外连带CsiFrame、让仓储持久化变得困难、并在序列化与网络传输时产生循环引用。仓库中的ProcessedSignal.source_frame_ids: VecFrameId、PoseEstimate.source_signal_ids: VecFrameIdtypes.rs都是这一原则的实例聚合根只保存上游的 ID 列表需要完整数据时通过对应仓储按 ID 加载。九、仓储模式聚合的持久化边界每个聚合根对应一个仓储接口aggregates.md给出的统一抽象为#[async_trait] pub trait AggregateRepositoryA, ID { async fn find_by_id(self, id: ID) - ResultOptionA, RepositoryError; async fn save(self, aggregate: A) - Result(), RepositoryError; async fn delete(self, id: ID) - Resultbool, RepositoryError; }仓储只暴露find_by_id/save/delete三个基础操作不暴露任何查询细节。在domain-model.md中Storage Domain 列出了具体实现CsiFrameRepository、SessionRepository、DeviceRepository、PoseEstimateRepository、RecordingRepository、ModelRepository。与reconstitute工厂呼应——从持久化恢复聚合时跳过业务校验数据在创建时已验证但save仍需保证聚合内全部变更一次落库维护事务一致性。十、聚合全景从 CSI 帧到跌倒告警的完整链路把 5 个聚合与领域事件串起来RuView 的实时感知闭环可以概括为Device硬件域 │ register / connect / start_streaming能力校验 状态机 ▼ CsiFrame信号域聚合一 │ CsiFrameReceived → 校验维度/SNR/序列号→ 状态机流转 ▼ ProcessedSignal信号域聚合二 │ SignalProcessed特征提取 质量评分 存在检测 ▼ PoseEstimate姿态域聚合三核心域 │ PoseEstimated / MotionDetected / FallDetected ▼ Session流式域聚合四 │ 订阅过滤 节流 投递指标 → WebSocket 客户端跨聚合协作全部通过「ID 引用 领域事件」完成CsiFrame通过device_id关联Device通过session_id关联SessionProcessedSignal聚合多帧FrameIdPoseEstimate引用SignalId并产出触发告警的事件Session只关心事件流而非内部对象。每个聚合都拥有newtype 身份、私有/受控字段、构造即校验的工厂、明确的状态机、以及对应仓储。十一、落地建议与延伸阅读如果你要在 RuView 生态内实践这套聚合模型建议遵循以下几点从仓库核心类型出发领域数据类型以wifi_densepose_core::types为共享内核Shared Kernel跨 crate 统一引用 lib.rs 的 re-export避免各模块各自复制结构体用 newtype 锁住身份所有*Id一律独立 newtype禁止裸用Uuid或String传参状态机用枚举 命令方法任何状态迁移都写成返回Result_, DomainError的方法非法迁移返回带from/to的错误便于告警与审计跨聚合只发事件需要异步协作的场景如运动/跌倒告警参考 domain-events.md 的事件目录与EventEnvelope事件类型命名遵循「过去时 聚合名」约定DeviceConnected而非ConnectDevice聚合边界要小CsiFrame与ProcessedSignal各自独立成聚合而非合并成一个巨型聚合——这正是为了在数据量巨大的 CSI 管线上保持事务粒度可控。相关文档与源码索引聚合规格v2/docs/ddd/aggregates.md领域模型总览与上下文图v2/docs/ddd/domain-model.mdDDD 文档目录v2/docs/ddd/README.md领域事件目录与事件流图v2/docs/ddd/domain-events.md核心类型实现CsiFrame/ProcessedSignal/PoseEstimate 等v2/crates/wifi-densepose-core/src/types.rs错误类型与核心 traitv2/crates/wifi-densepose-core/src/error.rs、v2/crates/wifi-densepose-core/src/traits.rs硬件 CSI 解析与传输v2/crates/wifi-densepose-hardware/src/csi_frame.rs帧契约 ADRdocs/adr/ADR-136-ruview-streaming-engine-frame-contracts.md、docs/adr/ADR-135-empty-room-baseline-calibration.md【免费下载链接】RuViewπ RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video.项目地址: https://gitcode.com/GitHub_Trending/wi/RuView创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网