采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - What: 1) 选区统计不再挤在曲线下方(中文长句会顶出面板右边界)——框选后三个数值卡的大号数字变成该窗口的峰值、下方小字为该窗口的均值·σ(chart.selection_detail),峰值语义放在卡片 tooltip;曲线下方只保留「选区 4.00s / 121 点」(完整一行留在它的 tooltip 里)。「全程总览」不算选区(那是默认适配),此时卡片回到"游标处样本";切换单位/换数据源/返回最新都会正确切回。2) 上方的 63.0 FPS 是采集循环自己的轮询速率,根因是同一帧被反复当成新样本:CaptureSource.read_latest 返回后台线程的最新帧,没有新帧时把同一帧再给一次,循环未做判断,于是 30 fps 的设备录出约 63 行/秒(frame_id 还各不相同)。新增 _capture_sequence():读 capture_source.status.last_seq,计数未变即"还没有新帧",睡 2 ms 重试、不进处理链、不发 packet;只有带后台读取线程的实时相机有该标记,视频文件是同步解码、每次都取下一帧,因此忽略(行为不变)。packet.fps 改为速率 (窗口帧数-1)/(首末到达差),窗口 1 s——按"1 秒内到达几帧"数数会系统性偏大。容器速率仍取传感器配置值,与此层无关。3) 底部状态栏只留一次性通知:删除右下角常驻「配置 sensor.json」(顶栏摘要按钮已写明),删除与顶栏重复的 status.run_started(运行中(0))与 status.connecting(正在连接…);QStatusBar 换成 _NoticeBar——Qt 的 showMessage(text) 默认 msecs=0 表示永不消失,这正是那句「运行中(0)」一直挂到底部的原因,子类统一补 6 秒超时;status.units_raw_fallback 之前每帧都发(等于永久占住通知行),改为每次运行只提示一次。附带修复:英文下表头模式名被截断(长句移入 tooltip,标签只留模式名 chart.mode_*_tip);数据源切换后曲线下方的选区文案会残留,改为在 _on_source_changed 里清零。 - Why: 用户评审 3 点——选区统计显示不全、顶部 fps 应受硬件限制最大 30、右下角配置文件名与左下角「运行中(0)」和顶栏信息重复。第 2 点查出根因是重复采样(数据缺陷,不只是显示问题),修掉后设备速率与记录行数才真正一致。 - Verification: ui_check.py 546 项 0 失败(新增:选区统计在卡片、卡片小字不裁切、状态栏无常驻标签且消息过期、顶栏 FPS 即 packet 交付速率、表头模式名中英不裁切);test_ui_smoke.py 71 项 0 失败(新增选区读数与通知行过期两例);drive_shared.py 148 项 0 失败(新增 shared/tests/test_worker_frame_rate.py:同一帧返回 3 次时 packet 数≈交付帧数,去掉判据为 11317:39,且 fps 与交付速率一致);locale_studio.py 267 键齐全、zh==en、prefs 对齐;reproduce.py 退出 0;ui_snapshot.py 新增 --select A,B,六张复核图(选区中英、实时、录制中、回放、标定 1280x720)。 - Not in this change: 录制文件格式与 CSV 列序不变;去重只影响"哪些帧算一帧",视频文件源行为不变;曲线下方不再显示三通道峰/均/σ(数值在卡片上,完整一行在 tooltip 里)。
This commit is contained in:
@@ -910,15 +910,21 @@ def main() -> int:
|
||||
not rail_squeezed(window),
|
||||
f"{theme}: the status rail squeezes {rail_squeezed(window)}",
|
||||
)
|
||||
check(
|
||||
"sensor.json" in window.device_label.text(),
|
||||
f"{theme}: the status bar does not name the config file: "
|
||||
f"{window.device_label.text()!r}",
|
||||
)
|
||||
check(
|
||||
not hasattr(window.toolbar, "source_combo"),
|
||||
f"{theme}: the source mode is still a combobox",
|
||||
)
|
||||
# The status bar is a notice line, not a second state line: the config
|
||||
# file and the phase are already on the rail, and a message must expire.
|
||||
check(
|
||||
not hasattr(window, "device_label"),
|
||||
f"{theme}: the status bar still carries a permanent device line",
|
||||
)
|
||||
window.statusBar().showMessage("notice")
|
||||
check(
|
||||
window.statusBar().currentMessage() == "notice",
|
||||
f"{theme}: the status bar does not show a message at all",
|
||||
)
|
||||
|
||||
# The sensor dialog owns the pickers and the calibration facts.
|
||||
window._open_sensor_dialog()
|
||||
@@ -1617,6 +1623,29 @@ def main() -> int:
|
||||
f"the arrow footer does not say what is magnified: "
|
||||
f"{header_window.arrow_panel.scale_label.text()!r}",
|
||||
)
|
||||
# The header's mode label shares its row with three icon buttons and the unit
|
||||
# chip: the long form of the mode sentence was cut mid-word in English.
|
||||
for lang in ("zh_CN", "en_US"):
|
||||
header_window._set_language(lang)
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
for mode, name in ((False, "follow"), (True, "browse")):
|
||||
if mode:
|
||||
header_plots.set_window(0.2, 1.0)
|
||||
app.processEvents()
|
||||
label = header_plots.mode_label
|
||||
check(
|
||||
label.width() >= label.sizeHint().width(),
|
||||
f"{lang}/{name}: the chart mode label is clipped: "
|
||||
f"{label.width()} < {label.sizeHint().width()} ({label.text()!r})",
|
||||
)
|
||||
check(
|
||||
label.toolTip() != "",
|
||||
f"{lang}/{name}: the mode label does not explain itself",
|
||||
)
|
||||
# The locale is process-wide: leave it where the other sections expect it.
|
||||
header_window._set_language("zh_CN")
|
||||
app.processEvents()
|
||||
header_window.close()
|
||||
app.processEvents()
|
||||
|
||||
@@ -1761,6 +1790,25 @@ def main() -> int:
|
||||
finally:
|
||||
app_module.recordings_dir = original_root
|
||||
_shutil.rmtree(scratch, ignore_errors=True)
|
||||
|
||||
# The rail's FPS is the worker's *delivered* frame rate. It used to be a count
|
||||
# of loop iterations (a 30 fps device read 63), so the number the rail prints
|
||||
# must be the packet's, verbatim.
|
||||
window.act_show_fps.setChecked(True)
|
||||
app.processEvents()
|
||||
packet = _fake_packet(0, mono_t=0.0)
|
||||
packet.fps = 30.4
|
||||
stuck.emit(packet)
|
||||
window._on_run_tick()
|
||||
app.processEvents()
|
||||
check(
|
||||
"30.4 FPS" in window.toolbar.source_summary.text(),
|
||||
f"the rail does not show the delivered frame rate: "
|
||||
f"{window.toolbar.source_summary.text()!r}",
|
||||
)
|
||||
window.act_show_fps.setChecked(False)
|
||||
app.processEvents()
|
||||
|
||||
window._stop_run(reason="user_stop")
|
||||
stuck.release()
|
||||
app.processEvents()
|
||||
@@ -2460,9 +2508,16 @@ def main() -> int:
|
||||
all(plot.getViewBox().state["mouseEnabled"][0] for plot in plots.plots),
|
||||
"browsing does not enable panning",
|
||||
)
|
||||
window_texts = plots.stats_label.text()
|
||||
check(window_texts != "", "no selection statistics in browse mode")
|
||||
check("峰" in window_texts, f"the peak is missing from the readout: {window_texts!r}")
|
||||
# "Whole session" is the default fit, not a selection: the cards still state
|
||||
# the sample under the cursor, and the strip carries no selection summary.
|
||||
check(
|
||||
plots._selection is None,
|
||||
"fitted to the whole session, the cards still hold selection statistics",
|
||||
)
|
||||
check(
|
||||
plots.stats_label.text() == "",
|
||||
f"a fitted session shows a selection summary: {plots.stats_label.text()!r}",
|
||||
)
|
||||
|
||||
# Decimation must keep the spike while bounding the point count.
|
||||
drawn = max(int(curve.getData()[0].size) for curve in plots.curves)
|
||||
@@ -2476,6 +2531,50 @@ def main() -> int:
|
||||
plots.overview_curve.getData()[0].size > 0,
|
||||
"the whole-session overview strip is empty",
|
||||
)
|
||||
# A window narrower than the session is a selection: its peak becomes the big
|
||||
# number on each card and its mean/σ the line under it, while the strip keeps
|
||||
# only the size of the selection (the long per-channel line did not fit).
|
||||
check(plots._selection is not None, "zooming in did not record a selection")
|
||||
summary = plots.stats_label.text()
|
||||
check(summary != "", "the strip does not state the size of the selection")
|
||||
check(
|
||||
"峰" not in summary and "均" not in summary,
|
||||
f"the per-channel statistics are still in the strip: {summary!r}",
|
||||
)
|
||||
check(
|
||||
"选区" in summary or "Selection" in summary,
|
||||
f"the strip does not say that the numbers describe a selection: {summary!r}",
|
||||
)
|
||||
expected_peak = float(plots._selection.peaks[0])
|
||||
peak_text = (
|
||||
f"{expected_peak:.3f}" if plots.unit() == "N" else f"{expected_peak:.1f}"
|
||||
)
|
||||
check(
|
||||
plots.value_labels[0].text() == peak_text,
|
||||
f"the FN card is not the selection's peak: {plots.value_labels[0].text()!r}",
|
||||
)
|
||||
check(
|
||||
plots.value_details[0].text() != ""
|
||||
and ("均" in plots.value_details[0].text() or "mean" in plots.value_details[0].text()),
|
||||
f"the card does not state the selection's mean: "
|
||||
f"{plots.value_details[0].text()!r}",
|
||||
)
|
||||
for card, label, detail in zip(
|
||||
plots.value_cards, plots.value_labels, plots.value_details
|
||||
):
|
||||
check(
|
||||
detail.width() >= detail.sizeHint().width(),
|
||||
f"the selection detail line is clipped: "
|
||||
f"{detail.width()} < {detail.sizeHint().width()}",
|
||||
)
|
||||
# Leaving the selection hands the cards back to the cursor readout.
|
||||
plots.follow_latest()
|
||||
check(plots._selection is None, "the selection survived 'back to latest'")
|
||||
check(
|
||||
plots.value_details[0].text() == "",
|
||||
f"the mean line survived leaving the selection: "
|
||||
f"{plots.value_details[0].text()!r}",
|
||||
)
|
||||
|
||||
# A user gesture (the signal pyqtgraph emits for a wheel/drag) enters browse;
|
||||
# "back to latest" must actually leave it.
|
||||
|
||||
Reference in New Issue
Block a user