返回系列

总览:RespiraScope 的系统边界与设计取舍

从项目定位、整体架构和核心取舍开始,理解 RespiraScope 为什么被设计成一个后端采集处理服务加 Web Console 的实时信号系统。

背景问题

RespiraScope 要解决的不是“画一条曲线”这么简单。真实问题是:呼吸传感器不断输出 TCP 采样点,后端要给每个点编号、实时滤波、识别波峰波谷、计算 BPM 和稳定性,同时把原始数据、滤波数据、事件、指标、记录片段推给 Web Console 和外部系统。

这类系统最容易失控的地方有三个:实时链路会被 UI 和算法耦合在一起,滤波逻辑会被“实时显示”和“离线复盘”互相污染,记录文件会只保存一段波形而丢失上下文。RespiraScope 的设计重点就是把这些边界拆开。

RespiraScope 实时监测界面

设计方案对比

方案优点问题适合场景
单脚本 Demo上手最快,几百行代码就能读 TCP、滤波、画图难测试,难复用,前端和算法耦合,异常状态难解释一次性课堂演示
纯前端处理UI 交互灵活,部署简单浏览器不适合直接接真实 TCP 设备,SciPy 算法不可直接复用已有 WebSocket 数据源的轻量展示
后端服务 + Web Console采集、算法、API、UI 边界清楚,外部系统也能接入需要维护 HTTP、Socket.IO、静态前端和配置RespiraScope 当前目标
桌面一体化商业工具用户体验完整,可管控环境打包、升级、日志、设备适配复杂度更高长期交付型产品

RespiraScope 选择“后端服务 + Web Console”,原因是它既要服务自己的 Monitor 页面,也要给外部业务系统留出集成入口。后端负责最稳定的部分:设备连接、滤波、识别、记录和协议;前端负责展示、交互、回看和参数调整。

当前方案

项目核心链路由 BreathProcessSystem 装配。它创建 DataReceiver、SignalProcessor、PeakValleyDetector、RecordManager、DataQueueManager、DataSender 和 SignalQualityAnalyzer。这些对象不是靠一个巨大的循环彼此调用,而是通过小的 handler 接口串起来。

传感器输入 真实 TCP / Mock TCP
<rect x="282" y="50" width="190" height="128" rx="16" fill="var(--surface)" stroke="var(--line)" />
<text x="377" y="85" text-anchor="middle" fill="currentColor" font-weight="800">后端实时服务</text>
<text x="377" y="112" text-anchor="middle" fill="var(--muted)" font-size="13">FastAPI</text>
<text x="377" y="136" text-anchor="middle" fill="var(--muted)" font-size="13">SignalProcessor</text>
<text x="377" y="160" text-anchor="middle" fill="var(--muted)" font-size="13">PeakValleyDetector</text>

<rect x="545" y="50" width="180" height="128" rx="16" fill="var(--surface)" stroke="var(--line)" />
<text x="635" y="85" text-anchor="middle" fill="currentColor" font-weight="800">数据出口</text>
<text x="635" y="112" text-anchor="middle" fill="var(--muted)" font-size="13">HTTP 控制面</text>
<text x="635" y="136" text-anchor="middle" fill="var(--muted)" font-size="13">Socket.IO 数据面</text>
<text x="635" y="160" text-anchor="middle" fill="var(--muted)" font-size="13">JSON 记录文件</text>

<rect x="790" y="72" width="150" height="84" rx="14" fill="var(--surface)" stroke="var(--line)" />
<text x="865" y="107" text-anchor="middle" fill="currentColor" font-weight="800">消费者</text>
<text x="865" y="132" text-anchor="middle" fill="var(--muted)" font-size="13">Console / 外部系统</text>

<path d="M210 114 H270" stroke="var(--accent)" stroke-width="2.5" marker-end="url(#arrow-rs-overview)" />
<path d="M472 114 H533" stroke="var(--accent)" stroke-width="2.5" marker-end="url(#arrow-rs-overview)" />
<path d="M725 114 H778" stroke="var(--accent)" stroke-width="2.5" marker-end="url(#arrow-rs-overview)" />

<rect x="282" y="252" width="190" height="92" rx="16" fill="var(--surface)" stroke="var(--line)" />
<text x="377" y="288" text-anchor="middle" fill="currentColor" font-weight="800">记录复盘</text>
<text x="377" y="315" text-anchor="middle" fill="var(--muted)" font-size="13">pre / record / post</text>

<rect x="545" y="252" width="180" height="92" rx="16" fill="var(--surface)" stroke="var(--line)" />
<text x="635" y="288" text-anchor="middle" fill="currentColor" font-weight="800">模拟与异常</text>
<text x="635" y="315" text-anchor="middle" fill="var(--muted)" font-size="13">可重复调试场景</text>

<path d="M377 178 V240" stroke="var(--accent)" stroke-width="2.5" marker-end="url(#arrow-rs-overview)" />
<path d="M635 178 V240" stroke="var(--accent)" stroke-width="2.5" marker-end="url(#arrow-rs-overview)" />

软件结构可以按职责读:

层次关键代码责任
启动与配置config.py、main.py、frontend_static.py读取外部 TOML,启动后端和统一 Web Console
采集输入socket_client.py、data_receive.py、mock_sensor/*连接真实或模拟 TCP 传感器,生成 sequence
实时处理signal_processor.py、filter_strategies.py滑动窗口滤波,区分实时与离线策略
事件识别peak_valley_detector.py、signal_quality.py峰谷、BPM、稳定性、异常质量事件
记录复盘record_manager.py保存记录区间和前后辅助上下文
通信出口http_service.py、socket_io_service.py、send_to_client.pyHTTP 控制、Socket.IO 推送、记录保存
Web Consolefrontend-console、frontend-monitor、frontend-lab实时监测、模拟设置、记录回看

为什么这样选

第一,实时系统必须能解释时间顺序。项目给每个 raw 点分配递增 sequence_number,后续 filtered、peak、valley、record、scan 都靠同一条序号轴对齐。相比只靠时间戳,sequence 更适合判断断流、补齐、回放和前端图表对齐。

第二,实时显示和复盘分析目标不同。实时滤波要低延迟,允许轻微相位滞后;离线滤波要更平滑、更准确,可以使用完整片段和双向滤波。把两者做成 RealtimeFilterStrategy 和 OfflineFilterStrategy,比在一个函数里塞很多分支更容易解释。

第三,项目保留 HTTP 和 Socket.IO 两种接口。HTTP 适合“开始接收、停止接收、开始记录、保存记录”这类明确命令;Socket.IO 适合 40ms 一批的实时数据。全部用 HTTP 会变成高频轮询,全部用 Socket 又会让控制命令和错误响应难以规范。

代价和边界

这个设计不是诊断级临床系统。它没有权限审计、医疗设备认证、长期数据归档,也没有对所有传感器协议做抽象。它更适合设备联调、算法验证、教学、演示和小规模工程集成。

当前设计也不是最高性能架构。Python、FastAPI、Socket.IO 和浏览器图表足以支撑当前采样率和演示规模,但如果要处理多床位、多设备、高并发长期监测,就需要引入消息队列、持久化数据库、指标监控和更严格的服务拆分。

关键函数表

函数或类作用设计意义
create_socket_app()创建 Socket.IO ASGI 应用并挂载 FastAPI让 HTTP 和实时推送共用一个后端端口
BreathProcessSystem.start()更新滤波参数并启动 receiver task把启动流程收敛到一个生命周期入口
DataReceiver.data_handler()给采样点编号并分发给 handlers保证所有下游模块共享同一 sequence
SignalProcessor.process_sliding_window()每次处理固定 step 的新数据平衡延迟和计算成本
PeakValleyDetector.detect_peak_valley_batch()离线批量识别峰谷给记录复盘提供更稳定的结果
RecordManager.build_record_data()组装记录文件结构保存数据、区间、scan 和滤波结果的上下文
DataQueueManager.snapshot_messages()给新 Socket client 补发最近快照页面刷新后不用从空图表开始等待

读源码建议

先读 BreathProcessSystem.__init__() 和 _setup_connections(),理解对象如何被装配。再读 start()、DataReceiver.data_handler()、SignalProcessor.process_sliding_window()、DataSender.start_sending(),就能顺着实时链路看到每个点如何从 TCP 进入浏览器。最后读 RecordManager,理解为什么一个记录文件不只是 raw 数组,而是带有前后辅助区间和 scan 标注的复盘对象。