autocal layout first design
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""Typed, side-effect-free client for the ZMotion ZMC controller DLLs.
|
||||
|
||||
Only the calls the AutoCal platform control needs are wrapped here, rather than
|
||||
copying the vendor's 5381-line ``zmcdll/zauxdllPython.py``. The vendor wrapper
|
||||
is transcribed method by method -- its Chinese docstrings are kept because they
|
||||
document the parameter contract (the English reference for the same calls would
|
||||
be the ``ZAUXDLL``/``ZAux_Direct_*`` API documentation).
|
||||
|
||||
Differences from the vendor file that matter:
|
||||
|
||||
* **No import side effects.** Importing this package does not probe, load, or
|
||||
``print()`` anything about the native library. The DLL is resolved and loaded
|
||||
on first :func:`load_dll`, so a machine with no controller still imports the
|
||||
whole app.
|
||||
* **No path guessing at import time.** See :mod:`.loader`.
|
||||
* **Errors are values, not tuples.** Every wrapper returns a
|
||||
:class:`ZmotionResult` (or :class:`ZmotionValue` for reads), so a caller can
|
||||
never silently treat a failed read's zero as data.
|
||||
|
||||
No Qt binding is imported here, deliberately: this module is usable from a
|
||||
headless test process and from either binding.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .client import (
|
||||
ZAuxError,
|
||||
ZmotionClient,
|
||||
ZmotionDllNotFound,
|
||||
ZmotionResult,
|
||||
ZmotionValue,
|
||||
)
|
||||
from .loader import ZAUXDLL_DLL, ZMOTION_DLL, load_dll, resolve_dll_dir
|
||||
|
||||
__all__ = [
|
||||
"ZAUXDLL_DLL",
|
||||
"ZMOTION_DLL",
|
||||
"ZAuxError",
|
||||
"ZmotionClient",
|
||||
"ZmotionDllNotFound",
|
||||
"ZmotionResult",
|
||||
"ZmotionValue",
|
||||
"load_dll",
|
||||
"resolve_dll_dir",
|
||||
]
|
||||
@@ -0,0 +1,468 @@
|
||||
"""Typed wrappers around the ZMotion ``ZAux_*`` controller API.
|
||||
|
||||
Every wrapper returns :class:`ZmotionResult` (commands) or :class:`ZmotionValue`
|
||||
(reads) instead of the vendor's ``(code, ctypes_value)`` pairs, so a failed read
|
||||
can never be mistaken for data. The ~25 calls here are exactly the ones the
|
||||
AutoCal platform control uses; see ``ZAUXDLL2.h``
|
||||
(``shared/resources/zmotion/x64/``) for the underlying contract.
|
||||
|
||||
Loading is lazy: constructing :class:`ZmotionClient` does not touch the native
|
||||
library, so importing the app on a machine with no controller (or no DLL at all)
|
||||
is always safe. Call :meth:`ZmotionClient.load` explicitly, or let
|
||||
:meth:`ZmotionClient.connect` do it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
from dataclasses import dataclass
|
||||
from typing import Any # noqa: F401 (kept for ZmotionValue.value's annotation)
|
||||
|
||||
from .loader import ZmotionDllNotFound, load_dll, resolve_dll_dir
|
||||
|
||||
__all__ = [
|
||||
"ZAuxError",
|
||||
"ZmotionClient",
|
||||
"ZmotionDllNotFound",
|
||||
"ZmotionResult",
|
||||
"ZmotionValue",
|
||||
]
|
||||
|
||||
|
||||
class ZAuxError(RuntimeError):
|
||||
"""A ``ZAux_*`` call returned a non-zero code.
|
||||
|
||||
Carries the numeric code so callers can map it to a localised message
|
||||
without re-parsing a string.
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, detail: str = "") -> None:
|
||||
self.code = int(code)
|
||||
self.detail = detail
|
||||
message = f"ZMotion error {self.code}"
|
||||
if detail:
|
||||
message = f"{message}: {detail}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZmotionResult:
|
||||
"""Outcome of a command that returns only a status code."""
|
||||
|
||||
code: int
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.code == 0
|
||||
|
||||
def raise_for_status(self, detail: str = "") -> "ZmotionResult":
|
||||
if not self.ok:
|
||||
raise ZAuxError(self.code, detail)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZmotionValue:
|
||||
"""Outcome of a read: a status code plus the value it produced."""
|
||||
|
||||
code: int
|
||||
value: Any
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return self.code == 0
|
||||
|
||||
@property
|
||||
def value_or_none(self) -> Any | None:
|
||||
"""The value when the read succeeded, otherwise ``None``.
|
||||
|
||||
Callers that want a default must say so themselves; silently using a
|
||||
failed read's zero is what makes a wrong number look like a
|
||||
measurement.
|
||||
"""
|
||||
return self.value if self.ok else None
|
||||
|
||||
def raise_for_status(self, detail: str = "") -> "ZmotionValue":
|
||||
if not self.ok:
|
||||
raise ZAuxError(self.code, detail)
|
||||
return self
|
||||
|
||||
|
||||
class ZmotionClient:
|
||||
"""The controller connection, one instance per process.
|
||||
|
||||
Not thread-safe: the underlying DLL handle is shared state. AutoCal owns one
|
||||
client inside a single poller thread (see
|
||||
``orisys_autocal.motion.poller``) and never calls it from two threads.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._dll: ctypes.CDLL | None = None
|
||||
self._handle = ctypes.c_void_p()
|
||||
self._dll_dir: str | None = None
|
||||
|
||||
# ------------------------------------------------------------------ state
|
||||
|
||||
@property
|
||||
def loaded(self) -> bool:
|
||||
return self._dll is not None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self._handle.value is not None
|
||||
|
||||
@property
|
||||
def dll_dir(self) -> str | None:
|
||||
"""Directory the runtime was loaded from, for logs and About boxes."""
|
||||
return self._dll_dir
|
||||
|
||||
def load(self) -> "ZmotionClient":
|
||||
"""Resolve and load the native runtime. Idempotent."""
|
||||
if self._dll is None:
|
||||
resolved = resolve_dll_dir()
|
||||
self._dll = load_dll(resolved)
|
||||
self._dll_dir = str(resolved)
|
||||
return self
|
||||
|
||||
def _require_handle(self) -> ctypes.c_void_p:
|
||||
if self._handle.value is None:
|
||||
raise ZAuxError(-2, "no controller connection is open")
|
||||
return self._handle
|
||||
|
||||
# ------------------------------------------------------------ connection
|
||||
|
||||
def open_eth(self, ip: str) -> ZmotionResult:
|
||||
"""Connect to a controller by IPv4 address (string form)."""
|
||||
self.load()
|
||||
self._handle = ctypes.c_void_p()
|
||||
ret = self._dll.ZAux_OpenEth( # type: ignore[union-attr]
|
||||
ip.encode("utf-8"), ctypes.pointer(self._handle)
|
||||
)
|
||||
if ret != 0:
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def open_com(self, com_id: int) -> ZmotionResult:
|
||||
"""Connect over a serial port by index."""
|
||||
self.load()
|
||||
self._handle = ctypes.c_void_p()
|
||||
ret = self._dll.ZAux_OpenCom( # type: ignore[union-attr]
|
||||
ctypes.c_int(com_id), ctypes.pointer(self._handle)
|
||||
)
|
||||
if ret != 0:
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def open_pci(self, card_num: int) -> ZmotionResult:
|
||||
"""Connect to a PCI/PCIe card by card index."""
|
||||
self.load()
|
||||
self._handle = ctypes.c_void_p()
|
||||
ret = self._dll.ZAux_OpenPci( # type: ignore[union-attr]
|
||||
ctypes.c_int(card_num), ctypes.pointer(self._handle)
|
||||
)
|
||||
if ret != 0:
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def fast_open(self, conn_type: int, connect_string: str, timeout_ms: int) -> ZmotionResult:
|
||||
"""Open a local/simulated controller (``ZAux_FastOpen``)."""
|
||||
self.load()
|
||||
self._handle = ctypes.c_void_p()
|
||||
ret = self._dll.ZAux_FastOpen( # type: ignore[union-attr]
|
||||
ctypes.c_int(conn_type),
|
||||
connect_string.encode("utf-8"),
|
||||
ctypes.c_uint32(timeout_ms),
|
||||
ctypes.pointer(self._handle),
|
||||
)
|
||||
if ret != 0:
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def close(self) -> ZmotionResult:
|
||||
"""Close the connection. Safe to call when nothing is open."""
|
||||
if self._dll is None or self._handle.value is None:
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(0)
|
||||
ret = self._dll.ZAux_Close(self._handle) # type: ignore[union-attr]
|
||||
self._handle = ctypes.c_void_p()
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def search_eth(self, buffer_length: int = 10240, timeout_ms: int = 200) -> ZmotionValue:
|
||||
"""Scan the local network segment for controllers.
|
||||
|
||||
Returns the whitespace-separated address list as text. Does not need an
|
||||
open connection.
|
||||
"""
|
||||
self.load()
|
||||
buffer = (ctypes.c_char * int(buffer_length))()
|
||||
ret = self._dll.ZAux_SearchEthlist( # type: ignore[union-attr]
|
||||
buffer, ctypes.c_uint32(buffer_length), ctypes.c_uint32(timeout_ms)
|
||||
)
|
||||
text = buffer.value.decode("utf-8", errors="ignore") if buffer.value else ""
|
||||
return ZmotionValue(int(ret), text.strip())
|
||||
|
||||
# ----------------------------------------------------------- BAS / command
|
||||
|
||||
def bas_download(self, filename: str, run_mode: int = 1) -> ZmotionResult:
|
||||
"""Download a ``.bas`` program to the controller ROM.
|
||||
|
||||
``run_mode=1`` downloads to ROM, which makes the controller run the
|
||||
program's task after the configured delay (see ``ECAT.bas``).
|
||||
"""
|
||||
ret = self._dll.ZAux_BasDown( # type: ignore[union-attr]
|
||||
self._require_handle(), filename.encode("utf-8"), ctypes.c_uint32(run_mode)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def execute(self, command: str) -> ZmotionValue:
|
||||
"""Run an online command string (e.g. ``RUN "ECAT.bas",4``)."""
|
||||
ret = self._dll.ZAux_Execute( # type: ignore[union-attr]
|
||||
self._require_handle(),
|
||||
command.encode("utf-8"),
|
||||
None,
|
||||
ctypes.c_uint32(0),
|
||||
)
|
||||
return ZmotionValue(int(ret), command)
|
||||
|
||||
def get_user_var(self, name: str) -> ZmotionValue:
|
||||
"""Read a global variable from the running ``.bas`` program.
|
||||
|
||||
The vendor wrapper returns a ``c_float`` here while other reads return a
|
||||
plain float; both are normalised to a Python float.
|
||||
"""
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetUserVar( # type: ignore[union-attr]
|
||||
self._require_handle(), name.encode("utf-8"), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
# --------------------------------------------------------------- bus state
|
||||
|
||||
def bus_node_count(self, slot: int = 0) -> ZmotionValue:
|
||||
"""Number of slave nodes the controller found on the bus."""
|
||||
value = ctypes.c_uint32()
|
||||
ret = self._dll.ZAux_BusCmd_GetNodeNum( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(slot), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def bus_home_status(self, axis: int) -> ZmotionValue:
|
||||
"""Driver homing status: 1 once homing has completed."""
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_BusCmd_GetHomeStatus( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def bus_datum(self, axis: int, home_mode: int) -> ZmotionResult:
|
||||
"""Start driver homing with a CiA-402 homing method."""
|
||||
ret = self._dll.ZAux_BusCmd_Datum( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_int(home_mode)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def bus_set_datum_offset(self, axis: int, value: float) -> ZmotionResult:
|
||||
"""Set the homing offset (machine units)."""
|
||||
ret = self._dll.ZAux_BusCmd_SetDatumOffpos( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def bus_drive_clear(self, axis: int, mode: int = 0) -> ZmotionResult:
|
||||
"""Clear a drive fault/alert."""
|
||||
ret = self._dll.ZAux_BusCmd_DriveClear( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_int(mode)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def sdo_read(
|
||||
self, slot: int, node: int, index: int, subindex: int, data_type: int
|
||||
) -> ZmotionValue:
|
||||
"""Read an EtherCAT object-dictionary entry.
|
||||
|
||||
``data_type``: 1=int8, 2=int16, 3=int32, 4=uint8, 5=uint16, 6=uint32.
|
||||
"""
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_BusCmd_SDORead( # type: ignore[union-attr]
|
||||
self._require_handle(),
|
||||
ctypes.c_int(slot),
|
||||
ctypes.c_int(node),
|
||||
ctypes.c_int(index),
|
||||
ctypes.c_int(subindex),
|
||||
ctypes.c_int(data_type),
|
||||
ctypes.byref(value),
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def sdo_write(
|
||||
self,
|
||||
slot: int,
|
||||
node: int,
|
||||
index: int,
|
||||
subindex: int,
|
||||
data_type: int,
|
||||
value: int,
|
||||
) -> ZmotionResult:
|
||||
"""Write an EtherCAT object-dictionary entry. See :meth:`sdo_read`."""
|
||||
ret = self._dll.ZAux_BusCmd_SDOWrite( # type: ignore[union-attr]
|
||||
self._require_handle(),
|
||||
ctypes.c_int(slot),
|
||||
ctypes.c_int(node),
|
||||
ctypes.c_int(index),
|
||||
ctypes.c_int(subindex),
|
||||
ctypes.c_int(data_type),
|
||||
ctypes.c_int(value),
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def rtex_read(self, axis: int, parameter: int) -> ZmotionValue:
|
||||
"""Read an RTEX drive parameter."""
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_BusCmd_RtexRead( # type: ignore[union-attr]
|
||||
self._require_handle(),
|
||||
ctypes.c_int(axis),
|
||||
ctypes.c_int(parameter),
|
||||
ctypes.byref(value),
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def rtex_write(self, axis: int, parameter: int, value: float) -> ZmotionResult:
|
||||
"""Write an RTEX drive parameter."""
|
||||
ret = self._dll.ZAux_BusCmd_RtexWrite( # type: ignore[union-attr]
|
||||
self._require_handle(),
|
||||
ctypes.c_int(axis),
|
||||
ctypes.c_int(parameter),
|
||||
ctypes.c_float(value),
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
# ------------------------------------------------------------------- axis
|
||||
|
||||
def set_axis_enable(self, axis: int, value: int) -> ZmotionResult:
|
||||
"""Servo enable (1) / disable (0) for one axis."""
|
||||
ret = self._dll.ZAux_Direct_SetAxisEnable( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_int(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def get_axis_enable(self, axis: int) -> ZmotionValue:
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_Direct_GetAxisEnable( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def set_units(self, axis: int, value: float) -> ZmotionResult:
|
||||
"""Pulse equivalent (user units per pulse)."""
|
||||
ret = self._dll.ZAux_Direct_SetUnits( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def get_units(self, axis: int) -> ZmotionValue:
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetUnits( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def set_speed(self, axis: int, value: float) -> ZmotionResult:
|
||||
ret = self._dll.ZAux_Direct_SetSpeed( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def get_speed(self, axis: int) -> ZmotionValue:
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetSpeed( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def set_accel(self, axis: int, value: float) -> ZmotionResult:
|
||||
ret = self._dll.ZAux_Direct_SetAccel( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def get_accel(self, axis: int) -> ZmotionValue:
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetAccel( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def set_creep(self, axis: int, value: float) -> ZmotionResult:
|
||||
"""Creep (slow homing) speed."""
|
||||
ret = self._dll.ZAux_Direct_SetCreep( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(value)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def get_atype(self, axis: int) -> ZmotionValue:
|
||||
"""Axis type (65=position, 66=speed, 67=torque)."""
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_Direct_GetAtype( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def get_dpos(self, axis: int) -> ZmotionValue:
|
||||
"""Commanded position, in user units."""
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetDpos( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def get_mpos(self, axis: int) -> ZmotionValue:
|
||||
"""Measured (feedback) position, in user units."""
|
||||
value = ctypes.c_float()
|
||||
ret = self._dll.ZAux_Direct_GetMpos( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), float(value.value))
|
||||
|
||||
def get_axis_status(self, axis: int) -> ZmotionValue:
|
||||
"""Axis status word. Bit 6 is the homing-in-progress bit."""
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_Direct_GetAxisStatus( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def get_if_idle(self, axis: int) -> ZmotionValue:
|
||||
"""0 while the axis is moving, -1 when it has stopped."""
|
||||
value = ctypes.c_int()
|
||||
ret = self._dll.ZAux_Direct_GetIfIdle( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.byref(value)
|
||||
)
|
||||
return ZmotionValue(int(ret), int(value.value))
|
||||
|
||||
def single_vmove(self, axis: int, direction: int) -> ZmotionResult:
|
||||
"""Continuous jog: ``direction`` 1 for forward, -1 for reverse."""
|
||||
ret = self._dll.ZAux_Direct_Single_Vmove( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_int(direction)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def single_move(self, axis: int, distance: float) -> ZmotionResult:
|
||||
"""Move by a distance relative to the current position."""
|
||||
ret = self._dll.ZAux_Direct_Single_Move( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(distance)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def single_move_abs(self, axis: int, position: float) -> ZmotionResult:
|
||||
"""Move to an absolute position."""
|
||||
ret = self._dll.ZAux_Direct_Single_MoveAbs( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_float(position)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
|
||||
def single_cancel(self, axis: int, mode: int = 2) -> ZmotionResult:
|
||||
"""Stop the axis. ``mode=2`` is a decelerating stop."""
|
||||
ret = self._dll.ZAux_Direct_Single_Cancel( # type: ignore[union-attr]
|
||||
self._require_handle(), ctypes.c_int(axis), ctypes.c_int(mode)
|
||||
)
|
||||
return ZmotionResult(int(ret))
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Resolve and load the vendored ZMotion runtime DLLs.
|
||||
|
||||
Resolution order:
|
||||
|
||||
1. ``ORISYS_ZMOTION_DLL_DIR`` — an explicit override, e.g. a newer
|
||||
controller-support CD.
|
||||
2. ``<exe dir>/_internal/zmotion/x64`` and ``<exe dir>/zmotion/x64`` — a frozen
|
||||
(PyInstaller) bundle, matching how ``build/studio`` places branding.
|
||||
3. ``shared/resources/zmotion/x64`` in the source tree — the vendored copy
|
||||
(see ``shared/resources/zmotion/x64/README.md`` for provenance).
|
||||
|
||||
Nothing here runs at import time; :func:`load_dll` is called by the app once a
|
||||
connection is actually requested.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ZAUXDLL_DLL = "zauxdll.dll"
|
||||
ZMOTION_DLL = "zmotion.dll"
|
||||
|
||||
#: Environment variable that overrides the vendored DLL directory.
|
||||
DLL_DIR_ENV = "ORISYS_ZMOTION_DLL_DIR"
|
||||
|
||||
|
||||
class ZmotionDllNotFound(OSError):
|
||||
"""The ZMotion runtime could not be located.
|
||||
|
||||
Carries every path that was searched, so the message can tell an operator
|
||||
what to install or which environment variable to set instead of just
|
||||
naming the missing file.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, searched: list[Path]) -> None:
|
||||
self.name = name
|
||||
self.searched = searched
|
||||
listing = "\n".join(f" - {path}" for path in searched)
|
||||
super().__init__(
|
||||
f"{name} not found. Searched:\n{listing}\n"
|
||||
f"Install the ZMotion runtime, or set {DLL_DIR_ENV} to the folder "
|
||||
f"that contains it."
|
||||
)
|
||||
|
||||
|
||||
def _source_tree_dir() -> Path:
|
||||
# .../orisys-app/shared/src/orisys_app_shared/zmotion/loader.py -> shared/
|
||||
return Path(__file__).resolve().parents[3] / "resources" / "zmotion" / "x64"
|
||||
|
||||
|
||||
def _frozen_dirs() -> list[Path]:
|
||||
if not getattr(sys, "frozen", False):
|
||||
return []
|
||||
exe_dir = Path(sys.executable).resolve().parent
|
||||
return [exe_dir / "_internal" / "zmotion" / "x64", exe_dir / "zmotion" / "x64"]
|
||||
|
||||
|
||||
def candidate_dll_dirs() -> list[Path]:
|
||||
"""Every directory that will be searched for the runtime DLLs, in order.
|
||||
|
||||
An explicit ``ORISYS_ZMOTION_DLL_DIR`` is authoritative: it is the only
|
||||
candidate, so a typo fails loudly instead of quietly loading a different
|
||||
build of the runtime than the operator asked for. On a motion controller,
|
||||
silently using the wrong DLL version is exactly the kind of failure that
|
||||
looks like a working machine until it moves.
|
||||
"""
|
||||
override = os.environ.get(DLL_DIR_ENV, "").strip()
|
||||
if override:
|
||||
return [Path(override).expanduser()]
|
||||
|
||||
candidates: list[Path] = []
|
||||
candidates.extend(_frozen_dirs())
|
||||
candidates.append(_source_tree_dir())
|
||||
|
||||
seen: set[Path] = set()
|
||||
unique: list[Path] = []
|
||||
for path in candidates:
|
||||
resolved = path.resolve() if path.exists() else path
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
unique.append(resolved)
|
||||
return unique
|
||||
|
||||
|
||||
def resolve_dll_dir() -> Path:
|
||||
"""Return the first candidate directory that holds ``zauxdll.dll``.
|
||||
|
||||
Raises:
|
||||
ZmotionDllNotFound: no candidate directory contains the runtime.
|
||||
"""
|
||||
searched = candidate_dll_dirs()
|
||||
for path in searched:
|
||||
if (path / ZAUXDLL_DLL).is_file():
|
||||
return path
|
||||
raise ZmotionDllNotFound(ZAUXDLL_DLL, searched)
|
||||
|
||||
|
||||
def _prepare_search_path(dll_dir: Path) -> None:
|
||||
"""Make ``dll_dir`` available to the loader for dependent DLLs.
|
||||
|
||||
``zauxdll.dll`` links against ``zmotion.dll`` beside it, so the directory
|
||||
must be on the search path *before* ``zauxdll.dll`` is loaded. Both the
|
||||
process ``PATH`` and the loader's own DLL directory are set: the former is
|
||||
what the vendor wrapper relied on, the latter is the documented Win32
|
||||
mechanism.
|
||||
"""
|
||||
dll_dir_str = str(dll_dir)
|
||||
path_entries = os.environ.get("PATH", "").split(os.pathsep)
|
||||
if dll_dir_str not in path_entries:
|
||||
os.environ["PATH"] = dll_dir_str + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel32.SetDllDirectoryW(ctypes.c_wchar_p(dll_dir_str))
|
||||
except (OSError, AttributeError): # pragma: no cover - defensive
|
||||
# Not fatal: the PATH entry above is usually enough, and the real
|
||||
# failure (if any) surfaces as a clearer error from load_dll.
|
||||
pass
|
||||
|
||||
|
||||
def load_dll(dll_dir: Path | None = None) -> ctypes.CDLL:
|
||||
"""Load ``zauxdll.dll`` with its directory on the search path.
|
||||
|
||||
Args:
|
||||
dll_dir: directory to load from; resolved automatically when omitted.
|
||||
|
||||
Returns:
|
||||
The loaded CDLL.
|
||||
|
||||
Raises:
|
||||
ZmotionDllNotFound: the runtime is not present.
|
||||
OSError: the DLL is present but the platform loader rejected it.
|
||||
"""
|
||||
resolved = dll_dir or resolve_dll_dir()
|
||||
dll_path = resolved / ZAUXDLL_DLL
|
||||
if not dll_path.is_file():
|
||||
raise ZmotionDllNotFound(ZAUXDLL_DLL, [resolved])
|
||||
_prepare_search_path(resolved)
|
||||
return ctypes.CDLL(str(dll_path))
|
||||
Reference in New Issue
Block a user