#!/usr/bin/env python3 """Does the switch signal actually toggle? Sample the bits, count the edges. ``home_trace.py`` prints a line whenever anything about the axis changes, which is right for watching a homing run. This does the opposite job: it samples as fast as the controller answers, counts every transition of the switch bits, and reports them as a total. That turns "I think the switch works" into a number. Use it to answer one question — *does this switch reach the drive at all?* — for a rotary axis where a partial move may simply not cross the switch: # drive the axis through at least one full revolution while this runs python apps\\autocal\\tools\\switch_probe.py --axis 3 --seconds 60 python apps\\autocal\\tools\\switch_probe.py --axis 3 --seconds 60 --csv edges3.csv Interpretation: * **``HOME`` edges > 0** — the bit follows the switch. Its polarity is the ``rest→active`` pair printed in the summary; pick the homing method whose edge matches. * **``HOME`` edges = 0 while ``travel`` covers a full revolution** — the signal never reached the drive. On a fail-safe input wired normally closed, a broken wire looks exactly like "switch made" (the drive reads one constant level), so zero edges after a full turn is a *wiring* finding, not a homing-method one. Change methods all you like; no CiA-402 method can use a signal the drive does not see. Reading is non-mutating: it never enables, moves, homes or stops anything. You drive the axis (app jog, MPG, or hand), this only listens. Exit status is 0 in all outcomes — including "no edges", which is a result. """ from __future__ import annotations import argparse import csv import ctypes import os import sys import time from pathlib import Path APP_ROOT = Path(__file__).resolve().parents[1] for path in (APP_ROOT / "src", APP_ROOT.parents[1] / "shared" / "src"): if str(path) not in sys.path: sys.path.insert(0, str(path)) os.environ.setdefault("ORISYS_APP_ID", "autocal") #: The bits worth counting, as ``label -> (bit index in DRIVE_STATUS, description)``. WATCHED_BITS = { "P_OT": (1, "positive limit switch"), "N_OT": (2, "negative limit switch"), "FAULT": (3, "drive fault"), "HOME": (4, "home / datum switch"), } #: Reported alongside the drive bits, because the controller inverts these and #: the pair is what tells a wiring fault from a working switch. INPUT_OFFSETS = {"IN_REV": 0, "IN_FWD": 1, "IN_DATUM": 2} # from 128 + 16*axis class Sampler: def __init__(self, ip: str) -> None: from orisys_app_shared.zmotion import ZmotionClient self._client = ZmotionClient().load() self._client.open_eth(ip).raise_for_status(f"open {ip}") self._handle = self._client._require_handle() # noqa: SLF001 - one client def close(self) -> None: self._client.close() def param(self, name: str, axis: int) -> float | None: value = ctypes.c_float() code = self._client._dll.ZAux_Direct_GetParam( # noqa: SLF001 self._handle, name.encode(), ctypes.c_int(axis), ctypes.byref(value) ) return float(value.value) if code == 0 else None def input(self, pin: int) -> int | None: value = ctypes.c_int() code = self._client._dll.ZAux_Direct_GetIn( # noqa: SLF001 self._handle, ctypes.c_int(pin), ctypes.byref(value) ) return int(value.value) if code == 0 else None def now(self, axis: int) -> tuple[int, float | None, dict[str, int | None]]: status = int(self.param("DRIVE_STATUS", axis) or 0) & 0xFFFF base = 128 + 16 * axis inputs = {name: self.input(base + offset) for name, offset in INPUT_OFFSETS.items()} return status, self.param("MPOS", axis), inputs def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="switch-probe", description=__doc__) parser.add_argument("--axis", type=int, required=True) parser.add_argument("--seconds", type=float, default=60.0) parser.add_argument("--ip", default="192.168.0.11") parser.add_argument("--csv", default="", help="write the edge log to this CSV") args = parser.parse_args(argv) sampler = Sampler(args.ip) print( f"counting edges on axis {args.axis} for {args.seconds:g}s — " f"drive the axis through a full revolution now", flush=True, ) deadline = time.monotonic() + args.seconds edges: dict[str, list[tuple[float, int, int]]] = {name: [] for name in WATCHED_BITS} input_edges: dict[str, list[tuple[float, int, int]]] = {name: [] for name in INPUT_OFFSETS} counts: dict[str, int] = {name: 0 for name in WATCHED_BITS} input_counts: dict[str, int] = {name: 0 for name in INPUT_OFFSETS} previous: dict | None = None travel = 0.0 samples = 0 events: list[dict] = [] start = time.monotonic() def record(elapsed: float, status: int) -> None: for label, (bit, _desc) in WATCHED_BITS.items(): value = (status >> bit) & 1 if previous is not None and value != ((previous["status"] >> bit) & 1): counts[label] += 1 edges[label].append((elapsed, (previous["status"] >> bit) & 1, value)) events.append({"t": round(elapsed, 3), "kind": "drive_bit", "name": label, "from": (previous["status"] >> bit) & 1, "to": value}) try: while time.monotonic() < deadline: status, mpos, inputs = sampler.now(args.axis) elapsed = time.monotonic() - start samples += 1 if previous is not None and mpos is not None and previous["mpos"] is not None: travel += abs(mpos - previous["mpos"]) record(elapsed, status) for label, value in inputs.items(): if previous is not None and value != previous["inputs"][label]: input_counts[label] += 1 input_edges[label].append((elapsed, previous["inputs"][label], value)) events.append({"t": round(elapsed, 3), "kind": "input", "name": label, "from": previous["inputs"][label], "to": value}) previous = {"status": status, "mpos": mpos, "inputs": inputs, "t": elapsed} time.sleep(0.005) except KeyboardInterrupt: # pragma: no cover - operator convenience print("\ninterrupted", flush=True) finally: sampler.close() first_status = None if samples: first_status = previous["status"] print() print(f"samples: {samples} total travel: {travel:.1f} user units") print("drive bit edges:") for label, (bit, desc) in WATCHED_BITS.items(): if counts[label]: seq = " ".join(f"{a}->{b}" for _t, a, b in edges[label][:8]) print(f" {label:6s} ({desc}): {counts[label]} edges first: {seq}") else: print(f" {label:6s} ({desc}): 0 edges — constant at {(previous['status'] >> bit) & 1 if samples else '?'}") print("controller input edges (INVERT_IN applied):") for label in INPUT_OFFSETS: state = previous["inputs"][label] if samples else "?" print(f" {label:8s}: {input_counts[label]} edges constant at {state}") if args.csv: path = Path(args.csv) with path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=["t", "kind", "name", "from", "to"]) writer.writeheader() writer.writerows(events) print(f"wrote {len(events)} edge events to {path}") if samples and counts["HOME"] == 0 and counts["P_OT"] == 0 and counts["N_OT"] == 0: print() print("NOTE: no switch bit moved at all. If the travel above covers a full revolution,") print(" the drive is not receiving a working switch signal — a broken wire on a") print(" normally-closed fail-safe input reads exactly like a made switch. Check") print(" the drive's DI wiring then, not the homing method.") return 0 if __name__ == "__main__": raise SystemExit(main())