重构 Autocal 采集页:单一队列与控制器驱动的主按钮

选点、对准、加载与录像分散在两个面板,状态靠隐藏相位判断。现改由不依赖 Qt 的采集控制器驱动,并加入录像选项、汇总平铺副本、保持到时自动结束、会话结束回到空闲,以及无设备时的模拟采集。已运行 Autocal 非 Qt 测试并通过范围内门禁;界面测试待 Windows 复核,未做实机验收。
This commit is contained in:
ykcheungab
2026-10-18 16:04:32 +08:00
parent 85d0b396e3
commit 06e06126ca
44 changed files with 7769 additions and 2218 deletions
+31 -32
View File
@@ -26,11 +26,20 @@ Replaces the vendor example program it was ported from:
and RTEX parameters. Each family is enabled only when the controller reports
that bus type.
## Capture workflow
The 标定采集 page is one queue, not two panels — a single point is a queue with one
row. `Ready` lists the six facts a run needs and the **primary button follows the
step**, always stating why it is disabled. Alignment is only a request: the move
goes through the guarded pose-move path, and only a completed move unlocks capture.
Recording options (video, codec, lossless frames) are chosen before the session and
written once into `session.json`; an unavailable codec is disabled with its reason,
never silently substituted. [docs/workflow.md](docs/workflow.md) has the detail.
## Run it
PySide6 only — Qt's LGPLv3 binding, so an internal closed-source tool needs no
licence purchase. (PyQt5 is GPLv3-or-commercial; the studio app moved to PySide6
for the same reason and no longer uses it.)
licence purchase (PyQt5 is GPLv3-or-commercial).
```powershell
# with the environment that already has PySide6 (Anaconda py310, for example)
@@ -50,12 +59,9 @@ resolve to `apps/autocal/` rather than the studio's.
### Controller library
The ZMotion runtime is vendored at `shared/resources/zmotion/x64/` (see the
README beside it for provenance). Point `ORISYS_ZMOTION_DLL_DIR` at another
directory to use a different build — on a motion controller, silently loading
the wrong runtime version is the kind of fault that looks like a working machine
until it moves, so the override is authoritative: if it does not contain
`zauxdll.dll`, the app refuses to connect instead of quietly falling back.
The ZMotion runtime is vendored at `shared/resources/zmotion/x64/`. `ORISYS_ZMOTION_DLL_DIR` overrides it, and the
override is authoritative: a directory without `zauxdll.dll` makes the app refuse
to connect rather than quietly load another build.
One contract in `shared/src/orisys_app_shared/zmotion/client.py` is worth
knowing before touching it: `ZAux_Execute` **must** be given a reply buffer.
@@ -71,8 +77,7 @@ The vendor sequence, with the checks AutoCal's own state machine adds. P1 of
1. **Connect** (`eth` + the controller's IP). On success the app adopts any BAS
program the controller is already running from a previous session instead of
claiming the bus is uninitialised; `BUS_TYPE` is read immediately, and
`Bus_InitStatus` on the first poll.
claiming the bus is uninitialised; `BUS_TYPE` is read immediately.
2. **Download only if the bus type is unknown.** `ECAT.bas` / `RTEX.bas` are
chosen from `config/bas/` (the vendor requires a non-Chinese path) and
written to ROM. A controller that still has the program needs no download.
@@ -97,7 +102,7 @@ The vendor sequence, with the checks AutoCal's own state machine adds. P1 of
| Range | Family | What the operator should do |
|---|---|---|
| `0` | success | — |
| `20000–29999` | PC-library fault (handshake, socket, address) | check the cable/segment and the IP, make sure nothing else holds the connection, then retry. Frequently transient: a first open on a cold route can fail and succeed on the next attempt, which is why an Ethernet connect makes up to 3 attempts before reporting. |
| `20000–29999` | PC-library fault (handshake, socket, address) | check the cable/segment and the IP, then retry; a first open on a cold route is often transient, which is why an Ethernet connect makes up to 3 attempts. |
| `30000+` | the auxiliary block `zauxdll2.h` documents | the message names it (`无应答`, `参数错误`, …) |
| anything else | the controller's own code | look it up in ZDevelop's table |
@@ -158,6 +163,10 @@ apps/autocal/
tests/
```
`capture_controller.py` is the capture page's Qt-free step machine (queue, `Ready`,
one primary action, remeasure/skip/stop, poll); `recording_options.py` turns the
video choice into a `RecordConfig` and probes which codecs this machine can write.
`motion/poller.py` is the only place a controller call can originate. It owns
the DLL handle on its own thread, so the vendor library's blocking calls (a ROM
write, a bus scan) never freeze the window and the ctypes handle is never shared
@@ -177,26 +186,18 @@ python -m pytest apps\autocal\tests # offscreen suite
```
Connection and bus initialisation are used once per machine start-up, so they live
in a **modeless device-setup dialog** (`ui/device_setup.py`) rather than the rail:
the rail carries what is read while working — the axis readout and the collapsed
SDO/RTEX monitor. The dialog opens from the status strip's 设备设置 button, from the
connection/bus/init chips (each opens the tab that explains it) or from
文件 → 设备设置, and the strip keeps reporting the live state whether it is open or
shut.
in a **modeless device-setup dialog** (`ui/device_setup.py`), opened from the status
strip, its chips, or 文件 → 设备设置. The rail carries what is read while working:
the axis readout and the collapsed SDO/RTEX monitor.
`preview_snapshot.py` is the one tool that can photograph the **3D pane**, which
`panel_snapshot.py` cannot: the offscreen platform has no OpenGL context at all, so
the pane is empty in those shots. It needs the real platform plugin and a visible
window, and it writes either the pane alone or the whole page with the pane
composited in. What the view draws, and what each colour means, is in
`preview_snapshot.py` is the only tool that can photograph the **3D pane** (the
offscreen platform has no OpenGL context, so the pane is empty there). It needs
the real platform plugin and a visible window. See
[docs/finger-preview.md](docs/finger-preview.md).
`layout_mockup.py` is a **wireframe, not the application**: it paints the
proposed single-page layout with the shared theme tokens and imports nothing
from the app, so the layout can be reviewed and marked up without building the
panels. It draws the full page plus previews of the other workspace tabs in one
canvas, and `--page setup` draws the device-setup dialog over the page it opens
from.
`layout_mockup.py` is a **wireframe, not the application**: it paints the proposed
single-page layout with the shared theme tokens and imports nothing from the app,
so the layout can be reviewed without building the panels.
`panel_check.py` builds the real window in both themes at 1440×900 and
1920×1080, asserts that no widget escapes its parent, that motion is
@@ -207,10 +208,8 @@ The suite is written for **PySide6**. Where PySide6 is missing, the test modules
are skipped rather than failed: a broken environment should not look like a
broken application. `tests/test_binding_purity.py` enforces the binding policy
statically — no module may import PyQt5/PySide2/PyQt6, no module may use an
unscoped Qt enumerator (removed in PySide6), `shared/` may import PySide6 and no
other binding, and the controller interface must stay importable headless. The
studio app enforces the same rules for itself in
`apps/studio/tests/test_binding_purity.py`.
unscoped Qt enumerator, `shared/` may import PySide6 and no other binding, and
the controller interface must stay importable headless.
## Do not ship this app