06e06126ca
选点、对准、加载与录像分散在两个面板,状态靠隐藏相位判断。现改由不依赖 Qt 的采集控制器驱动,并加入录像选项、汇总平铺副本、保持到时自动结束、会话结束回到空闲,以及无设备时的模拟采集。已运行 Autocal 非 Qt 测试并通过范围内门禁;界面测试待 Windows 复核,未做实机验收。
221 lines
11 KiB
Markdown
221 lines
11 KiB
Markdown
# Orisys AutoCal
|
||
|
||
Internal control application for the **5-axis calibration platform**, driving a
|
||
ZMotion ZMC bus controller (EtherCAT or RTEX) from the PC.
|
||
|
||
Replaces the vendor example program it was ported from:
|
||
|
||
D:\orisys_ws\zmotion\Python64\例程9-总线控制运动
|
||
|
||
> **Internal use only.** AutoCal is not part of the customer installer. See
|
||
> "Do not ship this app" below.
|
||
|
||
## What it does
|
||
|
||
- **Connect** over Ethernet (with an IP scan), serial, PCI, local/simulated, or
|
||
a built-in **demo** transport that needs no hardware and no DLL.
|
||
- **Initialise the bus** by downloading `ECAT.bas` or `RTEX.bas` to the
|
||
controller, then follow the controller's own state machine to completion — or
|
||
to a timeout, with the vendor's diagnostic hint.
|
||
- **Read axis state** for all five drives at once: enable, axis type, pulse
|
||
equivalent, speed, acceleration, commanded and measured position (in user
|
||
units *and* millimetres), axis status word, motion and homing state.
|
||
- **Move**: enable/disable, jog, absolute and relative moves, stop, stop-all,
|
||
driver homing, clear a drive alarm. Every command is guarded — see below.
|
||
- **Exchange raw bus data**: EtherCAT object-dictionary (SDO) reads and writes,
|
||
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).
|
||
|
||
```powershell
|
||
# with the environment that already has PySide6 (Anaconda py310, for example)
|
||
$env:PYTHONPATH = "D:\orisys_ws\orisys-app\shared\src;D:\orisys_ws\orisys-app\apps\autocal\src"
|
||
python D:\orisys_ws\orisys-app\apps\autocal\run.py
|
||
```
|
||
|
||
Or with no controller attached at all:
|
||
|
||
```powershell
|
||
python .\apps\autocal\run.py --demo
|
||
```
|
||
|
||
`run.py` sets `ORISYS_APP_ID=autocal` before importing anything from
|
||
`orisys_app_shared`, so this app's `config/`, `preference.json` and `outputs/`
|
||
resolve to `apps/autocal/` rather than the studio's.
|
||
|
||
### Controller library
|
||
|
||
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.
|
||
Passing `NULL` with a zero length is a write through a null pointer inside the
|
||
DLL — a native crash the worker thread cannot catch — and the buffer is also
|
||
how a refused online command explains itself, because `ZAux_Execute` returns 0
|
||
for a `RUN` whose file the controller does not have.
|
||
|
||
## Bring-up sequence
|
||
|
||
The vendor sequence, with the checks AutoCal's own state machine adds. P1 of
|
||
`docs/workflow.md` is this list in the workflow's vocabulary.
|
||
|
||
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.
|
||
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.
|
||
3. **Wait for `Bus_InitStatus`** — `−1` running, `1` success, `0` failed —
|
||
inside the vendor's 25 s window, then a timeout message naming the ZDevelop
|
||
diagnostic (`?*ETHERCAT`).
|
||
4. **Re-initialise** re-runs the program without re-downloading it:
|
||
`STOPTASK <task>` then `RUN "<file>",<task>`. The task number is
|
||
`bas_task` in `preference.json` (default **4**, the task the vendor
|
||
reference uses); the file name comes from this session's download, or from the
|
||
remembered `bas_path` when the session adopted a running program (the
|
||
controller does not report the name). The controller answers a `RUN` for a
|
||
file it does not have with **code 0** and the refusal in its reply text, so
|
||
that text is checked and shown rather than reported as an initialisation in
|
||
progress.
|
||
5. **Enable, then jog.** Motion stays refused until step 3 reports success.
|
||
|
||
### Connection and error codes
|
||
|
||
`ZAux_*` returns two unrelated families of code, and they need different answers:
|
||
|
||
| Range | Family | What the operator should do |
|
||
|---|---|---|
|
||
| `0` | success | — |
|
||
| `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 |
|
||
|
||
A **host-library** code is never sent to ZDevelop's table, because that table
|
||
cannot contain it. Observed at the bench: `20008` from `ZAux_OpenEth` on a first,
|
||
transient attempt that succeeded immediately afterwards.
|
||
|
||
## Safety model
|
||
|
||
The guards live in `motion/service.py`, not in the UI, so they hold whatever
|
||
calls them:
|
||
|
||
- **Motion is refused** while the bus is not initialised, while the axis is
|
||
unknown, or on a non-finite target — with a reason, never a silent no-op.
|
||
- **A pose move is refused before the first axis moves** if any part of it is wrong:
|
||
a target past the P0 limit — millimetres for x/y/z, **degrees** for yaw/pitch, so a
|
||
400° stage turn cannot pass a 100 mm check — an axis mapping that is incomplete,
|
||
points outside the drive range, or claims one drive twice, or a rotary role on an
|
||
axis with no user-units-per-degree calibration. A sequence that stopped halfway
|
||
would leave the machine in a pose nobody simulated, so every check runs first.
|
||
- **A move is stepped, not looped.** The drives are commanded one at a time from the
|
||
poll snapshots; a step that stalls (idle but short of its target), times out or
|
||
raises an alarm aborts the rest. That is also what makes **stop** work — it is an
|
||
ordinary command, so it runs at the next poll tick, and the queue is cancelled
|
||
before it is sent so no later axis is commanded.
|
||
- **Homing needs a configured mode.** The vendor example pre-fills `17`, which
|
||
is one specific drive's mode; AutoCal starts at `0` and refuses, so the
|
||
operator chooses for the drive in front of them instead of inheriting a number
|
||
from another machine.
|
||
- **Homing is refused on RTEX**, as the vendor example itself does.
|
||
- **Disconnect stops the axes first** (the vendor program closes the handle and
|
||
leaves the platform coasting).
|
||
- **SDO/RTEX are gated by the reported bus type**, so a write can never be aimed
|
||
at the wrong family.
|
||
- **A failed read is marked, not zeroed.** `ZmotionValue.value_or_none` returns
|
||
`None` on failure; nothing silently treats a failed read's zero as a position.
|
||
|
||
The displayed millimetre value is *derived* from the pulse equivalent
|
||
(bench-measured: `units=10` → 1000 user units = 2 mm). It is a display value,
|
||
not a calibrated length, and the UI says so.
|
||
|
||
## Layout
|
||
|
||
```text
|
||
apps/autocal/
|
||
run.py # source-tree launcher
|
||
config/
|
||
preferences.default.json # shipped defaults
|
||
locales/{zh_CN,en_US}.json
|
||
bas/{ECAT,RTEX}.bas # copies: the vendor needs a non-Chinese path
|
||
src/orisys_autocal/
|
||
main.py # logging, locale, theme, window
|
||
preferences.py # AutoCal's own preference schema
|
||
motion/ # Qt-free except poller.py (the Qt worker bridge)
|
||
models.py errors.py units.py service.py transport.py poller.py
|
||
ui/ # window.py + one module per panel
|
||
tools/ # panel_check.py, panel_snapshot.py, preview_snapshot.py, layout_mockup.py
|
||
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
|
||
between threads. The UI talks to it through signals only.
|
||
|
||
## Verify
|
||
|
||
Both work with no display, no controller and no DLL:
|
||
|
||
```powershell
|
||
python .\apps\autocal\tools\panel_check.py # layout/theme/locale/gating checks
|
||
python .\apps\autocal\tools\panel_snapshot.py --demo --theme dark --out shot.png
|
||
python .\apps\autocal\tools\panel_snapshot.py --compare --out tabs.png # one image per tab
|
||
python .\apps\autocal\tools\panel_snapshot.py --setup-dialog bus --out setup.png
|
||
python .\apps\autocal\tools\layout_mockup.py --out layout.png # proposed layout, drawn
|
||
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`), 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 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 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
|
||
unreachable before the bus is initialised, that the SDO/RTEX panels follow the
|
||
reported bus family, and then runs a full simulated session from `ECAT.bas`.
|
||
|
||
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, `shared/` may import PySide6 and no other binding, and
|
||
the controller interface must stay importable headless.
|
||
|
||
## Do not ship this app
|
||
|
||
AutoCal is an internal tool. It is deliberately **not** packaged by
|
||
`build/studio`, whose PyInstaller spec must not learn about the ZMotion runtime
|
||
DLLs, and it is not in the customer installer. The ZMotion SDK is vendor-supplied
|
||
hardware support software with its own terms; do not redistribute those DLLs
|
||
with a customer product.
|