griip-core-dev 0.5.6.dev25546__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.
- griip_core/__init__.py +0 -0
- griip_core/generated/__init__.py +0 -0
- griip_core/generated/griip_pb2.py +147 -0
- griip_core/generated/griip_pb2.pyi +669 -0
- griip_core/generated/griip_pb2_grpc.py +721 -0
- griip_core/generated/griip_pb2_grpc.pyi +570 -0
- griip_core/hardware/__init__.py +0 -0
- griip_core/hardware/tests/__init__.py +0 -0
- griip_core/hardware/tests/test_vrmcs_registration.py +179 -0
- griip_core/hardware/vrmcs_client.py +1257 -0
- griip_core/hardware/vrmcs_client.pyi +147 -0
- griip_core/hardware/vrmcs_registration.py +129 -0
- griip_core/py.typed +0 -0
- griip_core/robot_drivers.py +6 -0
- griip_core/utils/__init__.py +0 -0
- griip_core/utils/joint_math.py +90 -0
- griip_core/utils/tests/__init__.py +0 -0
- griip_core/utils/tests/test_joint_math.py +116 -0
- griip_core/utils/transforms.py +42 -0
- griip_core_dev-0.5.6.dev25546.dist-info/METADATA +12 -0
- griip_core_dev-0.5.6.dev25546.dist-info/RECORD +22 -0
- griip_core_dev-0.5.6.dev25546.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
"""Thin gRPC transport for VRMCS, backported from the VRMCS-py-client repo.
|
|
2
|
+
|
|
3
|
+
Vendored verbatim from ``vrmcs-client/vrmcs/client.py`` at VRMCS-py-client
|
|
4
|
+
fa1ff32 (the 1.5.0 release). It used to reach us inside the
|
|
5
|
+
``vention-firmware-grpc-client`` wheel; the wheel it is being replaced by,
|
|
6
|
+
``vention-grpc-py-sdk``, ships only the generated protobuf/gRPC stubs and no
|
|
7
|
+
hand-written client, so this had to come across with it.
|
|
8
|
+
|
|
9
|
+
It lives in griip-core rather than griip-sdk because four of its five callers
|
|
10
|
+
-- griip-sdk, vention-perception, mmai-griip-api and (through griip-sdk)
|
|
11
|
+
griip-teleop -- already depend on griip-core, and because griip-core is where
|
|
12
|
+
the rest of the shared VRMCS plumbing lives (see ``vrmcs_registration``).
|
|
13
|
+
Putting it in griip-sdk would make perception and mmai-griip-api depend on the
|
|
14
|
+
whole robot SDK -- toppra, trimesh, urchin, pymodbus, pyserial -- to reach a
|
|
15
|
+
gRPC transport.
|
|
16
|
+
|
|
17
|
+
Keep it in step with upstream by re-copying rather than editing in place: local
|
|
18
|
+
changes here will be lost the next time it is refreshed, and belong upstream.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import threading
|
|
23
|
+
import warnings
|
|
24
|
+
from typing import Callable, Optional
|
|
25
|
+
|
|
26
|
+
import grpc
|
|
27
|
+
from google.protobuf import empty_pb2
|
|
28
|
+
from vention.robots.v1 import robot_service_pb2_grpc
|
|
29
|
+
from vention.robots.v1.move_interpolation_request_pb2 import (
|
|
30
|
+
MoveInterpolationRequest,
|
|
31
|
+
MoveInterpolationResponse,
|
|
32
|
+
)
|
|
33
|
+
from vention.robots.v1.move_request_pb2 import (
|
|
34
|
+
MoveRequest,
|
|
35
|
+
MoveResponse,
|
|
36
|
+
MoveResultCode,
|
|
37
|
+
WaitForMoveCompletionRequest,
|
|
38
|
+
WaitForMoveCompletionResponse,
|
|
39
|
+
)
|
|
40
|
+
from vention.robots.v1.move_until_pb2 import (
|
|
41
|
+
MoveUntilRequest,
|
|
42
|
+
MoveUntilResponse,
|
|
43
|
+
MoveUntilResultCode,
|
|
44
|
+
)
|
|
45
|
+
from vention.robots.v1.plan_request_pb2 import (
|
|
46
|
+
PlanRequest,
|
|
47
|
+
PlanResponse,
|
|
48
|
+
PlanResultCode,
|
|
49
|
+
)
|
|
50
|
+
from vention.robots.v1.robot_alarm_pb2 import (
|
|
51
|
+
RobotAlarm,
|
|
52
|
+
RobotAlarmStreamRequest,
|
|
53
|
+
)
|
|
54
|
+
from vention.robots.v1.robot_configuration_pb2 import (
|
|
55
|
+
GetTcpRequest,
|
|
56
|
+
GetTcpResponse,
|
|
57
|
+
SetPayloadRequest,
|
|
58
|
+
SetPayloadResponse,
|
|
59
|
+
SetPayloadResultCode,
|
|
60
|
+
SetTcpRequest,
|
|
61
|
+
SetTcpResponse,
|
|
62
|
+
SetTcpResultCode,
|
|
63
|
+
)
|
|
64
|
+
from vention.robots.v1.robot_feedback_pb2 import (
|
|
65
|
+
Feedback,
|
|
66
|
+
FeedbackStreamRequest,
|
|
67
|
+
GetFeedbackRequest,
|
|
68
|
+
GetFeedbackResponse,
|
|
69
|
+
)
|
|
70
|
+
from vention.robots.v1.robot_kinematics_pb2 import (
|
|
71
|
+
ForwardKinematicsRequest,
|
|
72
|
+
ForwardKinematicsResponse,
|
|
73
|
+
InverseKinematicsBatchRequest,
|
|
74
|
+
InverseKinematicsBatchResponse,
|
|
75
|
+
InverseKinematicsRequest,
|
|
76
|
+
InverseKinematicsResponse,
|
|
77
|
+
KinResultCode,
|
|
78
|
+
)
|
|
79
|
+
from vention.robots.v1.robot_management_pb2 import (
|
|
80
|
+
ListRobotsResponse,
|
|
81
|
+
RegisterRobotRequest,
|
|
82
|
+
RegisterRobotResponse,
|
|
83
|
+
RobotInfo,
|
|
84
|
+
UnregisterRobotResponse,
|
|
85
|
+
)
|
|
86
|
+
from vention.robots.v1.robot_requests_pb2 import (
|
|
87
|
+
GetRobotInfoRequest,
|
|
88
|
+
JointStatesStreamRequest,
|
|
89
|
+
PositionStreamRequest,
|
|
90
|
+
StopRequest,
|
|
91
|
+
UnregisterRobotRequest,
|
|
92
|
+
)
|
|
93
|
+
from vention.robots.v1.robot_state_pb2 import (
|
|
94
|
+
CommandState,
|
|
95
|
+
CommandStateStreamRequest,
|
|
96
|
+
GetCommandStateRequest,
|
|
97
|
+
GetCommandStateResponse,
|
|
98
|
+
GetJointStatesRequest,
|
|
99
|
+
GetJointStatesResponse,
|
|
100
|
+
GetPositionStateRequest,
|
|
101
|
+
GetPositionStateResponse,
|
|
102
|
+
GetRobotStatesRequest,
|
|
103
|
+
GetRobotStatesResponse,
|
|
104
|
+
JointStates,
|
|
105
|
+
PositionState,
|
|
106
|
+
RobotStates,
|
|
107
|
+
RobotStatesStreamRequest,
|
|
108
|
+
SetToFreedriveRequest,
|
|
109
|
+
SetToFreedriveResponse,
|
|
110
|
+
SetToNormalRequest,
|
|
111
|
+
SetToNormalResponse,
|
|
112
|
+
)
|
|
113
|
+
from vention.robots.v1.stop_request_pb2 import (
|
|
114
|
+
StopResponse,
|
|
115
|
+
StopResultCode,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger = logging.getLogger("vrmcs-client")
|
|
119
|
+
logger.addHandler(logging.NullHandler())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class VRMCSClient:
|
|
123
|
+
"""
|
|
124
|
+
A client for interacting with the Vention Robot Motion Control System (VRMCS).
|
|
125
|
+
|
|
126
|
+
.. deprecated::
|
|
127
|
+
This client is deprecated. Use the gRPC stubs from
|
|
128
|
+
``vention.robots.v1.robot_service_pb2_grpc.RobotServiceStub`` directly
|
|
129
|
+
with types from ``vention.robots.v1.*_pb2``. This client will be removed in a future version (mmai v3.6.0 release).
|
|
130
|
+
|
|
131
|
+
The client communicates with the VRMCS server using gRPC and supports operations
|
|
132
|
+
such as:
|
|
133
|
+
- Robot registration and management
|
|
134
|
+
- Movement execution and planning
|
|
135
|
+
- Operating mode control (normal/freedrive)
|
|
136
|
+
- Interpolated trajectory execution
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, channel_url: str) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Initialize the VRMCS client with a gRPC channel.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
channel_url (str): The URL of the VRMCS server (e.g., "localhost:50051").
|
|
145
|
+
This should include the host and port where the VRMCS
|
|
146
|
+
gRPC server is listening.
|
|
147
|
+
"""
|
|
148
|
+
warnings.warn(
|
|
149
|
+
"VRMCSClient is deprecated. Use the gRPC stubs from "
|
|
150
|
+
"vention.robots.v1.robot_service_pb2_grpc.RobotServiceStub directly "
|
|
151
|
+
"with types from vention.robots.v1.*_pb2.",
|
|
152
|
+
DeprecationWarning,
|
|
153
|
+
stacklevel=2,
|
|
154
|
+
)
|
|
155
|
+
self.channel = grpc.insecure_channel(channel_url)
|
|
156
|
+
self.stub = robot_service_pb2_grpc.RobotServiceStub(self.channel)
|
|
157
|
+
|
|
158
|
+
self.joint_stream_thread: Optional[threading.Thread] = None
|
|
159
|
+
|
|
160
|
+
self.position_stream_thread: Optional[threading.Thread] = None
|
|
161
|
+
|
|
162
|
+
self.states_stream_thread: Optional[threading.Thread] = None
|
|
163
|
+
|
|
164
|
+
self.command_state_stream_thread: Optional[threading.Thread] = None
|
|
165
|
+
|
|
166
|
+
self.feedback_stream_thread: Optional[threading.Thread] = None
|
|
167
|
+
|
|
168
|
+
self.robot_alarm_stream_thread: Optional[threading.Thread] = None
|
|
169
|
+
|
|
170
|
+
def register_robot(self, register_robot_req: RegisterRobotRequest) -> RegisterRobotResponse:
|
|
171
|
+
"""
|
|
172
|
+
Register a robot instance with the VRMCS server.
|
|
173
|
+
|
|
174
|
+
This method registers a new robot with the system, making it available
|
|
175
|
+
for subsequent control operations. The robot must be properly configured
|
|
176
|
+
before registration.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
register_robot_req (RegisterRobotRequest): The robot registration request
|
|
180
|
+
containing robot instance details, configuration, and other metadata
|
|
181
|
+
required for registration.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
RegisterRobotResponse: Response containing registration status and any
|
|
185
|
+
error information if registration failed.
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
grpc.RpcError: If there's a communication error with the VRMCS server.
|
|
189
|
+
The error is caught and converted to a failed response.
|
|
190
|
+
|
|
191
|
+
Note:
|
|
192
|
+
Check the response.success field to determine if registration was successful.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
register_robot_res: RegisterRobotResponse = self.stub.RegisterRobot(register_robot_req)
|
|
196
|
+
if register_robot_res.success:
|
|
197
|
+
logger.info(
|
|
198
|
+
f"Robot '{register_robot_req.robot_instance.name}' "
|
|
199
|
+
+ f"with ID '{register_robot_req.robot_instance.id}' registered successfully"
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
logger.error(
|
|
203
|
+
f"Failed to register robot '{register_robot_req.robot_instance.name}' "
|
|
204
|
+
+ f"with ID '{register_robot_req.robot_instance.id}': {register_robot_res.error_message}"
|
|
205
|
+
)
|
|
206
|
+
except grpc.RpcError as e:
|
|
207
|
+
logger.error(f"gRPC error during RegisterRobot: {e.code()} - {e.details()}")
|
|
208
|
+
register_robot_res = RegisterRobotResponse(success=False, error_message=str(e))
|
|
209
|
+
return register_robot_res
|
|
210
|
+
|
|
211
|
+
def stop(self, stop_req: StopRequest) -> StopResponse:
|
|
212
|
+
"""
|
|
213
|
+
Sends a stop command to the robot.
|
|
214
|
+
This method instructs the robot to stop its current motion.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
stop_req (StopRequest): The stop request containing the robot ID.
|
|
218
|
+
Returns:
|
|
219
|
+
StopResponse: The response from the server containing the result of the stop operation.
|
|
220
|
+
This includes a result code indicating success or failure.
|
|
221
|
+
Raises:
|
|
222
|
+
May raise gRPC exceptions if communication with the server fails.
|
|
223
|
+
"""
|
|
224
|
+
stop_res: StopResponse = self.stub.Stop(stop_req)
|
|
225
|
+
if stop_res.result_code == StopResultCode.STOP_RESULT_CODE_SUCCESS:
|
|
226
|
+
logger.info(f"Stop command executed successfully for robot with ID '{stop_req.id}'")
|
|
227
|
+
else:
|
|
228
|
+
logger.error(
|
|
229
|
+
f"Stop command failed for robot with ID '{stop_req.id}' with result code {StopResultCode.Name(stop_res.result_code)}"
|
|
230
|
+
)
|
|
231
|
+
return stop_res
|
|
232
|
+
|
|
233
|
+
def set_to_normal(self, set_to_normal_req: SetToNormalRequest) -> SetToNormalResponse:
|
|
234
|
+
"""
|
|
235
|
+
Set a robot to normal operation state.
|
|
236
|
+
|
|
237
|
+
This method transitions a robot from any special operating mode (such as
|
|
238
|
+
freedrive or error state) back to normal operation mode, allowing it to
|
|
239
|
+
execute movement commands and other standard operations.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
set_to_normal_req (SetToNormalRequest): Request containing the robot ID
|
|
243
|
+
and any additional parameters needed to set the robot to normal state.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
SetToNormalResponse: Response indicating whether the state change was
|
|
247
|
+
successful and any error information if it failed.
|
|
248
|
+
|
|
249
|
+
Note:
|
|
250
|
+
The robot must be properly initialized and connected to transition
|
|
251
|
+
to normal state successfully.
|
|
252
|
+
"""
|
|
253
|
+
set_to_normal_res: SetToNormalResponse = self.stub.SetToNormal(set_to_normal_req)
|
|
254
|
+
if set_to_normal_res.success:
|
|
255
|
+
logger.info(f"Robot with ID '{set_to_normal_req.id}' set to normal operation state")
|
|
256
|
+
else:
|
|
257
|
+
logger.error(f"Failed to set robot with ID '{set_to_normal_req.id}' to normal operation state")
|
|
258
|
+
return set_to_normal_res
|
|
259
|
+
|
|
260
|
+
def set_to_freedrive(self, set_to_freedrive_req: SetToFreedriveRequest) -> SetToFreedriveResponse:
|
|
261
|
+
"""
|
|
262
|
+
Enable or disable freedrive mode for a robot.
|
|
263
|
+
|
|
264
|
+
Freedrive mode allows manual manipulation of the robot arm by disabling
|
|
265
|
+
motor torques while maintaining position awareness. This is useful for
|
|
266
|
+
manual teaching, positioning, or when physical interaction with the robot
|
|
267
|
+
is required.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
set_to_freedrive_req (SetToFreedriveRequest): Request containing the
|
|
271
|
+
robot ID and enable flag. Set enable=True to activate freedrive
|
|
272
|
+
mode, or enable=False to deactivate it.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
SetToFreedriveResponse: Response indicating whether the mode change was
|
|
276
|
+
successful and any error information if it failed.
|
|
277
|
+
|
|
278
|
+
Warning:
|
|
279
|
+
When freedrive mode is enabled, the robot cannot execute movement
|
|
280
|
+
commands. Ensure the robot is in a safe position before enabling
|
|
281
|
+
freedrive mode.
|
|
282
|
+
"""
|
|
283
|
+
set_to_freedrive_res: SetToFreedriveResponse = self.stub.SetToFreedrive(set_to_freedrive_req)
|
|
284
|
+
if set_to_freedrive_res.success:
|
|
285
|
+
state = "enabled" if set_to_freedrive_req.enable else "disabled"
|
|
286
|
+
logger.info(f"Freedrive mode {state} for robot with ID '{set_to_freedrive_req.id}'")
|
|
287
|
+
else:
|
|
288
|
+
logger.error(f"Failed to change freedrive mode for robot with ID '{set_to_freedrive_req.id}'")
|
|
289
|
+
return set_to_freedrive_res
|
|
290
|
+
|
|
291
|
+
def move(self, move_req: MoveRequest) -> MoveResponse:
|
|
292
|
+
"""
|
|
293
|
+
Execute a robot movement command.
|
|
294
|
+
|
|
295
|
+
This method sends a movement request to the robot, which can include
|
|
296
|
+
various types of motion such as joint moves, Cartesian moves, or
|
|
297
|
+
complex trajectories. The method blocks until the movement is completed
|
|
298
|
+
or fails.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
move_req (MoveRequest): The movement request containing target positions,
|
|
302
|
+
motion parameters, robot ID, and other movement specifications.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
MoveResponse: Response containing the execution result, duration,
|
|
306
|
+
and error information if the movement failed. Check result_code
|
|
307
|
+
for success/failure status.
|
|
308
|
+
|
|
309
|
+
Note:
|
|
310
|
+
- The robot must be in normal operation mode to execute moves
|
|
311
|
+
- Failed movements will include error_segment information indicating
|
|
312
|
+
where the failure occurred in multi-segment moves
|
|
313
|
+
- Duration is reported in seconds for successful moves
|
|
314
|
+
"""
|
|
315
|
+
move_res: MoveResponse = self.stub.Move(move_req)
|
|
316
|
+
if move_res.result_code == PlanResultCode.PLAN_RESULT_CODE_SUCCESS:
|
|
317
|
+
logger.info(f"Move command executed successfully in {move_res.duration} seconds")
|
|
318
|
+
else:
|
|
319
|
+
logger.error(f"Move command failed at segment {move_res.error_segment} with plan result code {move_res.result_code}")
|
|
320
|
+
return move_res
|
|
321
|
+
|
|
322
|
+
def plan(self, plan_req: PlanRequest) -> PlanResponse:
|
|
323
|
+
"""
|
|
324
|
+
Plan a robot trajectory without executing it.
|
|
325
|
+
|
|
326
|
+
This method generates and validates a motion plan for the specified
|
|
327
|
+
movement without actually executing the robot motion. This is useful
|
|
328
|
+
for verifying that a planned movement is feasible before execution,
|
|
329
|
+
or for pre-computing trajectories.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
plan_req (PlanRequest): The planning request containing target positions,
|
|
333
|
+
motion parameters, robot ID, and constraints for trajectory planning.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
PlanResponse: Response containing the planning result, estimated duration,
|
|
337
|
+
and error information if planning failed. Check result_code for
|
|
338
|
+
success/failure status.
|
|
339
|
+
|
|
340
|
+
Note:
|
|
341
|
+
- Failed plans will include error_segment information indicating
|
|
342
|
+
where the failure occurred in multi-segment plans
|
|
343
|
+
- Planning does not change robot state or position
|
|
344
|
+
- Successful planning provides duration estimates for the planned motion
|
|
345
|
+
"""
|
|
346
|
+
plan_res: PlanResponse = self.stub.Plan(plan_req)
|
|
347
|
+
if plan_res.result_code == PlanResultCode.PLAN_RESULT_CODE_SUCCESS:
|
|
348
|
+
logger.info(f"Plan command executed successfully in {plan_res.duration} seconds")
|
|
349
|
+
else:
|
|
350
|
+
logger.error(f"Plan command failed at segment {plan_res.error_segment} with plan result code {plan_res.result_code}")
|
|
351
|
+
return plan_res
|
|
352
|
+
|
|
353
|
+
def wait_for_move_completion(self, request: WaitForMoveCompletionRequest) -> WaitForMoveCompletionResponse:
|
|
354
|
+
"""
|
|
355
|
+
Block until the robot completes its current movement.
|
|
356
|
+
|
|
357
|
+
This method continuously checks the robot's status and blocks execution
|
|
358
|
+
until the robot has finished executing its current move command. This is
|
|
359
|
+
useful for ensuring that subsequent commands are not sent until the
|
|
360
|
+
robot is idle.
|
|
361
|
+
Args:
|
|
362
|
+
request (WaitForMoveCompletionRequest): Request containing the robot ID
|
|
363
|
+
for which to wait for movement completion.
|
|
364
|
+
Returns:
|
|
365
|
+
WaitForMoveCompletionResponse: Response with error code indicating whether the move
|
|
366
|
+
was completed successfully and any error information if it failed.
|
|
367
|
+
"""
|
|
368
|
+
wait_res: WaitForMoveCompletionResponse = self.stub.WaitForMoveCompletion(request)
|
|
369
|
+
if wait_res.result_code == MoveResultCode.MOVE_RESULT_CODE_SUCCESS:
|
|
370
|
+
logger.info(f"Robot with ID '{request.id}' has completed its movement and is now idle")
|
|
371
|
+
else:
|
|
372
|
+
logger.error(f"Failed while waiting for robot with ID '{request.id}' to complete movement")
|
|
373
|
+
return wait_res
|
|
374
|
+
|
|
375
|
+
def move_until(self, move_until_req: MoveUntilRequest) -> MoveUntilResponse:
|
|
376
|
+
"""
|
|
377
|
+
Execute a force/torque-aware move-until command.
|
|
378
|
+
|
|
379
|
+
This method moves the robot in a given Cartesian direction until one of
|
|
380
|
+
the stop conditions is met, such as contact force threshold reached or
|
|
381
|
+
maximum travel distance reached.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
move_until_req (MoveUntilRequest): Request containing robot ID,
|
|
385
|
+
direction, velocity, travel limit, and contact thresholds.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
MoveUntilResponse: Response containing the result code and optional
|
|
389
|
+
contact pose.
|
|
390
|
+
"""
|
|
391
|
+
move_until_res: MoveUntilResponse = self.stub.MoveUntil(move_until_req)
|
|
392
|
+
if move_until_res.result_code == MoveUntilResultCode.MOVE_UNTIL_RESULT_CODE_CONTACT_REACHED:
|
|
393
|
+
logger.info(f"MoveUntil completed for robot '{move_until_req.id}' with contact reached")
|
|
394
|
+
elif move_until_res.result_code == MoveUntilResultCode.MOVE_UNTIL_RESULT_CODE_MAX_TRAVEL_REACHED:
|
|
395
|
+
logger.info(f"MoveUntil completed for robot '{move_until_req.id}' with max travel reached")
|
|
396
|
+
else:
|
|
397
|
+
logger.error(
|
|
398
|
+
"MoveUntil failed for robot '%s' with result code %s",
|
|
399
|
+
move_until_req.id,
|
|
400
|
+
MoveUntilResultCode.Name(move_until_res.result_code),
|
|
401
|
+
)
|
|
402
|
+
return move_until_res
|
|
403
|
+
|
|
404
|
+
def _joint_stream_consume(
|
|
405
|
+
self,
|
|
406
|
+
stream_req: JointStatesStreamRequest,
|
|
407
|
+
on_data: Callable[[JointStates], None],
|
|
408
|
+
):
|
|
409
|
+
self.joint_stream_iterator = self.stub.JointStatesStream(stream_req)
|
|
410
|
+
try:
|
|
411
|
+
for response in self.joint_stream_iterator:
|
|
412
|
+
result: JointStates = response
|
|
413
|
+
try:
|
|
414
|
+
on_data(result)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.error(f"Error in joint states callback: {e}")
|
|
417
|
+
except grpc.RpcError as e:
|
|
418
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
419
|
+
logger.info("Joint stream cancelled successfully.")
|
|
420
|
+
else:
|
|
421
|
+
logger.error(f"gRPC error during JointStatesStream: {e.code()} - {e.details()}")
|
|
422
|
+
|
|
423
|
+
def joint_stream_subscribe(
|
|
424
|
+
self,
|
|
425
|
+
stream_req: JointStatesStreamRequest,
|
|
426
|
+
on_data: Callable[[JointStates], None],
|
|
427
|
+
):
|
|
428
|
+
"""
|
|
429
|
+
Subscribe to a stream of joint states.
|
|
430
|
+
|
|
431
|
+
This method starts a background thread that consumes joint state data from the server
|
|
432
|
+
and calls the provided callback function for each update.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
stream_req (JointStatesStreamRequest): Configuration for the joint states stream.
|
|
436
|
+
Includes id and optional max_frequency_hz (0 = no limit).
|
|
437
|
+
on_data (Callable[[JointStates], None]): Callback function that will be called
|
|
438
|
+
with each JointStates update. Any exceptions raised in the callback will be
|
|
439
|
+
logged but won't stop the stream.
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
```python
|
|
443
|
+
def handle_joint_states(joint_states: JointStates):
|
|
444
|
+
print(f"Joint positions: {joint_states.joint_position.positions}")
|
|
445
|
+
|
|
446
|
+
client.joint_stream_subscribe(
|
|
447
|
+
JointStatesStreamRequest(id="robot_1"),
|
|
448
|
+
on_data=handle_joint_states
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Note:
|
|
453
|
+
This method starts a background thread. To stop the stream, call
|
|
454
|
+
joint_stream_unsubscribe().
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
if (self.joint_stream_thread is None) or (not self.joint_stream_thread.is_alive()):
|
|
458
|
+
self.joint_stream_thread = threading.Thread(
|
|
459
|
+
target=lambda: self._joint_stream_consume(stream_req, on_data),
|
|
460
|
+
daemon=True, # Daemon thread won't block program exit
|
|
461
|
+
)
|
|
462
|
+
self.joint_stream_thread.start()
|
|
463
|
+
else:
|
|
464
|
+
logger.warning("Joint stream is already active. Subscription request ignored.")
|
|
465
|
+
|
|
466
|
+
def joint_stream_unsubscribe(self):
|
|
467
|
+
"""
|
|
468
|
+
Unsubscribes from the joint stream.
|
|
469
|
+
|
|
470
|
+
This method stops the joint stream thread by cancelling the gRPC iterator,
|
|
471
|
+
which causes the streaming loop to exit gracefully, then waits for the thread to terminate.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
None
|
|
475
|
+
"""
|
|
476
|
+
if self.joint_stream_thread:
|
|
477
|
+
if self.joint_stream_thread.is_alive() and self.joint_stream_iterator:
|
|
478
|
+
self.joint_stream_iterator.cancel()
|
|
479
|
+
self.joint_stream_thread.join()
|
|
480
|
+
if self.joint_stream_thread.is_alive():
|
|
481
|
+
logger.warning("Joint stream thread did not exit cleanly within timeout")
|
|
482
|
+
self.joint_stream_thread = None
|
|
483
|
+
self.joint_stream_iterator = None
|
|
484
|
+
|
|
485
|
+
def _position_stream_consume(
|
|
486
|
+
self,
|
|
487
|
+
stream_req: PositionStreamRequest,
|
|
488
|
+
on_data: Callable[[PositionState], None],
|
|
489
|
+
):
|
|
490
|
+
self.position_stream_iterator = self.stub.PositionStream(stream_req)
|
|
491
|
+
try:
|
|
492
|
+
for response in self.position_stream_iterator:
|
|
493
|
+
result: PositionState = response
|
|
494
|
+
try:
|
|
495
|
+
on_data(result)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
logger.error(f"Error in position state callback: {e}")
|
|
498
|
+
except grpc.RpcError as e:
|
|
499
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
500
|
+
logger.info("Position stream cancelled successfully.")
|
|
501
|
+
else:
|
|
502
|
+
logger.error(f"gRPC error during PositionStream: {e.code()} - {e.details()}")
|
|
503
|
+
|
|
504
|
+
def position_stream_subscribe(
|
|
505
|
+
self,
|
|
506
|
+
stream_req: PositionStreamRequest,
|
|
507
|
+
on_data: Callable[[PositionState], None],
|
|
508
|
+
):
|
|
509
|
+
"""
|
|
510
|
+
Subscribe to a position data stream from the VRMCS server.
|
|
511
|
+
|
|
512
|
+
This method starts a background thread that consumes position data from the server
|
|
513
|
+
and calls the provided callback function for each update.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
stream_req (PositionStreamRequest): The request parameters for the position stream.
|
|
517
|
+
Includes id and optional max_frequency_hz (0 = no limit).
|
|
518
|
+
|
|
519
|
+
on_data (Callable[[PositionState], None]): Callback function that will be called
|
|
520
|
+
with each PositionState update. Any exceptions raised in the callback will be
|
|
521
|
+
logged but won't stop the stream.
|
|
522
|
+
|
|
523
|
+
Example:
|
|
524
|
+
```python
|
|
525
|
+
def handle_position(position_state: PositionState):
|
|
526
|
+
pose = position_state.cartesian_pose
|
|
527
|
+
print(f"Position: x={pose.position.x}, y={pose.position.y}, z={pose.position.z}")
|
|
528
|
+
|
|
529
|
+
client.position_stream_subscribe(
|
|
530
|
+
PositionStreamRequest(id="robot_1"),
|
|
531
|
+
on_data=handle_position
|
|
532
|
+
)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Note:
|
|
536
|
+
To stop the stream, call the position_stream_unsubscribe method.
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
if (self.position_stream_thread is None) or (not self.position_stream_thread.is_alive()):
|
|
540
|
+
self.position_stream_thread = threading.Thread(
|
|
541
|
+
target=lambda: self._position_stream_consume(stream_req, on_data),
|
|
542
|
+
daemon=True, # Daemon thread won't block program exit
|
|
543
|
+
)
|
|
544
|
+
self.position_stream_thread.start()
|
|
545
|
+
else:
|
|
546
|
+
logger.warning("Position stream is already active. Subscription request ignored.")
|
|
547
|
+
|
|
548
|
+
def position_stream_unsubscribe(self):
|
|
549
|
+
"""
|
|
550
|
+
Unsubscribe from the position stream.
|
|
551
|
+
|
|
552
|
+
This method stops the position streaming thread by cancelling the gRPC iterator,
|
|
553
|
+
which causes the streaming loop to exit gracefully, then waits for the thread to terminate.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
None
|
|
557
|
+
"""
|
|
558
|
+
if self.position_stream_thread:
|
|
559
|
+
if self.position_stream_thread.is_alive() and self.position_stream_iterator:
|
|
560
|
+
self.position_stream_iterator.cancel()
|
|
561
|
+
self.position_stream_thread.join()
|
|
562
|
+
if self.position_stream_thread.is_alive():
|
|
563
|
+
logger.warning("Position stream thread did not exit cleanly within timeout")
|
|
564
|
+
self.position_stream_thread = None
|
|
565
|
+
self.position_stream_iterator = None
|
|
566
|
+
|
|
567
|
+
def _states_stream_consume(
|
|
568
|
+
self,
|
|
569
|
+
stream_req: RobotStatesStreamRequest,
|
|
570
|
+
on_data: Callable[[RobotStates], None],
|
|
571
|
+
):
|
|
572
|
+
self.states_stream_iterator = self.stub.RobotStatesStream(stream_req)
|
|
573
|
+
try:
|
|
574
|
+
for response in self.states_stream_iterator:
|
|
575
|
+
result: RobotStates = response
|
|
576
|
+
try:
|
|
577
|
+
on_data(result)
|
|
578
|
+
except Exception as e:
|
|
579
|
+
logger.error(f"Error in robot states callback: {e}")
|
|
580
|
+
except grpc.RpcError as e:
|
|
581
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
582
|
+
logger.info("Robot states stream cancelled successfully.")
|
|
583
|
+
else:
|
|
584
|
+
logger.error(f"gRPC error during RobotStatesStream: {e.code()} - {e.details()}")
|
|
585
|
+
|
|
586
|
+
def states_stream_subscribe(
|
|
587
|
+
self,
|
|
588
|
+
stream_req: RobotStatesStreamRequest,
|
|
589
|
+
on_data: Callable[[RobotStates], None],
|
|
590
|
+
):
|
|
591
|
+
"""
|
|
592
|
+
Subscribe to a stream of robot state changes.
|
|
593
|
+
|
|
594
|
+
This method starts a background thread that consumes robot state data from the server
|
|
595
|
+
and calls the provided callback function for each state change. Unlike position/joint
|
|
596
|
+
streams which send continuous updates, this is an event-driven stream that only sends
|
|
597
|
+
messages when the robot's state actually changes (connection_state, operational_state,
|
|
598
|
+
or safety_state).
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
stream_req (RobotStatesStreamRequest): Configuration for the robot states stream
|
|
602
|
+
including robot ID.
|
|
603
|
+
on_data (Callable[[RobotStates], None]): Callback function that will be called
|
|
604
|
+
with each RobotStates update. Any exceptions raised in the callback will be
|
|
605
|
+
logged but won't stop the stream.
|
|
606
|
+
|
|
607
|
+
Example:
|
|
608
|
+
```python
|
|
609
|
+
def handle_state_change(states: RobotStates):
|
|
610
|
+
print(f"State changed: {RobotOperationalState.Name(states.operational_state)}")
|
|
611
|
+
|
|
612
|
+
client.states_stream_subscribe(
|
|
613
|
+
RobotStatesStreamRequest(id="robot_1"),
|
|
614
|
+
on_data=handle_state_change
|
|
615
|
+
)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Note:
|
|
619
|
+
To stop the stream, call states_stream_unsubscribe().
|
|
620
|
+
Only one robot states stream can be active at a time per client.
|
|
621
|
+
"""
|
|
622
|
+
if (self.states_stream_thread is None) or (not self.states_stream_thread.is_alive()):
|
|
623
|
+
self.states_stream_thread = threading.Thread(
|
|
624
|
+
target=lambda: self._states_stream_consume(stream_req, on_data),
|
|
625
|
+
daemon=True, # Daemon thread won't block program exit
|
|
626
|
+
)
|
|
627
|
+
self.states_stream_thread.start()
|
|
628
|
+
else:
|
|
629
|
+
logger.warning("States stream is already active. Subscription request ignored.")
|
|
630
|
+
|
|
631
|
+
def states_stream_unsubscribe(self):
|
|
632
|
+
"""
|
|
633
|
+
Unsubscribe from the robot states stream.
|
|
634
|
+
|
|
635
|
+
This method stops the robot states streaming thread by cancelling the gRPC iterator,
|
|
636
|
+
which causes the streaming loop to exit gracefully, then waits for the thread to terminate.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
None
|
|
640
|
+
"""
|
|
641
|
+
if self.states_stream_thread:
|
|
642
|
+
if self.states_stream_thread.is_alive() and self.states_stream_iterator:
|
|
643
|
+
self.states_stream_iterator.cancel()
|
|
644
|
+
self.states_stream_thread.join()
|
|
645
|
+
if self.states_stream_thread.is_alive():
|
|
646
|
+
logger.warning("States stream thread did not exit cleanly within timeout")
|
|
647
|
+
self.states_stream_thread = None
|
|
648
|
+
self.states_stream_iterator = None
|
|
649
|
+
|
|
650
|
+
def move_interpolation(self, move_interp_req: MoveInterpolationRequest) -> MoveInterpolationResponse:
|
|
651
|
+
"""
|
|
652
|
+
Execute an interpolated trajectory movement.
|
|
653
|
+
|
|
654
|
+
This method executes a pre-defined trajectory consisting of multiple
|
|
655
|
+
waypoints with interpolated motion between them. The robot follows
|
|
656
|
+
a smooth path through all specified points, which is useful for
|
|
657
|
+
complex motions and precise trajectory following.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
move_interp_req (MoveInterpolationRequest): Request containing the
|
|
661
|
+
trajectory points, interpolation parameters, timing constraints,
|
|
662
|
+
robot ID, and other trajectory specifications.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
MoveInterpolationResponse: Response containing execution result,
|
|
666
|
+
duration, and error information if the trajectory failed.
|
|
667
|
+
Check result_code for success/failure status.
|
|
668
|
+
|
|
669
|
+
Note:
|
|
670
|
+
- The robot must be in normal operation mode to execute trajectories
|
|
671
|
+
- Failed trajectories will include error_point information indicating
|
|
672
|
+
which waypoint caused the failure
|
|
673
|
+
- Duration is reported in seconds for successful trajectory planning
|
|
674
|
+
"""
|
|
675
|
+
move_interp_res: MoveInterpolationResponse = self.stub.MoveInterpolation(move_interp_req)
|
|
676
|
+
if move_interp_res.result_code == PlanResultCode.PLAN_RESULT_CODE_SUCCESS:
|
|
677
|
+
logger.info(f"Trajectory started successfully in {move_interp_res.duration} seconds")
|
|
678
|
+
else:
|
|
679
|
+
logger.error(
|
|
680
|
+
f"Trajectory plan failed at point {move_interp_res.error_point} " + f"with plan result code {move_interp_res.result_code}"
|
|
681
|
+
)
|
|
682
|
+
return move_interp_res
|
|
683
|
+
|
|
684
|
+
def list_robots(self) -> ListRobotsResponse:
|
|
685
|
+
"""
|
|
686
|
+
Retrieve a list of all registered robots from the VRMCS server.
|
|
687
|
+
|
|
688
|
+
"""
|
|
689
|
+
list_robots_res: ListRobotsResponse = self.stub.ListRobots(empty_pb2.Empty())
|
|
690
|
+
return list_robots_res
|
|
691
|
+
|
|
692
|
+
def set_payload(self, set_payload_req: SetPayloadRequest) -> SetPayloadResponse:
|
|
693
|
+
"""
|
|
694
|
+
Set the payload mass properties for a robot.
|
|
695
|
+
|
|
696
|
+
This method configures the robot's payload properties, which affects dynamics
|
|
697
|
+
calculations and motion planning. Proper payload configuration is essential
|
|
698
|
+
for accurate motion control and safety.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
set_payload_req (SetPayloadRequest): Request containing the robot ID
|
|
702
|
+
and mass properties (mass, center of mass, inertia tensor).
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
SetPayloadResponse: Response indicating whether the payload was set
|
|
706
|
+
successfully and result code.
|
|
707
|
+
"""
|
|
708
|
+
set_payload_res: SetPayloadResponse = self.stub.SetPayload(set_payload_req)
|
|
709
|
+
if set_payload_res.result_code == SetPayloadResultCode.SET_PAYLOAD_RESULT_CODE_SUCCESS:
|
|
710
|
+
logger.info("Set payload command executed successfully")
|
|
711
|
+
else:
|
|
712
|
+
logger.error(f"Set payload command failed with result code {set_payload_res.result_code}")
|
|
713
|
+
return set_payload_res
|
|
714
|
+
|
|
715
|
+
def unregister_robot(self, unregister_robot_req: UnregisterRobotRequest) -> UnregisterRobotResponse:
|
|
716
|
+
"""
|
|
717
|
+
Unregister a robot from the VRMCS server.
|
|
718
|
+
|
|
719
|
+
This method removes a previously registered robot from the system, freeing
|
|
720
|
+
up resources and making it unavailable for control operations.
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
unregister_robot_req (UnregisterRobotRequest): Request containing the
|
|
724
|
+
robot ID to unregister.
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
UnregisterRobotResponse: Response indicating success or failure with
|
|
728
|
+
error message if unregistration failed.
|
|
729
|
+
|
|
730
|
+
Raises:
|
|
731
|
+
grpc.RpcError: If there's a communication error with the VRMCS server.
|
|
732
|
+
The error is caught and converted to a failed response.
|
|
733
|
+
"""
|
|
734
|
+
try:
|
|
735
|
+
unregister_robot_res: UnregisterRobotResponse = self.stub.UnregisterRobot(unregister_robot_req)
|
|
736
|
+
if unregister_robot_res.success:
|
|
737
|
+
logger.info(f"Robot with ID '{unregister_robot_req.id}' unregistered successfully")
|
|
738
|
+
else:
|
|
739
|
+
logger.error(f"Failed to unregister robot with ID '{unregister_robot_req.id}': {unregister_robot_res.error_message}")
|
|
740
|
+
except grpc.RpcError as e:
|
|
741
|
+
logger.error(f"gRPC error during UnregisterRobot: {e.code()} - {e.details()}")
|
|
742
|
+
unregister_robot_res = UnregisterRobotResponse(success=False, error_message=str(e))
|
|
743
|
+
return unregister_robot_res
|
|
744
|
+
|
|
745
|
+
def get_robot_info(self, get_robot_info_req: GetRobotInfoRequest) -> RobotInfo:
|
|
746
|
+
"""
|
|
747
|
+
Get detailed information about a registered robot.
|
|
748
|
+
|
|
749
|
+
This method retrieves configuration and status information for a specific
|
|
750
|
+
robot including its name, type, and joint configuration.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
get_robot_info_req (GetRobotInfoRequest): Request containing the robot ID.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
RobotInfo: Information about the robot including id, name,
|
|
757
|
+
robot_type, and joint_names.
|
|
758
|
+
|
|
759
|
+
Raises:
|
|
760
|
+
grpc.RpcError: If there's a communication error with the VRMCS server
|
|
761
|
+
or if the robot is not found.
|
|
762
|
+
"""
|
|
763
|
+
robot_info: RobotInfo = self.stub.GetRobotInfo(get_robot_info_req)
|
|
764
|
+
logger.info(f"Retrieved info for robot '{robot_info.name}' (ID: '{robot_info.id}')")
|
|
765
|
+
return robot_info
|
|
766
|
+
|
|
767
|
+
def _command_state_stream_consume(
|
|
768
|
+
self,
|
|
769
|
+
stream_req: CommandStateStreamRequest,
|
|
770
|
+
on_data: Callable[[CommandState], None],
|
|
771
|
+
):
|
|
772
|
+
self.command_state_stream_iterator = self.stub.CommandStateStream(stream_req)
|
|
773
|
+
try:
|
|
774
|
+
for response in self.command_state_stream_iterator:
|
|
775
|
+
result: CommandState = response
|
|
776
|
+
try:
|
|
777
|
+
on_data(result)
|
|
778
|
+
except Exception as e:
|
|
779
|
+
logger.error(f"Error in command state callback: {e}")
|
|
780
|
+
except grpc.RpcError as e:
|
|
781
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
782
|
+
logger.info("Command state stream cancelled successfully.")
|
|
783
|
+
else:
|
|
784
|
+
logger.error(f"gRPC error during CommandStateStream: {e.code()} - {e.details()}")
|
|
785
|
+
|
|
786
|
+
def command_state_stream_subscribe(
|
|
787
|
+
self,
|
|
788
|
+
stream_req: CommandStateStreamRequest,
|
|
789
|
+
on_data: Callable[[CommandState], None],
|
|
790
|
+
):
|
|
791
|
+
"""
|
|
792
|
+
Subscribe to a stream of robot command states.
|
|
793
|
+
|
|
794
|
+
This method starts a background thread that consumes command state data from the server
|
|
795
|
+
and calls the provided callback function for each update. Command states include joint
|
|
796
|
+
and Cartesian positions, velocities, and accelerations being commanded to the robot.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
stream_req (CommandStateStreamRequest): Configuration for the command
|
|
800
|
+
state stream including id and optional max_frequency_hz (0 = no limit).
|
|
801
|
+
|
|
802
|
+
on_data (Callable[[CommandState], None]): Callback function that will be called
|
|
803
|
+
with each CommandState update. Any exceptions raised in the callback will be
|
|
804
|
+
logged but won't stop the stream.
|
|
805
|
+
|
|
806
|
+
Example:
|
|
807
|
+
```python
|
|
808
|
+
def handle_command_state(cmd_state: CommandState):
|
|
809
|
+
print(f"Command joint positions: {cmd_state.joint_position.positions}")
|
|
810
|
+
|
|
811
|
+
client.command_state_stream_subscribe(
|
|
812
|
+
CommandStateStreamRequest(id="robot_1"),
|
|
813
|
+
on_data=handle_command_state
|
|
814
|
+
)
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
Note:
|
|
818
|
+
To stop the stream, call command_state_stream_unsubscribe().
|
|
819
|
+
Only one command state stream can be active at a time per client.
|
|
820
|
+
"""
|
|
821
|
+
if (self.command_state_stream_thread is None) or (not self.command_state_stream_thread.is_alive()):
|
|
822
|
+
self.command_state_stream_thread = threading.Thread(
|
|
823
|
+
target=lambda: self._command_state_stream_consume(stream_req, on_data),
|
|
824
|
+
daemon=True, # Daemon thread won't block program exit
|
|
825
|
+
)
|
|
826
|
+
self.command_state_stream_thread.start()
|
|
827
|
+
else:
|
|
828
|
+
logger.warning("Command state stream is already active. Subscription request ignored.")
|
|
829
|
+
|
|
830
|
+
def command_state_stream_unsubscribe(self):
|
|
831
|
+
"""
|
|
832
|
+
Unsubscribe from the command state stream.
|
|
833
|
+
|
|
834
|
+
This method stops the command state streaming thread by cancelling the gRPC iterator,
|
|
835
|
+
which causes the streaming loop to exit gracefully, then waits for the thread to terminate.
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
None
|
|
839
|
+
"""
|
|
840
|
+
if self.command_state_stream_thread:
|
|
841
|
+
if self.command_state_stream_thread.is_alive() and self.command_state_stream_iterator:
|
|
842
|
+
self.command_state_stream_iterator.cancel()
|
|
843
|
+
self.command_state_stream_thread.join()
|
|
844
|
+
if self.command_state_stream_thread.is_alive():
|
|
845
|
+
logger.warning("Command state stream thread did not exit cleanly within timeout")
|
|
846
|
+
self.command_state_stream_thread = None
|
|
847
|
+
self.command_state_stream_iterator = None
|
|
848
|
+
|
|
849
|
+
def inverse_kinematics_batch(self, ik_req: InverseKinematicsBatchRequest) -> InverseKinematicsBatchResponse:
|
|
850
|
+
"""
|
|
851
|
+
Calculate joint positions for a list of given Cartesian poses (Inverse Kinematics).
|
|
852
|
+
|
|
853
|
+
A seed joint position is used as a starting point for the IK solver.
|
|
854
|
+
The initial joint position is used for all target poses.
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
ik_req (InverseKinematicsBatchRequest): Request containing robot ID, seed
|
|
858
|
+
joint position in radians, and a list of target Cartesian poses.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
InverseKinematicsBatchResponse: Response containing computed a list of joint positions,
|
|
862
|
+
result code, and error message if computation failed. Each index of list corresponds
|
|
863
|
+
to the target poses in the request.
|
|
864
|
+
|
|
865
|
+
Note:
|
|
866
|
+
- Multiple solutions may exist; the returned solution will be closest
|
|
867
|
+
to the seed position
|
|
868
|
+
- May fail if target pose is unreachable, in singularity, causes
|
|
869
|
+
self-collision, or exceeds joint limits
|
|
870
|
+
"""
|
|
871
|
+
ik_res: InverseKinematicsBatchResponse = self.stub.InverseKinematicsBatch(ik_req)
|
|
872
|
+
return ik_res
|
|
873
|
+
|
|
874
|
+
def inverse_kinematics(self, ik_req: InverseKinematicsRequest) -> InverseKinematicsResponse:
|
|
875
|
+
"""
|
|
876
|
+
Calculate joint positions for a given Cartesian pose (Inverse Kinematics).
|
|
877
|
+
|
|
878
|
+
This method computes the joint configuration needed to achieve a specified
|
|
879
|
+
TCP pose. A seed joint position is used as a starting point for the IK solver.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
ik_req (InverseKinematicsRequest): Request containing robot ID, seed
|
|
883
|
+
joint position, target Cartesian pose, and optional joint_constraints
|
|
884
|
+
(list of JointRangeConstraints to limit specific joint angles).
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
InverseKinematicsResponse: Response containing computed joint positions,
|
|
888
|
+
result code, and error message if computation failed.
|
|
889
|
+
|
|
890
|
+
Note:
|
|
891
|
+
- Multiple solutions may exist; the returned solution will be closest
|
|
892
|
+
to the seed position
|
|
893
|
+
- May fail if target pose is unreachable, in singularity, causes
|
|
894
|
+
self-collision, or exceeds joint limits
|
|
895
|
+
- Joint constraints can be specified to limit specific joints to custom ranges
|
|
896
|
+
"""
|
|
897
|
+
ik_res: InverseKinematicsResponse = self.stub.InverseKinematics(ik_req)
|
|
898
|
+
if ik_res.result_code == KinResultCode.KIN_RESULT_CODE_SUCCESS:
|
|
899
|
+
logger.info(f"Inverse kinematics computed successfully for robot '{ik_req.id}'")
|
|
900
|
+
else:
|
|
901
|
+
logger.error(f"Inverse kinematics failed for robot '{ik_req.id}': {ik_res.error_message}")
|
|
902
|
+
return ik_res
|
|
903
|
+
|
|
904
|
+
def forward_kinematics(self, fk_req: ForwardKinematicsRequest) -> ForwardKinematicsResponse:
|
|
905
|
+
"""
|
|
906
|
+
Calculate Cartesian pose for given joint positions (Forward Kinematics).
|
|
907
|
+
|
|
908
|
+
This method computes the TCP pose that results from a specified joint
|
|
909
|
+
configuration. Useful for verifying joint positions and trajectory planning.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
fk_req (ForwardKinematicsRequest): Request containing robot ID and
|
|
913
|
+
joint positions (radians).
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
ForwardKinematicsResponse: Response containing computed Cartesian pose,
|
|
917
|
+
result code, and error message if computation failed.
|
|
918
|
+
|
|
919
|
+
Note:
|
|
920
|
+
Forward kinematics has a unique solution for any valid joint configuration.
|
|
921
|
+
May fail if joint positions are invalid or exceed limits.
|
|
922
|
+
"""
|
|
923
|
+
fk_res: ForwardKinematicsResponse = self.stub.ForwardKinematics(fk_req)
|
|
924
|
+
if fk_res.result_code == KinResultCode.KIN_RESULT_CODE_SUCCESS:
|
|
925
|
+
logger.info(f"Forward kinematics computed successfully for robot '{fk_req.id}'")
|
|
926
|
+
else:
|
|
927
|
+
logger.error(f"Forward kinematics failed for robot '{fk_req.id}': {fk_res.error_message}")
|
|
928
|
+
return fk_res
|
|
929
|
+
|
|
930
|
+
def set_tcp(self, set_tcp_req: SetTcpRequest) -> SetTcpResponse:
|
|
931
|
+
"""
|
|
932
|
+
Set the Tool Center Point (TCP) for a robot.
|
|
933
|
+
|
|
934
|
+
This method configures the TCP, which defines the position and orientation
|
|
935
|
+
of the tool attached to the robot's end effector. Proper TCP configuration
|
|
936
|
+
is essential for accurate motion control and task execution.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
set_tcp_req (SetTcpRequest): Request containing the robot ID and TCP frame
|
|
940
|
+
relative to the tool base.
|
|
941
|
+
Returns:
|
|
942
|
+
SetTcpResponse: Response indicating whether the TCP was set successfully
|
|
943
|
+
and result code.
|
|
944
|
+
"""
|
|
945
|
+
set_tcp_res: SetTcpResponse = self.stub.SetTcp(set_tcp_req)
|
|
946
|
+
if set_tcp_res.result == SetTcpResultCode.SET_TCP_RESULT_CODE_SUCCESS:
|
|
947
|
+
logger.info("Set TCP command executed successfully")
|
|
948
|
+
else:
|
|
949
|
+
logger.error(f"Set TCP command failed with result code {set_tcp_res.result}")
|
|
950
|
+
return set_tcp_res
|
|
951
|
+
|
|
952
|
+
def get_tcp(self, get_tcp_req: GetTcpRequest) -> GetTcpResponse:
|
|
953
|
+
"""
|
|
954
|
+
Get the Tool Center Point (TCP) configuration for a robot.
|
|
955
|
+
|
|
956
|
+
This method retrieves the current TCP configuration, which defines the
|
|
957
|
+
position and orientation of the tool attached to the robot's end effector
|
|
958
|
+
relative to the tool base frame.
|
|
959
|
+
|
|
960
|
+
Args:
|
|
961
|
+
get_tcp_req (GetTcpRequest): Request containing the robot ID for which
|
|
962
|
+
to retrieve the TCP configuration.
|
|
963
|
+
|
|
964
|
+
Returns:
|
|
965
|
+
GetTcpResponse: Response containing the TCP configuration including
|
|
966
|
+
the TCP ID and frame (position and orientation).
|
|
967
|
+
|
|
968
|
+
Raises:
|
|
969
|
+
grpc.RpcError: If there's a communication error with the VRMCS server
|
|
970
|
+
or if the robot is not found.
|
|
971
|
+
|
|
972
|
+
Note:
|
|
973
|
+
The returned frame represents the transformation from the tool base
|
|
974
|
+
to the TCP (tool center point).
|
|
975
|
+
"""
|
|
976
|
+
get_tcp_res: GetTcpResponse = self.stub.GetTcp(get_tcp_req)
|
|
977
|
+
logger.info(f"Retrieved TCP configuration for robot '{get_tcp_req.id}'")
|
|
978
|
+
return get_tcp_res
|
|
979
|
+
|
|
980
|
+
def get_joint_states(self, get_joint_states_req: GetJointStatesRequest) -> GetJointStatesResponse:
|
|
981
|
+
"""
|
|
982
|
+
Get the current joint states of a robot (one-time query).
|
|
983
|
+
|
|
984
|
+
This method retrieves a single snapshot of the robot's joint positions
|
|
985
|
+
at the time of the request. For continuous monitoring, use
|
|
986
|
+
joint_stream_subscribe instead.
|
|
987
|
+
|
|
988
|
+
Args:
|
|
989
|
+
get_joint_states_req (GetJointStatesRequest): Request containing the
|
|
990
|
+
robot ID.
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
GetJointStatesResponse: Response containing joint states with positions
|
|
994
|
+
and timestamp.
|
|
995
|
+
|
|
996
|
+
Note:
|
|
997
|
+
Use the streaming API for high-frequency monitoring.
|
|
998
|
+
"""
|
|
999
|
+
res: GetJointStatesResponse = self.stub.GetJointStates(get_joint_states_req)
|
|
1000
|
+
logger.info(f"Retrieved joint states for robot '{get_joint_states_req.id}'")
|
|
1001
|
+
return res
|
|
1002
|
+
|
|
1003
|
+
def get_position_state(self, get_position_state_req: GetPositionStateRequest) -> GetPositionStateResponse:
|
|
1004
|
+
"""
|
|
1005
|
+
Get the current position state of a robot (one-time query).
|
|
1006
|
+
|
|
1007
|
+
This method retrieves a single snapshot of the robot's joint and Cartesian
|
|
1008
|
+
positions at the time of the request. For continuous monitoring, use
|
|
1009
|
+
position_stream_subscribe instead.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
get_position_state_req (GetPositionStateRequest): Request containing
|
|
1013
|
+
the robot ID.
|
|
1014
|
+
|
|
1015
|
+
Returns:
|
|
1016
|
+
GetPositionStateResponse: Response containing position state with joint
|
|
1017
|
+
positions, Cartesian pose, and timestamp.
|
|
1018
|
+
|
|
1019
|
+
Note:
|
|
1020
|
+
Use the streaming API for high-frequency monitoring.
|
|
1021
|
+
"""
|
|
1022
|
+
res: GetPositionStateResponse = self.stub.GetPositionState(get_position_state_req)
|
|
1023
|
+
logger.info(f"Retrieved position state for robot '{get_position_state_req.id}'")
|
|
1024
|
+
return res
|
|
1025
|
+
|
|
1026
|
+
def get_robot_states(self, get_robot_states_req: GetRobotStatesRequest) -> GetRobotStatesResponse:
|
|
1027
|
+
"""
|
|
1028
|
+
Get the current robot states (one-time query).
|
|
1029
|
+
|
|
1030
|
+
This method retrieves a single snapshot of the robot's operational,
|
|
1031
|
+
safety, and connection states at the time of the request. For continuous
|
|
1032
|
+
monitoring, use states_stream_subscribe instead.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
get_robot_states_req (GetRobotStatesRequest): Request containing the
|
|
1036
|
+
robot ID.
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
GetRobotStatesResponse: Response containing robot states including
|
|
1040
|
+
operational state, safety state, connection state, and timestamp.
|
|
1041
|
+
|
|
1042
|
+
Note:
|
|
1043
|
+
Use the streaming API for high-frequency monitoring.
|
|
1044
|
+
"""
|
|
1045
|
+
res: GetRobotStatesResponse = self.stub.GetRobotStates(get_robot_states_req)
|
|
1046
|
+
logger.info(f"Retrieved robot states for robot '{get_robot_states_req.id}'")
|
|
1047
|
+
return res
|
|
1048
|
+
|
|
1049
|
+
def get_command_state(self, get_command_state_req: GetCommandStateRequest) -> GetCommandStateResponse:
|
|
1050
|
+
"""
|
|
1051
|
+
Get the current command state of a robot (one-time query).
|
|
1052
|
+
|
|
1053
|
+
This method retrieves a single snapshot of the robot's commanded joint
|
|
1054
|
+
and Cartesian positions, velocities, and accelerations at the time of
|
|
1055
|
+
the request. For continuous monitoring, use command_state_stream_subscribe
|
|
1056
|
+
instead.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
get_command_state_req (GetCommandStateRequest): Request containing the
|
|
1060
|
+
robot ID.
|
|
1061
|
+
|
|
1062
|
+
Returns:
|
|
1063
|
+
GetCommandStateResponse: Response containing command state with joint
|
|
1064
|
+
and Cartesian positions, velocities, accelerations, and timestamps.
|
|
1065
|
+
|
|
1066
|
+
Note:
|
|
1067
|
+
Use the streaming API for high-frequency monitoring of command states.
|
|
1068
|
+
"""
|
|
1069
|
+
res: GetCommandStateResponse = self.stub.GetCommandState(get_command_state_req)
|
|
1070
|
+
logger.info(f"Retrieved command state for robot '{get_command_state_req.id}'")
|
|
1071
|
+
return res
|
|
1072
|
+
|
|
1073
|
+
def get_feedback(self, get_feedback_req: GetFeedbackRequest) -> GetFeedbackResponse:
|
|
1074
|
+
"""
|
|
1075
|
+
Get the current robot feedback (one-time query).
|
|
1076
|
+
|
|
1077
|
+
This method retrieves a single snapshot of measured robot feedback,
|
|
1078
|
+
including measured joint positions, velocities, torques, and optional
|
|
1079
|
+
Cartesian wrench/velocity information when available.
|
|
1080
|
+
|
|
1081
|
+
Args:
|
|
1082
|
+
get_feedback_req (GetFeedbackRequest): Request containing the robot ID.
|
|
1083
|
+
|
|
1084
|
+
Returns:
|
|
1085
|
+
GetFeedbackResponse: Response containing a Feedback message.
|
|
1086
|
+
|
|
1087
|
+
Note:
|
|
1088
|
+
Use feedback_stream_subscribe for continuous feedback monitoring.
|
|
1089
|
+
"""
|
|
1090
|
+
res: GetFeedbackResponse = self.stub.GetFeedback(get_feedback_req)
|
|
1091
|
+
logger.info(f"Retrieved feedback for robot '{get_feedback_req.id}'")
|
|
1092
|
+
return res
|
|
1093
|
+
|
|
1094
|
+
def _feedback_stream_consume(
|
|
1095
|
+
self,
|
|
1096
|
+
stream_req: FeedbackStreamRequest,
|
|
1097
|
+
on_data: Callable[[Feedback], None],
|
|
1098
|
+
):
|
|
1099
|
+
self.feedback_stream_iterator = self.stub.FeedbackStream(stream_req)
|
|
1100
|
+
try:
|
|
1101
|
+
for response in self.feedback_stream_iterator:
|
|
1102
|
+
result: Feedback = response
|
|
1103
|
+
try:
|
|
1104
|
+
on_data(result)
|
|
1105
|
+
except Exception as e:
|
|
1106
|
+
logger.error(f"Error in feedback callback: {e}")
|
|
1107
|
+
except grpc.RpcError as e:
|
|
1108
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
1109
|
+
logger.info("Feedback stream cancelled successfully.")
|
|
1110
|
+
else:
|
|
1111
|
+
logger.error(f"gRPC error during FeedbackStream: {e.code()} - {e.details()}")
|
|
1112
|
+
|
|
1113
|
+
def feedback_stream_subscribe(
|
|
1114
|
+
self,
|
|
1115
|
+
stream_req: FeedbackStreamRequest,
|
|
1116
|
+
on_data: Callable[[Feedback], None],
|
|
1117
|
+
):
|
|
1118
|
+
"""
|
|
1119
|
+
Subscribe to a stream of measured robot feedback.
|
|
1120
|
+
|
|
1121
|
+
This method starts a background thread that consumes measured feedback
|
|
1122
|
+
data from the server and calls the provided callback function for each
|
|
1123
|
+
update.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
stream_req (FeedbackStreamRequest): Configuration for the feedback
|
|
1127
|
+
stream including robot ID and optional max_frequency_hz
|
|
1128
|
+
(0 = no limit).
|
|
1129
|
+
on_data (Callable[[Feedback], None]): Callback function that will be
|
|
1130
|
+
called with each Feedback update. Any exceptions raised in the
|
|
1131
|
+
callback will be logged but won't stop the stream.
|
|
1132
|
+
|
|
1133
|
+
Example:
|
|
1134
|
+
```python
|
|
1135
|
+
def handle_feedback(feedback: Feedback):
|
|
1136
|
+
print(f"Measured joints: {feedback.joint_position}")
|
|
1137
|
+
|
|
1138
|
+
client.feedback_stream_subscribe(
|
|
1139
|
+
FeedbackStreamRequest(id="robot_1"),
|
|
1140
|
+
on_data=handle_feedback,
|
|
1141
|
+
)
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
Note:
|
|
1145
|
+
To stop the stream, call feedback_stream_unsubscribe().
|
|
1146
|
+
Only one feedback stream can be active at a time per client.
|
|
1147
|
+
"""
|
|
1148
|
+
if (self.feedback_stream_thread is None) or (not self.feedback_stream_thread.is_alive()):
|
|
1149
|
+
self.feedback_stream_thread = threading.Thread(
|
|
1150
|
+
target=lambda: self._feedback_stream_consume(stream_req, on_data),
|
|
1151
|
+
daemon=True, # Daemon thread won't block program exit
|
|
1152
|
+
)
|
|
1153
|
+
self.feedback_stream_thread.start()
|
|
1154
|
+
else:
|
|
1155
|
+
logger.warning("Feedback stream is already active. Subscription request ignored.")
|
|
1156
|
+
|
|
1157
|
+
def feedback_stream_unsubscribe(self):
|
|
1158
|
+
"""
|
|
1159
|
+
Unsubscribe from the feedback stream.
|
|
1160
|
+
|
|
1161
|
+
This method stops the feedback streaming thread by cancelling the gRPC
|
|
1162
|
+
iterator, which causes the streaming loop to exit gracefully, then
|
|
1163
|
+
waits for the thread to terminate.
|
|
1164
|
+
|
|
1165
|
+
Returns:
|
|
1166
|
+
None
|
|
1167
|
+
"""
|
|
1168
|
+
if self.feedback_stream_thread:
|
|
1169
|
+
if self.feedback_stream_thread.is_alive() and self.feedback_stream_iterator:
|
|
1170
|
+
self.feedback_stream_iterator.cancel()
|
|
1171
|
+
self.feedback_stream_thread.join()
|
|
1172
|
+
if self.feedback_stream_thread.is_alive():
|
|
1173
|
+
logger.warning("Feedback stream thread did not exit cleanly within timeout")
|
|
1174
|
+
self.feedback_stream_thread = None
|
|
1175
|
+
self.feedback_stream_iterator = None
|
|
1176
|
+
|
|
1177
|
+
def _robot_alarm_stream_consume(
|
|
1178
|
+
self,
|
|
1179
|
+
stream_req: RobotAlarmStreamRequest,
|
|
1180
|
+
on_data: Callable[[RobotAlarm], None],
|
|
1181
|
+
):
|
|
1182
|
+
self.robot_alarm_stream_iterator = self.stub.RobotAlarmStream(stream_req)
|
|
1183
|
+
try:
|
|
1184
|
+
for response in self.robot_alarm_stream_iterator:
|
|
1185
|
+
result: RobotAlarm = response
|
|
1186
|
+
try:
|
|
1187
|
+
on_data(result)
|
|
1188
|
+
except Exception as e:
|
|
1189
|
+
logger.error(f"Error in robot alarm callback: {e}")
|
|
1190
|
+
except grpc.RpcError as e:
|
|
1191
|
+
if e.code() == grpc.StatusCode.CANCELLED:
|
|
1192
|
+
logger.info("Robot alarm stream cancelled successfully.")
|
|
1193
|
+
else:
|
|
1194
|
+
logger.error(f"gRPC error during RobotAlarmStream: {e.code()} - {e.details()}")
|
|
1195
|
+
|
|
1196
|
+
def robot_alarm_stream_subscribe(
|
|
1197
|
+
self,
|
|
1198
|
+
stream_req: RobotAlarmStreamRequest,
|
|
1199
|
+
on_data: Callable[[RobotAlarm], None],
|
|
1200
|
+
):
|
|
1201
|
+
"""
|
|
1202
|
+
Subscribe to a stream of robot alarms and notifications.
|
|
1203
|
+
|
|
1204
|
+
This method starts a background thread that consumes robot alarm data from the server
|
|
1205
|
+
and calls the provided callback function for each alarm update. Alarms include
|
|
1206
|
+
notifications from the motion controller, robot driver, and actuator driver.
|
|
1207
|
+
|
|
1208
|
+
Args:
|
|
1209
|
+
stream_req (RobotAlarmStreamRequest): Configuration for the robot alarm stream
|
|
1210
|
+
including robot ID.
|
|
1211
|
+
on_data (Callable[[RobotAlarm], None]): Callback function that will be called
|
|
1212
|
+
with each RobotAlarm update containing a list of Notification objects.
|
|
1213
|
+
Any exceptions raised in the callback will be logged but won't stop the stream.
|
|
1214
|
+
|
|
1215
|
+
Example:
|
|
1216
|
+
```python
|
|
1217
|
+
def handle_alarm(alarm: RobotAlarm):
|
|
1218
|
+
for notification in alarm.notifications:
|
|
1219
|
+
print(f"[{SeverityLevel.Name(notification.severity)}] {notification.message}")
|
|
1220
|
+
|
|
1221
|
+
client.robot_alarm_stream_subscribe(
|
|
1222
|
+
RobotAlarmStreamRequest(id="robot_1"),
|
|
1223
|
+
on_data=handle_alarm
|
|
1224
|
+
)
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
Note:
|
|
1228
|
+
To stop the stream, call robot_alarm_stream_unsubscribe().
|
|
1229
|
+
Only one robot alarm stream can be active at a time per client.
|
|
1230
|
+
"""
|
|
1231
|
+
if (self.robot_alarm_stream_thread is None) or (not self.robot_alarm_stream_thread.is_alive()):
|
|
1232
|
+
self.robot_alarm_stream_thread = threading.Thread(
|
|
1233
|
+
target=lambda: self._robot_alarm_stream_consume(stream_req, on_data),
|
|
1234
|
+
daemon=True, # Daemon thread won't block program exit
|
|
1235
|
+
)
|
|
1236
|
+
self.robot_alarm_stream_thread.start()
|
|
1237
|
+
else:
|
|
1238
|
+
logger.warning("Robot alarm stream is already active. Subscription request ignored.")
|
|
1239
|
+
|
|
1240
|
+
def robot_alarm_stream_unsubscribe(self):
|
|
1241
|
+
"""
|
|
1242
|
+
Unsubscribe from the robot alarm stream.
|
|
1243
|
+
|
|
1244
|
+
This method stops the robot alarm streaming thread by cancelling the gRPC iterator,
|
|
1245
|
+
which causes the streaming loop to exit gracefully, then waits for the thread to terminate.
|
|
1246
|
+
|
|
1247
|
+
Returns:
|
|
1248
|
+
None
|
|
1249
|
+
"""
|
|
1250
|
+
if self.robot_alarm_stream_thread:
|
|
1251
|
+
if self.robot_alarm_stream_thread.is_alive() and self.robot_alarm_stream_iterator:
|
|
1252
|
+
self.robot_alarm_stream_iterator.cancel()
|
|
1253
|
+
self.robot_alarm_stream_thread.join()
|
|
1254
|
+
if self.robot_alarm_stream_thread.is_alive():
|
|
1255
|
+
logger.warning("Robot alarm stream thread did not exit cleanly within timeout")
|
|
1256
|
+
self.robot_alarm_stream_thread = None
|
|
1257
|
+
self.robot_alarm_stream_iterator = None
|