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/hses_client.py
ADDED
|
@@ -0,0 +1,1659 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Yaskawa YRC1000 — High-Speed Ethernet Server (HSES) client.
|
|
3
|
+
UDP port 10040, YERC binary protocol. No external dependencies.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import socket
|
|
9
|
+
import struct
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Optional, Self
|
|
13
|
+
|
|
14
|
+
from .hses_errors import status_text, added_status_text
|
|
15
|
+
|
|
16
|
+
# Diagnostic log of the HSES layer (requests/responses, control commands, errors).
|
|
17
|
+
# This is a *software* trace for field debugging — not the telemetry CSV. The
|
|
18
|
+
# library only emits records; the application decides handlers/levels. Requests
|
|
19
|
+
# and responses go to DEBUG, submitted control commands to INFO, and any robot
|
|
20
|
+
# error / malformed reply to ERROR.
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
HSES_PORT = 10040
|
|
24
|
+
_MAGIC = b"YERC"
|
|
25
|
+
_HDR_SIZE = 0x0020 # 32 bytes, always
|
|
26
|
+
_PROC_ROBOT = 1 # processing division: robot control
|
|
27
|
+
|
|
28
|
+
# Command numbers per HW1483358 Section 3.3.3.x
|
|
29
|
+
_CMD_ALARM = 0x70 # Alarm Data Reading
|
|
30
|
+
_CMD_ALARM_HIST = 0x71 # Alarm History Reading
|
|
31
|
+
_CMD_STATUS = 0x72 # Status Information Reading
|
|
32
|
+
_CMD_JOB = 0x73 # Executing Job Information Reading
|
|
33
|
+
_CMD_AXIS_CFG = 0x74 # Axis Configuration Information Reading
|
|
34
|
+
_CMD_POSITION = 0x75 # Robot Position Data Reading
|
|
35
|
+
_CMD_POSERR = 0x76 # Position Error (following error) Reading
|
|
36
|
+
_CMD_TORQUE = 0x77 # Torque Data Reading
|
|
37
|
+
_CMD_IO = 0x78 # I/O Data Reading
|
|
38
|
+
_CMD_REGISTER = 0x79 # Register Data Reading / Writing
|
|
39
|
+
_CMD_B_VAR = 0x7A # Byte Variable (B) Reading
|
|
40
|
+
_CMD_I_VAR = 0x7B # Integer Variable (I) Reading
|
|
41
|
+
_CMD_D_VAR = 0x7C # Double Variable (D) Reading / Writing
|
|
42
|
+
_CMD_R_VAR = 0x7D # Real Variable (R) Reading / Writing
|
|
43
|
+
_CMD_S_VAR = 0x8C # 32-Byte Character Variable (S) Reading / Writing (DX200/YRC1000)
|
|
44
|
+
_CMD_S16_VAR = 0x7E # 16-Byte Character Variable (S) Reading / Writing (DX100)
|
|
45
|
+
_CMD_P_VAR = 0x7F # Robot Position Variable (P) Reading / Writing
|
|
46
|
+
_CMD_BP_VAR = 0x80 # Base Position Variable (BP) Reading / Writing
|
|
47
|
+
_CMD_EX_VAR = 0x81 # Station/External Axis Variable (EX) Reading / Writing
|
|
48
|
+
_CMD_RESET = 0x82 # Alarm Reset / Error Cancel
|
|
49
|
+
_CMD_HOLD_SERVO = 0x83 # HOLD / Servo ON-OFF / HLOCK
|
|
50
|
+
_CMD_CYCLE = 0x84 # Step / Cycle / Auto switching
|
|
51
|
+
_CMD_MSG = 0x85 # Character string display to pendant
|
|
52
|
+
_CMD_JOB_START = 0x86 # Start-up (Job START)
|
|
53
|
+
_CMD_JOB_SELECT = 0x87 # Job select
|
|
54
|
+
_CMD_MGMT_TIME = 0x88 # Management Time Acquiring
|
|
55
|
+
_CMD_SYS_INFO = 0x89 # System Information Acquiring
|
|
56
|
+
_CMD_IO_PLURAL = 0x300 # Plural I/O Data Reading / Writing
|
|
57
|
+
_CMD_REG_PLURAL = 0x301 # Plural Register Reading / Writing
|
|
58
|
+
_CMD_B_PLURAL = 0x302 # Plural Byte (B) Reading / Writing
|
|
59
|
+
_CMD_I_PLURAL = 0x303 # Plural Integer (I) Reading / Writing
|
|
60
|
+
_CMD_D_PLURAL = 0x304 # Plural Double (D) Reading / Writing
|
|
61
|
+
_CMD_R_PLURAL = 0x305 # Plural Real (R) Reading / Writing
|
|
62
|
+
_CMD_P_PLURAL = 0x307 # Plural Robot Position (P) Reading / Writing
|
|
63
|
+
_CMD_BP_PLURAL = 0x308 # Plural Base Position (BP) Reading / Writing
|
|
64
|
+
_CMD_EX_PLURAL = 0x309 # Plural Station/External Axis (EX) Reading / Writing
|
|
65
|
+
_CMD_S_PLURAL = 0x30C # Plural 32-Byte Character (S) Reading / Writing (DX200/YRC1000)
|
|
66
|
+
_CMD_S16_PLURAL = 0x306 # Plural 16-Byte Character (S) Reading / Writing (DX100)
|
|
67
|
+
_CMD_ALARM_SUB = 0x30A # Alarm Data Reading (Sub-Code strings)
|
|
68
|
+
_CMD_ALARM_HIST_SUB = 0x30B # Alarm History Reading (Sub-Code strings)
|
|
69
|
+
_CMD_ENC_TEMP = 0x0411 # Encoder Temperature Reading (SW YAS4.81+)
|
|
70
|
+
_CMD_REGEN_PWR = 0x0412 # Regenerative Power Reading (SW YAS4.90+)
|
|
71
|
+
_CMD_CONV_TEMP = 0x0413 # Converter Temperature Reading (SW YAS5.20+)
|
|
72
|
+
|
|
73
|
+
# Controller generations. They share almost every data layout; the differences are
|
|
74
|
+
# the S-variable command/width (DX100 = 16-byte 0x7E/0x306, newer = 32-byte
|
|
75
|
+
# 0x8C/0x30C), the meaning of status 0x72 Data1 bit4, and the YRC1000-only
|
|
76
|
+
# temperature commands (0x0411-0x0413). See manual HW1483358.
|
|
77
|
+
MODEL_DX100 = "dx100"
|
|
78
|
+
MODEL_DX200 = "dx200"
|
|
79
|
+
MODEL_YRC1000 = "yrc1000"
|
|
80
|
+
_VALID_MODELS = (MODEL_DX100, MODEL_DX200, MODEL_YRC1000)
|
|
81
|
+
|
|
82
|
+
_STATUS_CMD_UNDEFINED = 0x08 # "Requested command is undefined" (§4.1)
|
|
83
|
+
_CMD_MOVE_CART = 0x8A # Move Instruction (Cartesian)
|
|
84
|
+
_CMD_MOVE_PULSE = 0x8B # Move Instruction (Pulse)
|
|
85
|
+
|
|
86
|
+
# Service codes
|
|
87
|
+
_SVC_GET_ALL = 0x01
|
|
88
|
+
_SVC_SET_ALL = 0x02
|
|
89
|
+
_SVC_GET_SINGLE = 0x0E
|
|
90
|
+
_SVC_SET_SINGLE = 0x10
|
|
91
|
+
_SVC_READ_ALL = 0x33 # plural read (cmd 0x30x)
|
|
92
|
+
_SVC_WRITE_ALL = 0x34 # plural write (cmd 0x30x)
|
|
93
|
+
|
|
94
|
+
# Instance selectors for HOLD / Servo command (0x83)
|
|
95
|
+
_HS_HOLD = 1
|
|
96
|
+
_HS_SERVO = 2
|
|
97
|
+
_HS_HLOCK = 3
|
|
98
|
+
|
|
99
|
+
# ON/OFF data values shared by control commands
|
|
100
|
+
_ON = 1
|
|
101
|
+
_OFF = 2
|
|
102
|
+
|
|
103
|
+
# Interpolation modes = instance selector for the move commands (0x8A/0x8B)
|
|
104
|
+
_MOVE_LINK_ABS = 1 # joint interpolation to an absolute pose
|
|
105
|
+
_MOVE_STRAIGHT_ABS = 2 # linear interpolation to an absolute pose
|
|
106
|
+
_MOVE_STRAIGHT_INC = 3 # linear interpolation by an increment (0x8A only)
|
|
107
|
+
|
|
108
|
+
# Speed classification for the move commands (value 2 = VR 0.1 °/s is passed as a
|
|
109
|
+
# raw int on the speed_class field; see CartesianMove/PulseMove)
|
|
110
|
+
_SPEED_PERCENT = 0 # 0.01 % of max joint speed (link moves)
|
|
111
|
+
_SPEED_V = 1 # 0.1 mm/s (straight moves)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class _RawResp:
|
|
116
|
+
cmd: int
|
|
117
|
+
status: int # 0x00 = normal
|
|
118
|
+
added: int # added-status word (0 if none)
|
|
119
|
+
data: bytes # raw payload
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class PositionVar:
|
|
124
|
+
"""
|
|
125
|
+
Robot position variable P[number] (cmd 0x7F) — a 52-byte record of 13 int32.
|
|
126
|
+
|
|
127
|
+
`axes` holds the eight raw protocol coordinate values (unused axes are 0).
|
|
128
|
+
For a cartesian data_type (16 Base / 17 Robot / 18 Tool / 19 User) the first
|
|
129
|
+
six are X/Y/Z in µm and Rx/Ry/Rz in 0.0001°; for pulse (data_type 0) each is
|
|
130
|
+
an encoder pulse count. Values are kept raw so a read → write round-trip is
|
|
131
|
+
lossless.
|
|
132
|
+
"""
|
|
133
|
+
data_type: int # 0=Pulse, 16=Base, 17=Robot, 18=Tool, 19=User
|
|
134
|
+
figure: int = 0 # configuration bits (arm flip/no-flip etc.)
|
|
135
|
+
tool_no: int = 0
|
|
136
|
+
user_coord_no: int = 0
|
|
137
|
+
extended_figure: int = 0
|
|
138
|
+
axes: list = field(default_factory=lambda: [0] * 8)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_bytes(cls, data: bytes) -> "PositionVar":
|
|
142
|
+
"""Decode a 52-byte P record; short data is zero-padded to 13 int32."""
|
|
143
|
+
n = len(data) // 4
|
|
144
|
+
vals = list(struct.unpack_from(f"<{n}i", data)) if n else []
|
|
145
|
+
vals += [0] * (13 - len(vals))
|
|
146
|
+
return cls(
|
|
147
|
+
data_type=vals[0], figure=vals[1], tool_no=vals[2],
|
|
148
|
+
user_coord_no=vals[3], extended_figure=vals[4], axes=vals[5:13],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def to_bytes(self) -> bytes:
|
|
152
|
+
"""Encode to the 52-byte request payload (axes padded/validated to 8)."""
|
|
153
|
+
axes = list(self.axes)
|
|
154
|
+
if len(axes) > 8:
|
|
155
|
+
raise ValueError("axes must have at most 8 elements")
|
|
156
|
+
axes += [0] * (8 - len(axes))
|
|
157
|
+
return struct.pack(
|
|
158
|
+
"<13i", self.data_type, self.figure, self.tool_no,
|
|
159
|
+
self.user_coord_no, self.extended_figure, *axes,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class AxisPositionVar:
|
|
165
|
+
"""
|
|
166
|
+
Base position (BP, cmd 0x80) or station/external axis (EX, cmd 0x81)
|
|
167
|
+
variable — a 36-byte record of 9 int32: a data type plus eight axis values.
|
|
168
|
+
|
|
169
|
+
For BP with a cartesian data_type (16) the first three axes are X/Y/Z in µm
|
|
170
|
+
(base/positioner axes carry no orientation); data_type 0 (pulse) and every EX
|
|
171
|
+
record carry raw encoder pulse counts. Values are kept raw so a read → write
|
|
172
|
+
round-trip is lossless.
|
|
173
|
+
"""
|
|
174
|
+
data_type: int # BP: 0=Pulse, 16=Base; EX: 0=Pulse only
|
|
175
|
+
axes: list = field(default_factory=lambda: [0] * 8)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_bytes(cls, data: bytes) -> "AxisPositionVar":
|
|
179
|
+
"""Decode a 36-byte BP/EX record; short data is zero-padded to 9 int32."""
|
|
180
|
+
n = len(data) // 4
|
|
181
|
+
vals = list(struct.unpack_from(f"<{n}i", data)) if n else []
|
|
182
|
+
vals += [0] * (9 - len(vals))
|
|
183
|
+
return cls(data_type=vals[0], axes=vals[1:9])
|
|
184
|
+
|
|
185
|
+
def to_bytes(self) -> bytes:
|
|
186
|
+
"""Encode to the 36-byte request payload (axes padded/validated to 8)."""
|
|
187
|
+
axes = list(self.axes)
|
|
188
|
+
if len(axes) > 8:
|
|
189
|
+
raise ValueError("axes must have at most 8 elements")
|
|
190
|
+
axes += [0] * (8 - len(axes))
|
|
191
|
+
return struct.pack("<9i", self.data_type, *axes)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _pad(values, n: int, name: str) -> list:
|
|
195
|
+
"""Right-pad a raw-int list with zeros to exactly `n` (over-length is an error)."""
|
|
196
|
+
vals = list(values)
|
|
197
|
+
if len(vals) > n:
|
|
198
|
+
raise ValueError(f"{name} must have at most {n} elements")
|
|
199
|
+
return vals + [0] * (n - len(vals))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class CartesianMove:
|
|
204
|
+
"""
|
|
205
|
+
Request record for a Cartesian move (cmd 0x8A) — 104 bytes = 26 int32.
|
|
206
|
+
|
|
207
|
+
Coordinates are raw protocol units: X/Y/Z in µm, Rx/Ry/Rz in 0.0001°. A move
|
|
208
|
+
targets EITHER the robot group OR a station group, never both (the controller
|
|
209
|
+
rejects robot+station together with added-status 0xB008). Servo ON and Command
|
|
210
|
+
Remote are required — this MOVES the robot.
|
|
211
|
+
"""
|
|
212
|
+
x: int = 0
|
|
213
|
+
y: int = 0
|
|
214
|
+
z: int = 0
|
|
215
|
+
rx: int = 0
|
|
216
|
+
ry: int = 0
|
|
217
|
+
rz: int = 0
|
|
218
|
+
speed_class: int = _SPEED_V # 0=% (link) · 1=V mm/s · 2=VR °/s
|
|
219
|
+
speed: int = 0 # link 0.01% · V 0.1 mm/s · VR 0.1°/s
|
|
220
|
+
coord: int = 17 # 16=Base 17=Robot 18=User 19=Tool
|
|
221
|
+
robot_group: int = 1 # 1-8 (0 = none, use station instead)
|
|
222
|
+
station_group: int = 0 # 1-24 (0 = none)
|
|
223
|
+
type: int = 0 # pose configuration bitfield (like 0x75)
|
|
224
|
+
expanded_type: int = 0
|
|
225
|
+
tool_no: int = 0 # 0-63
|
|
226
|
+
user_coord_no: int = 0 # 1-63 (only when coord=18 User)
|
|
227
|
+
base_axes: list = field(default_factory=lambda: [0, 0, 0]) # µm, 3 axes
|
|
228
|
+
station_axes: list = field(default_factory=lambda: [0] * 6) # pulse, 6 axes
|
|
229
|
+
|
|
230
|
+
def to_bytes(self) -> bytes:
|
|
231
|
+
"""Encode to the 104-byte request payload in protocol field order."""
|
|
232
|
+
base = _pad(self.base_axes, 3, "base_axes")
|
|
233
|
+
station = _pad(self.station_axes, 6, "station_axes")
|
|
234
|
+
return struct.pack(
|
|
235
|
+
"<26i",
|
|
236
|
+
self.robot_group, self.station_group, self.speed_class, self.speed,
|
|
237
|
+
self.coord, self.x, self.y, self.z, self.rx, self.ry, self.rz,
|
|
238
|
+
0, 0, # reserve (offsets 44/48)
|
|
239
|
+
self.type, self.expanded_type, self.tool_no, self.user_coord_no,
|
|
240
|
+
*base, *station,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class PulseMove:
|
|
246
|
+
"""
|
|
247
|
+
Request record for a Pulse move (cmd 0x8B) — 88 bytes = 22 int32.
|
|
248
|
+
|
|
249
|
+
`robot_axes` are raw encoder pulse counts (up to 8). Like the Cartesian move
|
|
250
|
+
this targets EITHER the robot OR a station group (robot+station → 0xB008) and
|
|
251
|
+
requires Servo ON + Command Remote. This MOVES the robot.
|
|
252
|
+
"""
|
|
253
|
+
robot_axes: list = field(default_factory=lambda: [0] * 8) # pulse, 8 axes
|
|
254
|
+
speed_class: int = _SPEED_PERCENT # 0=% · 1=V · 2=VR
|
|
255
|
+
speed: int = 0
|
|
256
|
+
robot_group: int = 1 # 1-8 (0 = none)
|
|
257
|
+
station_group: int = 0 # 1-24 (0 = none)
|
|
258
|
+
tool_no: int = 0 # 0-63
|
|
259
|
+
base_axes: list = field(default_factory=lambda: [0, 0, 0]) # pulse, 3 axes
|
|
260
|
+
station_axes: list = field(default_factory=lambda: [0] * 6) # pulse, 6 axes
|
|
261
|
+
|
|
262
|
+
def to_bytes(self) -> bytes:
|
|
263
|
+
"""Encode to the 88-byte request payload in protocol field order."""
|
|
264
|
+
robot = _pad(self.robot_axes, 8, "robot_axes")
|
|
265
|
+
base = _pad(self.base_axes, 3, "base_axes")
|
|
266
|
+
station = _pad(self.station_axes, 6, "station_axes")
|
|
267
|
+
return struct.pack(
|
|
268
|
+
"<22i",
|
|
269
|
+
self.robot_group, self.station_group, self.speed_class, self.speed,
|
|
270
|
+
*robot, self.tool_no, *base, *station,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class HsesError(Exception):
|
|
275
|
+
"""Raised when the robot returns a non-zero status code."""
|
|
276
|
+
def __init__(self, cmd: int, status: int, added: int) -> None:
|
|
277
|
+
msg = (f"HSES error cmd=0x{cmd:04X} "
|
|
278
|
+
f"status=0x{status:02X} ({status_text(status)})")
|
|
279
|
+
if added:
|
|
280
|
+
msg += f" added=0x{added:04X} ({added_status_text(added)})"
|
|
281
|
+
super().__init__(msg)
|
|
282
|
+
self.cmd = cmd
|
|
283
|
+
self.status = status
|
|
284
|
+
self.added = added
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class _HsesSocket:
|
|
288
|
+
"""Shared UDP-transport lifecycle for the HSES telemetry and file clients:
|
|
289
|
+
lazy socket open (optionally bound to a local adapter), idempotent close,
|
|
290
|
+
context-manager support, and the 1..0xFF request-id rollover."""
|
|
291
|
+
|
|
292
|
+
def __init__(self, host: str, port: int, timeout: float,
|
|
293
|
+
bind_host: Optional[str] = None) -> None:
|
|
294
|
+
self._host = host
|
|
295
|
+
self._port = port
|
|
296
|
+
self._timeout = timeout
|
|
297
|
+
self._bind_host = bind_host # local adapter IP to send from (optional)
|
|
298
|
+
self._sock: Optional[socket.socket] = None
|
|
299
|
+
self._req_id = 1
|
|
300
|
+
|
|
301
|
+
def open(self) -> None:
|
|
302
|
+
if self._sock is not None:
|
|
303
|
+
return
|
|
304
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
305
|
+
if self._bind_host:
|
|
306
|
+
s.bind((self._bind_host, 0)) # pin egress to the chosen adapter
|
|
307
|
+
s.settimeout(self._timeout)
|
|
308
|
+
self._sock = s
|
|
309
|
+
|
|
310
|
+
def close(self) -> None:
|
|
311
|
+
if self._sock is not None:
|
|
312
|
+
try:
|
|
313
|
+
self._sock.close()
|
|
314
|
+
except OSError:
|
|
315
|
+
pass
|
|
316
|
+
self._sock = None
|
|
317
|
+
|
|
318
|
+
def __enter__(self) -> Self:
|
|
319
|
+
self.open()
|
|
320
|
+
return self
|
|
321
|
+
|
|
322
|
+
def __exit__(self, *_: object) -> None:
|
|
323
|
+
self.close()
|
|
324
|
+
|
|
325
|
+
def _bump_req_id(self) -> int:
|
|
326
|
+
rid = self._req_id
|
|
327
|
+
self._req_id = (self._req_id % 0xFF) + 1
|
|
328
|
+
return rid
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class HsesClient(_HsesSocket):
|
|
332
|
+
"""
|
|
333
|
+
UDP client for the Yaskawa YRC1000 High-Speed Ethernet Server.
|
|
334
|
+
|
|
335
|
+
Usage:
|
|
336
|
+
with HsesClient("192.0.2.10") as c:
|
|
337
|
+
print(c.read_status())
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def __init__(self, host: str, port: int = HSES_PORT, timeout: float = 2.0,
|
|
341
|
+
bind_host: Optional[str] = None, model: str = MODEL_YRC1000) -> None:
|
|
342
|
+
super().__init__(host, port, timeout, bind_host)
|
|
343
|
+
self._model = model
|
|
344
|
+
self._axis_names: Optional[tuple] = None # real 0x74 names, once loaded
|
|
345
|
+
self._apply_model() # derives S command/width from the model
|
|
346
|
+
|
|
347
|
+
# ── controller model ───────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
def _apply_model(self) -> None:
|
|
350
|
+
"""Derive the model-dependent S-variable command and field width. DX100
|
|
351
|
+
uses the 16-byte S variant (0x7E / 0x306); DX200 and YRC1000 the 32-byte
|
|
352
|
+
one (0x8C / 0x30C)."""
|
|
353
|
+
if self._model not in _VALID_MODELS:
|
|
354
|
+
raise ValueError(f"unknown controller model: {self._model!r} "
|
|
355
|
+
f"(expected one of {_VALID_MODELS})")
|
|
356
|
+
s16 = self._model == MODEL_DX100
|
|
357
|
+
self._s_cmd = _CMD_S16_VAR if s16 else _CMD_S_VAR
|
|
358
|
+
self._s_plural_cmd = _CMD_S16_PLURAL if s16 else _CMD_S_PLURAL
|
|
359
|
+
self._s_width = 16 if s16 else 32
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def model(self) -> str:
|
|
363
|
+
return self._model
|
|
364
|
+
|
|
365
|
+
@model.setter
|
|
366
|
+
def model(self, value: str) -> None:
|
|
367
|
+
self._model = value
|
|
368
|
+
self._apply_model()
|
|
369
|
+
|
|
370
|
+
def detect_model(self) -> str:
|
|
371
|
+
"""Detect the controller generation with a capability probe (no version
|
|
372
|
+
strings — those are not documented). Sends a 32-byte S read (0x8C): a
|
|
373
|
+
DX100 rejects the command with status 0x08 ("undefined"), while DX200 /
|
|
374
|
+
YRC1000 accept it. Sets self.model to the detected value and returns it.
|
|
375
|
+
|
|
376
|
+
Cannot distinguish DX200 from YRC1000 (identical S behaviour) — it reports
|
|
377
|
+
"yrc1000" for both; use an explicit model to get DX200's temperature-command
|
|
378
|
+
guard. The status-0x08 assumption must be confirmed live against a DX100
|
|
379
|
+
(docs/online_tests.md)."""
|
|
380
|
+
try:
|
|
381
|
+
self._tx(_CMD_S_VAR, instance=0, attribute=1, service=_SVC_GET_SINGLE)
|
|
382
|
+
detected = MODEL_YRC1000
|
|
383
|
+
except HsesError as e:
|
|
384
|
+
detected = MODEL_DX100 if e.status == _STATUS_CMD_UNDEFINED else MODEL_YRC1000
|
|
385
|
+
self.model = detected
|
|
386
|
+
log.info("detect_model: probed 0x8C -> %s", detected)
|
|
387
|
+
return detected
|
|
388
|
+
|
|
389
|
+
def _require_yrc1000(self, what: str) -> None:
|
|
390
|
+
"""Guard for the YRC1000-only telemetry commands (0x0411-0x0413), which
|
|
391
|
+
DX100/DX200 do not implement."""
|
|
392
|
+
if self._model != MODEL_YRC1000:
|
|
393
|
+
raise RuntimeError(
|
|
394
|
+
f"{what} is only available on YRC1000 "
|
|
395
|
+
f"(controller model is {self._model})")
|
|
396
|
+
|
|
397
|
+
# ── axis names ─────────────────────────────────────────────────────────────
|
|
398
|
+
@property
|
|
399
|
+
def axis_names(self) -> tuple:
|
|
400
|
+
"""Per-axis labels used by the pulse-position / following-error / torque /
|
|
401
|
+
encoder-temperature decoders. Returns the controller's real names once
|
|
402
|
+
`load_axis_names()` has fetched them (e.g. S,L,U,T on a 4-axis palletiser),
|
|
403
|
+
otherwise the S,L,U,R,B,T,E,8 default."""
|
|
404
|
+
return self._axis_names or _AXIS_NAMES
|
|
405
|
+
|
|
406
|
+
def load_axis_names(self, group: int = 1) -> tuple:
|
|
407
|
+
"""Fetch and cache the real axis names from Axis Configuration (0x74) so the
|
|
408
|
+
per-axis reads are labelled with the controller's actual axis names instead
|
|
409
|
+
of the S,L,U,R,B,T default (the 4th axis is R on a wrist robot but T on a
|
|
410
|
+
palletiser). Empty slots keep a positional `axisN` label. Best-effort: on any
|
|
411
|
+
error the cache is left unset and the default names apply. Call once after
|
|
412
|
+
the connection is verified."""
|
|
413
|
+
try:
|
|
414
|
+
cfg = self.read_axis_config(group)
|
|
415
|
+
names = tuple((cfg.get(f"axis_{i + 1}") or f"axis{i + 1}") for i in range(8))
|
|
416
|
+
self._axis_names = names
|
|
417
|
+
log.info("axis names from 0x74: %s", cfg.get("axes"))
|
|
418
|
+
except Exception as e: # noqa: BLE001 — best-effort, fall back to defaults
|
|
419
|
+
log.warning("load_axis_names failed, using default names: %s", e)
|
|
420
|
+
return self.axis_names
|
|
421
|
+
|
|
422
|
+
# ── packet building ────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _build(
|
|
426
|
+
cmd: int, instance: int, attribute: int, service: int,
|
|
427
|
+
req_id: int, payload: bytes = b"",
|
|
428
|
+
) -> bytes:
|
|
429
|
+
hdr = bytearray(_HDR_SIZE)
|
|
430
|
+
hdr[0:4] = _MAGIC
|
|
431
|
+
struct.pack_into("<H", hdr, 4, _HDR_SIZE) # header size (fixed 32)
|
|
432
|
+
struct.pack_into("<H", hdr, 6, len(payload)) # data size
|
|
433
|
+
hdr[8] = 0x03 # Reserve1 FIXED
|
|
434
|
+
hdr[9] = _PROC_ROBOT
|
|
435
|
+
hdr[10] = 0x00 # ACK = request
|
|
436
|
+
hdr[11] = req_id
|
|
437
|
+
struct.pack_into("<I", hdr, 12, 0) # Block No.
|
|
438
|
+
hdr[16:24] = b"99999999" # Reserve2 FIXED
|
|
439
|
+
struct.pack_into("<H", hdr, 24, cmd)
|
|
440
|
+
struct.pack_into("<H", hdr, 26, instance)
|
|
441
|
+
hdr[28] = attribute
|
|
442
|
+
hdr[29] = service
|
|
443
|
+
struct.pack_into("<H", hdr, 30, 0) # Padding
|
|
444
|
+
return bytes(hdr) + payload
|
|
445
|
+
|
|
446
|
+
# ── low-level exchange ────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
@staticmethod
|
|
449
|
+
def _parse_resp(raw: bytes) -> _RawResp:
|
|
450
|
+
"""Decode a validated response header (magic/length already checked) into
|
|
451
|
+
a _RawResp. In the answer header the Instance/Attribute/Padding fields are
|
|
452
|
+
repurposed: bytes 24-25 → echoed command, 26 → status, 28 → added-status
|
|
453
|
+
size, 30-31 → added-status word 1. Shared by _tx and _tx_multi."""
|
|
454
|
+
dat_size = struct.unpack_from("<H", raw, 6)[0]
|
|
455
|
+
resp_status = raw[26]
|
|
456
|
+
added_size = raw[28]
|
|
457
|
+
added = struct.unpack_from("<H", raw, 30)[0] if added_size >= 1 else 0
|
|
458
|
+
cmd_echo = struct.unpack_from("<H", raw, 24)[0]
|
|
459
|
+
data = raw[_HDR_SIZE: _HDR_SIZE + dat_size]
|
|
460
|
+
return _RawResp(cmd=cmd_echo, status=resp_status, added=added, data=data)
|
|
461
|
+
|
|
462
|
+
def _tx(
|
|
463
|
+
self,
|
|
464
|
+
cmd: int,
|
|
465
|
+
instance: int = 1,
|
|
466
|
+
attribute: int = 0,
|
|
467
|
+
service: int = _SVC_GET_ALL,
|
|
468
|
+
payload: bytes = b"",
|
|
469
|
+
) -> _RawResp:
|
|
470
|
+
if self._sock is None:
|
|
471
|
+
raise RuntimeError("Call open() first")
|
|
472
|
+
|
|
473
|
+
rid = self._bump_req_id()
|
|
474
|
+
pkt = self._build(cmd, instance, attribute, service, rid, payload)
|
|
475
|
+
log.debug("tx cmd=0x%04X inst=%d attr=%d svc=0x%02X id=%d payload=%dB",
|
|
476
|
+
cmd, instance, attribute, service, rid, len(payload))
|
|
477
|
+
self._sock.sendto(pkt, (self._host, self._port))
|
|
478
|
+
|
|
479
|
+
# Match the response by request ID (like _tx_multi). Without this
|
|
480
|
+
# check, the late response of a previously timed-out request is read
|
|
481
|
+
# as the response to this one — the offset then persists and silently
|
|
482
|
+
# yields foreign payload data (e.g. bytes of a job name as the value
|
|
483
|
+
# of a B variable).
|
|
484
|
+
deadline = time.monotonic() + self._timeout
|
|
485
|
+
try:
|
|
486
|
+
while True:
|
|
487
|
+
remaining = deadline - time.monotonic()
|
|
488
|
+
if remaining <= 0:
|
|
489
|
+
log.error("timeout cmd=0x%04X inst=%d id=%d after %.1fs",
|
|
490
|
+
cmd, instance, rid, self._timeout)
|
|
491
|
+
raise socket.timeout(
|
|
492
|
+
f"no response for cmd=0x{cmd:04X} id={rid}")
|
|
493
|
+
self._sock.settimeout(remaining)
|
|
494
|
+
try:
|
|
495
|
+
raw = self._sock.recv(4096)
|
|
496
|
+
except socket.timeout:
|
|
497
|
+
log.error("timeout cmd=0x%04X inst=%d id=%d after %.1fs",
|
|
498
|
+
cmd, instance, rid, self._timeout)
|
|
499
|
+
raise
|
|
500
|
+
if len(raw) < _HDR_SIZE:
|
|
501
|
+
log.error("malformed response cmd=0x%04X id=%d: too short (%d B)",
|
|
502
|
+
cmd, rid, len(raw))
|
|
503
|
+
raise IOError(f"Response too short: {len(raw)} B")
|
|
504
|
+
if raw[0:4] != _MAGIC:
|
|
505
|
+
log.error("malformed response cmd=0x%04X id=%d: bad magic %r",
|
|
506
|
+
cmd, rid, raw[0:4])
|
|
507
|
+
raise IOError(f"Bad magic: {raw[0:4]!r}")
|
|
508
|
+
if raw[11] != rid:
|
|
509
|
+
log.warning("verwerfe verspaetete Antwort id=%d (erwartet %d, "
|
|
510
|
+
"cmd=0x%04X)", raw[11], rid, cmd)
|
|
511
|
+
continue
|
|
512
|
+
break
|
|
513
|
+
finally:
|
|
514
|
+
self._sock.settimeout(self._timeout)
|
|
515
|
+
|
|
516
|
+
resp = self._parse_resp(raw)
|
|
517
|
+
|
|
518
|
+
if resp.status != 0x00:
|
|
519
|
+
log.error("rx cmd=0x%04X id=%d status=0x%02X (%s) added=0x%04X (%s)",
|
|
520
|
+
cmd, rid, resp.status, status_text(resp.status),
|
|
521
|
+
resp.added, added_status_text(resp.added) if resp.added else "-")
|
|
522
|
+
raise HsesError(cmd, resp.status, resp.added)
|
|
523
|
+
|
|
524
|
+
log.debug("rx cmd=0x%04X id=%d status=0 data=%dB", cmd, rid, len(resp.data))
|
|
525
|
+
return resp
|
|
526
|
+
|
|
527
|
+
# ── parallel burst ────────────────────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
def _tx_multi(
|
|
530
|
+
self,
|
|
531
|
+
reqs: list[tuple],
|
|
532
|
+
) -> "list[Optional[_RawResp]]":
|
|
533
|
+
"""
|
|
534
|
+
Send all requests simultaneously, collect responses matched by req_id.
|
|
535
|
+
reqs: list of (cmd, instance, attribute, service, payload)
|
|
536
|
+
Returns list of _RawResp|None in the same order as reqs.
|
|
537
|
+
"""
|
|
538
|
+
if self._sock is None:
|
|
539
|
+
raise RuntimeError("Call open() first")
|
|
540
|
+
|
|
541
|
+
log.debug("tx_multi %d requests", len(reqs))
|
|
542
|
+
req_ids: list[int] = []
|
|
543
|
+
for cmd, instance, attribute, service, payload in reqs:
|
|
544
|
+
rid = self._bump_req_id()
|
|
545
|
+
req_ids.append(rid)
|
|
546
|
+
pkt = self._build(cmd, instance, attribute, service, rid, payload)
|
|
547
|
+
self._sock.sendto(pkt, (self._host, self._port))
|
|
548
|
+
|
|
549
|
+
id_to_resp: dict[int, _RawResp] = {}
|
|
550
|
+
pending = set(req_ids)
|
|
551
|
+
deadline = time.monotonic() + self._timeout
|
|
552
|
+
|
|
553
|
+
while pending:
|
|
554
|
+
remaining = deadline - time.monotonic()
|
|
555
|
+
if remaining <= 0:
|
|
556
|
+
break
|
|
557
|
+
self._sock.settimeout(remaining)
|
|
558
|
+
try:
|
|
559
|
+
raw = self._sock.recv(4096)
|
|
560
|
+
except OSError:
|
|
561
|
+
break
|
|
562
|
+
if len(raw) < _HDR_SIZE or raw[0:4] != _MAGIC:
|
|
563
|
+
continue
|
|
564
|
+
rid = raw[11]
|
|
565
|
+
if rid not in pending:
|
|
566
|
+
continue
|
|
567
|
+
id_to_resp[rid] = self._parse_resp(raw)
|
|
568
|
+
pending.discard(rid)
|
|
569
|
+
|
|
570
|
+
self._sock.settimeout(self._timeout)
|
|
571
|
+
if pending:
|
|
572
|
+
log.warning("tx_multi: %d/%d responses missing (timeout)",
|
|
573
|
+
len(pending), len(req_ids))
|
|
574
|
+
return [id_to_resp.get(rid) for rid in req_ids]
|
|
575
|
+
|
|
576
|
+
def poll_all(
|
|
577
|
+
self,
|
|
578
|
+
*,
|
|
579
|
+
alarm_slots: int = 0,
|
|
580
|
+
include_job: bool = True,
|
|
581
|
+
position_group: int = 0,
|
|
582
|
+
b_vars: "list[dict]" = [],
|
|
583
|
+
i_vars: "list[dict]" = [],
|
|
584
|
+
io_signals: "list[dict]" = [],
|
|
585
|
+
read_cio: bool = False,
|
|
586
|
+
) -> dict:
|
|
587
|
+
"""
|
|
588
|
+
Fetch all configured data in one parallel UDP burst.
|
|
589
|
+
|
|
590
|
+
alarm_slots: number of alarm history slots (0=skip, max 4)
|
|
591
|
+
include_job: read executing job info
|
|
592
|
+
position_group: 0=skip, 1-8=pulse, 101-108=cartesian
|
|
593
|
+
b_vars/i_vars: list of {"label": str, "no": int}
|
|
594
|
+
io_signals: list of {"label": str, "logical": int}
|
|
595
|
+
|
|
596
|
+
Returns flat dict of all decoded fields.
|
|
597
|
+
Raises HsesError if the STATUS command returns an error.
|
|
598
|
+
"""
|
|
599
|
+
reqs: list[tuple] = []
|
|
600
|
+
tags: list[str] = []
|
|
601
|
+
|
|
602
|
+
reqs.append((_CMD_STATUS, 1, 0, _SVC_GET_ALL, b""))
|
|
603
|
+
tags.append("status")
|
|
604
|
+
|
|
605
|
+
for slot in range(1, alarm_slots + 1):
|
|
606
|
+
reqs.append((_CMD_ALARM, slot, 0, _SVC_GET_ALL, b""))
|
|
607
|
+
tags.append(f"alarm{slot}")
|
|
608
|
+
|
|
609
|
+
if include_job:
|
|
610
|
+
reqs.append((_CMD_JOB, 1, 0, _SVC_GET_ALL, b""))
|
|
611
|
+
tags.append("job")
|
|
612
|
+
|
|
613
|
+
if position_group:
|
|
614
|
+
reqs.append((_CMD_POSITION, position_group, 0, _SVC_GET_ALL, b""))
|
|
615
|
+
tags.append("position")
|
|
616
|
+
|
|
617
|
+
for v in b_vars:
|
|
618
|
+
reqs.append((_CMD_B_VAR, v["no"], 1, _SVC_GET_SINGLE, b""))
|
|
619
|
+
tags.append(f"b_var:{v['label']}")
|
|
620
|
+
|
|
621
|
+
for v in i_vars:
|
|
622
|
+
reqs.append((_CMD_I_VAR, v["no"], 1, _SVC_GET_SINGLE, b""))
|
|
623
|
+
tags.append(f"i_var:{v['label']}")
|
|
624
|
+
|
|
625
|
+
# CIO counter: low byte at logical 72000 (inst 7200), high byte at 72010 (inst 7201)
|
|
626
|
+
if read_cio:
|
|
627
|
+
reqs.append((_CMD_IO_PLURAL, 7200, 0, _SVC_READ_ALL, struct.pack("<I", 2)))
|
|
628
|
+
tags.append("cio")
|
|
629
|
+
|
|
630
|
+
# One plural read covers all io_signals regardless of count
|
|
631
|
+
io_min_inst = 0
|
|
632
|
+
if io_signals:
|
|
633
|
+
io_min_inst = min(sig["logical"] // 10 for sig in io_signals)
|
|
634
|
+
io_max_inst = max(sig["logical"] // 10 for sig in io_signals)
|
|
635
|
+
n_bytes = io_max_inst - io_min_inst + 1
|
|
636
|
+
if n_bytes % 2 != 0:
|
|
637
|
+
n_bytes += 1
|
|
638
|
+
reqs.append((_CMD_IO_PLURAL, io_min_inst, 0, _SVC_READ_ALL,
|
|
639
|
+
struct.pack("<I", n_bytes)))
|
|
640
|
+
tags.append("io_plural")
|
|
641
|
+
|
|
642
|
+
responses = self._tx_multi(reqs)
|
|
643
|
+
out: dict = {}
|
|
644
|
+
|
|
645
|
+
for tag, resp in zip(tags, responses):
|
|
646
|
+
if resp is None:
|
|
647
|
+
continue
|
|
648
|
+
if tag == "status":
|
|
649
|
+
if resp.status != 0:
|
|
650
|
+
raise HsesError(resp.cmd, resp.status, resp.added)
|
|
651
|
+
out.update(_decode_status(resp.data, self._model))
|
|
652
|
+
elif tag.startswith("alarm"):
|
|
653
|
+
if resp.status == 0:
|
|
654
|
+
slot = int(tag[5:])
|
|
655
|
+
for k, v in _decode_alarm(resp.data).items():
|
|
656
|
+
out[f"alarm{slot}_{k[len('alarm_'):]}"] = v
|
|
657
|
+
elif tag == "job":
|
|
658
|
+
if resp.status == 0:
|
|
659
|
+
out.update(_decode_job(resp.data))
|
|
660
|
+
elif tag == "position":
|
|
661
|
+
if resp.status == 0:
|
|
662
|
+
out.update(_decode_position(resp.data))
|
|
663
|
+
elif tag.startswith("b_var:"):
|
|
664
|
+
if resp.status == 0 and len(resp.data) >= 1:
|
|
665
|
+
out[f"var_{tag[6:]}"] = resp.data[0]
|
|
666
|
+
elif tag.startswith("i_var:"):
|
|
667
|
+
if resp.status == 0 and len(resp.data) >= 2:
|
|
668
|
+
out[f"var_{tag[6:]}"] = struct.unpack_from("<h", resp.data, 0)[0]
|
|
669
|
+
elif tag == "cio":
|
|
670
|
+
if resp.status == 0 and len(resp.data) >= 6:
|
|
671
|
+
# response: 4-byte count + 2 data bytes
|
|
672
|
+
lo = resp.data[4]
|
|
673
|
+
hi = resp.data[5]
|
|
674
|
+
out["cio_counter"] = (hi << 8) | lo
|
|
675
|
+
elif tag == "io_plural":
|
|
676
|
+
if resp.status == 0 and len(resp.data) >= 4:
|
|
677
|
+
n = struct.unpack_from("<I", resp.data, 0)[0]
|
|
678
|
+
io_bytes = resp.data[4: 4 + n]
|
|
679
|
+
for sig in io_signals:
|
|
680
|
+
offset = sig["logical"] // 10 - io_min_inst
|
|
681
|
+
if offset < len(io_bytes):
|
|
682
|
+
out[f"io_{sig['label']}"] = io_bytes[offset]
|
|
683
|
+
|
|
684
|
+
return out
|
|
685
|
+
|
|
686
|
+
# ── public read methods ────────────────────────────────────────────────────
|
|
687
|
+
|
|
688
|
+
def read_status(self) -> dict:
|
|
689
|
+
"""
|
|
690
|
+
Status Information Reading (cmd 0x72).
|
|
691
|
+
Returns dict with boolean flags:
|
|
692
|
+
step, one_cycle, auto, running, speed_limit, teach, play, cmd_remote,
|
|
693
|
+
hold_pendant, hold_external, hold_cmd, alarm, error, servo_on
|
|
694
|
+
On DX100 the bit4 flag is keyed `in_guard_safe` instead of `speed_limit`.
|
|
695
|
+
"""
|
|
696
|
+
r = self._tx(_CMD_STATUS, instance=1, attribute=0, service=_SVC_GET_ALL)
|
|
697
|
+
if len(r.data) < 8:
|
|
698
|
+
raise IOError(f"Status data too short: {len(r.data)} B")
|
|
699
|
+
return _decode_status(r.data, self._model)
|
|
700
|
+
|
|
701
|
+
def read_alarm(self, slot: int = 1) -> dict:
|
|
702
|
+
"""
|
|
703
|
+
Alarm Data Reading (cmd 0x70).
|
|
704
|
+
slot: 1=latest, 2=second from latest, 3=third, 4=fourth.
|
|
705
|
+
Returns dict: alarm_code, alarm_data, alarm_type, alarm_time, alarm_name.
|
|
706
|
+
alarm_type=0 means no alarm in that slot.
|
|
707
|
+
"""
|
|
708
|
+
r = self._tx(_CMD_ALARM, instance=slot, attribute=0, service=_SVC_GET_ALL)
|
|
709
|
+
return _decode_alarm(r.data)
|
|
710
|
+
|
|
711
|
+
def read_alarm_history(self, number: int = 1) -> dict:
|
|
712
|
+
"""
|
|
713
|
+
Alarm History Reading (cmd 0x71). Same 60-byte layout as read_alarm().
|
|
714
|
+
`number` is the history slot per class:
|
|
715
|
+
1-100 major alarms
|
|
716
|
+
1001-1100 minor alarms
|
|
717
|
+
2001-2100 user alarms (system)
|
|
718
|
+
3001-3100 user alarms (user)
|
|
719
|
+
4001-4100 off-line alarms
|
|
720
|
+
Returns dict: alarm_code, alarm_data, alarm_type, alarm_time, alarm_name.
|
|
721
|
+
alarm_type=0 (and code=0) means the slot is empty.
|
|
722
|
+
"""
|
|
723
|
+
r = self._tx(_CMD_ALARM_HIST, instance=number, attribute=0,
|
|
724
|
+
service=_SVC_GET_ALL)
|
|
725
|
+
return _decode_alarm(r.data)
|
|
726
|
+
|
|
727
|
+
def read_alarm_sub(self, slot: int = 1) -> dict:
|
|
728
|
+
"""
|
|
729
|
+
Alarm Data Reading with sub-code strings (cmd 0x30A). Extends
|
|
730
|
+
read_alarm() with the diagnostic strings that appear next to the
|
|
731
|
+
sub-code on the pendant.
|
|
732
|
+
slot: 1=latest, 2=second from latest, 3=third, 4=fourth.
|
|
733
|
+
Returns read_alarm()'s dict plus sub_code_info, sub_code_str and
|
|
734
|
+
reverse_display. alarm_type=0 means no alarm in that slot.
|
|
735
|
+
"""
|
|
736
|
+
r = self._tx(_CMD_ALARM_SUB, instance=slot, attribute=0,
|
|
737
|
+
service=_SVC_GET_ALL)
|
|
738
|
+
return _decode_alarm_sub(r.data)
|
|
739
|
+
|
|
740
|
+
def read_alarm_history_sub(self, number: int = 1) -> dict:
|
|
741
|
+
"""
|
|
742
|
+
Alarm History Reading with sub-code strings (cmd 0x30B). Same extended
|
|
743
|
+
268-byte layout as read_alarm_sub(); `number` is the history slot per
|
|
744
|
+
class exactly as in read_alarm_history() (1-100 major, 1001+ minor,
|
|
745
|
+
2001+/3001+ user, 4001+ off-line). Read-only.
|
|
746
|
+
Returns the same dict as read_alarm_sub(); an empty slot has
|
|
747
|
+
alarm_type=0 and alarm_code=0.
|
|
748
|
+
"""
|
|
749
|
+
r = self._tx(_CMD_ALARM_HIST_SUB, instance=number, attribute=0,
|
|
750
|
+
service=_SVC_GET_ALL)
|
|
751
|
+
return _decode_alarm_sub(r.data)
|
|
752
|
+
|
|
753
|
+
def read_job(self, task: int = 1) -> dict:
|
|
754
|
+
"""
|
|
755
|
+
Executing Job Information Reading (cmd 0x73).
|
|
756
|
+
task: 1=master task, 2-16=sub tasks.
|
|
757
|
+
Returns dict: job_name, job_line, job_step, job_speed.
|
|
758
|
+
"""
|
|
759
|
+
r = self._tx(_CMD_JOB, instance=task, attribute=0, service=_SVC_GET_ALL)
|
|
760
|
+
return _decode_job(r.data)
|
|
761
|
+
|
|
762
|
+
def read_axis_config(self, group: int = 1) -> dict:
|
|
763
|
+
"""
|
|
764
|
+
Axis Configuration Information Reading (cmd 0x74).
|
|
765
|
+
group 1-8 = R1-R8 (pulse), 101-108 = R1-R8 (cartesian).
|
|
766
|
+
Defines the axis order used by 0x75/0x76/0x77 (non-present axes are
|
|
767
|
+
skipped, no gaps). Returns axis_1..axis_8 (name string per axis, empty
|
|
768
|
+
if absent) plus 'axes' = list of present names. This robot: S,L,U,R.
|
|
769
|
+
"""
|
|
770
|
+
r = self._tx(_CMD_AXIS_CFG, instance=group, attribute=0, service=_SVC_GET_ALL)
|
|
771
|
+
return _decode_axis_config(r.data)
|
|
772
|
+
|
|
773
|
+
def read_position(self, group: int = 1) -> dict:
|
|
774
|
+
"""
|
|
775
|
+
Robot Position Data Reading (cmd 0x75).
|
|
776
|
+
group 1-8 = R1-R8 pulse (joint angles in pulse units).
|
|
777
|
+
group 101-108 = R1-R8 cartesian (X/Y/Z in mm, Rx/Ry/Rz in degrees).
|
|
778
|
+
"""
|
|
779
|
+
r = self._tx(_CMD_POSITION, instance=group, attribute=0, service=_SVC_GET_ALL)
|
|
780
|
+
return _decode_position(r.data, self.axis_names)
|
|
781
|
+
|
|
782
|
+
def read_position_error(self, group: int = 1) -> dict:
|
|
783
|
+
"""
|
|
784
|
+
Position Error (following error) Reading (cmd 0x76).
|
|
785
|
+
group 1-8 = R1-R8. Returns the following error per axis in pulse units,
|
|
786
|
+
keyed pe_S/pe_L/pe_U/pe_R/... in the axis order defined by 0x74.
|
|
787
|
+
This 4-axis robot (S,L,U,R) leaves the remaining words at 0.
|
|
788
|
+
"""
|
|
789
|
+
r = self._tx(_CMD_POSERR, instance=group, attribute=0, service=_SVC_GET_ALL)
|
|
790
|
+
return _decode_axis_int32(r.data, "pe", self.axis_names)
|
|
791
|
+
|
|
792
|
+
def read_torque(self, group: int = 1) -> dict:
|
|
793
|
+
"""
|
|
794
|
+
Torque Data Reading (cmd 0x77).
|
|
795
|
+
group 1-8 = R1-R8. Returns torque per axis (typ. % of rated torque,
|
|
796
|
+
sign = direction), keyed tq_S/tq_L/tq_U/tq_R/... in the axis order
|
|
797
|
+
defined by 0x74. This 4-axis robot leaves the remaining words at 0.
|
|
798
|
+
"""
|
|
799
|
+
r = self._tx(_CMD_TORQUE, instance=group, attribute=0, service=_SVC_GET_ALL)
|
|
800
|
+
return _decode_axis_int32(r.data, "tq", self.axis_names)
|
|
801
|
+
|
|
802
|
+
def read_io(self, logical: int) -> int:
|
|
803
|
+
"""
|
|
804
|
+
I/O Data Reading (cmd 0x78).
|
|
805
|
+
logical: signal logical number as shown on the PP (e.g. 10010 = first output).
|
|
806
|
+
instance = logical // 10. Returns raw DWORD (0=OFF, non-zero=ON).
|
|
807
|
+
"""
|
|
808
|
+
r = self._tx(_CMD_IO, instance=logical // 10, attribute=1, service=_SVC_GET_SINGLE)
|
|
809
|
+
if not r.data:
|
|
810
|
+
return 0
|
|
811
|
+
return r.data[0]
|
|
812
|
+
|
|
813
|
+
def read_io_plural(self, start_logical: int, count: int) -> bytes:
|
|
814
|
+
"""
|
|
815
|
+
Plural I/O Data Reading (cmd 0x300, service 0x33).
|
|
816
|
+
Reads `count` consecutive I/O bytes starting at start_logical in one request.
|
|
817
|
+
count must be even, max 474.
|
|
818
|
+
Returns raw bytes of length count.
|
|
819
|
+
instance = start_logical // 10
|
|
820
|
+
"""
|
|
821
|
+
if count % 2 != 0:
|
|
822
|
+
count += 1 # must be even per spec
|
|
823
|
+
payload = struct.pack("<I", count)
|
|
824
|
+
r = self._tx(_CMD_IO_PLURAL, instance=start_logical // 10,
|
|
825
|
+
attribute=0, service=_SVC_READ_ALL, payload=payload)
|
|
826
|
+
# Response data: DWORD 1 = count returned, then count bytes of I/O data
|
|
827
|
+
if len(r.data) < 4:
|
|
828
|
+
return b""
|
|
829
|
+
n_returned = struct.unpack_from("<I", r.data, 0)[0]
|
|
830
|
+
return r.data[4: 4 + n_returned]
|
|
831
|
+
|
|
832
|
+
def write_io_plural(self, start_logical: int, data: bytes) -> None:
|
|
833
|
+
"""
|
|
834
|
+
Plural I/O Data Writing (cmd 0x300, service 0x34).
|
|
835
|
+
Writes len(data) consecutive I/O bytes starting at start_logical.
|
|
836
|
+
len(data) must be even, max 474.
|
|
837
|
+
instance = start_logical // 10
|
|
838
|
+
Writable areas: network inputs (27010–29560), pseudo inputs (87010–87200).
|
|
839
|
+
"""
|
|
840
|
+
count = len(data)
|
|
841
|
+
if count % 2 != 0:
|
|
842
|
+
data = data + b"\x00"
|
|
843
|
+
count += 1
|
|
844
|
+
payload = struct.pack("<I", count) + data
|
|
845
|
+
self._tx(_CMD_IO_PLURAL, instance=start_logical // 10,
|
|
846
|
+
attribute=0, service=_SVC_WRITE_ALL, payload=payload)
|
|
847
|
+
|
|
848
|
+
def write_io(self, logical: int, value: int) -> None:
|
|
849
|
+
"""
|
|
850
|
+
Write a single I/O byte by logical address.
|
|
851
|
+
value: 0–255 (one byte, 8 IO bits packed).
|
|
852
|
+
Uses write_io_plural with 2 bytes (minimum even payload).
|
|
853
|
+
"""
|
|
854
|
+
byte_val = value & 0xFF
|
|
855
|
+
self.write_io_plural(logical, bytes([byte_val, 0x00]))
|
|
856
|
+
|
|
857
|
+
# ── control commands ────────────────────────────────────────────────────────
|
|
858
|
+
#
|
|
859
|
+
# All of these need Command Remote enabled (RS 005 = 1) and, depending on the
|
|
860
|
+
# action, the right mode / servo state. _tx() raises HsesError on a non-zero
|
|
861
|
+
# status, so a failed command (e.g. 0x2080 incorrect mode, 0x2100 command
|
|
862
|
+
# remote not set) surfaces as an exception carrying the added-status code.
|
|
863
|
+
|
|
864
|
+
def _control(self, cmd: int, instance: int, value: int) -> None:
|
|
865
|
+
"""Single-element control write (service 0x10) with a 4-byte int payload."""
|
|
866
|
+
self._tx(cmd, instance=instance, attribute=1, service=_SVC_SET_SINGLE,
|
|
867
|
+
payload=struct.pack("<I", value))
|
|
868
|
+
|
|
869
|
+
def reset_alarm(self) -> None:
|
|
870
|
+
"""Alarm Reset (cmd 0x82, instance 1). Clears the active alarm."""
|
|
871
|
+
log.info("control: reset_alarm")
|
|
872
|
+
self._control(_CMD_RESET, 1, 1)
|
|
873
|
+
|
|
874
|
+
def cancel_error(self) -> None:
|
|
875
|
+
"""Error Cancel (cmd 0x82, instance 2). Cancels the active error."""
|
|
876
|
+
log.info("control: cancel_error")
|
|
877
|
+
self._control(_CMD_RESET, 2, 1)
|
|
878
|
+
|
|
879
|
+
def hold(self, on: bool = True) -> None:
|
|
880
|
+
"""HOLD (cmd 0x83, instance 1). on=True → hold, on=False → release."""
|
|
881
|
+
log.info("control: hold %s", "on" if on else "off")
|
|
882
|
+
self._control(_CMD_HOLD_SERVO, _HS_HOLD, _ON if on else _OFF)
|
|
883
|
+
|
|
884
|
+
def servo(self, on: bool = True) -> None:
|
|
885
|
+
"""Servo power (cmd 0x83, instance 2). on=True → ON, on=False → OFF."""
|
|
886
|
+
log.info("control: servo %s", "on" if on else "off")
|
|
887
|
+
self._control(_CMD_HOLD_SERVO, _HS_SERVO, _ON if on else _OFF)
|
|
888
|
+
|
|
889
|
+
def hlock(self, on: bool = True) -> None:
|
|
890
|
+
"""
|
|
891
|
+
HLOCK (cmd 0x83, instance 3). Interlocks the programming pendant and I/O
|
|
892
|
+
operation-system signals. on=True → lock, on=False → unlock.
|
|
893
|
+
Invalid while the pendant is in edit mode or a file is being accessed.
|
|
894
|
+
"""
|
|
895
|
+
log.info("control: hlock %s", "on" if on else "off")
|
|
896
|
+
self._control(_CMD_HOLD_SERVO, _HS_HLOCK, _ON if on else _OFF)
|
|
897
|
+
|
|
898
|
+
def set_cycle(self, mode: int) -> None:
|
|
899
|
+
"""
|
|
900
|
+
Step / Cycle / Auto switching (cmd 0x84, instance 2).
|
|
901
|
+
mode: 1=STEP, 2=1 CYCLE, 3=AUTO (continuous).
|
|
902
|
+
"""
|
|
903
|
+
if mode not in (1, 2, 3):
|
|
904
|
+
raise ValueError("cycle mode must be 1 (STEP), 2 (1 CYCLE) or 3 (AUTO)")
|
|
905
|
+
log.info("control: set_cycle mode=%d", mode)
|
|
906
|
+
self._control(_CMD_CYCLE, 2, mode)
|
|
907
|
+
|
|
908
|
+
def display_message(self, text: str) -> None:
|
|
909
|
+
"""
|
|
910
|
+
Character string display to the programming pendant (cmd 0x85).
|
|
911
|
+
Up to 30 half-width characters (32-byte field, encoded in the pendant's
|
|
912
|
+
language code — use latin-1 to match the YRC1000).
|
|
913
|
+
"""
|
|
914
|
+
log.info("control: display_message %r", text)
|
|
915
|
+
raw = text.encode("latin-1", errors="replace")[:32]
|
|
916
|
+
payload = raw.ljust(32, b"\x00")
|
|
917
|
+
self._tx(_CMD_MSG, instance=1, attribute=1, service=_SVC_SET_SINGLE,
|
|
918
|
+
payload=payload)
|
|
919
|
+
|
|
920
|
+
def start_job(self) -> None:
|
|
921
|
+
"""
|
|
922
|
+
Start-up / Job START (cmd 0x86, instance 1). Plays back the currently
|
|
923
|
+
selected job. Requires servo ON and play mode.
|
|
924
|
+
"""
|
|
925
|
+
log.info("control: start_job")
|
|
926
|
+
self._control(_CMD_JOB_START, 1, 1)
|
|
927
|
+
|
|
928
|
+
def select_job(self, name: str, line: int = 0, *,
|
|
929
|
+
master_task: Optional[int] = None) -> None:
|
|
930
|
+
"""
|
|
931
|
+
Job Select (cmd 0x87, service 0x02 Set_All).
|
|
932
|
+
name: job name WITHOUT the .JBI extension (max 32 half-width chars).
|
|
933
|
+
line: start line number, 0-9999.
|
|
934
|
+
master_task: None → set the executing job (instance 1).
|
|
935
|
+
0-15 → set the master job for that task (instance 10-25).
|
|
936
|
+
|
|
937
|
+
Attribute is 0 (Set_All targets element 0 = the whole record). The robot
|
|
938
|
+
rejects a ".JBI" suffix and a non-zero attribute with status 0x01
|
|
939
|
+
(verified live 2026-07-04).
|
|
940
|
+
"""
|
|
941
|
+
if master_task is None:
|
|
942
|
+
instance = 1
|
|
943
|
+
elif 0 <= master_task <= 15:
|
|
944
|
+
instance = 10 + master_task
|
|
945
|
+
else:
|
|
946
|
+
raise ValueError("master_task must be 0-15 or None")
|
|
947
|
+
if not 0 <= line <= 9999:
|
|
948
|
+
raise ValueError("line must be 0-9999")
|
|
949
|
+
log.info("control: select_job name=%r line=%d task=%s",
|
|
950
|
+
name, line, master_task)
|
|
951
|
+
raw = name.encode("latin-1", errors="replace")[:32].ljust(32, b"\x00")
|
|
952
|
+
payload = raw + struct.pack("<I", line)
|
|
953
|
+
self._tx(_CMD_JOB_SELECT, instance=instance, attribute=0,
|
|
954
|
+
service=_SVC_SET_ALL, payload=payload)
|
|
955
|
+
|
|
956
|
+
# ── motion commands (⚠️ move the robot) ──────────────────────────────────────
|
|
957
|
+
#
|
|
958
|
+
# These order a real motion: Servo ON, Command Remote (RS 005 = 1) and PLAY
|
|
959
|
+
# mode are required, and the controller executes the move immediately. Only
|
|
960
|
+
# use them with a clear work envelope and the emergency stop within reach.
|
|
961
|
+
|
|
962
|
+
def move_cartesian(self, move: CartesianMove,
|
|
963
|
+
mode: int = _MOVE_STRAIGHT_ABS) -> None:
|
|
964
|
+
"""
|
|
965
|
+
Cartesian move (cmd 0x8A). ⚠️ MOVES the robot to the pose in `move`.
|
|
966
|
+
|
|
967
|
+
mode: 1=link-absolute (joint interp.), 2=straight-absolute (linear),
|
|
968
|
+
3=straight-increment (linear, relative to the current pose).
|
|
969
|
+
Attribute is fixed 1, service is Set_All (0x02, execute).
|
|
970
|
+
"""
|
|
971
|
+
if mode not in (_MOVE_LINK_ABS, _MOVE_STRAIGHT_ABS, _MOVE_STRAIGHT_INC):
|
|
972
|
+
raise ValueError("mode must be 1 (link), 2 (straight) or 3 (increment)")
|
|
973
|
+
log.info("control: move_cartesian mode=%d coord=%d group=r%d/s%d",
|
|
974
|
+
mode, move.coord, move.robot_group, move.station_group)
|
|
975
|
+
self._tx(_CMD_MOVE_CART, instance=mode, attribute=1,
|
|
976
|
+
service=_SVC_SET_ALL, payload=move.to_bytes())
|
|
977
|
+
|
|
978
|
+
def move_pulse(self, move: PulseMove,
|
|
979
|
+
mode: int = _MOVE_LINK_ABS) -> None:
|
|
980
|
+
"""
|
|
981
|
+
Pulse move (cmd 0x8B). ⚠️ MOVES the robot to the joint pulses in `move`.
|
|
982
|
+
|
|
983
|
+
mode: 1=link-absolute (joint interp.), 2=straight-absolute (linear).
|
|
984
|
+
Attribute is fixed 1, service is Set_All (0x02, execute).
|
|
985
|
+
"""
|
|
986
|
+
if mode not in (_MOVE_LINK_ABS, _MOVE_STRAIGHT_ABS):
|
|
987
|
+
raise ValueError("mode must be 1 (link) or 2 (straight)")
|
|
988
|
+
log.info("control: move_pulse mode=%d group=r%d/s%d",
|
|
989
|
+
mode, move.robot_group, move.station_group)
|
|
990
|
+
self._tx(_CMD_MOVE_PULSE, instance=mode, attribute=1,
|
|
991
|
+
service=_SVC_SET_ALL, payload=move.to_bytes())
|
|
992
|
+
|
|
993
|
+
# ── variable reads ──────────────────────────────────────────────────────────
|
|
994
|
+
|
|
995
|
+
def read_register(self, number: int) -> int:
|
|
996
|
+
"""
|
|
997
|
+
Register M[number] (cmd 0x79). Registers are 16-bit values carried in a
|
|
998
|
+
4-byte field; returns 0-65535. Instance is the register number 0-999.
|
|
999
|
+
"""
|
|
1000
|
+
r = self._tx(_CMD_REGISTER, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1001
|
+
if len(r.data) < 2:
|
|
1002
|
+
return 0
|
|
1003
|
+
return struct.unpack_from("<H", r.data, 0)[0]
|
|
1004
|
+
|
|
1005
|
+
def read_b_var(self, number: int) -> int:
|
|
1006
|
+
"""Byte Variable B[number] (cmd 0x7A). Returns 0-255."""
|
|
1007
|
+
r = self._tx(_CMD_B_VAR, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1008
|
+
if len(r.data) < 1:
|
|
1009
|
+
return 0
|
|
1010
|
+
return r.data[0]
|
|
1011
|
+
|
|
1012
|
+
def read_i_var(self, number: int) -> int:
|
|
1013
|
+
"""Integer Variable I[number] (cmd 0x7B). Returns the signed 16-bit value
|
|
1014
|
+
carried in the low word of the 4-byte slot (-32768..32767); use D (0x7C)
|
|
1015
|
+
for 32-bit. Matches write_i_var / the plural 0x303 path."""
|
|
1016
|
+
r = self._tx(_CMD_I_VAR, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1017
|
+
if len(r.data) < 2:
|
|
1018
|
+
return 0
|
|
1019
|
+
return struct.unpack_from("<h", r.data, 0)[0]
|
|
1020
|
+
|
|
1021
|
+
def read_d_var(self, number: int) -> int:
|
|
1022
|
+
"""Double Variable D[number] (cmd 0x7C). Returns signed 32-bit int."""
|
|
1023
|
+
r = self._tx(_CMD_D_VAR, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1024
|
+
if len(r.data) < 4:
|
|
1025
|
+
return 0
|
|
1026
|
+
return struct.unpack_from("<i", r.data, 0)[0]
|
|
1027
|
+
|
|
1028
|
+
def read_r_var(self, number: int) -> float:
|
|
1029
|
+
"""Real Variable R[number] (cmd 0x7D). Returns IEEE-754 float32."""
|
|
1030
|
+
r = self._tx(_CMD_R_VAR, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1031
|
+
if len(r.data) < 4:
|
|
1032
|
+
return 0.0
|
|
1033
|
+
return struct.unpack_from("<f", r.data, 0)[0]
|
|
1034
|
+
|
|
1035
|
+
def read_s_var(self, number: int) -> str:
|
|
1036
|
+
"""
|
|
1037
|
+
Character Variable S[number]. 32-byte cmd 0x8C on DX200/YRC1000, 16-byte
|
|
1038
|
+
cmd 0x7E on DX100 (selected by the client model). Returns the string with
|
|
1039
|
+
trailing NULs stripped, decoded as latin-1 to match the pendant's language.
|
|
1040
|
+
"""
|
|
1041
|
+
r = self._tx(self._s_cmd, instance=number, attribute=1, service=_SVC_GET_SINGLE)
|
|
1042
|
+
return r.data[:self._s_width].split(b"\x00", 1)[0].decode("latin-1")
|
|
1043
|
+
|
|
1044
|
+
# ── variable writes ─────────────────────────────────────────────────────────
|
|
1045
|
+
|
|
1046
|
+
def write_register(self, number: int, value: int) -> None:
|
|
1047
|
+
"""
|
|
1048
|
+
Write Register M[number] (cmd 0x79, service 0x10 Set_Single).
|
|
1049
|
+
value: 0-65535, carried in the low word of a 4-byte slot. Only registers
|
|
1050
|
+
below the writable limit (0-559, controller-dependent) accept a write;
|
|
1051
|
+
higher numbers are read-only and reject with an HsesError.
|
|
1052
|
+
"""
|
|
1053
|
+
if not 0 <= value <= 65535:
|
|
1054
|
+
raise ValueError("register value must be 0-65535")
|
|
1055
|
+
self._tx(_CMD_REGISTER, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1056
|
+
payload=struct.pack("<I", value))
|
|
1057
|
+
|
|
1058
|
+
def write_b_var(self, number: int, value: int) -> None:
|
|
1059
|
+
"""
|
|
1060
|
+
Write Byte Variable B[number] (cmd 0x7A, service 0x10 Set_Single).
|
|
1061
|
+
value: 0-255. Payload is a 4-byte slot with the value in the low byte.
|
|
1062
|
+
"""
|
|
1063
|
+
if not 0 <= value <= 255:
|
|
1064
|
+
raise ValueError("B variable value must be 0-255")
|
|
1065
|
+
self._tx(_CMD_B_VAR, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1066
|
+
payload=struct.pack("<I", value))
|
|
1067
|
+
|
|
1068
|
+
def write_i_var(self, number: int, value: int) -> None:
|
|
1069
|
+
"""
|
|
1070
|
+
Write Integer Variable I[number] (cmd 0x7B, service 0x10 Set_Single).
|
|
1071
|
+
value: signed 16-bit (-32768..32767), carried in the low word of a
|
|
1072
|
+
4-byte slot.
|
|
1073
|
+
"""
|
|
1074
|
+
if not -32768 <= value <= 32767:
|
|
1075
|
+
raise ValueError("I variable value must be a signed 16-bit int")
|
|
1076
|
+
self._tx(_CMD_I_VAR, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1077
|
+
payload=struct.pack("<i", value))
|
|
1078
|
+
|
|
1079
|
+
def write_d_var(self, number: int, value: int) -> None:
|
|
1080
|
+
"""
|
|
1081
|
+
Write Double Variable D[number] (cmd 0x7C, service 0x10 Set_Single).
|
|
1082
|
+
value: signed 32-bit int (-2147483648..2147483647).
|
|
1083
|
+
"""
|
|
1084
|
+
if not -2_147_483_648 <= value <= 2_147_483_647:
|
|
1085
|
+
raise ValueError("D variable value must be a signed 32-bit int")
|
|
1086
|
+
self._tx(_CMD_D_VAR, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1087
|
+
payload=struct.pack("<i", value))
|
|
1088
|
+
|
|
1089
|
+
def write_r_var(self, number: int, value: float) -> None:
|
|
1090
|
+
"""
|
|
1091
|
+
Write Real Variable R[number] (cmd 0x7D, service 0x10 Set_Single).
|
|
1092
|
+
value: IEEE-754 float32.
|
|
1093
|
+
"""
|
|
1094
|
+
self._tx(_CMD_R_VAR, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1095
|
+
payload=struct.pack("<f", value))
|
|
1096
|
+
|
|
1097
|
+
def write_s_var(self, number: int, value: str) -> None:
|
|
1098
|
+
"""
|
|
1099
|
+
Write Character Variable S[number] (service 0x10). 32-byte cmd 0x8C on
|
|
1100
|
+
DX200/YRC1000, 16-byte cmd 0x7E on DX100. value is encoded as latin-1
|
|
1101
|
+
(pendant language code), truncated and NUL-padded to the field width.
|
|
1102
|
+
"""
|
|
1103
|
+
raw = value.encode("latin-1", errors="replace")[:self._s_width]
|
|
1104
|
+
self._tx(self._s_cmd, instance=number, attribute=1, service=_SVC_SET_SINGLE,
|
|
1105
|
+
payload=raw.ljust(self._s_width, b"\x00"))
|
|
1106
|
+
|
|
1107
|
+
def read_position_var(self, number: int) -> PositionVar:
|
|
1108
|
+
"""
|
|
1109
|
+
Robot Position Variable P[number] (cmd 0x7F, service 0x01 Get_All).
|
|
1110
|
+
Instance is the variable number 0-127. P only supports "All" access, so
|
|
1111
|
+
the whole 52-byte record is returned as a PositionVar.
|
|
1112
|
+
"""
|
|
1113
|
+
r = self._tx(_CMD_P_VAR, instance=number, attribute=0, service=_SVC_GET_ALL)
|
|
1114
|
+
return PositionVar.from_bytes(r.data)
|
|
1115
|
+
|
|
1116
|
+
def write_position_var(self, number: int, pos: PositionVar) -> None:
|
|
1117
|
+
"""
|
|
1118
|
+
Write Robot Position Variable P[number] (cmd 0x7F, service 0x02 Set_All).
|
|
1119
|
+
P only supports "All" access, so the whole 52-byte record is written at
|
|
1120
|
+
once from the given PositionVar.
|
|
1121
|
+
"""
|
|
1122
|
+
self._tx(_CMD_P_VAR, instance=number, attribute=0, service=_SVC_SET_ALL,
|
|
1123
|
+
payload=pos.to_bytes())
|
|
1124
|
+
|
|
1125
|
+
def read_bp_var(self, number: int) -> AxisPositionVar:
|
|
1126
|
+
"""
|
|
1127
|
+
Base Position Variable BP[number] (cmd 0x80, service 0x01 Get_All).
|
|
1128
|
+
Instance is the variable number 0-127. Returns the whole 36-byte record
|
|
1129
|
+
as an AxisPositionVar (data_type 0=Pulse or 16=Base cartesian).
|
|
1130
|
+
"""
|
|
1131
|
+
r = self._tx(_CMD_BP_VAR, instance=number, attribute=0, service=_SVC_GET_ALL)
|
|
1132
|
+
return AxisPositionVar.from_bytes(r.data)
|
|
1133
|
+
|
|
1134
|
+
def write_bp_var(self, number: int, pos: AxisPositionVar) -> None:
|
|
1135
|
+
"""
|
|
1136
|
+
Write Base Position Variable BP[number] (cmd 0x80, service 0x02 Set_All).
|
|
1137
|
+
Only data_type 0 (Pulse) and 16 (Base cartesian) are valid.
|
|
1138
|
+
"""
|
|
1139
|
+
self._tx(_CMD_BP_VAR, instance=number, attribute=0, service=_SVC_SET_ALL,
|
|
1140
|
+
payload=pos.to_bytes())
|
|
1141
|
+
|
|
1142
|
+
def read_ex_var(self, number: int) -> AxisPositionVar:
|
|
1143
|
+
"""
|
|
1144
|
+
Station/External Axis Variable EX[number] (cmd 0x81, service 0x01 Get_All).
|
|
1145
|
+
Instance is the variable number 0-127. EX only carries pulse counts
|
|
1146
|
+
(data_type 0); returns the 36-byte record as an AxisPositionVar.
|
|
1147
|
+
"""
|
|
1148
|
+
r = self._tx(_CMD_EX_VAR, instance=number, attribute=0, service=_SVC_GET_ALL)
|
|
1149
|
+
return AxisPositionVar.from_bytes(r.data)
|
|
1150
|
+
|
|
1151
|
+
def write_ex_var(self, number: int, pos: AxisPositionVar) -> None:
|
|
1152
|
+
"""
|
|
1153
|
+
Write Station/External Axis Variable EX[number] (cmd 0x81, service 0x02
|
|
1154
|
+
Set_All). EX is pulse-only (data_type 0).
|
|
1155
|
+
"""
|
|
1156
|
+
self._tx(_CMD_EX_VAR, instance=number, attribute=0, service=_SVC_SET_ALL,
|
|
1157
|
+
payload=pos.to_bytes())
|
|
1158
|
+
|
|
1159
|
+
# ── plural variable reads / writes ────────────────────────────────────────────
|
|
1160
|
+
#
|
|
1161
|
+
# Plural commands (0x301–0x309, 0x30C) read/write a run of consecutive
|
|
1162
|
+
# variables in a single request. Attribute is fixed 0, service 0x33 reads /
|
|
1163
|
+
# 0x34 writes. The request carries a uint32 element count; a read response
|
|
1164
|
+
# echoes the count returned, followed by the packed elements. Instance is the
|
|
1165
|
+
# start variable number. Per-command element sizes and max counts are in
|
|
1166
|
+
# HW1483358 §3.3.3.28–36/42. The 16-byte S variant (0x306) is
|
|
1167
|
+
# skipped in favour of the 32-byte 0x30C, mirroring the scalar 0x8C choice.
|
|
1168
|
+
|
|
1169
|
+
def _read_plural(self, cmd: int, start: int, count: int) -> bytes:
|
|
1170
|
+
"""Send a plural read (service 0x33) and return the raw element bytes,
|
|
1171
|
+
trimmed to the element count the controller reports it returned."""
|
|
1172
|
+
r = self._tx(cmd, instance=start, attribute=0, service=_SVC_READ_ALL,
|
|
1173
|
+
payload=struct.pack("<I", count))
|
|
1174
|
+
if len(r.data) < 4:
|
|
1175
|
+
return b""
|
|
1176
|
+
return r.data[4:]
|
|
1177
|
+
|
|
1178
|
+
def _write_plural(self, cmd: int, start: int, count: int, data: bytes) -> None:
|
|
1179
|
+
"""Send a plural write (service 0x34): a uint32 element count then the
|
|
1180
|
+
packed element bytes, starting at variable number `start`."""
|
|
1181
|
+
self._tx(cmd, instance=start, attribute=0, service=_SVC_WRITE_ALL,
|
|
1182
|
+
payload=struct.pack("<I", count) + data)
|
|
1183
|
+
|
|
1184
|
+
def _read_plural_scalar(self, cmd: int, start: int, count: int,
|
|
1185
|
+
fmt: str, size: int) -> list:
|
|
1186
|
+
"""Plural read of fixed-width scalar elements (`fmt`/`size` per struct),
|
|
1187
|
+
empty-safe. Shared by the register/I/D/R variable reads."""
|
|
1188
|
+
buf = self._read_plural(cmd, start, count)
|
|
1189
|
+
n = len(buf) // size
|
|
1190
|
+
return list(struct.unpack_from(f"<{n}{fmt}", buf)) if n else []
|
|
1191
|
+
|
|
1192
|
+
def read_registers(self, start: int, count: int) -> "list[int]":
|
|
1193
|
+
"""
|
|
1194
|
+
Plural Register read (cmd 0x301). Reads `count` consecutive registers
|
|
1195
|
+
starting at register number `start`. Each register is an unsigned 16-bit
|
|
1196
|
+
value (2-byte element); returns a list of 0-65535 ints (max 237).
|
|
1197
|
+
"""
|
|
1198
|
+
return self._read_plural_scalar(_CMD_REG_PLURAL, start, count, "H", 2)
|
|
1199
|
+
|
|
1200
|
+
def write_registers(self, start: int, values: "list[int]") -> None:
|
|
1201
|
+
"""
|
|
1202
|
+
Plural Register write (cmd 0x301). Writes `values` to consecutive
|
|
1203
|
+
registers from `start`. Each value 0-65535 (2-byte element). Only
|
|
1204
|
+
registers below the writable limit (0-559) accept a write.
|
|
1205
|
+
"""
|
|
1206
|
+
for v in values:
|
|
1207
|
+
if not 0 <= v <= 65535:
|
|
1208
|
+
raise ValueError("register value must be 0-65535")
|
|
1209
|
+
self._write_plural(_CMD_REG_PLURAL, start, len(values),
|
|
1210
|
+
struct.pack(f"<{len(values)}H", *values))
|
|
1211
|
+
|
|
1212
|
+
def read_b_vars(self, start: int, count: int) -> "list[int]":
|
|
1213
|
+
"""
|
|
1214
|
+
Plural Byte Variable read (cmd 0x302). Returns `count` consecutive B
|
|
1215
|
+
variables from `start` as a list of 0-255 ints (max 474). The wire count
|
|
1216
|
+
must be even, so an odd `count` reads one extra element and trims it.
|
|
1217
|
+
"""
|
|
1218
|
+
buf = self._read_plural(_CMD_B_PLURAL, start, count + (count & 1))
|
|
1219
|
+
return list(buf[:count])
|
|
1220
|
+
|
|
1221
|
+
def write_b_vars(self, start: int, values: "list[int]") -> None:
|
|
1222
|
+
"""
|
|
1223
|
+
Plural Byte Variable write (cmd 0x302). Writes `values` (each 0-255) to
|
|
1224
|
+
consecutive B variables from `start`. The wire count must be even, so an
|
|
1225
|
+
odd number of values is rejected — split or pad the run explicitly.
|
|
1226
|
+
"""
|
|
1227
|
+
if len(values) % 2 != 0:
|
|
1228
|
+
raise ValueError("B plural write needs an even number of values")
|
|
1229
|
+
for v in values:
|
|
1230
|
+
if not 0 <= v <= 255:
|
|
1231
|
+
raise ValueError("B variable value must be 0-255")
|
|
1232
|
+
self._write_plural(_CMD_B_PLURAL, start, len(values), bytes(values))
|
|
1233
|
+
|
|
1234
|
+
def read_i_vars(self, start: int, count: int) -> "list[int]":
|
|
1235
|
+
"""
|
|
1236
|
+
Plural Integer Variable read (cmd 0x303). Returns `count` consecutive I
|
|
1237
|
+
variables from `start` as signed 16-bit ints (2-byte element, max 237).
|
|
1238
|
+
"""
|
|
1239
|
+
return self._read_plural_scalar(_CMD_I_PLURAL, start, count, "h", 2)
|
|
1240
|
+
|
|
1241
|
+
def write_i_vars(self, start: int, values: "list[int]") -> None:
|
|
1242
|
+
"""
|
|
1243
|
+
Plural Integer Variable write (cmd 0x303). Writes `values` (each a signed
|
|
1244
|
+
16-bit int) to consecutive I variables from `start`.
|
|
1245
|
+
"""
|
|
1246
|
+
for v in values:
|
|
1247
|
+
if not -32768 <= v <= 32767:
|
|
1248
|
+
raise ValueError("I variable value must be a signed 16-bit int")
|
|
1249
|
+
self._write_plural(_CMD_I_PLURAL, start, len(values),
|
|
1250
|
+
struct.pack(f"<{len(values)}h", *values))
|
|
1251
|
+
|
|
1252
|
+
def read_d_vars(self, start: int, count: int) -> "list[int]":
|
|
1253
|
+
"""
|
|
1254
|
+
Plural Double Variable read (cmd 0x304). Returns `count` consecutive D
|
|
1255
|
+
variables from `start` as signed 32-bit ints (4-byte element, max 118).
|
|
1256
|
+
"""
|
|
1257
|
+
return self._read_plural_scalar(_CMD_D_PLURAL, start, count, "i", 4)
|
|
1258
|
+
|
|
1259
|
+
def write_d_vars(self, start: int, values: "list[int]") -> None:
|
|
1260
|
+
"""
|
|
1261
|
+
Plural Double Variable write (cmd 0x304). Writes `values` (each a signed
|
|
1262
|
+
32-bit int) to consecutive D variables from `start`.
|
|
1263
|
+
"""
|
|
1264
|
+
for v in values:
|
|
1265
|
+
if not -2_147_483_648 <= v <= 2_147_483_647:
|
|
1266
|
+
raise ValueError("D variable value must be a signed 32-bit int")
|
|
1267
|
+
self._write_plural(_CMD_D_PLURAL, start, len(values),
|
|
1268
|
+
struct.pack(f"<{len(values)}i", *values))
|
|
1269
|
+
|
|
1270
|
+
def read_r_vars(self, start: int, count: int) -> "list[float]":
|
|
1271
|
+
"""
|
|
1272
|
+
Plural Real Variable read (cmd 0x305). Returns `count` consecutive R
|
|
1273
|
+
variables from `start` as IEEE-754 float32 (4-byte element, max 118).
|
|
1274
|
+
"""
|
|
1275
|
+
return self._read_plural_scalar(_CMD_R_PLURAL, start, count, "f", 4)
|
|
1276
|
+
|
|
1277
|
+
def write_r_vars(self, start: int, values: "list[float]") -> None:
|
|
1278
|
+
"""
|
|
1279
|
+
Plural Real Variable write (cmd 0x305). Writes `values` as IEEE-754
|
|
1280
|
+
float32 to consecutive R variables from `start`.
|
|
1281
|
+
"""
|
|
1282
|
+
self._write_plural(_CMD_R_PLURAL, start, len(values),
|
|
1283
|
+
struct.pack(f"<{len(values)}f", *values))
|
|
1284
|
+
|
|
1285
|
+
def read_s_vars(self, start: int, count: int) -> "list[str]":
|
|
1286
|
+
"""
|
|
1287
|
+
Plural Character Variable read. 32-byte cmd 0x30C on DX200/YRC1000
|
|
1288
|
+
(max 14), 16-byte cmd 0x306 on DX100 (max 29). Returns `count` consecutive
|
|
1289
|
+
S variables from `start` as strings (trailing NULs stripped, latin-1).
|
|
1290
|
+
"""
|
|
1291
|
+
w = self._s_width
|
|
1292
|
+
buf = self._read_plural(self._s_plural_cmd, start, count)
|
|
1293
|
+
n = len(buf) // w
|
|
1294
|
+
return [buf[i * w:i * w + w].split(b"\x00", 1)[0].decode("latin-1")
|
|
1295
|
+
for i in range(n)]
|
|
1296
|
+
|
|
1297
|
+
def write_s_vars(self, start: int, values: "list[str]") -> None:
|
|
1298
|
+
"""
|
|
1299
|
+
Plural Character Variable write. 32-byte cmd 0x30C on DX200/YRC1000,
|
|
1300
|
+
16-byte cmd 0x306 on DX100. Writes `values` to consecutive S variables
|
|
1301
|
+
from `start`; each is latin-1 encoded, truncated and NUL-padded to width.
|
|
1302
|
+
"""
|
|
1303
|
+
w = self._s_width
|
|
1304
|
+
data = b"".join(
|
|
1305
|
+
v.encode("latin-1", errors="replace")[:w].ljust(w, b"\x00")
|
|
1306
|
+
for v in values
|
|
1307
|
+
)
|
|
1308
|
+
self._write_plural(self._s_plural_cmd, start, len(values), data)
|
|
1309
|
+
|
|
1310
|
+
def read_position_vars(self, start: int, count: int) -> "list[PositionVar]":
|
|
1311
|
+
"""
|
|
1312
|
+
Plural Robot Position Variable read (cmd 0x307). Returns `count`
|
|
1313
|
+
consecutive P variables from `start` as PositionVar records (52-byte
|
|
1314
|
+
element, max 9).
|
|
1315
|
+
"""
|
|
1316
|
+
buf = self._read_plural(_CMD_P_PLURAL, start, count)
|
|
1317
|
+
n = len(buf) // 52
|
|
1318
|
+
return [PositionVar.from_bytes(buf[i * 52:i * 52 + 52]) for i in range(n)]
|
|
1319
|
+
|
|
1320
|
+
def write_position_vars(self, start: int, values: "list[PositionVar]") -> None:
|
|
1321
|
+
"""
|
|
1322
|
+
Plural Robot Position Variable write (cmd 0x307). Writes `values` to
|
|
1323
|
+
consecutive P variables from `start`.
|
|
1324
|
+
"""
|
|
1325
|
+
self._write_plural(_CMD_P_PLURAL, start, len(values),
|
|
1326
|
+
b"".join(p.to_bytes() for p in values))
|
|
1327
|
+
|
|
1328
|
+
def _read_axispos_plural(self, cmd: int, start: int, count: int) -> "list[AxisPositionVar]":
|
|
1329
|
+
"""Plural read of 36-byte AxisPositionVar records. Shared by BP and EX."""
|
|
1330
|
+
buf = self._read_plural(cmd, start, count)
|
|
1331
|
+
n = len(buf) // 36
|
|
1332
|
+
return [AxisPositionVar.from_bytes(buf[i * 36:i * 36 + 36])
|
|
1333
|
+
for i in range(n)]
|
|
1334
|
+
|
|
1335
|
+
def read_bp_vars(self, start: int, count: int) -> "list[AxisPositionVar]":
|
|
1336
|
+
"""
|
|
1337
|
+
Plural Base Position Variable read (cmd 0x308). Returns `count`
|
|
1338
|
+
consecutive BP variables from `start` as AxisPositionVar records
|
|
1339
|
+
(36-byte element, max 13).
|
|
1340
|
+
"""
|
|
1341
|
+
return self._read_axispos_plural(_CMD_BP_PLURAL, start, count)
|
|
1342
|
+
|
|
1343
|
+
def write_bp_vars(self, start: int, values: "list[AxisPositionVar]") -> None:
|
|
1344
|
+
"""
|
|
1345
|
+
Plural Base Position Variable write (cmd 0x308). Writes `values` to
|
|
1346
|
+
consecutive BP variables from `start`.
|
|
1347
|
+
"""
|
|
1348
|
+
self._write_plural(_CMD_BP_PLURAL, start, len(values),
|
|
1349
|
+
b"".join(p.to_bytes() for p in values))
|
|
1350
|
+
|
|
1351
|
+
def read_ex_vars(self, start: int, count: int) -> "list[AxisPositionVar]":
|
|
1352
|
+
"""
|
|
1353
|
+
Plural Station/External Axis Variable read (cmd 0x309). Returns `count`
|
|
1354
|
+
consecutive EX variables from `start` as AxisPositionVar records
|
|
1355
|
+
(36-byte element, max 13; pulse-only).
|
|
1356
|
+
"""
|
|
1357
|
+
return self._read_axispos_plural(_CMD_EX_PLURAL, start, count)
|
|
1358
|
+
|
|
1359
|
+
def write_ex_vars(self, start: int, values: "list[AxisPositionVar]") -> None:
|
|
1360
|
+
"""
|
|
1361
|
+
Plural Station/External Axis Variable write (cmd 0x309). Writes `values`
|
|
1362
|
+
to consecutive EX variables from `start` (pulse-only).
|
|
1363
|
+
"""
|
|
1364
|
+
self._write_plural(_CMD_EX_PLURAL, start, len(values),
|
|
1365
|
+
b"".join(p.to_bytes() for p in values))
|
|
1366
|
+
|
|
1367
|
+
# ── system / management info ──────────────────────────────────────────────────
|
|
1368
|
+
|
|
1369
|
+
def read_management_time(self, kind: int = 1) -> dict:
|
|
1370
|
+
"""
|
|
1371
|
+
Management Time Acquiring (cmd 0x88).
|
|
1372
|
+
kind (instance): 1=Control-Power-ON, 10=Servo-Power-ON total, 11-18=Servo
|
|
1373
|
+
R1-R8, 110=Playback total, 210=Moving total, 301-308=Operation app 1-8.
|
|
1374
|
+
Returns dict: start_time (e.g. "2011/10/10 15:49"), elapse_time
|
|
1375
|
+
(e.g. "000000:00'00").
|
|
1376
|
+
"""
|
|
1377
|
+
r = self._tx(_CMD_MGMT_TIME, instance=kind, attribute=0, service=_SVC_GET_ALL)
|
|
1378
|
+
return _decode_management_time(r.data)
|
|
1379
|
+
|
|
1380
|
+
def read_system_info(self, kind: int = 11) -> dict:
|
|
1381
|
+
"""
|
|
1382
|
+
System Information Acquiring (cmd 0x89).
|
|
1383
|
+
kind (instance): 11-18=Type R1-R8, 21-44=Type S1-S24, 101-108=Application.
|
|
1384
|
+
Returns dict: sw_version (e.g. "YAS1.00.00A. (JP/US) -00"), model,
|
|
1385
|
+
parameter_version.
|
|
1386
|
+
"""
|
|
1387
|
+
r = self._tx(_CMD_SYS_INFO, instance=kind, attribute=0, service=_SVC_GET_ALL)
|
|
1388
|
+
return _decode_system_info(r.data)
|
|
1389
|
+
|
|
1390
|
+
def read_encoder_temp(self, group: int = 1) -> dict:
|
|
1391
|
+
"""
|
|
1392
|
+
Encoder Temperature Reading (cmd 0x0411, controller SW YAS4.81+).
|
|
1393
|
+
group 1-8 = R1-R8. Returns encoder temperature per axis in °C, keyed
|
|
1394
|
+
enc_temp_S/enc_temp_L/... (axis order per 0x74).
|
|
1395
|
+
Raises HsesError if the firmware predates YAS4.81, or RuntimeError on a
|
|
1396
|
+
DX100/DX200 controller (command not implemented).
|
|
1397
|
+
"""
|
|
1398
|
+
self._require_yrc1000("Encoder temperature (0x0411)")
|
|
1399
|
+
r = self._tx(_CMD_ENC_TEMP, instance=group, attribute=1, service=_SVC_GET_ALL)
|
|
1400
|
+
return _decode_axis_int32(r.data, "enc_temp", self.axis_names)
|
|
1401
|
+
|
|
1402
|
+
def read_regen_power(self) -> dict:
|
|
1403
|
+
"""
|
|
1404
|
+
Regenerative Power Reading (cmd 0x0412, controller SW YAS4.90+).
|
|
1405
|
+
Returns dict: valid_boards (bitmask), status (0=servo-off, 1=consumption,
|
|
1406
|
+
2=regeneration), unit (0=kWh, 1=0.01 kWh) and boards[] each with
|
|
1407
|
+
today/yesterday/cumulative energy.
|
|
1408
|
+
Raises HsesError if the firmware predates YAS4.90, or RuntimeError on a
|
|
1409
|
+
DX100/DX200 controller (command not implemented).
|
|
1410
|
+
"""
|
|
1411
|
+
self._require_yrc1000("Regenerative power (0x0412)")
|
|
1412
|
+
r = self._tx(_CMD_REGEN_PWR, instance=1, attribute=1, service=_SVC_GET_ALL)
|
|
1413
|
+
return _decode_regen_power(r.data)
|
|
1414
|
+
|
|
1415
|
+
def read_converter_temp(self, board: int = 1) -> int:
|
|
1416
|
+
"""
|
|
1417
|
+
Converter Temperature Reading (cmd 0x0413, controller SW YAS5.20+).
|
|
1418
|
+
board 1-8 = servo board number. Returns converter temperature in °C.
|
|
1419
|
+
Raises HsesError if the firmware predates YAS5.20, or RuntimeError on a
|
|
1420
|
+
DX100/DX200 controller (command not implemented).
|
|
1421
|
+
"""
|
|
1422
|
+
self._require_yrc1000("Converter temperature (0x0413)")
|
|
1423
|
+
r = self._tx(_CMD_CONV_TEMP, instance=board, attribute=1, service=_SVC_GET_ALL)
|
|
1424
|
+
if len(r.data) < 4:
|
|
1425
|
+
return 0
|
|
1426
|
+
return struct.unpack_from("<i", r.data, 0)[0]
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
# ── response decoders ─────────────────────────────────────────────────────────
|
|
1430
|
+
|
|
1431
|
+
def _decode_status(data: bytes, model: str = MODEL_YRC1000) -> dict:
|
|
1432
|
+
if len(data) < 8:
|
|
1433
|
+
return {}
|
|
1434
|
+
d1 = struct.unpack_from("<I", data, 0)[0]
|
|
1435
|
+
d2 = struct.unpack_from("<I", data, 4)[0]
|
|
1436
|
+
# Data1 bit4 differs by generation: DX100 = "in-guard safe operation",
|
|
1437
|
+
# DX200/YRC1000 = "speed limit". Emit the model-appropriate key.
|
|
1438
|
+
bit4_key = "in_guard_safe" if model == MODEL_DX100 else "speed_limit"
|
|
1439
|
+
return {
|
|
1440
|
+
"step": bool(d1 & 0x01),
|
|
1441
|
+
"one_cycle": bool(d1 & 0x02),
|
|
1442
|
+
"auto": bool(d1 & 0x04),
|
|
1443
|
+
"running": bool(d1 & 0x08),
|
|
1444
|
+
bit4_key: bool(d1 & 0x10),
|
|
1445
|
+
"teach": bool(d1 & 0x20),
|
|
1446
|
+
"play": bool(d1 & 0x40),
|
|
1447
|
+
"cmd_remote": bool(d1 & 0x80),
|
|
1448
|
+
"hold_pendant": bool(d2 & 0x02),
|
|
1449
|
+
"hold_external": bool(d2 & 0x04),
|
|
1450
|
+
"hold_cmd": bool(d2 & 0x08),
|
|
1451
|
+
"alarm": bool(d2 & 0x10),
|
|
1452
|
+
"error": bool(d2 & 0x20),
|
|
1453
|
+
"servo_on": bool(d2 & 0x40),
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def _decode_alarm(data: bytes) -> dict:
|
|
1458
|
+
"""Decode alarm data (up to 60 bytes = 15 × 32-bit LE ints, Section 3.3.3.1)."""
|
|
1459
|
+
out: dict = {
|
|
1460
|
+
"alarm_code": 0, "alarm_data": 0, "alarm_type": 0,
|
|
1461
|
+
"alarm_time": "", "alarm_name": "",
|
|
1462
|
+
}
|
|
1463
|
+
if len(data) < 12:
|
|
1464
|
+
return out
|
|
1465
|
+
out["alarm_code"] = struct.unpack_from("<I", data, 0)[0]
|
|
1466
|
+
out["alarm_data"] = struct.unpack_from("<I", data, 4)[0]
|
|
1467
|
+
out["alarm_type"] = struct.unpack_from("<I", data, 8)[0]
|
|
1468
|
+
if len(data) >= 28:
|
|
1469
|
+
# integers 4-7 = alarm time, 16 chars
|
|
1470
|
+
out["alarm_time"] = data[12:28].rstrip(b"\x00").decode("latin-1", errors="replace")
|
|
1471
|
+
if len(data) >= 60:
|
|
1472
|
+
# integers 8-15 = alarm name, 32 chars
|
|
1473
|
+
out["alarm_name"] = data[28:60].rstrip(b"\x00").decode("latin-1", errors="replace")
|
|
1474
|
+
return out
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
def _decode_alarm_sub(data: bytes) -> dict:
|
|
1478
|
+
"""
|
|
1479
|
+
Decode extended alarm data with sub-code strings (0x30A/0x30B, up to 268
|
|
1480
|
+
bytes, Section 3.3.3.37). First 60 bytes are identical to _decode_alarm();
|
|
1481
|
+
the three trailing char fields carry the diagnostic sub-code strings:
|
|
1482
|
+
[60] char[16] sub-code extra info (e.g. "[SV#1]")
|
|
1483
|
+
[76] char[96] sub-code data string
|
|
1484
|
+
[172] char[96] reverse-display info ("0"=normal, "1"=inverted)
|
|
1485
|
+
"""
|
|
1486
|
+
out = _decode_alarm(data)
|
|
1487
|
+
out.update({"sub_code_info": "", "sub_code_str": "", "reverse_display": ""})
|
|
1488
|
+
if len(data) >= 76:
|
|
1489
|
+
out["sub_code_info"] = _cstr(data[60:76])
|
|
1490
|
+
if len(data) >= 172:
|
|
1491
|
+
out["sub_code_str"] = _cstr(data[76:172])
|
|
1492
|
+
if len(data) >= 268:
|
|
1493
|
+
out["reverse_display"] = _cstr(data[172:268])
|
|
1494
|
+
return out
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def _decode_job(data: bytes) -> dict:
|
|
1498
|
+
"""Decode executing job data (44 bytes = 11 × 32-bit LE ints, Section 3.3.3.4)."""
|
|
1499
|
+
out: dict = {"job_name": "", "job_line": 0, "job_step": 0, "job_speed": 0}
|
|
1500
|
+
if len(data) < 32:
|
|
1501
|
+
return out
|
|
1502
|
+
out["job_name"] = data[0:32].rstrip(b"\x00").decode("latin-1", errors="replace")
|
|
1503
|
+
if len(data) >= 44:
|
|
1504
|
+
line, step, speed = struct.unpack_from("<3I", data, 32)
|
|
1505
|
+
out["job_line"] = line
|
|
1506
|
+
out["job_step"] = step
|
|
1507
|
+
out["job_speed"] = speed
|
|
1508
|
+
return out
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def _cstr(raw: bytes) -> str:
|
|
1512
|
+
"""Decode a fixed-width, null/space-padded ASCII field to a stripped str."""
|
|
1513
|
+
return raw.decode("latin-1", errors="replace").split("\x00", 1)[0].strip()
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def _decode_regen_power(data: bytes) -> dict:
|
|
1517
|
+
"""
|
|
1518
|
+
Decode regenerative power (0x0412, 104 bytes = 26 × int32): word0 valid-board
|
|
1519
|
+
bitmask, word1 byte-packed status/unit, then per board (1-8) three int32s
|
|
1520
|
+
today / yesterday / cumulative.
|
|
1521
|
+
"""
|
|
1522
|
+
out: dict = {"valid_boards": 0, "status": 0, "unit": 0, "boards": []}
|
|
1523
|
+
if len(data) < 8:
|
|
1524
|
+
return out
|
|
1525
|
+
out["valid_boards"] = struct.unpack_from("<I", data, 0)[0]
|
|
1526
|
+
out["status"] = data[4] # 0=servo-off, 1=consumption, 2=regeneration
|
|
1527
|
+
out["unit"] = data[5] # 0=kWh, 1=0.01 kWh
|
|
1528
|
+
for b in range(8):
|
|
1529
|
+
off = 8 + b * 12
|
|
1530
|
+
if off + 12 > len(data):
|
|
1531
|
+
break
|
|
1532
|
+
today, yesterday, cumulative = struct.unpack_from("<3i", data, off)
|
|
1533
|
+
out["boards"].append({
|
|
1534
|
+
"board": b + 1,
|
|
1535
|
+
"valid": bool(out["valid_boards"] & (1 << b)),
|
|
1536
|
+
"today": today,
|
|
1537
|
+
"yesterday": yesterday,
|
|
1538
|
+
"cumulative": cumulative,
|
|
1539
|
+
})
|
|
1540
|
+
return out
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
def _decode_management_time(data: bytes) -> dict:
|
|
1544
|
+
"""Decode management time (0x88): char[16] start + char[12] elapse."""
|
|
1545
|
+
out = {"start_time": "", "elapse_time": ""}
|
|
1546
|
+
if len(data) >= 16:
|
|
1547
|
+
out["start_time"] = _cstr(data[0:16])
|
|
1548
|
+
if len(data) >= 28:
|
|
1549
|
+
out["elapse_time"] = _cstr(data[16:28])
|
|
1550
|
+
return out
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _decode_system_info(data: bytes) -> dict:
|
|
1554
|
+
"""Decode system info (0x89): char[24] sw + char[16] model + char[8] param."""
|
|
1555
|
+
out = {"sw_version": "", "model": "", "parameter_version": ""}
|
|
1556
|
+
if len(data) >= 24:
|
|
1557
|
+
out["sw_version"] = _cstr(data[0:24])
|
|
1558
|
+
if len(data) >= 40:
|
|
1559
|
+
out["model"] = _cstr(data[24:40])
|
|
1560
|
+
if len(data) >= 48:
|
|
1561
|
+
out["parameter_version"] = _cstr(data[40:48])
|
|
1562
|
+
return out
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _decode_axis_config(data: bytes) -> dict:
|
|
1566
|
+
"""
|
|
1567
|
+
Decode axis configuration (32 bytes = 8 × 4-byte words, 0x74). Each word
|
|
1568
|
+
holds an axis name as ASCII characters (LE, null-padded); 0 = axis absent.
|
|
1569
|
+
"""
|
|
1570
|
+
out: dict = {"axes": []}
|
|
1571
|
+
n = min(len(data) // 4, 8)
|
|
1572
|
+
for i in range(n):
|
|
1573
|
+
name = data[i * 4:i * 4 + 4].rstrip(b"\x00").decode("latin-1", errors="replace")
|
|
1574
|
+
out[f"axis_{i + 1}"] = name
|
|
1575
|
+
if name:
|
|
1576
|
+
out["axes"].append(name)
|
|
1577
|
+
return out
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
_AXIS_NAMES = ("S", "L", "U", "R", "B", "T", "E", "8")
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _decode_axis_int32(data: bytes, prefix: str, names: tuple = _AXIS_NAMES) -> dict:
|
|
1584
|
+
"""
|
|
1585
|
+
Decode 8 × 32-bit LE signed ints as per-axis values (0x76 position error,
|
|
1586
|
+
0x77 torque, 0x0411 encoder temp). Keys are f"{prefix}_{axis}" in the axis
|
|
1587
|
+
order defined by 0x74 — pass the client's `axis_names` to label the 4th axis
|
|
1588
|
+
correctly (R on a wrist robot, T on a palletiser); defaults to S,L,U,R,B,T,E,8.
|
|
1589
|
+
"""
|
|
1590
|
+
out: dict = {}
|
|
1591
|
+
n = min(len(data) // 4, len(names))
|
|
1592
|
+
if n == 0:
|
|
1593
|
+
return out
|
|
1594
|
+
vals = struct.unpack_from(f"<{n}i", data)
|
|
1595
|
+
for name, v in zip(names, vals):
|
|
1596
|
+
out[f"{prefix}_{name}"] = v
|
|
1597
|
+
return out
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _decode_position(data: bytes, names: Optional[tuple] = None) -> dict:
|
|
1601
|
+
"""Decode position data (52 bytes = 13 × 32-bit LE signed ints, Section 3.3.3.6).
|
|
1602
|
+
|
|
1603
|
+
For a pulse record the joint values are labelled with the axis order from 0x74
|
|
1604
|
+
(pass the client's `axis_names`); without it the S,L,U,R,B,T default applies."""
|
|
1605
|
+
out: dict = {}
|
|
1606
|
+
n = len(data) // 4
|
|
1607
|
+
if n < 6:
|
|
1608
|
+
return out
|
|
1609
|
+
vals = list(struct.unpack_from(f"<{n}i", data))
|
|
1610
|
+
data_type = vals[0] # 0=pulse, 16=cartesian (base coordinate)
|
|
1611
|
+
axes = vals[5:]
|
|
1612
|
+
if data_type == 16:
|
|
1613
|
+
# X/Y/Z in µm → mm, Rx/Ry/Rz in 0.0001° → °
|
|
1614
|
+
for i, name in enumerate(["X", "Y", "Z", "Rx", "Ry", "Rz"]):
|
|
1615
|
+
if i < len(axes):
|
|
1616
|
+
out[f"pos_{name}"] = (
|
|
1617
|
+
round(axes[i] / 1000.0, 3) if i < 3 else
|
|
1618
|
+
round(axes[i] / 10000.0, 4)
|
|
1619
|
+
)
|
|
1620
|
+
else:
|
|
1621
|
+
pulse_names = names or ("S", "L", "U", "R", "B", "T")
|
|
1622
|
+
for i, name in enumerate(pulse_names):
|
|
1623
|
+
if i < len(axes):
|
|
1624
|
+
out[f"pos_{name}"] = axes[i]
|
|
1625
|
+
return out
|
|
1626
|
+
|
|
1627
|
+
|
|
1628
|
+
# ── quick self-test ───────────────────────────────────────────────────────────
|
|
1629
|
+
|
|
1630
|
+
if __name__ == "__main__":
|
|
1631
|
+
import sys
|
|
1632
|
+
|
|
1633
|
+
host = sys.argv[1] if len(sys.argv) > 1 else "192.0.2.10"
|
|
1634
|
+
print(f"Testing HSES at {host}:{HSES_PORT}")
|
|
1635
|
+
|
|
1636
|
+
with HsesClient(host, timeout=3.0) as c:
|
|
1637
|
+
try:
|
|
1638
|
+
s = c.read_status()
|
|
1639
|
+
print(f"STATUS: {s}")
|
|
1640
|
+
except Exception as e:
|
|
1641
|
+
print(f"STATUS: ERROR — {e}")
|
|
1642
|
+
|
|
1643
|
+
try:
|
|
1644
|
+
a = c.read_alarm(1)
|
|
1645
|
+
print(f"ALARM 1: {a}")
|
|
1646
|
+
except Exception as e:
|
|
1647
|
+
print(f"ALARM 1: ERROR — {e}")
|
|
1648
|
+
|
|
1649
|
+
try:
|
|
1650
|
+
j = c.read_job(1)
|
|
1651
|
+
print(f"JOB: {j}")
|
|
1652
|
+
except Exception as e:
|
|
1653
|
+
print(f"JOB: ERROR — {e}")
|
|
1654
|
+
|
|
1655
|
+
try:
|
|
1656
|
+
p = c.read_position(1)
|
|
1657
|
+
print(f"POS: {p}")
|
|
1658
|
+
except Exception as e:
|
|
1659
|
+
print(f"POS: ERROR — {e}")
|