yrcom 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yrcom/__init__.py +48 -0
- yrcom/hses_client.py +1659 -0
- yrcom/hses_errors.py +190 -0
- yrcom/hses_file.py +234 -0
- yrcom/hses_sim.py +173 -0
- yrcom/robot_connection.py +434 -0
- yrcom/yaskawa_client.py +615 -0
- yrcom-0.2.0.dist-info/METADATA +112 -0
- yrcom-0.2.0.dist-info/RECORD +13 -0
- yrcom-0.2.0.dist-info/WHEEL +5 -0
- yrcom-0.2.0.dist-info/entry_points.txt +2 -0
- yrcom-0.2.0.dist-info/licenses/LICENSE +21 -0
- yrcom-0.2.0.dist-info/top_level.txt +1 -0
yrcom/yaskawa_client.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yaskawa Ethernet Server client — standalone, zero external dependencies.
|
|
3
|
+
|
|
4
|
+
Supports NX100 and YRC1000 controllers via the HOSTCTRL_REQUEST protocol
|
|
5
|
+
(Ethernet Server Function, TCP port 80).
|
|
6
|
+
|
|
7
|
+
Protocol references:
|
|
8
|
+
NX100: HW0483202 (NX100 OPTIONS INSTRUCTIONS FOR ETHERNET SERVER FUNCTION)
|
|
9
|
+
YRC1000: HW1483358 Chapter 8 (Ethernet Server Function — backward compatible)
|
|
10
|
+
|
|
11
|
+
Typical usage
|
|
12
|
+
─────────────
|
|
13
|
+
# Single transaction (auto connect/close):
|
|
14
|
+
client = YaskawaClient("192.168.1.24")
|
|
15
|
+
status = client.read_rstats()
|
|
16
|
+
|
|
17
|
+
# Persistent connection (context manager):
|
|
18
|
+
with YaskawaClient("192.168.1.24", timeout=2) as client:
|
|
19
|
+
client.send_start(keep_alive=3)
|
|
20
|
+
v = client.read_variable(VarType.BYTE, 10, persistent=True)
|
|
21
|
+
client.write_variable(VarType.BYTE, 10, 5, persistent=True)
|
|
22
|
+
|
|
23
|
+
# Block write (one TCP session, N commands):
|
|
24
|
+
with YaskawaClient(ip) as client:
|
|
25
|
+
ok = client.write_variable_block([
|
|
26
|
+
(VarType.BYTE, 10, 5),
|
|
27
|
+
(VarType.DOUBLE, 1, 1000),
|
|
28
|
+
])
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import socket
|
|
34
|
+
from typing import Any, Literal, overload
|
|
35
|
+
|
|
36
|
+
from enum import IntEnum
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VarType(IntEnum):
|
|
40
|
+
"""Yaskawa variable types (SAVEV / LOADV Data-1 parameter).
|
|
41
|
+
|
|
42
|
+
Single source of truth for yrcom.
|
|
43
|
+
NX100 manual §3.2 / YRC1000 manual Ch.8 §8.3.2.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
BYTE = 0 # B-variable (8-bit)
|
|
47
|
+
INTEGER = 1 # I-variable (signed 16-bit)
|
|
48
|
+
DOUBLE = 2 # D-variable (double-precision INTEGER, signed 32-bit — not a float!)
|
|
49
|
+
FLOAT = 3 # R-variable (real / IEEE-754 float)
|
|
50
|
+
ROBOT_POS = 4 # P-variable (robot-axis position)
|
|
51
|
+
BASE_POS = 5 # BP-variable (base-axis position)
|
|
52
|
+
STATION_POS = 6 # EX-variable / station-axis position (pulse type only)
|
|
53
|
+
STRING = 7 # S-variable (character string)
|
|
54
|
+
|
|
55
|
+
# ── Public constants ──────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
DEFAULT_PORT: int = 80
|
|
58
|
+
DEFAULT_TIMEOUT: float = 5.0
|
|
59
|
+
_RECV_SIZE: int = 4096
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── Type aliases ──────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
ParsedData = list[int] | list[str] | dict[str, Any]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Client ────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
class YaskawaClient:
|
|
70
|
+
"""
|
|
71
|
+
Yaskawa Ethernet Server protocol client (NX100 / YRC1000).
|
|
72
|
+
|
|
73
|
+
Protocol flow
|
|
74
|
+
─────────────
|
|
75
|
+
1. TCP connect to robot_ip:80
|
|
76
|
+
2. Send: CONNECT Robot_access\\r\\n
|
|
77
|
+
(or CONNECT Robot_access Keep-Alive:N\\r\\n for N commands)
|
|
78
|
+
3. Recv: OK: NX Information Server ...\\r\\n
|
|
79
|
+
4. Send: HOSTCTRL_REQUEST {CMD} {len(data)}\\r\\n
|
|
80
|
+
5. Recv: OK: {CMD}\\r\\n
|
|
81
|
+
6. Send: {data}\\r (if len(data) > 0)
|
|
82
|
+
7. Recv: {answer}\\r (or {answer}\\r\\n for "0000" acks)
|
|
83
|
+
8. Repeat steps 4-7 if Keep-Alive; socket auto-closes after last command.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
─────
|
|
87
|
+
- Data size in the HOSTCTRL_REQUEST header is the byte length of the data
|
|
88
|
+
string *including* the trailing \\r.
|
|
89
|
+
- IO data is always in multiples of 8 contact points (one byte per 8 bits).
|
|
90
|
+
- IOWRITE can only target network inputs #22010–#23287 on NX100.
|
|
91
|
+
- START: pass job name without the .JBI extension.
|
|
92
|
+
- SETMJ/GETMJ require Remote/Play mode on the controller.
|
|
93
|
+
- write + read-back: always use separate send_start() sessions to avoid
|
|
94
|
+
double send_start() on the same connection returning empty responses.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
host: str,
|
|
100
|
+
port: int = DEFAULT_PORT,
|
|
101
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
102
|
+
) -> None:
|
|
103
|
+
self.host = host
|
|
104
|
+
self.port = port
|
|
105
|
+
self.timeout = timeout
|
|
106
|
+
self.sock: socket.socket | None = None
|
|
107
|
+
|
|
108
|
+
# ── Context manager ───────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> "YaskawaClient":
|
|
111
|
+
self.connect()
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def __exit__(self, *_: object) -> None:
|
|
115
|
+
self.close()
|
|
116
|
+
|
|
117
|
+
# ── Connection ────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def connect(self) -> None:
|
|
120
|
+
"""Open TCP socket to the controller. No-op if already connected."""
|
|
121
|
+
if self.sock is not None:
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
|
|
125
|
+
except Exception:
|
|
126
|
+
self.sock = None
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
def close(self) -> None:
|
|
130
|
+
"""Close TCP socket."""
|
|
131
|
+
if self.sock:
|
|
132
|
+
self.sock.close()
|
|
133
|
+
self.sock = None
|
|
134
|
+
|
|
135
|
+
# ── Protocol primitives ───────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def send_start(self, keep_alive: int | None = None) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Send CONNECT Robot_access handshake.
|
|
140
|
+
|
|
141
|
+
keep_alive=None → single-command session (socket closes after 1 command)
|
|
142
|
+
keep_alive=N → multi-command session (N commands before close)
|
|
143
|
+
keep_alive=-1 → infinite keep-alive (close manually or via context mgr)
|
|
144
|
+
"""
|
|
145
|
+
if self.sock is None:
|
|
146
|
+
raise RuntimeError("Socket not connected — call connect() first")
|
|
147
|
+
if keep_alive is None:
|
|
148
|
+
line = "CONNECT Robot_access\r\n"
|
|
149
|
+
else:
|
|
150
|
+
line = f"CONNECT Robot_access Keep-Alive:{keep_alive}\r\n"
|
|
151
|
+
self.sock.sendall(line.encode("ascii"))
|
|
152
|
+
return self._recv_until_ok()
|
|
153
|
+
|
|
154
|
+
def send_command(self, command: str, size: int = 0, data: str = "") -> str:
|
|
155
|
+
"""
|
|
156
|
+
Send HOSTCTRL_REQUEST header and optional data, return raw answer string.
|
|
157
|
+
Expects send_start() to have been called on this session already.
|
|
158
|
+
"""
|
|
159
|
+
if self.sock is None:
|
|
160
|
+
raise RuntimeError("Socket not connected")
|
|
161
|
+
header = f"HOSTCTRL_REQUEST {command} {size}\r\n"
|
|
162
|
+
self.sock.sendall(header.encode("ascii"))
|
|
163
|
+
self._recv_until_ok()
|
|
164
|
+
if size > 0 and data:
|
|
165
|
+
payload = data if data.endswith("\r") else data + "\r"
|
|
166
|
+
self.sock.sendall(payload.encode("ascii"))
|
|
167
|
+
return self._recv_answer()
|
|
168
|
+
|
|
169
|
+
def _recv_until_ok(self) -> str:
|
|
170
|
+
"""Receive until OK or NG appears in the buffer."""
|
|
171
|
+
if self.sock is None:
|
|
172
|
+
raise RuntimeError("Socket not connected")
|
|
173
|
+
buf = ""
|
|
174
|
+
while True:
|
|
175
|
+
chunk = self.sock.recv(_RECV_SIZE)
|
|
176
|
+
if not chunk:
|
|
177
|
+
break
|
|
178
|
+
buf += chunk.decode("ascii")
|
|
179
|
+
if "OK" in buf or "NG" in buf:
|
|
180
|
+
break
|
|
181
|
+
return buf
|
|
182
|
+
|
|
183
|
+
def _recv_answer(self) -> str:
|
|
184
|
+
"""Receive the answer data (ends with \\r or \\r\\n)."""
|
|
185
|
+
if self.sock is None:
|
|
186
|
+
raise RuntimeError("Socket not connected")
|
|
187
|
+
buf = ""
|
|
188
|
+
while True:
|
|
189
|
+
chunk = self.sock.recv(_RECV_SIZE)
|
|
190
|
+
if not chunk:
|
|
191
|
+
break
|
|
192
|
+
buf += chunk.decode("ascii")
|
|
193
|
+
if "\r\n" in buf or "\r" in buf:
|
|
194
|
+
break
|
|
195
|
+
return buf
|
|
196
|
+
|
|
197
|
+
# ── Transaction patterns ──────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
@overload
|
|
200
|
+
def transaction(self, command: str, size: int = ..., data: str = ...,
|
|
201
|
+
keep_alive: int | None = ..., parse: Literal[True] = ...) -> ParsedData: ...
|
|
202
|
+
@overload
|
|
203
|
+
def transaction(self, command: str, size: int = ..., data: str = ...,
|
|
204
|
+
keep_alive: int | None = ..., parse: Literal[False] = ...) -> str: ...
|
|
205
|
+
|
|
206
|
+
def transaction(
|
|
207
|
+
self,
|
|
208
|
+
command: str,
|
|
209
|
+
size: int = 0,
|
|
210
|
+
data: str = "",
|
|
211
|
+
keep_alive: int | None = None,
|
|
212
|
+
parse: bool = True,
|
|
213
|
+
) -> ParsedData | str:
|
|
214
|
+
"""
|
|
215
|
+
Self-contained transaction: auto-connect if needed, auto-close if we opened.
|
|
216
|
+
When called inside an open with-block, reuses the existing socket.
|
|
217
|
+
"""
|
|
218
|
+
was_connected = self.sock is not None
|
|
219
|
+
self.connect()
|
|
220
|
+
try:
|
|
221
|
+
self.send_start(keep_alive)
|
|
222
|
+
raw = self.send_command(command, size, data)
|
|
223
|
+
if parse:
|
|
224
|
+
part = raw.split("OK")[-1] if "OK" in raw else raw
|
|
225
|
+
return self._parse(command, part.strip())
|
|
226
|
+
return raw
|
|
227
|
+
finally:
|
|
228
|
+
if not was_connected:
|
|
229
|
+
self.close()
|
|
230
|
+
|
|
231
|
+
@overload
|
|
232
|
+
def send_transaction(self, command: str, size: int = ..., data: str = ...,
|
|
233
|
+
parse: Literal[True] = ...) -> ParsedData: ...
|
|
234
|
+
@overload
|
|
235
|
+
def send_transaction(self, command: str, size: int = ..., data: str = ...,
|
|
236
|
+
parse: Literal[False] = ...) -> str: ...
|
|
237
|
+
|
|
238
|
+
def send_transaction(
|
|
239
|
+
self,
|
|
240
|
+
command: str,
|
|
241
|
+
size: int = 0,
|
|
242
|
+
data: str = "",
|
|
243
|
+
parse: bool = True,
|
|
244
|
+
) -> ParsedData | str:
|
|
245
|
+
"""
|
|
246
|
+
Execute one command on an *already-open* session (after send_start()).
|
|
247
|
+
Use this inside a with-block for multi-command sessions.
|
|
248
|
+
"""
|
|
249
|
+
raw = self.send_command(command, size, data)
|
|
250
|
+
if parse:
|
|
251
|
+
part = raw.split("OK")[-1] if "OK" in raw else raw
|
|
252
|
+
return self._parse(command, part.strip())
|
|
253
|
+
return raw
|
|
254
|
+
|
|
255
|
+
def _exec(self, persistent: bool, command: str, size: int = 0,
|
|
256
|
+
data: str = "", parse: bool = True) -> ParsedData | str:
|
|
257
|
+
"""Route one command to the persistent (send_transaction) or one-shot
|
|
258
|
+
(transaction) path — both share the command/size/data/parse signature."""
|
|
259
|
+
if persistent:
|
|
260
|
+
return self.send_transaction(command, size=size, data=data, parse=parse)
|
|
261
|
+
return self.transaction(command, size=size, data=data, parse=parse)
|
|
262
|
+
|
|
263
|
+
# ── Parsing ───────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _clean(text: str) -> str:
|
|
267
|
+
return text.replace("\r\n", "").replace("\r", "").strip()
|
|
268
|
+
|
|
269
|
+
def _parse(self, command: str, answer: str) -> ParsedData:
|
|
270
|
+
answer = self._clean(answer)
|
|
271
|
+
parts = [p for p in answer.split(",") if p]
|
|
272
|
+
|
|
273
|
+
if command == "RSTATS":
|
|
274
|
+
return [int(p) for p in parts]
|
|
275
|
+
if command == "RPOSJ":
|
|
276
|
+
return [int(p) for p in parts]
|
|
277
|
+
if command == "RPOSC":
|
|
278
|
+
return {
|
|
279
|
+
"X": float(parts[0]), "Y": float(parts[1]), "Z": float(parts[2]),
|
|
280
|
+
"Rx": float(parts[3]), "Ry": float(parts[4]), "Rz": float(parts[5]),
|
|
281
|
+
"Type": int(parts[6]), "ToolNo": int(parts[7]),
|
|
282
|
+
}
|
|
283
|
+
if command == "RJSEQ":
|
|
284
|
+
return {"JobName": parts[0], "Line": int(parts[1]), "Step": int(parts[2])}
|
|
285
|
+
if command == "RALARM":
|
|
286
|
+
return [int(p) for p in parts]
|
|
287
|
+
if command == "IOREAD":
|
|
288
|
+
return [int(p) for p in parts]
|
|
289
|
+
if command == "RJDIR":
|
|
290
|
+
return parts
|
|
291
|
+
return parts
|
|
292
|
+
|
|
293
|
+
# ── Variable payload helpers ──────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _var_payload(var_type: VarType, var_no: int, value: Any = None) -> str:
|
|
297
|
+
vt = int(var_type)
|
|
298
|
+
if value is None:
|
|
299
|
+
return f"{vt},{var_no}\r"
|
|
300
|
+
if var_type in (VarType.BYTE, VarType.INTEGER, VarType.DOUBLE, VarType.FLOAT, VarType.STRING):
|
|
301
|
+
return f"{vt},{var_no},{value}\r"
|
|
302
|
+
if var_type in (VarType.ROBOT_POS, VarType.BASE_POS, VarType.STATION_POS):
|
|
303
|
+
if isinstance(value, (list, tuple)):
|
|
304
|
+
return f"{vt},{var_no}," + ",".join(str(v) for v in value) + "\r"
|
|
305
|
+
raise ValueError("Position variable types require a list/tuple value")
|
|
306
|
+
raise ValueError(f"Unknown VarType: {var_type}")
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _parse_var(var_type: VarType, raw: str) -> Any:
|
|
310
|
+
if var_type in (VarType.BYTE, VarType.INTEGER, VarType.DOUBLE):
|
|
311
|
+
try:
|
|
312
|
+
return int(raw)
|
|
313
|
+
except (ValueError, TypeError):
|
|
314
|
+
return None
|
|
315
|
+
if var_type == VarType.FLOAT:
|
|
316
|
+
try:
|
|
317
|
+
return float(raw)
|
|
318
|
+
except (ValueError, TypeError):
|
|
319
|
+
return None
|
|
320
|
+
if var_type in (VarType.ROBOT_POS, VarType.BASE_POS, VarType.STATION_POS):
|
|
321
|
+
return [float(p) if "." in p else int(p) for p in raw.split(",") if p]
|
|
322
|
+
return raw # STRING
|
|
323
|
+
|
|
324
|
+
# ── Variable read / write ─────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
def read_variable(self, var_type: VarType, var_no: int, persistent: bool = False) -> Any:
|
|
327
|
+
"""Read a single variable (SAVEV). persistent=True: call inside existing session."""
|
|
328
|
+
data = self._var_payload(var_type, var_no)
|
|
329
|
+
raw = self._exec(persistent, "SAVEV", size=len(data), data=data, parse=False)
|
|
330
|
+
return self._parse_var(var_type, self._clean(raw))
|
|
331
|
+
|
|
332
|
+
def write_variable(self, var_type: VarType, var_no: int, value: Any, persistent: bool = False) -> bool:
|
|
333
|
+
"""Write a single variable (LOADV). persistent=True: call inside existing session."""
|
|
334
|
+
data = self._var_payload(var_type, var_no, value)
|
|
335
|
+
raw = self._exec(persistent, "LOADV", size=len(data), data=data, parse=False)
|
|
336
|
+
return self._clean(raw) == "0000"
|
|
337
|
+
|
|
338
|
+
def write_variable_block(self, var_tuples: list[tuple]) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Write N variables in one TCP session (one send_start, N LOADV commands).
|
|
341
|
+
var_tuples: [(VarType, var_no, value), ...]
|
|
342
|
+
Call inside a with-block (socket must already be open).
|
|
343
|
+
"""
|
|
344
|
+
self.send_start(keep_alive=len(var_tuples))
|
|
345
|
+
for var_type, var_no, value in var_tuples:
|
|
346
|
+
data = self._var_payload(var_type, var_no, value)
|
|
347
|
+
raw = self.send_command("LOADV", size=len(data), data=data)
|
|
348
|
+
if self._clean(raw) != "0000":
|
|
349
|
+
return False
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
def read_variable_block(self, var_tuples: list[tuple]) -> dict:
|
|
353
|
+
"""
|
|
354
|
+
Read N variables in one TCP session.
|
|
355
|
+
var_tuples: [(VarType, var_no), ...]
|
|
356
|
+
Call inside a with-block (socket must already be open).
|
|
357
|
+
Returns: {(VarType, var_no): value}
|
|
358
|
+
"""
|
|
359
|
+
results: dict = {}
|
|
360
|
+
self.send_start(keep_alive=len(var_tuples))
|
|
361
|
+
for var_type, var_no in var_tuples:
|
|
362
|
+
data = self._var_payload(var_type, var_no)
|
|
363
|
+
raw = self.send_command("SAVEV", size=len(data), data=data)
|
|
364
|
+
results[(var_type, var_no)] = self._parse_var(var_type, self._clean(raw))
|
|
365
|
+
return results
|
|
366
|
+
|
|
367
|
+
# ── IO ────────────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
def io_read(self, start_no: int, num_points: int, persistent: bool = False) -> list[int]:
|
|
370
|
+
"""
|
|
371
|
+
Read IO bits (IOREAD). Returns one integer per 8-bit group.
|
|
372
|
+
|
|
373
|
+
start_no: Yaskawa robot IO address (e.g. 10010 for IN#1, 50010 for OUT#1).
|
|
374
|
+
num_points: Number of bits to read; must be a multiple of 8.
|
|
375
|
+
"""
|
|
376
|
+
if num_points % 8:
|
|
377
|
+
raise ValueError("num_points must be a multiple of 8")
|
|
378
|
+
data = f"{start_no},{num_points}\r"
|
|
379
|
+
raw = self._exec(persistent, "IOREAD", size=len(data), data=data, parse=False)
|
|
380
|
+
return [int(v) for v in self._clean(raw).split(",") if v]
|
|
381
|
+
|
|
382
|
+
def io_write(self, start_no: int, values: list[int], persistent: bool = False) -> bool:
|
|
383
|
+
"""
|
|
384
|
+
Write IO bits (IOWRITE). values: one byte value per 8 contacts.
|
|
385
|
+
NX100: only network inputs #22010–#23287 can be written.
|
|
386
|
+
"""
|
|
387
|
+
num_points = len(values) * 8
|
|
388
|
+
data = f"{start_no},{num_points}," + ",".join(str(int(v)) for v in values) + "\r"
|
|
389
|
+
raw = self._exec(persistent, "IOWRITE", size=len(data), data=data, parse=False)
|
|
390
|
+
return self._clean(raw) == "0000"
|
|
391
|
+
|
|
392
|
+
def read_io_bit(self, address: int, persistent: bool = False) -> bool:
|
|
393
|
+
"""
|
|
394
|
+
Read a single IO bit by Yaskawa robot address (e.g. 50023, 80023).
|
|
395
|
+
Uses IOREAD on the 8-bit block containing the address.
|
|
396
|
+
Yaskawa address format: last digit = bit index (0-7), rest = byte base.
|
|
397
|
+
"""
|
|
398
|
+
bit = address % 10
|
|
399
|
+
base = address - bit
|
|
400
|
+
values = self.io_read(base, 8, persistent=persistent)
|
|
401
|
+
return bool((values[0] >> bit) & 1)
|
|
402
|
+
|
|
403
|
+
# ── Status ────────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
def read_rstats(self, decode: bool = True, persistent: bool = False) -> dict[str, bool] | list[int]:
|
|
406
|
+
"""
|
|
407
|
+
Read robot status (RSTATS).
|
|
408
|
+
|
|
409
|
+
decode=True → dict with named booleans (recommended)
|
|
410
|
+
decode=False → raw [data1, data2] integers
|
|
411
|
+
|
|
412
|
+
data1 bits D0=step D1=1cycle D2=auto D3=running D4=speed_limit
|
|
413
|
+
D5=teach D6=play D7=cmd_remote
|
|
414
|
+
data2 bits D1=hold_pendant D2=hold_external D3=hold_cmd
|
|
415
|
+
D4=alarm D5=error D6=servo_on
|
|
416
|
+
"""
|
|
417
|
+
raw = self._exec(persistent, "RSTATS")
|
|
418
|
+
if not decode:
|
|
419
|
+
return raw # type: ignore[return-value]
|
|
420
|
+
if isinstance(raw, str):
|
|
421
|
+
raw = [int(p) for p in self._clean(raw).split(",") if p]
|
|
422
|
+
return self._decode_rstats(raw) # type: ignore[arg-type]
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _decode_rstats(data: list[int]) -> dict[str, bool]:
|
|
426
|
+
d1, d2 = data[0], data[1]
|
|
427
|
+
return {
|
|
428
|
+
"step": bool(d1 & (1 << 0)),
|
|
429
|
+
"one_cycle": bool(d1 & (1 << 1)),
|
|
430
|
+
"auto": bool(d1 & (1 << 2)),
|
|
431
|
+
"running": bool(d1 & (1 << 3)),
|
|
432
|
+
"speed_limit": bool(d1 & (1 << 4)),
|
|
433
|
+
"teach": bool(d1 & (1 << 5)),
|
|
434
|
+
"play": bool(d1 & (1 << 6)),
|
|
435
|
+
"cmd_remote": bool(d1 & (1 << 7)),
|
|
436
|
+
"hold_pendant": bool(d2 & (1 << 1)),
|
|
437
|
+
"hold_external": bool(d2 & (1 << 2)),
|
|
438
|
+
"hold_cmd": bool(d2 & (1 << 3)),
|
|
439
|
+
"alarm": bool(d2 & (1 << 4)),
|
|
440
|
+
"error": bool(d2 & (1 << 5)),
|
|
441
|
+
"servo_on": bool(d2 & (1 << 6)),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
def read_alarm(self, persistent: bool = False) -> list[int]:
|
|
445
|
+
"""
|
|
446
|
+
Read error/alarm codes (RALARM).
|
|
447
|
+
Returns [error_code, error_data, alarm1_code, alarm1_data, ...alarm4_code, alarm4_data].
|
|
448
|
+
"""
|
|
449
|
+
raw = self._exec(persistent, "RALARM")
|
|
450
|
+
return raw if isinstance(raw, list) else [] # type: ignore[return-value]
|
|
451
|
+
|
|
452
|
+
def read_position_joint(self, persistent: bool = False) -> list[int]:
|
|
453
|
+
"""Read current joint position in pulses (RPOSJ). Returns [S, L, U, R, B, T, ...]."""
|
|
454
|
+
raw = self._exec(persistent, "RPOSJ")
|
|
455
|
+
return raw if isinstance(raw, list) else [] # type: ignore[return-value]
|
|
456
|
+
|
|
457
|
+
def read_position_cartesian(
|
|
458
|
+
self,
|
|
459
|
+
coord: int = 0,
|
|
460
|
+
external_axis: bool = False,
|
|
461
|
+
persistent: bool = False,
|
|
462
|
+
) -> dict:
|
|
463
|
+
"""
|
|
464
|
+
Read current Cartesian position (RPOSC).
|
|
465
|
+
coord: 0=base, 1=robot, 2-25=user coordinate 1-24.
|
|
466
|
+
Returns: {X, Y, Z, Rx, Ry, Rz, Type, ToolNo}
|
|
467
|
+
"""
|
|
468
|
+
data = f"{coord},{1 if external_axis else 0}\r"
|
|
469
|
+
raw = self._exec(persistent, "RPOSC", size=len(data), data=data)
|
|
470
|
+
return raw if isinstance(raw, dict) else {}
|
|
471
|
+
|
|
472
|
+
def read_job_seq(self, persistent: bool = False) -> dict:
|
|
473
|
+
"""Read current job name, line No., step No. (RJSEQ). Returns {JobName, Line, Step}."""
|
|
474
|
+
raw = self._exec(persistent, "RJSEQ")
|
|
475
|
+
return raw if isinstance(raw, dict) else {}
|
|
476
|
+
|
|
477
|
+
def list_jobs(self, parent_job: str = "*", persistent: bool = False) -> list[str]:
|
|
478
|
+
"""
|
|
479
|
+
List registered job names (RJDIR).
|
|
480
|
+
parent_job='*' → all jobs; pass a job name to list its child jobs.
|
|
481
|
+
"""
|
|
482
|
+
data = f"{parent_job}\r"
|
|
483
|
+
raw = self._exec(persistent, "RJDIR", size=len(data), data=data, parse=False)
|
|
484
|
+
return [n for n in self._clean(raw).split(",") if n]
|
|
485
|
+
|
|
486
|
+
# ── Control commands ──────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
def servo_on(self, on: bool = True, persistent: bool = False) -> bool:
|
|
489
|
+
"""Set servo power (SVON). on=True → ON, on=False → OFF."""
|
|
490
|
+
data = f"{1 if on else 0}\r"
|
|
491
|
+
raw = self._exec(persistent, "SVON", size=len(data), data=data, parse=False)
|
|
492
|
+
return self._clean(raw).startswith("0000")
|
|
493
|
+
|
|
494
|
+
def hold(self, on: bool = True, persistent: bool = False) -> bool:
|
|
495
|
+
"""Set hold (HOLD). on=True → hold on, on=False → release hold."""
|
|
496
|
+
data = f"{1 if on else 0}\r"
|
|
497
|
+
raw = self._exec(persistent, "HOLD", size=len(data), data=data, parse=False)
|
|
498
|
+
return self._clean(raw).startswith("0000")
|
|
499
|
+
|
|
500
|
+
def reset(self, persistent: bool = False) -> bool:
|
|
501
|
+
"""Reset alarm (RESET). Transmission alarms can only be reset from the programming pendant."""
|
|
502
|
+
raw = self._exec(persistent, "RESET", parse=False)
|
|
503
|
+
return self._clean(raw).startswith("0000")
|
|
504
|
+
|
|
505
|
+
def cancel(self, persistent: bool = False) -> bool:
|
|
506
|
+
"""Cancel error (CANCEL)."""
|
|
507
|
+
raw = self._exec(persistent, "CANCEL", parse=False)
|
|
508
|
+
return self._clean(raw).startswith("0000")
|
|
509
|
+
|
|
510
|
+
def set_mode(self, mode: int, persistent: bool = False) -> bool:
|
|
511
|
+
"""
|
|
512
|
+
Set operation mode (MODE). Requires external mode switch enabled on controller.
|
|
513
|
+
mode: 1=Teach, 2=Play
|
|
514
|
+
"""
|
|
515
|
+
data = f"{mode}\r"
|
|
516
|
+
raw = self._exec(persistent, "MODE", size=len(data), data=data, parse=False)
|
|
517
|
+
return self._clean(raw).startswith("0000")
|
|
518
|
+
|
|
519
|
+
def set_cycle(self, cycle: int, persistent: bool = False) -> bool:
|
|
520
|
+
"""
|
|
521
|
+
Set cycle mode (CYCLE).
|
|
522
|
+
cycle: 1=Step, 2=1cycle, 3=Auto
|
|
523
|
+
"""
|
|
524
|
+
data = f"{cycle}\r"
|
|
525
|
+
raw = self._exec(persistent, "CYCLE", size=len(data), data=data, parse=False)
|
|
526
|
+
return self._clean(raw).startswith("0000")
|
|
527
|
+
|
|
528
|
+
def hlock(self, on: bool = True, persistent: bool = False) -> bool:
|
|
529
|
+
"""
|
|
530
|
+
Set programming-pendant interlock (HLOCK).
|
|
531
|
+
on=True: locks out pendant and I/O except emergency stop.
|
|
532
|
+
"""
|
|
533
|
+
data = f"{1 if on else 0}\r"
|
|
534
|
+
raw = self._exec(persistent, "HLOCK", size=len(data), data=data, parse=False)
|
|
535
|
+
return self._clean(raw).startswith("0000")
|
|
536
|
+
|
|
537
|
+
def start_job(self, jobname: str | None = None, persistent: bool = False) -> bool:
|
|
538
|
+
"""
|
|
539
|
+
Start a job (START). Servo must be ON before calling with a job name.
|
|
540
|
+
jobname=None → start from current program pointer.
|
|
541
|
+
jobname='FOO' → start FOO from the beginning (no .JBI extension).
|
|
542
|
+
"""
|
|
543
|
+
data = f"{jobname}\r" if jobname else "\r"
|
|
544
|
+
raw = self._exec(persistent, "START", size=len(data), data=data, parse=False)
|
|
545
|
+
return self._clean(raw).startswith("0000")
|
|
546
|
+
|
|
547
|
+
def set_master_job(self, jobname: str, persistent: bool = False) -> bool:
|
|
548
|
+
"""
|
|
549
|
+
Set master job permanently in the controller (SETMJ).
|
|
550
|
+
Also sets it as the current execution job.
|
|
551
|
+
Requires Remote/Play mode; error 2100 otherwise.
|
|
552
|
+
"""
|
|
553
|
+
data = f"{jobname}\r"
|
|
554
|
+
raw = self._exec(persistent, "SETMJ", size=len(data), data=data, parse=False)
|
|
555
|
+
return self._clean(raw) == "0000"
|
|
556
|
+
|
|
557
|
+
def get_master_job(self, persistent: bool = False) -> str | None:
|
|
558
|
+
"""Read the currently set master job name (GETMJ). Returns None on error."""
|
|
559
|
+
raw = self._exec(persistent, "GETMJ", parse=False)
|
|
560
|
+
result = self._clean(raw)
|
|
561
|
+
return result if result else None
|
|
562
|
+
|
|
563
|
+
def set_job_seq(self, jobname: str, line: int, persistent: bool = False) -> bool:
|
|
564
|
+
"""Set job name and execution line number (JSEQ)."""
|
|
565
|
+
data = f"{jobname},{line}\r"
|
|
566
|
+
raw = self._exec(persistent, "JSEQ", size=len(data), data=data, parse=False)
|
|
567
|
+
return self._clean(raw) == "0000"
|
|
568
|
+
|
|
569
|
+
def display_message(self, message: str, persistent: bool = False) -> bool:
|
|
570
|
+
"""
|
|
571
|
+
Display a message on the programming-pendant remote display (MDSP).
|
|
572
|
+
Maximum 30 bytes; truncated silently if longer.
|
|
573
|
+
"""
|
|
574
|
+
data = f"{message[:30]}\r"
|
|
575
|
+
raw = self._exec(persistent, "MDSP", size=len(data), data=data, parse=False)
|
|
576
|
+
return self._clean(raw) == "0000"
|
|
577
|
+
|
|
578
|
+
def set_control_group(self, robot_bits: int, station_bits: int, persistent: bool = False) -> bool:
|
|
579
|
+
"""
|
|
580
|
+
Change the target control group (CGROUP).
|
|
581
|
+
robot_bits: bitmask of robots (D0=R1, D1=R2, D2=R3, D3=R4).
|
|
582
|
+
station_bits: bitmask of stations (D0=S1 … D11=S12).
|
|
583
|
+
"""
|
|
584
|
+
data = f"{robot_bits},{station_bits}\r"
|
|
585
|
+
raw = self._exec(persistent, "CGROUP", size=len(data), data=data, parse=False)
|
|
586
|
+
return self._clean(raw) == "0000"
|
|
587
|
+
|
|
588
|
+
def delete_job(self, jobname: str = "*", persistent: bool = False) -> bool:
|
|
589
|
+
"""
|
|
590
|
+
Delete a job (DELETE). jobname='*' deletes ALL registered jobs.
|
|
591
|
+
"""
|
|
592
|
+
data = f"{jobname}\r"
|
|
593
|
+
raw = self._exec(persistent, "DELETE", size=len(data), data=data, parse=False)
|
|
594
|
+
return self._clean(raw) == "0000"
|
|
595
|
+
|
|
596
|
+
# ── IO address helpers ────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def io_addr_to_byte_bit(rob_addr: int) -> tuple[int, int]:
|
|
600
|
+
"""
|
|
601
|
+
Split a Yaskawa robot IO address into (byte_base, bit_index).
|
|
602
|
+
Each Yaskawa IO byte spans 10 address units; the last digit is the bit.
|
|
603
|
+
|
|
604
|
+
Examples:
|
|
605
|
+
50053 → (50050, 3) bit 3 of byte at 50050
|
|
606
|
+
10015 → (10010, 5) bit 5 of byte at 10010
|
|
607
|
+
50020 → (50020, 0) bit 0 of byte at 50020
|
|
608
|
+
"""
|
|
609
|
+
bit = rob_addr % 10
|
|
610
|
+
return rob_addr - bit, bit
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# ── Backwards-compatible alias ────────────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
EthernetServerClient = YaskawaClient
|