feat: 新增单路流入口 SensorStream(T08a)

SensorStream 串起 Sensor/SensorContext、ImagePipeline 与 ForceStreamSession:实时模式只保留最新候选并计数覆盖;StreamPipeline 负责线程与原子复位版本,力侧只在 HHD 线程的 on_frame 中运行;reset_and_zero 原子领取目标版本,在力线程帧边界发起同版本空载归零,每次调用只等待自己的命令结果;借用的 Sensor 不被关闭,stop 后恢复原来的 measurement session 状态,拥有的 Sensor 在 stop 时关闭。回归 402 项、lint 通过。
This commit is contained in:
ykcheungab
2026-10-21 17:52:24 +08:00
parent 26382b359f
commit d31503330d
3 changed files with 889 additions and 4 deletions
+50 -4
View File
@@ -1,4 +1,4 @@
# 新力侧会话(T06a/T06b、T07 输出滤波)
# 新力侧会话与单路入口(T06a/T06b、T07 输出滤波、T08a)
本文件是单路 SensorStream 计划 T06a/T06b/T07 的 SDK 合同:新的力侧会话直接消费 T05 的
`ImageMeasurement`(T03 `MeasurementFrame` 头 + 同帧 raw/field),参考版本取帧上的
@@ -73,6 +73,52 @@ assert session.output_settings.median_window == 3, "switch waits for a frame bou
assert session.design_sample_rate_hz == 30.0
```
## SensorStream 入口(T08a)
`SensorStream` 把 T04–T07 串成一个运行中的单路入口:
- **所有权。** `SensorStream.open(source, ...)` 自己打开 Sensor 并在 `stop()` 关闭;调用方传入
已打开的 Sensor 或外部喂帧时只借用,`stop()` 不关闭借用源。运行中只有一个采集所有者;
流在 `start()` 启用 measurement session,`stop()`/启动失败时恢复借用 Sensor 原来的模式
(原本已启用的保持启用)。
- **实时 latest。** `push_frame(...)` 的未领取候选帧会被最新输入覆盖并计入
`overwritten_frames`;已经交给流水线的帧照常完成。`wait_output()` 是 latest-wins 结果流,
`drain_events()` 取会话事件。
- **线程/版本。** 复用 `StreamPipeline` 的线程与原子 `request_reset()` 版本;力侧只在 HHD
线程的 `on_frame` 中调用 `ForceStreamSession`,因此 T07 的 `drain_events` 线程约束由门面满足,
没有第二套版本或复位记账。
- **组合归零。** `reset_and_zero(reference_id=...)` 执行“图像复位确认 → 同版本 30 帧无载基线 →
力 ready”;用户不管理版本号。在途旧版本帧不会进入新基线(T06a 的规则)。每次调用只等待
本条命令的结果:力线程把该命令的 `ZeroProgress`/错误写回命令对象并通知,调用方线程不读取
`session.zero`,因此连续两次调用时第二次不会复用第一次的零点。
- **T08a 边界。** 实时模式、无标定/标量会话、逐帧结果最新视图已实现;field/contact 的
`include`、文件/debug 完整逐帧、有界背压、时间来源与 PTS 标注、`source_dt_s`/长间隔诊断和
完整生命周期诊断属于 **T08b**。
- **时间基准(T08b 实现约定)。** 实时相机 `capture_monotonic`;Orisys 自己录像(文件内保存
信号时间)`recorded_signal`;外部调用方提供可证明的容器 PTS 才 `video_pts`;内置 OpenCV
读取普通视频文件用 `estimated_source`,`capture_time_ns` 按 `source_frame_id ÷ 名义帧率` 推算,
POS_MSEC 只进 diagnostics。T08a 的外部帧默认 `capture_monotonic`。
```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
@@ -98,8 +144,8 @@ assert row.zero.phase in ("resetting", "collecting")
## 后续
T06b 已交付标量/field/FN-only 三类标定与旧 ForceSession 的完整数值等价;T07 已交付固定
设计帧率、显式切换事件与历史/直通合同。spatial_gain/接触映射模型不在 T06/T07 验收范围,
需要时另立任务。
T08 接单路流入口;T09/T10 迁移 App 后删除旧 `ForceSession`。
设计帧率、显式切换事件与历史/直通合同;T08a 已交付 SensorStream 实时入口、所有权与组合归零。
spatial_gain/接触映射模型不在 T06/T07 验收范围,需要时另立任务。
T08b 补齐文件/debug 完整逐帧、有界背压、时间来源/PTS 标注与完整诊断后再进入 T09/T10(App 迁移)。
另一项后续任务:删除旧 `ForceSession` 之前,先把零点、标定、读数引擎从旧类中抽出来,
避免新会话依赖待删除的类型(T09 之前)。