autocal: 新增单位与换算页(默认锁定)及轴页报警与事件日志

- 设备设置新增“单位与换算”页:显示各轴 UNITS、读回值、脉冲/mm 换算与核对状态;
  默认锁定,点击“修改”后才可编辑,应用前弹窗确认,旋转轴偏离标定值时额外警告
- 轴调试页:脉冲当量改为只读并提供入口;新增报警与事件日志(报警、使能、
  回零、读取失败、单位不符等)及单位核对摘要
- 新增 motion/unit_report.py、motion/events.py 及对应测试
This commit is contained in:
ykcheungab
2026-10-19 10:15:07 +08:00
parent d477a4d8b7
commit 570daae68f
4 changed files with 45 additions and 23 deletions
+2 -2
View File
@@ -942,8 +942,8 @@
"col_check": "Check", "col_check": "Check",
"kind_linear": "Linear (mm)", "kind_linear": "Linear (mm)",
"kind_rotary": "Rotary (°)", "kind_rotary": "Rotary (°)",
"scale_linear": "1 unit = {mm} mm; {per} units/mm", "scale_linear": "{mm} mm/unit · {per}/mm",
"scale_rotary": "1 unit = {deg}°; {per} units/°", "scale_rotary": "{deg}°/unit · {per}/°",
"status_ok": "Agree", "status_ok": "Agree",
"status_unread": "Not read", "status_unread": "Not read",
"status_mismatch": "Mismatch", "status_mismatch": "Mismatch",
+3 -3
View File
@@ -942,8 +942,8 @@
"col_check": "检查", "col_check": "检查",
"kind_linear": "线性 (mm)", "kind_linear": "线性 (mm)",
"kind_rotary": "旋转 (°)", "kind_rotary": "旋转 (°)",
"scale_linear": "1 单位 = {mm} mm;{per} 单位/mm", "scale_linear": "{mm} mm/单位 · {per}/mm",
"scale_rotary": "1 单位 = {deg}°;{per} 单位/°", "scale_rotary": "{deg}°/单位 · {per}/°",
"status_ok": "一致", "status_ok": "一致",
"status_unread": "未读回", "status_unread": "未读回",
"status_mismatch": "不一致", "status_mismatch": "不一致",
@@ -958,7 +958,7 @@
"summary_ok": "单位检查:{total} 个方向全部一致", "summary_ok": "单位检查:{total} 个方向全部一致",
"summary_bad": "单位检查:{bad}/{total} 个方向需要处理", "summary_bad": "单位检查:{bad}/{total} 个方向需要处理",
"summary_unread": "单位检查:尚未读回(未连接)", "summary_unread": "单位检查:尚未读回(未连接)",
"constants": "固定标定常数(来自实机测量,不能在此修改):线性轴 {ppm} 脉冲/mm;旋转轴 {rotary};以上在 UNITS={cal} 下测得。毫米与角度都是由这些常数换算的显示值,不是新的测量。", "constants": "固定标定常数(实机测得,不能在此修改)\n线性轴:{ppm} 脉冲/mm\n旋转轴:{rotary}\n以上均在 UNITS={cal} 下测得;毫米与角度是由这些常数换算的显示值,不是新的测量。",
"rotary_item": "轴 {axis}:{units} 用户单位 = 90°", "rotary_item": "轴 {axis}:{units} 用户单位 = 90°",
"modify": "修改", "modify": "修改",
"apply": "应用到控制器", "apply": "应用到控制器",
@@ -23,6 +23,7 @@ from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QBrush, QColor from PySide6.QtGui import QBrush, QColor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QHeaderView,
QLabel, QLabel,
QLineEdit, QLineEdit,
QMessageBox, QMessageBox,
@@ -60,7 +61,9 @@ COLUMNS = (
"units.col_check", "units.col_check",
) )
CONFIGURED_COLUMN = 3 CONFIGURED_COLUMN = 3
ROW_HEIGHT = 34 STRETCH_COLUMNS = (0, 5, 6)
ROW_HEIGHT = 40
EDIT_HEIGHT = 26
_ROLE_KEYS = { _ROLE_KEYS = {
"x": "setup.role_x", "x": "setup.role_x",
@@ -116,7 +119,7 @@ class UnitsPage(QWidget):
column = QVBoxLayout(self) column = QVBoxLayout(self)
column.setContentsMargins(8, 8, 8, 8) column.setContentsMargins(8, 8, 8, 8)
column.setSpacing(8) column.setSpacing(10)
self.hint_label = QLabel(self) self.hint_label = QLabel(self)
self.hint_label.setObjectName("hintLabel") self.hint_label.setObjectName("hintLabel")
@@ -153,10 +156,23 @@ class UnitsPage(QWidget):
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.setAlternatingRowColors(True) self.table.setAlternatingRowColors(True)
self.table.verticalHeader().setDefaultSectionSize(ROW_HEIGHT) self.table.verticalHeader().setDefaultSectionSize(ROW_HEIGHT)
self.table.horizontalHeader().setStretchLastSection(True) self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
for index, width in enumerate((110, 60, 90, 110, 100, 230)): self.table.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.table.setColumnWidth(index, width) self.table.setWordWrap(False)
self.table.setMinimumHeight(40 + ROW_HEIGHT * 5 + 4) self.table.setTextElideMode(Qt.TextElideMode.ElideRight)
header = self.table.horizontalHeader()
header.setMinimumSectionSize(48)
header.setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
# Short columns size to their text; the wide ones share what is left, so the
# table always fits the dialog and never needs a scroll bar.
for index in range(len(COLUMNS)):
header.setSectionResizeMode(
index,
QHeaderView.ResizeMode.Stretch
if index in STRETCH_COLUMNS
else QHeaderView.ResizeMode.ResizeToContents,
)
self.table.setFixedHeight(header.height() + ROW_HEIGHT * 5 + 6)
for row in range(5): for row in range(5):
for col in range(len(COLUMNS)): for col in range(len(COLUMNS)):
if col != CONFIGURED_COLUMN: if col != CONFIGURED_COLUMN:
@@ -243,7 +259,13 @@ class UnitsPage(QWidget):
edit = QLineEdit(self.table) edit = QLineEdit(self.table)
edit.setObjectName(f"unitsEdit_{row.role}") edit.setObjectName(f"unitsEdit_{row.role}")
edit.setAlignment(Qt.AlignmentFlag.AlignCenter) edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
edit.setFixedHeight(28) edit.setFixedHeight(EDIT_HEIGHT)
edit.setMinimumWidth(84)
# The global style gives line edits generous padding; inside a table cell
# that clips the text, so the cell edit is compact.
edit.setStyleSheet(
"QLineEdit { padding: 0 6px; margin: 0; min-height: 0; border-radius: 4px; }"
)
self._edits[row.role] = edit self._edits[row.role] = edit
self.table.setCellWidget(index, CONFIGURED_COLUMN, edit) self.table.setCellWidget(index, CONFIGURED_COLUMN, edit)
edit.setReadOnly(self._locked) edit.setReadOnly(self._locked)
+11 -11
View File
@@ -53,7 +53,7 @@ def test_the_page_is_locked_until_the_operator_presses_modify(make_window):
page = window.device_setup.units_page page = window.device_setup.units_page
page.set_rows(_rows(connected=False), False) page.set_rows(_rows(connected=False), False)
assert page.locked assert page.locked
assert all(edit.isReadOnly() for edit in page._edits.values()) # noqa: SLF001 assert all(edit.isReadOnly() for edit in page._edits.values())
# Nothing to change on a machine that is not connected. # Nothing to change on a machine that is not connected.
assert not page.modify_button.isEnabled() assert not page.modify_button.isEnabled()
@@ -61,7 +61,7 @@ def test_the_page_is_locked_until_the_operator_presses_modify(make_window):
assert page.modify_button.isEnabled() assert page.modify_button.isEnabled()
page.modify_button.click() page.modify_button.click()
assert not page.locked assert not page.locked
assert not any(edit.isReadOnly() for edit in page._edits.values()) # noqa: SLF001 assert not any(edit.isReadOnly() for edit in page._edits.values())
assert page.apply_button.isEnabled() assert page.apply_button.isEnabled()
window.close() window.close()
@@ -75,7 +75,7 @@ def test_a_confirmed_change_is_emitted_and_the_page_locks_again(make_window, mon
asked = _answer(monkeypatch, QMessageBox.StandardButton.Yes) asked = _answer(monkeypatch, QMessageBox.StandardButton.Yes)
page.modify_button.click() page.modify_button.click()
page._edits["x"].setText("5") # noqa: SLF001 page._edits["x"].setText("5")
page.apply_button.click() page.apply_button.click()
assert emitted == [{0: 5.0}] assert emitted == [{0: 5.0}]
@@ -93,14 +93,14 @@ def test_declining_the_confirmation_changes_nothing(make_window, monkeypatch):
_answer(monkeypatch, QMessageBox.StandardButton.No) _answer(monkeypatch, QMessageBox.StandardButton.No)
page.modify_button.click() page.modify_button.click()
page._edits["y"].setText("20") # noqa: SLF001 page._edits["y"].setText("20")
page.apply_button.click() page.apply_button.click()
assert emitted == [] assert emitted == []
assert not page.locked # still the operator's to finish or cancel assert not page.locked # still the operator's to finish or cancel
page.cancel_button.click() page.cancel_button.click()
assert page.locked assert page.locked
assert page._edits["y"].text() == "10" # noqa: SLF001 assert page._edits["y"].text() == "10"
window.close() window.close()
@@ -111,7 +111,7 @@ def test_a_rotary_value_off_the_calibration_is_warned_about(make_window, monkeyp
asked = _answer(monkeypatch, QMessageBox.StandardButton.No) asked = _answer(monkeypatch, QMessageBox.StandardButton.No)
page.modify_button.click() page.modify_button.click()
page._edits["yaw"].setText("5") # noqa: SLF001 page._edits["yaw"].setText("5")
page.apply_button.click() page.apply_button.click()
assert t("units.confirm_rotary", roles=t("setup.role_yaw"), cal="10") in asked[0] assert t("units.confirm_rotary", roles=t("setup.role_yaw"), cal="10") in asked[0]
@@ -128,7 +128,7 @@ def test_an_unusable_value_is_refused_before_any_question(make_window, monkeypat
asked = _answer(monkeypatch, QMessageBox.StandardButton.Yes) asked = _answer(monkeypatch, QMessageBox.StandardButton.Yes)
page.modify_button.click() page.modify_button.click()
page._edits["z"].setText(bad) # noqa: SLF001 page._edits["z"].setText(bad)
page.apply_button.click() page.apply_button.click()
assert emitted == [] and asked == [] assert emitted == [] and asked == []
@@ -157,13 +157,13 @@ def test_a_changed_axis_mapping_drops_an_open_edit(make_window):
page = window.device_setup.units_page page = window.device_setup.units_page
page.set_rows(_rows(), True) page.set_rows(_rows(), True)
page.modify_button.click() page.modify_button.click()
page._edits["x"].setText("5") # noqa: SLF001 page._edits["x"].setText("5")
swapped = dict(ROLES, x=1, y=0) swapped = dict(ROLES, x=1, y=0)
page.set_rows(build_unit_rows(swapped, lambda axis: 10.0, _snapshot()), True) page.set_rows(build_unit_rows(swapped, lambda axis: 10.0, _snapshot()), True)
assert page.locked assert page.locked
assert page._edits["x"].text() == "10" # noqa: SLF001 assert page._edits["x"].text() == "10"
window.close() window.close()
@@ -205,9 +205,9 @@ def test_the_axis_page_logs_alarms_and_can_clear_the_log(make_window):
def test_a_unit_mismatch_reaches_the_axis_page_summary(make_window): def test_a_unit_mismatch_reaches_the_axis_page_summary(make_window):
window = make_window() window = make_window()
window.axis_panel.update_snapshot(_snapshot(units=5.0)) window.axis_panel.update_snapshot(_snapshot(units=5.0))
window._refresh_unit_report() # noqa: SLF001 - the window does this on every poll window._refresh_unit_report()
assert t("units.summary_bad", bad=5, total=5) == window.axis_panel.units_chip.text() assert t("units.summary_bad", bad=5, total=5) == window.axis_panel.units_chip.text()
window.axis_panel.update_snapshot(_snapshot(units=10.0)) window.axis_panel.update_snapshot(_snapshot(units=10.0))
window._refresh_unit_report() # noqa: SLF001 window._refresh_unit_report()
assert t("units.summary_ok", total=5) == window.axis_panel.units_chip.text() assert t("units.summary_ok", total=5) == window.axis_panel.units_chip.text()
window.close() window.close()