| @@ -0,0 +1,213 @@ | |||||
| #!/usr/bin/env python3 | |||||
| """ | |||||
| Minimal TCP client to probe the laser (EZCAD / Bag2 plugin). | |||||
| Examples: | |||||
| python testLaser.py | |||||
| python testLaser.py "GetMarkStatus;" | |||||
| python testLaser.py -c "0;GetMarkStatus;;" | |||||
| python testLaser.py --host 192.168.18.69 --port 45678 --preset bag2-status | |||||
| Default host/port: 192.168.18.69 / 45678 | |||||
| """ | |||||
| from __future__ import annotations | |||||
| import argparse | |||||
| import socket | |||||
| import sys | |||||
| DEFAULT_HOST = "192.168.18.69" | |||||
| DEFAULT_PORT = 45678 | |||||
| # Shortcuts (second column = description) | |||||
| PRESETS: dict[str, tuple[str, str]] = { | |||||
| "status": ("GetMarkStatus;", "plain command + semicolon"), | |||||
| "data": ("GetMarkData;", "plain command + semicolon"), | |||||
| "count": ("GetMarkedCount;", "plain command + semicolon"), | |||||
| "bag2-status": ("0;GetMarkStatus;;", "Bag2 line shape (same idea as FPSMS backend)"), | |||||
| "bag2-data": ("0;GetMarkData;;", "Bag2 line shape"), | |||||
| "bag2-count": ("0;GetMarkedCount;;", "Bag2 line shape"), | |||||
| } | |||||
| SWEEP_PAYLOADS = [ | |||||
| "GetMarkStatus;", | |||||
| "GetMarkStatus", | |||||
| "GetMarkStatus\r\n", | |||||
| "GetMarkData;", | |||||
| "GetMarkData", | |||||
| "GetMarkData\r\n", | |||||
| "GetMarkedCount;", | |||||
| "GetMarkedCount", | |||||
| "GetMarkedCount\r\n", | |||||
| "0;GetMarkStatus;;", | |||||
| "0;;GetMarkStatus;;", | |||||
| "0;GetMarkData;;", | |||||
| "0;;GetMarkData;;", | |||||
| "0;GetMarkedCount;;", | |||||
| "0;;GetMarkedCount;;", | |||||
| ] | |||||
| def send_and_read( | |||||
| host: str, | |||||
| port: int, | |||||
| payload: str, | |||||
| *, | |||||
| encoding: str = "utf-8", | |||||
| shutdown_write: bool = True, | |||||
| read_timeout: float = 3.0, | |||||
| ) -> bytes: | |||||
| data = payload.encode(encoding) | |||||
| with socket.create_connection((host, port), timeout=5) as sock: | |||||
| sock.sendall(data) | |||||
| if shutdown_write: | |||||
| try: | |||||
| sock.shutdown(socket.SHUT_WR) | |||||
| except OSError: | |||||
| pass | |||||
| sock.settimeout(read_timeout) | |||||
| chunks: list[bytes] = [] | |||||
| while True: | |||||
| try: | |||||
| block = sock.recv(8192) | |||||
| except socket.timeout: | |||||
| break | |||||
| if not block: | |||||
| break | |||||
| chunks.append(block) | |||||
| return b"".join(chunks) | |||||
| def interactive(host: str, port: int, shutdown_write: bool) -> None: | |||||
| print(f"Host={host} Port={port} (empty line = quit)") | |||||
| print("Presets: 1=GetMarkStatus; 2=GetMarkData; 3=GetMarkedCount;") | |||||
| print(" 4=0;GetMarkStatus;; 5=0;GetMarkData;; 6=0;GetMarkedCount;;") | |||||
| while True: | |||||
| try: | |||||
| line = input("> ").strip() | |||||
| except (EOFError, KeyboardInterrupt): | |||||
| print() | |||||
| break | |||||
| if not line: | |||||
| break | |||||
| if line == "1": | |||||
| line = "GetMarkStatus;" | |||||
| elif line == "2": | |||||
| line = "GetMarkData;" | |||||
| elif line == "3": | |||||
| line = "GetMarkedCount;" | |||||
| elif line == "4": | |||||
| line = "0;GetMarkStatus;;" | |||||
| elif line == "5": | |||||
| line = "0;GetMarkData;;" | |||||
| elif line == "6": | |||||
| line = "0;GetMarkedCount;;" | |||||
| try: | |||||
| raw = send_and_read(host, port, line, shutdown_write=shutdown_write) | |||||
| except OSError as e: | |||||
| print(f"ERROR: {e}", file=sys.stderr) | |||||
| continue | |||||
| text = raw.decode("utf-8", errors="replace") | |||||
| print(f"Sent {len(line.encode('utf-8'))} bytes, recv {len(raw)} bytes:") | |||||
| print(repr(text)) | |||||
| if text.strip(): | |||||
| print(text) | |||||
| def sweep(host: str, port: int, shutdown_write: bool) -> int: | |||||
| print(f"Sweep Host={host} Port={port}") | |||||
| invalid_hits = 0 | |||||
| non_empty = 0 | |||||
| for i, payload in enumerate(SWEEP_PAYLOADS, start=1): | |||||
| try: | |||||
| raw = send_and_read(host, port, payload, shutdown_write=shutdown_write) | |||||
| text = raw.decode("utf-8", errors="replace").strip() | |||||
| except OSError as e: | |||||
| print(f"[{i:02}] {payload!r} -> ERROR: {e}") | |||||
| continue | |||||
| if text: | |||||
| non_empty += 1 | |||||
| if "errorinvalid data" in text.lower(): | |||||
| invalid_hits += 1 | |||||
| print(f"[{i:02}] {payload!r} -> {text!r}") | |||||
| print(f"Done. non-empty={non_empty}, invalid-data={invalid_hits}") | |||||
| return 0 | |||||
| def main() -> None: | |||||
| ap = argparse.ArgumentParser(description="Send one TCP command to the laser and print the reply.") | |||||
| ap.add_argument("--host", default=DEFAULT_HOST, help=f"default {DEFAULT_HOST}") | |||||
| ap.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"default {DEFAULT_PORT}") | |||||
| ap.add_argument( | |||||
| "-c", | |||||
| "--command", | |||||
| help="Single command to send (e.g. GetMarkStatus;). If omitted, interactive mode.", | |||||
| ) | |||||
| ap.add_argument( | |||||
| "--preset", | |||||
| choices=sorted(PRESETS.keys()), | |||||
| help="Use a built-in payload instead of --command", | |||||
| ) | |||||
| ap.add_argument( | |||||
| "--no-shutdown", | |||||
| action="store_true", | |||||
| help="Do not shutdown(SHUT_WR) after send (some devices differ)", | |||||
| ) | |||||
| ap.add_argument( | |||||
| "--sweep", | |||||
| action="store_true", | |||||
| help="Try multiple payload variants automatically and print all replies.", | |||||
| ) | |||||
| ap.add_argument( | |||||
| "command_positional", | |||||
| nargs="?", | |||||
| help="Same as -c (so you can run: python testLaser.py GetMarkStatus;)", | |||||
| ) | |||||
| args = ap.parse_args() | |||||
| shutdown_write = not args.no_shutdown | |||||
| if args.command and args.command_positional: | |||||
| ap.error("Use either -c/--command or the positional argument, not both") | |||||
| if args.preset and (args.command or args.command_positional): | |||||
| ap.error("Use either --preset or a custom command, not both") | |||||
| if args.sweep and (args.preset or args.command or args.command_positional): | |||||
| ap.error("Use --sweep alone (without --preset/--command)") | |||||
| if args.sweep: | |||||
| raise SystemExit(sweep(args.host, args.port, shutdown_write)) | |||||
| if args.preset: | |||||
| payload, desc = PRESETS[args.preset] | |||||
| print(f"preset {args.preset}: {desc}") | |||||
| elif args.command: | |||||
| payload = args.command | |||||
| elif args.command_positional: | |||||
| payload = args.command_positional | |||||
| else: | |||||
| interactive(args.host, args.port, shutdown_write) | |||||
| return | |||||
| try: | |||||
| raw = send_and_read( | |||||
| args.host, | |||||
| args.port, | |||||
| payload, | |||||
| shutdown_write=shutdown_write, | |||||
| ) | |||||
| except OSError as e: | |||||
| print(f"ERROR: {e}", file=sys.stderr) | |||||
| sys.exit(1) | |||||
| text = raw.decode("utf-8", errors="replace") | |||||
| print(f"Sent to {args.host}:{args.port} -> {payload!r}") | |||||
| print(f"Recv {len(raw)} bytes:") | |||||
| print(repr(text)) | |||||
| if text.strip(): | |||||
| print("---") | |||||
| print(text) | |||||
| if __name__ == "__main__": | |||||
| main() | |||||