146 lines
6.6 KiB
Markdown
146 lines
6.6 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.
|
||
|
||
## 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.)
|
||
|
||
```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/` (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.
|
||
|
||
## 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.
|
||
- **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
|
||
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:
|
||
|
||
```powershell
|
||
python .\apps\autocal\tools\panel_check.py # 120 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\layout_mockup.py --out layout.png # proposed layout, drawn
|
||
python -m pytest apps\autocal\tests # offscreen suite
|
||
```
|
||
|
||
`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.
|
||
|
||
`panel_check.py` builds the real window in both themes at 1280×720, 1600×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.
|