Files
orisys-sdk/docs/force-stream-session.zh.md
T
ykcheungab ad4c3ba6b9 feat: ForceStreamOutput 新增同幀未濾波量測欄位
App 需要濾波前的 raw 與 N 供錄製、分析與標定取樣;診斷幀保留 raw 特徵、N 為 NaN。已以帶標定、中值加低通的會話逐幀對照直通結果驗證。
2026-10-23 10:41:05 +08:00

167 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 新力侧会话与单路入口(T06a/T06b、T07 输出滤波、T08a/T08b)
本文件是单路 SensorStream 计划 T06a/T06b/T07 的 SDK 合同:新的力侧会话直接消费 T05 的
`ImageMeasurement`(T03 `MeasurementFrame` 头 + 同帧 raw/field),参考版本取帧上的
`reference_generation`。旧 `ForceSession` 仍为 App 保留到 T09/T10 迁移,本会话不提供
`process_measurement`、`request_zero_external_reset`、`tracker_reset_seq` 等旧外部复位接口。
## 接口
- `ForceStreamSession(sensor_family=..., calibration=None, settings=None)`:构造。T06a 支持
无标定与 schema2 标量曲线;T06b 补充 schema3 field 模型(`hhd_normal_l1_field/1`,需要
`context` 与同帧 field)和 FN-only 全局增益模型(`outputs=["fn"]`);`settings` 为
`OutputSettings`(None 用旧默认)。
- `measurement_requirements`:`(want_field, want_contact)`,告诉 T05 调用方是否需要把
`field` include 进 `ImageMeasurement`(field 模型为 `(True, False)`,标量/FN-only 为 `(False, False)`)。
- `request_zero(reference_id=..., declared_unloaded=True, expected_reference_generation=版本, max_gap_s=None, max_window_s=None)`:
为指定参考版本确认无载归零;可选窗口上限。
- `process(measurement) -> ForceStreamOutput`:消费一个 T05 `ImageMeasurement`,返回逐帧结果
(`frame` 携带 T03 帧头与 `reference_generation`,以及 `raw/newton/status/quality/zero`)。
`measurement_raw`/`measurement_newton` 是同帧、**未经输出滤波**的测量三元组(T09c):接受帧取
`ForceOutput.measurement` 的未滤波 raw/N;诊断帧的 `measurement_raw` 等于该帧 `force_raw`、
`measurement_newton` 为 NaN。直通设置下它们与 `raw`/`newton` 逐位相同。
## 规则
- **版本/会话变化即废旧零点(K8.3)。** `reference_generation` 或 `session_id` 一旦变化,
resetting、collecting、ready 任一阶段的旧零点立即失败(`zero.phase == "failed"`),
必须对新的版本显式调用 `request_zero(expected_reference_generation=新版本)` 才能恢复有效 N。
正常顺序(会话在处理 v0 → 复位得到目标 v1 → v1 到达)中,等于**请求目标**的新版本会被采用
并开始/继续同版本窗口,不会作废这次请求;只有未请求的版本变化或版本超过目标才失败。
旧的 `generation` 在采样开始后不再检查的缺陷(阶段 0 P3-2)由本会话修复。
- **基线只取请求版本的帧(K8.3/K8.5)。** 归零请求绑定一个版本。目标到达前、仍属当前版本的
在途帧记为 `zero_pending`(不是迟到,也绝不进入窗口);比当前版本更旧的帧才是 `late`;
超过目标的版本使请求失败,而不是静默改绑。基线固定 30 帧无载输入;
可选 `max_gap_s`/`max_window_s` 超限时失败并把原因写进 `zero.failure_reason`。
- **重复、迟到、同时间戳(K4.2)。** 以 `sensor_id + session_id + source_frame_id`
(无源编号时用 `frame_id`)去重,凡是送入引擎的帧都会登记身份。重复帧只产生诊断结果;
旧版本和同时间戳/乱序帧只标记 `late`/`zero_pending`,不推进零点或滤波,也不作废旧零点。
不同源帧时间戳相同不算重复。
- **逐帧结果流(K6.1)。** 每个输入帧都返回一个冻结 `ForceStreamOutput`,携带 T03 帧头与
`reference_generation`;`quality` 为 `ok` / `uncalibrated` / `zero_pending` / `duplicate` /
`late` / `session_changed` / `version_changed` / `window_failed` / `invalid`。
迟到、无效和带质量标记的结果不会被丢弃;拒绝帧仍保留 raw 特征。
- **数值等价与滤波。** 零点/标定换算/读数滤波复用已发布算法;同一输入、无复位、同一标定时
与旧 `ForceSession` 逐帧相等(T06a 覆盖标量曲线、无标定、median+lowpass 滤波器;T06b 覆盖
field 模型与 FN-only 全局增益模型,三类标定都在同一平台同一次运行中对照旧会话)。
- **三类标定与不匹配。** 标量曲线、field、FN-only 各有有效测试;无标定会话保持 raw 与
`uncalibrated` 状态;field 模型缺 `context` 时构造报 `CalibrationError`,基线帧缺 field 或
模型拒绝帧时零点失败并给出 `invalid_input`/原因;FN-only 的 FX/FY 永远保持 NaN +
`uncalibrated`,不冒充三轴标定。
- **输出滤波(T07)。** 设计帧率在构造时固定(`design_sample_rate_hz`),实测帧率漂移和长
间隔只作为 `measured_sample_rate_hz`/`sample_rate_irregular` 诊断,不重新设计链路、不清空
历史。`request_output_settings(settings)` 可从任意线程发出,只在力线程下一帧边界生效:只
重建滤波链并清空其历史,图像参考与已完成的零点不变;相同设置是空操作。每次实际切换发出
一个 `output_filter_changed` StreamEvent(规范化设置、指纹、算法版本、生效帧、设计帧率、
预热信息,可 JSON 往返),由 `drain_events()` 取出。预热帧数为
`max(1, median_window) + 2×lowpass_order(低通开启时)`;结果行提供当前设置身份与预热计数。
全阶段关闭时输出与输入完全相同(含 NaN 与带符号的零)。已发布 `ForceOutputFilter`/旧会话
的自适应调频默认行为不变。
- **线程与只读。** 一个实例只属于一个力侧线程;输入数组只读,输出只含标量/状态/不可变进度。
## 输出滤波示例
```python
from orisys.force_readout import OutputSettings
from orisys.force_stream_session import ForceStreamSession
session = ForceStreamSession(
sensor_family="box",
settings=OutputSettings(sample_rate_hz=30.0, median_window=3, lowpass_hz=5.0),
design_sample_rate_hz=30.0,
)
new_settings = OutputSettings(sample_rate_hz=30.0, median_window=5, lowpass_hz=10.0)
session.request_output_settings(new_settings)
assert session.output_settings.median_window == 3, "switch waits for a frame boundary"
assert session.design_sample_rate_hz == 30.0
```
## SensorStream 入口(T08a/T08b)
`SensorStream` 把 T04–T07 串成一个运行中的单路入口:
- **所有权。** `SensorStream.open(source, ...)` 自己打开 Sensor 并在 `stop()` 关闭;调用方传入
已打开的 Sensor 或外部喂帧时只借用,`stop()` 不关闭借用源。运行中只有一个采集所有者;
流在 `start()` 启用 measurement session,`stop()`/启动失败时恢复借用 Sensor 原来的模式
(原本已启用的保持启用)。
- **实时 latest(T08a)。** `push_frame(...)` 的未领取候选帧会被最新输入覆盖并计入
`overwritten_frames`;已经交给流水线的帧照常完成。`wait_output()` 是 latest-wins 结果流,
`drain_events()` 取会话事件。
- **文件/debug 完整(T08b)。** `realtime=False` 时逐帧处理每个被接受的输入:外部帧进入
有上限的 FIFO(`depth`),计算跟不上时 `push_frame` 阻塞并计入 `backpressure_waits`,不丢帧;
`end_input()` 结束序列,`wait_outputs(n)`/`drain_outputs()` 按发布顺序返回结果。内置文件源由
`SensorStream.open(path, realtime=False, nominal_fps=...)` 打开,同样逐帧处理。文件模式下,
只有在**所有已接受帧都已通过力阶段**(`published >= accepted`)且零点仍未完成时,
`reset_and_zero` 才以 `SensorStreamError`(“input ended before unloaded zero completed”)
结束;生产者刚读到结束标记、管线中还有已接受帧时不会提前判失败,足够的已接受帧仍会正常完成零点。
- **线程/版本。** 复用 `StreamPipeline` 的线程与原子 `request_reset()` 版本;力侧只在 HHD
线程的 `on_frame` 中调用 `ForceStreamSession`,因此 T07 的 `drain_events` 线程约束由门面满足,
没有第二套版本或复位记账。
- **组合归零。** `reset_and_zero(reference_id=...)` 执行“图像复位确认 → 同版本 30 帧无载基线 →
力 ready”;用户不管理版本号。在途旧版本帧不会进入新基线(T06a 的规则)。每次调用只等待
本条命令的结果:力线程把该命令的 `ZeroProgress`/错误写回命令对象并通知,调用方线程不读取
`session.zero`,因此连续两次调用时第二次不会复用第一次的零点。**超时后命令仍在力线程进行**
(仍可能稍后 ready),调用方不应假设它被取消;在下一条命令完成前再次调用会被拒绝。
- **时间来源(T08b)。** 实时相机 `capture_monotonic`,内置采集用 `CapturedFrame.host_mono_ns`
和 `sequence`;Orisys 自己录像(文件内保存信号时间)`recorded_signal`;外部调用方提供可证明
的容器 PTS 才 `video_pts`(`source_pts` 原值保留);内置 OpenCV 普通视频用
`estimated_source`,`capture_time_ns = source_frame_id ÷ nominal_fps`,`CAP_PROP_POS_MSEC`
只作为 `diagnostics` 的 `pos_msec`,不填 `source_pts`。没有时间的输入按帧号推算并标
`estimated_source`。同一 PTS 的不同源帧是两帧;入口按源身份忽略重复/迟到输入并计数。
- **诊断与 field(T08b)。** 结果行的 `diagnostics` 含覆盖/重复/迟到计数、处理延迟、
`long_gap`、`source_dt_s`、`source_time_estimated` 和 `pos_msec`(有则);`source_dt_s` 取同路
上一条实际处理帧的源时间差。`include=("field",)` 让 `StreamPipeline` 在 HHD 线程 `_build`
中复制同帧 field;门面在标定模型需要 field 时自动启用。默认快照不含 field,已发布场不会被
后续帧改写。
```python
import numpy as np
from orisys.sensor import Sensor
from orisys.sensor_stream import SensorStream
sensor = Sensor("external", config_name="box", verbose=False, backend="cpu")
stream = SensorStream(sensor, sensor_id="BOX-001", session_id="run-1")
stream.start()
try:
stream.push_frame(
np.full((365, 365), 100, dtype=np.uint8),
source_frame_id=0,
capture_time_ns=1_000_000_000,
)
output = stream.wait_output(timeout=5.0)
assert output is not None and output.frame.stamp.sensor_id == "BOX-001"
finally:
stream.stop()
```
## 示例
```python
from orisys.force_stream_session import ForceStreamSession
from orisys.image_pipeline import ImageMeasurement
from orisys.stream_contract import FrameStamp, MeasurementFrame
session = ForceStreamSession(sensor_family="box")
session.request_zero(reference_id="run-1", declared_unloaded=True,
expected_reference_generation=0)
frame = MeasurementFrame(
stamp=FrameStamp(sensor_id="BOX-001", session_id="run-1", frame_id=1,
source_frame_id=1, capture_time_ns=33_333_333,
clock_id="host", capture_time_basis="capture_monotonic"),
source_dt_s=None, reference_generation=0,
)
row = session.process(ImageMeasurement(frame=frame, force_raw=(0.0, 0.0, 0.0),
torque=0.0, flow=None))
assert row.frame.reference_generation == 0
assert row.zero.phase in ("resetting", "collecting")
```
## 后续
T06b 已交付标量/field/FN-only 三类标定与旧 ForceSession 的完整数值等价;T07 已交付固定
设计帧率、显式切换事件与历史/直通合同;T08a 已交付 SensorStream 实时入口、所有权与组合归零;
T08b 已交付文件/debug 完整逐帧、有界背压、时间来源/PTS 标注、`include="field"` 与诊断。
spatial_gain/接触映射模型不在 T06/T07 验收范围,需要时另立任务。
下一步进入 T09/T10(App 迁移);删除旧 `ForceSession` 前先把零点、标定、读数引擎从旧类中
抽出来,避免新会话依赖待删除的类型。