Files
orisys-app/apps/autocal/README.md
T
ykcheungab 797235c6a4 加入独立参考力接口与离线恒力反馈骨架
复用法向力投影,区分持续补偿与固定位置松弛记录。189 项定向检查及 lint 通过;扩大检查的失败与限制见架构审查。
2026-10-18 17:18:47 +08:00

223 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
[Offline force-feedback scaffold](docs/force-control-scaffold.zh.md).
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.