采集循环只认新帧,选区统计移到数值卡,状态栏改为会过期的通知行 - 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:
@@ -1968,6 +1968,117 @@ def test_a_raw_only_recording_is_never_shown_in_newton(make_window, replay_recor
|
||||
assert panel.value_labels[0].text() not in ("", t("chart.value_unavailable"))
|
||||
|
||||
|
||||
# ------------------------------------------- selection readout and notices
|
||||
|
||||
|
||||
def test_a_selection_moves_its_statistics_onto_the_value_cards(make_window):
|
||||
"""The per-channel line under the charts did not fit; the cards hold it now.
|
||||
|
||||
Selecting a window makes the big number that window's peak and the line under
|
||||
it its mean · σ. The strip keeps only how big the selection is, and leaving
|
||||
the selection puts the cursor readout back.
|
||||
"""
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window, stuck = _running_window(make_window)
|
||||
window.toolbar.units_combo.setCurrentIndex(0) # newton
|
||||
window.force_plots.reset()
|
||||
for index in range(90):
|
||||
stuck.emit(ui_check._fake_packet(index))
|
||||
window._on_run_tick()
|
||||
|
||||
plots = window.force_plots
|
||||
assert plots._selection is None, "following live is not a selection"
|
||||
assert plots.stats_label.text() == ""
|
||||
assert plots.value_details[0].text() == "", "a live reading carries a mean line"
|
||||
|
||||
plots.set_window(1.0, 3.0)
|
||||
assert plots.mode() == "browse"
|
||||
assert plots._selection is not None, "the window was not read as a selection"
|
||||
|
||||
summary = plots.stats_label.text()
|
||||
assert summary, "the strip does not state the size of the selection"
|
||||
assert t("chart.selection_summary", duration="1", samples=2)[:4] in summary or (
|
||||
"选区" in summary or "Selection" in summary
|
||||
), summary
|
||||
# The numbers a reader cannot fit in the strip are on the cards instead.
|
||||
expected_peak = float(plots._selection.peaks[0])
|
||||
expected = f"{expected_peak:.3f}"
|
||||
assert plots.value_labels[0].text() == expected, (
|
||||
f"the FN card is not the selection's peak: {plots.value_labels[0].text()!r}"
|
||||
)
|
||||
detail = plots.value_details[0].text()
|
||||
assert detail and detail != "", detail
|
||||
assert f"{float(plots._selection.means[0]):.4g}" in detail, (detail, plots._selection.means[0])
|
||||
assert "均" in detail or "mean" in detail, detail
|
||||
assert plots.value_labels[0].toolTip(), "the peak is not explained anywhere"
|
||||
|
||||
# "Whole session" is the default fit, so it is not a selection.
|
||||
plots.show_all()
|
||||
assert plots._selection is None, "'whole session' is still treated as a selection"
|
||||
assert plots.value_details[0].text() == ""
|
||||
|
||||
# ...and neither is live mode.
|
||||
plots.set_window(1.0, 3.0)
|
||||
assert plots._selection is not None
|
||||
plots.follow_latest()
|
||||
assert plots._selection is None, "the selection survived 'back to latest'"
|
||||
assert plots.stats_label.text() == ""
|
||||
_finish(window, stuck)
|
||||
|
||||
|
||||
def test_the_status_bar_is_a_notice_line_not_a_state_line(make_window):
|
||||
"""The rail is the state; the bar shows one-off notices and lets them expire.
|
||||
|
||||
A permanent "config sensor.json" line repeated what the rail's sensor chip
|
||||
already says, and a message with no timeout (the old "运行中(0)") stayed under
|
||||
the window for the whole session.
|
||||
"""
|
||||
import time
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
import orisys_studio.app_window as app_module
|
||||
|
||||
window = _show(make_window(), (1440, 880))
|
||||
assert not hasattr(window, "device_label"), (
|
||||
"the status bar still carries a permanent device line"
|
||||
)
|
||||
assert isinstance(window.statusBar(), app_module._NoticeBar)
|
||||
|
||||
# Qt's default is msecs=0, which means "no timeout": a message that never
|
||||
# expires is what left an hour-old "运行中(0)" under the window.
|
||||
window.statusBar().showMessage("plain call")
|
||||
assert window.statusBar().currentMessage() == "plain call"
|
||||
|
||||
original_timeout = app_module._NoticeBar.DEFAULT_TIMEOUT_MS
|
||||
app_module._NoticeBar.DEFAULT_TIMEOUT_MS = 40
|
||||
try:
|
||||
window.statusBar().showMessage("expiring notice")
|
||||
assert window.statusBar().currentMessage() == "expiring notice"
|
||||
deadline = time.perf_counter() + 1.5
|
||||
while time.perf_counter() < deadline:
|
||||
QApplication.processEvents()
|
||||
if window.statusBar().currentMessage() == "":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
assert window.statusBar().currentMessage() == "", (
|
||||
"a notice stayed on the status bar after its timeout"
|
||||
)
|
||||
finally:
|
||||
app_module._NoticeBar.DEFAULT_TIMEOUT_MS = original_timeout
|
||||
|
||||
window._notify("")
|
||||
assert window.statusBar().currentMessage() == ""
|
||||
window._notify("something happened")
|
||||
assert window.statusBar().currentMessage() == "something happened"
|
||||
|
||||
# The rail states the phase, so the bar does not repeat it.
|
||||
window, stuck = _running_window(make_window)
|
||||
assert window.toolbar.status_chip.text().strip(), "the rail lost its phase chip"
|
||||
_finish(window, stuck)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user