朝上判断误用平台轴、接触高度报旋转前数值、多点对准复用缓存姿态,且多点会话被复核入口拒绝;
现按修正计划改正并接通 autocal-multipoint-1.0 只读复核,缺失帧源如实报告而不补零。
离线验证:新回归在 dc5a6f8 上失败、此处通过,panel_check 通过,两截图点接触高度复现
19.0510 / 21.5781 mm;实机项未做,详见 .scratch/autocal-motion-fix/verification.md。
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.basorRTEX.basto 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.
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.)
# 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:
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/ (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.
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.
- 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_TYPEis read immediately, andBus_InitStatuson the first poll. - Download only if the bus type is unknown.
ECAT.bas/RTEX.basare chosen fromconfig/bas/(the vendor requires a non-Chinese path) and written to ROM. A controller that still has the program needs no download. - Wait for
Bus_InitStatus—−1running,1success,0failed — inside the vendor's 25 s window, then a timeout message naming the ZDevelop diagnostic (?*ETHERCAT). - Re-initialise re-runs the program without re-downloading it:
STOPTASK <task>thenRUN "<file>",<task>. The task number isbas_taskinpreference.json(default 4, the task the vendor reference uses); the file name comes from this session's download, or from the rememberedbas_pathwhen the session adopted a running program (the controller does not report the name). The controller answers aRUNfor 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. - 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, 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. |
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 at0and 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_nonereturnsNoneon 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
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/
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:
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) 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.
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
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.
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 (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.
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.