fixed platform not moving problem

This commit is contained in:
2026-09-23 09:52:45 +08:00
parent f1bec62ca4
commit b3279cc2a6
21 changed files with 1200 additions and 59 deletions
@@ -16,20 +16,25 @@ from .models import (
Snapshot,
)
from .poller import MotionPoller
from .service import PLATFORM_AXES, MotionService
from .service import DEFAULT_BAS_TASK, PLATFORM_AXES, MotionService, clamp_bas_task
from .transport import DemoTransport, NativeTransport, ZmcTransport
from .units import (
DEFAULT_ACCEL,
DEFAULT_SPEED,
DEFAULT_UNITS,
PULSES_PER_MM,
ROTARY_UNITS_PER_90_DEG,
deg_to_user_units,
mm_to_user_units,
safe_float,
units_per_degree,
user_units_to_deg,
user_units_to_mm,
)
__all__ = [
"DEFAULT_ACCEL",
"DEFAULT_BAS_TASK",
"DEFAULT_SPEED",
"DEFAULT_UNITS",
"INIT_FAILED",
@@ -38,6 +43,7 @@ __all__ = [
"INIT_TIMEOUT_S",
"PLATFORM_AXES",
"PULSES_PER_MM",
"ROTARY_UNITS_PER_90_DEG",
"AxisParams",
"AxisState",
"AxisStatusBits",
@@ -52,7 +58,11 @@ __all__ = [
"NativeTransport",
"Snapshot",
"ZmcTransport",
"clamp_bas_task",
"deg_to_user_units",
"mm_to_user_units",
"safe_float",
"units_per_degree",
"user_units_to_deg",
"user_units_to_mm",
]
@@ -26,6 +26,14 @@ logger = logging.getLogger(__name__)
#: Auxiliary (host-side) error block offset, from ``zauxdll2.h``.
ERR_AUX_OFFSET = 30000
#: The block the PC library uses for its *own* faults — handshake, socket,
#: address, a controller that stopped answering mid-session. It sits below
#: ``ERR_AUX_OFFSET``, and it is **not** in the firmware's code table: pointing
#: an operator at ZDevelop for one of these sends them to a table that cannot
#: contain the number. Observed live: ``20008`` from ``ZAux_OpenEth`` on a first,
#: transient attempt that succeeded on the next.
HOST_LIB_ERROR_RANGE = (20000, 29999)
#: Meaning per code, as ``(english, chinese)``.
MESSAGES: dict[int, tuple[str, str]] = {
ERR_AUX_OFFSET: ("No response from the controller", "控制器无应答"),
@@ -42,6 +50,24 @@ UNKNOWN_HINT = (
"or run the same command from ZDevelop to see the decoded fault."
)
#: Advice for a host-library fault, which is a communication problem the
#: operator can actually act on — and which is frequently transient.
HOST_LIB_HINT_ZH = (
"上位机库通信错误:请确认网线/网段与控制器 IP,"
"确认控制器已上电、没有其它程序占用连接,然后重试。"
)
HOST_LIB_HINT_EN = (
"Host-library communication error: check the cable/segment and the "
"controller IP, make sure the controller is powered and no other program "
"holds the connection, then retry."
)
def is_host_lib_error(code: int) -> bool:
"""True for a PC-library fault, which is not a controller code."""
low, high = HOST_LIB_ERROR_RANGE
return low <= int(code) <= high
def describe(code: int, *, language: str = "zh_CN") -> str:
"""A human-readable message for a controller return code.
@@ -60,18 +86,23 @@ def describe(code: int, *, language: str = "zh_CN") -> str:
index = 1 if language == "zh_CN" else 0
known = MESSAGES.get(code)
if known is None:
# An unknown code that still resolves to an auxiliary slot is more
# useful than a bare integer, but only when it is documented.
logger.debug("Undocumented ZMotion return code %s", code)
prefix = "错误码" if language == "zh_CN" else "Error code"
if known is not None:
return known[index]
logger.debug("Undocumented ZMotion return code %s", code)
if is_host_lib_error(code):
prefix = "上位机库错误" if language == "zh_CN" else "Host-library error"
return f"{prefix} {code}"
return known[index]
prefix = "错误码" if language == "zh_CN" else "Error code"
return f"{prefix} {code}"
def describe_with_hint(code: int, *, language: str = "zh_CN") -> str:
"""As :func:`describe`, plus the ZDevelop pointer for unknown codes."""
"""As :func:`describe`, plus the advice the code's family warrants."""
message = describe(code, language=language)
if int(code) == 0 or int(code) in MESSAGES:
return message
if is_host_lib_error(code):
hint = HOST_LIB_HINT_ZH if language == "zh_CN" else HOST_LIB_HINT_EN
return f"{message} — {hint}"
return f"{message} — {UNKNOWN_HINT}"
@@ -134,11 +134,17 @@ class BusStatus:
def supports_rtex(self) -> bool:
return self.bus_type == "rtex"
def timed_out(self, now: float) -> bool:
"""True while a BAS run started long ago has still not reported."""
def timed_out(self, now: float, timeout: float = INIT_TIMEOUT_S) -> bool:
"""True when a BAS run started long ago has still not reported.
``timeout`` is the window to judge against. It defaults to the vendor's
25 s, but a caller that owns a configurable window (the service) must
pass it: reading the module constant here instead made the configured
timeout invisible to every caller that asked this question.
"""
if self.init_status != INIT_RUNNING or self.init_started_at is None:
return False
return (now - self.init_started_at) > INIT_TIMEOUT_S
return (now - self.init_started_at) > float(timeout)
@dataclass(frozen=True)
@@ -279,8 +279,8 @@ class MotionPoller(QObject):
"""Queue a service call. Returns the id it will be reported under."""
return self._worker.submit(action)
def connect_controller(self, spec) -> int:
return self.submit(lambda service: service.connect(spec))
def connect_controller(self, spec, *, bas_path: str = "") -> int:
return self.submit(lambda service: service.connect(spec, bas_path=bas_path))
def disconnect_controller(self) -> int:
def _disconnect(service: MotionService):
@@ -52,6 +52,48 @@ PLATFORM_AXES = 5
#: ``BUS_TYPE`` values written by the vendor ``.bas`` programs.
_BUS_TYPE_NAMES: dict[int, BusType] = {0: "ecat", 1: "rtex"}
#: The controller task the vendor reference starts its BAS program on
#: (``RUN "<file>",4``). Re-initialisation must name the *same* task, or it
#: stops a task that was never running and restarts nothing.
DEFAULT_BAS_TASK = 4
#: Accepted task range. The controller's task numbers are small; anything
#: outside this is a typed-in mistake, not a task to send.
_BAS_TASK_RANGE = (1, 8)
#: Substrings the firmware puts in an online command's reply when the command
#: was refused *without* a non-zero return code — a ``RUN`` naming a program the
#: controller does not have is the one that matters here (``ZAux_Execute``
#: returns 0 for it).
_ONLINE_FAILURE_MARKERS = ("not find", "fail", "error")
def clamp_bas_task(value: object, fallback: int = DEFAULT_BAS_TASK) -> int:
"""A usable controller task number, or the vendor default."""
try:
task = int(value)
except (TypeError, ValueError):
return fallback
low, high = _BAS_TASK_RANGE
return task if low <= task <= high else fallback
def _online_failure(reply: str) -> str:
"""The controller's own refusal text from an online command, or ``""``.
``ZAux_Execute`` reports a rejected online command in its reply rather than
in its return code, so a caller that only checks the code cannot tell
"started" from "no such program". Anything the firmware answers that carries
a failure marker is returned for the UI to show verbatim.
"""
text = " ".join(str(reply or "").split())
if not text:
return ""
lowered = text.lower()
if any(marker in lowered for marker in _ONLINE_FAILURE_MARKERS):
return text
return ""
class MotionService:
"""The controller session: one connection, five axes, one state machine."""
@@ -64,12 +106,14 @@ class MotionService:
units_per_mm: float = DEFAULT_UNITS,
pulses_per_mm: float = PULSES_PER_MM,
language: str = "zh_CN",
bas_task: int = DEFAULT_BAS_TASK,
) -> None:
self._transport: ZmcTransport = transport or NativeTransport()
self.axis_count = int(axis_count)
self._units_per_mm = safe_float(units_per_mm, DEFAULT_UNITS)
self._pulses_per_mm = pulses_per_mm
self.language = language
self.bas_task = clamp_bas_task(bas_task)
self._spec: ConnectionSpec | None = None
self._bus = BusStatus()
@@ -125,11 +169,18 @@ class MotionService:
# ------------------------------------------------------------ connect/init
def connect(self, spec: ConnectionSpec) -> MotionResult:
def connect(self, spec: ConnectionSpec, *, bas_path: str = "") -> MotionResult:
"""Open the controller.
A previous connection is closed first: switching transport type without
closing leaks the DLL handle, which the reference program does.
``bas_path`` is the ``.bas`` file the operator last used, as remembered
by the UI. It is *recorded*, never downloaded: a controller usually still
has the program in ROM from the last session, and re-writing it on every
connect is both slow and a bus re-scan the operator did not ask for. Its
only job is to make :meth:`reinit_bus` possible on a session that adopted
a running program instead of downloading one.
"""
if self._spec is not None:
self.disconnect()
@@ -146,25 +197,35 @@ class MotionService:
logger.info("Connected: %s via %s", spec.describe(), self._transport.source)
# A controller may already have a BAS program running from a previous
# session; adopt it rather than claiming the bus is uninitialised.
self._adopt_existing_bas()
self._adopt_existing_bas(bas_path=bas_path)
return MotionResult.success(f"已连接 {spec.describe()}" if self._is_zh else f"Connected {spec.describe()}")
@property
def _is_zh(self) -> bool:
return self.language == "zh_CN"
def _adopt_existing_bas(self) -> None:
def _adopt_existing_bas(self, *, bas_path: str = "") -> None:
try:
bus_type = self._transport.user_var("BUS_TYPE")
except (ZAuxError, OSError):
self._bas_loaded = False
self._bus = BusStatus(bus_type="unknown", init_status=INIT_RUNNING)
return
path = str(bas_path or "").strip()
self._bas_loaded = True
# ``init_status`` stays "running" until the first poll reads the real
# ``Bus_InitStatus``: connect() must not claim a state it has not read.
# The adoption is *timestamped* though, so a controller that never
# answers still reaches the timeout instead of looking busy forever —
# and so "re-initialise" is not refused as "already running" five
# minutes into a session.
self._init_started_at = time.monotonic()
self._bus = BusStatus(
bus_type=_BUS_TYPE_NAMES.get(int(bus_type), "unknown"),
init_status=INIT_RUNNING,
bas_path=path,
bas_loaded=True,
init_started_at=self._init_started_at,
)
def disconnect(self) -> None:
@@ -224,29 +285,50 @@ class MotionService:
)
def reinit_bus(self) -> MotionResult:
"""Re-run the BAS program's init task without re-downloading it."""
"""Re-run the BAS program's init task without re-downloading it.
The vendor sequence, exactly: stop the task the program runs on, then
``RUN`` the file on that same task. Task numbers are not interchangeable
— the reference program starts on task 4, so stopping task 2 restarts
nothing.
``ZAux_Execute`` returns 0 for a ``RUN`` naming a program the controller
does not have, putting the diagnosis in the reply text instead. That text
is checked here rather than trusted: a re-initialisation that cannot
happen must not be reported as one that is running.
"""
if not self.connected:
return MotionResult.refused(self._text("未连接控制器", "Not connected"))
if not self._bas_loaded or not self._bus.bas_path:
return MotionResult.refused(
self._text(
"Bas文件未加载,请先下载 ECAT.bas 或 RTEX.bas",
"No BAS program loaded; download ECAT.bas or RTEX.bas first",
"没有 BASIC 程序文件名,无法重新初始化;请先下载 ECAT.bas 或 RTEX.bas",
"No BAS program file name is known, so it cannot be "
"re-initialised; download ECAT.bas or RTEX.bas first",
)
)
if self._bus.init_status == INIT_RUNNING and not self._bus.timed_out(time.monotonic()):
if self._bus.init_status == INIT_RUNNING and not self._bus.timed_out(
time.monotonic(), self._init_timeout
):
return MotionResult.refused(
self._text("总线正在初始化,请稍候", "Bus initialisation is already running")
)
script = os.path.basename(self._bus.bas_path)
task = clamp_bas_task(self.bas_task)
try:
self._transport.execute("STOPTASK 2")
self._transport.execute(f'RUN "{script}",4')
stop_reply = self._transport.execute(f"STOPTASK {task}")
run_reply = self._transport.execute(f'RUN "{script}",{task}')
except (ZAuxError, OSError) as exc:
code = getattr(exc, "code", 0)
return MotionResult.failed(code, self._msg(code) if code else str(exc))
# The stop command has no meaningful reply; the RUN one does.
failure = _online_failure(run_reply)
if failure:
logger.warning("Re-initialise rejected for %s on task %s: %s", script, task, failure)
return MotionResult.failed(0, failure)
self._init_started_at = time.monotonic()
self._bus = BusStatus(
bus_type=self._bus.bus_type,
@@ -255,6 +337,19 @@ class MotionService:
bas_loaded=True,
init_started_at=self._init_started_at,
)
# Re-read BUS_TYPE now: the program may be a different family than the
# one the previous session left behind.
try:
bus_type = self._transport.user_var("BUS_TYPE")
self._bus = _replace_bus_type(self._bus, _BUS_TYPE_NAMES.get(int(bus_type), "unknown"))
except (ZAuxError, OSError):
pass
logger.info(
"Re-initialising %s on controller task %s (stop reply=%r)",
script,
task,
stop_reply,
)
return MotionResult.success(
self._text("正在重新初始化总线", "Re-initialising the bus")
)
@@ -17,7 +17,9 @@ Three implementations:
from __future__ import annotations
import logging
import math
import os
import time
from typing import Protocol, runtime_checkable
@@ -26,6 +28,8 @@ from orisys_app_shared.zmotion import ZAuxError, ZmotionClient
from .errors import ERR_AUX_OFFSET
from .models import BusType, ConnectionSpec
logger = logging.getLogger(__name__)
@runtime_checkable
class ZmcTransport(Protocol):
@@ -42,7 +46,7 @@ class ZmcTransport(Protocol):
def bas_download(self, filename: str, run_mode: int = 1) -> None: ...
def execute(self, command: str) -> None: ...
def execute(self, command: str) -> str: ...
def user_var(self, name: str) -> float: ...
@@ -114,6 +118,14 @@ class ZmcTransport(Protocol):
class NativeTransport:
"""The real controller, through the vendored ZMotion runtime."""
#: How many times an Ethernet open is attempted before the failure is
#: reported, and how long to wait between attempts. A first connection to a
#: controller on a cold ARP/route table can fail once and succeed on the
#: next try — the reference program's answer is "click connect again", which
#: the operator should not have to know.
ETH_OPEN_ATTEMPTS = 3
ETH_OPEN_RETRY_DELAY_S = 0.3
def __init__(self, client: ZmotionClient | None = None) -> None:
self._client = client or ZmotionClient()
@@ -127,16 +139,39 @@ class NativeTransport:
def open(self, spec: ConnectionSpec) -> None:
if spec.mode == "eth":
result = self._client.open_eth(spec.ip)
self._open_eth_with_retry(spec.ip)
elif spec.mode == "com":
result = self._client.open_com(spec.com_id)
result.raise_for_status(f"open {spec.describe()}")
elif spec.mode == "pci":
result = self._client.open_pci(spec.card_num)
result.raise_for_status(f"open {spec.describe()}")
elif spec.mode == "local":
result = self._client.fast_open(5, spec.local_string, 1000)
result.raise_for_status(f"open {spec.describe()}")
else:
raise ValueError(f"{spec.mode!r} is not a native connection mode")
result.raise_for_status(f"open {spec.describe()}")
def _open_eth_with_retry(self, ip: str) -> None:
"""Open over Ethernet, retrying a *failed* attempt a couple of times."""
last_code = 0
for attempt in range(1, self.ETH_OPEN_ATTEMPTS + 1):
result = self._client.open_eth(ip)
if result.ok:
if attempt > 1:
logger.info("Ethernet open of %s succeeded on attempt %s", ip, attempt)
return
last_code = result.code
logger.warning(
"Ethernet open of %s failed on attempt %s/%s with code %s",
ip,
attempt,
self.ETH_OPEN_ATTEMPTS,
result.code,
)
if attempt < self.ETH_OPEN_ATTEMPTS:
time.sleep(self.ETH_OPEN_RETRY_DELAY_S)
ZAuxError(last_code, f"open {ip}").raise_for_status(f"open {ip}")
def close(self) -> None:
self._client.close()
@@ -152,8 +187,15 @@ class NativeTransport:
def bas_download(self, filename: str, run_mode: int = 1) -> None:
self._client.bas_download(filename, run_mode).raise_for_status("BAS download")
def execute(self, command: str) -> None:
self._client.execute(command).raise_for_status(command)
def execute(self, command: str) -> str:
"""Run an online command; return the controller's own reply text.
The reply is not decoration: an online command the firmware refuses
(``RUN`` naming a program it does not have) returns code 0 and explains
itself only in this text. See
:meth:`orisys_autocal.motion.service.MotionService.reinit_bus`.
"""
return str(self._client.execute(command).raise_for_status(command).value or "")
def user_var(self, name: str) -> float:
return float(self._client.get_user_var(name).raise_for_status(name).value)
@@ -278,6 +320,7 @@ class DemoTransport:
self._homing_until = {axis: 0.0 for axis in range(self._axes)}
self._home_done = {axis: False for axis in range(self._axes)}
self._bas_loaded = False
self._bas_file = ""
self._init_started: float | None = None
self._opened = False
self._sdo: dict[tuple[int, int, int], int] = {}
@@ -301,15 +344,25 @@ class DemoTransport:
if not self._opened:
raise ZAuxError(ERR_NOT_OPEN, "not connected")
self._bas_loaded = True
# The simulation keeps the file name, so a re-init can tell a program
# the controller has from one it does not — the distinction the real
# firmware makes in an online command's reply text.
self._bas_file = os.path.basename(str(filename).replace("\\", "/"))
# ECAT.bas delays 3 s before scanning; mimic the delay so the UI's
# "initialising" state is visible rather than instantaneous.
self._init_started = time.monotonic() + DEMO_INIT_DELAY_S
def execute(self, command: str) -> None:
def execute(self, command: str) -> str:
if not self._opened:
raise ZAuxError(ERR_NOT_OPEN, "not connected")
if "RUN" in command.upper() and self._bas_loaded:
wanted = _run_filename(command)
if wanted and self._bas_file and wanted != self._bas_file:
# The real firmware returns code 0 here and explains itself only
# in this text. See MotionService.reinit_bus.
return f"Online command error, Run filename:{wanted} not find."
self._init_started = time.monotonic() + DEMO_INIT_DELAY_S
return ""
def user_var(self, name: str) -> float:
if not self._bas_loaded:
@@ -465,3 +518,19 @@ DEMO_HOME_S = 2.0
#: Demo-only failure codes, kept outside the documented vendor range on purpose.
ERR_NOT_OPEN = 30500
ERR_DEMO_UNSUPPORTED = 30501
def _run_filename(command: str) -> str:
"""The quoted file name in an online ``RUN "file",task`` command, or ``""``.
Shared by the simulated controller and its tests so the demo rejects the
same commands the real firmware rejects.
"""
text = str(command or "")
start = text.find('"')
if start < 0:
return ""
end = text.find('"', start + 1)
if end < 0:
return ""
return text[start + 1 : end].strip()
+42 -10
View File
@@ -1,31 +1,39 @@
"""User-unit to millimetre conversion.
"""Platform unit conversion (linear mm and rotary degrees).
The numbers come from bench measurement on the platform, transcribed from the
vendor reference program's comments::
Bench measurements (controller user units, with the platform's usual
``UNITS = 10`` pulse equivalent unless noted)::
# 轴0(X)/轴1(Y) 实测: UNITS=10 时 1000用户单位 = 2mm → 每毫米 5000 脉冲
# 轴0(X)/轴1(Y): UNITS=10 时 1000用户单位 = 2mm → 每毫米 5000 脉冲
# 当量5 → 1000单位=1mm,不是100单位=1mm
# 轴2: 12500 用户单位 = 90°
# 轴3: 29500 用户单位 = 90°
so ``PULSES_PER_MM = 5000`` is the calibration constant and ``UNITS = 10`` is
the pulse equivalent the platform is configured with. The displayed millimetre
value is therefore *derived*, not measured: the UI labels it as such, and no
code path may treat it as a calibrated length.
Linear display millimetres use ``PULSES_PER_MM`` and the axis pulse equivalent.
Rotary display degrees use :data:`ROTARY_UNITS_PER_90_DEG` for axes 2 and 3.
Displayed values are *derived*, not fresh measurements.
"""
from __future__ import annotations
import math
#: Measured: 5000 pulses per millimetre (see the module docstring).
#: Measured: 5000 pulses per millimetre (axes 0 / 1).
PULSES_PER_MM = 5000.0
#: Pulse equivalent the platform is configured with by default.
#: Pulse equivalent the linear axes are configured with by default.
DEFAULT_UNITS = 10.0
#: Move-parameter defaults, matching the reference program's UI initialisation.
DEFAULT_SPEED = 1000.0
DEFAULT_ACCEL = 10000.0
#: Bench: user units that correspond to a 90° turn, keyed by axis number.
#: Axis 2 and 3 are rotary; axes 0 / 1 / 4 stay out of this map until measured.
ROTARY_UNITS_PER_90_DEG: dict[int, float] = {
2: 12500.0,
3: 29500.0,
}
def safe_float(text: object, fallback: float) -> float:
"""Parse ``text`` as a finite float, falling back on anything unusable.
@@ -70,3 +78,27 @@ def mm_to_user_units(
if units <= 0:
return math.nan
return float(value) * float(pulses_per_mm) / float(units)
def units_per_degree(axis: int) -> float:
"""User units per degree for a rotary axis, or NaN if not calibrated."""
per_90 = ROTARY_UNITS_PER_90_DEG.get(int(axis))
if per_90 is None or per_90 <= 0:
return math.nan
return float(per_90) / 90.0
def user_units_to_deg(axis: int, value: float) -> float:
"""Convert controller user units to degrees for a calibrated rotary axis."""
scale = units_per_degree(axis)
if not math.isfinite(scale) or scale == 0.0:
return math.nan
return float(value) / scale
def deg_to_user_units(axis: int, degrees: float) -> float:
"""Inverse of :func:`user_units_to_deg`."""
scale = units_per_degree(axis)
if not math.isfinite(scale):
return math.nan
return float(degrees) * scale
@@ -13,7 +13,7 @@ from typing import Any
from orisys_app_shared import prefs as _prefs
from orisys_app_shared.paths import preference_path
from .motion.service import PLATFORM_AXES
from .motion.service import DEFAULT_BAS_TASK, PLATFORM_AXES, clamp_bas_task
from .motion.units import DEFAULT_ACCEL, DEFAULT_SPEED, DEFAULT_UNITS
#: Connection modes, in the order the combo offers them.
@@ -155,6 +155,11 @@ def default_preferences() -> dict[str, Any]:
# Remembered so "download the BAS program" is one click after the first
# time, instead of re-picking the file every session.
"bas_path": "",
#: The controller task the BAS program runs on. The vendor reference
#: starts it on 4 and re-initialises it with ``RUN "<file>",4``; stopping
#: any other task restarts nothing. Kept configurable because the task
#: number is a property of the project loaded on the controller.
"bas_task": DEFAULT_BAS_TASK,
#: Displayed millimetres are derived from this; it is a bench-measured
#: constant, not a calibrated length (see motion/units.py).
"units_per_mm": DEFAULT_UNITS,
@@ -199,6 +204,8 @@ def load_preferences() -> dict[str, Any]:
axis_params = prefs.setdefault("axis_params", {})
for axis, values in _default_axis_params().items():
axis_params.setdefault(axis, values)
# A hand-edited task number must not reach the controller as typed.
prefs["bas_task"] = clamp_bas_task(prefs.get("bas_task"))
return prefs
@@ -146,13 +146,17 @@ class ConnectionPanel(QGroupBox):
self.address_edit.setText("")
def set_ips(self, ips: list[str]) -> None:
"""Offer scanned addresses for the Ethernet mode."""
"""Offer scanned addresses for the Ethernet mode.
The hint is shown whatever the address box currently holds: a scan that
found the controller is exactly the moment the operator wants to see the
address, and suppressing it because the box already held a different one
made the button look broken. The box itself is never overwritten, so a
typed address survives a scan.
"""
if self.mode_combo.currentData() != "eth" or not ips:
return
current = self.address_edit.text().strip()
if current and current not in ips:
return
self.ip_hint.setText(" ".join(ips[:4]))
self.ip_hint.setText(" ".join(str(ip) for ip in ips[:4]))
def set_connected(self, connected: bool, source: str = "") -> None:
self.open_button.setEnabled(not connected)
+17 -3
View File
@@ -40,6 +40,7 @@ from PySide6.QtWidgets import (
from .. import __version__ as app_version
from ..motion import AxisParams, ConnectionSpec, MotionPoller, MotionService, Snapshot
from ..motion.kinematics import PlatformStack
from ..motion.service import clamp_bas_task
from ..preferences import load_preferences, save_preferences
from ..sensor import BundleError, load_bundle, load_uv_texture, rasterise, texture_path_for
from ..sensor.uv_flat import DEFAULT_FLAT_SIZE
@@ -655,7 +656,10 @@ class AutoCalMainWindow(QMainWindow):
def _build_poller(self) -> None:
self.poller = MotionPoller(
MotionService(language=self._language),
MotionService(
language=self._language,
bas_task=clamp_bas_task(self.prefs.get("bas_task")),
),
axes=range(self.axis_panel.axis_count),
)
self.poller.snapshotReady.connect(self._on_snapshot)
@@ -679,7 +683,12 @@ class AutoCalMainWindow(QMainWindow):
) -> int:
"""Ask the poller to open the controller. Returns the command id."""
target = spec or self.connection_panel.spec
token = self.poller.connect_controller(target)
# The remembered BAS file travels with the connect so that a session
# which adopts a program already running on the controller can still
# re-initialise it (the controller does not report the file name).
token = self.poller.connect_controller(
target, bas_path=str(self.prefs.get("bas_path") or "")
)
self._pending[token] = label
self.connection_panel.set_status(t("connection.state_connecting"))
return token
@@ -770,7 +779,12 @@ class AutoCalMainWindow(QMainWindow):
message, ok = _interpret_result(result)
if ok:
self._last_error = ""
if message:
if label_key == "connection.scan" and isinstance(result, (list, tuple)):
# A scan exists to put the addresses somewhere the operator can
# click; reporting "OK" with the list only in the status bar
# leaves the IP box untouched.
self.connection_panel.set_ips([str(item) for item in result])
elif message:
self.bus_data_panel.set_result(message)
text = f"{name} {t('action.ok')}".strip()
self.statusBar().showMessage(text)